众所周知,nginx是高性能web server的代表,看nginx的代码,随处可以发现对性能的考究,像建立数据结构考虑到cpu的cache line size,比较字符串4个字节转换成整数比较等,这里我们说一下cpu affinity(或cpu亲和)。nginx一般推荐是有几个cpu核,就设置几个worker,这样可以减少进程调度的开销,从而充分地利用cpu。再设置下worker_cpu_affinity,则会把worker绑定在相应的cpu上,从而让worker进程固定在一个cpu上执行,减少切换cpu的cache miss,能获得更好的性能。
这是nginx文档中介绍 worker_cpu_affinity 的部分
syntax: worker_cpu_affinity cpumask ...;default: —context: mainBinds worker processes to the sets of CPUs. Each CPU set is represented by a bitmask of allowed CPUs. There should be a separate set defined for each of the worker processes. By default, worker processes are not bound to any specific CPUs.For example,worker_processes 4;worker_cpu_affinity 0001 0010 0100 1000;binds each worker process to a separate CPU, whileworker_processes 2;worker_cpu_affinity 0101 1010;binds the first worker process to CPU0/CPU2, and the second worker process to CPU1/CPU3. The second example is suitable for hyper-threading.The directive is only available on FreeBSD and Linux.
看nginx的实现,相关内容在源文件os/unix/ngx_setaffinity.c中
void ngx_setaffinity(uint64_t cpu_affinity, ngx_log_t *log) { cpu_set_t mask; ngx_uint_t i; ngx_log_error(NGX_LOG_NOTICE, log, 0, "sched_setaffinity(0x%08Xl)", cpu_affinity); CPU_ZERO(&mask); i = 0; do { if (cpu_affinity & 1) { CPU_SET(i, &mask); } i++; cpu_affinity >>= 1; } while (cpu_affinity); if (sched_setaffinity(0, sizeof(cpu_set_t), &mask) == -1) { ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, "sched_setaffinity() failed"); } }
略去上面代码中的nginx_log_t等其它不相关内容,这里主要的操作就是sched_setaffinity。上面nginx文档中有说这个特性仅在FreeBSD和linux上有效,如果系统是FreeBSD,调用的是cpuset_setaffinity;如果系统是linux,调用的是sched_setaffinity(就是上面代码中展示的)。
以linux为例,man sched_setaffinity看一下,系统调用原型是
int sched_setaffinity(pid_t pid, size_t cpusetsize,cpu_set_t *mask);
pid是进程id,一般填0,表示改变当前进程的cpu affinity。
cpu_set_t,指示要设置的cpu affinity,类似select中的fd_set和FD系列宏,这个cpu_set_t的操作也有操作宏,具体可参见man CPU_SET。
cpu_set_t的定义见/usr/include/bits/sched.h /* Size definition for CPU sets. */ # define __CPU_SETSIZE 1024 # define __NCPUBITS (8 * sizeof (__cpu_mask)) /* Type for array elements in 'cpu_set_t'. */ typedef unsigned long int __cpu_mask; /* Data structure to describe CPU mask. */ typedef struct { __cpu_mask __bits[__CPU_SETSIZE / __NCPUBITS]; } cpu_set_t;
可以看到是用bit位来表示cpu的状态的,宏定义写了支持1024个cpu,一般应该是足够使用了。另外对cpu_set_t的操作宏CPU_SET实际的定义也在/usr/include/bits/sched.h中,也就是对__cpu_mask的一些位操作。
/* Access functions for CPU masks. */ # define __CPU_ZERO_S(setsize, cpusetp) \ do { \ size_t __i; \ size_t __imax = (setsize) / sizeof (__cpu_mask); \ __cpu_mask *__bits = (cpusetp)->__bits; \ for (__i = 0; __i < __imax; ++__i) \ __bits[__i] = 0; \ } while (0) # endif # define __CPU_SET_S(cpu, setsize, cpusetp) \ (__extension__ \ ({ size_t __cpu = (cpu); \ __cpu / 8 < (setsize) \ ? (((__cpu_mask *) ((cpusetp)->__bits))[__CPUELT (__cpu)] \ |= __CPUMASK (__cpu)) \ : 0; })) # define __CPU_CLR_S(cpu, setsize, cpusetp) \ (__extension__ \ ({ size_t __cpu = (cpu); \ __cpu / 8 < (setsize) \ ? (((__cpu_mask *) ((cpusetp)->__bits))[__CPUELT (__cpu)] \ &= ~__CPUMASK (__cpu)) \ : 0; })) # define __CPU_ISSET_S(cpu, setsize, cpusetp) \ (__extension__ \ ({ size_t __cpu = (cpu); \ __cpu / 8 < (setsize) \ ? ((((const __cpu_mask *) ((cpusetp)->__bits))[__CPUELT (__cpu)] \ & __CPUMASK (__cpu))) != 0 \ : 0; }))
再结合nginx文档中的例子和nginx的源码来看
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;
如果这个内容写的nginx的配置文件中,然后nginx启动或者重新加载配置的时候,看到worker_process是4,就会起4个worker,然后把worker_cpu_affinity后面的四个值当作四个cpu affinity mask,分别调用ngx_setaffinity,然后就把四个worker进程分别绑定到cpu 0-3上。
如果手头没有nginx,上面的代码就不能在nginx上直观的看到,我们可以自己写个代码来验证一下。
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> int print_cpu_affinity() { int i = 0; cpu_set_t cpu_mask; if(sched_getaffinity(0, sizeof(cpu_set_t), &cpu_mask) == -1) { printf("get_cpu_affinity fail, %s\n", strerror(errno)); return -1; } printf("cpu affinity: "); for(i = 0; i < CPU_SETSIZE; i++) { if(CPU_ISSET(i, &cpu_mask)) printf("%d ", i); } printf("\n"); return 0; } int set_cpu_affinity(uint32_t cpu_affinity) { int i = 0; cpu_set_t cpu_mask; CPU_ZERO(&cpu_mask); for(i = 0;cpu_affinity; i++, cpu_affinity >>= 1) { if(cpu_affinity & 1) { CPU_SET(i, &cpu_mask); } } if(sched_setaffinity(0, sizeof(cpu_set_t), &cpu_mask) == -1) { printf("set_cpu_affinity fail, %s\n", strerror(errno)); return -1; } return 0; } void hold() { while(1) { ; } } int main(int argc, char **argv) { uint32_t cpu_affinity = 10; if( argc < 2 ) { printf("no affinity given, use cpu_affinity = 10\n"); } else { cpu_affinity = strtoul(argv[1], NULL, 2); } print_cpu_affinity(); set_cpu_affinity(cpu_affinity); print_cpu_affinity(); hold(); return 0; }
编译运行
$ cc cpu_affinity.c -o cpu_affinity $ ./cpu_affinity 100 cpu affinity: 0 1 2 3 4 5 6 7 cpu affinity: 2 $ mpstat -P ALL 1 Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle Average: all 12.51 0.00 0.00 0.00 0.00 0.00 0.06 0.00 0.00 87.43 Average: 0 0.00 0.00 0.17 0.00 0.00 0.00 0.00 0.00 0.00 99.83 Average: 1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 2 99.50 0.00 0.00 0.00 0.00 0.00 0.50 0.00 0.00 0.00 Average: 3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 4 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 5 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 6 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 7 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
可以看到cpu affinity设置成功了,进程占用了整个cpu2。
设置cpu affinity还有对接口 pthread_getaffinity_np和pthread_setaffinity_np ,这是在 sched_setaffinity这个系统调用的基础上封装的,用线程的时候可以考虑用这个。
用fork产生的child会继承父进程的cpu affinity,进程中产生的也会继承主进程的cpu affinity。
taskset是一个单独的工具,可以设置cpu affinity,既可以在程序启动的时候指定,也可以设置正在运行的进程的cpu affinity。
用taskset来启动我们上面这个程序
# taskset -c 3,5-7 ./cpu_affinity 100 cpu affinity: 3 5 6 7 cpu affinity: 2
可以看到,程序启动的时候的cpu affinity就是-c参数设置的,然后又被我们改成2了。
对于正在运行的进程,可以这样,还是以上面的程序为例
# pgrep cpu_affinity 11353 # taskset -p -c 3,5-7 11353 pid 11353's current affinity list: 2 pid 11353's new affinity list: 3,5-7
可以看到,这个程序之前被我们设置成在cpu2上运行,被taskset改成了3,5-7了。