bash內置命令的特殊性,後臺任務的"本質"

本文解釋bash內置命令的特殊性、前臺、後臺任務的"本質",以及前、後臺任務和bash進程、終端的關係。網上沒相似的資料,因此都是本身的感悟和總結,若有錯誤,120分的期待盼請指正。html

由於要詳細分析每個涉及到的內容,我用了不少示例,因此結論比較分散。所以在文章的結尾,我將這些結論大概作了個總結。shell

1.引子:一個示例

首先經過一個示例作個引子。bash

當直接在當前bash環境下執行一個普通命令,這個普通命令的進程會掛在當前bash進程之下(即父進程爲當前bash進程)。ssh

例如:工具

# 在窗口1執行:
[root@xuexi ~]# sleep 30

# 在窗口2查看sleep進程信息:
[root@xuexi ~]# pstree -p | grep slee[p]
           |-sshd(1145)---sshd(5230)-+-bash(5232)---sleep(5599)

若是,在當前bash環境下將普通命令放入後臺執行,這個命令的進程仍是會掛在當前bash進程下。ui

若是是在當前bash環境下執行一個內置命令呢?由於是內置命令,它不會有本身的進程(緣由後文解釋)。this

# 窗口1查詢當前bash進程信息
[root@xuexi ~]# pstree -p | grep bas[h]
           |-sshd(1145)---sshd(5230)-+-bash(5232)---sleep(5642)
           |                         `-bash(5557)-+-grep(5644)

# 窗口2執行bash內置命令if
[root@xuexi ~]# if true;then sleep 30;fi

# 回到窗口1查詢當前bash進程信息,和前面是同樣的
[root@xuexi ~]# pstree -p | grep bas[h]
           |-sshd(1145)---sshd(5230)-+-bash(5232)---sleep(5642)
           |                         `-bash(5557)-+-grep(5644)

發現bash進程沒有任何變化。code

再若是一次,將bash內置命令放入後臺執行呢?htm

例如:blog

# 當前bash進程信息
[root@xuexi ~]# pstree -p | grep bas[h]
           |-sshd(1145)---sshd(5230)-+-bash(5232)
           |                         `-bash(5557)-+-grep(5634)

[root@xuexi ~]# if true;then sleep 30;fi &
[1] 5635

# 再次查看bash進程信息
[root@xuexi ~]# pstree -p | grep bas[h]
           |-sshd(1145)---sshd(5230)-+-bash(5232)---bash(5635)---sleep(5636)
           |                         `-bash(5557)-+-grep(5638)

結果發現,多了一個bash進程。爲何會如此?

2.bash進程和bash內置命令

當登錄Linux系統時,會爲用戶分配一個shell。若是在/etc/passwd中該用戶配置的shell爲 /bin/bash ,那麼就爲用戶分配一個bash shell。

[root@xuexi ~]# head -1 /etc/passwd
root:x:0:0:root:/root:/bin/bash

當登錄用戶的身份審覈經過後,就會加載bash進程,bash進程再加載它的各個配置文件(/etc/profile、/etc/profile.d/*.sh、~/.bashrc等),從而配置好bash的執行環境。注意"執行環境"這個詞,它將貫穿本文。

再來講bash內置命令。

bash內置命令和普通的命令都能在bash環境下執行,並實現它們對應的功能。但它們卻有很大區別,最典型的一個區別是ps等工具能捕捉到普通命令的進程,卻捕捉不到bash內置命令的進程。

那麼哪些是bash內置命令呢?只要查它的man文檔時,給出bash文檔的都是bash內置命令,如cd、declare、read等。但不表明man時不是bash手冊的就不是內置命令,例如kill、echo、pwd、test等。

如下是內置命令列表。

bash, :, ., [, alias, bg, bind, break, builtin, caller, cd, command, compgen, complete, compopt, continue, declare, dirs, disown, echo, enable, eval, exec, exit, export, false, fc, fg, getopts, hash, help, history, jobs, kill, let, local, logout, mapfile, popd, printf, pushd, pwd, read, readonly, return, set, shift, shopt, source, suspend, test, times, trap, true, type, typeset, ulimit, umask, unalias, unset, wait

除此以外,還有一些保留關鍵字也是bash內置命令。包括:

! case do done elif else esac fi for function if in select then until while { } time [[ ]]

那麼bash內置命令和bash進程有什麼關係?

bash內置命令和普通命令不同。普通命令能夠直接執行,不依賴於某種執行環境。例如,sleep命令,能夠直接以pid=1的init/systemd爲父進程而執行。那些daemon類的服務進程更是如此,它們不依賴於終端,也不依賴於執行環境,只要給它們配置好,就能夠直接找init/systemd當爹。

而bash內置命令,既然稱之爲"bash內置命令",顧名思義是bash內置的。當咱們在當前bash環境下執行bash內置命令,通過shell的一輪解析以後,發現這是個bash內置命令,因而直接在當前bash進程的內部調用執行它們。因此bash內置命令自身是沒有進程的。

換句話說,bash內置命令的執行是由它們的bash爹帶着它們執行的。這個bash爹是一個負責任的好爹,什麼都幫它們準備好,還帶着它們一塊兒浪。但正由於爹太負責,把孩子們給寵壞了,這些bash內置命令不管何時執行都必須先找好bash爹爲它們提供執行環境。

因而問題出現了,若是它們的bash爹死了怎麼辦(即bash進程被殺或者已經結束)?這個問題並不像想象中的那麼簡單。下面會很是詳細地結合後臺任務來分析它。

3.前臺任務和後臺任務的本質

後臺任務,是專業術語"做業"的一種。做業是指"能選擇性地中止、暫停、繼續運行某個進程的能力",通俗地說就是做業用來控制誰能夠得到終端(前臺進程)、誰不能得到終端(後臺進程)。這和咱們理解的"放進看不見的後臺默默地執行"好像有點區別啊?這可有可無。

關於後臺任務,首先要說明的是後臺任務是怎麼實現的:經過bash和系統終端的驅動共同提供的交互式界面來實現後臺做業能力。在bash手冊中是如此解釋的:

A user typically employs this facility via an interactive interface supplied jointly by the operating system kernel’s terminal driver and Bash.

其實,當一個進程的進程組號和當前終端的進程組號相同,則這個進程是前臺進程,受鍵盤影響,能夠讀、寫終端。當一個進程的進程組號和當前終端的進程組號不一樣時,則這個進程是後臺進程,它們不受鍵盤影響,讀、寫終端時須要發送特定的信號。

換句話說,後臺任務依賴於當前所在的終端,由於它可能會恢復到前臺去運行。但當前終端每每會和一個bash進程關聯綁定,該bash進程具備當前終端的控制權,因此殺掉終端進程和殺掉當前所在bash進程都能結束一個終端,可是它們卻有本質上的區別,後文我用了軍營、將軍來比喻當前終端、當前bash進程的關係,這就是它們的區別,詳細內容見後文一步一步的分析。

當一個進程是前臺進程時,它是在當前bash進程下執行的,此時該bash進程失去控制權,也就是被阻塞。當進程進入後臺,意味着它會離開當前bash環境,進入後臺執行環境,由於只有離開當前bash環境,才能馬上將終端的控制權還給當前bash進程

因而,能夠將當前終端進程、當前bash進程、前臺進程、後臺進程的關係大概理解爲下圖形式:

3.1 普通命令和bash內置命令放入後臺的區別

回頭看本文開頭的引子,爲何sleep 30 &的sleep進程是在當前bash進程下的,而if true;then sleep 30;fi &則會新開一個bash進程?

上一小節中說過:bash內置命令的執行依賴於bash進程提供的執行環境,而普通命令則沒有依賴性。

sleep 30 &的sleep是普通命令,不依賴於bash進程,因此它能夠直接進入後臺,但它畢竟是後臺任務,它暫時還依賴於當前終端,且受當前bash進程的控制(例如能放回前臺,能被bash查看後臺任務信息),因此它暫時還必須掛在當前bash進程下。之因此是暫時,稍後就解釋。

[root@xuexi ~]# sleep 30 &
[1] 6300

[root@xuexi ~]# pstree -p | grep bas[h]
           |-sshd(1145)-+-sshd(5230)---bash(5557)-+-grep(6302)
           |            `-sshd(6047)---bash(6049)---sleep(6300)

if true;then sleep 30;fi &是bash內置命令要放入後臺,放入後臺意味着它要離開當前bash環境,因此它在進入後臺開始執行前,必須新找一個bash爹爲它提供執行環境,因此它新生成了一個bash進程。

# 當前bash進程信息
[root@xuexi ~]# pstree -p | grep bas[h]
           |-sshd(1145)---sshd(5230)-+-bash(5232)
           |                         `-bash(5557)-+-grep(5634)

[root@xuexi ~]# if true;then sleep 30;fi &
[1] 5635

# 再次查看bash進程信息
[root@xuexi ~]# pstree -p | grep bas[h]
           |-sshd(1145)---sshd(5230)-+-bash(5232)---bash(5635)---sleep(5636)
           |                         `-bash(5557)-+-grep(5638)

補充一個小知識:其實這個新的bash爹和當前bash進程是不同的,這個新bash爹是非交互式的shell,能夠直接使用kill -15殺掉這個新bash進程。而交互式shell下,在沒有設置任何陷阱(trap)時,默認是忽略TERM信號的,沒法直接kill -15殺掉一個當前活動的bash進程。

因此,完善一下上面的圖:

3.2 殺掉後臺任務的父進程

仍是圍繞普通命令的後臺和bash內置命令的後臺任務來講明。

上一小節在解釋普通命令放入後臺執行時,後臺進程會掛在當前bash進程下,還特意加上了"暫時"兩個字。其實,普通命令的進程進入後臺時,它不是必定要掛在當前bash進程下的,甚至它再也不依賴於終端,之因此還暫時掛在當前bash進程下,是由於它仍是個後臺任務,還須要被當前bash管理。例如將其放回前臺,查看後臺任務列表,若是不在當前bash進程下,當前bash進程必然沒法管理它。

若是將當前bash進程或者當前終端進程殺掉,對普通命令的後臺任務會形成什麼影響?試試看。

# 在窗口1執行:
[root@xuexi ~]# sleep 67 & 
[1] 6464

# 在窗口2查看sleep進程的父進程
[root@xuexi ~]# pstree -p | grep sleep
           |            `-sshd(6047)---bash(6049)---sleep(6464)

# 殺掉父進程:bash進程。由於是交互式shell,因此必須使用SIGKILL信號
[root@xuexi ~]# kill -9 6049

# 再查看sleep進程
[root@xuexi ~]# pstree -p | grep sleep
           |-sleep(6464)

從結果中不難發現,殺掉後臺sleep進程的父進程bash(由於和終端綁定,因此也是殺掉終端)後,sleep進程沒有隨之停止,而是掛在init/systemd下。前面分析過,它是暫時掛在bash進程下的,它不依賴於bash進程,也不依賴於終端。

再來分析bash內置命令放入後臺時,殺掉它的父進程會如何。

if true;then sleep 55;fi &爲例,這裏還要再細緻一點地分析它的父進程。

[root@xuexi ~]# if true;then sleep 55;fi &
[1] 6520
[root@xuexi ~]# pstree -p | grep sleep
           |         `-sshd(6476)---bash(6478)-+-bash(6520)---sleep(6521)

這裏的sleep有兩個父bash進程,其中pid=6520的是新生成的bash爹,pid=6478的是當前bash進程。是否注意到上面if放入後臺時返回的進程號爲6520,這個進程號對應的是新bash爹。換句話說,pid=6520的bash進程對應的是if命令,而這纔是後臺進程,但由於sleep在if所在bash進程的進程組內,因此sleep也是後臺進程。

因此,當殺掉pid=6520的bash進程後,表示殺掉的是普通命令sleep後臺的父進程,也就是if結構,因此sleep進程會直接掛在init/systemd下;當殺掉pid=6478的bash進程後,表示殺掉內置命令if(對應的是pid=6520的bash進程)的父進程,因此if命令的bash爹將帶着整個進程組掛在init/systemd下。

分別驗證它們。

# 殺掉新生成的bash爹
[root@xuexi ~]# if true;then sleep 55;fi &
[1] 6551

[root@xuexi ~]# pstree -p | grep sleep     
           |        `-sshd(6476)---bash(6478)-+-bash(6551)---sleep(6552)

[root@xuexi ~]# kill 6551

# 查看sleep進程,發現確實已經掛在Init/systemd下了
[root@xuexi ~]# pstree -p | grep sleep
           |-sleep(6552)
# 殺掉新生成的bash爹的父bash進程,也就是左邊那個bash進程

# 在窗口1執行
[root@xuexi ~]# if true;then sleep 55;fi &
[1] 6563

# 在窗口2執行
[root@xuexi ~]# pstree -p | grep sleep    
           |            `-sshd(6476)---bash(6478)-+-bash(6563)---sleep(6564)
[root@xuexi ~]# kill -9 6478

# 窗口2查看sleep進程,發現bash爹帶着整個進程組都掛在init/systemd下
[root@xuexi ~]# pstree -p | grep sleep
           |-bash(6563)---sleep(6564)

若是此時把pid=6563的bash爹殺了,會如何呢?若是前面都理解了的話,這裏很容易知道答案。這個進程組是個後臺進程組,包括其中的sleep進程,把sleep的父進程殺掉,sleep固然是直接掛在init/systemd下。

過程參考下圖:

還沒完呢。上面殺的都是它們的直系爹bash進程,若是把它們的爺爺殺掉呢?或者直接把終端關掉呢?這時又不同了。

3.3 殺掉後臺任務所在的終端

殺掉終端進程和殺掉當前bash進程對後臺任務的影響不同:殺掉當前bash進程後,後臺任務會掛在init/systemd下,而殺掉終端後,後臺任務也會停止。

來一個示例。

# 窗口1執行
[root@xuexi ~]# sleep 65 &
[1] 7108

# 窗口2查看
[root@xuexi ~]# pstree -p | grep sleep
           |            `-sshd(7014)---bash(7016)---sleep(7108)

# 窗口2殺掉pid=7014,或者直接關掉窗口1
[root@xuexi ~]# kill 7014

# 窗口2再查看sleep進程信息,啥也沒有
[root@xuexi ~]# pstree -p | grep sleep

前面說過,後臺任務依賴於當前所在的終端。但當前終端每每會和一個bash進程關聯綁定,該bash進程具備當前終端的控制權,因此殺掉終端進程和殺掉當前所在bash進程都能結束一個終端,可是它們卻又本質上的區別。

其實,能夠將當前終端、當前bash進程(更確切地說是後臺任務的父進程)、後臺的關係看做軍營、將軍、小兵的關係。當軍營中分配了一個將軍後,軍營爲將軍和小兵提供休息、商量等環境,將軍具備軍營的控制權,負責管理軍營中的一切,包括小兵。若是殺掉軍營中的將軍,小兵們發現"營中無大王",因而馬上收拾行李就走,投奔皇帝去了(init/systemd)。但若是直接把軍營給炸了,那麼將軍和小兵將無一倖免,全軍覆沒。

實際上,殺掉終端進程時,終端進程會給本身進程組內的全部進程包括bash進程發送一個SIGHUP信號,正是由於收到這個信號,進程組內的全部進程纔會停止。

如何讓後臺進程不依賴於終端?不考慮藉助nohup、screen、tmux等第三方工具實現,bash其實也提供了多種解決方案,介紹其中兩種方法:

1.將後臺任務放入子shell。

這算是對bash深入認識後才能想到或真正理解的方法。由於將後臺任務放入子shell(子shell下一節說明),當子shell結束後,其內後臺任務會當即掛到init/systemd下,這樣就脫離了終端。

[root@xuexi ~]# (sleep 30 &)
[root@xuexi ~]# pstree -p | grep sleep
           |-sleep(2392)

或者,放進一個腳本(執行腳本也是進入一個子shell)。例如如下是a.sh的內容。

#!/bin/bash

sleep 60 &

sleep 20

在執行該腳本的20秒內,兩個sleep進程都是在a.sh進程下的。

[root@xuexi ~]# pstree -p | grep sleep
           |---sshd(2317)-+-bash(2348)---a.sh(2449)-+-sleep(2450)
           |              |                         `-sleep(2451)

20秒後,腳本結束,也就是子shell退出,該子shell中的後臺sleep將掛在init/systemd下。

[root@xuexi ~]# pstree -p | grep sleep
           |-sleep(2450)

若是把腳本中的sleep 20去掉,那麼後臺sleep也將是瞬間就掛到init/systemd下的。

2.利用bash內置命令disown將任務移出後臺或設置爲忽略SIGHUP信號。

# 在窗口1執行
[root@xuexi ~]# sleep 60 &   # jobs的做業號碼:%1
[root@xuexi ~]# disown %1    # 將後臺做業%1移出後臺
[root@xuexi ~]# jobs         # 返回空

當進程disown移出後臺後,雖然暫時還掛在bash進程下,但結束終端進程時,該進程將掛到init/systemd下。因此,這樣作也將脫離終端。

或者,disown -h設置後臺做業忽略SIGHUP信號。前文說過,當終端進程退出時,將會向終端進程組中的全部進程發送SIGHUP信號,收到這些信號,終端下的全部進程都會終止。

[root@xuexi ~]# sleep 60 &
[root@xuexi ~]# disown -h %1  # 爲%1後臺做業打上忽略SIGHUP的標記

而後關閉終端,會發現sleep進程也將掛在init/systemd下。

4."奇怪"的問題以及解決方案

前面驗證過殺掉if true;then sleep 55;fi &的bash爹的父進程(也就是sleep的爺爺進程)後,bash爹將帶着sleep一塊兒掛在init/systemd進程下。

可是考慮一個"極端"一點的問題。若是這裏不是if,而是while/for/until循環,若是這個命令不是在當前bash下執行,而是在一個腳本中執行,殺掉腳本進程後,會如何?

好比,某腳本test.sh內容以下:

#!/bin/bash

while true;do
    sleep 10
done &

或者以下:

#!/bin/bash

while true;do
    sleep 10
done &

sleep 50

執行這個腳本時,將有兩個test.sh進程,其中一個是test.sh進程自身(稱之爲進程A),一個是爲while提供bash環境的子shell進程(稱之爲進程B)。

當在腳本運行時,殺掉進程A時或者按下CTRL+C(第二個腳本狀況)時,若是查看進程的話,會發現後臺永遠有一個 "test.sh進程+sleep進程" 在運行,若是在while循環中有輸出語句的話(好比echo),那麼還會時不時地向終端輸出點東西。這不是咱們想要的結果,咱們想要的是腳本結束時,裏面的進程也一塊兒結束,而不是有個進程在後臺"偷偷地"運行。

之因此出現這樣的問題,是由於進程A終止後,while的bash爹(進程B)帶着while結構裏的進程sleep一塊兒掛到init/systemd下了,並且很不幸,這是個while循環,會一直不斷地在後臺運行。

應該怎樣解決這種問題?能夠將全部的test.sh進程都殺掉,好比使用killall sleep test.sh命令。還有更好的方法,見個人另外一篇文章,專門寫這個問題:如何讓shell腳本自殺

5.本文的總結

  1. 其實本文講的全是子shell的內容,儘管文中不多出現子shell的字眼。。
  2. bash內置命令的執行依賴於bash進程提供的執行環境。
  3. 當前bash環境下執行bash內置命令不會新生成bash進程,除非將它放進後臺。
  4. 殺掉後臺任務的父進程,後臺任務會掛到pid=1的init/systemd進程下。
  5. 終端進程、bash進程和後臺任務之間的關係:軍營、將軍、小兵。
    • (1).終端進程爲bash進程和其餘進程提供生存環境。
    • (2).終端進程每每會和一個bash進程綁定,這個bash進程具備終端的控制權,也就是管理軍營。
    • (3).殺掉終端的管理bash進程,終端進程也會隨之終止。
    • (4).bash進程是後臺任務的暫時管理者。當bash進程終止時,後臺任務就會掛到pid=1的進程下接受init/systemd的管理。
  6. 殺掉終端進程,會發送SIGHUP信號給終端進程組中的全部進程。收到SIGHUP信號後,這些進程都會終止,包括bash進程和後臺任務。
  7. 讓後臺任務脫離終端的方法,除了nohup、screen、tmux等三方工具,bash自身也能實現。只需將後臺任務放進子shell中執行便可(最簡單的方法(sleep 30 &)),或者用disown命令將後臺任務移除後臺,或disown -h設置後臺進程忽略SIGHUP信號。
  8. 分析下面的腳本,爲何執行過程當中按下 CTRL+C 後,還會時不時地向終端上輸出點東西,如何解決這個問題?
#!/bin/bash
while true;do
    sleep 3
    echo "hello world! hello world! "
done &
sleep 60

最後,本文的姊妹篇:

  1. 子shell以及何時進入子shell
  2. 如何讓shell腳本自殺
相關文章
相關標籤/搜索