1. 程式人生 > 其它 >vc sleep不佔程序_前後臺程序、孤兒程序和daemon類程序的父子關係

vc sleep不佔程序_前後臺程序、孤兒程序和daemon類程序的父子關係

技術標籤:vc sleep不佔程序

前後臺程序父子關係和daemon類程序

為什麼要花篇幅解釋程序父子關係?因為理解它們對於後面理解Systemd Service的型別很重要。

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

前臺程序是佔用當前終端的程序,只有該程序執行完成或被終止之後,才會釋放終端並將終端交還給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),這兩種方式有區別:

  • system('sleep 30')會呼叫sh,並將sleep 30作為sh -c的引數來執行,等價於sh -c 'sleep 30',所以sleep程序是sh程序的子程序,而sh程序是Perl程序的子程序
    • 因為呼叫了sh,所以允許shell命令列的解析,比如使用重定向、管道符號等
    • 如果呼叫的命令是簡單命令,沒有涉及任何需要Shell解析的符號和邏輯,將優化為直接呼叫命令,而不通過sh來呼叫
  • system('sleep', 30)是直接呼叫sleep程式,sleep程序是當前perl程序的子程序,因為沒有呼叫中間程式sh,所以不支援Shell命令列的解析,即不能使用類似重定向、管道符號等特殊符號

舉幾個例子幫助理解,假設有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)

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

  • 在程序A中通過system('cmd1',arg1,arg2...)system('cmd1')的方式執行一個程序B,程序B將是程序A的子程序
    • 在Shell環境中直接執行命令,或者子Shell中執行單條簡單命令(cmd1),也屬於這種情況
  • 在程序A中通過system('cmd_with_shell')的方式執行一個程序B,程序B的父程序是sh,sh的父程序是程序A
    • 在子Shell中執行非簡單單條命令(即需要Shell解析的參與)時,也屬於這種情況

孤兒程序和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類程式時,總是會有一個瞬間消失的父程序。這一點對於後面理解Systemd Service的型別非常關鍵,甚至前面介紹的一大堆關於程序的篇幅都是為這一句話做鋪墊。

前面演示的幾個孤兒程序示例已經說明了這一點。為了更接近實際環境,這裡再用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程序的管理。

a56b9ebdf5fc5fce35c1073c98f05e2a.png