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