并行学习总结

Thread的提出有一部分原因就是来因为IPC效率低下,像这样使用多进程仅仅是把本来应该自己做的同步交给了OS去完成。而且最终数据要汇集到一个进程去最终完成,这样的话效率最终很可能就被这最后一个进程限制住,从而影响了整体的效率。

所以采用进程中开多个线程来提高通信效率避免进程间通信效率低下的问题,再把每个线程绑定到每个核上去就可以避免线程切换带来的开销弊端。再开多个进程进而把所有的核都利用起来(保证每个核上都运行一个线程)!!!!!!!

 

多核处理器,要注意使用多线程/进程+IO多路复用,这样才能将CPU的利用率提高到最大。

 

从简便的角度看,每个任务一个进程或线程是最好的,但是会有额外的调度损耗,如果实现得不讲究,惊群的成本很高。

从效率的角度看,应该依照CPU个数创建一个线程池,把N个任务分配给它们,可以最大程度降低调度成本。

总之,你要在实现效率和运行效率之间作出决断。

 

最终我们采用的模型是:主体框架是多进程,主进程内部有若干线程用于处理诸如命令接收、文件监控、配置同步、统计数据写出、debug数据写出等功能。

另外一种多进程创建方式,就是脚本直接启动多个进程

shell脚本中用nohup启动3个进程

( nohup ./a ) &

( nohup ./b ) &

( nohup ./c ) &

wait

说明:开启三个子shell在后台执行操作,( )表示开启子shell, 若不加圆括号直接这样写,则直接在父shell操作,可能造成逻辑错误,因为这个不是在命令行执行的进程, wait根据实际情况添加,表示等前面三个进程执行结束在进行下一步

启动新进程的方式:

http://www.ibm.com/developerworks/cn/linux/l-cn-nohup/

http://blog.csdn.net/ljianhui/article/details/10089345

 

由于核内的线程本质就是进程,其调度过程跟进程一样。

采用多核架构,主体进程/线程是绑定到不同的物理CPU core上并独占的,所以发生调度和切换的情况不多,因而这种影响不是很重要。

多进程单线程模型与单进程多线程模型之争

服务器,事件

多进程单线程模型典型代表:nginx
单进程多线程模型典型代表:memcached

另外redis, mongodb也可以说是走的“多进程单线程模”模型(集群),只不过作为数据库服务器,需要进行写保护,只提供了读同步。

原因很简单,因为服务器的发展大部分都是归功于Linux Unix,而不是Windows。

Linux内核提供的epoll为开发服务器提供了很大的便利,libeventlibev都是对epoll的封装,nginx自己实现了对epoll的封装。

libevent和libev都是知名的Linux系统C事件驱动编程框架。

我没说错的话,nodejs是建立在libev基础上。

memcached也依赖libevent。

所以,nginx在Windows上不像Linux快是有很大原因的。

模型,模型,多进程单线程 单进程多线程

多进程单线程

master进程管理worker进程:

接收来自外界的信号

向各worker进程发送信号

监控woker进程的运行状态

当woker进程退出后(异常情况下),会自动重新启动新的woker进程

友情提示:nodejs属于这一种好不好,不是只能单核

单进程多线程

·        单进程多线程

主线程负责监听客户端的连接请求,workers线程负责处理已经建立好的连接的读写等事件

单进程多线程

单进程多线程肯定比多进程单线程快一些

多进程单线程与单进程多线程的目的都是想尽可能的利用CPU,减少CPU的空闲时间,特别是多核环境。

他们在实际运行中,所利用的CPU工作数应该都是相同的。也就是说,你有4核,在某个时刻要么是CPU同时在4个进程做任务(多进程单线程),要么是CPU同时在4个线程上做任务(单进程多线程)。

不过,单进程多线程肯定比多进程单线程快一些。

这是因为,多进程单线程的CPU切换,是从一个进程到另一个进程,而单进程多线程的CPU切换则只在一个进程内,每个进程|线程都有自己的上下文堆栈保存,进程间的切换消耗更大一些。

这就好比是,多进程单线程是在4个函数中切换,各自拥有自己的变量;单进程多线程在1个函数中的4个子函数切换,拥有相同的全局变量。

但是,没有你想象的“快几倍”。

副作用,副作用,单进程多线程肯定有其不利的一面

我一直提过副作用。

如果你仔细看多进程单线程的图,就应该明白,这种模型提供了一种保护机制。

当其中一个进程内部读取错误,master可以让ta重启。这使得你的服务器在表面上并没有感到“曾经崩溃”。

对于master,完全不涉及服务器的业务,使得ta能被安全隔离。

再来看单进程多线程。

问题很明显,只有一个进程,一旦其中出现一个错误,整个进程都有可能挂掉。你当然可以为ta编写一个“守护程序”来重启,但是重启期间,你的服务器是真的“挂掉了”。

另外,编写单进程多线程这样的服务器,在代码上非常容易出错,而且难以控制代码的稳定性,有很多你难以琢磨的bug在等着你,因为有太多的锁,太多的全局变量需要处理,这也是函数式“纯函数”所反对的。

 

 

 

Z考虑的问题之前也考虑过,我现在采用epoll加多线程的模型。下面说说各种方式的优缺点。

优点:

多进程的好处是稳定性高,创建多个子进程来处理数据,一个子进程崩溃不会影响到其它进程,所以服务不会中断。

多线程的好处是写程序更加灵活,线程与其宿主进程共享堆栈,所以只要控制好同步,写起程序来和单线程没什么区别。另外线程的开销比进程小也是其优点,不过基本可以忽略不计。在*unx下,线程可以当做简化版本的进程。

缺点:

多进程,由于是不同的内存空间,所以要共享数据就很麻烦,我采用共享内存+用管道的方式控制,不过这样做增加了编程难度,很容易出错。而且业务逻辑基本都是由一个单独的进程实现,如果这个进程崩溃了那网络层还在跑也就没什么意义了,不幸的是业务逻辑是最容易出错的地方。

多线程,由于其使用宿主进程的堆栈,所以如果一个线程出错或锁死整个进程基本上就玩完了。

对比之下我选择了多线程模型,在相同的需求下多线程实现起来更简单清晰,不容易出错。而且就算出错,多进程的结果也和多线程一样,不会好太多。

LZ说的多进程上加多线程的方式,这个方式我也实现了一个原型,效果跟多进程和多线程一样,没有什么优势,只会增加出错几率而已。编程的时候要考虑一下硬件需求,现今的硬件及网络环境,多进程或多线程就足已满足要求了。

看到LS的提到惊群,顺便说两句。惊群现象不完全是坏的,当accept时引发惊群是可以接受的。因为连接是不可以丢弃的,必须让用户第一时间连接上服务器,所以需要由同时多个线程等待连接进,引起惊群现象也再所难免。当然普通的read/write操作引起惊群是不能接受的。

至于CPU利用率问题,理论上讲当工作的线程/进程数量为CPU个数*2+1CPU利用率最高。这里CPU个数包含核心的个数,例如双核U就是2*2+1,四核U就是4*2+1

 

游戏服务器要提高负载,都是集群的方式,一般都有N个网关的,不管你用IOCP,EPOLL还是KQUEUE,甚至是SELECT都可以的,而网关的功能很简单的,就是外网和内网之间的信息转发。因此一个线程就够了。 
对于游戏服务器组件之间的通讯(IPC)来说,就那么几个连接,SOCKET的上下文切换的消耗是很小的。 
另外,IOCP,EPOLL,KQUEUE这种机制,只有在服务器接受了N个客户端,但是服务器只和其中的很少一部分客户端在交互的情况下(很少的客户端在并发接收和发送)才能体现出它们的优点。 
而对于游戏服务器来说,你有10000个客户端在线,这些客户端基本都在同时发包,可读可写事件每个FD都差不多同时产生,这种情况下,EPOLL还是SELECT,效率上来看差别不大。因此,对于需要处理大量高并发的长连接请求的服务器来说,多进程的方式要轻松的多。

 

Thread的提出有一部分原因就是来因为IPC效率低下,像这样使用多进程仅仅是把本来应该自己做的同步交给了OS去完成。而且最终数据要汇集到一个进程去最终完成,这样的话效率最终很可能就被这最后一个进程限制住,从而影响了整体的效率。

所以采用进程中开多个线程来提高通信效率避免进程间通信效率低下的问题,再把每个线程绑定到每个核上去就可以避免线程切换带来的开销弊端。再开多个进程进而把所有的核都利用起来(保证每个核上都运行一个线程)!!!!!!!

 

个人认为如果真的要用多进程,应当是用于提高并发程度,并利用进程对于数据的保护提高程序的健壮性。

 

什么时候该使用多线程呢?这要分四种情况讨论:

 

a.多核CPU——计算密集型任务。此时要尽量使用多线程,可以提高任务执行效率,例如加密解密,数据压缩解压缩(视频、音频、普通数据),否则只能使一个核心满载,而其他核心闲置。

b.单核CPU——计算密集型任务。此时的任务已经把CPU资源100%消耗了,就没必要也不可能使用多线程来提高计算效率了;相反,如果要做人机交互,最好还是要用多线程,避免用户没法对计算机进行操作。

c.单核CPU——IO密集型任务,使用多线程还是为了人机交互方便,

d.多核CPU——IO密集型任务,这就更不用说了,跟单核时候原因一样。

4.程序员需要掌握的技巧/技术

(1)减少串行化的代码用以提高效率。这是废话。

(2)单一的共享数据分布化:把一个数据复制很多份,让不同线程可以同时访问。

(3)负载均衡,分为静态的和动态的两种。具体的参见有关文献。

 

 

经常在网络上看到有的XDJM问“多进程好还是多线程好?”、“Linux下用多进程还是多线程?”等等期望一劳永逸的问题,我只能说:没有最好,只有更好。根据实际情况来判断,哪个更加合适就是哪个好。

 

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  • 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。


呵呵,有这么简单我就不用在这里浪费口舌了,还是那句话,没有绝对的好与坏,只有哪个更加合适的问题。我们来看实际应用中究竟如何判断更加合适。

1)需要频繁创建销毁的优先用线程

原因请看上面的对比。

这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

2)需要进行大量计算的优先使用线程

所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。

这种原则最常见的是图像处理、算法处理。

3)强相关的处理用线程,弱相关的处理用进程

什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。

当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

4)可能要扩展到多机分布的用进程,多核分布的用线程

原因请看上面对比。

5)都满足需求的情况下,用你最熟悉、最拿手的方式

至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。

 

需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是进程+线程的结合方式,千万不要真的陷入一种非此即彼的误区。

 

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位

进程实验代码(fork.c):

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <signal.h>
  4.  
  5. #define P_NUMBER 255 //并发进程数量
  6. #define COUNT 5 //每次进程打印字符串数
  7. #define TEST_LOGFILE "logFile.log"
  8. FILE *logFile=NULL;
  9.  
  10. char *s="hello linux\0";
  11.  
  12. int main()
  13. {
  14.     int i=0,j=0;
  15.     logFile=fopen(TEST_LOGFILE,"a+");//打开日志文件
  16.     for(i=0;i<P_NUMBER;i++)
  17.     {
  18.         if(fork()==0)//创建子进程,if(fork()==0){}这段代码是子进程运行区间
  19.         {
  20.             for(j=0;j<COUNT;j++)
  21.             {
  22.                 printf("[%d]%s\n",j,s);//向控制台输出
  23.                 /*当你频繁读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。可能导致测试结果不准,所以在此注释*/
  24.                 //fprintf(logFile,"[%d]%s\n",j,s);//向日志文件输出,
  25.             }
  26.             exit(0);//子进程结束
  27.         }
  28.     }
  29.     
  30.     for(i=0;i<P_NUMBER;i++)//回收子进程
  31.     {
  32.         wait(0);
  33.     }
  34.     
  35.     printf("Okay\n");
  36.     return 0;
  37. }

进程实验代码(thread.c):

  1. #include <pthread.h>
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <stdio.h>
  5.  
  6. #define P_NUMBER 255//并发线程数量
  7. #define COUNT 5 //每线程打印字符串数
  8. #define TEST_LOG "logFile.log"
  9. FILE *logFile=NULL;
  10.  
  11. char *s="hello linux\0";
  12.  
  13. print_hello_linux()//线程执行的函数
  14. {
  15.     int i=0;
  16.     for(i=0;i<COUNT;i++)
  17.     {
  18.         printf("[%d]%s\n",i,s);//想控制台输出
  19.         /*当你频繁读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。可能导致测试结果不准,所以在此注释*/
  20.         //fprintf(logFile,"[%d]%s\n",i,s);//向日志文件输出
  21.     }
  22.     pthread_exit(0);//线程结束
  23. }
  24.  
  25. int main()
  26. {
  27.     int i=0;
  28.     pthread_t pid[P_NUMBER];//线程数组
  29.     logFile=fopen(TEST_LOG,"a+");//打开日志文件
  30.     
  31.     for(i=0;i<P_NUMBER;i++)
  32.         pthread_create(&pid[i],NULL,(void *)print_hello_linux,NULL);//创建线程
  33.         
  34.     for(i=0;i<P_NUMBER;i++)
  35.         pthread_join(pid[i],NULL);//回收线程
  36.         
  37.     printf("Okay\n");
  38.     return 0;
  39. }

两段程序做的事情是一样的,都是创建若干个进程/线程,每个创建出的进程/线程打印若干“hello linux”字符串到控制台和日志文件,两个若干由两个宏 P_NUMBERCOUNT分别定义,程序编译指令如下:

gcc -o fork fork.c
gcc -lpthread -o thread thread.c

 

 

 

 

1.浅谈多核CPU、多线程与并行计算

http://www.cnblogs.com/lihaozy/archive/2013/03/13/2957520.html

1.CPU发展趋势

核心数目依旧会越来越多,依据摩尔定律,由于单个核心性能提升有着严重的瓶颈问题,普通的桌面PC有望在2017年末2018年初达到24核心(或者1632线程),我们如何来面对这突如其来的核心数目的增加?编程也要与时俱进。笔者斗胆预测,CPU各个核心之间的片内总线将会采用4路组相连:),因为全相连太过复杂,单总线又不够给力。而且应该是非对称多核处理器,可能其中会混杂几个DSP处理器或流处理器。

3.线程越多越好吗?什么时候才有必要用多线程?

线程必然不是越多越好,线程切换也是要开销的,当你增加一个线程的时候,增加的额外开销要小于该线程能够消除的阻塞时间,这才叫物有所值。

Linux自从2.6内核开始,就会把不同的线程交给不同的核心去处理。Windows也从NT.4.0开始支持这一特性。

什么时候该使用多线程呢?这要分四种情况讨论:

a.多核CPU——计算密集型任务。此时要尽量使用多线程,可以提高任务执行效率,例如加密解密,数据压缩解压缩(视频、音频、普通数据),否则只能使一个核心满载,而其他核心闲置。

b.单核CPU——计算密集型任务。此时的任务已经把CPU资源100%消耗了,就没必要也不可能使用多线程来提高计算效率了;相反,如果要做人机交互,最好还是要用多线程,避免用户没法对计算机进行操作。

c.单核CPU——IO密集型任务,使用多线程还是为了人机交互方便,

d.多核CPU——IO密集型任务,这就更不用说了,跟单核时候原因一样。

 

红色代表针对计算密集型的讨论(多核加快计算速度);

绿色代表针对IO密集型的讨论(就是避免因为IO而程序卡死);

 

4.程序员需要掌握的技巧/技术

(1)减少串行化的代码用以提高效率。这是废话。

(2)单一的共享数据分布化:把一个数据复制很多份,让不同线程可以同时访问。

(3)负载均衡,分为静态的和动态的两种。具体的参见有关文献。浅谈多核CPU、多线程与并行计算

 

2. 什么情况下适合线程绑定cpu

http://www.newsmth.net/nForum/#!article/KernelTech/47030?p=2

从cache miss角度来说这个问题,如果邦定cpu,cache命中提高了,但是cpus的使用率也降低了,需要作出一定的平衡。  
理论上来讲,这个平衡应该由kernel来作,而不应该由app来做。因为: 
     kerenel了解system上的全部应用的情况,而app只了解自己的情况。 
     kerenel是调度算法的主导,了解关于调度的每个细节,而app不能。 
前面luohandsome已经说过,linux kernel会自动注意cpu的粘合性.只有在kernel认为load balance的收益大于cache命中的收益的时候才会跨cpu调度。所以说一般的应用,邦定cpu会使系统的效率降低。 
但是也有特例,如果system的全部应用的情况你都了解(如嵌入式系里就那么几个固定的app),并且系统呈现的是io瓶颈,那么邦定cpu确实可能从cache角度提高系统效率。 
另外,一般的pc系统访问延时,l1 cahce 12 nanosecondsl2 cache 6-8nanoseconds,main memory 60-120 nanoseconds 
context switching的时间是 515 microseconds 
所以和cachemiss相比,context switching更是个效率问题,所以邦定cpu一般配合SCHED_RR使用在实时要求高的嵌入式系统里,主要是用特定的app占住特定的core,避免contextswitching的产生。

在NUMA的系统上,由于remote memory和local memory的访问延迟不同,如果有自己的内存分配模式,把thread binding到CPU上还是有用的。 
还有就是在multicore的时代,比如两个core是share cache的,如果两个thread需要交互数据,那我倾向于把他们放到share cache的两个core上,很高效;如果两个thread会大规模无规律的访问数据,那我倾向于把他们放到不share cache的两个core上。目前,kernel很难了解到这样的信息,所以还需要程序员手工来binding .

http://www.mjmwired.net/kernel/Documentation/cpu-hotplug.txt

 

3. 线程绑定CPU

Linux系统提供API函数sched_setaffinitysched_getaffinity用于设置或获取线程的可以使用的CPU核。

intsched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask);

这个函数中pid表示需要设置或获取绑定信息的线程id(或进程id),如果为0,表示对当前调用的线程进行设置;第2个参数cpusetsize一般设置为sizeof(cpu_set_t),用以表示第3个参数指向的内存结构对象的大小;第3个参数mask指向类型为cpu_set_t对象的指针,用以设置或获取指定线程可以使用的CPU核列表。Linux提供函数CPU_ZEROCPU_SETCPU_ISSETcpu_set_t类型的对象进行操作,其中CPU_ZERO用于清空cpu_set_t类型对象的内容,CPU_SET用于设置cpu_set_t类型对象,CPU_ISSET用于判断cpu_set_t类型对象与核对应的位是否被设置。下面通过简单的代码示例来说明这两个函数的具体用法。

设置线程绑定代码:

cpu_set_tmask;

intblist[8]={2, 5, 13, 9, 3, 6, 7, 4}; //设置需要绑定的核列表

#pragmaomp parallel private(mask)

{

        CPU_ZERO(&mask);

        CPU_SET(blist[omp_get_thread_num()], &mask); //对每个线程设置绑定方案

        sched_setaffinity(0,sizeof(cpu_set_t), &mask);

}

该段代码将paralleregion里面的8个线程依次绑定到核2,5,13,9,3,6,7,4。同样可以使用sched_getaffinity函数获取线程的能够使用的核的列表,示例代码如下:

intnum_processors = sysconf(_SC_NPROCESSORS_CONF); //获取当前节点核的数目

cpu_set_tget;

inti = 0;

CPU_ZERO(&get);

sched_getaffinity(0,sizeof(cpu_set_t), &get); //获取当前调用线程的可以使用的核

for(i= 0; i < num_processors; i++)

{

        if(CPU_ISSET(i, &get))

        {

                  printf(“The current thread %d bound to core %d\n“, omp_get_thread_num(), i);

        }

}

下面是一个完整的例子

文件bind.c

#include<stdlib.h>

#include<stdio.h>

#include<sys/types.h>

#include<sys/sysinfo.h>

#include<unistd.h>

 

#define__USE_GNU

#include<sched.h>

#include<ctype.h>

#include<string.h>

#include<pthread.h>

#defineTHREAD_MAX_NUM 100  //1个CPU内的最多进程数

 

intnum=0;  //cpu中核数

void*threadFun(void* arg)  //arg  传递线程标号(自己定义)

{

        cpu_set_t mask;  //CPU核的集合

        cpu_set_t get;   //获取在集合中的CPU

        int *a = (int *)arg; 

        printf("the a is:%d\n",*a);  //显示是第几个线程

        CPU_ZERO(&mask);    //置空

        CPU_SET(*a,&mask);   //设置亲和力值

        if (sched_setaffinity(0, sizeof(mask), &mask) == -1)//设置线程CPU亲和力

        {

                  printf("warning: could not set CPU affinity, continuing...\n");

        }

        while (1)

        {

                  CPU_ZERO(&get);

                  if (sched_getaffinity(0, sizeof(get), &get) == -1)//获取线程CPU亲和力

                  {

                           printf("warning: cound not get thread affinity, continuing...\n");

                  }

                  int i;

                  for (i = 0; i < num; i++)

                  {

                           if (CPU_ISSET(i, &get))//判断线程与哪个CPU有亲和力

                           {

                                    printf("this thread %d is running processor : %d\n", i,i);

                           }

                  }

        }

 

        return NULL;

}

 

intmain(int argc, char* argv[])

{

        num = sysconf(_SC_NPROCESSORS_CONF);  //获取核数

        pthread_t thread[THREAD_MAX_NUM];

        printf("system has %i processor(s). \n", num);

        int tid[THREAD_MAX_NUM];

        int i;

        for(i=0;i<num;i++)

        {

                  tid[i] = i;  //每个线程必须有个tid[i]

                  pthread_create(&thread[0],NULL,threadFun,(void*)&tid[i]);

        }

        for(i=0; i< num; i++)

        {

                  pthread_join(thread[i],NULL);//等待所有的线程结束,线程为死循环所以CTRL+C结束

        }

        return 0;

}

 

编译命令:gcc bind.c-o bind -lpthread

执行:./bind

输出结果:略

 

特别注意:

#define__USE_GNU不要写成#define_USE_GNU

#include<pthread.h>必须写在#define__USE_GNU之后,否则编译会报错

查看你的线程情况可以在执行时在另一个窗口使用top -H来查看线程的情况,查看各个核上的情况请使用top命令然后按数字“1”来查看。

 

 

top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。下面详细介绍它的使用方法。

top -01:06:48 up 1:22, 1 user, load average: 0.06, 0.60, 0.48
Tasks: 29 total, 1 running, 28 sleeping, 0 stopped, 0 zombie
Cpu(s): 0.3% us, 1.0% sy, 0.0% ni, 98.7% id, 0.0% wa, 0.0% hi, 0.0% si
Mem: 191272k total, 173656k used, 17616k free, 22052k buffers
Swap: 192772k total, 0k used, 192772k free, 123988k cached

PID USERPR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1379 root 16 0 7976 2456 1980 S 0.7 1.3 0:11.03 sshd
14704 root 16 0 2128 980 796 R 0.7 0.5 0:02.72 top
1 root 16 0 1992 632 544 S 0.0 0.3 0:00.90 init
2 root 34 19 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0
3 root RT 0 0 0 0 S 0.0 0.0 0:00.00 watchdog/0

统计信息区
前五行是系统整体的统计信息。第一行是任务队列信息,同uptime 命令的执行结果。其内容如下:

01:06:48 当前时间
up 1:22
系统运行时间,格式为时:
1 user
当前登录用户数
load average: 0.06, 0.60, 0.48
系统负载,即任务队列的平均长度。
三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值。

第二、三行为进程和CPU的信息。当有多个CPU时,这些内容可能会超过两行。内容如下:

Tasks: 29total 进程总数
1 running
正在运行的进程数
28 sleeping
睡眠的进程数
0 stopped
停止的进程数
0 zombie
僵尸进程数
Cpu(s): 0.3% us
用户空间占用CPU百分比
1.0% sy
内核空间占用CPU百分比
0.0% ni
用户进程空间内改变过优先级的进程占用CPU百分比
98.7% id
空闲CPU百分比
0.0% wa
等待输入输出的CPU时间百分比
0.0% hi
0.0% si

最后两行为内存信息。内容如下:

Mem:191272k total 物理内存总量
173656k used
使用的物理内存总量
17616k free
空闲内存总量
22052k buffers
用作内核缓存的内存量
Swap: 192772k total
交换区总量
0k used
使用的交换区总量
192772k free
空闲交换区总量
123988k cached
缓冲的交换区总量。
内存中的内容被换出到交换区,而后又被换入到内存,但使用过的交换区尚未被覆盖,
该数值即为这些内容已存在于内存中的交换区的大小。
相应的内存再次被换出时可不必再对交换区写入。

进程信息区
统计信息区域的下方显示了各个进程的详细信息。首先来认识一下各列的含义。

序号列名含义
a PID
进程id
b PPID
父进程id
c RUSER Real user name
d UID
进程所有者的用户id
e USER
进程所有者的用户名
f GROUP
进程所有者的组名
g TTY
启动进程的终端名。不是从终端启动的进程则显示为?
h PR
优先级
i NI nice
值。负值表示高优先级,正值表示低优先级
j P
最后使用的CPU,仅在多CPU环境下有意义
k %CPU
上次更新到现在的CPU时间占用百分比
l TIME
进程使用的CPU时间总计,单位秒
m TIME+
进程使用的CPU时间总计,单位1/100
n %MEM
进程使用的物理内存百分比
o VIRT
进程使用的虚拟内存总量,单位kbVIRT=SWAP+RES
p SWAP
进程使用的虚拟内存中,被换出的大小,单位kb
q RES
进程使用的、未被换出的物理内存大小,单位kbRES=CODE+DATA
r CODE
可执行代码占用的物理内存大小,单位kb
s DATA
可执行代码以外的部分(数据段+)占用的物理内存大小,单位kb
t SHR
共享内存大小,单位kb
u nFLT
页面错误次数
v nDRT
最后一次写入到现在,被修改过的页面数。
w S
进程状态。
D=
不可中断的睡眠状态
R=
运行
S=
睡眠
T=
跟踪/停止
Z=
僵尸进程
x COMMAND
命令名/命令行
y WCHAN
若该进程在睡眠,则显示睡眠中的系统函数名
z Flags
任务标志,参考sched.h

默认情况下仅显示比较重要的 PIDUSERPRNIVIRTRESSHRS%CPU%MEMTIME+COMMAND 列。可以通过下面的快捷键来更改显示内容。

更改显示内容
通过 f 键可以选择显示的内容。按 f 键之后会显示列的列表,按 a-z 即可显示或隐藏对应的列,最后按回车键确定。

o 键可以改变列的显示顺序。按小写的 a-z 可以将相应的列向右移动,而大写的A-Z 可以将相应的列向左移动。最后按回车键确定。

按大写的 F O 键,然后按 a-z 可以将进程按照相应的列进行排序。而大写的R 键可以将当前的排序倒转。

命令使用

1工具(命令)名称
top
2
.工具(命令)作用
显示系统当前的进程和其他状况;top是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止. 比较准确的说,top命令提供了实时的对系统处理器的状态监视.它将显示系统中CPU敏感的任务列表.该命令可以按CPU使用.内存使用和执行时间对任务进行排序;而且该命令的很多特性都可以通过交互式命令或者在个人定制文件中进行设定.
3
.环境设置
Linux下使用。
4
.使用方法
4
1使用格式
top [-] [d] [p] [q] [c] [C] [S] [s] [n]
4
2参数说明
d
指定每两次屏幕信息刷新之间的时间间隔。当然用户可以使用s交互命令来改变之。
p
通过指定监控进程ID来仅仅监控某个进程的状态。
q
该选项将使top没有任何延迟的进行刷新。如果调用程序有超级用户权限,那么top将以尽可能高的优先级运行。
S
指定累计模式
s
使top命令在安全模式中运行。这将去除交互命令所带来的潜在危险。
i
使top不显示任何闲置或者僵死进程。
c
显示整个命令行而不只是显示命令名
4.3
其他
  下面介绍在top命令执行过程中可以使用的一些交互命令。从使用角度来看,熟练的掌握这些命令比掌握选项还重要一些。这些命令都是单字母的,如果在命令行选项中使用了s选项,则可能其中一些命令会被屏蔽掉。
Ctrl+L 擦除并且重写屏幕。
h或者? 显示帮助画面,给出一些简短的命令总结说明。
k 终止一个进程。系统将提示用户输入需要终止的进程PID,以及需要发送给该进程什么样的信号。一般的终止进程可以使用15信号;如果不能正常结束那就使用信号9强制结束该进程。默认值是信号15。在安全模式中此命令被屏蔽。
i 忽略闲置和僵死进程。这是一个开关式命令。
q 退出程序。
r 重新安排一个进程的优先级别。系统提示用户输入需要改变的进程PID以及需要设置的进程优先级值。输入一个正值将使优先级降低,反之则可以使该进程拥有更高的优先权。默认值是10
S 切换到累计模式。
s 改变两次刷新之间的延迟时间。系统将提示用户输入新的时间,单位为s。如果有小数,就换算成m s。输入0值则系统将不断刷新,默认值是5s。需要注意的是如果设置太小的时间,很可能会引起不断刷新,从而根本来不及看清显示的情况,而且系统负载也会大大增加。
f或者F 从当前显示中添加或者删除项目。
o或者O 改变显示项目的顺序。
l 切换显示平均负载和启动时间信息。
m 切换显示内存信息。
t 切换显示进程和CPU状态信息。
c 切换显示命令名称和完整命令行。
M 根据驻留内存大小进行排序。
P 根据CPU使用百分比大小进行排序。
T 根据时间/累计时间进行排序。
W
将当前设置写入~/.toprc文件中。这是写top配置文件的推荐方法。

 

 

-H线程开关

在终端输入top -H后,top将以上一次系统记得的“H"状态的相反的状态运行。如上次top的H状态是off,则这次运行时H状态就变成on了,H状态是on时,所有的单独的线程都会被显示出来,但是,top是显示一个进程的所有线程的总和。如下图,当把"H”设为off时,top显示的qemu-kvm只有两栏,把"H"设为on时,top显示的qemu-kvm有总共有四栏。

-i 空闲进程开关

以系统记得的“i”状态的相反状态启动top。当开关是off时,空闲的或僵死的任务将不被显示。

-n 设置重复次数

如设置top -n 5则top将屏幕刷新5次后top退出。

-u 以给定的有效的UID或用户名启动top

如在终端中输入top -u wang 则只显示用户wang相关的进程。

-U 和-u差不多,但是-U后可以跟真实的,有效的,保存的和文件系统的UID。

-p 监视指定PID的进程

可以以-p1 -p2 -p67 的格式最多输入20次,也可以以-p 1,2,3,4,5,6,7的格式最多跟20个PID。如果想恢复正常显示,即显示所有进程,不必终止或重启top。按住“=”就可以切换了。

-s 以安全模式操作

-S 累计时间模式切换开关

 

4.OpenMP并行程序设计

 

 

OpenMP并行程序设计(二)

1fork/join并行执行模式的概念

OpenMP是一个编译器指令和库函数的集合,主要是为共享式存储计算机上的并行程序设计使用的。

前面一篇文章中已经试用了OpenMP的一个Parallel for指令。从上篇文章中我们也可以发现OpenMP并行执行的程序要全部结束后才能执行后面的非并行部分的代码。这就是标准的并行模式fork/join式并行模式,共享存储式并行程序就是使用fork/join式并行的。

标准并行模式执行代码的基本思想是,程序开始时只有一个主线程,程序中的串行部分都由主线程执行,并行的部分是通过派生其他线程来执行,但是如果并行部分没有结束时是不会执行串行部分的,如上一篇文章中的以下代码:

int main(int argc, char* argv[])

{

     clock_t t1 =clock();

#pragma omp parallel for

     for ( int j = 0; j < 2; j++ ){

        test();

     }

     clock_t t2 =clock();

     printf("Total time = %d/n", t2-t1);

 

     test();

     return 0;

}

在没有执行完for循环中的代码之前,后面的clock_t t2 = clock();这行代码是不会执行的,如果和调用线程创建函数相比,它相当于先创建线程,并等待线程执行完,所以这种并行模式中在主线程里创建的线程并没有和主线程并行运行。

2OpenMP指令和库函数介绍

下面来介绍OpenMP的基本指令和常用指令的用法,

C/C++中,OpenMP指令使用的格式为

       pragma omp 指令 [子句[子句]…]

前面提到的parallel for就是一条指令,有些书中也将OpenMP指令叫做编译指导语句,后面的子句是可选的。例如:

#pragma omp parallel private(i, j)

parallel 就是指令, private是子句

为叙述方便把包含#pragmaOpenMP指令的一行叫做语句,如上面那行叫parallel语句。

 

OpenMP的指令有以下一些:

       parallel,用在一个代码段之前,表示这段代码将被多个线程并行执行

       for,用于for循环之前,将循环分配到多个线程中并行执行,必须保证每次循环之间无相关性。

       parallel for parallel for语句的结合,也是用在一个for循环之前,表示for循环的代码将被多个线程并行执行。

       sections,用在可能会被并行执行的代码段之前

       parallel sectionsparallelsections两个语句的结合

       critical,用在一段代码临界区之前

       single,用在一段只被单个线程执行的代码段之前,表示后面的代码段将被单线程执行。

      flush

barrier,用于并行区内代码的线程同步,所有线程执行到barrier时要停止,直到所有线程都执行到barrier时才继续往下执行。

atomic,用于指定一块内存区域被制动更新

master,用于指定一段代码块由主线程执行

ordered用于指定并行区域的循环按顺序执行

threadprivate, 用于指定一个变量是线程私有的。

OpenMP除上述指令外,还有一些库函数,下面列出几个常用的库函数:

       omp_get_num_procs, 返回运行本线程的多处理机的处理器个数。

       omp_get_num_threads, 返回当前并行区域中的活动线程个数。

       omp_get_thread_num返回线程号

       omp_set_num_threads设置并行执行代码时的线程个数

omp_init_lock, 初始化一个简单锁

omp_set_lock上锁操作

omp_unset_lock解锁操作,要和omp_set_lock函数配对使用。

omp_destroy_lock omp_init_lock函数的配对操作函数,关闭一个锁

 

OpenMP的子句有以下一些

private, 指定每个线程都有它自己的变量私有副本。

firstprivate指定每个线程都有它自己的变量私有副本,并且变量要被继承主线程中的初值。

lastprivate主要是用来指定将线程中的私有变量的值在并行处理结束后复制回主线程中的对应变量。

reduce用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的运算。

nowait忽略指定中暗含的等待

num_threads指定线程的个数

schedule指定如何调度for循环迭代

shared指定一个或多个变量为多个线程间的共享变量

ordered用来指定for循环的执行要按顺序执行

copyprivate用于single指令中的指定变量为多个线程的共享变量

copyin用来指定一个threadprivate的变量的值要用主线程的值进行初始化。

default用来指定并行处理区域内的变量的使用方式,缺省是shared

3parallel 指令的用法

parallel 是用来构造一个并行块的,也可以使用其他指令如forsections等和它配合使用。

C/C++中,parallel的使用方法如下:

#pragma omp parallel [for | sections] [子句[子句]…]

{

       //代码

}

parallel语句后面要跟一个大括号对将要并行执行的代码括起来。

void main(int argc, char *argv[]) {

#pragma ompparallel

{

             printf(“Hello, World!/n”);

}

}

执行以上代码将会打印出以下结果

Hello, World!

Hello, World!

Hello, World!

Hello, World!

可以看得出parallel语句中的代码被执行了四次,说明总共创建了4个线程去执行parallel语句中的代码。

也可以指定使用多少个线程来执行,需要使用num_threads子句:

void main(int argc, char *argv[]) {

#pragma ompparallel num_threads(8)

{

             printf(“Hello, World!, ThreadId=%d/n”, omp_get_thread_num() );

}

}

执行以上代码,将会打印出以下结果:

Hello, World!, ThreadId = 2

Hello, World!, ThreadId = 6

Hello, World!, ThreadId = 4

Hello, World!, ThreadId = 0

Hello, World!, ThreadId = 5

Hello, World!, ThreadId = 7

Hello, World!, ThreadId = 1

Hello, World!, ThreadId = 3

ThreadId的不同可以看出创建了8个线程来执行以上代码。所以parallel指令是用来为一段代码创建多个线程来执行它的。parallel块中的每行代码都被多个线程重复执行。

和传统的创建线程函数比起来,相当于为一个线程入口函数重复调用创建线程函数来创建线程并等待线程执行完。

 

5. 负载均衡

 

http://blog.csdn.net/honey_yyang/article/details/7848621

 

1.前言

随着互联网信息的爆炸性增长,负载均衡(load balance)已经不再是一个很陌生的话题,顾名思义,负载均衡即是将负载分摊到不同的服务单元,既保证服务的可用性,又保证响应足够快,给用户很好的体验。快速增长的访问量和数据流量催生了各式各样的负载均衡产品,很多专业的负载均衡硬件提供了很好的功能,但却价格不菲,这使得负载均衡软件大受欢迎,nginx就是其中的一个。

nginx第一个公开版本发布于2004年,2011年发布了1.0版本。它的特点是稳定性高、功能强大、资源消耗低,从其目前的市场占有而言,nginx大有与apache抢市场的势头。其中不得不提到的一个特性就是其负载均衡功能,这也成了很多公司选择它的主要原因。本文将从源码的角度介绍nginx的内置负载均衡策略和扩展负载均衡策略,以实际的工业生产为案例,对比各负载均衡策略,为nginx使用者提供参考。

2.   源码剖析

nginx的负载均衡策略可以划分为两大类:内置策略和扩展策略。内置策略包含加权轮询和ip hash,在默认情况下这两种策略会编译进nginx内核,只需在nginx配置中指明参数即可。扩展策略有很多,如fair、通用hash、consistent hash等,默认不编译进nginx内核。由于在nginx版本升级中负载均衡的代码没有本质性的变化,因此下面将以nginx1.0.15稳定版为例,从源码角度分析各个策略。

2.1.          加权轮询(weighted round robin)

轮询的原理很简单,首先我们介绍一下轮询的基本流程。如下是处理一次请求的流程图:

图中有两点需要注意,第一,如果可以把加权轮询算法分为先深搜索和先广搜索,那么nginx采用的是先深搜索算法,即将首先将请求都分给高权重的机器,直到该机器的权值降到了比其他机器低,才开始将请求分给下一个高权重的机器;第二,当所有后端机器都down掉时,nginx会立即将所有机器的标志位清成初始状态,以避免造成所有的机器都处在timeout的状态,从而导致整个前端被夯住。

接下来看下源码。nginx源码的目录结构很清晰,加权轮询所在路径为nginx-1.0.15/src/http/ngx_http_upstream_round_robin.[c|h],在源码的基础上,针对重要的、不易理解的地方我加了注释。首先看下ngx_http_upstream_round_robin.h中的重要声明:

从变量命名中,我们就可以大致猜出其作用。其中,current_weight和weight的区别主要是前者为权重排序的值,随着处理请求会动态的变化,后者是配置值,用于恢复初始状态。

接下来看下轮询的创建过程,代码如下图所示。

这里有个tried变量需要做些说明。tried中记录了服务器当前是否被尝试连接过。他是一个位图。如果服务器数量小于32,则只需在一个int中即可记录下所有服务器状态。如果服务器数量大于32,则需在内存池中申请内存来存储。对该位图数组的使用可参考如下代码:

最后是实际的策略代码,逻辑很简单,代码实现也只有30行,直接上代码。

2.2.          ip hash

ip hash是nginx内置的另一个负载均衡的策略,流程和轮询很类似,只是其中的算法和具体的策略有些变化,如下图所示:

ip hash算法的核心实现如下图:

从代码中可以看出,hash值既与ip有关又与后端机器的数量有关。经过测试,上述算法可以连续产生1045个互异的value,这是该算法的硬限制。对此nginx使用了保护机制,当经过20次hash仍然找不到可用的机器时,算法退化成轮询。因此,从本质上说,ip hash算法是一种变相的轮询算法,如果两个ip的初始hash值恰好相同,那么来自这两个ip的请求将永远落在同一台服务器上,这为均衡性埋下了很深的隐患。

2.3.          fair

fair策略是扩展策略,默认不被编译进nginx内核。其原理是根据后端服务器的响应时间判断负载情况,从中选出负载最轻的机器进行分流。这种策略具有很强的自适应性,但是实际的网络环境往往不是那么简单,因此要慎用。

2.4.          通用hash、一致性hash

这两种也是扩展策略,在具体的实现上有些差别,通用hash比较简单,可以以nginx内置的变量为key进行hash,一致性hash采用了nginx内置的一致性hash环,可以支持memcache。

3.   对比测试

本测试主要为了对比各个策略的均衡性、一致性、容灾性等,从而分析出其中的差异性,并据此给出各自的适用场景。为了能够全面、客观的测试nginx的负载均衡策略,我们采用了两个测试工具、在不同场景下做测试,以此来降低环境对测试结果造成的影响。首先简单介绍测试工具、测试网络拓扑和基本的测试流程。

3.1.          测试工具

3.1.1  easyABC

easyABC是公司内部开发的性能测试工具,采用epool模型实现,简单易上手,可以模拟GET/POST请求,极限情况下可以提供上万的压力,在公司内部得到了广泛的使用。由于被测试对象为反向代理服务器,因此需要在其后端搭建桩服务器,这里用nginx作为桩webserver,提供最基本的静态文件服务。

3.1.2  polygraph

polygraph是一款免费的性能测试工具,以对缓存服务、代理、交换机等方面的测试见长。它有规范的配置语言PGL(Polygraph Language),为软件提供了强大的灵活性。其工作原理如下图所示:

polygraph提供client端和server端,将测试目标nginx放在二者之间,三者之间的网络交互均走http协议,只需配置ip+port即可。client端可以配置虚拟robot的个数以及每个robot发请求的速率,并向代理服务器发起随机的静态文件请求,server端将按照请求的url生成随机大小的静态文件做响应。这也是选用这个测试软件的一个主要原因:可以产生随机的url作为nginx各种hash策略的key。

另外,polygraph还提供了日志分析工具,功能比较丰富,感兴趣的同学可以参考附录中的相关材料。

3.2.          测试环境

本测试运行在5台物理机上,其中被测对象单独搭在一台8核机器上,另外四台4核机器分别搭建了easyABC、webserver桩和polygraph,如下图所示:

3.3.          测试方案

首先介绍下关键的测试指标:

均衡性:是否能够将请求均匀的发送给后端

一致性:同一个key的请求,是否能落到同一台机器

容灾性:当部分后端机器挂掉时,是否能够正常工作

以上述指标为指导,我们针对如下四个测试场景分别用easyABC和polygraph进行测试:

场景1     server_*均正常提供服务;

场景2     server_4挂掉,其他正常;

场景3     server_3、server_4挂掉,其他正常;

场景4     server_*均恢复正常服务。

上述四个场景将按照时间顺序进行,每个场景将建立在上一个场景基础上,被测试对象无需做任何操作,以最大程度模拟实际情况。另外,考虑到测试工具自身的特点,在easyabc上的测试压力在17000左右,polygraph上的测试压力在4000左右。以上测试均保证被测试对象可以正常工作,且无任何notice级别以上(alert/error/warn)的日志出现,在每个场景中记录下server_*的qps用于最后的策略分析。

3.4.          测试结果

表1和图1是轮询策略在两种测试工具下的负载情况。对比在两种测试工具下的测试结果会发现,结果完全一致,因此可以排除测试工具的影响。从图表中可以看出,轮询策略对于均衡性和容灾性都可以做到很好的满足。(点击图片查看大图)

表2和图2是fair策略在两种测试工具下的负载情况。fair策略受环境影响非常大,在排除了测试工具的干扰之后,结果仍然有非常大的抖动。从直观上讲,这完全不满足均衡性。但是从另一个角度出发,恰恰是由于这种自适应性确保了在复杂的网络环境中能够物尽所用。因此,在应用到工业生产中之前,需要在具体的环境中做好测试工作。(点击图片查看大图)

以下图表是各种hash策略,所不同的仅仅是hash key或者是具体的算法实现,因此一起做对比。实际测试中发现,通用hash和一致性hash均存在一个问题:当某台后端的机器挂掉时,原有落到这台机器上的流量会丢失,但是在ip hash中就不存在这样的问题。正如上文中对ip hash源码的分析,当ip hash失效时,会退化为轮询策略,因此不会有丢失流量的情况。从这个层面上说,ip hash也可以看成是轮询的升级版。(点击图片查看大图)

图5为ip hash策略,ip hash是nginx内置策略,可以看做是前两种策略的特例:以来源ip为key。由于测试工具不便于模拟海量ip下的请求,因此这里截取线上实际的情况加以分析,如下图所示:

图5 ip hash策略

图中前1/3使用轮询策略,中间段使用ip hash策略,后1/3仍然是轮询策略。可以明显的看出,ip hash的均衡性存在着很大的问题。原因并不难分析,在实际的网络环境中,有大量的高校出口路由器ip、企业出口路由器ip等网络节点,这些节点带来的流量往往是普通用户的成百上千倍,而ip hash策略恰恰是按照ip来划分流量,因此造成上述后果也就自然而然了。

4.   总结与展望

通过实际的对比测试,我们对nginx各个负载均衡策略进行了验证。下面从均衡性、一致性、容灾性以及适用场景等角度对比各种策略。(点击图片查看大图)

以上从源码和实际的测试数据角度分析说明了nginx负载均衡的策略,并给出了各种策略适合的应用场景。通过本文的分析不难发现,无论哪种策略都不是万金油,在具体的场景下应该选择哪种策略一定程度上依赖于使用者对这些策略的熟悉程度。希望本文的分析和测试数据能够对读者有所帮助,更希望有越来越多、越来越好的负载均衡策略产出。

 

 

回调函数
如果参数是一个函数指针,调用者可以传递一个函数的地址给实现者,让实现者去调用它,
这叫做回调函数callbackfuction
示例:voidfunc(void(*f)(void *),void *p);
调用者:
1 提供一个回调函数,再提供一个准备传给回调函数的参数
2 把回调函数传给参数f,把准备传给回调函数的参数按void *类型传给参数p

实现者:
在适当的时候根据调用者传来的函数指针f调用回调函数,将调用者传来的参数p转交给回调函数,就是调用fp);

 

intel多核平台编程优化大赛报告

http://blog.csdn.net/honey_yyang/article/details/7849986

 

 
代码优化前所需时间:4.765秒 
代码优化后所需时间:0.25秒(保留小数点后7位精度)


前言 
本次优化使用的CPU是Intel Xeon 5130 主频为2.0GHz 同Intel酷睿2一样是基于Core Microarchitecture 的双核处理器。本次优化在Intel的工具帮助下主要针对Core Microarchitecture 系列处理器进行优化。但是由于未知原因,Intel VTuneAnalyzers并不能在该系统下正常工作。所以,所有使用Intel VTuneAnalyzers的测试均使用另外一个奔腾D 820的系统测试。 
第一章主要介绍了程序的串行优化。其中有关于Intel编译器使用,以及Intel Math KernelLibrary使用,Intel VTune Analyzers使用的介绍。在借助Intel工具的帮助下,结合Intel Core Microarchitectured的特性。设计出了针对L1 Cache进行优化的,高效率的串行代码。程序的执行时间从优化前的4.765秒达到了优化后的0.765秒。 
第二章主要介绍了程序的并行化。首先讨论了2种并行算法的优缺点。然后选择了适合本程序的并行算法进行优化。并且在最后分析了并行化时的性能瓶颈。通过并行化,程序达到了0.437秒。 
第三章主要介绍了程序的汇编优化。首先介绍了计算的数学理论。然后介绍了汇编代码的编写。最后进行了性能分析。通过该步优化程序在保留小数点后7位精度的前提下达到了0.312秒的好成绩。并且在Intel酷睿2 E6600 上测试达到了0.25秒。 
附录A 说明了本次报告的目录结构和优化方法。 
附录B 列出了进行本次竞赛所参考的文献。 
目录 
一、串行优化 
1.1 代码的基本修改和优化 
1.2 基于Intel编译器的优化 
1.3 使用Intel VTune Analyzers进行性能分析 
1.3.1 Intel VTune Analyzers概述 
1.3.2 基于SAMPLING方式的分析 
1.3.3 对于本次程序的分析 
1.4 优化computePot函数 
1.5 使用Intel Math Kernel Library 
1.6 根据Cache大小优化Intel Math KernelLibrary调用 
1.7 优化updatePositions函数 
1.8 其他优化以及性能分析 
二、并行优化 
2.1 并行优化概述 
2.2 优化方案一 
2.3 优化方案二 
2.4 并行实现 
2.5 性能分析 
三、汇编级优化 
3.1 优化目标 
3.2 数学理论 
3.3 汇编码实现 
3.4 性能分析 
3.5 总结 
附录A 目录结构和编译方法 
附录B 参考文献 

 

 

6. 在多核CPU下,同一进程下的多个线程可以并行运行吗?

http://bbs.csdn.net/topics/270083226

多线程的概念主要有两种:一种是用户态多线程;一种是内核态多线程
内核态多线程,如楼上所言,在操作系统内核的支持下可以在多核下并行运行;
对于用户态多线程,尽管没有内核的直接支持,但若一个用户态线程对应于内核的一个进程的话(从这个角度,内核还是间接支持的),仍然是可以在多核上并行运行的。
因此,这归结为,用户态多线程的实现技术。
似乎目前Linux上的用户态多线程,就是利用了内核的进程来实现的。

Linux 以前的内核版本并没有提供对线程的支持, 在内核中是用轻量级进程来支持线程的, 这是不符合POSIX标准的.

但在2.4以后的内核实现中, 提供了对线程的支持, 也就是内核开发者重写了内核的线程库. 具体,可以Google一下: Linux NPTL.

如果是内核线程(就是fork出来的,pthread_create2.4后最终也用fork,具体参看其实现),那么可以调度到多cpu,内核支持线程的诱导因素之一就是可以利用多cpu资源进行并行计算;如果是用户线程,那么就不能在多cpu上并行计算了,用户库线程的弊端之一就是不能利用多cpu资源;具体到调度,就是当内核发现本cpu没有任何可运行线程时,就会去别的忙cpu上拽几个下来,当然这是内核自发进行的多cpu调度,作为用户也可以自觉地将线程邦定到具体cpu,更加确定的利用多cpu,当然什么都不是任其发展的,内核不能随便从哪个忙cpu上拽线程,还要看线程愿不愿意(参见linux内核函数cpuset_cpus_allowed),另外还要考虑smt(在intel平台即超线程)的兄弟cpu,还要考虑numa,你总不能让本地cpu的线程跑到遥远的cpu上运行吧.....(还有很多好玩的,自己看代码吧)
  
以上扯的都是linux的实现,是我对代码的总结,感兴趣可自行阅读。至于windows平台,道道就更多了(windows的道道总是能把你搞晕,不过玩玩挺好,挺有趣,比如动态优先级提升之类的),它甚至可以让内核线程运行在一个专门cpu上,别的用户线程分享别的cpulinux也可以做到,但是有意思吗?).....
  
希望能解决lz的问题,如有不明:qq23870617。本人专注操作系统,呵呵,愿结志同道合之士

Linux SMP结构既可以在多个CPU上并行运行多个进程,也可以在多个CPU上并行运行同一进程的多个线程。
对于运行在多CPU上的LINUX来说,每个CPU有一个自己的调度队列。当多个调度队列中的进程数相差超过一定的数值时,内核会自动进行调整,从而使得各CPU上的进程数保持均衡。
另外需要说明的是,Linux下的线程,相当于进程,因为它在内核中有自己的task_struct。其实线程与进程的唯一差别是,线程没有自己独立的虚存空间。
也就是说,如果一个进程创建了一个线程,那么新线程与老的主线程,就相当于两个共享虚存空间的进程。内核的调度程序是以task_struct为单位进行调度的。

还有,创建线程的时候,可以指定该线程绑定到哪个CPU上。

 

 

7.Linux 线程绑核

http://www.cnblogs.com/dongzhiquan/archive/2012/02/16/2354977.html

Linux 下多核CPU知识

1. 在Linux下,如何确认是多核或多CPU:

#cat /proc/cpuinfo

如果有多个类似以下的项目,则为多核或多CPU:

processor  : 0

......

processor  : 1

2. Linux下,如何看每个CPU的使用率:

#top -d 1

之后按下1. 则显示多个CPU

Cpu0  :  1.0%us,  3.0%sy,  0.0%ni,96.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st 
Cpu1  :  0.0%us,  0.0%sy,  0.0%ni,100.0%id, 0.0%wa,  0.0%hi,  0.0%si,  0.0%st

3. 如何察看某个进程在哪个CPU上运行:

#top -d 1

之后按下f.进入topCurrent Fields设置页面:

选中:j:P  = Last used cpu (SMP)

则多了一项:P 显示此进程使用哪个CPU

Sam经过试验发现:同一个进程,在不同时刻,会使用不同CPUCore.这应该是LinuxKernel SMP处理的。

4. 配置Linux Kernel使之支持多Core

内核配置期间必须启用 CONFIG_SMP 选项,以使内核感知 SMP

Processor type and features  ---> Symmetric multi-processingsupport

察看当前LinuxKernel是否支持(或者使用)SMP

#uname -a

5. Kernel 2.6SMP负载平衡:

SMP 系统中创建任务时,这些任务都被放到一个给定的 CPU 运行队列中。通常来说,我们无法知道一个任务何时是短期存在的,何时需要长期运行。因此,最初任务到 CPU 的分配可能并不理想。

为了在 CPU 之间维护任务负载的均衡,任务可以重新进行分发:将任务从负载重的 CPU 上移动到负载轻的 CPU 上。Linux 2.6 版本的调度器使用负载均衡(load balancing)提供了这种功能。每隔200ms,处理器都会检查 CPU的负载是否不均衡;如果不均衡,处理器就会在 CPU之间进行一次任务均衡操作。

这个过程的一点负面影响是新 CPU 的缓存对于迁移过来的任务来说是冷的(需要将数据读入缓存中)。

记住 CPU 缓存是一个本地(片上)内存,提供了比系统内存更快的访问能力。如果一个任务是在某个 CPU 上执行的,与这个任务有关的数据都会被放到这个 CPU 的本地缓存中,这就称为热的。如果对于某个任务来说,CPU 的本地缓存中没有任何数据,那么这个缓存就称为冷的。

不幸的是,保持 CPU 繁忙会出现 CPU 缓存对于迁移过来的任务为冷的情况。

6. 应用程序如何利用多Core:

开发人员可将可并行的代码写入线程,而这些线程会被SMP操作系统安排并发运行。

另外,Sam设想,对于必须顺序执行的代码。可以将其分为多个节点,每个节点为一个thread.并在节点间放置channel.节点间形如流水线。这样也可以大大增强CPU利用率。

 

http://www.cnblogs.com/dongzhiquan/archive/2012/02/16/2354989.html

管理处理器的亲和性(affinity

为什么(3 个原因)以及如何使用硬(相对于软)CPU 亲和性(affinity)

简介: 了解 Linux® 2.6 调度器如何处理 CPU 亲和性(affinity)可以帮助您更好地设计用户空间的应用程序。软亲和性(affinity) 意味着进程并不会在处理器之间频繁迁移,而 硬亲和性(affinity) 则意味着进程需要在您指定的处理器上运行。本文介绍了当前的亲和性(affinity)机制,解释为什么和如何使用亲和性(affinity),并给出了几个样例代码来显示如何使用这种功能。

简单地说,CPU 亲和性(affinity 就是进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。Linux 内核进程调度器天生就具有被称为  CPU 亲和性(affinity 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。

2.6 版本的 Linux 内核还包含了一种机制,它让开发人员可以编程实现 硬 CPU 亲和性(affinity)。这意味着应用程序可以显式地指定进程在哪个(或哪些)处理器上运行。

什么是 Linux 内核硬亲和性(affinity)?

Linux 内核中,所有的进程都有一个相关的数据结构,称为 task_struct。这个结构非常重要,原因有很多;其中与亲和性(affinity)相关度最高的是 cpus_allowed 位掩码。这个位掩码由 n 位组成,与系统中的 n 个逻辑处理器一一对应。具有 4 个物理 CPU 的系统可以有 4 位。如果这些 CPU 都启用了超线程,那么这个系统就有一个 8 位的位掩码。

如果为给定的进程设置了给定的位,那么这个进程就可以在相关的 CPU 上运行。因此,如果一个进程可以在任何 CPU 上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是 1。实际上,这就是 Linux 中进程的缺省状态。

Linux 内核 API 提供了一些方法,让用户可以修改位掩码或查看当前的位掩码:

·        sched_set_affinity() (用来修改位掩码)

·        sched_get_affinity() (用来查看当前的位掩码)

注意,cpu_affinity 会被传递给子线程,因此应该适当地调用 sched_set_affinity。

回页首

为什么应该使用硬亲和性(affinity)?

通常 Linux 内核都可以很好地对进程进行调度,在应该运行的地方运行进程(这就是说,在可用的处理器上运行并获得很好的整体性能)。内核包含了一些用来检测 CPU 之间任务负载迁移的算法,可以启用进程迁移来降低繁忙的处理器的压力。

一般情况下,在应用程序中只需使用缺省的调度器行为。然而,您可能会希望修改这些缺省行为以实现性能的优化。让我们来看一下使用硬亲和性(affinity) 的 3 个原因。

原因 1. 有大量计算要做

基于大量计算的情形通常出现在科学和理论计算中,但是通用领域的计算也可能出现这种情况。一个常见的标志是您发现自己的应用程序要在多处理器的机器上花费大量的计算时间。

原因 2. 您在测试复杂的应用程序

测试复杂软件是我们对内核的亲和性(affinity)技术感兴趣的另外一个原因。考虑一个需要进行线性可伸缩性测试的应用程序。有些产品声明可以在 使用更多硬件 时执行得更好。

我们不用购买多台机器(为每种处理器配置都购买一台机器),而是可以:

·        购买一台多处理器的机器

·        不断增加分配的处理器

·        测量每秒的事务数

·        评估结果的可伸缩性

如果应用程序随着 CPU 的增加可以线性地伸缩,那么每秒事务数和 CPU 个数之间应该会是线性的关系(例如斜线图 —— 请参阅下一节的内容)。这样建模可以确定应用程序是否可以有效地使用底层硬件。

Amdahl 法则

Amdahl 法则是有关使用并行处理器来解决问题相对于只使用一个串行处理器来解决问题的加速比的法则。加速比(Speedup 等于串行执行(只使用一个处理器)的时间除以程序并行执行(使用多个处理器)的时间:

      T(1)

S = ------

      T(j)

     

其中 T(j) 是在使用 j 个处理器执行程序时所花费的时间。

Amdahl 法则说明这种加速比在现实中可能并不会发生,但是可以非常接近于该值。对于通常情况来说,我们可以推论出每个程序都有一些串行的组件。随着问题集不断变大,串行组件最终会在优化解决方案时间方面达到一个上限。

Amdahl 法则在希望保持高 CPU 缓存命中率时尤其重要。如果一个给定的进程迁移到其他地方去了,那么它就失去了利用 CPU 缓存的优势。实际上,如果正在使用的 CPU 需要为自己缓存一些特殊的数据,那么所有其他 CPU 都会使这些数据在自己的缓存中失效。

因此,如果有多个线程都需要相同的数据,那么将这些线程绑定到一个特定的 CPU 上是非常有意义的,这样就确保它们可以访问相同的缓存数据(或者至少可以提高缓存的命中率)。否则,这些线程可能会在不同的 CPU 上执行,这样会频繁地使其他缓存项失效。

原因 3. 您正在运行时间敏感的、决定性的进程

我们对 CPU 亲和性(affinity)感兴趣的最后一个原因是实时(对时间敏感的)进程。例如,您可能会希望使用硬亲和性(affinity)来指定一个 8 路主机上的某个处理器,而同时允许其他 7 个处理器处理所有普通的系统调度。这种做法确保长时间运行、对时间敏感的应用程序可以得到运行,同时可以允许其他应用程序独占其余的计算资源。

下面的样例应用程序显示了这是如何工作的。

如何利用硬亲和性(affinity)

现在让我们来设计一个程序,它可以让 Linux 系统非常繁忙。可以使用前面介绍的系统调用和另外一些用来说明系统中有多少处理器的 API 来构建这个应用程序。实际上,我们的目标是编写这样一个程序:它可以让系统中的每个处理器都繁忙几秒钟。可以从后面的“下载”一节中 下载样例程序

 

http://www.cnblogs.com/dongzhiquan/default.html?page=21

 

http://www.cnblogs.com/dongzhiquan/archive/2012/02/15/2353274.html

CPU亲合力就是指在Linux系统中能够将一个或多个进程绑定到一个或多个处理器上运行.

一个进程的CPU亲合力掩码决定了该进程将在哪个或哪几个CPU上运行.在一个多处理器系统中,设置CPU亲合力的掩码可能会获得更好的性能.

一个CPU的亲合力掩码用一个cpu_set_t结构体来表示一个CPU集合,下面的几个宏分别对这个掩码集进行操作:

   ·CPU_ZERO() 清空一个集合

   ·CPU_SET()CPU_CLR()分别对将一个给定的CPU号加到一个集合或者从一个集合中去掉.

   ·CPU_ISSET()检查一个CPU号是否在这个集合中.
下面两个函数就是用来设置获取线程CPU亲和力状态
    ·sched_setaffinity(pid_t pid, unsigned int cpusetsize,cpu_set_t *mask) 
     
该函数设置进程为pid的这个进程,让它运行在mask所设定的CPU.如果pid的值为0,则表示指定的是当前进程,使当前进程运行在mask所设定的那些CPU.第二个参数cpusetsizemask所指定的数的长度.通常设定为sizeof(cpu_set_t).如果当前pid所指定的进程此时没有运行在mask所指定的任意一个CPU,则该指定的进程会从其它CPU上迁移到mask的指定的一个CPU上运行
    ·sched_getaffinity(pid_t pid, unsigned int cpusetsize,cpu_set_t *mask) 
     
该函数获得pid所指示的进程的CPU位掩码,并将该掩码返回到mask所指向的结构中.即获得指定pid当前可以运行在哪些CPU.同样,如果pid的值为0.也表示的是当前进程.

 

cpu_set_t的定义

# define __CPU_SETSIZE 1024
# define __NCPUBITS (8 * sizeof (__cpu_mask))
typedef unsigned long int __cpu_mask;
# define __CPUELT(cpu) ((cpu) / __NCPUBITS)
# define __CPUMASK(cpu) ((__cpu_mask) 1 << ((cpu) %__NCPUBITS))
typedef struct
{
__cpu_mask __bits[__CPU_SETSIZE / __NCPUBITS];
} cpu_set_t;

# define __CPU_ZERO(cpusetp) \
do { \
unsigned int __i; \
cpu_set_t *__arr = (cpusetp); \
for (__i = 0; __i < sizeof(cpu_set_t) / sizeof (__cpu_mask); ++__i) \
__arr->__bits[__i] = 0; \
} while (0)
# define __CPU_SET(cpu, cpusetp) \
((cpusetp)->__bits[__CPUELT (cpu)] |= __CPUMASK (cpu))
# define __CPU_CLR(cpu, cpusetp) \
((cpusetp)->__bits[__CPUELT (cpu)] &= ~__CPUMASK (cpu))
# define __CPU_ISSET(cpu, cpusetp) \
(((cpusetp)->__bits[__CPUELT (cpu)] & __CPUMASK (cpu)) != 0)

上面几个宏与函数的具体用法:

cpu.c
 
#include<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/sysinfo.h>
#include<unistd.h>
#define __USE_GNU
#include<sched.h>
#include<ctype.h>
#include<string.h>

int main(int argc, char*argv[])
{
        int num =sysconf(_SC_NPROCESSORS_CONF);
        intcreated_thread = 0;
        int myid;
        int i;
        int j = 0;
        cpu_set_t mask;
        cpu_set_t get;
        if (argc!= 2)
        {
                printf("usage : ./cpu num\n");
                exit(1);
        }
        myid = atoi(argv[1]);
        printf("system has %i processor(s). \n", num);
        CPU_ZERO(&mask);
        CPU_SET(myid, &mask);
        if(sched_setaffinity(0, sizeof(mask), &mask) == -1)
        {
                printf("warning: could not set CPU affinity,continuing...\n");
        }
        while (1)
        {
                CPU_ZERO(&get);
                if(sched_getaffinity(0, sizeof(get), &get) == -1)
                {
                        printf("warning: cound not get cpu affinity,continuing...\n");
                }
                for (i = 0; i < num; i++)
                {
                        if (CPU_ISSET(i, &get))
                        {
                                printf("this process %d is running processor :%d\n",getpid(), i);
                        }
                }
        }
        return 0;
}

 

 

http://www.cnblogs.com/dongzhiquan/archive/2012/02/15/2353215.html

Linux 线程绑核

假设业务模型中耗费cpu的分四种类型,(1)网卡中断(2)1个处理网络收发包进程(3)耗费cpu的n个worker进程(4)其他不太耗费cpu的进程

基于1中的 负载均衡是针对进程数,那么(1)(2)大部分时间会出现在cpu0上,(3)的n个进程会随着调度,平均到其他多个cpu上,(4)里的进程也是随着调度分配到各个cpu上;

当发生网卡中断的时候,cpu被打断了,处理网卡中断,那么分配到cpu0上的worker进程肯定是运行不了的

其他cpu上不是太耗费cpu的进程获得cpu时,就算它的时间片很短,它也是要执行的,那么这个时候,你的worker进程还是被影响到了;按照调度逻辑,一种非常恶劣的情况是:(1)(2)(3)的进程全部分配到cpu0上,其他不太耗费cpu的进程数很多,全部分配到cpu1,cpu2,cpu3上。。那么网卡中断发生的时候,你的业务进程就得不到cpu了

如果从业务的角度来说,worker进程运行越多,肯定业务处理越快,人为的将它捆绑到其他负载低的cpu上,肯定能提高worker进程使用cpu的时间

每个cpu都利用起来了,负载会比不绑定的情况下好很多

有效果的原因:

依据《linux内核设计与实现》的42节,人为控制一下cpu的绑定还是有用处地 
    linux的SMP负载均衡是基于进程数的,每个cpu都有一个可执行进程队列(为什么不是线程队列呢??),只有当其中一个cpu的可执行队列里进程数比其他cpu队列进程数多25%时,才会将进程移动到另外空闲cpu上,也就是说cpu0上的进程数应该是比其他cpu上多,但是会在25%以内。

CPU亲和力
    
linux下的进程可以通过sched_setaffinity系统调用设置进程亲和力,限定进程只能在某些特定的CPU上运行。负载均衡必须考虑遵守这个限制(前面也多次提到)。
迁移线程
    
前面说到,在普通进程的load_balance过程中,如果负载不均衡,当前CPU会试图从最繁忙的run_queue中pull几个进程到自己的run_queue来。
    但是如果进程迁移失败呢?当失败达到一定次数的时候,内核会试图让目标CPU主动push几个进程过来,这个过程叫做active_load_balance。这里的“一定次数”也是跟调度域的层次有关的,越低层次,则“一定次数”的值越小,越容易触发active_load_balance。
    这里需要先解释一下,为什么load_balance的过程中迁移进程会失败呢?最繁忙run_queue中的进程,如果符合以下限制,则不能迁移:
    1、进程的CPU亲和力限制了它不能在当前CPU上运行;
    2、进程正在目标CPU上运行(正在运行的进程显然是不能直接迁移的);
    (此外,如果进程在目标CPU上前一次运行的时间距离当前时间很小,那么该进程被cache的数据可能还有很多未被淘汰,则称该进程的cache还是热的。对于cache热的进程,也尽量不要迁移它们。但是在满足触发active_load_balance的条件之前,还是会先试图迁移它们。)
    对于CPU亲和力有限制的进程(限制1),即使active_load_balance被触发,目标CPU也不能把它push过来。所以,实际上,触发active_load_balance的目的是要尝试把当时正在目标CPU上运行的那个进程弄过来(针对限制2)。
    在每个CPU上都会运行一个迁移线程,active_load_balance要做的事情就是唤醒目标CPU上的迁移线程,让它执行active_load_balance的回调函数。在这个回调函数中尝试把原先因为正在运行而未能迁移的那个进程push过来。为什么load_balance的时候不能迁移,active_load_balance的回调函数中就可以了呢?因为这个回调函数是运行在目标CPU的迁移线程上的。一个CPU在同一时刻只能运行一个进程,既然这个迁移线程正在运行,那么期望被迁移的那个进程肯定不是正在被执行的,限制2被打破。
    当然,在active_load_balance被触发,到回调函数在目标CPU上被执行之间,目标CPU上的TASK_RUNNING状态的进程可能发生一些变化,所以回调函数发起迁移的进程未必就只有之前因为限制2而未能被迁移的那一个,可能更多,也可能一个没有。

 

http://blog.chinaunix.net/uid-27714502-id-3515874.html

 

Linux中线程与CPU核的绑定

最近在对项目进行性能优化,由于在多核平台上,所以了解了些进程、线程绑定cpu核的问题,在这里将所学记录一下。

    不管是线程还是进程,都是通过设置亲和性(affinity)来达到目的。对于进程的情况,一般是使用sched_setaffinity这个函数来实现,网上讲的也比较多,这里主要讲一下线程的情况。

   与进程的情况相似,线程亲和性的设置和获取主要通过下面两个函数来实现:

   int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize

const cpu_set_t *cpuset);

   int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,

cpu_set_t *cpuset);

    从函数名以及参数名都很明了,唯一需要点解释下的可能就是cpu_set_t这个结构体了。这个结构体的理解类似于select中的fd_set,可以理解为cpu集,也是通过约定好的宏来进行清除、设置以及判断:

   //初始化,设为空

      void CPU_ZERO (cpu_set_t *set);

      //将某个cpu加入cpu集中

       void CPU_SET (int cpu, cpu_set_t *set);

       //将某个cpu从cpu集中移出

       void CPU_CLR (int cpu, cpu_set_t *set);

       //判断某个cpu是否已在cpu集中设置了

       int CPU_ISSET (int cpu, const cpu_set_t*set);

       cpu集可以认为是一个掩码,每个设置的位都对应一个可以合法调度的 cpu,而未设置的位则对应一个不可调度的 CPU。换而言之,线程都被绑定了,只能在那些对应位被设置了的处理器上运行。通常,掩码中的所有位都被置位了,也就是可以在所有的cpu中调度。      

      以下为测试代码:

点击(此处)折叠或打开

#define_GNU_SOURCE

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<unistd.h>

#include<pthread.h>

#include<sched.h>

 

void *myfun(void *arg)

{

    cpu_set_t mask;

    cpu_set_t get;

    char buf[256];

    int i;

    int j;

    int num = sysconf(_SC_NPROCESSORS_CONF);

    printf("system has %dprocessor(s)\n", num);

 

    for (i = 0; i < num; i++) {

        CPU_ZERO(&mask);

        CPU_SET(i, &mask);

        if (pthread_setaffinity_np(pthread_self(),sizeof(mask), &mask) < 0)      {

            fprintf(stderr, "set threadaffinity failed\n");

        }

        CPU_ZERO(&get);

        if (pthread_getaffinity_np(pthread_self(),sizeof(get), &get) < 0) {

            fprintf(stderr, "get threadaffinity failed\n");

        }

        for (j = 0; j < num; j++) {

            if (CPU_ISSET(j, &get)) {

                printf("thread %d isrunning in processor %d\n", (int)pthread_self(), j);

            }

        }

        j = 0;

        while (j++ < 100000000) {

            memset(buf, 0, sizeof(buf));

        }

    }

    pthread_exit(NULL);

}

 

int main(intargc, char *argv[])

{

   pthread_t tid;

    if (pthread_create(&tid, NULL, (void *)myfun, NULL) != 0) {

        fprintf(stderr, "thread createfailed\n");

        return -1;

    }

    pthread_join(tid, NULL);

    return 0;

}

       这段代码将使myfun线程在所有cpu中依次执行一段时间,在我的四核cpu上,执行结果为  :

       system has 4 processor(s)       

       thread 1095604544 is running inprocessor 0       

       thread 1095604544 is running inprocessor 1       

       thread 1095604544 is running inprocessor 2       

       thread 1095604544 is running inprocessor 3

       在一些嵌入式设备中,运行的进程线程比较单一,如果指定进程线程运行于特定的cpu核,减少进程、线程的核间切换,有可能可以获得更高的性能。

 

http://www.oschina.net/code/snippet_214654_25600

线程绑定核

多线程中将线程绑定到固定的核上效率有所提高。。。

test1.c ~ 3KB     下载(30)     跳至 [1] [全屏预览]

#define _GNU_SOURCE

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <pthread.h>

#include <sched.h>

#include <sys/time.h>

#define DEF_TIME(secB, usecB, secE, usecE) ((secE - secB)*1000000 +(usecE-usecB))

char buf[256];

void *myfun0(void *arg)

{

    cpu_set_t mask;

    cpu_set_t get;

    char buf[256];

    CPU_ZERO(&mask);

    CPU_SET(0,&mask);

   if(pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) <0)

        printf("can not setthread affinity!\n");

    CPU_ZERO(&get);

    if(pthread_getaffinity_np(pthread_self(), sizeof(get), &get) < 0)

        printf("can not getthread affinity!\n");

    if(CPU_ISSET(0, &get))

        printf("i am runningon processor 0\n");

    int index = 0;

    while(index++ < 100000000)

        memset(buf, 0,sizeof(buf));

    pthread_exit(NULL);

}

 

void *myfun1(void *arg)

{

    cpu_set_t mask;

    cpu_set_t get;

    CPU_ZERO(&mask);

    CPU_SET(1,&mask);

   if(pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) <0)

        printf("can not setthread affinity!\n");

    CPU_ZERO(&get);

    if(pthread_getaffinity_np(pthread_self(), sizeof(get), &get) < 0)

        printf("can not getthread affinity!\n");

    if(CPU_ISSET(1, &get))

        printf("i am runningon processor 1\n");

    int index = 0;

    while(index++ < 100000000)

        memset(buf, 0,sizeof(buf));

    pthread_exit(NULL);

}

void *myfun2(void *arg)

{

    int index = 0;

    while(index++ < 100000000)

        memset(buf, 0,sizeof(buf));

}

void *myfun3(void *arg)

{

    int index = 0;

    while(index++ < 100000000)

        memset(buf, 0,sizeof(buf));

}

 

int main(int argc, char* argv[])

{

    struct timeval start, end;

    //struct timezoon tz;

    int cpu_num =sysconf(_SC_NPROCESSORS_CONF);

    if(argc == 1)

    {

        gettimeofday(&start,NULL);

        pthread_t tid0, tid1;

       if(pthread_create(&tid0, NULL, (void*)myfun0, NULL) != 0 || \

           pthread_create(&tid1, NULL, (void*)myfun1, NULL) != 0)

            return -1;

        pthread_join(tid0, NULL);

        pthread_join(tid1, NULL);

        gettimeofday(&end,NULL);

        printf("threadtime:%d\n", DEF_TIME(start.tv_sec, start.tv_usec, end.tv_sec,end.tv_usec));

        return 0;

    }

    else

    {

        pthread_t tid2, tid3;

        gettimeofday(&start,NULL);

       if(pthread_create(&tid2, NULL, (void*)myfun2, NULL) != 0 || \

           pthread_create(&tid3, NULL, (void*)myfun3, NULL) != 0)

            return -1;

        pthread_join(tid2, NULL);

        pthread_join(tid3, NULL);

        gettimeofday(&end,NULL);

        printf("threadtime:%d\n", DEF_TIME(start.tv_sec, start.tv_usec, end.tv_sec,end.tv_usec));

        return 0;

    }

}

 

http://www.360doc.com/content/12/1017/13/7851074_242002818.shtml

Linux 多核系统下绑定进程或线程到指定CPU 核执行 2012-08-25    CPU 绑定,有以下两种方式供大家参考:    1. 命令行参数指定CPU 例:  $ taskset0x00000001 morley   /* morley 程序绑定在 1 CPU CORE 执行 */ 其中 0x00000001 CPU mask,   0x00000004 表示在第 3 CPU CORE 执行 /* 4 代表2进制的 100 所以是 3 CPU CORE . */  0x00000006 表示在第 2,3 号两个CPU CORE 同时执行 /* 6 代表2进制的 110 所以是 23 CPU CORE . */       2. 代码中锁定进程或线程对应的 CPU ID:

例:  #define_GNU_SOURCE    #include <sched.h>#include <stdio.h> #include <errno.h>    int main(void) {  cpu_set_t *mask; size_t size; int i;  int nrcpus = 1024; /* 此处为系统可供使用的 CPU CORE 总数*/    realloc:          mask = CPU_ALLOC(nrcpus);          size = CPU_ALLOC_SIZE(nrcpus);         CPU_ZERO_S(size, mask);          if ( sched_getaffinity(0, size, mask)== -1 ) {                 CPU_FREE(mask);                  if (errno == EINVAL&&nrcpus < (1024 << 8)) {                 nrcpus = nrcpus <<2;                 goto realloc;                 }                 perror("sched_getaffinity");                 return -1;         }   for ( i = 0; i < nrcpus; i++ ) {

if ( CPU_ISSET_S(i, size, mask) ) {   /* i 改为相应的进程号或者线程号,即可将 i 号进程或线程绑定在 i CPU CORE */         printf("CPU %d is set\n", (i+1));  /* 此处插入第 i 进程或线程对应的函数或代码 */        } }    CPU_FREE(mask); return 0; }

http://www.cnblogs.com/zackyang/archive/2012/02/08/2342141.html

Linux技巧:多核下绑定硬件进程到不同CPU

硬件中断发生频繁,是件很消耗 CPU 资源的事情,在多核 CPU 条件下如果有办法把大量硬件中断分配给不同的 CPU (core) 处理显然能很好的平衡性能。现在的服务器上动不动就是多 CPU 多核、多网卡、多硬盘,如果能让网卡中断独占1 CPU (core)、磁盘 IO 中断独占1 CPU 的话将会大大减轻单一 CPU 的负担、提高整体处理效率。我前天收到一位网友的邮件提到了 SMP IRQ Affinity,引发了今天的话题。以下操作在 SUN FIre X2100 M2 服务器+ 64位版本 CentOS 5.5 + Linux 2.6.18-194.3.1.el5 上执行。

  什么是中断

  中文教材上对中断的定义太生硬了,简单的说就是,每个硬件设备(如:硬盘、网卡等)都需要和 CPU 有某种形式的通信以便 CPU 及时知道发生了什么事情,这样 CPU 可能就会放下手中的事情去处理应急事件,硬件设备主动打扰 CPU 的现象就可称为硬件中断,就像你正在工作的时候受到 QQ 干扰一样,一次 QQ 摇头就可以被称为中断。

  中断是一种比较好的 CPU 和硬件沟通的方式,还有一种方式叫做轮询(polling),就是让 CPU 定时对硬件状态进行查询然后做相应处理,就好像你每隔5分钟去检查一下 QQ 看看有没有人找你一样,这种方式是不是很浪费你(CPU)的时间?所以中断是硬件主动的方式,比轮询(CPU 主动)更有效一些。

  好了,这里又有了一个问题,每个硬件设备都中断,那么如何区分不同硬件呢?不同设备同时中断如何知道哪个中断是来自硬盘、哪个来自网卡呢?这个很容易,不是每个 QQ 号码都不相同吗?同样的,系统上的每个硬件设备都会被分配一个 IRQ 号,通过这个唯一的 IRQ 号就能区别张三和李四了。

  在计算机里,中断是一种电信号,由硬件产生,并直接送到中断控制器( 8259A)上,然后再由中断控制器向 CPU 发送信号,CPU 检测到该信号后,就中断当前的工作转而去处理中断。然后,处理器会通知操作系统已经产生中断,这样操作系统就会对这个中断进行适当的处理。现在来看一下中断控制器,常见的中断控制器有两种:可编程中断控制器 8259A 和高级可编程中断控制器(APIC),中断控制器应该在大学的硬件接口和计算机体系结构的相关课程中都学过。传统的 8259A 只适合单 CPU 的情况,现在都是多 CPU 多核的 SMP 体系,所以为了充分利用 SMP 体系结构、把中断传递给系统上的每个 CPU 以便更好实现并行和提高性能,Intel 引入了高级可编程中断控制器(APIC)

  光有高级可编程中断控制器的硬件支持还不够,Linux 内核还必须能利用到这些硬件特质,所以只有 kernel 2.4 以后的版本才支持把不同的硬件中断请求(IRQs)分配到特定的 CPU 上,这个绑定技术被称为 SMP IRQ Affinity. 更多介绍请参看 Linux 内核源代码自带的文档:linux-2.6.31.8/Documentation/IRQ-affinity.txt

  如何使用

  先看看系统上的中断是怎么分配在 CPU 上的,很显然 CPU0 上处理的中断多一些:

  # cat /proc/interrupts

  CPU0 CPU1

  0: 918926335 0IO-APIC-edge timer

  1: 2 0 IO-APIC-edgei8042

  8: 0 0IO-APIC-edge rtc

  9: 0 0IO-APIC-level acpi

  12: 4 0IO-APIC-edge i8042

  14: 8248017 0IO-APIC-edge ide0

  50: 194 0IO-APIC-level ohci_hcd:usb2

  58: 31673 0IO-APIC-level sata_nv

  90: 1070374 0PCI-MSI eth0

  233: 10 0 IO-APIC-levelehci_hcd:usb1

  NMI: 5077 2032

  LOC: 918809969918809894

  ERR: 0

  MIS: 0

  为了不让 CPU0 很累怎么把部分中断转移到 CPU1 上呢?或者说如何把 eth0 网卡的中断转到 CPU1 上呢?先查看一下 IRQ 90 中断的 smp affinity,看看当前中断是怎么分配在不同 CPU 上的(ffffffff 意味着分配在所有可用 CPU 上):

  # cat/proc/irq/90/smp_affinity

  7fffffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff

  在进一步动手之前我们需要先停掉 IRQ 自动调节的服务进程,这样才能手动绑定 IRQ 到不同 CPU,否则自己手动绑定做的更改将会被自动调节进程给覆盖掉。如果想修改 IRQ 90 的中断处理,绑定到第2 CPU(CPU1)

  #/etc/init.d/irqbalance stop

  # echo "2"> /proc/irq/90/smp_affinity

  过段时间在看 /proc/interrupts,是不是 90:eth0 CPU1 上的中断增加了(145)、在 CPU0 上的中断没变?不断打印 /proc/interrupts 就会发现 eth0 CPU0 上的中断数始终保持不变,而在 CPU1 上的中断数是持续增加的,这正是我们想要的结果:

  # cat/proc/interrupts

  CPU0 CPU1

  0: 922506515 0IO-APIC-edge timer

  1: 2 0 IO-APIC-edgei8042

  8: 0 0IO-APIC-edge rtc

  9: 0 0IO-APIC-level acpi

  12: 4 0IO-APIC-edge i8042

  14: 8280147 0IO-APIC-edge ide0

  50: 194 0IO-APIC-level ohci_hcd:usb2

  58: 31907 0IO-APIC-level sata_nv

  90: 1073399 145PCI-MSI eth0

  233: 10 0 IO-APIC-levelehci_hcd:usb1

  NMI: 5093 2043

  LOC: 922389696922389621

  ERR: 0

  MIS: 0

  有什么用

  在网络非常 heavy 的情况下,对于文件服务器、高流量 Web 服务器这样的应用来说,把不同的网卡 IRQ 均衡绑定到不同的 CPU 上将会减轻某个 CPU 的负担,提高多个 CPU 整体处理中断的能力;对于数据库服务器这样的应用来说,把磁盘控制器绑到一个 CPU、把网卡绑定到另一个 CPU 将会提高数据库的响应时间、优化性能。合理的根据自己的生产环境和应用的特点来平衡 IRQ 中断有助于提高系统的整体吞吐能力和性能。

  本人经常收到网友来信问到如何优化 Linux、优化 VPS、这个问题不太好回答,要记住的是性能优化是一个过程而不是结果,不是看了些文档改了改参数就叫优化了,后面还需要大量的测试、监测以及持续的观察和改进。

  绑定进程到不同CPU

  介绍了在 Linux 多核下如何绑定硬件中断到不同 CPU,其实也可以用类似的做法把进程手动分配到特定的 CPU 上,平时在 Linux 上运行的各种进程都是由 Linux 内核统一分配和管理的,由进程调度算法来决定哪个进程可以开始使用 CPU、哪个进程需要睡眠或等待、哪个进程运行在哪个 CPU 上等。如果你对操作系统的内核和进程调度程序感兴趣的话,不妨看看那本经典的 Operating SystemsDesign and Implementation(Linus Torvalds 就是看了这本书受到启发写出了 Linux),从简单的 Minix 入手,hack 内核是件很有意思的事情,本人以前修改过 Minix 内核的进程调度,学到了内核方面的很多东西。另外推荐一本课外读物:Just for Fun,Linus Torvalds 写的一本自传。

  Linux 给我们提供了方便的工具用来手动分配进程到不同的 CPU (CPU Affinity),这样我们可以按照服务器和应用的特性来安排特定的进程到特定的 CPU 上,比如 Oracle 要消耗大量 CPU I/O 资源,如果我们能分配 Oracle 进程到某个或多个 CPU 上并由这些 CPU 专门处理 Oracle 的话会毫无疑问的提高应用程序的响应和性能。还有一些特殊情况是必须绑定应用程序到某个 CPU 上的,比如某个软件的授权是单 CPU 的,如果想运行在多 CPU 机器上的话就必须限制这个软件到某一个 CPU 上。

  安装 schedutils

  在 CentOS/Fedora 下安装 schedutils:

  # yum installschedutils

  在 Debian/Ubuntu 下安装 schedutils:

  # apt-get installschedutils

  如果正在使用CentOS/Fedora/Debian/Ubuntu 的最新版本的话,schedutils/util-linux这个软件包可能已经装上了。

  计算 CPU Affinity 和计算 SMP IRQ Affinity 差不多:

  0x00000001 (CPU0)

  0x00000002 (CPU1)

  0x00000003(CPU0+CPU1)

  0x00000004 (CPU2)

  ...

  使用 schedutils

  如果想设置进程号(PID)为 12212 的进程到 CPU0 上的话:

# taskset 0x00000001-p 12212

http://www.csdn.net/article/2012-06-21/2806814

性能调优攻略

二、系统性能测试

经过上述的说明,我们知道要测试系统的性能,需要我们收集系统的ThroughputLatency这两个值。

·        首先,需要定义Latency这个值,比如说,对于网站系统响应时间必需是5秒以内(对于某些实时系统可能需要定义的更短,比如5ms以内,这个更根据不同的业务来定义)

·        其次,开发性能测试工具,一个工具用来制造高强度的Throughput,另一个工具用来测量Latency。对于第一个工具,你可以参考一下十个免费的Web压力测试工具,关于如何测量Latency,你可以在代码中测量,但是这样会影响程序的执行,而且只能测试到程序内部的Latency,真正的Latency是整个系统都算上,包括操作系统和网络的延时,你可以使用Wireshark来抓网络包来测量。这两个工具具体怎么做,这个还请大家自己思考去了。

·        最后,开始性能测试。你需要不断地提升测试的Throughput,然后观察系统的负载情况,如果系统顶得住,那就观察Latency的值。这样,你就可以找到系统的最大负载,并且你可以知道系统的响应延时是多少。

再多说一些

·        关于Latency,如果吞吐量很少,这个值估计会非常稳定,当吞吐量越来越大时,系统的Latency会出现非常剧烈的抖动,所以,我们在测量Latency的时候,我们需要注意到Latency的分布,也就是说,有百分之几的在我们允许的范围,有百分之几的超出了,有百分之几的完全不可接受。也许,平均下来的Latency达标了,但是其中仅有50%的达到了我们可接受的范围。那也没有意义。

·        关于性能测试,我们还需要定义一个时间段。比如:在某个吞吐量上持续15分钟。因为当负载到达的时候,系统会变得不稳定,当过了一两分钟后,系统才会稳定。另外,也有可能是,你的系统在这个负载下前几分钟还表现正常,然后就不稳定了,甚至垮了。所以,需要这么一段时间。这个值,我们叫做峰值极限。

·        性能测试还需要做Soak Test,也就是在某个吞吐量下,系统可以持续跑一周甚至更长。这个值,我们叫做系统的正常运行的负载极限。

性能测试有很多很复要的东西,比如:burst test等。这里不能一一详述,这里只说了一些和性能调优相关的东西。总之,性能测试是一细活和累活。

三、定位性能瓶颈

有了上面的铺垫,我们就可以测试到到系统的性能了,再调优之前,我们先来说说如何找到性能的瓶颈。我见过很多朋友会觉得这很容易,但是仔细一问,其实他们并没有一个比较系统的方法。

3.1查看操作系统负载

首先,当我们系统有问题的时候,我们不要急于去调查我们代码,这个毫无意义。我们首要需要看的是操作系统的报告。看看操作系统的CPU利用率,看看内存使用率,看看操作系统的IO,还有网络的IO,网络链接数,等等。Windows下的perfmon是一个很不错的工具,Linux下也有很多 相关的命令和工具,比如:SystemTapLatencyTOPvmstatsariostattoptcpdump等等。通过观察这些数据,我们就可以知道我们的软件的性能基本上出在哪里。比如:

1)先看CPU利用率,如果CPU利用率不高,但是系统的ThroughputLatency上不去了,这说明我们的程序并没有忙于计算,而是忙于别的一些事,比如IO。(另外,CPU的利用率还要看内核态的和用户态的,内核态的一上去了,整个系统的性能就下来了。而对于多核CPU来说,CPU 0是相当关键的,如果CPU 0的负载高,那么会影响其它核的性能,因为CPU各核间是需要有调度的,这靠CPU0完成)

2)然后,我们可以看一下IO大不大,IOCPU一般是反着来的,CPU利用率高则IO不大,IO大则CPU就小。关于IO,我们要看三个事,一个是磁盘文件IO,一个是驱动程序的IO(如:网卡),一个是内存换页率。这三个事都会影响系统性能。

3)然后,查看一下网络带宽使用情况,在Linux下,你可以使用iftopiptrafntoptcpdump这些命令来查看。或是用Wireshark来查看。

4)如果CPU不高,IO不高,内存使用不高,网络带宽使用不高。但是系统的性能上不去。这说明你的程序有问题,比如,你的程序被阻塞了。可能是因为等那个锁,可能是因为等某个资源,或者是在切换上下文。

通过了解操作系统的性能,我们才知道性能的问题,比如:带宽不够,内存不够,TCP缓冲区不够,等等,很多时候,不需要调整程序的,只需要调整一下硬件或操作系统的配置就可以了。

常见的系统瓶颈

下面这些东西是我所经历过的一些问题,也许并不全,也许并不对,大家可以补充指正,我纯属抛砖引玉。关于系统架构方面的性能调优,大家可移步看一下《由12306.cn谈谈网站性能技术》,关于Web方面的一些性能调优的东西,大家可以看看《Web开发中需要了解的东西》一文中的性能一章。我在这里就不再说设计和架构上的东西了。

一般来说,性能优化也就是下面的几个策略:

·        用空间换时间。各种cacheCPU L1/L2/RAM到硬盘,都是用空间来换时间的策略。这样策略基本上是把计算的过程一步一步的保存或缓存下来,这样就不用每次用的时候都要再计算一遍,比如数据缓冲,CDN,等。这样的策略还表现为冗余数据,比如数据镜象,负载均衡什么的。

·        用时间换空间。有时候,少量的空间可能性能会更好,比如网络传输,如果有一些压缩数据的算法(如前些天说的“Huffman编码压缩算法“rsync的核心算法),这样的算法其实很耗时,但是因为瓶颈在网络传输,所以用时间来换空间反而能省时间。

·        简化代码。最高效的程序就是不执行任何代码的程序,所以,代码越少性能就越高。关于代码级优化的技术大学里的教科书有很多示例了。如:减少循环的层数,减少递归,在循环中少声明变量,少做分配和释放内存的操作,尽量把循环体内的表达式抽到循环外,条件表达的中的多个条件判断的次序,尽量在程序启动时把一些东西准备好,注意函数调用的开销(栈上开销),注意面向对象语言中临时对象的开销,小心使用异常(不要用异常来检查一些可接受可忽略并经常发生的错误),等等,这连东西需要我们非常了解编程语言和常用的库。

·        并行处理。如果CPU只有一个核,你要玩多进程,多线程,对于计算密集型的软件会反而更慢(因为操作系统调度和切换开销很大),CPU的核多了才能真正体现出多进程多线程的优势。并行处理需要我们的程序有Scalability,不能水平或垂直扩展的程序无法进行并行处理。从架构上来说,这表再为——是否可以做到不改代码只是加加机器就可以完成性能提升?

总之,根据28原则来说,20%的代码耗了你80%的性能,找到那20%的代码,你就可以优化那80%的性能下面的一些东西都是我的一些经验,我只例举了一些最有价值的性能调优的的方法,供你参考,也欢迎补充。

4.1算法调优

算法非常重要,好的算法会有更好的性能。举几个我经历过的项目的例子,大家可以感觉一下。

·        一个是过滤算法。系统需要对收到的请求做过滤,我们把可以被filter in/out的东西配置在了一个文件中,原有的过滤算法是遍历过滤配置,后来,我们找到了一种方法可以对这个过滤配置进行排序,这样就可以用二分折半的方法来过滤,系统性能增加了50%

·        一个是哈希算法。计算哈希算法的函数并不高效,一方面是计算太费时,另一方面是碰撞太高,碰撞高了就跟单向链表一个性能(可参看Hash Collision DoS 问题)。我们知道,算法都是和需要处理的数据很有关系的,就算是被大家所嘲笑的冒泡排序在某些情况下(大多数数据是排好序的)其效率会高于所有的排序算法。哈希算法也一样,广为人知的哈希算法都是用英文字典做测试,但是我们的业务在数据有其特殊性,所以,对于还需要根据自己的数据来挑选适合的哈希算法。对于我以前的一个项目,公司内某牛人给我发来了一个哈希算法,结果让我们的系统性能上升了150%。(关于各种哈希算法,你一定要看看StackExchange上的这篇关于各种hash算法的文章

·        分而治之和预处理。以前有一个程序为了生成月报表,每次都需要计算很长的时间,有时候需要花将近一整天的时间。于是我们把我们找到了一种方法可以把这个算法发成增量式的,也就是说我每天都把当天的数据计算好了后和前一天的报表合并,这样可以大大的节省计算时间,每天的数据计算量只需要20分钟,但是如果我要算整个月的,系统则需要10个小时以上(SQL语句在大数据量面前性能成级数性下降)。这种分而治之的思路在大数据面前对性能有很帮助,就像merge排序一样。SQL语句和数据库的性能优化也是这一策略,如:使用嵌套式的Select而不是笛卡尔积的Select,使用视图,等等。

代码调优

·        字符串操作。这是最费系统性能的事了,无论是strcpystrcat还是strlen,最需要注意的是字符串子串匹配。所以,能用整型最好用整型。举几个例子,第一个例子是N年前做银行的时候,我的同事喜欢把日期存成字符串(如:2012-05-2908:30:02),我勒个去,一个select wherebetween语句相当耗时。另一个例子是,我以前有个同事把一些状态码用字符串来处理,他的理由是,这样可以在界面上直接显示,后来性能调优的时候,我把这些状态码全改成整型,然后用位操作查状态,因为有一个每秒钟被调用了150K次的函数里面有三处需要检查状态,经过改善以后,整个系统的性能上升了30%左右。还有一个例子是,我以前从事的某个产品编程规范中有一条是要在每个函数中把函数名定义出来,如:const char fname[]=”functionName()”,这是为了好打日志,但是为什么不声明成static类型的呢?

·        多线程调优。有人说,thread is evil,这个对于系统性能在某些时候是个问题。因为多线程瓶颈就在于互斥和同步的锁上,以及线程上下文切换的成本,怎么样的少用锁或不用锁是根本(比如:多版本并发控制(MVCC)在分布式系统中的应用中说的乐观锁可以解决性能问题),此外,还有读写锁也可以解决大多数是读操作的并发的性能问题。这里多说一点在C++中,我们可能会使用线程安全的智能指针AutoPtr或是别的一些容器,只要是线程安全的,其不管三七二十一都要上锁,上锁是个成本很高的操作,使用AutoPtr会让我们的系统性能下降得很快,如果你可以保证不会有线程并发问题,那么你应该不要用AutoPtr。我记得我上次我们同事去掉智能指针的引用计数,让系统性能提升了50%以上。对于Java对象的引用计数,如果我猜的没错的话,到处都是锁,所以,Java的性能问题一直是个问题。另外,线程不是越多越好,线程间的调度和上下文切换也是很夸张的事,尽可能的在一个线程里干,尽可能的不要同步线程。这会让你有很多的性能。

·        内存分配。不要小看程序的内存分配。malloc/realloc/calloc这样的系统调非常耗时,尤其是当内存出现碎片的时候。我以前的公司出过这样一个问题——在用户的站点上,我们的程序有一天不响应了,用GDB跟进去一看,系统hang在了malloc操作上,20秒都没有返回,重启一些系统就好了。这就是内存碎片的问题。这就是为什么很多人抱怨STL有严重的内存碎片的问题,因为太多的小内存的分配释放了。有很多人会以为用内存池可以解决这个问题,但是实际上他们只是重新发明了Runtime-C或操作系统的内存管理机制,完全于事无补。当然解决内存碎片的问题还是通过内存池,具体来说是一系列不同尺寸的内存池(这个留给大家自己去思考)。当然,少进行动态内存分配是最好的。说到内存池就需要说一下池化技术。比如线程池,连接池等。池化技术对于一些短作业来说(如http服务)相当相当的有效。这项技术可以减少链接建立,线程创建的开销,从而提高性能。

·        异步操作。我们知道Unix下的文件操作是有blocknon-block的方式的,像有些系统调用也是block式的,如:Socket下的selectWindows下的WaitforObject之类的,如果我们的程序是同步操作,那么会非常影响性能,我们可以改成异步的,但是改成异步的方式会让你的程序变复杂。异步方式一般要通过队列,要注间队列的性能问题,另外,异步下的状态通知通常是个问题,比如消息事件通知方式,有callback方式,等,这些方式同样可能会影响你的性能。但是通常来说,异步操作会让性能的吞吐率有很大提升(Throughput),但是会牺牲系统的响应时间(latency)。这需要业务上支持。

·        语言和代码库。我们要熟悉语言以及所使用的函数库或类库的性能。比如:STL中的很多容器分配了内存后,那怕你删除元素,内存也不会回收,其会造成内存泄露的假像,并可能造成内存碎片问题。再如,STL某些容器的size()==0empty()是不一样的,因为,size()O(n)复杂度,empty()O(1)的复杂度,这个要小心。Java中的JVM调优需要使用的这些参数:-Xms-Xmx-Xmn-XX:SurvivorRatio-XX:MaxTenuringThreshold,还需要注意JVMGCGC的霸气大家都知道,尤其是full GC(还整理内存碎片),他就像恐龙特级克赛号一样,他运行的时候,整个世界的时间都停止了。

·        网卡调优

·        对于网卡,我们也是可以调优的,这对于千兆以及网网卡非常必要,在Linux下,我们可以用ifconfig查看网上的统计信息,如果我们看到overrun上有数据,我们就可能需要调整一下txqueuelen的尺寸(一般默认为1000),我们可以调大一些,如:ifconfig eth0 txqueuelen 5000Linux下还有一个命令叫:ethtool可以用于设置网卡的缓冲区大小。Windows下,我们可以在网卡适配器中的高级选项卡中调整相关的参数(如:Receive Buffers, Transmit Buffer等,不同的网卡有不同的参数)。Buffer调大对于需要大数据量的网络传输非常有效。

·        其它网络性能

·        关于多路复用技术,也就是用一个线程来管理所有的TCP链接,有三个系统调用要重点注意:一个是select,这个系统调用只支持上限1024个链接,第二个是poll,其可以突破1024的限制,但是selectpoll本质上是使用的轮询机制,轮询机制在链接多的时候性能很差,因主是O(n)的算法,所以,epoll出现了,epoll是操作系统内核支持的,仅当在链接活跃时,操作系统才会callback,这是由操作系统通知触发的,但其只有LinuxKernel 2.6以后才支持(准确说是2.5.44中引入的),当然,如果所有的链接都是活跃的,过多的使用epoll_ctl可能会比轮询的方式还影响性能,不过影响的不大

·        另外,关于一些和DNS Lookup的系统调用要小心,比如:gethostbyaddr/gethostbyname,这个函数可能会相当的费时,因为其要到网络上去找域名,因为DNS的递归查询,会导致严重超时,而又不能通过设置什么参数来设置time out,对此你可以通过配置hosts文件来加快速度,或是自己在内存中管理对应表,在程序启动时查好,而不要在运行时每次都查。另外,在多线程下面,gethostbyname会一个更严重的问题,就是如果有一个线程的gethostbyname发生阻塞,其它线程都会在gethostbyname处发生阻塞,这个比较变态,要小心。(你可以试试GNUgethostbyname_r()这个的性能要好一些)这种到网上找信息的东西很多,比如,如果你的Linux使用了NIS,或是NFS,某些用户或文件相关的系统调用就很慢,所以要小心。

·        系统调优

·        AI/O模型

·        前面说到过select/poll/epoll这三个系统调用,我们都知道,Unix/Linux下把所有的设备都当成文件来进行I/O,所以,那三个操作更应该算是I/O相关的系统调用。说到I/O模型,这对于我们的I/O性能相当重要,我们知道,Unix/Linux经典的I/O方式是(关于Linux下的I/O模型,大家可以读一下这篇文章《使用异步I/O大大提高性能》):

·        第一种,同步阻塞式I/O,这个不说了

·        第二种,同步无阻塞方式。其通过fctnl设置O_NONBLOCK来完成。

·        第三种,对于select/poll/epoll这三个是I/O不阻塞,但是在事件上阻塞,算是:I/O异步,事件同步的调用。

·        第四种,AIO方式。这种I/O模型是一种处理与I/O并行的模型。I/O请求会立即返回,说明请求已经成功发起了。在后台完成I/O操作时,向应用程序发起通知,通知有两种方式:一种是产生一个信号,另一种是执行一个基于线程的回调函数来完成这次I/O处理过程。

·        第四种因为没有任何的阻塞,无论是I/O上,还是事件通知上,所以,其可以让你充分地利用CPU,比起第二种同步无阻塞好处就是,第二种要你一遍一遍地去轮询。Nginx之所所以高效,是其使用了epollAIO的方式来进行I/O的。

·        再说一下Windows下的I/O模型,

·        a)一个是WriteFile系统调用,这个系统调用可以是同步阻塞的,也可以是同步无阻塞的,关于看文件是不是以Overlapped打开的。关于同步无阻塞,需要设置其最后一个参数Overlapped,微软叫OverlappedI/O,你需要WaitForSingleObject才能知道有没有写完成。这个系统调用的性能可想而知。

·        b)另一个叫WriteFileEx的系统调用,其可以实现异步I/O,并可以让你传入一个callback函数,等I/O结束后回调之,但是这个回调的过程Windows是把callback函数放到了APCAsynchronousProcedure Calls)的队列中,然后,只用当应用程序当前线程成为可被通知状态(Alterable)时,才会被回调。只有当你的线程使用了这几个函数时WaitForSingleObjectExWaitForMultipleObjectsEx MsgWaitForMultipleObjectsExSignalObjectAndWait SleepEx,线程才会成为Alterable状态。可见,这个模型,还是有wait,所以性能也不高。

·        c)然后是IOCP–IOCompletion PortIOCP会把I/O的结果放在一个队列中,但是,侦听这个队列的不是主线程,而是专门来干这个事的一个或多个线程去干(老的平台要你自己创建线程,新的平台是你可以创建一个线程池)。IOCP是一个线程池模型。这个和Linux下的AIO模型比较相似,但是实现方式和使用方式完全不一样。

·        当然,真正提高I/O性能方式是把和外设的I/O的次数降到最低,最好没有,所以,对于读来说,内存cache通常可以从质上提升性能,因为内存比外设快太多了。对于写来说,cache住要写的数据,少写几次,但是cache带来的问题就是实时性的问题,也就是latency会变大,我们需要在写的次数上和相应上做权衡。

多核CPU调优

关于CPU的多核技术,我们知道,CPU0是很关键的,如果0CPU被用得过狠的话,别的CPU性能也会下降,因为CPU0是有调整功能的,所以,我们不能任由操作系统负载均衡,因为我们自己更了解自己的程序,所以,我们可以手动地为其分配CPU核,而不会过多地占用CPU0,或是让我们关键进程和一堆别的进程挤在一起。

·        对于Windows来说,我们可以通过任务管理器中的进程而中右键菜单中的设置相关性……”Set Affinity…)来设置并限制这个进程能被运行在哪些核上。

·        对于Linux来说,可以使用taskset命令来设置(你可以通过安装schedutils来安装这个命令:apt-get installschedutils

多核CPU还有一个技术叫NUMA技术(Non-Uniform MemoryAccess)。传统的多核运算是使用SMP(SymmetricMulti-Processor )模式,多个处理器共享一个集中的存储器和I/O总线。于是就会出现一致存储器访问的问题,一致性通常意味着性能问题。NUMA模式下,处理器被划分成多个node,每个node有自己的本地存储器空间。关于NUMA的一些技术细节,你可以查看一下这篇文章《LinuxNUMA技术》,在Linux下,对NUMA调优的命令是:numactl 。如下面的命令:(指定命令“myprogram arg1arg2”运行在node 0上,其内存分配在node 0 1上)

1.  numactl --cpubind=0 --membind=0,1 myprogram arg1 arg2 

当然,上面这个命令并不好,因为内存跨越了两个node,这非常不好。最好的方式是只让程序访问和自己运行一样的node,如:

1.  $ numactl --membind 1 --cpunodebind 1 --localalloc myapplication 

C)文件系统调优

关于文件系统,因为文件系统也是有cache的,所以,为了让文件系统有最大的性能。首要的事情就是分配足够大的内存,这个非常关键,在Linux下可以使用free命令来查看free/used/buffers/cached,理想来说,bufferscached应该有40%左右。然后是一个快速的硬盘控制器,SCSI会好很多。最快的是Intel SSD固态硬盘,速度超快,但是写次数有限。

接下来,我们就可以调优文件系统配置了,对于LinuxExt3/4来说,几乎在所有情况下都有所帮助的一个参数是关闭文件系统访问时间,在/etc/fstab下看看你的文件系统有没有noatime参数(一般来说应该有),还有一个是dealloc,它可以让系统在最后时刻决定写入文件发生时使用哪个块,可优化这个写入程序。还要注间一下三种日志模式:data=journaldata=ordereddata=writeback。默认设置data=ordered提供性能和防护之间的最佳平衡。

当然,对于这些来说,ext4的默认设置基本上是最佳优化了。

这里介绍一个Linux下的查看I/O的命令——iotop,可以让你看到各进程的磁盘读写的负载情况。

其它还有一些关于NFSXFS的调优,大家可以上google搜索一些相关优化的文章看看。关于各文件系统,大家可以看一下这篇文章——Linux日志文件系统及性能分析》。

·         

http://linux.chinaunix.net/docs/2006-10-11/2870.shtml

使用异步 I/O 大大提高应用程序的性能 

日期:2006-10-11 作者:M. Tim Jones 来自:IBM DW 中国


Linux® 中最常用的输入/输出(I/O)模型是同步 I/O。在这个模型中,当请求发出之后,应用程序就会阻塞,直到请求满足为止。这是很好的一种解决方案,因为调用应用程序在等待 I/O 请求完成时不需要使用任何中央处理单元(CPU)。但是在某些情况中,I/O 请求可能需要与其他进程产生交叠。可移植操作系统接口(POSIX)异步 I/O(AIO)应用程序接口(API)就提供了这种功能。在本文中,我们将对这个 API 概要进行介绍,并来了解一下如何使用它。

AIO 简介

Linux 异步 I/O 是 Linux 内核中提供的一个相当新的增强。它是 2.6 版本内核的一个标准特性,但是我们在 2.4 版本内核的补丁中也可以找到它。AIO 背后的基本思想是允许进程发起很多 I/O 操作,而不用阻塞或等待任何操作完成。稍后或在接收到 I/O 操作完成的通知时,进程就可以检索 I/O 操作的结果。

I/O 模型

在深入介绍 AIO API 之前,让我们先来探索一下 Linux 上可以使用的不同 I/O 模型。这并不是一个详尽的介绍,但是我们将试图介绍最常用的一些模型来解释它们与异步 I/O 之间的区别。图 1 给出了同步和异步模型,以及阻塞和非阻塞的模型。


1. 基本 Linux I/O 模型的简单矩阵
 

每个 I/O 模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点。本节将简要对其一一进行介绍。

同步阻塞 I/O

I/O 密集型与 CPU 密集型进程的比较

I/O 密集型进程所执行的 I/O 操作比执行的处理操作更多。CPU 密集型的进程所执行的处理操作比 I/O 操作更多。Linux 2.6 的调度器实际上更加偏爱 I/O 密集型的进程,因为它们通常会发起一个 I/O 操作,然后进行阻塞,这就意味着其他工作都可以在两者之间有效地交错进行。

最常用的一个模型是同步阻塞 I/O 模型。在这个模型中,用户空间的应用程序执行一个系统调用,这会导致应用程序阻塞。这意味着应用程序会一直阻塞,直到系统调用完成为止(数据传输完成或发生错误)。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。

图 2 给出了传统的阻塞 I/O 模型,这也是目前应用程序中最为常用的一种模型。其行为非常容易理解,其用法对于典型的应用程序来说都非常有效。在调用 read 系统调用时,应用程序会阻塞并对内核进行上下文切换。然后会触发读操作,当响应返回时(从我们正在从中读取的设备中返回),数据就被移动到用户空间的缓冲区中。然后应用程序就会解除阻塞(read 调用返回)。


2. 同步阻塞 I/O 模型的典型流程
 

从应用程序的角度来说,read 调用会延续很长时间。实际上,在内核执行读操作和其他工作时,应用程序的确会被阻塞。

同步非阻塞 I/O

同步阻塞 I/O 的一种效率稍低的变种是同步非阻塞 I/O。在这种模型中,设备是以非阻塞的形式打开的。这意味着 I/O 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或EWOULDBLOCK),如图 3 所示。


3. 同步非阻塞 I/O 模型的典型流程
 

非阻塞的实现是 I/O 命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。正如图 3 所示的一样,这个方法可以引入 I/O 操作的延时,因为数据在内核中变为可用到用户调用 read 返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低。

异步阻塞 I/O

另外一个阻塞解决方案是带有阻塞通知的非阻塞 I/O。在这种模型中,配置的是非阻塞 I/O,然后使用阻塞select 系统调用来确定一个 I/O 描述符何时有操作。使 select 调用非常有趣的是它可以用来为多个描述符提供通知,而不仅仅为一个描述符提供通知。对于每个提示符来说,我们可以请求这个描述符可以写数据、有读数据可用以及是否发生错误的通知。


4. 异步阻塞 I/O 模型的典型流程 (select)
 

select 调用的主要问题是它的效率不是非常高。尽管这是异步通知使用的一种方便模型,但是对于高性能的 I/O 操作来说不建议使用。

异步非阻塞 I/O(AIO)

最后,异步非阻塞 I/O 模型是一种处理与 I/O 重叠进行的模型。读请求会立即返回,说明 read 请求已经成功发起了。在后台完成读操作时,应用程序然后会执行其他处理操作。当 read 的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次 I/O 处理过程。


5. 异步非阻塞 I/O 模型的典型流程
 

在一个进程中为了执行多个 I/O 请求而对计算操作和 I/O 处理进行重叠处理的能力利用了处理速度与 I/O 速度之间的差异。当一个或多个 I/O 请求挂起时,CPU 可以执行其他任务;或者更为常见的是,在发起其他 I/O 的同时对已经完成的 I/O 进行操作。

下一节将深入介绍这种模型,探索这种模型使用的 API,然后展示几个命令。

异步 I/O 的动机

从前面 I/O 模型的分类中,我们可以看出 AIO 的动机。这种阻塞模型需要在 I/O 操作开始时阻塞应用程序。这意味着不可能同时重叠进行处理和 I/O 操作。同步非阻塞模型允许处理和 I/O 操作重叠进行,但是这需要应用程序根据重现的规则来检查 I/O 操作的状态。这样就剩下异步非阻塞 I/O 了,它允许处理和 I/O 操作重叠进行,包括 I/O 操作完成的通知。

除了需要阻塞之外,select 函数所提供的功能(异步阻塞 I/O)与 AIO 类似。不过,它是对通知事件进行阻塞,而不是对 I/O 调用进行阻塞。

Linux 上的 AIO 简介

本节将探索 Linux 的异步 I/O 模型,从而帮助我们理解如何在应用程序中使用这种技术。

在传统的 I/O 模型中,有一个使用惟一句柄标识的 I/O 通道。在 UNIX® 中,这些句柄是文件描述符(这对等同于文件、管道、套接字等等)。在阻塞 I/O 中,我们发起了一次传输操作,当传输操作完成或发生错误时,系统调用就会返回。

Linux 上的 AIO

AIO 在 2.5 版本的内核中首次出现,现在已经是 2.6 版本的产品内核的一个标准特性了。

在异步非阻塞 I/O 中,我们可以同时发起多个传输操作。这需要每个传输操作都有惟一的上下文,这样我们才能在它们完成时区分到底是哪个传输操作完成了。在 AIO 中,这是一个 aiocb(AIO I/O ControlBlock)结构。这个结构包含了有关传输的所有信息,包括为数据准备的用户缓冲区。在产生 I/O (称为完成)通知时,aiocb 结构就被用来惟一标识所完成的 I/O 操作。这个 API 的展示显示了如何使用它。

AIO API

AIO 接口的 API 非常简单,但是它为数据传输提供了必需的功能,并给出了两个不同的通知模型。表 1 给出了 AIO 的接口函数,本节稍后会更详细进行介绍。


1. AIO 接口 API

API 函数

说明

aio_read

请求异步读操作

aio_error

检查异步请求的状态

aio_return

获得完成的异步请求的返回状态

aio_write

请求异步写操作

aio_suspend

挂起调用进程,直到一个或多个异步请求已经完成(或失败)

aio_cancel

取消异步 I/O 请求

lio_listio

发起一系列 I/O 操作

 

每个 API 函数都使用 aiocb 结构开始或检查。这个结构有很多元素,但是清单 1 仅仅给出了需要(或可以)使用的元素。


清单 1. aiocb 结构中相关的域 

 

struct aiocb {

 

  int aio_fildes;               // File Descriptor

  int aio_lio_opcode;           // Valid only for lio_listio (r/w/nop)

  volatile void *aio_buf;       // Data Buffer

  size_t aio_nbytes;            // Number of Bytes in Data Buffer

  struct sigevent aio_sigevent; // Notification Structure

 

  /* Internal fields */

  ...

 

};

 

sigevent 结构告诉 AIO 在 I/O 操作完成时应该执行什么操作。我们将在 AIO 的展示中对这个结构进行探索。现在我们将展示各个 AIO 的 API 函数是如何工作的,以及我们应该如何使用它们。

aio_read

aio_read 函数请求对一个有效的文件描述符进行异步读操作。这个文件描述符可以表示一个文件、套接字甚至管道。aio_read 函数的原型如下:

int aio_read( struct aiocb *aiocbp );

 

aio_read 函数在请求进行排队之后会立即返回。如果执行成功,返回值就为 0;如果出现错误,返回值就为 -1,并设置 errno 的值。

要执行读操作,应用程序必须对 aiocb 结构进行初始化。下面这个简短的例子就展示了如何填充 aiocb 请求结构,并使用 aio_read 来执行异步读请求(现在暂时忽略通知)操作。它还展示了 aio_error 的用法,不过我们将稍后再作解释。


清单 2. 使用 aio_read 进行异步读操作的例子 

 

#include <aio.h>

 

...

 

  int fd, ret;

  struct aiocb my_aiocb;

 

  fd = open( "file.txt", O_RDONLY );

  if (fd < 0) perror("open");

 

  /* Zero out the aiocb structure (recommended) */

  bzero( (char *)&my_aiocb, sizeof(struct aiocb) );

 

  /* Allocate a data buffer for the aiocb request */

  my_aiocb.aio_buf = malloc(BUFSIZE+1);

  if (!my_aiocb.aio_buf) perror("malloc");

 

  /* Initialize the necessary fields in the aiocb */

  my_aiocb.aio_fildes = fd;

  my_aiocb.aio_nbytes = BUFSIZE;

  my_aiocb.aio_offset = 0;

 

  ret = aio_read( &my_aiocb );

  if (ret < 0) perror("aio_read");

 

  while ( aio_error( &my_aiocb ) == EINPROGRESS ) ;

 

  if ((ret = aio_return( &my_iocb )) > 0) {

    /* got ret bytes on the read */

  } else {

    /* read failed, consult errno */

  }

 

 

在清单 2 中,在打开要从中读取数据的文件之后,我们就清空了 aiocb 结构,然后分配一个数据缓冲区。并将对这个数据缓冲区的引用放到 aio_buf 中。然后,我们将 aio_nbytes 初始化成缓冲区的大小。并将aio_offset 设置成 0(该文件中的第一个偏移量)。我们将 aio_fildes 设置为从中读取数据的文件描述符。在设置这些域之后,就调用 aio_read 请求进行读操作。我们然后可以调用 aio_error 来确定 aio_read 的状态。只要状态是 EINPROGRESS,就一直忙碌等待,直到状态发生变化为止。现在,请求可能成功,也可能失败。

使用 AIO 接口来编译程序

我们可以在 aio.h 头文件中找到函数原型和其他需要的符号。在编译使用这种接口的程序时,我们必须使用 POSIX 实时扩展库(librt)。

注意使用这个 API 与标准的库函数从文件中读取内容是非常相似的。除了 aio_read 的一些异步特性之外,另外一个区别是读操作偏移量的设置。在传统的 read 调用中,偏移量是在文件描述符上下文中进行维护的。对于每个读操作来说,偏移量都需要进行更新,这样后续的读操作才能对下一块数据进行寻址。对于异步 I/O 操作来说这是不可能的,因为我们可以同时执行很多读请求,因此必须为每个特定的读请求都指定偏移量。

aio_error

aio_error 函数被用来确定请求的状态。其原型如下:

int aio_error( struct aiocb *aiocbp );

 

这个函数可以返回以下内容:

  • EINPROGRESS,说明请求尚未完成
  • ECANCELLED,说明请求被应用程序取消了
  • -1,说明发生了错误,具体错误原因可以查阅 errno

aio_return

异步 I/O 和标准块 I/O 之间的另外一个区别是我们不能立即访问这个函数的返回状态,因为我们并没有阻塞在 read 调用上。在标准的 read 调用中,返回状态是在该函数返回时提供的。但是在异步 I/O 中,我们要使用 aio_return 函数。这个函数的原型如下:

ssize_t aio_return( struct aiocb *aiocbp );

 

只有在 aio_error 调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数。aio_return 的返回值就等价于同步情况中 read 或 write 系统调用的返回值(所传输的字节数,如果发生错误,返回值就为 -1)。

aio_write

aio_write 函数用来请求一个异步写操作。其函数原型如下:

int aio_write( struct aiocb *aiocbp );

 

aio_write 函数会立即返回,说明请求已经进行排队(成功时返回值为 0,失败时返回值为 -1,并相应地设置errno)。

这与 read 系统调用类似,但是有一点不一样的行为需要注意。回想一下对于 read 调用来说,要使用的偏移量是非常重要的。然而,对于 write 来说,这个偏移量只有在没有设置 O_APPEND 选项的文件上下文中才会非常重要。如果设置了 O_APPEND,那么这个偏移量就会被忽略,数据都会被附加到文件的末尾。否则,aio_offset域就确定了数据在要写入的文件中的偏移量。

aio_suspend

我们可以使用 aio_suspend 函数来挂起(或阻塞)调用进程,直到异步请求完成为止,此时会产生一个信号,或者发生其他超时操作。调用者提供了一个 aiocb 引用列表,其中任何一个完成都会导致 aio_suspend 返回。aio_suspend 的函数原型如下:

int aio_suspend( const struct aiocb *const cblist[],

                  int n, const struct timespec *timeout );

 

aio_suspend 的使用非常简单。我们要提供一个 aiocb 引用列表。如果任何一个完成了,这个调用就会返回0。否则就会返回 -1,说明发生了错误。请参看清单 3。


清单 3. 使用 aio_suspend 函数阻塞异步 I/O 

 

struct aioct *cblist[MAX_LIST]

 

/* Clear the list. */

bzero( (char *)cblist, sizeof(cblist) );

 

/* Load one or more references into the list */

cblist[0] = &my_aiocb;

 

ret = aio_read( &my_aiocb );

 

ret = aio_suspend( cblist, MAX_LIST, NULL );

 

注意,aio_suspend 的第二个参数是 cblist 中元素的个数,而不是 aiocb 引用的个数。cblist 中任何 NULL元素都会被 aio_suspend 忽略。

如果为 aio_suspend 提供了超时,而超时情况的确发生了,那么它就会返回 -1,errno 中会包含 EAGAIN。

aio_cancel

aio_cancel 函数允许我们取消对某个文件描述符执行的一个或所有 I/O 请求。其原型如下:

int aio_cancel( int fd, struct aiocb *aiocbp );

 

要取消一个请求,我们需要提供文件描述符和 aiocb 引用。如果这个请求被成功取消了,那么这个函数就会返回 AIO_CANCELED。如果请求完成了,这个函数就会返回 AIO_NOTCANCELED。

要取消对某个给定文件描述符的所有请求,我们需要提供这个文件的描述符,以及一个对 aiocbp 的 NULL 引用。如果所有的请求都取消了,这个函数就会返回 AIO_CANCELED;如果至少有一个请求没有被取消,那么这个函数就会返回 AIO_NOT_CANCELED;如果没有一个请求可以被取消,那么这个函数就会返回 AIO_ALLDONE。我们然后可以使用 aio_error 来验证每个 AIO 请求。如果这个请求已经被取消了,那么 aio_error 就会返回 -1,并且 errno 会被设置为 ECANCELED。

lio_listio

最后,AIO 提供了一种方法使用 lio_listio API 函数同时发起多个传输。这个函数非常重要,因为这意味着我们可以在一个系统调用(一次内核上下文切换)中启动大量的 I/O 操作。从性能的角度来看,这非常重要,因此值得我们花点时间探索一下。lio_listio API 函数的原型如下:

int lio_listio( int mode, struct aiocb *list[], int nent,

                   struct sigevent *sig );

 

mode 参数可以是 LIO_WAIT 或 LIO_NOWAIT。LIO_WAIT 会阻塞这个调用,直到所有的 I/O 都完成为止。在操作进行排队之后,LIO_NOWAIT 就会返回。list 是一个 aiocb 引用的列表,最大元素的个数是由 nent 定义的。注意 list 的元素可以为 NULL,lio_listio 会将其忽略。sigevent 引用定义了在所有 I/O 操作都完成时产生信号的方法。

对于 lio_listio 的请求与传统的 read 或 write 请求在必须指定的操作方面稍有不同,如清单 4 所示。


清单 4. 使用 lio_listio 函数发起一系列请求 

 

 

struct aiocb aiocb1, aiocb2;

struct aiocb *list[MAX_LIST];

 

...

 

/* Prepare the first aiocb */

aiocb1.aio_fildes = fd;

aiocb1.aio_buf = malloc( BUFSIZE+1 );

aiocb1.aio_nbytes = BUFSIZE;

aiocb1.aio_offset = next_offset;

aiocb1.aio_lio_opcode = LIO_READ;

 

...

 

bzero( (char *)list, sizeof(list) );

list[0] = &aiocb1;

list[1] = &aiocb2;

 

ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL );

 

对于读操作来说,aio_lio_opcode 域的值为 LIO_READ。对于写操作来说,我们要使用 LIO_WRITE,不过LIO_NOP 对于不执行操作来说也是有效的。

AIO 通知

现在我们已经看过了可用的 AIO 函数,本节将深入介绍对异步通知可以使用的方法。我们将通过信号和函数回调来探索异步函数的通知机制。

使用信号进行异步通知

使用信号进行进程间通信(IPC)是 UNIX 中的一种传统机制,AIO 也可以支持这种机制。在这种范例中,应用程序需要定义信号处理程序,在产生指定的信号时就会调用这个处理程序。应用程序然后配置一个异步请求将在请求完成时产生一个信号。作为信号上下文的一部分,特定的 aiocb 请求被提供用来记录多个可能会出现的请求。清单 5 展示了这种通知方法。


清单 5. 使用信号作为 AIO 请求的通知 

 

void setup_io( ... )

{

  int fd;

  struct sigaction sig_act;

  struct aiocb my_aiocb;

 

  ...

 

  /* Set up the signal handler */

  sigemptyset(&sig_act.sa_mask);

  sig_act.sa_flags = SA_SIGINFO;

  sig_act.sa_sigaction = aio_completion_handler;

 

 

  /* Set up the AIO request */

  bzero( (char *)&my_aiocb, sizeof(struct aiocb) );

  my_aiocb.aio_fildes = fd;

  my_aiocb.aio_buf = malloc(BUF_SIZE+1);

  my_aiocb.aio_nbytes = BUF_SIZE;

  my_aiocb.aio_offset = next_offset;

 

  /* Link the AIO request with the Signal Handler */

  my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;

  my_aiocb.aio_sigevent.sigev_signo = SIGIO;

  my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;

 

  /* Map the Signal to the Signal Handler */

  ret = sigaction( SIGIO, &sig_act, NULL );

 

  ...

 

  ret = aio_read( &my_aiocb );

 

}

 

 

void aio_completion_handler( int signo, siginfo_t *info, void *context )

{

  struct aiocb *req;

 

 

  /* Ensure it's our signal */

  if (info->si_signo == SIGIO) {

 

    req = (struct aiocb *)info->si_value.sival_ptr;

 

    /* Did the request complete? */

    if (aio_error( req ) == 0) {

 

      /* Request completed successfully, get the return status */

      ret = aio_return( req );

 

    }

 

  }

 

  return;

}

 

在清单 5 中,我们在 aio_completion_handler 函数中设置信号处理程序来捕获 SIGIO 信号。然后初始化aio_sigevent 结构产生 SIGIO 信号来进行通知(这是通过 sigev_notify 中的 SIGEV_SIGNAL 定义来指定的)。当读操作完成时,信号处理程序就从该信号的 si_value 结构中提取出 aiocb,并检查错误状态和返回状态来确定 I/O 操作是否完成。

对于性能来说,这个处理程序也是通过请求下一次异步传输而继续进行 I/O 操作的理想地方。采用这种方式,在一次数据传输完成时,我们就可以立即开始下一次数据传输操作。

使用回调函数进行异步通知

另外一种通知方式是系统回调函数。这种机制不会为通知而产生一个信号,而是会调用用户空间的一个函数来实现通知功能。我们在 sigevent 结构中设置了对 aiocb 的引用,从而可以惟一标识正在完成的特定请求。请参看清单 6。


清单 6. 对 AIO 请求使用线程回调通知 

 

void setup_io( ... )

{

  int fd;

  struct aiocb my_aiocb;

 

  ...

 

  /* Set up the AIO request */

  bzero( (char *)&my_aiocb, sizeof(struct aiocb) );

  my_aiocb.aio_fildes = fd;

  my_aiocb.aio_buf = malloc(BUF_SIZE+1);

  my_aiocb.aio_nbytes = BUF_SIZE;

  my_aiocb.aio_offset = next_offset;

 

  /* Link the AIO request with a thread callback */

  my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;

  my_aiocb.aio_sigevent.notify_function = aio_completion_handler;

  my_aiocb.aio_sigevent.notify_attributes = NULL;

  my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;

 

  ...

 

  ret = aio_read( &my_aiocb );

 

}

 

 

void aio_completion_handler( sigval_t sigval )

{

  struct aiocb *req;

 

  req = (struct aiocb *)sigval.sival_ptr;

 

  /* Did the request complete? */

  if (aio_error( req ) == 0) {

 

    /* Request completed successfully, get the return status */

    ret = aio_return( req );

 

  }

 

  return;

}

 

在清单 6 中,在创建自己的 aiocb 请求之后,我们使用 SIGEV_THREAD 请求了一个线程回调函数来作为通知方法。然后我们将指定特定的通知处理程序,并将要传输的上下文加载到处理程序中(在这种情况中,是个对aiocb 请求自己的引用)。在这个处理程序中,我们简单地引用到达的 sigval 指针并使用 AIO 函数来验证请求已经完成。

AIO 进行系统优化

proc 文件系统包含了两个虚拟文件,它们可以用来对异步 I/O 的性能进行优化:

  • /proc/sys/fs/aio-nr 文件提供了系统范围异步 I/O 请求现在的数目。
  • /proc/sys/fs/aio-max-nr 文件是所允许的并发请求的最大个数。最大个数通常是 64KB,这对于大部分应用程序来说都已经足够了。

结束语

使用异步 I/O 可以帮助我们构建 I/O 速度更快、效率更高的应用程序。如果我们的应用程序可以对处理和 I/O 操作重叠进行,那么 AIO 就可以帮助我们构建可以更高效地使用可用 CPU 资源的应用程序。尽管这种 I/O 模型与在大部分 Linux 应用程序中使用的传统阻塞模式都不同,但是异步通知模型在概念上来说却非常简单,可以简化我们的设计。

http://blog.csdn.net/alexmahone_xie/article/details/6682774

linux内核SMP负载均衡浅析
在《linux进程调度浅析》一文中提到,在SMP(对称多处理器)环境下,每个CPU对应一个run_queue(可执行队列)。如果一个进程处于TASK_RUNNING状态(可执行状态),则它会被加入到其中一个run_queue(且同一时刻仅会被加入到一个run_queue),以便让调度程序安排它在这个run_queue对应的CPU上面运行。
一个CPU对应一个run_queue这样的设计,其好处是:
1
、一个持续处于TASK_RUNNING状态的进程总是趋于在同一个CPU上面运行(其间,这个进程可能被抢占、然后又被调度),这有利于进程的数据被CPU所缓存,提高运行效率;
2
、各个CPU上的调度程序只访问自己的run_queue,避免了竞争;
然而,这样的设计也可能使得各个run_queue里面的进程不均衡,造成一些CPU闲着、一些CPU忙不过来混乱局面。为了解决这个问题,load_balance(负载均衡)就登场了。

http://saupb.blog.163.com/blog/static/47124178201102782352235/

线程与CPU绑定  

1. 在用户空间进行线程与CPU绑定

1)头文件

#include <sched.h>

2)函数原型

int sched_setaffinity(pid_t pid, unsigned int len, unsignedlong *mask);

int sched_getaffinity(pid_t pid, unsigned int len, unsignedlong *mask);

sched_setaffinity, sched_getaffinity – set and get aprocess’s CPU affinity mask

3)程序实例

cpu_set_t mask;

__CPU_ZERO(&mask);

__CPU_SET(0, &mask);

sched_setaffinity(0,sizeof(cpu_set_t),&mask);

此段代码可将线程绑定在0CPU上。

2. 在内核空间进行线程与CPU绑定

1)头文件

#include <linux/sched.h>

2)函数原型

int set_cpus_allowed(struct task_struct *p, cpumask_tnew_mask);

Change a given task's CPU affinity

3)程序实例

#ifdef CONFIG_SMP

static cpumask_t apm_save_cpus(void)

{

    cpumask_t x =current->cpus_allowed;

    /* Some biosesdon't like being called from CPU != 0 */

   set_cpus_allowed(current, cpumask_of_cpu(0));

   BUG_ON(smp_processor_id() != 0);

    return x;

}

static inline void apm_restore_cpus(cpumask_t mask)

{

   set_cpus_allowed(current, mask);

}

#else

/*

*       No CPU lockdownneeded on a uniprocessor

*/

#define apm_save_cpus()         (current->cpus_allowed)

#define apm_restore_cpus(x)   (void)(x)

#endif

 

http://www.360doc.com/content/09/0304/16/36491_2710052.shtml

 

http://www.360doc.com/content/12/1017/13/7851074_242003381.shtml

父进程和子进程之间会继承对affinity的设置。

http://www.360doc.com/content/09/0310/15/36491_2768207.shtml

 

http://www.cnblogs.com/biyeymyhjob/archive/2012/08/01/2617884.html

 

Linux进程模型总结

Linux进程通过一个task_struct结构体描述,在linux/sched.h中定义,通过理解该结构,可更清楚的理解linux进程模型。       包含进程所有信息的task_struct数据结构是比较庞大的,但是该数据结构本身并不复杂,我们将它的所有域按其功能可做如下划分:

 

进程状态(State)

进程调度信息(Scheduling Information)

各种标识符(Identifiers)

进程通信有关信息(IPC:Inter_Process Communication)

时间和定时器信息(Times and Timers)

进程链接信息(Links)

文件系统信息(File System)

虚拟内存信息(Virtual Memory)

页面管理信息(page)

对称多处理器(SMP)信息

和处理器相关的环境(上下文)信息(Processor Specific Context)

其它信息

进程调度信息

   调度程序利用这部分信息决定系统中哪个进程最应该运行,并结合进程的状态信息保证系统运转的公平和高效。这一部分信息通常包括进程的类别(普通进程还是实时进程)、进程的优先级等。表2描述了跟进程调度有关的字段,表3.3说明了几种常用的进程调度算法及这些算法的使用范围,如先来先服务主要用于实时进程的调度。

表2 进程调度信息

域名

含义

need_resched

调度标志

Nice

静态优先级

Counter

动态优先级

Policy

调度策略

rt_priority

实时优先级

 

表3  进程调度的策略

名称

解释

适用范围

SCHED_OTHER

其他调度

普通进程

SCHED_FIFO

先来先服务调度

实时进程

SCHED_RR

时间片轮转调度

 

只有root用户能通过sched_setscheduler()系统调用来改变调度策略。

 

 

http://www.360doc.com/content/10/1217/11/3746120_78922826.shtml

 

http://www.360doc.com/relevant/242002818_more.shtml

 

 

http://www.cnblogs.com/dongzhiquan/archive/2012/02/04/2338662.html

http://www.cnblogs.com/dongzhiquan/archive/2011/12/15/2289450.html

 

http://www.cnblogs.com/dongzhiquan/archive/2011/12/15/2289438.html

 

http://www.cnblogs.com/dongzhiquan/archive/2011/07/09/2101580.html

http://www.cnblogs.com/dongzhiquan/archive/2011/07/09/2101578.html

 

 

8.多核技术概述 (   基于多核平台的流量测量方法研究)

 

作为计算机的处理核心,CPU运算的快慢决定了整个计算机性能的高低。因

CPU一直是计算机中发展最快的模块之一。从PCCPU的鼻祖英特尔8088芯片

开始,CPU的发展大致可以分为3个阶段:核上集成阶段、频率竞争阶段、核竞争

阶段。

20世纪70年代末到80年代末,以英特尔的80x86系列为代表的CPU芯片通过不

断增大CPU的集成度来提高CPU性能。而在90年代,CPU架构基本趋于稳定,

商通过不断增大芯片上晶体管集成密度来提高CPU主频,进而提升CPU的整体性

能。进入21世纪以来,随着主频的不断上升,CPU的能耗和散热问题越显特出,

目前单核处理器已经接近工艺上的物理极限,很难通过提升主频的方式来进一步

改善CPU性能。

既然单个处理器核心的性能已到达极限,很难再提升,那么自然研究人员就

会想到让多个处理器核心同时并行工作,来达到理论上让性能成倍的增加。因此

多核处理器技术应运而生,也逐渐在产业界流行起来,并成为当前处理器市场上

的主流。多核技术通过在一个芯片上集成多个简单的处理器以并行运行来提升整

个计算机系统的性能[36]

2.2.1 基本概念

1.进程

进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它

是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单

,也是基本的执行单元。在60年代初期,“进程”这一概念首先由麻省理工工学

院的MULTICS系统和IBM公司的CTSS/360系统引入[37]

由于进程执行时的间断性,决定了进程可能具有多种状态。

新建状态:操作系统根据要求创建的新进程,它还没有被加入到可执行进程

组中就绪。

就绪状态:进程已获得除处理机外的所需资源,等待分配处理机资源,只要

分配到CPU就可执行。

运行状态:进程获得处理机资源被执行。

阻塞状态:由于进程等待某种条件(I/O操作或进程同步),在条件满足之前无法继续执行。

退出状态:操作系统从可执行进程组中释放出的进程(自身正常停止或异常取

)

进程具有五个基本特征:动态性、并发行、独立性、异步性和结构特征。

2.线程

线程(thread)有时也被称为轻量级进程(Lightweight Process,LWP),是程序

执行流的最小单元。一个标准的线程由线程 ID,当前指令指针(PC),寄存器集

合和堆栈组成。线程是进程的一个实体,是被系统独立调度和分派的基本单位,

线程本身不拥有系统资源,但它可与同属一个进程的其它线程共享进程所拥有的

全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可

以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程

也有就绪、阻塞和运行等基本状态。每一个程序都至少有一个线程,那就是程序

本身。

3.多线程

多线程是指在单个程序中同时运行多个线程完成不同的工作。在支持多线程

的系统中,通常一个进程包括多个线程,每个线程都是作为利用CPU的基本单位,

是花费最小开销的实体。多线程是为了同步完成多项任务,不是为了提高运行效

,而是为了提高资源使用效率来提高系统的效率。

4.CPU亲和性

CPU亲和性可分为进程亲和性和中断亲和性。在多核平台上,一个任务可以

选择多个CPU;同样,一个CPU可以运行一个或多个任务。CPU亲和性可以有效

解决任务在调度执行中发生的迁移。中断亲和性允许将一个或多个中断绑定到特

定的CPU,也就是有特定的CPU来处理这些中断,实现多个I/0设备和一系列CPU

的匹配,这样就可以分散CPU中断负载的压力,提高系统的性能。进程的亲和性

是指将一个或多个进程或线程绑定到一个或多个执行核上。这样进程或线程将会

长时间的在特定的执行核上运行。也就是说,进程或线程要在被指定的CPU核上

运行而不被迁移到其他的CPU执行核上。另外进程亲和性分为软亲和性和硬亲和

性。软亲和性是指调度系统在调度时候,尽量让进程或线程不会在执行核上频繁

迁移。硬亲和性指进程或线程受控地在给定的CPU上运行。

因此,借助CPU亲和性,调度系统可以实现对进程或线程更精确的调度,

而解决多线程编程中各种调度问题。

2.2.2 多核相关技术

随着单个芯片上晶体管数目的增加,通过提升主频来提高性能的方式已达到

极限,处理器设计者需要提出下一代高性能处理器的体系结构。近年来,高端处

理器主要利用超标量(Superscalar)技术来提高性能。超标量处理器利用在每个时

(Instruction-Level Parallelism,ILP)而提高系统性能。但是单个程序的有限ILP

导致超标量处理的资源利用率不高,进一步增加指令发送速度只会使性能变得更

糟。对于下一代高性能处理器而言,开发的并行性不应该仅限于单个程序内细粒

度的ILP。实际上在许多工作负载中,存在多种形式的粗粒度的线程级并行性

(Thread-Level Parallelism,TLP)。例如,对于多道编程负载而言,存在多个独立

运行的应用,而并行程序则可能包含多个线程或者进程。

研究人员提出多种利用TLP来提高处理器资源利用率的处理器体系结构,

括多线程处理器(Multithreaded Processor)、单片多处理器(ChipMultiProcessor,

CMP)和同时多线程(Simultaneous Multithreading,SMT)结构。2005SUN公司提

出了片上多线程技术(Chip Multiple Threading,CMT),即将CMP技术和SMT技术

结合起来。

2.2.2.1 CMP 结构

1996 (Chip

Multi-Processor,CMP)结构的概念[38,39],并对CMP进行了系统的研究。单片多处

理器的基本思想是通过简化超标量结构设计,在单个处理器芯片上利用丰富的晶

体管资源来集成多个相对简单的超标量处理器核心,从而避免了线延迟的影响,

并且多核处理器的多个处理器核心同时并行执行,使得应用程序的指令级并行性

和线程级并行性得到充分开发。通过开发这种在各个层次的并行度,来提高系统

的性能和吞吐量。

片上多核处理器实现方式的异同主要包括以下几个方面:核心的数目、核心

同构与异构、各级Cache的独立与共享、Cache大小、所使用的工艺、芯片集成度

等。选择不同的依据,CMP有多种分类的方法。下面我们就介绍CMP的常用分类

方法,也即CMP的体系结构分类方法。

根据多核处理器的处理器核心类型的组成是否相同,CMP体系结构可分为同

CMP和异构CMP两类。同构CMP是指处理器内核的类型相同,并且处理器内核

的地位是对等的多核处理器,同构CMP大多数采用通用处理器来作为核心。异构

CMP是指处理器内核类型不同,并且处理器内核的地位是不对等的多核处理器,

异构CMP采用的是“主处理器核+协处理器核”的设计,主处理器核采用通用处理

,协处理器核则采用一些针对特定应用的计算部件(ASICVLIW处理器、

DSP和媒体处理器等)

作为同构CMP的典型代表,斯坦福大学研制的Hydra多核处理器就属于一种典

型的同构多核处理器。Hydra处理器使用4MIPS内核构造了一个片上多核处理

,每一个内核具有自己的指令和数据缓存,同时所有的内核都共享一个统一的

大小为1M的片上二级缓存,并且多个核心之间数据的一致性采用基于监听的一致

性协议来进行维护。这些处理器内核除了支持通常的读取和保存操作之外还支持

MIPS指令集中的LL(Load Locked)SC(Store Conditional)指令,以实现同步原

语。同构多核处理器(Hydra)的总体结构图如图2.5所示:

作为异构CMP的典型代表,索尼、IBM和东芝联合研发的Cell处理器就属于一

种典型的异构多核处理器。Cell处理器的设计目标是为多媒体应用带来高性能的

优化处理器,包含1个由PowerPC970简化而来的主处理器核心PPE8个称为SPE

的协作处理器,它们通过一个高速的内存一致的互联总线(EIB)进行连接。Cell

处理器的工作频率超过4GHz,未来Cell处理器将依照应用领域增加或者减少计算

核心数目。异构多核处理器(Cell)的总体结构图如图2.6所示:

2.2.2.2 SMP 结构

对称多处理机(Symmetrical Multi-Processing,SMP)[40,41]是指在一个计算机上

汇集了一组处理器(CPU),CPU之间共享内存子系统以及总线结构。在这种

架构中,计算机不再由单个CPU组成,而由多个处理器同时运行操作系统的单一

副本,并共享内存和计算机上的其他资源。系统将任务队列对称地分布于多个CPU

之上,所有CPU都可以平等地访问内存、外部中断和I/O。在对称多处理系统中,

系统资源被系统中所有CPU共享,工作负载能够均匀地分配到所有可用处理器之

,从而极大地提高整个系统的数据处理能力。

我们知道,在单核处理器系统上的所谓的并行执行并不是真正意义上的并行

执行,单核处理器系统中的并行执行只是一个宏观上的概念,因为系统中只有一

个处理器,所有任务只能串行执行,单处理器系统的并行执行指的是在某一段时

间内执行了多个任务,而具体到某一时间点,在处理器上执行的仍然只能是单个

的任务,即在宏观上是并行的而在微观上仍然是串行执行的。而对称多处理器却

能实现真正意义上的并行执行,因为系统中存在多个处理器,在同一时刻,每个

处理器上都可以同时执行任务,在微观上同时执行多条指令。当前,世界上的大

部分高性能计算机系统都是采用SMP技术或以SMP技术为基础进行构建的。典型

的对称多处理器(SMP)系统的体系结构如图2.7所示。

 

 

 

在一台4核的计算机上,我做了两个测试程序A,B,在A上面创建4个线程,B上面创建4个进程,每个进程和线程中执行一样的函数,做循环的i++操作。用top观察运行情况,在运行A的过程中cpu使用率接近400%,运行B的过程中创建的4个进程的CPU使用率都接近100%。在进程的运行过程中,只能有一个上下文环境,在一个事件点上就只能运行一个进程(是不是这里我理解错了),怎么能够使4个进程都并行的运行起来呢?如果没有并行的运行,那么4个进程的CPU怎么都是100%呢?

 

用户态进程和线程在内核看来都是一个task_struct,没有特殊的设置,调度都是一样的。

另外,topCPU占用率不准

 

对于实验现象,我的理解:

AB的操作内核层面来看都差不多:四个任务在四个核上面做i++操作,唯一的不同时,B对应的4个进程不共享地址空间,即四个任务的i对应不同的内存地址;而A对应的4个线程共享地址空间,也就是说4个线程共享变量i

因为top命令是按照进程来统计占用率的,所以A的四个进程各占100%,而B一个进程独占400%

 

 

对于你的疑问,我的解答:

"在进程的运行过程中,只能有一个上下文环境",这句话你理解的有问题。这句话中的“进程”如果替换成内核理解的“任务task”就没有问题了。事实上进程A的四个线程实际上就是运行在四个核上的不同任务,而每一个线程对应的任务都有自己的一个上下文环境,只不过四个上下文有些相同的东东罢了。

核心问题是,上下文到底指什么?操作系统和硬件密切相关,上下文概念与CPU密切相关。上下文说白了就是CPU的状态。系统要实现多任务在单CPU的轮转执行,就要不断的保存和恢复CPU的状态,这就叫上下文切换。

 

线程和进程的调度方式是一样的,你看到的top 400%,应该是因为top命令的Irix和线程模式吧,top执行后按HI,就可以看到区别了。

另外,每个进程都有独立的上下文,4个进程可同时在4个核上执行(当然,还取决于是否有临界资源)

 

线程间有一定相关度,可能会使调度器认为它们最好运行在同一CPU上,比如为了提高cache命中率。

 

内核在做调度的时候不区分线程和进程。top –H

 

 

CPU Affinity的设置,到底在系统中会有什么样的积极影响,切换cpu和限制在同一个cpu上到底会让一个进程的效率有什么改变,为此做了一些研究,结论如下:

CPU Cache可以在绑定的情况下,提高命中率。如果一个线程在多个Processor中来回切换,可能会导致CPUcache持续失效

如果多个线程访问的数据相同,设置同样的Affinity Mask可以帮助这个Processor提高命中

可以让一些需要高优先级的Process独享一个Processor。通过将系统其他的Process绑定到一个processor上,并将高优先级的Process绑定到空闲的Processor

对于一个开发者来说,他可以直接使用Linux内核2.5.8版本以后提供的API函数,用来设置自己进程的Affinity Mask,但是对于系统程序或者不是自己开发的进程怎么办。如果我们不能设置第三方程序的Mask,那么上文的第三点,让某个进程独享一个Processor就无法实现了。

多核CPU硬件架构介绍

1. 计算平台介绍

Flynn于1972年提出了计算平台的Flynn分类法,主要根据指令流和数据流来分类,共分为四种类型的计算平台,如下图所示:

 

图1.1.1:Flynn计算平台分类法

单指令流单数据流机器(SISD

SISD机器是一种传统的串行计算机,它的硬件不支持任何形式的并行计算,所有的指令都是串行执行。并且在某个时钟周期内,CPU只能处理一个数据流。因此这种机器被称作单指令流单数据流机器。早期的计算机都是SISD机器,如IBM PC机,早期的巨型机和许多8位的家用机等。

单指令流多数据流机器(SIMD

SIMD是采用一个指令流处理多个数据流。这类机器在数字信号处理、图像处理、以及多媒体信息处理等领域非常有效。

Intel处理器实现的MMXTM、SSE(StreamingSIMD Extensions)、SSE2及SSE3扩展指令集,都能在单个时钟周期内处理多个数据单元。也就是说我们现在用的单核计算机基本上都属于SIMD机器。

多指令流单数据流机器(MISD

MISD是采用多个指令流来处理单个数据流。由于实际情况中,采用多指令流处理多数据流才是更有效的方法,因此MISD只是作为理论模型出现,没有投入到实际应用之中。

多指令流多数据流机器(MIMD

MIMD机器可以同时执行多个指令流,这些指令流分别对不同数据流进行操作。最新的多核计算平台就属于MIMD的范畴,例如Intel和AMD的双核处理器等都属于MIMD。

本书所讲述的主要内容就是围绕多核计算平台而来的,下面就来介绍一下多核的硬件结构。

2. 多核CPU硬件结构

多核CPU是将多个CPU核集成到单个芯片中,每个CPU核都是一个单独的处理器。每个CPU核可以有自己单独的Cache,也可以多个CPU核共享同一Cache。下图便是一个不共享Cache的双核CPU体系结构。

 

图1.1.2:多核CPU硬件体系结构

在现代的多核硬件结构中,内存对多个CPU核是共享的,CPU核一般都是对称的,因此多核属于共享存储的对称多处理器(SymmetricMulti-processor,SMP)。

在多核硬件结构中,如果要充分发挥硬件的性能,必须要采用多线程(或多进程)执行,使得每个CPU核在同一时刻都有线程在执行。

和单核上的多线程不同,多核上的多个线程是在物理上并行执行的,是一种真正意义上的并行执行,在同一时刻有多个线程在并行执行。而单核上的多线程是一种多线程交错执行,实际上在同一时刻只有一个线程在执行。

 

任务的分解与调度问题

程序并发运行后,除了锁竞争导致的CPU饥饿问题外,还有一个问题也会导致CPU饥饿,那就是任务的分解与调度问题。

所谓任务指的是执行的某个程序功能,任务与线程不同,一个线程内可以执行一个或多个任务。

如果任务划分得不好,那么就很难均匀地分布到各个CPU核上;对于划分好的多个任务,如何将其均匀地分配到各个CPU核上进行计算是很重要的问题,也就是所谓的任务调度问题。任务分解与调度的好坏会影响各CPU核上计算的负载均衡。

比如有6个任务,耗时分别为18、17、12、8、5、3,如何将其在一个双核CPU上执行,取得好的加速比呢?

相信经过简单的口算,大部分读者都可以发现,将任务分为两组(18、8、5),(17、12、3),使用两个硬件线程来执行,每个线程执行其中的一组任务,能取得最好的加速比效果。

但是在实际情况中,任务数量较多,任务间耗时差距通常非常大,要均匀地将任务分配到各个CPU核上执行并非易事。事实上,任务的调度算法问题是一个NP难题。

当然上面的例子没有考虑任务间的执行依赖关系,有依赖关系时,任务调度又复杂了一些。在实际情况中,还有许多任务是动态产生的,事先并不知道任务耗时,如何调度这些动态任务,使计算均匀地分配到各个CPU核上则是更大的挑战。

本书的第5部分会给出任务分解与调度的解决方案。

 

 加速比性能问题

1. 单核和多核总计算时间的差别

单核时代,通常只要将各个串行算法优化一下,就可以使整体时间性能得到提升,程序的总计算时间取决于各段计算花费时间之和。

单核程序总计算时间  =   

因此在单核系统中, 对任意一段计算的优化都可以使总时间性能得到提升。

然而,到了多核系统中,能不能做到花费的总计算时间就等于相同级别单核上的总计算时间除以CPU核数呢?即能不能做到以下公式呢?

多核程序总计算时间  = 

上述公式是一个理想状态下的公式,实际情况中一般是做不到的,实际上多核程序总计算时间一般都是符合下不等式:

多核程序总计算时间  ≥   

例如在双核CPU上有两个任务,一个任务计算需要耗时50毫秒,另外一个任务需要耗时30毫秒,两个任务同时运行后,总的耗时并不是(50+30)/ 2 = 40毫秒,而是50毫秒。

从上面的例子也可以看出,如果仅仅对某段计算进行优化(例如优化那个30毫秒的任务为20毫秒),整体性能不一定能够得到提升(总时间还是50毫秒),这一点也是多核和单核系统在性能方面有重大差别的地方。

2. 加速比指标

为了更方便地描述多核程序计算的性能,一般采用加速比指标来进行度量。加速比定义如下 :

S(n) = 单处理器上最优串行化算法计算时间  / 使用n个处理器并行计算时间

一般来说,加速比通常都小于CPU核数,只有极少数并行算法可以获得超线性加速比,例如并行顺序搜索。因此加速比一般要求向CPU核数靠近,加速比越大,那么程序性能就越好。

有时为了评估系统中多个CPU的运行效率,可以用以下多CPU效率指标来进行评价:

多CPU效率 = 加速比 / CPU数量(核数)

加速比是多核编程中需要考虑的重要性能指标,串行计算、CPU饥饿问题、线程间的负载平衡问题都是影响加速比的因素。

加速比指标是在多核编程中有别于单核编程的最重要因素,一般来说,要更好地发挥多核CPU的性能,需要实现两个加速比目标:

加速比首先必须随CPU核数的增加而线性增加,即加速比的线性增长特性。

最后得到的加速比要尽量向CPU核数靠近。

第1个目标的实现是非常重要的,否则更多核数的CPU将发挥不了作用,因此第1个目标是实际情况中须实现的目标,满足第1个目标的加速比叫做"线性加速比"。第2个目标属于优化性目标,在实际情况中要想使加速比达到更高,那么需要花费更多的时间去设计和优化程序,因此实际情况中需要在软件开发成本和最终的加速比性能目标间取得合理的均衡。

 

CPU核负载平衡的区别

当将程序分解成多个线程执行后,在单核CPU中,不需要考虑各个CPU核间的负载平衡问题,即使各个线程的计算量相差非常大,也不会影响程序总的计算时间。

而在多核CPU中,必须考虑各个线程计算量均衡地分布到各个CPU核上的问题,也就是计算负载平衡问题。如果线程间的计算量无法取得好的负载平衡,那么某些CPU核计算量大耗时多,另外一些CPU核计算量小耗时少,耗时少的CPU核运行完成后,将处于空闲状态,导致CPU出现饥饿现象。

如果CPU核间负载不均衡,对加速比指标影响也是非常大的,举个例子如下:

假设一个4核CPU系统中有4个任务,各个任务耗时分别为20毫秒、5毫秒、3毫秒、2毫秒。

加速比 = (20+5+3+2)/ 20 = 1.5

多CPU效率 = 加速比 / CPU核数 = 1.5 / 4 = 37.5%

如果将各个任务的时间调整为10毫秒、8毫秒、6毫秒、6毫秒的话,那么加速比将变成  (10+8+6+6)/ 10 = 3

对应的多CPU效率将变成75%,效率增加了一倍,可见CPU核间负载不均衡对加速比的影响还是很大的。

 

任务调度策略的区别

在单核程序中,任务调度主要是为各任务间取得一定的分时效果,或者优先保证某些任务先运行完。任务的调度通常是由操作系统来完成,时间片轮转和优先级抢占是单核中的最常见的任务调度策略。

时间片轮转和优先级抢占调度算法如下图所示:

 

图1.3.3:单核环境中时间片轮转和优先级抢占调度示意图

在上图中,共有4个任务t1、t2 、t3、t4,每个任务使用一个单独的线程来执行,其中任务t1和t2优先级相同,任务t3 优先级比t1和t2高,任务t4优先级比t3高。任务优先级相同的t1和t2采用时间片轮转调度,t1先运行一个时间片,接着轮到t2运行一个时间片,这时更高优先级的任务t3开始运行了,它抢占了整个CPU的运行时间,此时t1和t2都得不到运行时间,任务3运行的过程中又碰到一个比它更高优先级的任务t4,t4抢占了整个CPU的运行时间,直到t4运行完,t3才得到运行时间,等t3运行完后,t1和t2才得到时间接着运行。

在这种时间片轮转和抢占式调度情况下,程序员不需要考虑调度算法问题,只需要安排一下任务的优先级就够了。

在多核程序中,任务调度相比单核时的调度有了新的需求。多核中的任务调度不仅要满足单核时的需求,而且要考虑各个任务的耗时问题,需要根据各个任务的耗时进行合理的安排,使得计算均摊在各个CPU核上。在多核中如果简单地进行时间片轮转和优先级抢占并不能使计算均摊到各个CPU核上,因此多核中需要新的任务调度策略和算法。

由于操作系统并不知道各个任务需要消耗多少时间,操作系统并不知道应该采用何种任务调度策略才能更好地让计算均摊到各个CPU核上;因此在多核中,任务调度策略的选择成了程序员需要考虑的问题。

 

CPU Cache存取的区别(伪共享问题)

伪共享问题在《多核程序设计技术》一书中有详细讲解,它是由于CPU cache机制造成的,CPU读取Cache时是以行为单位读取的,如果两个硬件线程的两块不同内存位于同一Cache行里,那么当两个硬件线程同时在对各自的内存进行写操作时,将会造成两个硬件线程写同一Cache行的问题,它会引起竞争,就像在乒乓球比赛一样,效率将成百倍的下降。

在单核系统中,伪共享问题是不存在的,因为同一时刻只有一个硬件线程在执行,不存在同时写同一Cache行的问题。

伪共享问题在实际情况中是经常可以碰到的,比如两个线程同时写一个数组的相邻部分,或者写两块相邻的内存,这些都有可能造成伪共享问题。

对于分配的内存,可以采取一定的内存分配算法使各块内存不在同一Cache行里,但对于数组或变量的访问,就必须要由程序员在设计时进行避免伪共享问题。

要解决伪共享问题,首先必须知道给定的内存中,那块区域会处于同一Cache行内,Intel的系统中,有一个简单的算法可以得到一块内存中对应的Cache行首地址,即每个Cache行首地址都是Cache行大小的整数倍。

比如一块内存大小为60字节,首地址为0x0012ff52,由于0x0012ff52除以64以后余数为0x12,因此这个地址不是Cache行的首地址。在这个地址之前的Cache行首地址为0x0012ff40,在这个地址之后的Cache行首地址为0x0012ff80。对应的内存位于两块不同的Cache中。如下图所示:

 

(点击查看大图)图1.3.4:Cache行对齐示意图

根据上面所说的Cache行首地址为Cache行大小的整数倍的特点,可以设计一个函数来取出给定地址之后的第0个Cache行首地址。代码如下:

1.  /** 计算给定地址之后的第个Cache行首地址  

2.      如果给定地址刚好为一个Cache行首地址,那么计算结果等于它自身  

3.      @param  void *pAddr - 给定的地址   

4.      @return void * - 返回给定地址之后的第0个Cache行首地址    

5.  */  

6.  void *GetCacheAlignedAddr(void *pAddr) 

7.  {  

8.      int  m = CACHE_LINE_SIZE;  

9.      void *pRet = (void *)((UINT(pAddr)) &(-m)); 

10.     return pRet; 

11.

取到了Cache行首地址后,就可以区分出两块内存是否在同一Cache行中了,对避免伪共享问题就有了很大的帮助。对于伪共享问题的解决,后面的相关章节中还会有详细的讨论,如并行数值计算、分布式内存管理中都会有详细讨论。

 

 共享存储与分布式存储的区别

多机分布式环境中,每台机器都有自己独立的存储器,因此它的内存是不共享的,如果要进行全局共享数据读写操作,必须依靠机器间的通信来进行数据搬迁。

在多核机器中,由于内存是共享的,对全局共享数据的访问不存在数据搬迁问题,只存在锁保护问题。

因此在多核环境中,影响共享数据访问效率的主要因素是锁竞争,而在多机环境中影响效率的因素主要是通信开销。

在目前的CPU中,一般每秒进行单纯的加锁解锁操作次数超过1000万次,如果每次锁操作中平均操作8字节的话,那么相当于能够处理80M字节/秒。如果每次锁操作中平均操作1024字节的话,那么相当于1024M字节/秒。每次锁操作中操作的数据量越大,则每秒处理的字节数就愈多,最大可以达到CPU可以处理数据速度的极限,取决于存储器和CPU Cache的访问速度。

100Mbit网络通信的效率为  12.5M字节/秒,有效数据的效率估计低于10M字节/秒。

可见锁操作的效率远大于通信效率。这种效率上的区别反映到具体的问题中会导致算法设计方法的差异,即在多核中的分布式算法和多机的分布式算法存在差异。

 

多核并行编程方法 

在多核的硬件结构中,如果要充分发挥硬件的性能,必须采用多线程(或多进程)执行,以提高CPU的利用率。多核系统的编程模型和多个CPUSMP系统的编程模型是一致的,都属于共享存储的编程模型;同时,多核环境中也可以使用的分布式编程模型。
目前,多核并行编程方法可以分为以下四类:基于Raw Thread API的方法、基于共享内存编程模型的方法、基于高层次模板库的方法、基于分布式编程的方法。
1)基于Raw Thread API的方法:这种方法主要使用系统底层API来进行多线程编程。Windows ThreadsPOSIX threads都属于使用系统底层API[2,3],是最低层次的并行编程API,不同点是Windows threads应用于Windows平台,Win32这样一个完整的库支持,相对较为成熟,POSIX threads主要用于UNIX/Linux平台,但编程难度相当大。这些系统底层Raw Thread API是非常强大的并行程序设计工具,它将巨大的潜能交到了程序员手中。但是,这种编程方法会遭遇上一节所提到的问题。
2)基于共享内存编程模型的方法:共享内存编程模型中最有代表性的是OpenMP,它可以帮助程序快速创建线程,解决多核编程中面临的问题。OpenMP形成于1997,用于编写可移植的多线程应用程序,起初只是一个Fortran标准,后来又发展到C/C++,目前在Intel C++ CompilerMicrosoft VS2005以及更高版本等编译器上都得到了广泛支持。虽然,OpenMP简单易用,但是它限制了程序员对并行的控制,限定了所提供信息的数量,例如OpenMP没有动态线程调度机制、伸缩性的内存分配器、内嵌式并行程序、并发的数据结构等。当“少许并行”就足够时,使用OpenMP会比较方便。[4-6]
3)基于高层次模板库的方法:这类方法具有面向对象的特征,基于模板技术构建了丰富的线程控制和并行计算的模板库。Java Threads是所有能显示支持并行的程序设计语言中使用最广泛的。在它的核心,提供了与POSIX Threads相同的概念特性,并且支持并发数据结构。Threading Building Blocks是由Intel针对多核平台开发的一组开源的C++的模板库,基于GPLv2开源证书,支持可伸缩的并行编程。它不需要特别的语言或者编译器的支持,因此可以广泛地被用于任何处理器、任何操作系统以及任何编译器。
使用这种方法进行多线程编程,程序员不需要将过多的精力放到同步、负载平衡、缓存优化等等的问题上,而能够轻松地实现自动调度的并行程序,使得CPU的多个核心处于高效运转之中。但是,这需要程序员熟练掌握这种开发工具。[7,8]
4)基于分布式编程的方法:多核环境中可以使用分布式的消息传递编程模型,Intel的编译器就支持在多核环境中使用MPI编程。但是,在多核环境中使用消息传递编程会带来性能上的损失,并且不是所有的共享数据类型都适用消息传递模型来解决,例如,查找算法在多核中就不适用。[1,9]

 

 

并行编程中多进程和多线程,什么情况下多进程能解决的多线程无法解决?

 

著作权归作者所有。

商业转载请联系作者获得授权,非商业转载请注明出处。

作者:vczh

链接:https://www.zhihu.com/question/25390536/answer/30625782

来源:知乎

 

不同进程里面的线程和相同进程里面的线程的唯一区别就是有没有共享同一个虚拟地址空间,所以原则上不存在多进程能解决而多线程搞不了的。当然现实上总是会受到一些限制,譬如说你的一台机器就是不够用,只能用100台机器才能解决问题,这个时候不同机器之间肯定不能跑同一个线程了,所以只能是多个进程。不过有些人总是生怕自己的系统会因为崩溃从而肉体上受到惩罚,所以倾向于用多进程的方法来做。如果是出于这个理由的话,显然只是拆东墙补西墙,完全没有解决根本问题。我们SQLAzure的架构师说得好,分布式系统的节点是一定会挂掉的,而你能做的,是控制他们以怎样的方式来挂掉。

 

 

系统的资源管理是以进程为单位的,多线程系统中任何一个线程的错误都会导致整个进程崩溃。所以多进程的一个明显的好处就是系统的可用性高。

 

网络服务器的一个典型架构就是一个monitor主进程负责启动和监控多个worker进程,如果某个worker进程崩溃了或者无法正常提供服务,monitor主进程就杀掉这个进程,同时另启一个新的worker进程。

异:running态的线程的退出只受函数的执行完毕,不能用任何方法中断一个执行中的线程。
进程就可以被父进程轻易杀死(尽管有时候可能导致结果异常),在进程中由数据异常,或者并发太高而引起的崩溃还可以被守护进程自动拉起。而错误的线程编程可能导致整个进程的崩溃。
同:线程之间的数据是可以互相独立(thread_local),可以互相共享。
子父进程也是如此(fork,vfork)但在IPC上面处理略复杂。
栗子:多进程的案例简直不要太多,几乎所有的webserver服务器服务都是多进程的,至少有一个守护进程配合一个worker进程,例如apached,httpd等等以d结尾的进程包括init.d本身就是0级总进程,所有你认知的进程都是它的子进程,如果是前置nginx,那么无疑也是多进程的案例。甚至还有专门托管多进程的工具supervisor。你能轻易接受到的chrome浏览器也是多进程的方式。
需要注意的是多进程间的切换代价比线程更大,但是编程相对容易,通常不需要考虑锁同步资源的问题,适合并行计算,大数据分析等集群环境。多线程适合查询密集型,统一的锁管理,避免脏数据的读取。单进程下并发能力高。

 

 

在现有的编程方法中,多进程技术和多线程技术都是比较流行的提高软件性能的编

程方法。在 1.3 节中提到多线程技术是多核编程的重要方法,实际上多进程技术同样是

多核编程的方法。多进程技术是指软件通过多个进程的相互合作完成软件功能的方法,

而多线程技术则是通过多个线程。每一个进程或线程都能占用一个多核处理器的处理单

元,当程序完全占用处理器的运算资源并且能有效工作的时候,程序的数据处理能力就

得到很大程度的提高。多进程技术与多线程技术在实际的应用中各有长处,下面分几个

方面来说明。

在软件的运行效率上,多线程技术要优于多进程技术,这是因为线程所占用的资源

远少进程,线程的创建与维护相对进程而言要高效很多。在软件的健壮性上,多进程技

术则优于多线程技术,因为进程都拥用自己独立的资源,当某个进程发生错误崩溃后其

他进程不会受到影响,而线程是共享资源的,一个线程的崩溃会导致整个软件崩溃。最

后,从编程的角度来看,多线程程序要比多进程程序更容易编写,因为线程间的同步方

法比进程间的要多,并且市场上大部分的并行编程技术都只技持多线程,比如 OpenMP

目前,使用多进程技术的软件主要是网页浏览器,如 Chrome 和搜狗浏览器,这样

可以避免某个网页出错的时候其他网页都不能运行。而其他大部分都软件都是使用多线

程技术,如现在流行的即时通信软件 QQ,各个对话窗口对应一个独立的线程。结合本

文的软控制器的实际需求可知,运行效率是首要考虑因素,因此应该采用多线程技术。

 

区别:

进程优点:编程、调试简单,可靠性高。

进程缺点:创建、销毁、切换速度慢,内存、资源占用大。

线程优点:创建、销毁、切换速度快,内存、资源占用小。

线程缺点:编程、调试复杂,可靠性差。

 

多进程或线程数 <= CPU数量:并行运行。

多进程或线程数 > CPU数量并发运行。

并发运行存在CPU的竞争,并行运行不存在CPU的竞争。并行运行的效率显然要高于并发运行,在多CPU的计算机中,如果只运行一个进程或线程,就不能发挥多CPU的优势。

 

并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”,它的思想简单介绍如下:在操作系统的管理下,所有正在运行的进程轮流使用CPU,每个进程允许占用CPU的时间非常短(比如10毫秒),这样用户根本感觉不出来CPU是在轮流为多个进程服务,就好象所有的进程都在不间断地运行一样。但实际上在任何一个时间内有且仅有一个进程占有CPU

 

Apache Spark的高性能一定程度上取决于它采用的异步并发模型(这里指server/driver端采用的模型),这与Hadoop 2.0(包括YARN和MapReduce)是一致的。Hadoop 2.0自己实现了类似Actor的异步并发模型,实现方式是epoll+状态机,而Apache Spark则直接采用了开源软件Akka,该软件实现了Actor模型,性能非常高。尽管二者在server端采用了一致的并发模型,但在任务级别(特指Spark任务和MapReduce任务)上却采用了不同的并行机制:Hadoop MapReduce采用了多进程模型,而Spark采用了多线程模型。

注意,本文的多进程和多线程,指的是同一个节点上多个任务的运行模式。无论是MapReduce和Spark,整体上看,都是多进程:MapReduce应用程序是由多个独立的Task进程组成的;Spark应用程序的运行环境是由多个独立的Executor进程构建的临时资源池构成的。

 

 

其实这个问题,某种程度上取决于你采用的操作系统平台:

1windows根本就不支持多进程模型;

2linux两者都支持,但实际上是以轻量级进程模拟实现的线程;

3、其它版本UNIX对线程的支持程度各有不同,但对进程的支持都很完善;

4*nux平台上都更推荐采用多进程,原因之一是进程与线程的开销其实没多大区别,进程由于不用考虑共享变量和临界资源之类的问题,实现起来更简单,之二则是更易于把故障限制在某个局部,系统整体的稳定性更好;

 

 

嗯,unixlinux上推荐使用多进程,windows上使用多线程。

 

thread提出有一部分原因就是来因为IPC效率低下,像这样使用多进程仅仅是把本来应该自己做的同步交给了OS去完成。而且最终数据要汇集到一个进程去最终完成,这样的话效率最终很可能就被这最后一个进程限制住,从而影响了整体的效率。

 

个人认为如果真的要用多进程,应当是用于提高并发程度,并利用进程对于数据的保护提高程序的健壮性。

 

 

Z考虑的问题之前也考虑过,我现在采用epoll加多线程的模型。下面说说各种方式的优缺点。

 

优点:

多进程的好处是稳定性高,创建多个子进程来处理数据,一个子进程崩溃不会影响到其它进程,所以服务不会中断。

多线程的好处是写程序更加灵活,线程与其宿主进程共享堆栈,所以只要控制好同步,写起程序来和单线程没什么区别。另外线程的开销比进程小也是其优点,不过基本可以忽略不计。在*unx下,线程可以当做简化版本的进程。

 

缺点:

多进程,由于是不同的内存空间,所以要共享数据就很麻烦,我采用共享内存+用管道的方式控制,不过这样做增加了编程难度,很容易出错。而且业务逻辑基本都是由一个单独的进程实现,如果这个进程崩溃了那网络层还在跑也就没什么意义了,不幸的是业务逻辑是最容易出错的地方。

多线程,由于其使用宿主进程的堆栈,所以如果一个线程出错或锁死整个进程基本上就玩完了。

 

对比之下我选择了多线程模型,在相同的需求下多线程实现起来更简单清晰,不容易出错。而且就算出错,多进程的结果也和多线程一样,不会好太多。

 

LZ说的多进程上加多线程的方式,这个方式我也实现了一个原型,效果跟多进程和多线程一样,没有什么优势,只会增加出错几率而已。编程的时候要考虑一下硬件需求,现今的硬件及网络环境,多进程或多线程就足已满足要求了。

 

看到LS的提到惊群,顺便说两句。惊群现象不完全是坏的,当accept时引发惊群是可以接受的。因为连接是不可以丢弃的,必须让用户第一时间连接上服务器,所以需要由同时多个线程等待连接进,引起惊群现象也再所难免。当然普通的read/write操作引起惊群是不能接受的。

 

至于CPU利用率问题,理论上讲当工作的线程/进程数量为CPU个数*2+1CPU利用率最高。这里CPU个数包含核心的个数,例如双核U就是2*2+1,四核U就是4*2+1

 

谈谈dpdk应用层包处理程序的多进程和多线程模型选择时的若干考虑

分类: 网络  |  作者: miaomiaodmiaomiao 相关  |  发布日期 : 2014-09-24  |  热度 :717°

看到知乎上有个关于linux多进程、多线程的讨论:链接地址

自己项目里也对这个问题有过很多探讨和测试,所以正好开贴整理一下,题目有点长,其实就2点:

1. 多进程模型和多线程模型,这两种模型在linux上有什么区别,各有何优缺点?

    这里仅限于linux平台,因为linux平台跟win平台关于线程的实现差异很大。

2. 采用inteldpdk做包处理程序,是采用多进程模型好,还是多线程模型好?

 这里仅限于包处理程序(ips,waf,其他网络设备引擎),因为不同应用场景区别也很大。

首先知乎里边的评论,有个miao网友说的跟我的经验比较相符,先将其说法贴一下:

"linux使用的1:1的线程模型,在内核中是不区分线程和进程的,都是可运行的任务而已。fork调用clone(最少的共享),pthread_create也是调用clone(最大共享).fork创建会比pthread_create多消耗一点点,因为要拷贝tables和cow mapping.但是其实差别真的很细微,这些在内核开发者的努力下已经变的很小了。
再来说说contex switch的cost吧。线程的context switch是要比process小一些,因为线程共享了大部分的memory和tables,当switch的时候这些东西已经在缓存中了。
但是其实差别也很细微。但是在multiprocessor的系统中不共享memory其实是会比共享memory要有一点优势的,因为当任务在不同的processor中运行的时候,同步memory带来的损耗是不可忽视的。"

 

他这里说了两点有价值的信息,1  linux里的线程实现决定,创建、调度、切换线程的开销跟进程相比,好不了多少。

              2 多核CPU下由于缓存命中率的问题,进程这种天生不共享内存的做法,实际上比线程这种天生共享内存

                的做法,从性能上是有好处的。

这两点见解跟我们项目实际测试和研究结果是相符合的。下面从几个方面探讨这些问题:

 

1 linux 线程创建方式

linux提供的线程实际上是核外线程,即主要的线程机制是通过应用层面的库pthread提供的(线程的id分配、线程创建和管理,据说基本实现是pthread库为每一个进程维护一个管理线程,单调用 pthread_create等posix API时,调用者与该管理线程通过管道传递命令),

核内层面,线程几乎可以等同于进程  这里贴一段从引用1 拷贝的内容:

Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用__clone()和fork(),最终都用不同的参数调用do_fork()核内API。 do_fork() 提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当使用fork系统调用产生多进程时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境。当使用pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的”进程”拥有共享的运行环境,只有栈是独立的,由 __clone()传入。

         即:Linux下不管是多线程编程还是多进程编程,最终都是用do_fork实现的多进程编程,只是进程创建时的参数不同,从而导致有不同的共享环境。Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。pthread 库使用一个管理线程(__pthread_manager() ,每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号,而主线程pthread_create()) 的调用者则通过管道将请求信息传给管理线程。

上述内容基本可以这么表示:

创建进程= fork ——> do_fork(不使用共享属性)

      创建线程=pthread_create——>__clone ——>do_fork(共享地址空间(代码区、数据区)、页表、文件描述符、信号。。)

这里其实另外一种多进程创建方式,就是脚本直接启动多个进程

 

下面再贴一段:

“对于一个进程来说必须有的数据段、代码段、堆栈段是不是全盘复制呢?对于多进程来说,代码段是肯定不用复制的,因为父进程和各子进程的代码段是相同的,数据段和堆栈段呢?也不一定,因为在Linux里广泛使用的一个技术叫copy-on-write,即写时拷贝。copy-on-write意味着什么呢?意味着资源节省,假设有一个变量x在父进程里存在,当这个父进程创建一个子进程或多个子进程时这个变量x是否复制到了子进程的内存空间呢?不会的,子进程和父进程使用同一个内存空间的变量,但当子进程或父进程要改变变量x的值时就会复制该变量,从而导致父子进程里的变量值不同。”

这里我的理解是,刚fork完,子进程和父进程代码段、页表等还是共享的,接下去有两种可能发展方向,1是子进程修改了数据,这时候,代码段:仍然是共享的,不需要拷贝;堆和静态数据区: 根据copy-on-wirte机制,不改变值的地方仍然共享,改变值的地方需要重新申请物理页面并修改值,修改页表(可能还要拷贝页表);栈: 不管进程还是线程,都不能共享,都需要创建的时候分配栈区。2是fork之后马上调用 exec 用新的进程替换,这时候会载入新的代码段、数据段,构建新的页表。

对于我们的包处理系统而已,无论怎么启动,创建时的性能开销其实是无所谓的,因为都是在系统初始化的时候创建。

 

2 调度和切换

由于核内的线程本质就是进程,其调度过程跟进程一样。切换,不论是进程切换还是线程切换,都需要替换运行环境(内核堆栈,运行时寄存器等),对于内存的切换,内核部分内存是一样的,用户空间部分:如果是进程,需要替换页目录基址寄存器,如果是线程,不需要替换;总体而言,linux进程和线程的切换,从内存寄存器、内核堆栈寄存器、其他寄存器等的换值开销应该是差不多的。具体切换代码参考引用2

但是由于多线程共享地址空间,从一个线程切换到同一个进程上另一个线程运行,页表,数据区等很多都已经在内存甚至缓存里,而从一个进程切换到另一个进程,可能由于刚切换进来的进程的页面被虚拟内存管理模块替换出去导致的页面替换开销,另外还有缓存tlb失效导致的缓存更新开销,这里性能有所差别。

 对于我们的包处理系统而已,采用多核架构,主体进程/线程是绑定到不同的物理CPU core上并独占的,所以发生调度和切换的情况不多,因而这种影响不是很重要。

1、多核之间同步一级二级三级缓存数据;
2、多线程的上下文切换;
3、缓存行数据的竞争;


实验设计不够清晰,得不到有效的结论,我觉得需要重新设计实验。我能想到影响这种环境下多线程性能的影响因素,从体系结构的角度出发,大概有: SMT,缓存一致性, DVFS,缓存共享等方面。具体来说,E5645这个处理器通过HT的话才有12个线程,超线程出来的线程不能简单视为的12个完全相同的硬件线程;缓存一致性来说,你的线程数目大于单个E5645的线程数目,基本可以确定线程会分布在两个处理器上运行,这样一致性的开销将会变大;动态频率调整,这个先不说了,和我正在做的研究相关;缓存共享,一方面假共享,另一方面就是对缓存资源的竞争。 如果真的想做对比,我的建议是,首先实现一个无锁无同步的版本,禁用超线程和DVFS,线程和core进行绑定,线程数目不要超过6,实验内容要包括多进程和多线程,n进程的性能应该会小于一个进程性能的n倍的,所以你比较的时候不能简单的从一个进程推算n个进程。

 

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:徐烨
链接:http://www.zhihu.com/question/25520997/answer/31061996
来源:知乎

static volatile unsigned long op[100];
static volatile int g_rand = 0;
static volatile unsigned long pre_op[100];

/*用于测试的不同锁*/

pthread_mutex_t g_mutex[100];

pthread_rwlock_t g_mutex_rw[100];

pthread_spinlock_t g_spinlock[100];

/*500W的Hash表*/

#define HASH_SIZE (5*1024*1024)

static unsigned int *g_hash[32];

/*测试线程

static void *thread_select(void *arg)

{

               int i= * (int *)arg;

               printf("thread:%d\n",i);

                cpu_set_tmask;

            cpu_set_t get;

            char buf[256];

            int num = sysconf(_SC_NPROCESSORS_CONF);

            printf("system has %dprocessor(s)\n", num);

            CPU_ZERO(&mask);

            CPU_SET(i, &mask);

            /*设置线程的CPU亲缘性。

             */

//            if(pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) < 0) {

//             fprintf(stderr, "set thread affinity failed\n");

//        }

 

//          unsigned int obj = rand();

               while(1)

               {

/***1.是否加锁****/

//                     pthread_mutex_lock(&g_mutex[i]);

//                     pthread_spin_lock(&g_spinlock[i]);

//                     pthread_rwlock_rdlock(&g_mutex_rw[i]);

                       //             time(NULL);

//                                    rand();

                       //      操作计数

                       op[i]++;

                       unsignedint obj  = g_rand;

                       memcmp(&obj,   g_hash[i] + (g_rand % HASH_SIZE ) ,sizeof(unsigned int )   );

//                     pthread_rwlock_unlock(&g_mutex_rw[i]);

//                     pthread_spin_unlock(&g_spinlock[i]);

//                     pthread_mutex_unlock(&g_mutex[i]);

               }

 

}

/*打印线程*/

void *thread_print(void *arg)

{

    int n;

    while(1)

    {

        /*打印前四个线程当前总的操作计数*/

 //     printf("op[3]:%lu\n",op[3]);

       printf("thread0_op_times: %lu pps     thread1_op_times:%lu pps        thread2_op_times: %lu pps      thread3_op_times: %lu pps\n",  op[0], op[1] ,op[2], op[3]  );

        unsignedlong all_op = 0;

        int j = 0;

        for(j = 0;j < 32; j ++)

        {

//             pthread_mutex_lock(&g_mutex[j]);

               all_op += (op[j] - pre_op[j]);

               pre_op[j] = op[j];

 //            pthread_mutex_unlock(&g_mutex[j]);

        }

        /*打印休眠周期内的总操作次数*/

       printf("all_op:%lu\n", all_op );

        all_op = 0;

        /*休眠*/

        sleep(10);

//       usleep(1000 * 990);

    }

    return NULL;

}

void *thread_rand(void *arg)

{

        while(1)

        {

               g_rand= rand();

        }

}

int main(void)

{

        pthread_tt[40];

        int i = 0;

        intthread_id[32];

        inttotal_num = 0;

        /*初始化随机数种子

         */

       srand(time(NULL));

        /*初始化锁与计数

         */

               /*启动随机数生成线程*/

               pthread_create(&t[34],NULL, thread_rand, NULL);

        for( i = 0; i < 32; i ++)

        {

               op[i] = 0;

               pre_op[i] = 0;

               pthread_mutex_init(&g_mutex[i],NULL);

               pthread_rwlock_init(&g_mutex_rw[i], NULL);

               pthread_spin_init(&g_spinlock[i],0);

               g_hash[i] = (unsigned int *)malloc(sizeof(unsigned int ) * HASH_SIZE );

               memset(g_hash[i],0,   sizeof(unsigned int ) * HASH_SIZE );

        }

               printf("startcreate thread_select\n");

                /*线程优先级*/

               structsched_param param;

               param.__sched_priority= 20;

               pthread_attr_tattr;

               pthread_attr_init(&attr);

               pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);

               pthread_attr_setschedpolicy(&attr,SCHED_FIFO);

               pthread_attr_setschedparam(&attr,&param);

               /*启动打印线程*/

               pthread_create(&t[35],NULL, thread_print, NULL);

               /*线程编号*/

        for(i=0;i<32;i++){

          thread_id[i]=i;

        }

        /***在这里修改线程数****/

               for(i=0;i< 1;i++){

                       /*是否设置线程的优先级*/

                       pthread_create(&t[i],NULL, thread_select, (void *)&thread_id[i]);

                   pthread_create(&t[i],&attr, thread_select, (void *)&thread_id[i]);

               }

 

        while(1) {

               sleep(1);

        }

        return 0;

}

 

 

 

内存的速度确实跟磁盘速度相差很大,所以采用虚拟内存等方法来,来提高内存的利用率

寄存器(1ns---》高速缓存(2ns---》内存(10ns----》磁盘(10ms-----》磁带(0.1s

 

针对 Cache 的优化

在串行程序设计过程中,为了节约带宽或者存储空间,比较直接的方法,就是对数据结构做一些针对性的设计,将数据压缩 (pack) 的更紧凑,减少数据的移动,以此来提高程序的性能。但在多核多线程程序中,这种方法往往有时会适得其反。

数据不仅在执行核和存储器之间移动,还会在执行核之间传输。根据数据相关性,其中有两种读写模式会涉及到数据的移动:写后读和写后写,因为这两种模式会引发数据的竞争,表面上是并行执行,但实际只能串行执行,进而影响到性能。

处理器交换的最小单元是 cache 行,或称 cache 块。在多核体系中,对于不共享 cache 的架构来说,两个独立的 cache 在需要读取同一 cache 行时,会共享该 cache 行,如果在其中一个 cache 中,该 cache 行被写入,而在另一个 cache 中该 cache 行被读取,那么即使读写的地址不相交,也需要在这两个 cache 之间移动数据,这就被称为 cache 伪共享,导致执行核必须在存储总线上来回传递这个 cache 行,这种现象被称为乒乓效应

同样地,当两个线程写入同一个 cache 的不同部分时,也会互相竞争该 cache 行,也就是写后写的问题。上文曾提到,不加锁的方案反而比加锁的方案更慢,就是互相竞争 cache 的原因。

X86 机器上,某些处理器的一个 cache 行是64字节,具体可以参看 Intel 的参考手册。

既然不加锁三线程方案的瓶颈在于 cache,那么让 apple 的两个成员 a  b 位于不同的 cache 行中,效率会有所提高吗?

修改后的代码片断如下:

清单 4. 针对Cache的优化

struct apple

{

         unsignedlong long a;

         charc[128];  /*32,64,128*/

         unsignedlong long b;

};

测量结果如下图所示:

图 4. 增加 Cache 时间耗时对比图

小小的一行代码,尽然带来了如此高的收益,不难看出,我们是用空间来换时间。当然读者也可以采用更简便的方法: __attribute__((__aligned__(L1_CACHE_BYTES))) 来确定 cache 的大小。

如果对加锁三线程方案中的 apple 数据结构也增加一行类似功能的代码,效率也是否会提升呢?性能不会有所提升,其原因是加锁的三线程方案效率低下的原因不是 Cache 失效造成的,而是那把锁。

在多核和多线程程序设计过程中,要全盘考虑多个线程的访存需求,不要单独考虑一个线程的需求。在选择并行任务分解方法时,要综合考虑访存带宽和竞争问题,将不同处理器和不同线程使用的数据放在不同的 Cache 行中,将只读数据和可写数据分离开。

 

 

 

http://blog.sina.com.cn/s/blog_53a1165e0102v5tr.html

 

现在很多服务器都自带双千兆网口,利用网卡绑定既能增加网络带宽,同时又能做相应的冗余,目前应用于很多的场景。linux操作系统下自带的网卡绑定模式,Linux bonding驱动提供了一个把多个网络接口设备捆绑为单个网络接口设置来使用,用于网络负载均衡及网络冗余。当然现在网卡产商也会出一些针对windows操作系统网卡管理软件来做网卡绑定(windows操作系统没有网卡绑定功能 需要第三方支持)。

我们公司是做分布式文件系统的,很多项目都用到网卡绑定来提高性能。在网络找了很多资料,也做了大量的测试,下面就网卡绑定谈一下自己的看法。

一、         Bonding的应用

                     1         网络负载均衡

对于bonding的网络负载均衡是我们在文件服务器中常用到的,比如把三块网卡,当做一块来用,解决一个IP地址,流量过大,服务器网络压力过大的问题。如果在内网中,文件服务器为了管理和应用上的方便,大多是用同一个IP地址。对于一个百M的本地网络来说,文件服务器在多个用户同时使用的情况下,网络压力是极大的,为了解决同一个IP地址,突破流量的限制,毕竟网线和网卡对数据的吞吐量是有限制的。如果在有限的资源的情况下,实现网络负载均衡,最好的办法就是bonding 

                     2         网络冗余

对于服务器来说,网络设备的稳定也是比较重要的,特别是网卡。大多通过硬件设备的冗余来提供服务器的可靠性和安全性,比如电源。bonding 也能为网卡提供冗余的支持。把网个网卡绑定到一个IP地址,当一块网卡发生物理性损坏的情况下,另一块网卡也能提供正常的服务。

二、         Bonding的原理

什么是bonding需要从网卡的混杂(promisc)模式说起。我们知道,在正常情况下,网卡只接收目的硬件地址(MAC Address)是自身Mac的以太网帧,对于别的数据帧都滤掉,以减轻驱动程序的负担。但是网卡也支持另外一种被称为混杂promisc的模式,可以接 收网络上所有的帧,比如说tcpdump,就是运行在这个模式下。bonding也运行在这个模式下,而且修改了驱动程序中的mac地址,将两块网卡的 Mac地址改成相同,可以接收特定mac的数据帧。然后把相应的数据帧传送给bond驱动程序处理。

三、         Bonding的模式

linux有七种网卡绑定模式:

模式代号

模式名称

模式方式

说明

0

(balance-rr) Round-robin policy

轮询策略

该策略是按照设备顺序依次传输数据包,直到最后一个设备。这种模式提供负载均衡和容错能力。

1

(active-backup) Active-backup policy

主备策略

该策略只有一个设备处于活动状态。 一个宕掉另一个马上由备份转换为主设备。mac地址是外部可见的。 此模式提供了容错能力。

2

(balance-xor) XOR policy

异或策略

该策略是根据MAC地址异或运算的结果来选择传输设备,提供负载均衡和容错能力。

3

Broadcast policy

广播策略

该策略将所有数据包传输给所有接口通过全部设备来传输所有数据,提供容错能力。

4

(802.3ad) IEEE 802.3ad Dynamic link aggregation

动态链接聚合

该策略通过创建聚合组来共享相同的传输速度,需要交换机也支持 802.3ad 模式,提供容错能力。

5

(balance-tlb) Adaptive transmit load balancing

适配器传输负载均衡

该策略是根据当前的负载把发出的数据分给每一个设备,由当前使用的设备处理收到的数据。本策略的通道联合不需要专用的交换机支持,提供负载均衡和容错能力。

 

6

(balance-alb) Adaptive load balancing

适配器负载均衡

该策略在IPV4情况下包含适配器传输负载均衡策略,由ARP协商完成接收的负载,通道联合驱动程序截获ARP在本地系统发送出的请求,用其中一个设备的硬件地址覆盖从属设备的原地址。

 

 

 

http://blog.csdn.net/wuyuxing24/article/details/48758927

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值