前後臺進程、孤兒進程和 daemon 類進程的父子關係


回到Shell系列文章大綱


前後臺進程、孤兒進程和daemon類進程的父子關係

前臺進程、後臺進程和進程父子關係

前臺進程是佔用當前終端的進程,只有該進程執行完成或被終止之後,纔會釋放終端並將終端交還給shell進程。

例如:

$ sleep 30

執行該命令後,將創建sleep進程,sleep進程是當前bash進程(假如當前的shell爲bash)的子進程:

$ pstree -p | grep sleep
     |-bash(31207)---sleep(31800)

在30秒內,sleep進程將佔用終端,所以此時的sleep稱爲前臺進程。當睡眠30秒之後,前臺進程sleep退出,終端控制權交還給當前shell進程,shell進程可繼續向下運行命令或等待用戶輸入新命令。

如果給命令加上一個&符號,該命令將在後臺運行。

$ sleep 30 &

此時,sleep仍然是當前bash的子進程,但是它不會佔用終端,而是在後臺默默地運行,並且在30秒之後默默的退出。

如果是在一個子Shell環境中運行一個前臺進程呢?例如:

$ ( sleep 30 )

執行這個命令時,小括號會開啓一個子Shell環境,這相當於當前的bash進程隔離了一個bash運行時環境。sleep進程將在這個新的子Shell環境中運行,sleep仍然是當前bash的子進程。由於它會佔用當前的終端,所以它是前臺進程。

30秒之後,sleep進程退出,它將釋放終端,與此同時,子Shell環境也會隨着sleep進程的終止而關閉。

如果不瞭解子Shell,也可以通過shell腳本來理解,或程序內部使用system()來理解,它們都是提供了一種執行外部命令的運行環境。

例如bash -c 'sleep 30',sleep進程將在該bash進程提供的環境下運行,它是該bash進程的子進程。

再例如shell腳本:

#!/bin/bash

sleep 30

sleep將在這個bash腳本進程提供的環境下運行,它是該腳本進程的子進程。

再例如Perl腳本:

#!/bin/perl

system('sleep 30')

sleep將在這個Perl腳本進程提供的環境下運行。

需注意,編程語言(如Perl)可能提供多種調用外部程序的方式,比如system('sleep 30')system('sleep',30),這兩種方式有區別:

舉幾個例子幫助理解,假設有Perl腳本a.pl,其內三行內容爲:

system('sleep',30);               #(1)
system('sleep 30 ; echo hhh');    #(2)
system('sleep 30');               #(3)

對於(1),命令和參數分開,perl將直接調用sleep程序,這時的sleep進程是perl進程a.pl的子進程,且不支持使用管道|、重定向> < >>&&等等屬於Shell支持的符號。

$ pstree -p | grep sleep
     |    `-bash(31696)---a.pl(32707)---sleep(32708)

對於(2),perl將調用sh,並將參數sleep 30; echo hhh作爲sh -c的參數運行,等價於sh -c 'sleep 30; echo hhh',所以sh進程將是perl進程的子進程,sleep進程將是sh進程的子進程。

$ pstree -p | grep sleep
     |    `-bash(31696)---a.pl(32747)---sh(32748)---sleep(32749)

另外需要注意的是,(2)中的命令是多條命令,而不是簡簡單單的單條命令,因爲識別多條命令並運行它們的能力是Shell解析提供的,所以上面涉及了Shell的解析過程。由於會調用sh命令,所以允許命令中使用Shell特殊符號,比如管道符號。

對於(3),perl本該調用sh,並將sleep 30作爲sh -c的參數運行。但此處是一個簡單命令,不涉及任何Shell解析過程,所以會優化爲等價於system('sleep', 30)的方式,即不再調用sh,而是直接調用sleep,也即sleep不再是sh的子進程,而是perl進程的子進程:

$ pstree -p | grep sleep
     |      `-bash(31696)---a.pl(32798)---sleep(32799)

其實子shell中運行命令和system()運行命令的行爲是類似的:

# sleep進程是當前shell進程的子進程
$ (sleep 30)

# 當前shell進程會創建一個子bash進程
# sleep進程和echo進程是該子bash進程的子進程
$ (sleep 30 ; echo hhh)

瞭解以上插曲後,想必能清晰地理解如下結論:

孤兒進程和Daemon類進程

如果在進程B退出前,父進程先退出了呢?這時進程B將成爲孤兒進程,因爲它的父進程已經死了

孤兒進程會被PID=1的systemd進程收養,所以進程B的父進程PPID會從原來的進程A變爲PID=1的systemd進程。

注意,孤兒進程會繼續保持運行,而不會隨父進程退出而終止,只不過其父進程發生了改變。

例如,在子Shell中運行後臺命令:

$ (sleep 30 &)

因爲後臺符號&是屬於Shell的,所以涉及到shell的解析過程,所以當前bash進程會創建一個子bash進程來解析命令並提供sleep進程的運行環境。

sleep進程將在這個子bash進程環境中運行,但因爲它是一個後臺命令,所以sleep進程創建成功之後立即返回,由於小括號內已經沒有其它命令,子bash進程會立即終止。這意味着sleep將成爲孤兒進程:

$ ps -o pid,ppid,cmd $(pgrep sleep) 
   PID   PPID CMD
 32843      1 sleep 30

再比如,Shell腳本內部運行一個後臺命令,並且讓Shell腳本在後臺命令退出前先退出。

#!/bin/bash

sleep 300 &
echo over

當上述腳本運行時,sleep在後臺運行並立即返回,於是立即執行echo進程,echo執行完成後腳本進程退出。

腳本進程退出前,sleep進程的父進程爲腳本進程,腳本進程退出後,sleep進程成爲孤兒進程繼續運行,它會被systemd進程收養,其父進程變成PID=1。

當一個進程脫離了Shell環境後,它就可以被稱爲後臺服務類進程,即Daemon類守護進程,顯然Daemon類進程的PPID=1。當某進程脫離Shell的控制,也意味着它脫離了終端:當終端斷開連接時,不會影響這些進程

需特別關注的是創建Daemon類進程的流程:先有一個父進程,父進程在某個時間點fork出一個子進程繼續運行代碼邏輯,父進程立即終止,該子進程成爲孤兒進程,即Daemon類進程。當然,要創建一個完善的Daemon類進程還需考慮其它一些事情,比如要獨立一個會話和進程組,要關閉stdin/stdout/stderr,要chdir到/下防止文件系統錯誤導致進程異常,等等。不過最關鍵的特性仍在於其脫離Shell、脫離終端。

爲什麼要fork一個子進程作爲Daemon進程?爲什麼父進程要立即退出

所有的Daemon類進程都要脫離Shell脫離終端,才能不受終端不受用戶影響,從而保持長久運行。

在代碼層面上,脫離Shell脫離終端是通過setsid()創建一個獨立的Session實現的,而進程組的首進程(pg leader)不允許創建新的Session自立山頭,只有進程組中的非首進程(比如進程組首進程的子進程)才能創建會話,從而脫離原會話。

而Shell命令行下運行的命令,總是會創建一個新的進程組併成爲leader進程,所以要讓該程序成爲長久運行的Daemon進程,只能創建一個新的子進程來創建新的session脫離當前的Shell。

另外,父進程立即退出的原因是可以立即將終端控制權交還給當前的Shell進程。但這不是必須的,比如可以讓子進程成爲Daemon進程後,父進程繼續運行並佔用終端,只不過這種代碼不友好罷了。

換句話說,當用戶運行一個Daemon類程序時,總是會有一個瞬間消失的父進程

前面演示的幾個孤兒進程示例已經說明了這一點。爲了更接近實際環境,這裏再用nginx來論證這個現象。

默認配置下,nginx以daemon方式運行,所以nginx啓動時會有一個瞬間消失的父進程。

$ ps -o pid,ppid,comm; nginx; ps -o pid,ppid,comm $(pgrep nginx)
   PID   PPID COMMAND
 34126  34124 bash
 34194  34126 ps
   PID   PPID COMMAND
 34196      1 nginx
 34197  34196 nginx
 34198  34196 nginx
 34200  34196 nginx
 34201  34196 nginx

第一個ps命令查看到當前分配到的PID值爲34194,下一個進程的PID應該分配爲34195,但是第二個ps查看到nginx的main進程PID爲34196,中間消失的就是nginx main進程的父進程。

可以修改配置文件使得nginx以非daemon方式運行,即在前臺運行,這樣nginx將佔用終端,且沒有中間的父進程,佔用終端的進程就是main進程。

$ ps -o pid,ppid,comm; nginx -g 'daemon off;' &
   PID   PPID COMMAND
 34126  34124 bash
 34439  34126 ps     #--> ps PID=34439
[1] 34440            #--> NGINX PID=34440

[~]->$ ps -o pid,ppid,comm $(pgrep nginx)
   PID   PPID COMMAND
 34440  34126 nginx
 34445  34440 nginx
 34446  34440 nginx
 34447  34440 nginx
 34448  34440 nginx

最後,需要區分後臺進程和Daemon類進程,它們都在後臺運行。但普通的後臺進程仍然受shell進程的監督和管理,用戶可以將其從後臺調度到前臺運行,即讓其再次獲得終端控制權。而Daemon類進程脫離了終端、脫離了Shell,它們不再受Shell的監督和管理,而是接受pid=1的systemd進程的管理。

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