一、理解fork()
fork()是一个绝对唯一的调用。Python中的大多数函数会之返回一次,因为sys.exit()会终止程序,所以它就不会返回。相比之下,Python的os.fork()是唯一返回两次的函数,任何返回两次的函数,在某种意义上,都可以调用os.fork()来实现。在调用fork()之后,就同时存在两个正在运行程序的拷贝。但是第二个拷贝并不是从开始就重新开始的。两个拷贝在对fork()调用后会继续——进程的整个地址空间被拷贝。这时可能会出现错误,而os.fork()可以产生异常。
对fork的调用,返回针对父进程而产生新进程的PID。对于子进程,它返回PID 0.因此,它的逻辑如下:
def handle():
pid = os.fork()
if pid:
#parent
close_child_connections()
handle_more_connections()
else:
#child
close_parent_connections()
process_this_connections()
二、zombie进程
fork()的语义是建立在父进程对找出子进程什么时候,以及如何终止感兴趣的假定上的。例如,一个shell脚本会对找出正在运行的程序中的退出代码感兴趣。父进程不仅可以找出退出代码,还可以找出根据信号,进程是坏掉还是终止。父进程是通过os.wait()或一个类似的调用来得到这些信息的。
在子进程终止和父进程调用wait()之间的这段时间,子进程被成为zombie进程。它停止了运行,但是内存结构还为允许父进程执行wait()保持着。在子进程终止后,必须调用wait()函数,否则系统系统资源会被大量的zombie进程消耗掉,最终会使服务器不可用。
操作系统可以非常容易地完成这个工作。每当子进程终止的时候,它会向父进程发送SIGCHLD信号(信号是一个通知进程某些事件的基本方法)。父进程可以设置一个信号处理程序来接受SIGCHLD和整理已经终止的子进程。
如果父亲进程在子进程之前终止,子进程会一直执行。系统会通过把它们的父进程设置为init(PID 1)来重新制定父进程。init进程就会负责清楚zombie进程。
三、fork()性能
由于fork()函数每次在客户端连接的时候必须在整个服务器中拷贝,所以或许有人会认为它是一个很慢的方法。事实上,fork()的性能对于几乎所有具有高负载的系统来说是可忽略的。
大多数的操作系统,例如linux,是通过copy-on-write内存来实现fork()的。这就意味着,只有内存需要被拷贝(当有进程要修改它)的时候,它才会真正被拷贝。实际上,对fork()的调用通常是瞬间的。
对fork()的调用是应用在整个系统中的。例如,当使用Shell,输入ls,Shell就会调用fork()来产生一个fork的拷贝,新的进程将调用ls。
四、fork()示例
#!/usr/bin/env python
#coding:utf-8
import os,time
print 'before the fork,my PID is',os.getpid()
if os.fork():
print 'Hello from the parent. My PID is',os.getpid()
else:
print 'Hello from the child. My PID is',os.getpid()
time.sleep(1)
print 'Hello from both of us.'
两个进程应该同时执行,当程序执行到该点的时候,实际上存在着两个程序的拷贝在执行。所以问候语在代码中只出现一次,而结果中却显示两次。
五、zombie示例
#!/usr/bin/env python
import os,time
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 120 seconds...'
time.sleep(120)
子进程会在fork()之后立刻终止,父进程在sleep,能看出子进程出现了zombie,可以从第三列中的Z和输出最后的<defunct>看出来。一旦父进程终止了,将可以确定两个进程都不存在了。
六、使用信号解决zombie问题
#!/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)
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 seconds...'
time.sleep(10)
print 'Sleep done.'
else:
print 'Child sleeping 5 seconds...'
time.sleep(5)
首先,这个程序定义了信号处理程序chldhandler()。每次收到SIGCHLD的时候,就会调用这个函数。它有一个简单的循环调用os.waitpid(),它的第一个参数-1,意思是等待所有的已经终止的子程序,而第二个参数是说如果没有已经终止的进程存在,就立刻返回。如果有子进程在等待,waitpid()返回一个进程的PID的tuple和退出信息。否则,它产生一个异常。使用wait()或waitpid()来搜集终止进程的信息被称为收割(reaping).
示例中子进程睡眠5秒钟后,父进程就开始收割。time.sleep()有一种特殊情况,如果任意一个信号处理程序被调用,睡眠会被立刻终止,而不是继续等待剩余的时间。
七、总结
大多数服务器都需要同时处理多个客户端。对于服务器的设计者来说,有几种方法可以实现它,其中最简单的就是forking,它主要适用于Linux和UNIX平台。
为了使用fork,需要调用os.fork(),它会返回两次。这个函数把子进程的进程ID返回给父进程,还会把零值返回给子进程。
当某个进程终止的时候,除非该进程的父进程调用了wait()或waitpid(),否则终止信息会一直保持在系统上。因此使用foring的程序必须确保在子进程终止时要调用wait()或waitpid(),方法之一是信号处理程序,还可以使用轮询(polling),定期检查终止的子程序。
使用forking的服务器通常会调用fork()来为每一个到来的连接建立一个新进程。对于进程中不使用的文件描述符,重要的一点是父进程和子进程都应该关闭。
如果文件被修改,锁定是非常重要的。锁定可以避免数据损坏。如果多个进程同时修改一个文件,或者一个进程读取文件的时候,另一个进程正在写文件,都会损坏文件。
如果系统不能执行fork,os.fork()函数可以产生异常。为了防止服务器当机,必须处理这个异常。