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