前言
在淘寶開源自己基于nginx打造的tegine伺服器的時候,有這麼一項特性引起了筆者的興趣。“自動根據CPU數目設定程序個數和綁定CPU親緣性”。當時筆者對CPU親緣性沒有任何概念,當時作者隻是下意識的打開了google并輸入CPU親緣性(CPU Affinity)簡單了做了個了解。
後來,在筆者參加實際工作以後,就碰到了這麼兩個問題。
問題一:如何在SMP的系統中,保證某個特定程序即使在其他程序都很忙的情況下都能夠獲得足夠的CPU資源?解決的思路主要有以下兩種:
- 提高程序的處理優先級
- 從SMP系統中,專門劃撥出某一個CPU用于運作該程式。 而将其他程序劃撥到其他的CPU上進行運作。
問題二:通過每日監控資料,我們發現伺服器的CPU使用率出現這樣子的情況,除了CPU0,其他CPU的負載都很低。
我們選擇了通過設定CPU親緣性的方式進行優化,在完成相關優化後,我們的應用程式性能得到了一定的提高。(大緻有10%的性能提升)
此次,筆者借着博文的機會将“CPU親緣性”這一特性的學習過程整理下來,以備日後查驗。注意,本文所提到的CPU親緣性均基于Linux。
什麼是CPU親緣性
所謂CPU親緣性可以分為兩大類:軟親緣性和硬親緣性。
Linux 核心程序排程器天生就具有被稱為 CPU 軟親緣性(soft affinity) 的特性,這意味着程序通常不會在處理器之間頻繁遷移。這種狀态正是我們希望的,因為程序遷移的頻率小就意味着産生的負載小。但不代表不會進行小範圍的遷移。
CPU 硬親緣性是指通過Linux提供的相關CPU親緣性設定接口,顯示的指定某個程序固定的某個處理器上運作。本文所提到的CPU親緣性主要是指硬親緣性。
使用CPU親緣性的好處
目前主流的伺服器配置都是SMP架構,在SMP的環境下,每個CPU本身自己會有緩存,緩存着程序使用的資訊,而程序可能會被kernel排程到其他CPU上(即所謂的core migration),如此,CPU cache命中率就低了。設定CPU親緣性,程式就會一直在指定的cpu運作,防止程序在多SMP的環境下的core migration,進而避免因切換帶來的CPU的L1/L2 cache失效。進而進一步提高應用程式的性能。
Linux CPU親緣性的使用
我們有兩種辦法指定程式運作的CPU親緣性。
- 通過Linux提供的taskset工具指定程序運作的CPU。
- 方式二,glibc本身也為我們提供了這樣的接口,借來的内容主要為大家講解如何通過程式設計的方式設定程序的CPU親緣性。
相關接口
利用glibc庫中的sched_getaffinity接口,我們擷取應用程式目前的cpu親緣性,而通過sched_setaffinity接口則可以把應用程式綁定到固定的某個或某幾cpu上運作。相關定義如下:

1 #include <sched.h>
2
3
4 void CPU_ZERO(cpu_set_t *set);
5 void CPU_CLR(int cpu, cpu_set_t *set);
6 void CPU_SET(int cpu, cpu_set_t *set);
7 int CPU_ISSET(int cpu, cpu_set_t *set);
8
9 int sched_getaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask);
10
11 int sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask);
View Code
其中的cpu_set_t結構體的具體定義:

1 /*/usr/include/bits/sched.h*/
2
3
4 # define __CPU_SETSIZE 1024
5 # define __NCPUBITS (8 * sizeof (__cpu_mask))
6
7 /* Type for array elements in 'cpu_set'. */
8 typedef unsigned long int __cpu_mask;
9
10 typedef struct
11 {
12 __cpu_mask __bits[__CPU_SETSIZE / __NCPUBITS];
13 } cpu_set_t;
可以看到其用每一bit位表示一個cpu的狀态,最多可以表示1024個cpu的親緣狀态,這在目前來說足夠用了.
在 Linux 核心中,所有的程序都有一個相關的資料結構,稱為
task_struct
。這個結構非常重要,原因有很多;其中與 親緣性(affinity)相關度最高的是
cpus_allowed
位掩碼。這個位掩碼由 n 位組成,與系統中的 n 個邏輯處理器一一對應。 具有 4 個實體 CPU 的系統可以有 4 位。如果這些 CPU 都啟用了超線程,那麼這個系統就有一個 8 位的位掩碼。
如果為給定的程序設定了給定的位,那麼這個程序就可以在相關的 CPU 上運作。是以,如果一個程序可以在任何 CPU 上運作,并且能夠根據需要在處理器之間進行遷移,那麼位掩碼就全是 1。實際上,這就是 Linux 中程序的預設狀态。相關核心排程代碼如下:

1 static inline
2 int select_task_rq(struct task_struct *p, int sd_flags, int wake_flags)
3 {
4 int cpu = p->sched_class->select_task_rq(p, sd_flags, wake_flags);
5
6 /*
7 * In order not to call set_task_cpu() on a blocking task we need
8 * to rely on ttwu() to place the task on a valid ->cpus_allowed
9 * cpu.
10 *
11 * Since this is common to all placement strategies, this lives here.
12 *
13 * [ this allows ->select_task() to simply return task_cpu(p) and
14 * not worry about this generic constraint ]
15 */
16 if (unlikely(!cpumask_test_cpu(cpu, &p->cpus_allowed) ||
17 !cpu_online(cpu)))
18 cpu = select_fallback_rq(task_cpu(p), p);
19
20 return cpu;
21 }
另外的幾個宏CPU_CLR\CPU_ISSET\CPU_SET\CPU_ZERO定義也都定義在頭檔案/usr/include/bits/sched.h内:

1 /* Access functions for CPU masks. */
2 # define __CPU_ZERO(cpusetp) \
3 do { \
4 unsigned int __i; \
5 cpu_set_t *__arr = (cpusetp); \
6 for (__i = 0; __i < sizeof (cpu_set_t) / sizeof (__cpu_mask); ++__i) \
7 __arr->__bits[__i] = 0; \
8 } while (0)
9 # define __CPU_SET(cpu, cpusetp) \
10 ((cpusetp)->__bits[__CPUELT (cpu)] |= __CPUMASK (cpu))
11 # define __CPU_CLR(cpu, cpusetp) \
12 ((cpusetp)->__bits[__CPUELT (cpu)] &= ~__CPUMASK (cpu))
13 # define __CPU_ISSET(cpu, cpusetp) \
14 (((cpusetp)->__bits[__CPUELT (cpu)] & __CPUMASK (cpu)) != 0)
15 #endif
利用這幾個宏友善我們操作指定cpu的對應bit位,比如清零,置位等。看一個完整的demo程式:

1 /**
2 * FileName: affinity_demo.c
3 */
4
5 #define _GNU_SOURCE
6
7 #include <stdint.h>
8 #include <stdio.h>
9 #include <sched.h>
10 #include <pthread.h>
11 #include <stdlib.h>
12
13
14 static inline void print_cpu_mask(cpu_set_t cpu_mask)
15 {
16 unsigned char flag = 0;
17 printf("Cpu affinity is ");
18 for (unsigned int i = 0; i < sizeof(cpu_set_t); i ++)
19 {
20 if (CPU_ISSET(i, &cpu_mask))
21 {
22 if (flag == 0)
23 {
24 flag = 1;
25 printf("%d", i);
26 }
27 else
28 {
29 printf(",%d", i);
30 }
31 }
32 }
33 printf(".\n");
34 }
35
36 static inline void get_cpu_mask(pid_t pid, cpu_set_t *mask)
37 {
38 if (sched_getaffinity(pid, sizeof(cpu_set_t), mask) == -1)
39 {
40 perror("get cpu affinity failed.\n");
41 abort();
42 }
43 }
44
45 static inline void set_cpu_mask(pid_t pid, cpu_set_t *mask)
46 {
47 if (sched_setaffinity(pid, sizeof(cpu_set_t), mask) == -1)
48 {
49 perror("set cpu affinity failed.\n");
50 abort();
51 }
52 }
53
54 int main(int argc, char *argv[])
55 {
56 unsigned int active_cpu = 0;
57 cpu_set_t cpu_mask;
58
59 get_cpu_mask(0, &cpu_mask);
60 print_cpu_mask(cpu_mask);
61
62 CPU_ZERO(&cpu_mask);
63 CPU_SET(active_cpu, &cpu_mask);
64 set_cpu_mask(0, &cpu_mask);
65
66 get_cpu_mask(0, &cpu_mask);
67 print_cpu_mask(cpu_mask);
68
69 for(;;)
70 {
71 ;
72 }
73 return 0;
74 }
編譯,并運作
gcc affinity_demo.c -o demo -std=c99
程式卡死在死循環,讓我們另開一個終端來看看目前系統cpu使用率:
mpstat -P ALL 1
0号cpu占用率為百分之百,而其它cpu基本完全空閑。我們再來試試把活動cpu設定為1的情況, 我們将上面程式的第56行修改為:
unsigned int active_cpu = 1;
編譯并運作,同時觀察一下此時我們的系統CPU使用率發生了什麼變化:
值得注意的是,cpu affinity會被傳遞給子線程。

1 /**
2 * FileName: affinity_demo.c
3 */
4 #define _GNU_SOURCE
5
6 #include <stdint.h>
7 #include <stdio.h>
8 #include <sched.h>
9 #include <pthread.h>
10 #include <stdlib.h>
11
12 static inline void print_cpu_mask(cpu_set_t cpu_mask)
13 {
14 unsigned char flag = 0;
15 printf("Cpu affinity is ");
16 for (unsigned int i = 0; i < sizeof(cpu_set_t); i ++)
17 {
18 if (CPU_ISSET(i, &cpu_mask))
19 {
20 if (flag == 0)
21 {
22 flag = 1;
23 printf("%d", i);
24 }
25 else
26 {
27 printf(",%d", i);
28 }
29 }
30 }
31 printf(".\n");
32 }
33
34 static inline void get_cpu_mask(pid_t pid, cpu_set_t *mask)
35 {
36 if (sched_getaffinity(pid, sizeof(cpu_set_t), mask) == -1)
37 {
38 perror("get cpu affinity failed.\n");
39 abort();
40 }
41 }
42
43 static inline void set_cpu_mask(pid_t pid, cpu_set_t *mask)
44 {
45 if (sched_setaffinity(pid, sizeof(cpu_set_t), mask) == -1)
46 {
47 perror("set cpu affinity failed.\n");
48 abort();
49 }
50 }
51
52 void *thread_func(void *param)
53 {
54 cpu_set_t cpu_mask;
55 get_cpu_mask(0, &cpu_mask);
56 printf("Slave thread ");
57 print_cpu_mask(cpu_mask);
58 while (1);
59 }
60
61 int main(int argc, char *argv[])
62 {
63 unsigned int active_cpu = 0;
64 cpu_set_t cpu_mask;
65 pthread_t thread;
66
67 get_cpu_mask(0, &cpu_mask);
68 print_cpu_mask(cpu_mask);
69
70 CPU_ZERO(&cpu_mask);
71 CPU_SET(active_cpu, &cpu_mask);
72 set_cpu_mask(0, &cpu_mask);
73
74 get_cpu_mask(0, &cpu_mask);
75 printf("Master thread ");
76 print_cpu_mask(cpu_mask);
77
78 if (pthread_create(&thread, NULL, thread_func, NULL) != 0)
79 {
80 perror("pthread_create failed.\n");
81 }
82 pthread_join(thread, NULL);
83
84 return 0;
85 }
當然,我們可以在子線程主函數thread_func再設定CPU親緣性

1 void *thread_func(void *param)
2 {
3 cpu_set_t cpu_mask;
4 get_cpu_mask(0, &cpu_mask);
5 printf("Slave thread ");
6 print_cpu_mask(cpu_mask);
7
8 CPU_ZERO(&cpu_mask);
9 CPU_SET(1, &cpu_mask);
10 CPU_SET(2, &cpu_mask);
11 set_cpu_mask(0, &cpu_mask);
12 get_cpu_mask(0, &cpu_mask);
13 printf("Slave thread ");
14 print_cpu_mask(cpu_mask);
15
16 for (;;)
17 {
18 ;
19 }
20 }
編譯并運作:
是吧
我們發現隻有有1号cpu的使用率為百分之百?這是因為線程的執行代碼太簡單了,隻有一個空的循環,而且目前系統也很空閑,即便是配置設定了兩個cpu,程序排程程式也根本就沒去排程它,是以它就随機的在某一個cpu上固定的死耗。當然,如果有其它程式要使用cpu1,那麼此種情況下demo就可能會被排程到cpu2上去執行。可以試試,開兩個終端都執行demo,此時看到的情況就是這樣了:
在上面調用sched_getaffinity和sched_setaffinity時,我們傳遞的第一個參數pid都為0,這意味着修改的親緣性就是針對目前調用該函數的線程,這也是最友善的,大多數情況下都這麼用,除非你确實想修改其它線程的cpu親緣性。
還有另外相關接口,可以用來指定某個線程的CPU親緣性:

1 #define _GNU_SOURCE
2 #include <pthread.h>
3
4 int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize, const cpu_set_t *cpuset);
5 int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize, cpu_set_t *cpuset);
在利用NPTL建立出來的線程代碼裡,為了更好的相容性,建議使用pthread_getaffinity_np和pthread_setaffinity_np,此時第一個參數不能再傳0,可改成pthread_self()即可。而在其它情況下,當然還是使用sched_getaffinity和sched_setaffinity。

1 /**
2 * FileName: affinity_demo.c
3 */
4 #define _GNU_SOURCE
5
6 #include <stdint.h>
7 #include <stdio.h>
8 #include <sched.h>
9 #include <pthread.h>
10 #include <stdlib.h>
11
12 static inline void print_cpu_mask(cpu_set_t cpu_mask)
13 {
14 unsigned char flag = 0;
15 printf("Cpu affinity is ");
16 for (unsigned int i = 0; i < sizeof(cpu_set_t); i ++)
17 {
18 if (CPU_ISSET(i, &cpu_mask))
19 {
20 if (flag == 0)
21 {
22 flag = 1;
23 printf("%d", i);
24 }
25 else
26 {
27 printf(",%d", i);
28 }
29 }
30 }
31 printf(".\n");
32 }
33
34 static inline void get_cpu_mask(pthread_t tid, cpu_set_t *mask)
35 {
36 if (pthread_getaffinity_np(tid, sizeof(cpu_set_t), mask) == -1)
37 {
38 perror("get cpu affinity failed.\n");
39 abort();
40 }
41 }
42
43 static inline void set_cpu_mask(pthread_t tid, cpu_set_t *mask)
44 {
45 if (pthread_setaffinity_np(tid, sizeof(cpu_set_t), mask) == -1)
46 {
47 perror("set cpu affinity failed.\n");
48 abort();
49 }
50 }
51
52 void *thread_func(void *param)
53 {
54 cpu_set_t cpu_mask;
55 get_cpu_mask(pthread_self(), &cpu_mask);
56 printf("Slave thread ");
57 print_cpu_mask(cpu_mask);
58
59 CPU_ZERO(&cpu_mask);
60 CPU_SET(1, &cpu_mask);
61 CPU_SET(2, &cpu_mask);
62 set_cpu_mask(pthread_self(), &cpu_mask);
63 get_cpu_mask(pthread_self(), &cpu_mask);
64 printf("Slave thread ");
65 print_cpu_mask(cpu_mask);
66
67 for (;;)
68 {
69 ;
70 }
71 }
72
73 int main(int argc, char *argv[])
74 {
75 unsigned int active_cpu = 0;
76 cpu_set_t cpu_mask;
77 pthread_t thread;
78
79 get_cpu_mask(pthread_self(), &cpu_mask);
80 print_cpu_mask(cpu_mask);
81
82 CPU_ZERO(&cpu_mask);
83 CPU_SET(active_cpu, &cpu_mask);
84 set_cpu_mask(pthread_self(), &cpu_mask);
85
86 get_cpu_mask(pthread_self(), &cpu_mask);
87 printf("Master thread ");
88 print_cpu_mask(cpu_mask);
89
90 if (pthread_create(&thread, NULL, thread_func, NULL) != 0)
91 {
92 perror("pthread_create failed.\n");
93 }
94 pthread_join(thread, NULL);
95
96 return 0;
97 }
備注
本文中有相當份量的内容參考借鑒了網絡上各位網友的熱心分享,特别是一些帶有完全參考的文章,其後附帶的連結内容更直接、更豐富,筆者隻是做了一下歸納&轉述,在此一并表示感謝。
參考
《CPU Affinity》
《CPU親和性的使用與機制》
《利用多核多線程進行程式優化》
《管理處理器的親和性(affinity)》
《深度剖析告訴你irqbalance有用嗎?》
《生成CPU使用率 sin 曲線 控制cpu使用率 程式設計之美》