本文解釋bash內置命令的特殊性、前臺、後臺任務的"本質",以及前、後臺任務和bash進程、終端的關係。網上沒相似的資料,因此都是本身的感悟和總結,若有錯誤,120分的期待盼請指正。html
由於要詳細分析每個涉及到的內容,我用了不少示例,因此結論比較分散。所以在文章的結尾,我將這些結論大概作了個總結。shell
首先經過一個示例作個引子。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進程。爲何會如此?
當登錄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進程被殺或者已經結束)?這個問題並不像想象中的那麼簡單。下面會很是詳細地結合後臺任務來分析它。
後臺任務,是專業術語"做業"的一種。做業是指"能選擇性地中止、暫停、繼續運行某個進程的能力",通俗地說就是做業用來控制誰能夠得到終端(前臺進程)、誰不能得到終端(後臺進程)。這和咱們理解的"放進看不見的後臺默默地執行"好像有點區別啊?這可有可無。
關於後臺任務,首先要說明的是後臺任務是怎麼實現的:經過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進程、前臺進程、後臺進程的關係大概理解爲下圖形式:
回頭看本文開頭的引子,爲何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進程。
因此,完善一下上面的圖:
仍是圍繞普通命令的後臺和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進程,若是把它們的爺爺殺掉呢?或者直接把終端關掉呢?這時又不同了。
殺掉終端進程和殺掉當前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下。
前面驗證過殺掉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腳本自殺。
(sleep 30 &)
),或者用disown命令將後臺任務移除後臺,或disown -h
設置後臺進程忽略SIGHUP信號。#!/bin/bash while true;do sleep 3 echo "hello world! hello world! " done & sleep 60
最後,本文的姊妹篇: