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了。

继续阅读

Tcpdump抓包重放

开发工作中经常有这样的场景,和同事联调的时候,对方发了一个测试包过来,这边用tcpdump –Xlnsp0抓到包了,但是程序结果不正确,然后自己debug,修改,然后需要再测试,这时候再让同事发一个?如果不正确,这一来一回的比较慢,如果有个工具自动把tcpdump抓下来的内容再发出去,就ok了。

于是问题就是这样的,比如用tcpdump –Xlnps0抓到一个udp包,然后用工具把这个包重新发出去。

 

继续阅读

Linux下用g++编译共享库的一个问题

最近在使用linux下的共享库so的时候遇到一个奇怪的问题,做个记录,方便备查。

一般来说,如果用gcc编译的时候加上-shared和-fPIC选项,可以把源文件编译成一个so文件,可以在其他源程序连接阶段把这个链接上去,从而可以调用so文件提供的函数接口,这样可以多文件共用一个so文件提供的函数,即节省内存空间,也便于更新,所有的接口只需要更新so文件就行。

其实除了上面的方法,还有一个方法,那就是在运行时由程序自己动态加载so文件,使用一系列系统调用如dlopen,dlsym,dlclose等来进行动态加载,获取函数地址从而进行函数调用,关闭加载的so文件等。

一般以第一种方法用得多,但是第二种方法更灵活,结合配置文件,更具一般意义上的服务扩展性。 但是就是在用第二种方法的时候出现了一点问题。

这里把问题抽象一下,假设有一个源文件是要编译为so文件的,假设这个源文件只提供一个简单的函数,add,取两个整数为参数,返回它们的和,源文件为add.c,代码如下:

#include <stdio.h> 
 
int add(int a, int b) 
{ 
    return a+b; 
} 

编译

gcc -shared -fPIC add.c -o libadd.so

 

然后写个脚手架程序来测试,test.c,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int main(int argc, char *argv[])
{
    void * handle;
    int (*func)(int, int);
    char *error;

    handle = dlopen("libadd.so", RTLD_LAZY);
    if(!handle)
    {
        fprintf(stderr, "%s\n", dlerror());
        exit(1);
    }

    func = (int (*)(int,int))dlsym(handle, "add");
    if((error = dlerror()) != NULL)
    {
        fprintf(stderr, "%s\n", error);
        exit(1);
    }

    func(3, 4);
    dlclose(handle);

    return 0;
}
 

编译

gcc test.c -o test -ldl

然后修改LD_LIBRARY_PATH变量,加上库所在的目录,这里是当前目录

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
 

好的,这样是没有问题的,这样的方法是标准的动态加载so库的方法,但是问题在什么地方呢,如果把gcc换成g++,问题就出现了 整个编译完成后,执行test,它会告诉你

./libadd.so: undefined symbol: add
 

找不到符号add 然后用nm看一下libadd.so的符号名

$ nm libadd.so |grep add

00000000000005ec T _Z3addii

它娘的,这是c++编译器的符号命名法,用c++filt看一下

$ c++filt _Z3addii

add(int, int)

看吗,这才是我们很显而易见的函数名 所以,解决方案出来,把库源文件的函数当C符号来处理,这样

extern "C" {
// the function code
...
}

然后编译运行,这就是好的,编译出来后用nm看一下

$ nm libadd.so |grep add

00000000000005dc T add

哈哈,这就好了,然后在dlsym中写符号名的时候就直接写add就行了。

这里例子虽然是用g++编译纯C文件,其实是真正的cpp文件也是可以的。

统计整数二进制表示中1的个数

这是一个很有意思的问题,也是在面试中最容易被问到的问题之一。这个问题有个正式的名字叫Hamming_weight,而且wikipedia上也提供了很好的位运算解决的方法,这个下面也会提到。

解决这个问题的第一想法是一位一位的观察,判断是否为1,是则计数器加一,否则跳到下一位,于是很容易有这样的程序。

int test(int n)
{
	int count=0;
	while(n != 0){
		if(n%2 ==1)
			count++;
		n /= 2;
	}
	return count;
}

或者和其等价的位运算版本:

int test(int n)
{
	int count=0;
	while(n != 0){
		count += n&1;
		n >>= 1;
	}
	return count;
}

这样的方法复杂度为二进制的位数,即[tex]\log_2n[/tex],于是可是想一下,有没有只与二进制中1的位数相关的算法呢。

可以考虑每次找到从最低位开始遇到的第一个1,计数,再把它清零,清零的位运算操作是与一个零,但是在有1的这一位与零的操作要同时不影响未统计过的位数和已经统计过的位数,于是可以有这样一个操作 n&(n-1) ,这个操作对比当前操作位高的位没有影响,对低位则完全清零。拿6(110)来做例子,第一次 110&101=100,这次操作成功的把从低位起第一个1消掉了,同时计数器加1,第二次100&011=000,同理又统计了高位的一个1,此时n已变为0,不需要再继续了,于是110中有2个1。

代码如下:

int test(int n)
{
	int count=0;
	while(n != 0){
		n &= n-1;
		count ++;
	}
	return count;
}

这几个方法虽然也用到了位运算,但是并没有体现其神奇之处,下面这个版本则彰显位运算的强大能力,若不告诉这个函数的功能,一般一眼看上去是想不到这是做什么的,这也是wikipedia上给出的计算hamming_weight方法。

int test(int n)
{
	n = (n&0x55555555) + ((n>>1)&0x55555555);
	n = (n&0x33333333) + ((n>>2)&0x33333333);
	n = (n&0x0f0f0f0f) + ((n>>4)&0x0f0f0f0f);
	n = (n&0x00ff00ff) + ((n>>8)&0x00ff00ff);
	n = (n&0x0000ffff) + ((n>>16)&0x0000ffff);

	return n;
}

没有循环,5个位运算语句,一次搞定。

比如这个例子,143的二进制表示是10001111,这里只有8位,高位的0怎么进行与的位运算也是0,所以只考虑低位的运算,按照这个算法走一次

+---+---+---+---+---+---+---+---+
| 1 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |   <---143
+---+---+---+---+---+---+---+---+
|  0 1  |  0 0  |  1 0  |  1 0  |   <---第一次运算后
+-------+-------+-------+-------+
|    0 0 0 1    |    0 1 0 0    |   <---第二次运算后
+---------------+---------------+
|        0 0 0 0 0 1 0 1        |   <---第三次运算后,得数为5
+-------------------------------+

这里运用了分治的思想,先计算每对相邻的2位中有几个1,再计算每相邻的4位中有几个1,下来8位,16位,32位,因为2^5=32,所以对于32位的机器,5条位运算语句就够了。

像这里第二行第一个格子中,01就表示前两位有1个1,00表示下来的两位中没有1,其实同理。再下来01+00=0001表示前四位中有1个1,同样的10+10=0100表示低四位中有4个1,最后一步0001+0100=00000101表示整个8位中有5个1。

15身份证号码转18位的程序

以前在哪看到的,安全焦点吧!丢这做个备份

 

/*输入原来的15位身份证号码,产生新的18位身份证号码的程序*/

#include "stdio.h"
#include "string.h"
#include "conio.h"

/*
* gen New 18 ID Card from old 15 ID
*/
char genNewID( char ID[], char NewID[])
{
    int W[18] = {7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2,1};
    char A[11] = {'1','0','x','9','8','7','6','5','4','3','2'};
    int i,j,S;

    if(strlen(ID) != 15)
        return -1;

    memcpy( NewID, ID, 6 );
    NewID[6]='1';
    NewID[7]='9';
    NewID[8]=0;
    strcat( NewID, &ID[6] );
    S = 0;
    for(i=0;i<17;i++)
    {
        j = (NewID[i] - '0') * W[i];
        S = S + j;
    }

    S = S % 11;
    NewID[17] = A[S];
    NewID[18] = 0;

    return A[S];
}

int main(int argc, char* argv[])
{
    char ID[20], NewID[20], ret;

    puts("输入原来的15位身份证号码,产生新的18位身份证号码\n");
    do{
        printf("Input your old 15 ID Card: ");
        scanf( "%s", ID );
        if(stricmp(ID, "exit") == 0)break;
        ret = genNewID( ID, NewID );
        printf("Your New 18 ID Card:       %s \n", ret != -1 ? NewID : "Input Error!!");
    }while(1);

    getch();

    return 0;
}