1. 程式人生 > >從Redis中的BGSAVE命令談起Fork—之一

從Redis中的BGSAVE命令談起Fork—之一

引言
本人近日在讀黃建巨集先生的《Redis設計與實現》中RDB檔案的建立與載入一節,瞭解到SAVE命令和BGSAVE命令的實現。
SAVE:其中SAVE命令是阻塞式的,它會阻塞Redis伺服器程序,直到RDB檔案建立完畢為止,在伺服器程序阻塞期間,伺服器不能處理任何命令請求。
BGSAVE:和SAVE命令直接阻塞伺服器程序的做法不同,BGSAVE命令會派生出一個子程序,然後由子程序負責建立RDB檔案,伺服器程序(即父程序)繼續處理命令請求。
本文由BGSAVE命令的實現談起Python/OS模組中的fork函式。為了更加清晰的瞭解fork函式的工作原理,讀者可以跟我一塊在linux下檢視程序(ps命令),並對這個方法做一些測試,加深理解。(需要注意

的是fork這個函式和操縱系統本身結合的非常緊密,windows下無法使用os.fork()函式,必須在POSIX系統(如Liunx/Unix/Mac系統)下進行測試

第一部分
首先,我們來看一下,BGSAVE命令的實現,下面以Python語言給出其實現虛擬碼:

def BGSAVE():
    #建立子程序
    pid = fork()
    if pid == 0:
        #子程序負責建立RDB檔案
        rdbSave()
        #完成之後向父程序發出訊號
        signal_parent()
    elif pid > 0:
        #父程序繼續處理命令請求,並通過輪詢等待子程序訊號
handle_request_and_wait_signal() else: #處理出錯情況 handle_fork_error()

讀者如果不瞭解fork函式或者說Python中多程序的建立,讀到這段程式碼可能有些不太明白,沒關係,請看下面(簡單直接)的解釋。

python中的os.fork()被呼叫後就會立即生成一個子程序,是通過copy父程序的地址空間和資源來實現子程序的建立,同時:

  • fork函式在子程序中返回的是0;
  • 在父程序中返回的是子程序的PID;
  • 出現錯誤,fork返回一個負值;

所以,我們可以通過fork的返回值來判斷當前程序是子程序還是父程序。現在再返回去看這個程式是不是就很清楚了,沒理解也沒關係,下面進行詳細的討論。

第二部分
首先我們來寫一個簡單的Python指令碼,這個程式執行時,系統會生成一個新的程序,看下面程式碼:

#!/usr/bin/env python
#coding=utf8
from time import sleep
print "Main thread!"
sleep(3000)

因為程式碼執行完後,程序就會被銷燬,所以這裡讓程式休眠30秒,方便看到效果。在linux下執行這個程式碼:

python hello.py &

加上&符號,可以讓程式在後臺執行,不會佔用終端。輸入ps -l命令檢視程序,在終端輸出如下:
這裡寫圖片描述
其中第二條記錄就是剛才執行的python程式。
接著,我們使用fork來建立子程序
使用fork建立一個新程序成功後,新程序會是原程序的子程序,原程序稱為父程序。如果發生錯誤,則會丟擲OSError異常。

#!/usr/bin/env python
#coding=utf8

from time import sleep
import os
try:
    pid = os.fork()
except OSError, e:
    pass
sleep(30)

執行程式碼,檢視程序,在終端上輸出如下:
這裡寫圖片描述
由圖知,執行程式碼後產生了兩條程序,可以看出第二條python程序就是第一條的子程序。
然後,我們再來看fork程序後的程式流程
使用fork建立子程序後,子程序會複製父程序的資料資訊,而後程式就分兩個程序繼續執行後面的程式,這也是fork(分叉)名字的含義了。在子程序內,這個方法會返回0;在父程序內,這個方法會返回子程序的編號PID。可以使用PID來區分兩個程序(這個程式就類似於文章開始時候給出的BGSAVE的實現虛擬碼):

#!/usr/bin/env python
#coding=utf8
import os

#建立子程序之前宣告的變數
source = 10
try:
    pid = os.fork()
    if pid == 0: #子程序
        print "this is child process."
        #在子程序中source自減1
        source = source - 1
        sleep(3)
    else: #父程序
        print "this is parent process."
    print source
except OSError, e:
    pass

上面程式碼中,在子程序建立前,聲明瞭一個變數source,然後在子程序中自減1,最後打印出source的值,顯然父程序打印出來的值應該為10,子程序打印出來的值應該為9。為了明顯區分父程序和子程序,讓子程序睡3秒,就看的比較明顯了。
既然子程序是父程序建立的,那麼父程序退出之後,子程序會怎麼樣呢?此時,子程序會被PID為1的程序接管,就是init程序了。這樣子程序就不會受終端退出影響了,使用這個特性就可以建立在後臺執行的程式,俗稱守護程序(daemon)。

第三部分
雖然fork給予多程序程式設計很大的方面性,但是濫用也是會有很多大問題的,如果程式程式碼中有fork子程序來操作資料,但是由於fork之後,沒有及時的退出,就會導致系統中的Python程序越來越多,子程序越來越多。請看Python下fork程序的測試程式碼:

def fork(a):
    def now():
        import datetime
        return datetime.datetime.now().strftime("%S.%f")
    import os
    import time
    print now(), a
    if os.fork() == 0:
        print '子程序[%s]:%s' % (now(), os.getpid())
        while 1:
            a-=10
            print '子程序的a值[%s]:%s' % (now(), a)
            if a < 1:
                break
        print '準備退出子程序'
        #os._exit(0) ## 你可以在這裡退出子程序
    else:
        print '父程序[%s]:%s' % (now(), os.getpid())
        while 1:
            a-=1
            print '父程序的a值[%s]:%s' % (now(), a)
            if a < 0:
                break
        time.sleep(1)
        print '等待子程序結束...'
        try:
            result = os.wait()
            if result:
                print '子程序:', result[0], result[1]
            else:
                print '沒有資料!'
        except:
            print '異常...'
        print '父程序...'
    print '最後的值:',a
    #exit(0)  ## 你也可以在這裡退出,注意,這裡是父程序和子程序都共用的地方,在這裡退出會導致父程序也一併退出

隨後的TIPS:
os.fork() 會有兩次返回值,分別是父程序和子程序的返回值
在父程序中,fork返回的值是子程序的PID;
子程序中,這個返回值為0
子程序會複製父程序的上下文
父子程序並不能確定執行順序
os.fork()之後,子程序一定要使用exit()或者os._exit()來退出子程序環境,建議使用os._exit()。