Crane
Table_bottom

Search
Loading
Table_bottom

分类
Table_bottom

随机文章
Table_bottom

标签云
Table_bottom

最新评论
Table_bottom

链接
Table_bottom

功能
Table_bottom

从nginx说cpu affinity

众所周知,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: main
 
Binds 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, while
 
    worker_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了。

继续阅读