python fork()多進程
- 2020 年 1 月 9 日
- 筆記
一、理解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()函數可以產生異常。為了防止伺服器當機,必須處理這個異常。