06-进程的创建速度
28.3 进程的创建速度
表28-3对采用不同方法创建进程的速度进行了比较。测试程序在循环中反复创建子进程并等待子进程终止,从而获得了这一结果。比较过程中使用了3种不同大小的进程内存,如表中虚拟内存总量(total virtual memory)值所示。对不同大小内存的模拟,依赖于程序计时之前在堆中分配(malloc())的额外内存。
表28-3中的进程大小(虚拟内存总量)取自命令ps –o“pid vsz cmd”输出的VSZ值。
| 进程的创建方法 | 虚拟内存总量 | | :----- | :----- | :----- | :----- | | 1.70 MB | 2.70 MB | 11 .70 MB | | 时间(秒) | 速率 | 时间(秒) | 速率 | 时间(秒) | 速率 | | fork() | 22.27 | (7.99) | 4544 | 26.38 | (8.98) | 4135 | 126.93 | (52.55) | 1276 | | vfork() | 3.52 | (2.49) | 28955 | 3.55 | (2.50) | 28621 | 3.53 | (2.51) | 28810 | | clone() | 2.97 | (2.14) | 34333 | 2.98 | (2.13) | 34217 | 2.93 | (2.10) | 34688 | | fork()+exec() | 135.72 | (12.39) | 764 | 146.15 | (16.69) | 719 | 260.34 | (61.86) | 435 | | vfork()+exec() | 107.36 | (6.27) | 969 | 107.81 | (6.35) | 964 | 107.97 | (6.38) | 960 |
表28-3对于每种进程大小都提供了两类统计数据。
- 第1项统计包含两种度量时间。以执行10万次进程创建期间所逝去的(实际)时间为主(较大值),以父进程所消耗的 CPU 时间(括号内的值)为辅。由于测试环境并无其他负载,两者之差应是测试期间创建子进程所消耗的时间总量。
- 第2项数据显示每(实际)秒创建的进程数,即创建速率,取各种情况下运行20次的平均值。实验基于x86-32系统,内核版本为2.6.27。
前3行针对的是简单的进程创建(子进程不运行新程序)。子进程在创建后立即退出,父进程等待子进程终止后再去创建下一个子进程。
第1行取自系统调用fork()。由数据可知,进程所占内存越大,fork()所需时间也就越长。额外时间花在了为子进程复制那些逐渐变大的页表,以及将数据段、堆段以及栈段的页记录标记为只读的工作上。因为子进程并未修改数据段或栈段,所以也没有对页(page)复制。
第2行取自vfork()。可以看出,尽管进程大小在增加,但所用时间保持不变,因为调用vfork()时并未复制页表或页,调用进程的虚拟内存大小并未造成影响。fork()和 vfork()在时间统计上的差值就是复制进程页表所需的时间总量。
表28-3中vfork()和clone()的各自数据在不同的进程内存大小下。之所以存在微小的差异,要归因于采样误差以及调度的变化。即使创建300MB大小的进程,两个系统调用的时间仍将保持不变。
第3行数据的统计信息来自对clone()的调用,所使用的标志如下:
前两个标志模拟vfork()的行为。剩余的标志则要求父、子进程应当共享文件系统属性文件权限掩码(umask)、根目录和当前工作目录,信号处置表以及打开文件描述符表。clone()和vfork()之间的数据差值则代表了vfork()将这些信息拷贝到子进程的少量额外工作。拷贝文件系统属性和信号处置表的成本是固定的。不过,拷贝打开文件描述符表的开销则取决于描述符数量。例如:父进程打开100个文件,vfork()的实际时间(表中第1列)会从3.52秒增至5.04秒,但不会影响clone()所需要的时间。
对clone()的计时针对的是glibc库的封装函数clone(),而非直接调用sys_clone()。另有测试(在此恕不一一列出)对sys_clone()和clone()(以子函数调用并立即退出) 做了比较,实验结果表明,时间上的差异可以忽略不计。
fork()和vfork()之间的差别非常明显,但仍需要注意以下几点。
- 最后一列数据表明,在大进程情况下,vfork()要比fork()快逾30倍。而针对普通进程,则会近乎于表中前两列的数据。
- 因为进程的创建时间往往比 exec()的执行时间要少得多,所以如果随后接着执行exec(),那么两者间的差异也就不再明显。表 28-3 的最后两行数据说明了这一点,其中的每个子进程都去调用exec(),而非直接退出。程序执行的是true命令(/bin/true,选择该程序的原因是因为它不产生任何输出)。这时,fork()和vfork()之间的相对差距就小了许多。
事实上,表28-3中所示数据并未揭示exec()的全部开销,因为测试程序的每个循环中子进程均执行同一程序。根本就未计入把程序文件读入内存的磁盘I/O开销,因为第一次运行exec()时就会将程序读入内核缓冲区,并一直保存在那里。如果测试每次循环执行的程序不同(例如,复制同一程序,并以不同文件名命名),那么应该可以观察到 exec()的开销要大出许多。