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

2023-04-17 12:00:53

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

參考資料:

殭屍程序與孤兒程序