天天看點

前背景程序、孤兒程序和daemon類程序的父子關系前背景程序、孤兒程序和daemon類程序的父子關系

回到Linux基礎系列文章大綱

回到Systemd系列文章大綱

回到Shell系列文章大綱

前背景程序、孤兒程序和daemon類程序的父子關系

前台程序、背景程序和程序父子關系

前台程序是占用目前終端的程序,隻有該程序執行完成或被終止之後,才會釋放終端并将終端交還給shell程序。

例如:

bash

1
      
$ sleep 30
      

執行該指令後,将建立sleep程序,sleep程序是目前bash程序(假如目前的shell為bash)的子程序:

bash

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

在30秒内,sleep程序将占用終端,是以此時的sleep稱為前台程序。當睡眠30秒之後,前台程序sleep退出,終端控制權交還給目前shell程序,shell程序可繼續向下運作指令或等待使用者輸入新指令。

如果給指令加上一個

&

符号,該指令将在背景運作。

bash

1
      
$ sleep 30 &
      

此時,sleep仍然是目前bash的子程序,但是它不會占用終端,而是在背景默默地運作,并且在30秒之後默默的退出。

如果是在一個子Shell環境中運作一個前台程序呢?例如:

bash

1
      
$ ( sleep 30 )
      

執行這個指令時,小括号會開啟一個子Shell環境,這相當于目前的bash程序隔離了一個bash運作時環境。sleep程序将在這個新的子Shell環境中運作,sleep仍然是目前bash的子程序。由于它會占用目前的終端,是以它是前台程序。

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

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

例如

bash -c 'sleep 30'

,sleep程序将在該bash程序提供的環境下運作,它是該bash程序的子程序。

再例如shell腳本:

bash

1
2
3
      
#!/bin/bash

sleep 30
      

sleep将在這個bash腳本程序提供的環境下運作,它是該腳本程序的子程序。

再例如Perl腳本:

perl

1
2
3
      
#!/bin/perl

system('sleep 30')
      

sleep将在這個Perl腳本程序提供的環境下運作。

需注意,程式設計語言(如Perl)可能提供多種調用外部程式的方式,比如

system('sleep 30')

system('sleep',30)

,這兩種方式有差別:

前背景程式、孤兒程式和daemon類程式的父子關系前背景程式、孤兒程式和daemon類程式的父子關系

舉幾個例子幫助了解,假設有Perl腳本a.pl,其内三行内容為:

perl

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

對于(1),指令和參數分開,perl将直接調用sleep程式,這時的sleep程序是perl程序a.pl的子程序,且不支援使用管道

|

、重定向

> < >>

&&

等等屬于Shell支援的符号。

bash

1
2
      
$ 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程序的子程序。

bash

1
2
      
$ 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程序的子程序:

bash

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

其實子shell中運作指令和system()運作指令的行為是類似的:

bash

1
2
3
4
5
6
      
# sleep程序是目前shell程序的子程序
$ (sleep 30)

# 目前shell程序會建立一個子bash程序
# sleep程序和echo程序是該子bash程序的子程序
$ (sleep 30 ; echo hhh)
      

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

前背景程式、孤兒程式和daemon類程式的父子關系前背景程式、孤兒程式和daemon類程式的父子關系

孤兒程序和Daemon類程序

如果在程序B退出前,父程序先退出了呢?這時程序B将成為孤兒程序,因為它的父程序已經死了。

孤兒程序會被PID=1的systemd程序收養,是以程序B的父程序PPID會從原來的程序A變為PID=1的systemd程序。

注意,孤兒程序會繼續保持運作,而不會随父程序退出而終止,隻不過其父程序發生了改變。

例如,在子Shell中運作背景指令:

bash

1
      
$ (sleep 30 &)
      

因為背景符号

&

是屬于Shell的,是以涉及到shell的解析過程,是以目前bash程序會建立一個子bash程序來解析指令并提供sleep程序的運作環境。

sleep程序将在這個子bash程序環境中運作,但因為它是一個背景指令,是以sleep程序建立成功之後立即傳回,由于小括号内已經沒有其它指令,子bash程序會立即終止。這意味着sleep将成為孤兒程序:

bash

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

再比如,Shell腳本内部運作一個背景指令,并且讓Shell腳本在背景指令退出前先退出。

bash

1
2
3
4
      
#!/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啟動時會有一個瞬間消失的父程序。

bash

1
2
3
4
5
6
7
8
9
10
      
$ 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程序。

bash

1
2
3
4
5
6
7
8
9
10
11
12
13
      
$ 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程序的管理。

繼續閱讀