前言
常在 linux 下玩耍的開發者肯定會經常遇到需要對程式排程的情況,在 windows 中點選 最小化
去幹別的就 OK 了,那麼在 linux 下怎麼辦呢。
可能有的小夥伴會說,再開一個終端視窗不就好了麼。可是開很多視窗管理會很不方便,還有萬一手賤點了x,或者長時間不操作,遠端終端斷開了連線,程式停止了,再次開啟,又是一番折騰。
今天來介紹幾個命令,幫大家系統地梳理一下 linux 的程式排程,並附上一些自己的使用心得和踩過的坑。
名詞
在此之前,我們必須(當然也不是必須,但瞭解原理有利於理解和解決錯誤)先弄懂幾個名詞。
程式組
程式組是一個或多個程式的集合,程式組方便了對多個程式的控制,在程式數較多的情況下,向程式組傳送訊號就行了。
它的 ID 由它的組長程式的程式 ID 決定。組長程式建立了程式組,但它並不能決定程式組的存活時間,只要程式組內還有一個程式存在,程式就存在,與組長程式是否已終止無關。
會話
會話(session)是一個或多個程式組的集合,它開始於使用者登陸終端,結束於使用者退出登陸。其義如其名,就是指使用者與系統的一次對話的全程。
會話包括控制程式(與終端建立連線的領頭程式),一個前臺程式組和任意後臺程式組。一個會話只能有一個控制終端,通常是登入到其上的終端裝置或偽終端裝置,產生在控制終端上的輸入和訊號將傳送給會話的前臺程式組中的所有程式。
控制終端
每當我們使用終端工具開啟一個本地或遠端 shell,我們便開啟了一個控制終端,通過 ps
命令可以檢視到 command 為 ttyn
的就是它對應的程式了,同時它對應 linux /dev/
目錄下的一個檔案。
作業
作業的概念與程式組類似,同樣由一個或多個程式組成,它分為前臺作業和後臺作業,一個會話會有一個前臺作業和多個後臺作業,與程式組不同的是,作業內的某個程式產生的子程式並不屬於這個作業。
類比
以上幾個概念可以類比為我們一次通過 QQ 聊天的全程,控制終端就是 QQ軟體,關閉了此軟體代表著聊天結束。聊天時傳送的每一條資訊都是一個程式,作業或程式組就是我們在聊的某一件事,它由很多條相互的資訊構成。而會話則是我們指我們從開始聊天到結束聊天的全過程,可能會聊很多個事。
它們之間的相關圖如下所示:
後臺執行
我們每次在終端視窗執行命令的時候,程式總會一直佔用著終端,走到程式結束,這段時間內,我們在終端的輸入是沒有用的。而且,當終端視窗關閉或網路連線失敗後,再次開啟終端,會發現程式已經中斷了。這是因為使用者登出或者網路斷開時,SIGHUP
訊號會被髮送到會話所屬的子程式,而此 SIGHUP
的預設處理方式是終止收到該訊號的程式。所以若程式中沒有捕捉該訊號,當終端關閉後,會話所屬程式就會退出。
我們要實現後臺執行的目的,實際上是要完成如下兩個目標:
- 使程式讓出前臺終端,讓我們可以繼續通過終端與系統進行互動。
- 使程式不再受終端關閉的影響,即系統在終端關閉後不再向程式傳送
SIGHUP
訊號或即使傳送了訊號程式也不會退出。
以下的命令就圍繞著這兩個目標來實現。
&
首先是我們最經常遇到的符號 &
,將它附在命令後面可以使程式在後臺執行,不會佔用前臺介面。它實際上是在會話中開啟了一個後臺作業,對作業的操作我們後面再說。
但我們會發現,如果此時終端被關閉後,程式還是會退出。這是因為,&
符號只有讓程式讓出前臺終端的功能,無法讓程式不受 SIGHUP
訊號的影響。
nohup
nohup
應該是另外一個我們常用的命令了,它的作用如其字面意思,使程式不受 SIGHUP
訊號的影響。但我們在使用 nohup php test.php
後會發現,程式還會一直佔用前臺終端,但即使終端被關閉或連線斷開了,程式還是會執行,另外我們會發現在當前資料夾下多了個名為 nohup.out
的檔案。
這是因為 nohup 的功能僅僅是讓程式不受 SIGHUP
訊號的影響,並不會讓出前臺終端,而且它還會在命令執行目錄下建立 nohup.out
用以儲存程式的輸出。如果程式不需要輸出,且不想讓 nohup 建立檔案,可以將標準輸出和標準錯誤輸出重定向。
我們常將 nohup
和 &
搭配到一塊使用,執行命令如下 nohup command >/dev/null 2>&1 &
這樣,就可以放心的等待程式執行結果了。
setsid
setsid 是另一個讓程式在後臺執行的命令,它的作用是讓程式開啟一個新的會話並執行程式,使用方式為 setsid command
。
根據上面的概念我們得知終端關閉後程式退出是因為會話首程式向程式傳送了 SIGHUP
訊號,setsid 就厲害了,它直接開啟一個新的會話來執行命令,那麼原會話的終端的狀態就再也不會影響到此程式了。
我們使用 pstree
來檢視使用 setsid
和 nohup ... &
兩種命令來執行程式時的程式樹狀態。
nohup php test.php &
1 2 3 4 5 6 7 8 9 10 |
pstree -a |grep -C 6 test |-sshd | `-sshd | `-sshd | `-bash | `-sudo -s | `-bash | |-grep -C 6 test | |-php test.php | `-pstree -a |
我是用 ssh 遠端登陸的機器,所以 test.php 程式是掛在 sshd 程式下的。正常情況下,一旦 sshd 程式結束,則 test.php也無法倖免。
setsid php test.php
1 2 3 4 5 6 7 |
pstree -a |grep -C 6 test |-{nscd} |-php test.php |-php-fpm -- |-sshd | `-sshd |
使用了 setsid 後,test.php 程式已經與 sshd 程式同級,屬於 init 程式的子程式了。
但是 setsid 並沒有為程式分配一個輸出終端,所以程式還是會輸出到當前終端上。
setsid的坑
另外,setsid 有個略坑的地方: 在終端中直接使用 setsid command
執行程式時,終端前臺並不會被影響,command 會在後臺默默執行。而在 shell 指令碼中,我們會發現執行 setsid 的程式會一直阻塞住,直到 command 程式執行結束。
這是因為,setsid 在其是程式組長時會 fork()
一個程式,但它不會 wait()
它的子程式,而是立刻退出,所以在終端內直接使用 setsid 時,setsid 作為程式組長不會佔用終端介面。
而在 shell 指令碼內,setsid 不是程式組長,它不會 fork()
子程式,而是由 bash 來fork()
一個子程式,而 bash 會 wait()
子程式,所以表現得像 setsid 在 wait()
子程式一樣。
要解決這個問題,有兩個辦法:
- 使用上面介紹的
&
符號,使 setsid 強行到後臺執行。 - 使用
.
或source
命令由終端執行 setsid;
其他
除了上面介紹的命令,還有 screen 和 tmux 等會話工具,他們都有自己的一套規範,也比較複雜,掌握本文的命令已經足夠你馳騁 linux 程式控制了。當然有想了解新知識的可以查詢學習一下,應該會比基礎命令好用。
作業命令
使用上面的後臺執行命令時可能還會遇到一些小狀況:
- 被我們放在後臺的程式執行時間過長,而我們又忘記使用 nohup 命令,那麼終端一旦斷開,程式又需要被重新執行。
- 我們直接開啟了某個程式,又想在不中斷程式的情況下讓它讓出前臺終端;
這些都要牽涉到今天的第二個模組–作業;
我們在終端裡執行的命令都可以理解為一個作業,有的佔用前臺終端,有的在後臺默默執行,下面的命令就是為了排程這些作業。
jobs
jobs 是作業的基礎命令,用它可以檢視正在執行的作業的資訊,其輸出如下:
1 2 3 |
jobs [1]- Running php test.php & [2]+ Stopped php test.php |
前面[ ]
內的數字是作業 ID,也是後面我們要操作作業的標識,然後是作業狀態和命令。
ctrl+z
ctrl+z
嚴格來說並是作業命令,它只是向當前程式傳送一個 SIGSTOP
訊號,促使程式進入暫停(stopped)狀態,此狀態下,程式狀態會被系統儲存,此程式會被放置到作業佇列中去,而讓出程式終端。
使用它,我們可以暫停正在佔用終端的程式而不停止它,從而讓我們使用終端命令來操作此程式。
bg
bg是 backgroud
的縮寫,顧名思義,bg %id
把作業放到後臺程式中執行。
結合 ctrl+z
和 bg
命令,我們可以解決上面提出的第一個問題,不停止地將正在佔用終端的程式放到後臺執行。
fg
fg 與 bg 相對,使用它可以把作業放到前臺來執行。
disown
disown 用來將作業從作業列表中移除,即使它 不屬於
會話,這樣終端關閉後不再向此作業傳送 SIGHUP
訊號,以阻止終端對程式的影響。
使用 disown 我們可以解決上面提出的第二個問題,不重新執行將一個沒使用 nohup 命令的程式不受終端關閉影響。
守護程式
以上介紹的都是一些臨時程式的處理,後臺執行的程式的最終方法是將程式變成守護程式。
守護程式
守護程式(daemon)是生存期較長的一種程式,一般在系統啟動時啟動,系統關閉時停止,沒有控制終端,也不會輸出。如我們的伺服器、fpm 等程式就是以守護程式的形式存在的。
建立過程
要建立一個守護程式,步驟為:
必選項
fork
子程式,退出父程式,子程式作為孤兒程式被 init 程式收養;- 使用
setsid
, 開啟新會話,程式成為會話組長,正式脫離終端控制; - 設定訊號處理(特別是子程式退出處理);可選項:
- 使用
chdir
改變程式工作目錄,一般到根目錄下,防止佔用可解除安裝檔案系統; - 用
umask
重設檔案許可權掩碼,不再繼承父程式的檔案許可權設定; - 關閉父程式開啟的檔案描述符;
程式碼
以下是 php 建立守護程式的虛擬碼,另外我的另一篇部落格 初探PHP多程式 也稍微介紹了一些相關內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$pid = pcntl_fork(); if ($pid > 0) { exit; // 父程式直接退出 } elseif ($pid < 0) { throw_error(); // 程式建立失敗 } posix_setsid(); // setsid成為會話領導程式 chdir($dir); // 切換目錄 umask(0); // 重置檔案許可權mask close_fd(); // 關閉父程式的檔案描述符 pcntl_signal($signal, $func); // 註冊訊號處理函式 while (true) { do_job(); // 處理程式任務 pcntl_signal_dispatch(); // 分發訊號處理 } |
總結
linux 是開發者的基礎技能,而程式的排程更是我們常用的功能,希望讀完本文的同學們能有所收穫。
又有大半個月沒發部落格了,最近鼓搗著重構程式碼,經常會在一個點上糾結半天,不知不覺就加了個班。而且這個是個沒法精確度量工作量和目標的活兒,優化沒有盡頭嘛。不過由於要更多地考慮一下程式碼的抽象、效率和擴充套件,對自己也是個挑戰,算是樂在其中吧~
最近可能會考慮寫一個守護程式和 cron 程式排程器,嗯,希望給我算到工作量裡,哈哈~想寫的太多了,只怨自己還不夠強大。。。