转--Linux和Windows创建进程速度比较

时间:2022-04-30 09:25:57
编辑器加载中...《Unix编程艺术》倡导多进程架构,书中认为“相对独立地址空间的轻量级进程,线程是个糟糕的替代;线程是那些进程生成昂贵、IPC功能薄弱的操作系统的概念”;“基于线程的程序不仅产生普通的竞争问题,而且产生了新一类bug:时序依赖。”。确实Unix对线程确实不怎么看重,属于Unix文化范畴的Nginx、Python和Chrome都采取多进程的策略。Nginx默认情况下使用多进程模型,在生产环境下也不开启多线程(已经向淘宝网相关技术人员求证过)。Python引入GIL锁使得Python上的多线程是伪多线程,但是这大大提高了单线程程序的效率。Chrome是第一个采用多进程架构的浏览器,这种新架构不仅证明了多进程也可以拥有非常快的速度,并且具有更高的安全性。Chrome在效率和安全性上都是当前浏览器领域的翘楚。 对于Linux而言,确实没有太多必要使用多线程。主要有以下原因: 1、Linux本身采用的是轻量级进程作为线程。创建线程的开销同创建进程的开销差不多。 2、线程频繁地竞争临界资源导致效率低下。 3、Linux拥有高效的IPC功能。通过共享内存,进程可以高效共享数据。 4、多进程架构安全性更佳。 下面分别测试了Windows和Linux创建进程的速度。Unix是在不断发展过程引入多线程功能的,Windows则是作为多线程操作系统而设计。可以预见Unix创建进程的速度会比Windows快得多。 子进程序执行的代码如下: int main(void) { return 0; } Windows测试代码如下: ?View Code C #include #include #include int main(void) { TCHAR cmdline[] = _T("TestPro"); PROCESS_INFORMATION pi; STARTUPINFO si; ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); int n = 1000; DWORD t0 = GetTickCount(); for (int i=0; i #include #include #include #define NUM 1000 int main() { pid_t pid; struct timeval begin, end; int i; gettimeofday(&begin, NULL); for (i = 0; i < NUM; ++i) { //创建子进程 pid = fork(); if (pid == -1) { perror("fork"); exit(-1); } //子进程立即退出 else if (pid == 0) { if (execlp("./TestPro", NULL, NULL) < 0) { perror("execlp"); } } } gettimeofday(&end, NULL); printf("%dms\n", ((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec) / 1000); return 0; } 测试硬件平台为Core 2 Duo T5450 1.66GHz,1GB内存。操作系统为Windows XP SP3/Ubuntu 10.04。测试时关闭所有窗口,断开网络,只开启一个终端运行测试程序。 Windows五次运行的结果如下: 8015ms 7968ms 7968ms 8016ms 7984ms Linux五次运行的结果如下: 291ms 281ms 292ms 281ms 277ms Windows创建一个进程大约需8ms左右,而Linux创建一个进程大约需0.28ms,二者相差大约28倍(因为vfork减少了一些拷贝过程,因此使用vfork速度还能再快一点点)。在测试过程中,我发现Linux一个进程至多只能创建1.5万个子进程左右,否者就会出现”Resource temporarily unavailable”这个错误。系统没有限制max user processes。 下面是使用strace得到的系统调用统计结果: ?View Code TEXT % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 100.00 0.001750 1 1229 229 clone 0.00 0.000000 0 1 read 0.00 0.000000 0 1 write 0.00 0.000000 0 2 open 0.00 0.000000 0 2 close 0.00 0.000000 0 1 execve 0.00 0.000000 0 3 3 access 0.00 0.000000 0 1 brk 0.00 0.000000 0 2 gettimeofday 0.00 0.000000 0 1 munmap 0.00 0.000000 0 4 mprotect 0.00 0.000000 0 7 mmap2 0.00 0.000000 0 3 fstat64 0.00 0.000000 0 1 set_thread_area ------ ----------- ----------- --------- --------- ---------------- 100.00 0.001750 1258 232 total 可以看到clone一共调用了1229次,其中有229次失败了。使用下面的代码测试Linux下线程的创建速度。 ?View Code C #include #include #include #include #define NUM 1000 void* callback(void* p) { return NULL; } int main() { pthread_t tid; struct timeval begin, end; int i; int ret; gettimeofday(&begin, NULL); for (i = 0; i < NUM; ++i) { //创建线程 ret = pthread_create(&tid, NULL, (void*)callback, NULL); if (ret == -1) { perror("pthread_create"); } } gettimeofday(&end, NULL); printf("%dms\n", ((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec) / 1000); return 0; } 创建1000个线程仅需29ms左右,系统调用统计如下。可以发现主要的时间都花在mmap2系统调用了,clone仅仅调用380个左右,有点奇怪。 ?View Code TEXT % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000306 0 1011 620 mmap2 0.00 0.000000 0 2 read 0.00 0.000000 0 1 write 0.00 0.000000 0 3 open 0.00 0.000000 0 3 close 0.00 0.000000 0 1 execve 0.00 0.000000 0 4 4 access 0.00 0.000000 0 3 brk 0.00 0.000000 0 2 gettimeofday 0.00 0.000000 0 1 munmap 0.00 0.000000 0 380 clone 0.00 0.000000 0 1 uname 0.00 0.000000 0 385 mprotect 0.00 0.000000 0 2 rt_sigaction 0.00 0.000000 0 1 rt_sigprocmask 0.00 0.000000 0 1 getrlimit 0.00 0.000000 0 4 fstat64 0.00 0.000000 0 2 1 futex 0.00 0.000000 0 1 set_thread_area 0.00 0.000000 0 1 set_tid_address 0.00 0.000000 0 1 set_robust_list ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000306 1810 625 total 昨天同三百同学交流了下,他马上就看了glibc中相关的源代码,然后就发现当mmap2调用失败的情况下,就不会再调用clone了。这样确实吻合得很好,因为mmap2调用失败的次数(620)和clone的调用次数(380)加起来刚好是1000次。这里调用mmap2多于1000次,多出来的应该是加载动态库导致的。pthread_create调用成功返回0,否者返回一个非零值,这点同一般的系统调用和库函数调用失败返回-1不一样,所以上面代码判断返回值为-1认为调用失败是错误的。正确的情况应该是判断返回值是否为非零值。经过测试,发现在我的机器上一个进程最多只能创建300多个线程。在使用Intel Xeon E5420(64位CPU,内存8GB)的RHEL AS 4服务器上创建10000个线程都没有问题。 这篇文章参照了windows和linux进程启动速度比较,不过原文中有一些不妥之处: 1、使用System启动子程序。Linux下System函数会创建子进程,这样Linux创建的进程比Windows多了一倍。应该使用exec系统调用。 2、子程序代码偏多,使用malloc和free等库函数,干扰了实验结果。 ;>