把程序繫結到某個 CPU 上執行是怎麼實現的?
首先,我們先來了解下將程序與 CPU 進行繫結的好處。
程序繫結 CPU 的好處:在多核 CPU 結構中,每個核心有各自的L1、L2快取,而L3快取是共用的。如果一個程序在核心間來回切換,各個核心的快取命中率就會受到影響。相反如果程序不管如何排程,都始終可以在一個核心上執行,那麼其資料的L1、L2 快取的命中率可以顯著提高。
所以,將程序與 CPU 進行繫結可以提高 CPU 快取的命中率,從而提高效能。而程序與 CPU 繫結被稱為:
CPU 親和性
。設定程序的 CPU 親和性
前面介紹了程序與 CPU 繫結的好處後,現在來介紹一下在 Linux 系統下怎麼將程序與 CPU 進行繫結的(也就是設定程序的 CPU 親和性)。
Linux 系統提供了一個名為
sched_setaffinity
的系統呼叫,此係統呼叫可以設定程序的 CPU 親和性。我們來看看 sched_setaffinity
系統呼叫的原型:intsched_setaffinity(pid_t pid, size_t cpusetsize, constcpu_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>intmain(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\n", strerror(errno));return-1; }return0;}
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
函式的實現:staticintmigrate_task(struct task_struct *p, int dest_cpu, struct migration_req *req){structrq *rq = task_rq(p);// 情況1:// 如果程序還沒有在任何執行佇列中// 那麼只需要將程序的 cpu 欄位設定為 dest_cpu 即可if (!p->se.on_rq && !task_running(rq, p)) { set_task_cpu(p, dest_cpu);return0; }// 情況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);return1;}
我們先來介紹一下
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
函式的實現:staticint__migrate_task(struct task_struct *p, int src_cpu, int dest_cpu){structrq *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 間的可執行佇列進行重平衡操作,有興趣的可以自行閱讀原始碼或參考相關資料。