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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章