圖解:進程怎么綁定 CPU
昨天在群里有朋友問:把進程綁定到某個 CPU 上運行是怎么實現(xiàn)的。
首先,我們先來了解下將進程與 CPU 進行綁定的好處。
進程綁定 CPU 的好處:在多核 CPU 結(jié)構(gòu)中,每個核心有各自的L1、L2緩存,而L3緩存是共用的。如果一個進程在核心間來回切換,各個核心的緩存命中率就會受到影響。相反如果進程不管如何調(diào)度,都始終可以在一個核心上執(zhí)行,那么其數(shù)據(jù)的L1、L2 緩存的命中率可以顯著提高。
所以,將進程與 CPU 進行綁定可以提高 CPU 緩存的命中率,從而提高性能。而進程與 CPU 綁定被稱為:CPU 親和性。
設(shè)置進程的 CPU 親和性
前面介紹了進程與 CPU 綁定的好處后,現(xiàn)在來介紹一下在 Linux 系統(tǒng)下怎么將進程與 CPU 進行綁定的(也就是設(shè)置進程的 CPU 親和性)。
Linux 系統(tǒng)提供了一個名為 sched_setaffinity 的系統(tǒng)調(diào)用,此系統(tǒng)調(diào)用可以設(shè)置進程的 CPU 親和性。我們來看看 sched_setaffinity 系統(tǒng)調(diào)用的原型:
- int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);
下面介紹一下 sched_setaffinity 系統(tǒng)調(diào)用各個參數(shù)的作用:
- pid:進程ID,也就是要進行綁定 CPU 的進程ID。
- cpusetsize:mask 參數(shù)所指向的 CPU 集合的大小。
- mask:與進程進行綁定的 CPU 集合(由于一個進程可以綁定到多個 CPU 上運行)。
參數(shù) mask 的類型為 cpu_set_t,而 cpu_set_t 是一個位圖,位圖的每個位表示一個 CPU,如下圖所示:
例如,將 cpu_set_t 的第0位設(shè)置為1,表示將進程綁定到 CPU0 上運行,當(dāng)然我們可以將進程綁定到多個 CPU 上運行。
我們通過一個例子來介紹怎么通過 sched_setaffinity 系統(tǒng)調(diào)用來設(shè)置進程的 CPU 親和性:
- #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)
- {
- cpu_set_t cpuset;
- CPU_ZERO(&cpuset); // 初始化CPU集合,將 cpuset 置為空
- CPU_SET(2, &cpuset); // 將本進程綁定到 CPU2 上
- // 設(shè)置進程的 CPU 親和性
- if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) {
- printf("Set CPU affinity failed, error: %s\n", strerror(errno));
- return -1;
- }
- return 0;
- }
CPU 親和性實現(xiàn)
知道怎么設(shè)置進程的 CPU 親和性后,現(xiàn)在我們來分析一下 Linux 內(nèi)核是怎樣實現(xiàn) CPU 親和性功能的。
本文使用的 Linux 內(nèi)核版本為 2.6.23
Linux 內(nèi)核為每個 CPU 定義了一個類型為 struct rq 的 可運行的進程隊列,也就是說,每個 CPU 都擁有一個獨立的可運行進程隊列。
一般來說,CPU 只會從屬于自己的可運行進程隊列中選擇一個進程來運行。也就是說,CPU0 只會從屬于 CPU0 的可運行隊列中選擇一個進程來運行,而絕不會從 CPU1 的可運行隊列中獲取。
所以,從上面的信息中可以分析出,要將進程綁定到某個 CPU 上運行,只需要將進程放置到其所屬的 可運行進程隊列 中即可。
下面我們來分析一下 sched_setaffinity 系統(tǒng)調(diào)用的實現(xiàn),sched_setaffinity 系統(tǒng)調(diào)用的調(diào)用鏈如下:
- sys_sched_setaffinity()
- └→ sched_setaffinity()
- └→ set_cpus_allowed()
- └→ migrate_task()
從上面的調(diào)用鏈可以看出,sched_setaffinity 系統(tǒng)調(diào)用最終會調(diào)用 migrate_task 函數(shù)來完成進程與 CPU 進行綁定的工作,我們來分析一下 migrate_task 函數(shù)的實現(xiàn):
- static int
- migrate_task(struct task_struct *p, int dest_cpu, struct migration_req *req)
- {
- struct rq *rq = task_rq(p);
- // 情況1:
- // 如果進程還沒有在任何運行隊列中
- // 那么只需要將進程的 cpu 字段設(shè)置為 dest_cpu 即可
- if (!p->se.on_rq && !task_running(rq, p)) {
- set_task_cpu(p, dest_cpu);
- return 0;
- }
- // 情況2:
- // 如果進程已經(jīng)在某一個 CPU 的可運行隊列中
- // 那么需要將進程從之前的 CPU 可運行隊列中遷移到新的 CPU 可運行隊列中
- // 這個遷移過程由 migration_thread 內(nèi)核線程完成
- // 構(gòu)建進程遷移請求
- init_completion(&req->done);
- req->task = p;
- req->dest_cpudest_cpu = dest_cpu;
- list_add(&req->list, &rq->migration_queue);
- return 1;
- }
我們先來介紹一下 migrate_task 函數(shù)各個參數(shù)的意義:
- p:要設(shè)置 CPU 親和性的進程描述符。
- dest_cpu:綁定的 CPU 編號。
- req:進程遷移請求對象(下面會介紹)。
所以,migrate_task 函數(shù)的作用就是將進程描述符為 p 的進程綁定到編號為 dest_cpu 的目標(biāo) CPU 上。
migrate_task 函數(shù)主要分兩種情況來將進程綁定到某個 CPU 上:
- 情況1:如果進程還沒有在任何 CPU 的可運行隊列中(不可運行狀態(tài)),那么只需要將進程描述符的 cpu 字段設(shè)置為 dest_cpu 即可。當(dāng)進程變?yōu)榭蛇\行時,會根據(jù)進程描述符的 cpu 字段來自動放置到對應(yīng)的 CPU 可運行隊列中。
- 情況2:如果進程已經(jīng)在某個 CPU 的可運行隊列中,那么需要將進程從之前的 CPU 可運行隊列中遷移到新的 CPU 可運行隊列中。遷移過程由 migration_thread 內(nèi)核線程完成,migrate_task 函數(shù)只是構(gòu)建一個進程遷移請求,并通知 migration_thread 內(nèi)核線程有新的遷移請求需要處理。
而進程遷移過程由 __migrate_task 函數(shù)完成,我們來看看 __migrate_task 函數(shù)的實現(xiàn):
- static int
- __migrate_task(struct task_struct *p, int src_cpu, int dest_cpu)
- {
- struct rq *rq_dest, *rq_src;
- int ret = 0, on_rq;
- ...
- rq_src = cpu_rq(src_cpu); // 進程所在的原可運行隊列
- rq_dest = cpu_rq(dest_cpu); // 進程希望放置的目標(biāo)可運行隊列
- ...
- on_rq = p->se.on_rq; // 進程是否在可運行隊列中(可運行狀態(tài))
- if (on_rq)
- deactivate_task(rq_src, p, 0); // 把進程從原來的可運行隊列中刪除
- set_task_cpu(p, dest_cpu);
- if (on_rq) {
- activate_task(rq_dest, p, 0); // 把進程放置到目標(biāo)可運行隊列中
- ...
- }
- ...
- return ret;
- }
__migrate_task 函數(shù)主要完成以下兩個工作:
- 把進程從原來的可運行隊列中刪除。
- 把進程放置到目標(biāo)可運行隊列中。
其工作過程如下圖所示(將進程從 CPU0 的可運行隊列遷移到 CPU3 的可運行隊列中):
如上圖所示,進程原本在 CPU0 的可運行隊列中,但由于重新將進程綁定到 CPU3,所以需要將進程從 CPU0 的可運行隊列遷移到 CPU3 的可運行中。
遷移過程首先將進程從 CPU0 的可運行隊列中刪除,然后再將進程插入到 CPU3 的可運行隊列中。
當(dāng) CPU 要運行進程時,首先從它所屬的可運行隊列中挑選一個進程,并將此進程調(diào)度到 CPU 中運行。
總結(jié)
從上面的分析可知,其實將進程綁定到某個 CPU 只是將進程放置到 CPU 的可運行隊列中。
由于每個 CPU 都有一個可運行隊列,所以就有可能會出現(xiàn) CPU 間可運行隊列負載不均衡問題。如 CPU0 可運行隊列中的進程比 CPU1 可運行隊列多非常多,從而導(dǎo)致 CPU0 的負載非常高,而 CPU1 負載非常低的情況。
當(dāng)出現(xiàn)上述情況時,就需要對 CPU 間的可運行隊列進行重平衡操作,有興趣的可以自行閱讀源碼或參考相關(guān)資料。