從一道面試題來學習前臺程式和後臺程式、孤兒程式和殭屍程式

畫個一樣的我發表於2023-04-17

1、面試題介紹

以前面試,面試官問了一個問題,大意是:

我們在終端中,透過執行 python main.py 命令,會啟動一臺前臺程式直到程式結束。現在我還是想透過執行 python main.py ,啟動一個後臺程式,讓後臺程式執行我們的業務邏輯。這個時候應該怎麼做呢?

回答上面這道題,需要先了解什麼是前臺程式和後臺程式,什麼是孤兒程式和殭屍程式?接下來,我們先一起看看前臺程式和後臺程式,以及孤兒程式和殭屍程式。最後再透過編寫程式碼來完成面試題的需求。

2、前臺程式和後臺程式

2.1 什麼是前臺程式

在 Linux 中,前臺程式是指當前正在執行的程式,它與使用者互動並佔用終端。當使用者在終端中輸入命令時,該命令所啟動的程式就是前臺程式。

前臺程式會佔用終端,直到它執行完畢或者被中斷(例如按下 Ctrl+C)。在前臺程式執行期間,使用者可以透過鍵盤輸入命令或者傳送訊號來與程式互動。

2.2 什麼是後臺程式

Linux 後臺程式是指在終端中執行的程式,但是不會佔用終端的輸入輸出,而是在後臺執行

在 Linux 中,可以透過在命令後面加上 & 符號來將程式放到後臺執行。例如,執行命令 command & 就可以將 command 程式放到後臺執行。

後臺程式可以在終端關閉後繼續執行,也可以同時執行多個後臺程式。可以使用 jobs 命令檢視當前執行的後臺程式,使用 fg 命令將後臺程式切換到前臺執行,使用 bg 命令將前臺程式切換到後臺執行。

需要注意的是,後臺程式在另外的終端是檢視不到任何資訊的,如果需要檢視程式的輸出資訊,一般是將輸出重定向到檔案中,然後使用 tail 命令實時檢視輸出。

2.3 前臺程式、後臺程式如何切換

在 Linux 中,可以使用以下命令將前臺程式切換到後臺程式:

  1. 使用 Ctrl + Z 將當前正在執行的前臺程式掛起。
  2. 使用 bg 命令將掛起的程式切換到後臺執行。

使用 command & ,這樣的方式是一開始就將 command 程式放到後臺執行。

我們透過下面這個例子,來感受下前臺程式和後臺程式是如何切換的。

1、編寫 test.py檔案。

import os
import time


def main():
    a = 1
    while True:
        time.sleep(1)
        a += 1
        print("a---->", a)
        if a > 30:
            break


if __name__ == '__main__':
    main()

2、透過 python test.py執行程式。然後使用ctrl + Z 是程式暫停到後臺。注意,這個時候程式不會再往下執行了。

  • 如果想程式繼續往下執行,使用 bg 命令將其切換到後臺執行。

  • 還有一種方式是:在最開始執行命令的時候,加上 &。即python main.py &

python test.py
a----> 2
a----> 3
^Z
[1]  + 1761 suspended  python test.py
sample_test [master●] %

3、透過jobs命令查詢後臺程式。

sample_test [master●] % jobs          
[1]  + suspended  python test.py
sample_test [master●] %

4、使用fg命令將指定的後臺程式切換到前臺執行。(fg %N,這裡的N是 jobs返回的序號,並不是程式的PID)

    
sample_test [master●] % fg %1     
[1]  + 1761 continued  python test.py
a----> 4
a----> 5
a----> 6
a----> 7

3、孤兒程式和殭屍程式

透過上面的講解,我們知道了什麼是後臺程式和前臺程式。接下來,我們再講講什麼是孤兒程式和殭屍程式。

3.1 什麼是孤兒程式

孤兒程式是指父程式已經結束或者異常退出,而子程式還在執行的情況下,子程式就會變成孤兒程式。孤兒程式會被作業系統的init程式接管init程式會成為孤兒程式的新的父程式,並負責回收孤兒程式的資源,避免資源洩露和浪費。

因此一般來說,孤兒程式並不會有什麼危害。

我們來看一個關於孤兒程式的例子:

在main函式中,建立子程式,然後讓父程式睡眠1s,讓子程式先執行列印出其程式id(pid)以及父程式id(ppid);隨後子程式睡眠3s(此時會排程到父程式執行直至結束),目的是讓父程式先於子程式結束,讓子程式有個孤兒的狀態;最後子程式再列印出其程式id(pid)以及父程式id(ppid);觀察兩次列印 其父程式id(ppid)的區別。

import os
import time


def main():
    pid = os.fork()  # 建立子程式
    if pid < 0:
        # 建立失敗
        print("fork failed!")
        exit(1)
    elif pid == 0:  # 子程式
        print("I am the child process.")
        print("pid:%d, ppid:%d " % (os.getpid(), os.getppid()))
        # 子程式睡眠3s,保證父程式先退出,此後子程式成為孤兒程式
        time.sleep(3)
        # 注意檢視孤兒程式的父程式變化
        print("after sleep, pid:%d, ppid:%d" % (os.getpid(), os.getppid()))
        assert os.getppid() == 1
        print("child process is exited.")
    else:
        print("I am father process.")
        # 為保證子程式先執行,讓父程式睡眠1s
        time.sleep(1)
        print("father process is exited.")




if __name__ == '__main__':
    main()

執行結果:

執行結果表明:當父程式結束後,子程式成為了孤兒程式。因為它的父程式id(ppid)變成了1,即init程式成為該子程式的父程式了。

3.2 什麼是殭屍程式

Linux 殭屍程式是指已經結束執行的程式,但是其父程式還沒有對其進行處理,導致該程式的程式描述符仍然存在於系統中,這種程式被稱為殭屍程式

殭屍程式不會佔用系統資源,但是如果大量的殭屍程式存在,會佔用系統的程式描述符資源,導致系統程式描述符資源耗盡,從而導致系統崩潰。

為了避免殭屍程式的出現,父程式需要及時對其進行處理,可以使用 wait() 或 waitpid() 等系統呼叫來等待子程式結束並回收其資源。另外,也可以使用訊號處理機制,在父程式中註冊 SIGCHLD 訊號處理函式來處理子程式結束的訊號。

關於殭屍程式的例子

在main函式中,建立子程式,然後讓父程式睡眠30s,讓子程式先終止(注意和孤兒程式例子的區別);這裡子程式結束後父程式沒有呼叫wait/waitpid函式獲取其狀態,用ps檢視程式狀態可以看出子程式為殭屍狀態。

import os
import time


def main():
    pid = os.fork()  # 建立子程式
    if pid < 0:
        # 建立失敗
        print("fork failed!")
        exit(1)
    elif pid == 0:  # 子程式
        print("I am the child process.I am exited.")
        exit(0)
    else:
        print("I am father process.")
        #  父程式睡眠30s等待子程式退出,且沒有呼叫wait/waitpid獲取其狀態
        #  子程式會成為殭屍程式
        time.sleep(30)
        print("father process is exited.")




if __name__ == '__main__':
    main()

開一個終端,在終端是執行test.py

在子程式結束,父程式睡眠(還沒退出)的時候,再開一個終端用PS檢視程式狀態。

注意:

  1. 任何一個子程式(init除外)在exit()之後,其實它並沒有真正的被銷燬,而是留下一個稱為殭屍程式(Zombie)的資料結構,等待父程式處理。這是每個子程式在結束時都要經過的階段。
  2. 如果子程式在exit()之後,父程式沒有來得及處理,這時用ps命令就能看到子程式的狀態是“Z”。如果父程式能及時處理,可能用ps命令就來不及看到子程式的殭屍狀態,但這並不等於子程式不經過殭屍狀態。
  3. 如果父程式在子程式結束之前退出,則子程式將由init接管。init將會以父程式的身份對殭屍狀態的子程式進行處理。
  4. 如果父程式是一個迴圈,不會結束,那麼子程式就會一直保持殭屍狀態,這就是系統中有時候會有很多殭屍程式的原因。

那麼如何殺死殭屍程式呢,可以查詢 殭屍程式與孤兒程式 連結,來進行學習,這裡就不再講述。

3.3 總結

  • 孤兒程式:父程式已亡,子程式成為孤兒,被init程式收養,由init程式對它們完成狀態收集工作,因此一般沒有壞的影響。
  • 殭屍程式:子程式已亡,父程式沒給子程式收屍,子程式成為殭屍程式,佔用系統資源。

4、面試題解決方式

現在再回過頭來看 面試題的要求:

在終端中執行 python main.py命令,啟動後臺程式來進行業務處理。

那麼我們可以利用孤兒程式的特性,完成上面的需求。

1、透過 os.fork()建立子程式。

2、建立完成後,讓父程式退出,子程式繼續執行。

簡單案例:

import os
import time


def main():
    pid = os.fork()  # 建立子程式
    a = 0
    if pid < 0:
        # 建立失敗
        print("fork failed!")
        exit(1)
    elif pid == 0:  # 子程式
        for i in range(10):
            time.sleep(1)
            a += i
        print("child process calculate result: %d" % a)
    else:
        print("I am father process.")
        exit(0)


if __name__ == '__main__':
    main()
sample_test [master●] % python test.py
I am father process.


10秒鐘過後
sample_test [master●] % child process calculate result: 45

參考資料:

殭屍程式與孤兒程式

相關文章