1. 程式人生 > 其它 >一文讀懂 程序怎麼繫結 CPU

一文讀懂 程序怎麼繫結 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 系統呼叫的原型:

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\n", 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 間的可執行佇列進行重平衡操作,有興趣的可以自行閱讀原始碼或參考相關資料。