Linux CPU親緣性詳解

前言

      在淘寶開源本身基於nginx打造的tegine服務器的時候,有這麼一項特性引發了筆者的興趣。「自動根據CPU數目設置進程個數和綁定CPU親緣性」。當時筆者對CPU親緣性沒有任何概念,當時做者只是下意識的打開了google並輸入CPU親緣性(CPU Affinity)簡單了作了個瞭解。html

      後來,在筆者參加實際工做之後,就碰到了這麼兩個問題。linux

      問題一:如何在SMP的系統中,保證某個特定進程即便在其餘進程都很忙的狀況下都可以得到足夠的CPU資源?解決的思路主要有如下兩種:nginx

  1. 提升進程的處理優先級
  2. 從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親緣性。

  1. 經過Linux提供的taskset工具指定進程運行的CPU。
  2. 方式二,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;
View Code

      能夠看到其用每一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 }
View Code

      另外的幾個宏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
View Code

      利用這幾個宏方便咱們操做指定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 }
View Code

     編譯,並運行

 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 }
View Code

     固然,咱們能夠在子線程主函數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 }
View Code

     編譯並運行:

是吧

 

      咱們發現只有有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);
View Code

      在利用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 }
View Code

備註

     本文中有至關分量的內容參考借鑑了網絡上各位網友的熱心分享,特別是一些帶有徹底參考的文章,其後附帶的連接內容更直接、更豐富,筆者只是作了一下概括&轉述,在此一併表示感謝。

參考

    《CPU Affinity

    《CPU親和性的使用與機制

    《利用多核多線程進行程序優化

    《管理處理器的親和性(affinity)

    《深度剖析告訴你irqbalance有用嗎?

    《生成CPU使用率 sin 曲線 控制cpu使用率 編程之美

相關文章
相關標籤/搜索