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