昨天在群里有朋友問:把進程綁定到某個 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 《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 上
// 設置進程的 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 間的可運行隊列進行重平衡操作,有興趣的可以自行閱讀源碼或參考相關資料。
編輯:jq
-
內核
+關注
關注
3文章
1372瀏覽量
40293 -
cpu
+關注
關注
68文章
10863瀏覽量
211786 -
Linux
+關注
關注
87文章
11304瀏覽量
209525
原文標題:圖解:進程怎么綁定 CPU
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論