python fork()多進程

一、理解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()函數可以產生異常。為了防止伺服器當機,必須處理這個異常。