1. CPU亲和性
1.1 CPU亲和性介绍
简单地说,CPU 亲和性(affinity)就是进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。Linux 内核进程调度器天生就具有被称为软 CPU 亲和性(affinity)的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。
2.6 版本的 Linux 内核还包含了一种机制,它让开发人员可以编程实现硬 CPU 亲和性(affinity)。这意味着应用程序可以显式地指定进程在哪个(或哪些)处理器上运行。
1.2为什么要使用CPU亲和性
通常 Linux 内核都可以很好地对进程进行调度,在应该运行的地方运行进程(这就是说,在可用的处理器上运行并获得很好的整体性能)。内核包含了一些用来检测 CPU 之间任务负载迁移的算法,可以启用进程迁移来降低繁忙的处理器的压力。
一般情况下,在应用程序中只需使用缺省的调度器行为。然而,您可能会希望修改这些缺省行为以实现性能的优化。让我们来看一下使用硬亲和性(affinity) 的 3 个原因。
基于大量计算的情形通常出现在科学和理论计算中,但是通用领域的计算也可能出现这种情况。一个常见的标志是您发现自己的应用程序要在多处理器的机器上花费大量的计算时间。
测试复杂软件是我们对内核的亲和性(affinity)技术感兴趣的另外一个原因。考虑一个需要进行线性可伸缩性测试的应用程序。有些产品声明可以在 使用更多硬件 时执行得更好。
我们不用购买多台机器(为每种处理器配置都购买一台机器),而是可以:
购买一台多处理器的机器
不断增加分配的处理器
测量每秒的事务数
评估结果的可伸缩性
如果应用程序随着 CPU 的增加可以线性地伸缩,那么每秒事务数和 CPU 个数之间应该会是线性的关系。
图1.1 Amdahl法则
Amdahl 法则说明这种加速比在现实中可能并不会发生,但是可以非常接近于该值。对于通常情况来说,我们可以推论出每个程序都有一些串行的组件。随着问题集不断变大,串行组件最终会在优化解决方案时间方面达到一个上限。
Amdahl 法则在希望保持高 CPU 缓存命中率时尤其重要。如果一个给定的进程迁移到其他地方去了,那么它就失去了利用 CPU 缓存的优势。实际上,如果正在使用的 CPU 需要为自己缓存一些特殊的数据,那么所有其他 CPU 都会使这些数据在自己的缓存中失效。
因此,如果有多个线程都需要相同的数据,那么将这些线程绑定到一个特定的 CPU 上是非常有意义的,这样就确保它们可以访问相同的缓存数据(或者至少可以提高缓存的命中率)。否则,这些线程可能会在不同的 CPU 上执行,这样会频繁地使其他缓存项失效。
我们对 CPU 亲和性(affinity)感兴趣的最后一个原因是实时(对时间敏感的)进程。例如,您可能会希望使用硬亲和性(affinity)来指定一个 8 路主机上的某个处理器,而同时允许其他 7 个处理器处理所有普通的系统调度。这种做法确保长时间运行、对时间敏感的应用程序可以得到运行,同时可以允许其他应用程序独占其余的计算资源。
下面的样例应用程序显示了这是如何工作的。
1.3 Linux 关于CPU亲和性API介绍
Linux中针对cpu亲和性特性提供的API如表1.1所示,表中cpu_set_t是一个掩码数组,一共有1024位,每一位对应系统中的一个逻辑处理器,最低位对应系统中的第一个逻辑处理器,而最高位则对应系统中最后一个逻辑处理器。如在一个四核的服务器上,0001表示第一个第一个逻辑处理器,0010表示第二个逻辑处理器,以此类推,在实际编程过程中不应该直接修改位掩码,而是使用系统提供的操作宏。
表1.1 linux提供的CPU亲和性API或或定义原型
API原型或宏定义 | 功能描述 | 参数意义 |
int sched_setaffinity(pid_t pid, size_t cpusetsize,cpu_set_t *mask) | 设定pid绑定的CPU | pid:设定的进程的进程号 |
cpusetsize:sizeof(cpu_set_t) | ||
mask:设置的cpu掩码 | ||
int sched_getaffinity(pid_t pid, size_t cpusetsize,cpu_set_t *mask) | 查看pid绑定的CPU | 同上 |
void CPU_ZERO(cpu_set_t *set) | 对CPU集set进行初始化,将其设置为空集 |
|
void CPU_SET(int cpu, cpu_set_t *set) | 将cpu加入CPU集set中 | cpu:cpu编号,从0开始到cpu_num. |
void CPU_CLR(int cpu, cpu_set_t *set) | 将cpu从CPU集set中清除 |
|
int CPU_ISSET(int cpu, cpu_set_t *set) | 判断cpu是否在CPU集set中,若在,返回true否则返回false |
|
需要注意的是,当进程设置了CPU亲和性后,进程就被绑定了,只能在那些对应的位被设置的逻辑处理器上运行,如果进程没有显示对CPU亲和性进行设置,则默认所有的位均被置位。另外,CPU亲和性具有遗传性,即设置了CPU亲和性的进程会将这些CPU亲和性传递给从他们派生的子进程,当然,子进程可以调用系统提供的接口,重新对CPU亲和性进行设置。
2.Nginx中的CPU亲和性
Nginx作为一款高性能的web服务器,根据实际使用系统的支持情况提供了CPU亲和性的能力。在实际的使用中,如果要使用nginx的CPU亲和性的功能,需要进行一定的配置。
2.1 Nginx CPU亲和性配置
Nginx中配置CPU亲和性的语法如图2.1所示。
图2.1 nginx中CPU亲和性配置的语法
配置示例:
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;
此配置的含义为共启动4个worker进程,第一个进程绑定到系统的第一个逻辑处理器,第二个进程绑定到系统的第二个逻辑处理器,以此类推。另外,也可以将一个进程绑定到多个CPU, 示例如下:
worker_processes 2;
worker_cpu_affinity 0101 1010;
此示例表示启动两个worker进程,其中第一个进程绑定到cpu0/cpu2,第二个进程绑定到cpu1/cpu3。
2.2 Nginx CPU亲和性代码简析
2.2.1 cpu亲和性配置读取
static ngx_command_t ngx_core_commands[] = {
……
{ ngx_string("worker_cpu_affinity"),
NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_1MORE,
0,
0,
NULL },
……
}
CPU亲和性的配置是在core模块初始化过程中读取的,从core_module的命令配置可见,读取CPU亲和性相关的回调函数为ngx_set_cpu_affinity,其实现位于nginx.c中。
static char *
ngx_set_cpu_affinity(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
#if (NGX_HAVE_CPU_AFFINITY)
ngx_core_conf_t *ccf = conf;
u_char ch;
uint64_t *mask;
ngx_str_t *value;
ngx_uint_t i, n;
if (ccf->cpu_affinity) {
return "is duplicate";
}
mask = ngx_palloc(cf->pool, (cf->args->nelts - 1) * sizeof(uint64_t));
if (mask == NULL) {
return NGX_CONF_ERROR;
}
ccf->cpu_affinity_n = cf->args->nelts - 1;
ccf->cpu_affinity = mask;
value = cf->args->elts;
for (n = 1; n < cf->args->nelts; n++) {
if (value[n].len > 64) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"worker_cpu_affinity\" supports up to 64 CPUs only");
return NGX_CONF_ERROR;
}
mask[n - 1] = 0;
for (i = 0; i < value[n].len; i++) {
ch = value[n].data[i];
if (ch == ' ') {
continue;
}
mask[n - 1] <<= 1;
if (ch == '0') {
continue;
}
if (ch == '1') {
mask[n - 1] |= 1;
continue;
}
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid character \"%c\" in \"worker_cpu_affinity\"",
ch);
return NGX_CONF_ERROR;
}
}
#else
ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
"\"worker_cpu_affinity\" is not supported "
"on this platform, ignored");
#endif
return NGX_CONF_OK;
}
此函数功能简单易懂,主要实现将配置文件转换成uint64_t位域的格式,并将转换后的数组保存到ccf->cpu_affinity中,同时通过ccf->cpu_affinity_n记录配置的CPU亲和性参数个数。从代码可以看出,只有定义了NGX_HAVE_CPU_AFFINITY宏后,nginx才可以使用CPU亲和性的特性,NGX_HAVE_CPU_AFFINITY依赖于configure对系统的检测,目前只有linux系统支持CPU亲和性的配置,在http://wiki.nginx.org/NginxHttpMainModule#worker_-
cpu_affInity讨论Nginx性能优化CPU参数worker_cpu_affinity使用说明中明确指出,CPU亲和性的配置是“linux only”的。
2.2.2 cpu亲和性设置
在nginx_process_cycle.c中,定义了和CPU亲和性相关的变量cpu_affinity,如下所示:
uint64_t cpu_affinity;
在调用ngx_start_worker_processes函数创建进程时,有如下代码:
static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
……
for (i = 0; i < n; i++) {
cpu_affinity = ngx_get_cpu_affinity(i);
ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,
"worker process", type);
……
}
}
进程创建后执行ngx_worker_process_cycle函数,在此函数中,我们可以看到如下代码:
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
……
ngx_worker_process_init(cycle, 1);
……
}
真正的CPU亲和性的设置将在ngx_worker_process_init函数中进行。
static void
ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority)
{
……
if (cpu_affinity) {
ngx_setaffinity(cpu_affinity, cycle->log);
}
……
}
由此可见,如果配置了CPU亲和性,则调用ngx_setaffinity进行真正的CPU亲和性设置,此函数的定义如下:
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");
}
}
函数的功能很简单,从地位到高位遍历cpu_affinity中的位值,如果某位为1,则将对应的CPU设置到CPU集mask中,最后调用sched_setaffinity设置本进程的CPU亲和性,同时,由于采用uint64_t存储转换后的结果,nginx最多只支持64 CPUs的CPU亲和性的配置。
3.总结
本文对CPU亲和性进行讨论并对nginx中实现CPU亲和性的代码进行了解析。
参考文献
[1] 管理处理器的亲和性(affinity).http://www.ibm.com/developerworks/cn/linux/l-affinity.html