Linux 進程管理之任務綁定
本文轉載自微信公眾號「人人都是極客」,作者布道師Peter 。轉載本文請聯系人人都是極客公眾號。
什么是進程的 CPU 親和性?
在多核結構中,每個核有各自的L1緩存,相同類型的核被劃分在同一個cluster中,而不同cluster之間又有共用的L2緩存。講負載均衡的時候我們講過一個進程在核之間來回切換的時候,各個核之間的緩存命中率會降低,所以,將進程與 CPU 進行綁定可以提高 CPU 緩存的命中率,從而提高性能。這種綁定關系就叫做:進程的 CPU 親和性。
如何設置進程的 CPU 親和性?
Linux 系統提供了一個名為 sched_setaffinity 的系統調用,此系統調用可以設置進程的 CPU 親和性。
- sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask)
- pid:進行綁定 CPU 的進程ID號
- cpusetsize:參數 mask 指向的 CPU 集合的大小
- mask:與進程綁定的 CPU 集合
cpu_set_t 類型是個位圖,可以理解為 CPU 集,通過宏來進行清除、設置以及判斷:
- //初始化,設為空
- void CPU_ZERO (cpu_set_t *set);
- //將某個cpu加入cpu集中
- void CPU_SET (int cpu, cpu_set_t *set);
- //將某個cpu從cpu集中移出
- void CPU_CLR (int cpu, cpu_set_t *set);
- //判斷某個cpu是否已在cpu集中設置了
- int CPU_ISSET (int cpu, const cpu_set_t *set);
CPU 集可以認為是一個掩碼,每個設置的位都對應一個可以合法調度的 CPU,而未設置的位則對應一個不可調度的 CPU。換言之,線程都被綁定了,只能在那些對應位被設置了的處理器上運行。通常,掩碼中的所有位都被置位了,也就是可以在所有的 CPU 中調度。
我們來看看 sched_setaffinity 系統調用的例子,將進程綁定到 CPU2 上運行:
- #define _GNU_SOURCE
- #include <sched.h>
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <errno.h>
- int main(int argc, char **argv)
- {
- int cpus = 0;
- int i = 0;
- cpu_set_t mask;
- cpu_set_t get;
- cpus = sysconf(_SC_NPROCESSORS_ONLN);
- printf("cpus: %d\n", cpus);
- CPU_ZERO(&mask); /* 初始化set集,將set置為空*/
- CPU_SET(2, &mask); /*將本進程綁定到CPU2上*/
- if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
- printf("Set CPU affinity failue, ERROR:%s\n", strerror(errno));
- return -1;
- }
- return 0;
- }
CPU 親和性的實現
我們知道每個 CPU 都擁有一個獨立的可運行進程隊列,系統運行的時候 CPU 只會從屬于自己的可運行進程隊列中按照 CFS 策略,選擇一個進程來運行。所以,把進程放置在 CPU 對應的可運行進程隊列上,也就可將進程綁定到指定的 CPU 上。
下面我們追蹤函數 sched_setaffinity 的調用順序,分析一下進程如何與 CPU 進行綁定的。
- SYSCALL_DEFINE3(sched_setaffinity, pid_t, pid, unsigned int, len, unsigned long __user *, user_mask_ptr)
- -- sched_setaffinity(pid_t pid, const struct cpumask *in_mask)
- --- __set_cpus_allowed_ptr(struct task_struct *p, const struct cpumask *new_mask, bool check)
- ---- stop_one_cpu(unsigned int cpu, cpu_stop_fn_t fn, void *arg)
- ----- migration_cpu_stop(void *data)
- ------ __migrate_task(struct rq *rq, struct task_struct *p, int dest_cpu)
- ------- move_queued_task(struct rq *rq, struct task_struct *p, int new_cpu)
- -------- enqueue_task(struct rq *rq, struct task_struct *p, int flags)
- --------- returns the new run queue of destination CPU
__set_cpus_allowed_ptr 函數主要分兩種情況來將進程綁定到某個 CPU 上:
- stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg):把還沒運行且在源運行隊列中進程,放到指定的 CPU 可運行隊列中
- move_queued_task(rq, &rf, p, dest_cpu):把已經運行的進程遷移到指定的 CPU 可運行隊列中
這兩種情況最終都會調用 move_queued_task:
- static struct rq *move_queued_task(struct rq *rq, struct rq_flags *rf,
- struct task_struct *p, int new_cpu)
- {
- lockdep_assert_held(&rq->lock);
- p->on_rq = TASK_ON_RQ_MIGRATING;
- dequeue_task(rq, p, DEQUEUE_NOCLOCK);
- set_task_cpu(p, new_cpu);
- rq_unlock(rq, rf);
- rq = cpu_rq(new_cpu);
- rq_lock(rq, rf);
- BUG_ON(task_cpu(p) != new_cpu);
- enqueue_task(rq, p, 0);
- p->on_rq = TASK_ON_RQ_QUEUED;
- check_preempt_curr(rq, p, 0);
- return rq;
- }
這里首先根據目標 CPU 找到對應的工作隊列 rq,然后通過 enqueue_task 把任務遷移到目標 CPU 對應的工作隊列中,CFS 調度器的話會調用到函數 enqueue_task_fair。
enqueue_task_fair 的執行流程如下:
- 如果通過struct sched_entity 的 on_rq 成員判斷進程已經在就緒隊列上, 則無事可。
否則, 具體的工作委托給 enqueue_entity,將任務插入到 CFS 紅黑樹中合適的結點。