测试环境

  • Intel(R) Pentium(R) CPU G2030 @ 3.00GHz
  • 8G内存
  • debian jessie
  • Linux 3.16-2-amd64
  • 2014年10月27日

附注一下,该CPU有2核心,无HT,1ns3个时钟周期。

测试方法

测试代码如下:

time -f "%e,%S,%c,%r,%s,%K,%P" ./perf_fork

数据的意义分别为: 总时间,占用CPU时间,context switch次数,读/写次数,内存耗用,CPU使用百分比。

数据处理方法如下:

import numpy as np
p = lambda s: [float(line.strip().split(',')[0]) for line in s.splitlines()]
q = lambda s: [float(line.strip().split(',')[1]) for line in s.splitlines()]
np.array(p(s)).mean()
np.array(p(s)).var()
np.array(q(s)).mean()
np.array(q(s)).var()

进程fork开销

使用s_fork程序(注释语句关闭模式),粒度1M次,重复6次,原始数据如下:

49.04,26.83,29784,0,0,0,55%
51.53,26.38,32057,0,0,0,52%
49.88,26.02,30892,0,0,0,53%
51.39,27.13,37573,0,0,0,54%
52.89,28.12,37924,0,0,0,54%
51.19,27.02,35880,0,0,0,54%

统计结果如下:

  • time mean = 50.98
  • time var = 1.52
  • cpu mean = 26.92
  • cpu var = 0.43

从数据上,我们可以简单得到结论。在测试设备上,每次fork的开销为51us,CPU开销为27us,精确级别在1-2us左右。粗略换算一下,一次fork大约消耗了150k个时钟周期。

注意,这个数据并不代表fork本身的速度。因为除去fork之外,我们还有子进程退出的开销,父进程wait的开销。甚至严格来说,还包括了至少一次的context switch(有趣的是,这个取决于fork后是优先执行子进程还是父进程)。

但是作为进程模式的服务程序,这些开销都是预料中必须付出的。

另外cs次数比产生的进程数远小(TODO: why?)。

fork模式强制优先执行子进程

s_fork中,注意那句注释。当优先执行子进程时,会发生什么现象?

预期来说,应当不发生变化,或者轻微的变慢。因为我们预期系统优先执行子进程(以减少exec前的page cow)。如果发生变化,那么说明这个假定是不正确的。真实情况是优先执行父进程或者无保证。

如果发生变化,首先是一次context switch会变为两次。因为如果在产生了大量子进程后再依次cs,那么需要N+1次cs来结束所有子进程并返回父进程,平均每个子进程一次cs(N足够大的情况下基本近似,例如在标准配置下30000以上)。而如果每次产生子进程就切换,那么会变为每个子进程两次cs。

其次,先执行父进程导致在每次调度时的活跃进程数更高,因此调度器的每次执行开销更高。按照算法量级估计,大约是4倍以上。但是实际复杂度的估量比平均值更加麻烦——因为活跃数总是在不停的变化中。大约是Sum(logn)/n=log(n!)/n。因此,虽然在cs次数上减少,但是每次cs的开销会增加。

最后,先执行子进程会导致上下文描述符表项被频繁的重用,从而提高命中率。当然,在我们的测试程序中做不到这点,因为每次都是开满才开始回收的。

下面是实际原始数据:

45.19,22.42,399890,0,0,0,51%
47.66,22.46,414808,0,0,0,48%
45.51,23.12,376053,0,0,0,52%
46.35,22.10,401536,0,0,0,49%
48.28,22.82,415162,0,0,0,48%
47.44,22.34,413285,0,0,0,48%

统计结果如下:

  • time mean = 46.73
  • time var = 1.29
  • cpu mean = 22.54
  • cpu var = 0.11

解读上可以发现,每10次fork产生四次cs(TODO: 为什么?),但是每次fork的开销降低为47us,CPU降为23us(用户态时间几乎不发生变化),精确级别在1us左右。

这提示我们至少一件事情——如果要用进程模式,记得先执行子进程。