程序怎麼繫結CPU

把程序繫結到某個 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(0sizeof(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 間的可執行佇列進行重平衡操作,有興趣的可以自行閱讀原始碼或參考相關資料。

相關文章