我在上篇文章(从Redis中的BGSAVE命令谈起Fork—之一)中,从Redis中的BGSAVE命令谈起,简单地讨论了Python中的fork函数实现多进程的话题,这篇文章将进一步讨论这个话题以及Zombie(僵尸进程 )的处理。
第一部分:系统基础
需要注意,有两种方式可以实现并发性。
- 一种方式是让每个“任务”或“进程”在单独的内在空间中工作,每个都有自已的工作内存区域。不过,虽然进程可在单独的内存空间中执行,但除非这些进程在单独的处理器上执行,否则,实际并不是“同时”运行的。是由操作系统把处理器的时间片分配给一个进程,用完时间片后就需退出处理器等待另一个时间片的到来。
- 另一种方式是在在程序中指定多个“执行线程”,让它们在相同的内存空间中工作。这称为“多线程处理”。线程比进程更有效,因为操作系统不必为每个线程创建单独的内存空间。
新建进程用os.fork函数(上一篇已讨论过),但它只在POSIX系统上可用,在windows版的python中,os模块没有定义os.fork函数。相反(所以),windows程序员会用多线程编程技术来完成并发任务。
os.fork回顾
os.fork函数创建进程的过程是这样的。程序每次执行时,操作系统都会创建一个新进程来运行程序指令。进程还可调用os.fork,要求操作系统新建一个进程。父进程是调用os.fork函数的进程。父进程所创建的进程叫子进程。每个进程都有一个不重复的进程ID号,称为pid,它对进程进行标识。子进程与父进程完全相同,子进程从父进程继承了多个值的拷贝,如全局变量和环境变量。两个进程的唯一区别是fork的返回值。子进程接收返回值0,而父进程接收子进程的pid作为返回值。一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID(PID)。对于程序,只要判断fork的返回值,就知道自己是处于父进程还是子进程中。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间,它们之间共享的存储空间只有代码段。
第二部分:Zombie
首先我们来看一段代码:
#!/usr/bin/env python
import os
def task(id):
print "work %d" %id
pid = os.fork()
if pid == 0:
print "I am a child"
task(1)
else:
print "I am father child"
执行结果如下:
I am father child
I am a child
work 1
多次执行上面的例子可能结果的顺序会不一样,子进程和父进程执行的顺序是无法预估的。os.fork()执行后父进程继续往下执行,子进程也会从os.fork()语句之后开始运行,并且子进程拥有父进程所有的变量,但是两者是独立的。
看下一个例子 :
#!/usr/bin/env python
import os
import time
def task(id):
print "work %d" %id
var = 10
pid = os.fork()
if pid == 0:
print "I am a child"
var = 9
task(1)
else:
print "I am father child"
time.sleep(3)
print var
结果是 :
I am father child
I am a child
work 1
10
这段代码演示了子进程和父进程的var这个变量是独立,在子进程中改变了var变量的值,并不影响父进程中的var变量值,两者是独立的,为了防止父进程先执行,子进程后执行,所以让父进程sleep(3)了一会再输出var的值。子进程结束后,但是父进程还没有结束的时候,子进程是出于Zombie状态的,这个需要父进程去收集子进程的信息释放子进程。如果父进程结束了子进程没有结束,那么子进程就会寄托给pid为1的进程来管理。
我们继续:
#!/usr/bin/env python
import os
import time
pid = os.fork()
if pid == 0:
print "I am a child %d" %os.getpid()
else:
time.sleep(100)
print "done"
看下面输出及过程:
输出信息:
I am a child 57786
一直处于等待状态......
done
在处于等待状态的时候立刻使用ps命令查看子进程的状态
[root@localhost ~]# ps ax|grep 57786
57786 pts/4 Z+ 0:00 [python] <defunct>
57792 pts/5 S+ 0:00 grep 57786
通过上面的状态发现子进程处于defunct这个状态就是Zombie状态。
第三部分:Zombie处理
对于处理Zimbie状态的子进程有两种方式。
- 第一种是基于信号的处理,子进程在结束后就会发出SIGCHLD这个信号,通过singal接收到这个信号就可以进行收集了。
#!/usr/bin/env python
import os,time,signal
def chldhandler(signum,stackframe):
while 1:
try:
result = os.waitpid(-1,os.WNOHANG)
except:
break
print "Reaped child process %d" % result[0]
signal.signal(signal.SIGCHLD,chldhandler)
signal.signal(signal.SIGCHLD,chldhandler)
print "Before the fork my PID is",os.getpid()
pid = os.fork()
if pid:
print "hello from the parent the child will be PID %d" %pid
print "Sleeping 10 sconds....."
time.sleep(1000)
print "sleep done"
else:
print "Child sleeping 5 seconds"
time.sleep(5)
执行结果及其过程:
执行结果:
Before the fork my PID is 58322
hello from the parent the child will be PID 58323
Sleeping 10 sconds.....
Child sleeping 5 seconds
Reaped child process 58323
sleep done
5秒过后子进程结束出发信号处理函数,对子进程进行了收集和处理。并且信号打断了父进程的sleep
singal是一个信号量函数,chldhandlr是一个自定义的的信号处理的handler,通过singal可以注册接收到什么信号就自动触发哪个handler
waitpid的第一个参数代表等待所有的子进程终止,第二个参数代表如果没有已经终止的子进程就立即返回。waitpid的返回结果是一个进程的PID和退出信息组成的一个元组。在while循环结束后又重新注册了信号处理函数这是因为有些unix在信号处理程序结束后就失效了,无法再次处理其他子进程。
- 第二种处理Zombie进程的方式是轮询,父进程一段时间就去收集一下子进程,释放Zombie进行。(上篇中开篇所给出的BGSAVE命令的实现就是采取这种方式)
#!/usr/bin/env python
import os,time
def reap():
while 1:
try:
result = os.waitpid(-1,os.WNOHANG)
except:
break
print "Reaped child process %d" % result[0]
print "Before the fork,my PID is",os.getpid()
pid = os.fork()
if pid:
print "Hello from the parent .The child will be PID %d" % pid
print "Parent sleeping 60 seconds"
time.sleep(60)
print "Parent sleep done"
reap()
print "Parent sleeping 60 seconds"
time.sleep(60)
print "Parent sleep done"
else:
print "Child sleeping 5 seconds..."
time.sleep(5)
print "Child terminating"
执行结果及其过程:
执行结果:
Parent sleeping 60 seconds
Child sleeping 5 seconds...
#等待5秒.......
Child terminating
#子进程结束了
#等待55秒父进程睡眠结束
Parent sleep done
#启动reap()去采集子进程的信息,释放子进程
Reaped child process 58595
Before the fork,my PID is 58594
Parent sleeping 60 seconds
Parent sleep done
此次由Redis中BGSAVE命令的实现所谈起的os.fork函数以及多进程的相关问题就此结束。