Linux:請允許我靜靜地后臺運行
前言
常在 linux 下玩耍的開發(fā)者肯定會經(jīng)常遇到需要對進程調度的情況,在 windows 中點擊 最小化 去干別的就 OK 了,那么在 linux 下怎么辦呢。
可能有的小伙伴會說,再開一個終端窗口不就好了么。可是開很多窗口管理會很不方便,還有萬一手賤點了x,或者長時間不操作,遠程終端斷開了連接,進程停止了,再次打開,又是一番折騰。
今天來介紹幾個命令,幫大家系統(tǒng)地梳理一下 linux 的進程調度,并附上一些自己的使用心得和踩過的坑。
名詞
在此之前,我們必須(當然也不是必須,但了解原理有利于理解和解決錯誤)先弄懂幾個名詞。
進程組
進程組是一個或多個進程的集合,進程組方便了對多個進程的控制,在進程數(shù)較多的情況下,向進程組發(fā)送信號就行了。
它的 ID 由它的組長進程的進程 ID 決定。組長進程創(chuàng)建了進程組,但它并不能決定進程組的存活時間,只要進程組內還有一個進程存在,進程就存在,與組長進程是否已終止無關。
會話
會話(session)是一個或多個進程組的集合,它開始于用戶登陸終端,結束于用戶退出登陸。其義如其名,就是指用戶與系統(tǒng)的一次對話的全程。
會話包括控制進程(與終端建立連接的領頭進程),一個前臺進程組和任意后臺進程組。一個會話只能有一個控制終端,通常是登錄到其上的終端設備或偽終端設備,產(chǎn)生在控制終端上的輸入和信號將發(fā)送給會話的前臺進程組中的所有進程。
控制終端
每當我們使用終端工具打開一個本地或遠程 shell,我們便打開了一個控制終端,通過 ps 命令可以查看到 command 為 ttyn 的就是它對應的進程了,同時它對應 linux /dev/ 目錄下的一個文件。
作業(yè)
作業(yè)的概念與進程組類似,同樣由一個或多個進程組成,它分為前臺作業(yè)和后臺作業(yè),一個會話會有一個前臺作業(yè)和多個后臺作業(yè),與進程組不同的是,作業(yè)內的某個進程產(chǎn)生的子進程并不屬于這個作業(yè)。
類比
以上幾個概念可以類比為我們一次通過 QQ 聊天的全程,控制終端就是 QQ軟件,關閉了此軟件代表著聊天結束。聊天時發(fā)送的每一條信息都是一個進程,作業(yè)或進程組就是我們在聊的某一件事,它由很多條相互的信息構成。而會話則是我們指我們從開始聊天到結束聊天的全過程,可能會聊很多個事。
它們之間的相關圖如下所示:

后臺執(zhí)行
我們每次在終端窗口執(zhí)行命令的時候,進程總會一直占用著終端,走到進程結束,這段時間內,我們在終端的輸入是沒有用的。而且,當終端窗口關閉或網(wǎng)絡連接失敗后,再次打開終端,會發(fā)現(xiàn)進程已經(jīng)中斷了。這是因為用戶注銷或者網(wǎng)絡斷開時,SIGHUP信號會被發(fā)送到會話所屬的子進程,而此 SIGHUP 的默認處理方式是終止收到該信號的進程。所以若程序中沒有捕捉該信號,當終端關閉后,會話所屬進程就會退出。
我們要實現(xiàn)后臺執(zhí)行的目的,實際上是要完成如下兩個目標:
- 使進程讓出前臺終端,讓我們可以繼續(xù)通過終端與系統(tǒng)進行交互。
- 使進程不再受終端關閉的影響,即系統(tǒng)在終端關閉后不再向進程發(fā)送 SIGHUP 信號或即使發(fā)送了信號程序也不會退出。
以下的命令就圍繞著這兩個目標來實現(xiàn)。
&
首先是我們最經(jīng)常遇到的符號 &,將它附在命令后面可以使進程在后臺執(zhí)行,不會占用前臺界面。它實際上是在會話中開啟了一個后臺作業(yè),對作業(yè)的操作我們后面再說。
但我們會發(fā)現(xiàn),如果此時終端被關閉后,進程還是會退出。這是因為,& 符號只有讓進程讓出前臺終端的功能,無法讓進程不受 SIGHUP 信號的影響。
nohup
nohup 應該是另外一個我們常用的命令了,它的作用如其字面意思,使進程不受 SIGHUP 信號的影響。但我們在使用 nohup php test.php 后會發(fā)現(xiàn),進程還會一直占用前臺終端,但即使終端被關閉或連接斷開了,程序還是會執(zhí)行,另外我們會發(fā)現(xiàn)在當前文件夾下多了個名為 nohup.out 的文件。
這是因為 nohup 的功能僅僅是讓進程不受 SIGHUP 信號的影響,并不會讓出前臺終端,而且它還會在命令執(zhí)行目錄下建立 nohup.out 用以存儲進程的輸出。如果進程不需要輸出,且不想讓 nohup 創(chuàng)建文件,可以將標準輸出和標準錯誤輸出重定向。
我們常將 nohup 和 & 搭配到一塊使用,執(zhí)行命令如下 nohup command >/dev/null 2>&1 & 這樣,就可以放心的等待進程運行結果了。
setsid
setsid 是另一個讓進程在后臺執(zhí)行的命令,它的作用是讓進程打開一個新的會話并運行進程,使用方式為 setsid command。
根據(jù)上面的概念我們得知終端關閉后進程退出是因為會話首進程向進程發(fā)送了 SIGHUP 信號,setsid 就厲害了,它直接打開一個新的會話來執(zhí)行命令,那么原會話的終端的狀態(tài)就再也不會影響到此進程了。
我們使用 pstree 來查看使用 setsid 和 nohup … & 兩種命令來運行進程時的進程樹狀態(tài)。
- nohup php test.php &
我是用 ssh 遠程登陸的機器,所以 test.php 進程是掛在 sshd 進程下的。正常情況下,一旦 sshd 進程結束,則 test.php也無法幸免。
- setsid php test.php
使用了 setsid 后,test.php 進程已經(jīng)與 sshd 進程同級,屬于 init 進程的子進程了。
但是 setsid 并沒有為進程分配一個輸出終端,所以進程還是會輸出到當前終端上。
setsid 的 坑
另外,setsid 有個略坑的地方: 在終端中直接使用 setsid command 運行進程時,終端前臺并不會被影響,command 會在后臺默默運行。而在 shell 腳本中,我們會發(fā)現(xiàn)運行 setsid 的進程會一直阻塞住,直到 command 進程執(zhí)行結束。
這是因為,setsid 在其是進程組長時會 fork() 一個進程,但它不會 wait() 它的子進程,而是立刻退出,所以在終端內直接使用 setsid 時,setsid 作為進程組長不會占用終端界面。
而在 shell 腳本內,setsid 不是進程組長,它不會 fork() 子進程,而是由 bash 來fork()一個子進程,而 bash 會 wait() 子進程,所以表現(xiàn)得像 setsid 在 wait() 子進程一樣。
要解決這個問題,有兩個辦法:
- 使用上面介紹的 &符號,使 setsid 強行到后臺執(zhí)行。
- 使用 . 或 source 命令由終端執(zhí)行 setsid;
其他
除了上面介紹的命令,還有 screen 和 tmux 等會話工具,他們都有自己的一套規(guī)范,也比較復雜,掌握本文的命令已經(jīng)足夠你馳騁 linux 進程控制了。當然有想了解新知識的可以查詢學習一下,應該會比基礎命令好用。
作業(yè)命令
使用上面的后臺執(zhí)行命令時可能還會遇到一些小狀況:
- 被我們放在后臺的進程執(zhí)行時間過長,而我們又忘記使用 nohup 命令,那么終端一旦斷開,進程又需要被重新執(zhí)行。
- 我們直接開啟了某個進程,又想在不中斷進程的情況下讓它讓出前臺終端;
這些都要牽涉到今天的第二個模塊–作業(yè);
我們在終端里運行的命令都可以理解為一個作業(yè),有的占用前臺終端,有的在后臺默默執(zhí)行,下面的命令就是為了調度這些作業(yè)。
jobs
jobs 是作業(yè)的基礎命令,用它可以查看正在運行的作業(yè)的信息,其輸出如下:
jobs
1- Running php test.php &
2+ Stopped php test.php
前面[ ]內的數(shù)字是作業(yè) ID,也是后面我們要操作作業(yè)的標識,然后是作業(yè)狀態(tài)和命令。
ctrl+z
ctrl+z 嚴格來說并是作業(yè)命令,它只是向當前進程發(fā)送一個 SIGSTOP 信號,促使進程進入暫停(stopped)狀態(tài),此狀態(tài)下,進程狀態(tài)會被系統(tǒng)保存,此進程會被放置到作業(yè)隊列中去,而讓出進程終端。
使用它,我們可以暫停正在占用終端的進程而不停止它,從而讓我們使用終端命令來操作此進程。
bg
bg是 backgroud 的縮寫,顧名思義,bg %id 把作業(yè)放到后臺進程中執(zhí)行。
結合 ctrl+z 和 bg 命令,我們可以解決上面提出的***個問題,不停止地將正在占用終端的進程放到后臺執(zhí)行。
fg
fg 與 bg 相對,使用它可以把作業(yè)放到前臺來執(zhí)行。
disown
disown 用來將作業(yè)從作業(yè)列表中移除,即使它 不屬于 會話,這樣終端關閉后不再向此作業(yè)發(fā)送 SIGHUP 信號,以阻止終端對進程的影響。
使用 disown 我們可以解決上面提出的第二個問題,不重新執(zhí)行將一個沒使用 nohup 命令的進程不受終端關閉影響。
守護進程
以上介紹的都是一些臨時進程的處理,后臺運行的進程的最終方法是將進程變成守護進程。
守護進程
守護進程(daemon)是生存期較長的一種進程,一般在系統(tǒng)啟動時啟動,系統(tǒng)關閉時停止,沒有控制終端,也不會輸出。如我們的服務器、fpm 等進程就是以守護進程的形式存在的。
創(chuàng)建過程
要創(chuàng)建一個守護進程,步驟為:
必選項
- fork 子進程,退出父進程,子進程作為孤兒進程被 init 進程收養(yǎng);
- 使用 setsid, 打開新會話,進程成為會話組長,正式脫離終端控制;
- 設置信號處理(特別是子進程退出處理);可選項:
- 使用 chdir 改變進程工作目錄,一般到根目錄下,防止占用可卸載文件系統(tǒng);
- 用 umask 重設文件權限掩碼,不再繼承父進程的文件權限設置;
- 關閉父進程打開的文件描述符;
代碼
以下是 php 創(chuàng)建守護進程的偽代碼:
- $pid = pcntl_fork();
- if ($pid > 0) {
- exit; // 父進程直接退出
- } elseif ($pid < 0) {
- throw_error(); // 進程創(chuàng)建失敗
- }
- posix_setsid(); // setsid成為會話領導進程
- chdir($dir); // 切換目錄
- umask(0); // 重置文件權限mask
- close_fd(); // 關閉父進程的文件描述符
- pcntl_signal($signal, $func); // 注冊信號處理函數(shù)
- while (true) {
- do_job(); // 處理進程任務
- pcntl_signal_dispatch(); // 分發(fā)信號處理
- }
總結
linux 是開發(fā)者的基礎技能,而進程的調度更是我們常用的功能,希望讀完本文的同學們能有所收獲。
又有大半個月沒發(fā)博客了,最近鼓搗著重構代碼,經(jīng)常會在一個點上糾結半天,不知不覺就加了個班。而且這個是個沒法精確度量工作量和目標的活兒,優(yōu)化沒有盡頭嘛。不過由于要更多地考慮一下代碼的抽象、效率和擴展,對自己也是個挑戰(zhàn),算是樂在其中吧~
最近可能會考慮寫一個守護進程和 cron 進程調度器,嗯,希望給我算到工作量里,哈哈~想寫的太多了,只怨自己還不夠強大。。。