精华内容
下载资源
问答
  • 多线程还是多进程选择及区别

    万次阅读 多人点赞 2014-01-05 23:42:36
    本人博客将陆续迁至https://dpjeep.com 欢迎造访 ...感觉这篇博文写的很棒,特此转载了   鱼还是熊掌:浅谈多进程多线程的选择 关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配...

    本人博客将陆续迁至https://dpjeep.com 欢迎造访

    原文:http://blog.csdn.net/lishenglong666/article/details/8557215  最原始的博主我没有找到,只能把我从何处转的此篇博文的链接发出来。感觉这篇博文写的很棒,特此转载了

     

    鱼还是熊掌:浅谈多进程多线程的选择

    关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有这么简单了,选的不好,会让你深受其害。

     

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

     

    我们按照多个不同的维度,来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。

     

     

     

    对比维度

    多进程

    多线程

    总结

    数据共享、同步

    数据共享复杂,需要用IPC;数据是分开的,同步简单

    因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂

    各有优势

    内存、CPU

    占用内存多,切换复杂,CPU利用率低

    占用内存少,切换简单,CPU利用率高

    线程占优

    创建销毁、切换

    创建销毁、切换复杂,速度慢

    创建销毁、切换简单,速度很快

    线程占优

    编程、调试

    编程简单,调试简单

    编程复杂,调试复杂

    进程占优

    可靠性

    进程间不会互相影响

    一个线程挂掉将导致整个进程挂掉

    进程占优

    分布式

    适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单

    适应于多核分布式

    进程占优

     

     

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

    原因请看上面的对比。

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

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

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

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

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

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

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

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

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

    原因请看上面对比。

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

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

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

     

    消耗资源:

    从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

    线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

    通讯方式:

    进程之间传递数据只能是通过通讯的方式,即费时又不方便。线程时间数据大部分共享(线程函数内部不共享),快捷方便。但是数据同步需要锁对于static变量尤其注意

    线程自身优势:

    提高应用程序响应;使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的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_NUMBER和COUNT分别定义,程序编译指令如下:
    gcc -o fork fork.c
    gcc -lpthread -o thread thread.c

    实验通过time指令执行两个程序,抄录time输出的挂钟时间(real时间):

    time ./fork
    time ./thread

    每批次的实验通过改动宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次测试五轮,得到的结果如下:

    一、重复周丽论文实验步骤

    (注:本文平均值算法采用的是去掉一个最大值去掉一个最小值,然后平均)

     

    单核(双核机器禁掉一核),进程/线程数:255,打印次数5

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m0.070s

     0m0.071s

    0m0.071s 

    0m0.070s 

    0m0.070s 

    0m0.070s 

    多线程

     0m0.049s

    0m0.049s 

    0m0.049s 

    0m0.049s 

    0m0.049s 

    0m0.049s 

     

    单核(双核机器禁掉一核),进程/线程数:255,打印次数10

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m0.112s

    0m0.101s 

    0m0.100s 

    0m0.085s 

    0m0.121s 

    0m0.104s 

    多线程

     0m0.097s

    0m0.089s 

    0m0.090s 

    0m0.104s 

    0m0.080s 

    0m0.092s 

     

     

    单核(双核机器禁掉一核),进程/线程数:255,打印次数50

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m0.459s

    0m0.531s 

    0m0.499s 

    0m0.499s 

    0m0.524s 

    0m0.507s 

    多线程

     0m0.387s

    0m0.456s 

    0m0.435s 

    0m0.423s 

    0m0.408s 

    0m0.422s 

     

     

    单核(双核机器禁掉一核),进程/线程数:255,打印次数100

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m1.141s

    0m0.992s 

    0m1.134s 

    0m1.027s 

    0m0.965s 

    0m1.051s 

    多线程

     0m0.925s

    0m0.899s 

    0m0.961s 

    0m0.934s 

    0m0.853s 

    0m0.919s 

     

     

    单核(双核机器禁掉一核),进程/线程数:255,打印次数500

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m5.221s

    0m5.258s 

    0m5.706s 

    0m5.288s 

    0m5.455s 

    0m5.334s

    多线程

     0m4.689s

    0m4.578s 

    0m4.670s 

    0m4.566s 

    0m4.722s 

    0m4.646s 

     

     

    单核(双核机器禁掉一核),进程/线程数:255,打印次数1000

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m12.680s

    0m16.555s 

    0m11.158s 

    0m10.922s 

    0m11.206s 

    0m11.681s 

    多线程

     0m12.993s

    0m13.087s 

    0m13.082s 

    0m13.485s 

    0m13.053s 

    0m13.074s 

     

     

    单核(双核机器禁掉一核),进程/线程数:255,打印次数5000

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     1m27.348s

    1m5.569s 

    0m57.275s 

    1m5.029s 

    1m15.174s 

    1m8.591s 

    多线程

     1m25.813s

    1m29.299s

    1m23.842s 

    1m18.914s 

    1m34.872s 

    1m26.318s 

     

     

    单核(双核机器禁掉一核),进程/线程数:255,打印次数10000

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     2m8.336s

    2m22.999s 

    2m11.046s 

    2m30.040s 

    2m5.752s 

    2m14.137s 

    多线程

     2m46.666s

    2m44.757s 

    2m34.528s 

    2m15.018s 

    2m41.436s 

    2m40.240s 


    出的结果是:任务量较大时,多进程比多线程效率高;而完成的任务量较小时,多线程比多进程要快,重复打印 600 次时,多进程与多线程所耗费的时间相同。

     

     

     

     

    、增加每进程/线程的工作强度的实验

    这次将程序打印数据增大,原来打印字符串为:

    1. char *s = "hello linux\0";

    现在修改为每次打印256个字节数据:

    1. char *s = "1234567890abcdef\
    2.     1234567890abcdef\
    3.     1234567890abcdef\
    4.     1234567890abcdef\
    5.     1234567890abcdef\
    6.     1234567890abcdef\
    7.     1234567890abcdef\
    8.     1234567890abcdef\
    9.     1234567890abcdef\
    10.     1234567890abcdef\
    11.     1234567890abcdef\
    12.     1234567890abcdef\
    13.     1234567890abcdef\
    14.     1234567890abcdef\
    15.     1234567890abcdef\
    16.     1234567890abcdef\0";

     

    单核(双核机器禁掉一核),进程/线程数:255  ,打印次数100

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m6.977s

     0m7.358s

     0m7.520s

     0m7.282s

     0m7.218s

     0m7.286

    多线程

     0m7.035s

     0m7.552s

     0m7.087s

     0m7.427s

     0m7.257s

     0m7.257

     

    单核(双核机器禁掉一核),进程/线程数:  255,打印次数500

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m35.666s

     0m36.009s

     0m36.532s

     0m35.578s

     0m41.537s

     0m36.069

    多线程

     0m37.290s

     0m35.688s

     0m36.377s

     0m36.693s

     0m36.784s

     0m36.618

     

    单核(双核机器禁掉一核),进程/线程数: 255,打印次数1000

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     1m8.864s

     1m11.056s

     1m10.273s

     1m12.317s

     1m20.193s

     1m11.215

    多线程

     1m11.949s

     1m13.088s

     1m12.291s

     1m9.677s

     1m12.210s

     1m12.150

     

    【实验结论】

    从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了很多,唉,又是据说!),多线程比较多进程在效率上没有任何的优势,在线程数增大时多线程程序还出现了运行错误,实验可以得出下面的结论:

    在Linux2.6上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优势。

    四、多进程和多线程在创建和销毁上的效率比较

    预先创建进程或线程可以节省进程或线程的创建、销毁时间,在实际的应用中很多程序使用了这样的策略,比如Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中,进程或线程的创建、销毁是比较耗时的,在stevesn的著作《Unix网络编程》中有这样 的对比图(第一卷 第三版 30章 客户/服务器程序设计范式):

     

    行号服务器描述进程控制CPU时间(秒,与基准之差)
    Solaris2.5.1Digital Unix4.0bBSD/OS3.0
    0迭代服务器(基准测试,无进程控制)0.00.00.0
    1简单并发服务,为每个客户请求fork一个进程504.2168.929.6
    2预先派生子进程,每个子进程调用accept 6.21.8
    3预先派生子进程,用文件锁保护accept25.210.02.7
    4预先派生子进程,用线程互斥锁保护accept21.5  
    5预先派生子进程,由父进程向子进程传递套接字36.710.96.1
    6并发服务,为每个客户请求创建一个线程18.74.7 
    7预先创建线程,用互斥锁保护accept8.63.5 
    8预先创建线程,由主线程调用accept14.55.0 

     

    stevens已驾鹤西去多年,但《Unix网络编程》一书仍具有巨大的影响力,上表中stevens比较了三种服务器上多进程和多线程的执行效 率,因为三种服务器所用计算机不同,表中数据只能纵向比较,而横向无可比性,stevens在书中提供了这些测试程序的源码(也可以在网上下载)。书中介 绍了测试环境,两台与服务器处于同一子网的客户机,每个客户并发5个进程(服务器同一时间最多10个连接),每个客户请求从服务器获取4000字节数据, 预先派生子进程或线程的数量是15个。

    第0行是迭代模式的基准测试程序,服务器程序只有一个进程在运行(同一时间只能处理一个客户请求),因为没有进程或线程的调度切换,因此它的速度是 最快的,表中其他服务模式的运行数值是比迭代模式多出的差值。迭代模式很少用到,在现有的互联网服务中,DNS、NTP服务有它的影子。第1~5行是多进 程服务模式,期中第1行使用现场fork子进程,2~5行都是预先创建15个子进程模式,在多进程程序中套接字传递不太容易(相对于多线 程),stevens在这里提供了4个不同的处理accept的方法。6~8行是多线程服务模式,第6行是现场为客户请求创建子线程,7~8行是预先创建 15个线程。表中有的格子是空白的,是因为这个系统不支持此种模式,比如当年的BSD不支持线程,因此BSD上多线程的数据都是空白的。

    从数据的比对看,现场为每客户fork一个进程的方式是最慢的,差不多有20倍的速度差异,Solaris上的现场fork和预先创建子进程的最大差别是504.2 :21.5,但我们不能理解为预先创建模式比现场fork快20倍,原因有两个:

    1. stevens的测试已是十几年前的了,现在的OS和CPU已起了翻天覆地的变化,表中的数值需要重新测试。

    2. stevens没有提供服务器程序整体的运行计时,我们无法理解504.2 :21.5的实际运行效率,有可能是1504.2 : 1021.5,也可能是100503.2 : 100021.5,20倍的差异可能很大,也可能可以忽略。

    因此我写了下面的实验程序,来计算在Linux2.6上创建、销毁10万个进程/线程的绝对用时。

    创建10万个进程(forkcreat.c):

    1. #include <stdio.h>
    2. #include <signal.h>
    3. #include <stdio.h>
    4. #include <unistd.h>
    5. #include <sys/stat.h>
    6. #include <fcntl.h>
    7. #include <sys/types.h>
    8. #include <sys/wait.h>
    9.  
    10. int count;//子进程创建成功数量 
    11. int fcount;//子进程创建失败数量 
    12. int scount;//子进程回收数量 
    13.  
    14. /*信号处理函数–子进程关闭收集*/
    15. void sig_chld(int signo)
    16. {
    17.     pid_t chldpid;//子进程id
    18.     int stat;//子进程的终止状态
    19.     
    20.     //子进程回收,避免出现僵尸进程
    21.     while((chldpid=wait(&stat)>0))
    22.     {
    23.         scount++;
    24.     }
    25. }
    26.  
    27. int main()
    28. {
    29.     //注册子进程回收信号处理函数
    30.     signal(SIGCHLD,sig_chld);
    31.     
    32.     int i;
    33.     for(i=0;i<100000;i++)//fork()10万个子进程
    34.     {
    35.         pid_t pid=fork();
    36.         if(pid==-1)//子进程创建失败
    37.         {
    38.             fcount++;
    39.         }
    40.         else if(pid>0)//子进程创建成功
    41.         {
    42.             count++;
    43.         }
    44.         else if(pid==0)//子进程执行过程
    45.         {
    46.             exit(0);
    47.         }
    48.     }
    49.     
    50.     printf("count:%d fount:%d scount:%d\n",count,fcount,scount);
    51. }

    创建10万个线程(pthreadcreat.c):

    1. #include <stdio.h>
    2. #include <pthread.h>
    3.  
    4. int count=0;//成功创建线程数量
    5.  
    6. void thread(void)
    7. {
    8.     //啥也不做
    9. }
    10.  
    11. int main(void)
    12. {
    13.     pthread_t id;//线程id
    14.     int i,ret;
    15.     
    16.     for(i=0;i<100000;i++)//创建10万个线程
    17.     {
    18.         ret=pthread_create(&id,NULL,(void *)thread,NULL);
    19.         if(ret!=0)
    20.         {
    21.             printf("Create pthread error!\n");
    22.             return(1);
    23.         }
    24.         count++;
    25.         pthread_join(id,NULL);
    26.     }
    27.     
    28.     printf("count:%d\n",count);
    29. }

    创建10万个线程的Java程序:

    1. public class ThreadTest
    2.     {
    3.         public static void main(String[] ags) throws InterruptedException
    4.         {
    5.             System.out.println("开始运行");
    6.             long start = System.currentTimeMillis();
    7.             for(int i = 0; i < 100000; i++) //创建10万个线程
    8.             {
    9.                 Thread athread = new Thread(); //创建线程对象
    10.                 athread.start(); //启动线程
    11.                 athread.join(); //等待该线程停止
    12.             }
    13.            
    14.             System.out.println("用时:" + (System.currentTimeMillis() – start) + " 毫秒");
    15.         }
    16.     }

     

    单核(双核机器禁掉一核),创建销毁10万个进程/线程

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均

    多进程

     0m8.774s

     0m8.780s

     0m8.475s

     0m8.592s

     0m8.687s

     0m8.684

    多线程

     0m0.663s

     0m0.660s

     0m0.662s

     0m0.660s

     0m0.661s

     0m0.661

    创建销毁10万个线程(Java)
    12286毫秒

     

    从数据可以看出,多线程比多进程在效率上有10多倍的优势,但不能让我们在使用哪种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能只横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理,我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,可以说是有质的飞跃的,平均创建销毁一个进程的速度是0.18毫秒,对于当前服务器几百、几千的并发量,还有预先派生子进程/线程的必要吗?

    预先派生子进程/线程比现场创建子进程/线程要复杂很多,不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的测试程序中,使用了“惊群”和“锁”技术。即使stevens的数据表格中,预先派生线程也不见得比现场创建线程快,在 《Unix网络编程》第三版中,新作者参照stevens的测试也提供了一组数据,在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优势。因此我对这一节实验下的结论是:

    预先派生进程/线程的模式(进程池、线程池)技术,不仅复杂,在效率上也无优势,在新的应用中可以放心大胆地为客户连接请求去现场创建进程和线程。

    我想,这是fork迷们最愿意看到的结论了。

    五、双核系统重复周丽论文实验步骤

     

    双核,进程/线程数:255 ,打印次数10

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均(单核倍数)

    多进程

    0m0.061s

    0m0.053s

    0m0.068s

    0m0.061s

    0m0.059s

     0m0.060(1.73)

    多线程

    0m0.054s

    0m0.040s

    0m0.053s

    0m0.056s

    0m0.042s

     0m0.050(1.84)

     

     

     

    双核,进程/线程数: 255,打印次数100

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均(单核倍数)

    多进程

    0m0.918s

    0m1.198s

    0m1.241s

    0m1.017s

     0m1.172s

     0m1.129(0.93)

    多线程

    0m0.897s

    0m1.166s

    0m1.091s 

    0m1.360s

     0m0.997s

     0m1.085(0.85)

     

     

     

    双核,进程/线程数: 255,打印次数1000

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均(单核倍数)

    多进程

    0m11.276s

    0m11.269s 

    0m11.218s

    0m10.919s

    0m11.201s

     0m11.229(1.04)

    多线程

    0m11.525s

    0m11.984s

    0m11.715s

    0m11.433s

    0m10.966s

     0m11.558(1.13)

     


     

     

    双核,进程/线程数:255 ,打印次数10000

     

    第1次

    第2次

    第3次

    第4次

    第5次

    平均(单核倍数)

    多进程

    1m54.328s

    1m54.748s

    1m54.807s

    1m55.950s

    1m57.655s

     1m55.168(1.16)

    多线程

    2m3.021s

    1m57.611s

    1m59.139s 

    1m58.297s

    1m57.258s 

     1m58.349(1.35)

     


    【实验结论】

    双核处理器在完成任务量较少时,没有系统其他瓶颈因素影响时基本上是单核的两倍,在任务量较多时,受系统其他瓶颈因素的影响,速度明显趋近于单核的速度。

    六、并发服务的不可测性

    看到这里,你会感觉到我有挺进程、贬线程的论调,实际上对于现实中的并发服务具有不可测性,前面的实验和结论只可做参考,而不可定性。对于不可测性,我举个生活中的例子。

    这几年在大都市生活的朋友都感觉城市交通状况越来越差,到处堵车,从好的方面想这不正反应了我国GDP的高速发展。如果你7、8年前来到西安市,穿 过南二环上的一些十字路口时,会发现一个奇怪的U型弯的交通管制,为了更好的说明,我画了两张图来说明,第一张图是采用U型弯之前的,第二张是采用U型弯 之后的。

    南二环交通图一

    南二环交通图一

    南二环交通图二

    南二环交通图二

    为了讲述的方便,我们不考虑十字路口左拐的情况,在图一中东西向和南北向的车辆交汇在十字路口,用红绿灯控制同一时间只能东西向或南北向通行,一般 的十字路口都是这样管控的。随着车辆的增多,十字路口的堵塞越来越严重,尤其是上下班时间经常出现堵死现象。于是交通部门在不动用过多经费的情况下而采用 了图二的交通管制,东西向车辆行进方式不变,而南北向车辆不能直行,需要右拐到下一个路口拐一个超大的U型弯,这样的措施避免了因车辆交错而引发堵死的次 数,从而提高了车辆的通过效率。我曾经问一个每天上下班乘公交经过此路口的同事,他说这样的改动不一定每次上下班时间都能缩短,但上班时间有保障了,从而 迟到次数减少了。如果今天你去西安市的南二环已经见不到U型弯了,东西向建设了高架桥,车辆分流后下层的十字路口已恢复为图一方式。

    从效率的角度分析,在图一中等一个红灯45秒,远远小于图二拐那个U型弯用去的时间,但实际情况正好相反。我们可以设想一下,如果路上的所有运行车 辆都是同一型号(比如说全是QQ3微型车),所有的司机都遵守交规,具有同样的心情和性格,那么图一的通行效率肯定比图二高。现实中就不一样了,首先车辆 不统一,有大车、小车、快车、慢车,其次司机的品行不一,有特别遵守交规的,有想耍点小聪明的,有性子慢的,也有的性子急,时不时还有三轮摩托逆行一下, 十字路口的“死锁”也就难免了。

    那么在什么情况下图二优于图一,是否能拿出一个科学分析数据来呢?以现在的科学技术水平是拿不出来的,就像长期的天气预报不可预测一样,西安市的交管部门肯定不是分析各种车辆的运行规律、速度,再进行复杂的社会学、心理学分析做出U型弯的决定的,这就是要说的不可测性。

    现实中的程序亦然如此,比如WEB服务器,有的客户在快车道(宽带),有的在慢车道(窄带),有的性子慢(等待半分钟也无所谓),有的性子急(拼命 的进行浏览器刷新),时不时还有一两个黑客混入其中,这种情况每个服务器都不一样,既是是同一服务器每时每刻的变化也不一样,因此说不具有可测性。开发者 和维护者能做的,不论是前面的这种实验测试,还是对具体网站进行的压力测试,最多也就能模拟相当于QQ3通过十字路口的场景。

    结束语

    本篇文章比较了Linux系统上多线程和多进程的运行效率,在实际应用时还有其他因素的影响,比如网络通讯时采用长连接还是短连接,是否采用 select、poll,java中称为nio的机制,还有使用的编程语言,例如Java不能使用多进程,PHP不能使用多线程,这些都可能影响到并发模 式的选型。

    最后还有两点提醒:

    1. 文章中的所有实验数据有环境约束。
    2. 由于并行服务的不可测性,文章中的观点应该只做参考,而不要去定性。

    展开全文
  • 进程调度

    千次阅读 2012-12-04 15:27:35
    进程调度 无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于...

     

    进程调度  
    进程调度

    无论是在批处理系统

    展开全文
  • 进程控制

    千次阅读 2016-07-02 21:19:27
    此外,还包括进程属性的各种ID——实际、有效和保存的用户ID和组ID,以及她们如何受进程控制原语的影响。也包括解释器文件和system函数,进程会计机制等。 8.2 进程标识 每个进程都有一个非负整数标识的唯一进程ID...

    8 进程控制

    8.1 简介

    进程控制,主要包括创建新进程、执行程序和进程终止。此外,还包括进程属性的各种ID——实际、有效和保存的用户ID和组ID,以及她们如何受进程控制原语的影响。也包括解释器文件和system函数,进程会计机制等。

    8.2 进程标识

    每个进程都有一个非负整数标识的唯一进程ID。因为进程ID标识符总是唯一的,常将其用于其他标识符的一部分以保证其唯一性。如应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件名。

    虽然是唯一的,但是进程ID是可复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为使用同一ID的某个已终止的先前进程。

    系统中有一些专用进程,但具体细节随实现而不同。ID为0的进程通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新版本中是/sbin/init。此进程负责在自举内核后启动一个UNIX系统。Init通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。Init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。Init可以成为所有孤儿进程的父进程。

    每个UNIX系统实现都有它自己的一套操作系统服务的内核进程。如在某些UNIX的虚拟存储器实现中,进程ID 2是页守护进程,此进程负责支持虚拟存储器系统的分页操作。

    处理进程ID,每个进程还有一些其他标识符,如父进程ID,使用用户ID,有效用户ID,实际组ID,有效组ID等。

    8.3 函数fork

    一个现有的进程可以调用fork函数创建一个新进程。

    由fork创建的新进程被称为子进程(child process)。Fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使得一个进程可以获得其所有子进程的进程ID。Fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所有子进程总是可以调用getppid以获的其父进程的进程ID(进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。

    子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。如子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。

    由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、堆和栈的完全副本。作为替代,使用了写时复用(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将他们的访问权限改变为只读。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。

    一般来说,在fork之后是父进程先执行,还是子进程先执行,是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。

    文件共享

    父进程和子进程共享同一个文件偏移量。

    在fork之后,处理文件描述符有以下两种常见的情况:

    ²  父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。

    ²  父进程和子进程各自执行不通的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭他们不需要的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

    除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:

    ²  实际用户ID,实际组ID,有效用户ID,有效组ID

    ²  附属组ID

    ²  进程组ID

    ²  会话ID

    ²  控制终端

    ²  设置用户ID标志和设置组ID标志

    ²  当前工作目录

    ²  根目录

    ²  文件模式创建屏蔽字

    ²  信号屏蔽和安排

    ²  对任意打开文件描述符的执行式关闭(close-on-exec)标志

    ²  环境

    ²  连接的共享存储段

    ²  存储映像

    ²  资源限制

    父进程和子进程之间的区别具体如下:

    ²  Fork的返回值不同

    ²  进程ID不同

    ²  这两个进程的父进程ID不同:子进程的父进程ID是创建它的进程ID,而父进程的父进程ID则不变

    ²  子进程的tms_utime,tms_stime,tms_cutime和tms_ustime的值设置为0

    ²  子进程不继承父进程设置的文件锁

    ²  子进程的未处理闹钟被清除

    ²  子进程的未处理信号集设置为空集

    使fork失败的主要原因有两个:

    ²  系统中已经有了太多的进程(通常意味着某个方面出现了问题)

    ²  该实际用户ID的进程总数超过了系统限制。

    Fork有以下两种用法:

    ²  一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程继续等待下一个服务请求。

    ²  一个进程要执行不通的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

    某些操作系统将第2种用法中的两个操作(fork之后执行exec)组合成一个操作,称为spawn。UNIX系统将这两个操作分开,因为在很多的场合需要单独使用fork,其后并不跟随exec。另外,将这两个操作分开,使得子进程在fork和exec之间可以更改自己的属性,如I/O重定向,用户ID,信号安排等。

    8.4 函数vfork

    Vfork函数的调用属性怒和返回值与fork相同,但两者的语义不同。Vfork函数用于创建一个新进程,而该进程的目的是exec一个新程序。Vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用改地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。这种优化工作方式在某些UNIX系统的实现中提高了效率,但如果子进程修改数据(除了用于存放vfork返回值的变量)、进行函数调用、或者没有调用exec或exit就返回都可能会带来未知的结果。(实现采用写时复用技术以提高fork之后跟随exec操作的效率,但是不复制比部分幅值还是要快一些)。

    Vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数的任意一个时,父进程会恢复运行,(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)。

    8.5 函数exit

    5种正常终止方式和3中异常终止方式具体如下:

    5种正常终止方式:

    ²  在main函数内执行return语句。等效于调用exit

    ²  调用exit函数。此函数由ISO C定义,其操作包括调用各终止处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准I/O流。因为ISO C并不处理文件描述符、多进程(父进程和子进程)以及作业控制,所以这一定义对UNIX系统而言是不完整的。

    ²  调用_exit或_Exit函数。ISO C定义_Exit,其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准I/O流是否进行冲洗,这取决于实现。在UNIX系统中,_Exit和_exit是同义的,并不冲洗标准I/O流。_exit函数由exit调用,它处理UNIX系统特定的细节。_exit是由POSIX.1说明。

    ²  进程的最后一个线程在其启动例程中执行return语句。但是该线程的返回值不用做进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。

    ²  进程的最后一个线程调用pthread_exit函数。如同前面一样,在这种情况下,进程终止状态总是0,这与传送给pthread_exit的参数无关。

    3种异常终止方式:

    ²  调用aboart。它产生SIGABRT信号,这是下一种异常终止的一种特例

    ²  当进程接收到某些信号时。信号可有进程自身(如调用aboart函数)、其他进程或内核产生。如若进程引用地址空间之外的存储单元、或者除以0,内核就会为该进程产生相应的信号

    ²  最后一个线程对“取消”的请求做出响应。默认情况下,“取消”以延迟方式发生:一个线程要求取消另一个线程,若干时间后,目标线程终止。

    不管进程如何终止,最后都会执行内核中同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

    对上述任意一种情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于3个终止函数(exit,_exit,_Exit),实现这一点的方法是,将其退出状态作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。

    注意:这里使用了“退出状态”(它是传递给向3个终止函数的参数,或main的返回值)和终止状态两个术语,以表示有所区别。在最后调用_exit时,内核将退出状态转换成终止状态。如果子进程正常终止,则父进程可以获得子进程的退出状态。

    父进程在子进程之前终止,则:对于父进程已经终止的所有进程,他们的父进程都改变为init进程,称这些进程由init进程收养。其操作过程如下:在一个进程终止时,内核逐个检查所有活动的进程,以判断他是否是正要终止进程的子进程,如果是,则该进程的父进程ID就改为1(init进程的ID),这种处理方法保证了每个进程有一个父进程。

    如果子进程在父进程之前终止,那么父进程如何能在做相应检查时得到子进程的终止状态?

    如果子进程完全消失了,父进程在最终准备好检查子进程是否终止时是无法获取它的终止状态的。内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或wiatpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止进程的有关信息,释放它扔占用的资源)的进程被称为僵死进程。如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵死进程。

    一个由init进程收养的进程终止会发生什么?它会不会变成一个僵死进程?

    答案是“否”,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中塞满僵死进程。当提及“一个init的子进程“时,指的可能是init直接产生的进程,也可能是其父进程已经终止,由init收养的进程。

    8.6 函数wait和waitpid

    当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止时个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系统动作默认是忽略它。

    调用wait和waitpid的进程可能会发生什么?

    ²  如果其所有子进程都换在运行,则阻塞

    ²  如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。

    ²  如果它没有任何子进程,则立即出错并返回

    如果进程由于接收到SIGCHLD信号而调用wait,我们期望wait会立即返回。但是如果在随机时间点调用wait,则进程可能会阻塞。

    Wait函数和waitpid函数区别如下:

    ²  在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞

    ²  Waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

    如果子进程已经终止,并且是一个僵死进程,则wait立即返回并取得子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止。如调用者阻塞而且它有多个子进程,则在其某一子进程终止时,wait就立即返回。因为wait返回终止子进程的进程ID,所以它总能了解是那一个子进程终止。

    如果要等待一个指定的进程终止(如果知道要等待进程的ID),该如何做?

    在早期的UNIX版本中,必须调用wait,然后将其返回的进程ID和所期望的进程ID相比较。如果终止进程不是所期望的,则将该进程ID和终止状态保存起来,然后再此调用wait。反复这样做,知道所期望的进程终止。下一次又想等待一个特定的进程时,先查看终止的进程列表,若其中已有要等待的进程,则获取相关信息;否则调用wait。

    Waitpid函数提供了wait函数没有提供的3个功能:

    ²  Waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。

    ²  Waitpid提供了一个wait的非阻塞版本。有时希望获取一个子进程的状态,但不想阻塞

    ²  Waitpid通过WUNTRACED和WCONTINUED选项支持作用控制。

    8.7 函数waitid

    Waitid函数是取得进程终止状态函数。它允许一个进程指定要等待的子进程。但它使用两个单独的参数表是要等待的子进程所属的类型,而不是将此与进程ID或进程组ID组合成一个参数。

    8.8 wait3和wait4函数

    大多数UNIX系统实现提供了另外两个函数wait3和wait4.历史上,这两个函数是从UNIX系统的BSD分支沿袭下来的。他们提供的功能比POSIX.1函数wait、waitpid和waitid所提供功能的要多一个,这与附加参数有关。该参数允许内核返回由终止进程及其子进程使用的资源概况。

    资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。

    8.9 竞争条件

    当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件(race condition)。如何在fork之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运行的还是子进程先运行,那么fork函数就会是竞争条件活跃的滋生地。通常,我们不能预料哪一个进程先运行。即使知道哪一个进程先运行,在该进程开始运行后,所发生的事情也依赖于系统负载预计内核的调度算法。

    如果一个进程要等待其父进程终止,可以使用轮询的方式,但是轮询浪费了CPU时间,因为调用者每间隔1s都被唤醒,然后进行条件测试。

    为了避免竞争条件和轮询,在多个进程之间需要有某种形式的信号发送和接收的方法。在UNIX中可以使用信号机制。此外,各种形式的进程间通信(IPC)也可以使用。

    在父进程和子进程的关系中,常常出现下述情况。在fork之后,父进程和子进程都有一些事情要做。如父进程可能要用子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。

    8.10 函数exec

    当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。Exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

    Fork函数用于创建新进程,而exec函数用于初始化执行新的程序。有7种不同的exec函数可供使用。

    在执行exec后,进程ID没有改变。但新程序从调用进程继承了下例属性:

    ²  进程ID和父进程ID

    ²  实际用户ID和实际组ID

    ²  附属组ID

    ²  进程组ID

    ²  会话ID

    ²  控制终端

    ²  闹钟尚余留的时间

    ²  当前工作目录

    ²  根目录

    ²  文件模式创建屏蔽字

    ²  文件锁

    ²  进程信号屏蔽字

    ²  未处理信号

    ²  资源限制

    ²  Nice值

    ²  Tms_utime,tms_stime,tms_cutime,tms_cstime值

    对打开文件的处理与每个描述符的执行时关闭(close-on-exec)标志值有关。

    注意:在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序文件的设置用户ID位和设置组ID位是否设置。如果新程序设置了用户ID位,则有效用户ID变成程序文件所有者的ID;否则有效用户ID不变。

    8.11 更改用户ID和用户组ID

    在UNIX系统中,特权(如能改变当前日期的表示法)以及访问控制(如能够读、写一个特定文件),是基于用户ID和组ID的。当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更改自己的用户ID或组ID,使得新ID具有合适的特权或访问权限。与此类似,当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用户ID或组ID,新ID不具有相应的特权或访问这些资源的能力。

    一般而言,在设计应用中,总是试图使用最小特权(least privilege)模型。依照此模型,我们的程序应当只具有为完成给定任务所需的最小特权。这降低了由恶意用户试图哄骗我们程序以及未预料的方式使用特权造成的安全风险。

    可以用setuid函数设置实际用户ID和有效用户ID。与此类似,可以用setgid函数设置实际组ID和有效组ID。

    更改用户ID的规则:

    ²  若进程具有超级用户特权,用setuid函数将实际用户ID、有效用户ID以及保存的设置用户ID(savedset-user-ID)设置为uid

    ²  若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid。不更改实际用户ID和保存的设置用户ID

    ²  如果上面条件都不满足,则errno设置为EPERM,并返回-1.

    内核所维护的3个用户ID,需要注意以下几点:

    ²  只有超级用户进程可以更改实际用户ID。通常实际用户ID是在用户登录时,由login程序设置的,而且决不会改变他。因为login是一个超级用户进程,当它调用setuid时,设置所有3个用户ID。

    ²  仅当对程序文件设置了设置用户ID位时,exec函数才设置有效用户ID。如果设置用户ID位没有设置,exec函数不会改变有效用户ID,而将维持其现有值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID。自然地,不能将有效用户ID设置为任一随机值

    ²  保存的设置用户ID是由exec复制有效用户ID而得到的。如果设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的有效ID以后,这个副本就被保存起来了。

    Getuid和geteuid函数只能获得实际用户ID和有效用户ID的当前值。

    8.12 解释器文件

    所有现今的UNIX系统都支持解释器文件(interpreter file)。这种文件是文本文件,其起始形式为:#! Pathname[ optional-argument] 在感叹号和path之间的空格是可选的最常见的解释器文件以下列行开始:#! /bin/sh。Pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索)对这种文件的识别是由内核作为exec系统调用处理的一部分来完成,内核使用exec函数的进行实际执行的并不是解释器文件,而是在该解释器文件中第一行pathname所指定的文件。一定要将解释器文件(文本文件,它是#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。

    很多系统对解释器文件第一行长度限制。这包括#!、pathname,可选参数,终止换行符以及空格数。

    是否一定需要解释器文件呢?那也不完全如此。但是他们确实使用户得到效率方面的好处,其代价是内核的额外开销(因为识别解释器文件的是内核)。由下述理由,解释器文件是有用的。

    ²  有些程序是用某种语言写的脚本,解释器文件可将这一事实隐藏起来。

    ²  解释器脚本在效率方面也提供了好处。用一个shell脚本代替解释器脚本需要更多的开销

    ²  解释器脚本使我们可以使用除/bin/sh以外的其他shell来编写shell甲苯。当execlp找到一个非机器可执行的可执行文件时,它总是调用/bin/sh来解释执行该文件。

    8.13 函数system

    System函数在其实现中调用了fork、exec和waitpid,故存在3种返回值

    ²  Fork失败或者waitpid返回除EINTR之外的错误,则system返回-1,并且设置errno以指示错误类型

    ²  如果exec失败(表示不能执行shell),则其返回值如通shell执行了exit一样。

    ²  否则所有3个函数(fork,exec,waitpid)都成功,那么system的返回值是shell的终止状态。

    使用了system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处理以及各种信号处理。

    如果一个程序正以特殊的权限(设置用户ID和设置组ID)运行,它又想生成另一个进程执行另一个程序,则它应当直接使用fork和exec,而且在fork之后,exec之前要更改回普通用户权限。设置用户ID或设置组ID程序决不应调用system函数。

    8.14 进程会计

    大多数UNIX系统提供了一个选项以进行进程会计处理。启用该选项后,每当进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名,所使用的CPU时间总量,用户ID和组ID,启动时间等。

    会计记录所需的各个数据(各CPU时间,传输的字符数等)都由内核保存在进程表中,并在一个新进程被创建时初始化(如fork之后在子进程中),进程终止时写一个会计记录。这产生两个后果:

    ²  我们不能获取永远不终止的进程的会计记录。像init这样的进程在系统生命周期中一直在运行,并不产生会计记录。这也同样适合于内核守护进程,他们通常不会终止。

    ²  在会计文件中记录的顺序对应于进程终止的顺序,而不是他们启动的顺序。

    ²  会计记录对应于进程而不是程序。在fork之后,内核为子进程初始化一个记录,而不是在一个新程序被执行时初始化。

    8.15 用户标识

    任一进程都可以得到其实际用户ID和有效用户ID及组ID。但又想得到用户的登录名。使用函数为getpwuid。通常系统记录用户登录时使用的名字,用getlogin函数获取登录名。

    8.16 进程调度

    UNIX系统历史上对进程提供的只是基于调度优先级的粗粒度的控制。调度策略和调度优先级是由内核确定的。进程可以通过调整nice值选择以更低优选级运行。只有特权进程允许提高调度权限。Nice值越小,优先级越高。进程可以通过nice函数获取或更改它的nice值。Getpriority函数可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关的进程nice值。

    Setpriority用于为进程、进程组和属于特定用户ID的所有进程设置优先级。

    8.17 进程时间

    可以度量的3个时间:墙上的时钟时间,用户CPU时间,系统CPU时间。


    参考文献:Unix高级环境编程

    展开全文
  • 鱼还是熊掌:浅谈多进程多线程的选择 关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就...

    转自http://blog.csdn.net/lishenglong666/article/details/8557215

    鱼还是熊掌:浅谈多进程多线程的选择

    关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有这么简单了,选的不好,会让你深受其害。

     

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

     

    我们按照多个不同的维度,来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。

    对比维度

    多进程

    多线程

    总结

    数据共享、同步

    数据共享复杂,需要用IPC;数据是分开的,同步简单

    因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂

    各有优势

    内存、CPU

    占用内存多,切换复杂,CPU利用率低

    占用内存少,切换简单,CPU利用率高

    线程占优

    创建销毁、切换

    创建销毁、切换复杂,速度慢

    创建销毁、切换简单,速度很快

    线程占优

    编程、调试

    编程简单,调试简单

    编程复杂,调试复杂

    进程占优

    可靠性

    进程间不会互相影响

    一个线程挂掉将导致整个进程挂掉

    进程占优

    分布式

    适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单

    适应于多核分布式

    进程占优

     

    看起来比较简单,优势对比上是“线程 3.5 v 2.5 进程”,我们只管选线程就是了?

     

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

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

    原因请看上面的对比。

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

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

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

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

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

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

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

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

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

    原因请看上面对比。

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

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

     

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


    1、进程与线程

    进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位

    线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

    "进程——资源分配的最小单位,线程——程序执行的最小单位"

    进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

     

    总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。(下面的内容摘自Linux下的多线程编程

    使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

    使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

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

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


    在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在B/S通讯中服务端并发技术 的选型上,比如WEB服务器技术中,Apache是采用多进程的(perfork模式,每客户连接对应一个进程,每进程中只存在唯一一个执行线 程),Java的Web容器Tomcat、Websphere等都是多线程的(每客户连接对应一个线程,所有线程都在一个进程中)。

    从Unix发展历史看,伴随着Unix的诞生多进程就出现了,而多线程很晚才被系统支持,例如Linux直到内核2.6,才支持符合Posix规范的NPTL线程库。进程和线程的特点,也就是各自的优缺点如下:

    进程优点:编程、调试简单,可靠性较高。
    进程缺点:创建、销毁、切换速度慢,内存、资源占用大。
    线程优点:创建、销毁、切换速度快,内存、资源占用小。
    线程缺点:编程、调试复杂,可靠性较差。

    上面的对比可以归结为一句话:“线程快而进程可靠性高”。线程有个别名叫“轻量级进程”,在有的书籍资料上介绍线程可以十倍、百倍的效率快于进程; 而进程之间不共享数据,没有锁问题,结构简单,一个进程崩溃不像线程那样影响全局,因此比较可靠。我相信这个观点可以被大部分人所接受,因为和我们所接受的知识概念是相符的。

    在写这篇文章前,我也属于这“大部分人”,这两年在用C语言编写的几个C/S通讯程序中,因时间紧总是采用多进程并发技术,而且是比较简单的现场为 每客户fork()一个进程,当时总是担心并发量增大时负荷能否承受,盘算着等时间充裕了将它改为多线程形式,或者改为预先创建进程的形式,直到最近在网 上看到了一篇论文《Linux系统下多线程与多进程性能分析》作者“周丽 焦程波 兰巨龙”,才认真思考这个问题,我自己也做了实验,结论和论文作者的相似,但对大部分人可以说是颠覆性的。

    下面是得出结论的实验步骤和过程,结论究竟是怎样的? 感兴趣就一起看看吧。

    实验代码使用周丽论文中的代码样例,我做了少量修改,值得注意的是这样的区别:

    论文实验和我的实验时间不同,论文所处的年代linux内核是2.4,我的实验linux内核是2.6,2.6使用的线程库是NPTL,2.4使用的是老的Linux线程库(用进程模拟线程的那个LinuxThread)。

    论文实验和我用的机器不同,论文描述了使用的环境:单cpu 机器基本配置为:celeron 2.0 GZ, 256M, Linux 9.2,内核 2.4.8。我的环境是:双核 Intel(R) Xeon(R) CPU 5130  @ 2.00GHz(做实验时,禁掉了一核),512MG内存,Red Hat Enterprise Linux ES release 4 (Nahant Update 4),内核2.6.9-42。

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

    1. #include <stdlib.h>
    2. #include <stdio.h>
    3. #include <signal.h>

    4. #define P_NUMBER 255 //并发进程数量
    5. #define COUNT 5 //每次进程打印字符串数
    6. #define TEST_LOGFILE "logFile.log"
    7. FILE *logFile=NULL;

    8. char *s="hello linux\0";

    9. int main()
    10. {
    11.     int i=0,j=0;
    12.     logFile=fopen(TEST_LOGFILE,"a+");//打开日志文件
    13.     for(i=0;i<P_NUMBER;i++)
    14.     {
    15.         if(fork()==0)//创建子进程,if(fork()==0){}这段代码是子进程运行区间
    16.         {
    17.             for(j=0;j<COUNT;j++)
    18.             {
    19.                 printf("[%d]%s\n",j,s);//向控制台输出
    20.                 /*当你频繁读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。可能导致测试结果不准,所以在此注释*/
    21.                 //fprintf(logFile,"[%d]%s\n",j,s);//向日志文件输出,
    22.             }
    23.             exit(0);//子进程结束
    24.         }
    25.     }
    26.     
    27.     for(i=0;i<P_NUMBER;i++)//回收子进程
    28.     {
    29.         wait(0);
    30.     }
    31.     
    32.     printf("Okay\n");
    33.     return 0;
    34. }

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

    1. #include <pthread.h>
    2. #include <unistd.h>
    3. #include <stdlib.h>
    4. #include <stdio.h>

    5. #define P_NUMBER 255//并发线程数量
    6. #define COUNT 5 //每线程打印字符串数
    7. #define TEST_LOG "logFile.log"
    8. FILE *logFile=NULL;

    9. char *s="hello linux\0";

    10. print_hello_linux()//线程执行的函数
    11. {
    12.     int i=0;
    13.     for(i=0;i<COUNT;i++)
    14.     {
    15.         printf("[%d]%s\n",i,s);//想控制台输出
    16.         /*当你频繁读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。可能导致测试结果不准,所以在此注释*/
    17.         //fprintf(logFile,"[%d]%s\n",i,s);//向日志文件输出
    18.     }
    19.     pthread_exit(0);//线程结束
    20. }

    21. int main()
    22. {
    23.     int i=0;
    24.     pthread_t pid[P_NUMBER];//线程数组
    25.     logFile=fopen(TEST_LOG,"a+");//打开日志文件
    26.     
    27.     for(i=0;i<P_NUMBER;i++)
    28.         pthread_create(&pid[i],NULL,(void *)print_hello_linux,NULL);//创建线程
    29.         
    30.     for(i=0;i<P_NUMBER;i++)
    31.         pthread_join(pid[i],NULL);//回收线程
    32.         
    33.     printf("Okay\n");
    34.     return 0;
    35. }

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

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

    实验通过time指令执行两个程序,抄录time输出的挂钟时间(real时间):

    time ./fork
    time ./thread

    每批次的实验通过改动宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次测试五轮,得到的结果如下:

    一、重复周丽论文实验步骤

    (注:本文平均值算法采用的是去掉一个最大值去掉一个最小值,然后平均)

    单核(双核机器禁掉一核),进程/线程数:255,打印次数5

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m0.070s

     0m0.071s

    0m0.071s 

    0m0.070s 

    0m0.070s 

    0m0.070s 

    多线程

     0m0.049s

    0m0.049s 

    0m0.049s 

    0m0.049s 

    0m0.049s 

    0m0.049s 


    单核(双核机器禁掉一核),进程/线程数:255,打印次数10

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m0.112s

    0m0.101s 

    0m0.100s 

    0m0.085s 

    0m0.121s 

    0m0.104s 

    多线程

     0m0.097s

    0m0.089s 

    0m0.090s 

    0m0.104s 

    0m0.080s 

    0m0.092s 


    单核(双核机器禁掉一核),进程/线程数:255,打印次数50

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m0.459s

    0m0.531s 

    0m0.499s 

    0m0.499s 

    0m0.524s 

    0m0.507s 

    多线程

     0m0.387s

    0m0.456s 

    0m0.435s 

    0m0.423s 

    0m0.408s 

    0m0.422s 


    单核(双核机器禁掉一核),进程/线程数:255,打印次数100

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m1.141s

    0m0.992s 

    0m1.134s 

    0m1.027s 

    0m0.965s 

    0m1.051s 

    多线程

     0m0.925s

    0m0.899s 

    0m0.961s 

    0m0.934s 

    0m0.853s 

    0m0.919s 


    单核(双核机器禁掉一核),进程/线程数:255,打印次数500

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m5.221s

    0m5.258s 

    0m5.706s 

    0m5.288s 

    0m5.455s 

    0m5.334s

    多线程

     0m4.689s

    0m4.578s 

    0m4.670s 

    0m4.566s 

    0m4.722s 

    0m4.646s 


    单核(双核机器禁掉一核),进程/线程数:255,打印次数1000

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m12.680s

    0m16.555s 

    0m11.158s 

    0m10.922s 

    0m11.206s 

    0m11.681s 

    多线程

     0m12.993s

    0m13.087s 

    0m13.082s 

    0m13.485s 

    0m13.053s 

    0m13.074s 


    单核(双核机器禁掉一核),进程/线程数:255,打印次数5000

     

    1

    2

    3

    4

    5

    平均

    多进程

     1m27.348s

    1m5.569s 

    0m57.275s 

    1m5.029s 

    1m15.174s 

    1m8.591s 

    多线程

     1m25.813s

    1m29.299s

    1m23.842s 

    1m18.914s 

    1m34.872s 

    1m26.318s 


    单核(双核机器禁掉一核),进程/线程数:255,打印次数10000

     

    1

    2

    3

    4

    5

    平均

    多进程

     2m8.336s

    2m22.999s 

    2m11.046s 

    2m30.040s 

    2m5.752s 

    2m14.137s 

    多线程

     2m46.666s

    2m44.757s 

    2m34.528s 

    2m15.018s 

    2m41.436s 

    2m40.240s 


    本轮实验是为了和周丽论文作对比,因此将进程/线程数量限制在255个,论文也是测试了255个进程/线程分别进行5次,10 次,50 次,100 次,500 次……10000 次打印的用时,论文得出的结果是:任务量较大时,多进程比多线程效率高;而完成的任务量较小时,多线程比多进程要快,重复打印 600 次时,多进程与多线程所耗费的时间相同。

    虽然我的实验直到1000打印次数时,多进程才开始领先,但考虑到使用的是NPTL线程库的缘故,从而可以证实了论文的观点。从我的实验数据看,多线程和多进程两组数据非常接近,考虑到数据的提取具有瞬间性,因此可以认为他们的速度是相同的。

    是不是可以得出这样的结论:多线程创建、销毁速度快,而多线程切换速度快,这个结论我们会在第二个试验中继续试图验证

    当前的网络环境中,我们更看中高并发、高负荷下的性能,纵观前面的实验步骤,最长的实验周期不过2分钟多一点,因此下面的实验将向两个方向延伸,第一,增加并发数量,第二,增加每进程/线程的工作强度。

    二、增加并发数量的实验

    下面的实验打印次数不变,而进程/线程数量逐渐增加。在实验过程中多线程程序在后四组(线程数350,500,800,1000)的测试中都出现了“段错误”,出现错误的原因和多线程预分配线程栈有关。

    实验中的计算机CPU是32位,寻址最大范围是4GB(2的32次方),Linux是按照3GB/1GB的方式来分配内存,其中1GB属于所有进程共享的内核空间,3GB属于用户空间(进程虚拟内存空间)。Linux2.6的默认线程栈大小是8M(通过ulimit -a查看),对于多线程,在创建线程的时候系统会为每一个线程预分配线程栈地址空间,也就是8M的虚拟内存空间。线程数量太多时,线程栈累计的大小将超过进程虚拟内存空间大小(计算时需要排除程序文本、数据、共享库等占用的空间),这就是实验中出现的“段错误”的原因。

    Linux2.6的默认线程栈大小可以通过 ulimit -s 命令查看或修改,我们可以计算出线程数的最大上线: (1024*1024*1024*3) / (1024*1024*8) = 384,实际数字应该略小与384,因为还要计算程序文本、数据、共享库等占用的空间。在当今的稍显繁忙的WEB服务器上,突破384的并发访问并不是稀 罕的事情,要继续下面的实验需要将默认线程栈的大小减小,但这样做有一定的风险,比如线程中的函数分配了大量的自动变量或者函数涉及很深的栈帧(典型的是 递归调用),线程栈就可能不够用了。可以配合使用POSIX.1规定的两个线程属性guardsize和stackaddr来解决线程栈溢出问 题,guardsize控制着线程栈末尾之后的一篇内存区域,一旦线程栈在使用中溢出并到达了这片内存,程序可以捕获系统内核发出的告警信号,然后使用 malloc获取另外的内存,并通过stackaddr改变线程栈的位置,以获得额外的栈空间,这个动态扩展栈空间办法需要手工编程,而且非常麻烦。

    有两种方法可以改变线程栈的大小,使用 ulimit -s 命令改变系统默认线程栈的大小,或者在代码中创建线程时通过pthread_attr_setstacksize函数改变栈尺寸,在实验中使用的是第一种,在程序运行前先执行ulimit指令将默认线程栈大小改为1M:

    ulimit -s 1024
    time ./thread

    单核(双核机器禁掉一核),进程/线程数:100 ,打印次数1000

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m3.834s

     0m3.759s

     0m4.376s

     0m3.936s

     0m3.851s

     0m3.874

    多线程

     0m3.646s

    0m4.498s

     0m4.219s

     0m3.893s

     0m3.943s

     0m4.018

    单核(双核机器禁掉一核),进程/线程数:255 ,打印次数1000

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m9.731s

     0m9.833s

     0m10.046s

     0m9.830s

     0m9.866s

     0m9.843s

    多线程

     0m9.961s

     0m9.699s

     0m9.958s

     0m10.111s

     0m9.686s

     0m9.873s


    单核(双核机器禁掉一核),进程/线程数:350  ,打印次数1000

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m13.773s

     0m13.500s

     0m13.519s

     0m13.474s

     0m13.351s

     0m13.498

    多线程

     0m12.754s

    0m13.251s 

     0m12.813s

     0m16.861s

     0m12.764s

     0m12.943


    单核(双核机器禁掉一核),进程/线程数: 500 ,打印次数1000

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m23.762s

     0m22.151s

     0m23.926s

     0m21.327s

     0m21.429s

     0m22.413

    多线程

     0m20.603s

     0m20.291s

     0m21.654s

     0m20.684s

     0m20.671s

     0m20.653


    单核(双核机器禁掉一核),进程/线程数:800  ,打印次数1000

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m33.616s

     0m31.757s

     0m31.759s

     0m32.232s

     0m32.498s

     0m32.163

    多线程

     0m32.050s

     0m32.787s

     0m33.055s

     0m32.902s

     0m32.235s

     0m32.641


    单核(双核机器禁掉一核),进程/线程数: 1000 ,打印次数1000

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m40.301s

     0m41.083s

     0m41.634s

     0m40.247s

     0m40.717s

     0m40.700

    多线程

     0m41.633s

     0m41.118s

     0m42.700s

     0m42.134s

     0m41.170s

     0m41.646


    【实验结论】 
    当线程/进程逐渐增多时,执行相同任务时,线程所花费时间相对于进程有下降的趋势(本人怀疑后两组数据受系统其他瓶颈的影响),这是不是进一步验证了 多线程创建、销毁速度快,而多进程切换速度快。

    出现了线程栈的问题,让我特别关心Java线程是怎样处理的,因此用Java语言写了同样的实验程序,Java程序加载虚拟机环境比较耗时,所以没 有用time提取测试时间,而直接将测时写入代码。对Linux上的C编程不熟悉的Java程序员也可以用这个程序去对比理解上面的C语言试验程序。
    1. import java.io.File;
    2.     import java.io.FileNotFoundException;
    3.     import java.io.FileOutputStream;
    4.     import java.io.IOException;
    5.      
    6.     public class MyThread extends Thread
    7.     {
    8.         static int P_NUMBER = 1000; /* 并发线程数量 */
    9.         static int COUNT = 1000; /* 每线程打印字符串次数 */
    10.      
    11.         static String s = "hello linux\n";
    12.            
    13.         static FileOutputStream out = null; /* 文件输出流 */
    14.         @Override
    15.         public void run()
    16.         {
    17.             for (int i = 0; i < COUNT; i++)
    18.             {
    19.                 System.out.printf("[%d]%s", i, s); /* 向控制台输出 */
    20.                
    21.                 StringBuilder sb = new StringBuilder(16);
    22.                 sb.append("[").append(i).append("]").append(s);
    23.                 try
    24.                 {
    25.                     out.write(sb.toString().getBytes());/* 向日志文件输出 */
    26.                 }
    27.                 catch (IOException e)
    28.                 {
    29.                     e.printStackTrace();
    30.                 }
    31.             }
    32.         }
    33.      
    34.         public static void main(String[] args) throws FileNotFoundException, InterruptedException
    35.         {
    36.             MyThread[] threads = new MyThread[P_NUMBER]; /* 线程数组 */
    37.            
    38.             File file = new File("Javalogfile.log");
    39.             out = new FileOutputStream(file, true); /* 日志文件输出流 */
    40.            
    41.             System.out.println("开始运行");
    42.             long start = System.currentTimeMillis();
    43.      
    44.             for (int i = 0; i < P_NUMBER; i++) //创建线程
    45.             {
    46.                 threads[i] = new MyThread();
    47.                 threads[i].start();
    48.             }
    49.      
    50.             for (int i = 0; i < P_NUMBER; i++) //回收线程
    51.             {
    52.                 threads[i].join();
    53.             }
    54.            
    55.             System.out.println("用时:" + (System.currentTimeMillis() – start) + " 毫秒");
    56.             return;
    57.         }
    58.       
    59.     }

    进程/线程数:1000  ,打印次数1000(用得原作者的数据)

     

    1

    2

    3

    4

    5

    平均

    多线程

     65664 ms

     66269 ms

     65546ms

     65931ms

     66540ms

     65990 ms

    Java程序比C程序慢一些在情理之中,但Java程序并没有出现线程栈问题,5次测试都平稳完成,可以用下面的ps指令获得java进程中线程的数量:

    diaoyf@ali:~$ ps -eLf | grep MyThread | wc -l
    1010

    用ps测试线程数在1010上维持了很长时间,多出的10个线程应该是jvm内部的管理线程,比如用于GC。我不知道Java创建线程时默认栈的大 小是多少,很多资料说法不统一,于是下载了Java的源码jdk-6u21-fcs-src-b07-jrl-17_jul_2010.jar(实验环境 安装的是 SUN jdk 1.6.0_20-b02),但没能从中找到需要的信息。对于jvm的运行,java提供了控制参数,因此再次测试时,通过下面的参数将Java线程栈大 小定义在8192k,和Linux的默认大小一致:

    diaoyf@ali:~/tmp1$ java -Xss8192k MyThread

    出乎意料的是并没有出现想象中的异常,但用ps侦测线程数最高到达337,我判断程序在创建线程时在栈到达可用内存的上线时就停止继续创建了,程序运行的时间远小于估计值也证明了这个判断。程序虽然没有抛出异常,但运行的并不正常,另一个问题是最后并没有打印出“用时 xxx毫秒”信息。

    这次测试更加深了我的一个长期的猜测:Java的Web容器不稳定。因为我是多年编写B/S的Java程序员,WEB服务不稳定常常挂掉也是司空见惯的,除了自己或项目组成员水平不高,代码编写太烂的原因之外,我一直猜测还有更深层的原因,如果就是线程原因的话,这颠覆性可比本篇文章的多进程性能颠覆性要大得多,想想世界上有多少Tomcat、Jboss、Websphere、weblogic在跑着,嘿嘿。

    这次测试还打破了以前的一个说法:单CPU上并发超过6、7百,线程或进程间的切换就会占用大量CPU时间,造成服务器效率会急剧下降。但从上面的实验来看,进程/线程数到1000时(这差不多是非常繁忙的WEB服务器了),仍具有很好的线性。

    三、增加每进程/线程的工作强度的实验

    这次将程序打印数据增大,原来打印字符串为:

    1. char *s = "hello linux\0";

    现在修改为每次打印256个字节数据:

    1. char *= "1234567890abcdef\
    2.     1234567890abcdef\
    3.     1234567890abcdef\
    4.     1234567890abcdef\
    5.     1234567890abcdef\
    6.     1234567890abcdef\
    7.     1234567890abcdef\
    8.     1234567890abcdef\
    9.     1234567890abcdef\
    10.     1234567890abcdef\
    11.     1234567890abcdef\
    12.     1234567890abcdef\
    13.     1234567890abcdef\
    14.     1234567890abcdef\
    15.     1234567890abcdef\
    16.     1234567890abcdef\0";

    单核(双核机器禁掉一核),进程/线程数:255  ,打印次数100

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m6.977s

     0m7.358s

     0m7.520s

     0m7.282s

     0m7.218s

     0m7.286

    多线程

     0m7.035s

     0m7.552s

     0m7.087s

     0m7.427s

     0m7.257s

     0m7.257


    单核(双核机器禁掉一核),进程/线程数:  255,打印次数500

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m35.666s

     0m36.009s

     0m36.532s

     0m35.578s

     0m41.537s

     0m36.069

    多线程

     0m37.290s

     0m35.688s

     0m36.377s

     0m36.693s

     0m36.784s

     0m36.618


    单核(双核机器禁掉一核),进程/线程数: 255,打印次数1000

     

    1

    2

    3

    4

    5

    平均

    多进程

     1m8.864s

     1m11.056s

     1m10.273s

     1m12.317s

     1m20.193s

     1m11.215

    多线程

     1m11.949s

     1m13.088s

     1m12.291s

     1m9.677s

     1m12.210s

     1m12.150



    【实验结论】

    从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了很多,唉,又是据说!),多线程比较多进程在效率上没有任何的优势,在线程数增大时多线程程序还出现了运行错误,实验可以得出下面的结论:

    在Linux2.6上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优势。

    四、多进程和多线程在创建和销毁上的效率比较

    预先创建进程或线程可以节省进程或线程的创建、销毁时间,在实际的应用中很多程序使用了这样的策略,比如Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中,进程或线程的创建、销毁是比较耗时的,在stevesn的著作《Unix网络编程》中有这样 的对比图(第一卷 第三版 30章 客户/服务器程序设计范式):

    行号服务器描述进程控制CPU时间(秒,与基准之差)
    Solaris2.5.1Digital Unix4.0bBSD/OS3.0
    0迭代服务器(基准测试,无进程控制)0.00.00.0
    1简单并发服务,为每个客户请求fork一个进程504.2168.929.6
    2预先派生子进程,每个子进程调用accept 6.21.8
    3预先派生子进程,用文件锁保护accept25.210.02.7
    4预先派生子进程,用线程互斥锁保护accept21.5  
    5预先派生子进程,由父进程向子进程传递套接字36.710.96.1
    6并发服务,为每个客户请求创建一个线程18.74.7 
    7预先创建线程,用互斥锁保护accept8.63.5 
    8预先创建线程,由主线程调用accept14.55.0 

    stevens已驾鹤西去多年,但《Unix网络编程》一书仍具有巨大的影响力,上表中stevens比较了三种服务器上多进程和多线程的执行效 率,因为三种服务器所用计算机不同,表中数据只能纵向比较,而横向无可比性,stevens在书中提供了这些测试程序的源码(也可以在网上下载)。书中介 绍了测试环境,两台与服务器处于同一子网的客户机,每个客户并发5个进程(服务器同一时间最多10个连接),每个客户请求从服务器获取4000字节数据, 预先派生子进程或线程的数量是15个。

    第0行是迭代模式的基准测试程序,服务器程序只有一个进程在运行(同一时间只能处理一个客户请求),因为没有进程或线程的调度切换,因此它的速度是 最快的,表中其他服务模式的运行数值是比迭代模式多出的差值。迭代模式很少用到,在现有的互联网服务中,DNS、NTP服务有它的影子。第1~5行是多进 程服务模式,期中第1行使用现场fork子进程,2~5行都是预先创建15个子进程模式,在多进程程序中套接字传递不太容易(相对于多线 程),stevens在这里提供了4个不同的处理accept的方法。6~8行是多线程服务模式,第6行是现场为客户请求创建子线程,7~8行是预先创建 15个线程。表中有的格子是空白的,是因为这个系统不支持此种模式,比如当年的BSD不支持线程,因此BSD上多线程的数据都是空白的。

    从数据的比对看,现场为每客户fork一个进程的方式是最慢的,差不多有20倍的速度差异,Solaris上的现场fork和预先创建子进程的最大差别是504.2 :21.5,但我们不能理解为预先创建模式比现场fork快20倍,原因有两个:

    1. stevens的测试已是十几年前的了,现在的OS和CPU已起了翻天覆地的变化,表中的数值需要重新测试。

    2. stevens没有提供服务器程序整体的运行计时,我们无法理解504.2 :21.5的实际运行效率,有可能是1504.2 : 1021.5,也可能是100503.2 : 100021.5,20倍的差异可能很大,也可能可以忽略。

    因此我写了下面的实验程序,来计算在Linux2.6上创建、销毁10万个进程/线程的绝对用时。

    创建10万个进程(forkcreat.c):

    1. #include <stdio.h>
    2. #include <signal.h>
    3. #include <stdio.h>
    4. #include <unistd.h>
    5. #include <sys/stat.h>
    6. #include <fcntl.h>
    7. #include <sys/types.h>
    8. #include <sys/wait.h>

    9. int count;//子进程创建成功数量 
    10. int fcount;//子进程创建失败数量 
    11. int scount;//子进程回收数量 

    12. /*信号处理函数–子进程关闭收集*/
    13. void sig_chld(int signo)
    14. {
    15.     pid_t chldpid;//子进程id
    16.     int stat;//子进程的终止状态
    17.     
    18.     //子进程回收,避免出现僵尸进程
    19.     while((chldpid=wait(&stat)>0))
    20.     {
    21.         scount++;
    22.     }
    23. }

    24. int main()
    25. {
    26.     //注册子进程回收信号处理函数
    27.     signal(SIGCHLD,sig_chld);
    28.     
    29.     int i;
    30.     for(i=0;i<100000;i++)//fork()10万个子进程
    31.     {
    32.         pid_t pid=fork();
    33.         if(pid==-1)//子进程创建失败
    34.         {
    35.             fcount++;
    36.         }
    37.         else if(pid>0)//子进程创建成功
    38.         {
    39.             count++;
    40.         }
    41.         else if(pid==0)//子进程执行过程
    42.         {
    43.             exit(0);
    44.         }
    45.     }
    46.     
    47.     printf("count:%d fount:%d scount:%d\n",count,fcount,scount);
    48. }

    创建10万个线程(pthreadcreat.c):

    1. #include <stdio.h>
    2. #include <pthread.h>

    3. int count=0;//成功创建线程数量

    4. void thread(void)
    5. {
    6.     //啥也不做
    7. }

    8. int main(void)
    9. {
    10.     pthread_t id;//线程id
    11.     int i,ret;
    12.     
    13.     for(i=0;i<100000;i++)//创建10万个线程
    14.     {
    15.         ret=pthread_create(&id,NULL,(void *)thread,NULL);
    16.         if(ret!=0)
    17.         {
    18.             printf("Create pthread error!\n");
    19.             return(1);
    20.         }
    21.         count++;
    22.         pthread_join(id,NULL);
    23.     }
    24.     
    25.     printf("count:%d\n",count);
    26. }

    创建10万个线程的Java程序:

    1. public class ThreadTest
    2.     {
    3.         public static void main(String[] ags) throws InterruptedException
    4.         {
    5.             System.out.println("开始运行");
    6.             long start = System.currentTimeMillis();
    7.             for(int i = 0; i < 100000; i++) //创建10万个线程
    8.             {
    9.                 Thread athread = new Thread(); //创建线程对象
    10.                 athread.start(); //启动线程
    11.                 athread.join(); //等待该线程停止
    12.             }
    13.            
    14.             System.out.println("用时:" + (System.currentTimeMillis() – start) + " 毫秒");
    15.         }
    16.     }

    单核(双核机器禁掉一核),创建销毁10万个进程/线程

     

    1

    2

    3

    4

    5

    平均

    多进程

     0m8.774s

     0m8.780s

     0m8.475s

     0m8.592s

     0m8.687s

     0m8.684

    多线程

     0m0.663s

     0m0.660s

     0m0.662s

     0m0.660s

     0m0.661s

     0m0.661

    创建销毁10万个线程(Java)
    12286毫秒

    从数据可以看出,多线程比多进程在效率上有10多倍的优势,但不能让我们在使用哪种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能只横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理,我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,可以说是有质的飞跃的,平均创建销毁一个进程的速度是0.18毫秒,对于当前服务器几百、几千的并发量,还有预先派生子进程/线程的必要吗?

    预先派生子进程/线程比现场创建子进程/线程要复杂很多,不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的测试程序中,使用了“惊群”和“锁”技术。即使stevens的数据表格中,预先派生线程也不见得比现场创建线程快,在 《Unix网络编程》第三版中,新作者参照stevens的测试也提供了一组数据,在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优势。因此我对这一节实验下的结论是:

    预先派生进程/线程的模式(进程池、线程池)技术,不仅复杂,在效率上也无优势,在新的应用中可以放心大胆地为客户连接请求去现场创建进程和线程。

    我想,这是fork迷们最愿意看到的结论了。

    五、双核系统重复周丽论文实验步骤

    双核,进程/线程数:255 ,打印次数10

     

    1

    2

    3

    4

    5

    平均(单核倍数)

    多进程

    0m0.061s

    0m0.053s

    0m0.068s

    0m0.061s

    0m0.059s

     0m0.060(1.73)

    多线程

    0m0.054s

    0m0.040s

    0m0.053s

    0m0.056s

    0m0.042s

     0m0.050(1.84)


    双核,进程/线程数: 255,打印次数100

     

    1

    2

    3

    4

    5

    平均(单核倍数)

    多进程

    0m0.918s

    0m1.198s

    0m1.241s

    0m1.017s

     0m1.172s

     0m1.129(0.93)

    多线程

    0m0.897s

    0m1.166s

    0m1.091s 

    0m1.360s

     0m0.997s

     0m1.085(0.85)


    双核,进程/线程数: 255,打印次数1000

     

    1

    2

    3

    4

    5

    平均(单核倍数)

    多进程

    0m11.276s

    0m11.269s 

    0m11.218s

    0m10.919s

    0m11.201s

     0m11.229(1.04)

    多线程

    0m11.525s

    0m11.984s

    0m11.715s

    0m11.433s

    0m10.966s

     0m11.558(1.13)



    双核,进程/线程数:255 ,打印次数10000

     

    1

    2

    3

    4

    5

    平均(单核倍数)

    多进程

    1m54.328s

    1m54.748s

    1m54.807s

    1m55.950s

    1m57.655s

     1m55.168(1.16)

    多线程

    2m3.021s

    1m57.611s

    1m59.139s 

    1m58.297s

    1m57.258s 

     1m58.349(1.35)


    【实验结论】

    双核处理器在完成任务量较少时,没有系统其他瓶颈因素影响时基本上是单核的两倍,在任务量较多时,受系统其他瓶颈因素的影响,速度明显趋近于单核的速度。

    六、并发服务的不可测性

    看到这里,你会感觉到我有挺进程、贬线程的论调,实际上对于现实中的并发服务具有不可测性,前面的实验和结论只可做参考,而不可定性。对于不可测性,我举个生活中的例子。

    这几年在大都市生活的朋友都感觉城市交通状况越来越差,到处堵车,从好的方面想这不正反应了我国GDP的高速发展。如果你7、8年前来到西安市,穿 过南二环上的一些十字路口时,会发现一个奇怪的U型弯的交通管制,为了更好的说明,我画了两张图来说明,第一张图是采用U型弯之前的,第二张是采用U型弯 之后的。

    南二环交通图一

    南二环交通图一

    南二环交通图二

    南二环交通图二

    为了讲述的方便,我们不考虑十字路口左拐的情况,在图一中东西向和南北向的车辆交汇在十字路口,用红绿灯控制同一时间只能东西向或南北向通行,一般 的十字路口都是这样管控的。随着车辆的增多,十字路口的堵塞越来越严重,尤其是上下班时间经常出现堵死现象。于是交通部门在不动用过多经费的情况下而采用 了图二的交通管制,东西向车辆行进方式不变,而南北向车辆不能直行,需要右拐到下一个路口拐一个超大的U型弯,这样的措施避免了因车辆交错而引发堵死的次 数,从而提高了车辆的通过效率。我曾经问一个每天上下班乘公交经过此路口的同事,他说这样的改动不一定每次上下班时间都能缩短,但上班时间有保障了,从而 迟到次数减少了。如果今天你去西安市的南二环已经见不到U型弯了,东西向建设了高架桥,车辆分流后下层的十字路口已恢复为图一方式。

    从效率的角度分析,在图一中等一个红灯45秒,远远小于图二拐那个U型弯用去的时间,但实际情况正好相反。我们可以设想一下,如果路上的所有运行车 辆都是同一型号(比如说全是QQ3微型车),所有的司机都遵守交规,具有同样的心情和性格,那么图一的通行效率肯定比图二高。现实中就不一样了,首先车辆 不统一,有大车、小车、快车、慢车,其次司机的品行不一,有特别遵守交规的,有想耍点小聪明的,有性子慢的,也有的性子急,时不时还有三轮摩托逆行一下, 十字路口的“死锁”也就难免了。

    那么在什么情况下图二优于图一,是否能拿出一个科学分析数据来呢?以现在的科学技术水平是拿不出来的,就像长期的天气预报不可预测一样,西安市的交管部门肯定不是分析各种车辆的运行规律、速度,再进行复杂的社会学、心理学分析做出U型弯的决定的,这就是要说的不可测性。

    现实中的程序亦然如此,比如WEB服务器,有的客户在快车道(宽带),有的在慢车道(窄带),有的性子慢(等待半分钟也无所谓),有的性子急(拼命 的进行浏览器刷新),时不时还有一两个黑客混入其中,这种情况每个服务器都不一样,既是是同一服务器每时每刻的变化也不一样,因此说不具有可测性。开发者 和维护者能做的,不论是前面的这种实验测试,还是对具体网站进行的压力测试,最多也就能模拟相当于QQ3通过十字路口的场景。

    结束语

    本篇文章比较了Linux系统上多线程和多进程的运行效率,在实际应用时还有其他因素的影响,比如网络通讯时采用长连接还是短连接,是否采用 select、poll,java中称为nio的机制,还有使用的编程语言,例如Java不能使用多进程,PHP不能使用多线程,这些都可能影响到并发模 式的选型。

    最后还有两点提醒:

    1. 文章中的所有实验数据有环境约束。
    2. 由于并行服务的不可测性,文章中的观点应该只做参考,而不要去定性。

    【参考资料】

    1. 《Linux系统下多线程与多进程性能分析》作者“周丽 焦程波 兰巨龙”,这是我写这篇文章的诱因之一,只是不知道引用原作的程序代码是否属于侵权行为。

    2. stevens著作的《Unix网络编程(第一卷)》和《Unix高级环境编程》,这两本书应该收集入IT的四书五经。

    3. Robert Love著作的《Linux内核设计与实现》。

    4. John Fusco 著作的《Linux开发工具箱》,这本书不太出名,但却是我读过的对内存和进程调度讲解最清晰明了的,第5章“开发者必备内核知识”和第6章“进程”是这本书的精华。

    展开全文
  • 进程管理

    千次阅读 多人点赞 2021-05-05 22:15:07
    进程管理
  • linux 进程优先级

    千次阅读 2018-09-16 18:42:02
    Linux 中进程的优先级绝不是如想象中的那么简单,相反它的概念比较混杂,它甚至不是很符合直觉。 Linux 进程的优先级跟随调度算法的不断发展,其意义在不同的阶段也有着不同的含义,所以本来想从 Linux 的调度...
  • Android内存管理的原理--进程管理

    千次阅读 2015-05-27 14:40:39
    Android内存管理的原理--进程管理 ...这些保留在内存中的进程通常情况下不会影响整体系统的运行速度,并且当用户再次激活这些进程时,提升了进程的启动速度。 那Android什么时候结束进程?结束哪个进程
  • 进程与线程

    千次阅读 2016-07-14 11:28:11
    一 、进程概念在计算机上所有可以运行的软件,通常也包括操作系统,被组织成若干顺序进程,一个进程就是一个正在执行程序的实例,包括内存地址空间,程序技术器、寄存器和变量的当前值。1.1 进程的创建:有四种主要...
  • linux进程管理

    万次阅读 2016-01-14 11:47:07
    Linux 计算需求的表现是以进程的通用抽象为中心的。进程可以是短期的(从命令行执行的一个命令),也可以是长期的(一种网络服务)。因此,对进程及其调度进行一般管理就显得极为重要。  在用户空间,进程是由进程...
  • linux进程调度

    千次阅读 2014-11-03 09:12:27
    如果每次只运行一个进程,一是其它进程得不到及时的响应,二是CPU时间不能得到充分利用。进程调度就是要解决诸如此类的一些问题,将CPU资源合理地分配给各个进程。同时调度过程又需要对各个进程不可见,在每个进程...
  • Linux 进程必知必会

    千次阅读 2020-07-10 08:58:31
    那么本篇文章我们就深入理解一下 Linux 内核来理解 Linux 的基本概念之进程和线程。系统调用是操作系统本身的接口,它对于创建进程和线程,内存分配,共享文件和 I/O 来说都很重要。 我们将从各个版本的共性出发来...
  • linux进程调度,优先级、进程nice值

    万次阅读 2016-05-03 16:06:04
    我自己补充一下:APUE8.16中讲到进程调度,UNIX系统历史上对进程提供的只是基于调度优先级的粗粒度的控制.调度策略和调度优先级是由内核确定的.但是内核可以通过调整nice值选择以更低优先级运行(通过调整nice值降低它...
  • oracle实例进程结构

    千次阅读 2015-09-23 10:52:09
    有这样5个后台进程,oracle使用它们历史长久,系统监视器(System Monitor,SMON)、进程监视器(Process Monitor,PMON)、数据库写入器(Database Writer,DBWn)、日志写入器(Log Writer,LGWR)和检查点进程...
  •   [精彩] 子进程及时知道父进程已经退出的最简单方案?http://www.chinaunix.net 作者:yuonunix 发表于:2003-10-31 10:14:14【发表评论】 【查看原文】 【C/C++讨论区】【关闭】 要父进程知道子...
  • Linux进程优先级的处理 日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 1 前景回顾1.1 进程调度内存中保存了对每个进程的唯一...
  • db2 进程全接触

    千次阅读 2014-06-23 20:29:02
    该信息不仅对执行问题和资源分析的管理员有用,而且对于那些开发高度可用性和故障转移脚本(这些脚本监控 DB2 进程,以确定何时需要进行诸如数据库重新启动或服务器故障转移之类的操作)的人也很有用。  如果您...
  • linux进程管理原理

    千次阅读 2017-08-16 05:09:57
    Linux 是一种动态系统,能够适应不断变化的计算需求。... 在用户空间,进程是由进程标识符(PID)表示的。从用户的角度来看,一个 PID 是一个数字值,可惟一标识一个进程。一个 PID 在进程的整个生命期间不会更
  • linux 进程

    千次阅读 2007-09-21 10:04:00
    进程 目 录 进程 信号 sched.c 进程信号队列 SMP 内核线程页目录的借用 代码分析 线程
  • Android进程分类与管理

    千次阅读 2016-06-30 13:53:05
    这些保留在内存中的进程通常情况下不会影响整体系统的运行速度,并且当用户再次激活这些进程时,提升了进程的启动速度。 那Android什么时候结束进程?结束哪个进程呢?之前普遍的认识是Android是依据一个名为LRU...
  • 再谈Android客户端进程保活

    千次阅读 2017-09-09 23:37:35
    进程保活:尽量保证应用的进程不被Android系统回收。 在很早以前,谈Android的保活都会涉及到进程常驻内存,如何进行性能优化等话题,今天就这些话题,做一个简单的总结。Android进程在讨论这个问题之前,我们首先...
  • Oracle 后台进程(五)SMON进程

    千次阅读 2019-05-24 17:11:00
    你所不知道的后台进程 SMON 功能 SMON(system monitor process)系统监控后台进程,有时候也被叫做 system cleanup process, 这么叫的原因是它负责完成很多清理(cleanup)任务。但凡学习过 Oracle 基础知识的技术...
  • [OS复习]进程管理4

    千次阅读 2016-08-01 14:22:40
    进程调度算法(Short-Term) 1.先来先服务(FCFS) 该方法按照进程到达的先后顺序排队,每次调度队首的进程(就像超市中购物付款一样)。 FCFS算法属于非剥夺调度方式,实现简单,看似公平。但是对于那些后进入...
  • 看到知乎上有个关于linux多进程、多线程的讨论:...1. 多进程模型和多线程模型,这两种模型在linux上有什么区别,各有何优缺点?  这里仅限于linux平台,因为linux平台跟
  • 4. copy_process: 进程描述符的处理  copy_process函数也在./linux/kernel/fork.c中。它会用当前进程的一个副本来创建新进程并分配pid,但不会实际启动这个新进程。它会复制寄存器中的值、所有与进程环境相关的...
  • linux进程间通信总结

    千次阅读 2015-06-26 11:14:21
    1. 概览 本文记录经典的IPC:pipes, FIFOs, message queues, semaphores, and shared memory。...历史上,它们是半双工的,现在某些系统提供全双工管道。它们只能在共有祖先的进程间使用。通常,一个管道由一

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 61,351
精华内容 24,540
关键字:

影响了历史进程的选择