首頁 >運維 >linux運維 >linux進程和訊號

linux進程和訊號

巴扎黑
巴扎黑原創
2017-06-23 13:49:162191瀏覽

本文目錄:

#9.1 進程的簡單說明

9.11 行程與程式的差異

# #9.12 多任務與cpu時間片

#9.13 父子行程及建立行程的方式

#9.14 進程的狀態

9.15 範例分析進程狀態轉換過程

9.16 行程結構與子shell

##9.2 job任務

#9.3 終端與進程的關係

#9.4 訊號

#9.41 需知道的訊號

9.42 SIGHUP

#9.43 殭屍行程與SIGCHLD

9.44 手動發送訊號(kill指令)

# #9.45 pkill和killall

#9.5 fuser和lsof


9.1 進程簡單說明

進程是一個非常複雜的概念,涉及的內容也非常非常多。在這一小節所列出內容,已經是我極度簡化後的內容了,應該盡可能都理解下來,我覺得這些理論比如何使用命令來查看狀態更重要,而且不明白這些理論,後面查看狀態信息時基本上不知道狀態對應的是什麼意思。

但對於非程式設計人員來說,更多的進程細節也沒有必要去深究,當然,多多益善是肯定的。

9.1.1 進程和程式的區別

#程式是二進位文件,是靜態存放在磁碟上的,不會佔用系統運行資源(cpu /內存)。

進程是使用者執行程式或觸發程式的結果,可以認為進程是程式的一個運行實例。進程是動態的,會申請和使用系統資源,並與作業系統核心進行互動。在後文中,不少狀態統計工具的結果中顯示的是system類別的狀態,其實system狀態的同義詞就是內核狀態。

9.1.2 多任務和cpu時間片

#現在所有的作業系統都能"同時"運行多個進程,也就是多任務或者說是並行執行。但其實這是人類的錯覺,

一顆物理cpu在同一時刻只能運行一個進程,只有多顆物理cpu才能真正意義上實現多任務

人類會產生錯覺,以為作業系統能並行做幾件事情,這是透過在極短時間內進行進程間切換實現的,因為時間極短,前一刻執行的是進程A,下一刻切換到進程B,不斷的在多個進程間進行切換,使得人類以為在同時處理多件事情。

不過,cpu如何選擇下一個要執行的進程,這是一件非常複雜的事情。

在Linux上,決定下一個要運行的進程是透過"調度類別"(調度程式)來實現的

。程式何時運行,由進程的優先權決定,但要注意,優先權值越低,優先權就越高,就越快被調度類別選取。在Linux中,改變行程的nice值,可以影響某一類別行程的優先權值。

有些進程比較重要,要讓其盡快完成,有些進程則比較次要,早點或晚點完成不會有太大影響,所以作業系統要能夠知道哪些進程比較重要,哪些進程比較次要。比較重要的進程,應該多給它一些cpu的執行時間,讓其盡快完成。下圖是cpu時間片的概念。

 

由此可以知道,所有的行程都有機會運行,但重要的行程總是會獲得更多的cpu時間,這種方式是"搶佔式多任務處理":核心可以強制在時間片耗盡的情況下收回cpu使用權,並將cpu交給調度類別選取的進程,此外,在某些情況下也可以直接搶佔當前運行的進程。隨著時間的流逝,分配給進程的時間也會逐漸消耗,當分配時間消耗完畢時,內核收回此進程的控制權,並讓下一個進程運行。但因為前面的進程還沒完成,在未來某個時候調度類別還是會選中它,所以核心應該將每個進程暫時停止時的運行時環境(寄存器中的內容和頁表)保存下來(保存位置為核心所佔用的記憶體),稱為保護現場,在下次進程恢復運行時,將原來的運行時環境載入到cpu上,這稱為恢復現場,這樣cpu可以在當初的運行時環境下繼續執行。

看書上說,Linux的調度器不是透過cpu的時間片流逝來選擇下一個要運行的進程的,而是考慮進程的等待時間,即在就緒隊列等待了多久,那些對時間需求最嚴格的進程應該儘早安排其執行。另外,重要的進程分配的cpu運行時間自然會較多。

調度類別選取了下一個要執行的進程後,要進行底層的任務切換,也就是上下文切換,這個過程需要和cpu進程緊密的互動。 進程切換不應太頻繁,也不應太慢。切換太頻繁將導致cpu閒置在保護和恢復現場的時間過長,保護和恢復現場對人類或進程來說是沒有產生生產力的(因為它沒有在執行程序)。切換太慢將導致進程調度切換慢,很可能下一個進程要等待很久才能輪到它執行,直白的說,如果你發出一個ls命令,你可能要等半天,這顯然是不允許的。

至此,也就知道了cpu的衡量單位是時間,就像記憶體的衡量單位是空間大小一樣。進程佔用的cpu時間長,表示cpu運作在它身上的時間就長。注意,cpu的百分比值不是其工作強度或頻率高低,而是"進程佔用cpu時間/cpu總時間",這個衡量概念一定不要搞錯。


9.1.3 父子行程及建立流程的方式

根據執行程式的使用者UID以及其他標準,會為每一個行程分配一個唯一的PID。

父子進程的概念,簡單來說,在某進程(父進程)的環境下執行或呼叫程序,這個程式觸發的進程就是子進程,而進程的PPID表示的是該進程的父進程的PID。由此也知道了,子行程總是由父行程創建

在Linux,父子程式以樹型結構的方式存在,父行程建立出來的多個子程序之間稱為兄弟行程。 CentOS 6上,init進程是所有進程的父進程,CentOS 7上則為systemd。

Linux上建立子進程的方式有三種(極為重要的概念):一個是fork出來的進程,一個是exec出來的進程,一個是clone出來的進程。

(1).fork是複製進程,它會複製目前進程的副本(不考慮寫入時複製的模式),以適當的方式將這些資源交給子進程。所以子行程掌握的資源和父行程是一樣的,包括記憶體中的內容,所以也包含環境變數和變數。但父子進程是完全獨立的,它們是一個程式的兩個實例。

(2).exec是載入另一個應用程序,取代目前運行的進程,也就是說在不建立新進程的情況下載入一個新程式。 exec還有一個動作,在行程執行完畢後,退出exec所在的shell。所以為了確保進程安全,若要形成新的且獨立的子進程,都會先fork一份當前進程,然後在fork出來的子進程上呼叫exec來載入新程式取代該子進程。例如在bash下執行cp指令,會先fork出一個bash,然後再exec載入cp程式覆蓋子bash程序變成cp程序。

(3).clone用於實作執行緒。 clone的工作原理和fork相同,但clone出來的新進程不獨立於父進程,它只會和父進程共享某些資源,在clone進程的時候,可以指定要共享的是哪些資源。

一般情況下,兄弟進程之間是相互獨立、互不可見的,但有時候透過特殊手段,它們會實現進程間通訊。例如管道協調了兩邊的進程,兩邊的進程屬於同一個進程組,它們的PPID是一樣的,管道使得它們可以以"管道"的方式傳遞資料。

進程是有所有者的,也就是它的發起者,某個用戶如果它非進程發起者、非父進程發起者、非root用戶,那麼它就無法殺死進程。且殺死父進程(非終端進程),會導致子進程變成孤兒進程,孤兒進程的父進程總是init/systemd。


9.1.4 行程的狀態

行程並非總是處於運作中,至少cpu沒有運作在它身上時它就是非運作的。進程有幾種狀態,不同的狀態之間可以實現狀態切換。下圖是非常經典的進程狀態描述圖,個人感覺右圖比較容易理解。

 

運行態:行程正在運行,也就是cpu正在它身上。

就緒(等待)態:進程可以運行,已經處於等待佇列中,也就是說調度類別下次可能會選取它

睡眠(阻塞)態:進程睡眠了,不可運行。

各狀態之間的轉換方式為:(也許可能不太好理解,可以結合稍後的例子)

(1)新狀態->就緒態:當等待佇列允許接納新行程時,核心便把新行程移入等待佇列。

(2)就緒態->運行態:調度類別選取等待佇列中的某個行程,該行程進入運行態。

(3)運行態->睡眠態:正在運作的程序因需要等待某事件(如IO等待、訊號等待等)的出現而無法執行,進入睡眠態。

(4)睡眠態->就緒態:進程所等待的事件發生了,進程就從睡眠態排入等待佇列,等待下次被選取執行。

(5)運行態->就緒態:正在執行的程序因時間片用完而被暫停執行;或者在搶佔式調度方式中,高優先級進程強制搶佔了正在執行的低優先級進程。

(6)運作態->終止態:一個行程已完成或發生某種特殊事件,進程將變成終止狀態。對於指令來說,一般都會回傳退出狀態碼。

注意上面的圖中,沒有"就緒-->睡眠"和"睡眠-->運行"的狀態切換。這很容易理解。對於"就緒-->睡眠",等待中的進程本就已經進入了等待隊列,表示可運行,而進入睡眠態表示暫時不可運行,這本身就是衝突的;對於"睡眠-->運行"這也是行不通的,因為調度類別只會從等待佇列中挑出下一次要執行的進程。

再說說運行狀態-->睡眠態。從運行態到睡眠態一般是等待某事件的出現,例如等待訊號通知,等待IO完成。訊號通知很容易理解,而對於IO等待,程式要運行起來,cpu就要執行該程式的指令,同時還需要輸入數據,可能是變數資料、鍵盤輸入資料或磁碟檔案中的數據,後兩種數據相對cpu來說,都是極慢極慢的。但不管怎樣,如果cpu在需要數據的那一刻卻得不到數據,cpu就只能閒置下來,這肯定是不應該的,因為cpu是極其珍貴的資源,所以核心應該讓正在運行且需要數據的進程暫時進入睡眠,等它的資料都準備好了再回到等待佇列等待被調度類別選取。這就是IO等待。

其實上面的圖少了一種行程的特殊狀態-殭屍態。 殭屍態行程表示的是進程已經轉為終止態,它已經完成了它的使命並消逝了,但是內核還沒來得及將它在進程列表中的項刪除,也就是說內核沒給它料理後事,這就造成了一個進程是死的也是活著的假象,說它死了是因為它不再消耗資源,調度類也不可能選中它並讓它運行,說它活著是因為在進程列表中也存在對應的表項,可以被捕捉。殭屍態進程並不佔用多少資源,它只在進程列表中佔用一點點的記憶體。大多數殭屍行程的出現都是因為行程正常終止(包括kill -9),但父行程沒有確認該行程已經終止,所以沒有通告給內核,內核也就不知道該行程已經終止了。殭屍行程更具體說明請見後文

另外,睡眠態是一個非常廣泛的概念,分為可中斷睡眠和不可中斷睡眠。可中斷睡眠是允許接收外界訊號和核心訊號而被喚醒的睡眠,絕大多數睡眠都是可中斷睡眠,能ps或top捕捉到的睡眠也幾乎總是可中斷睡眠;不可中斷睡眠只能由內核發起訊號來喚醒,外界無法透過訊號來喚醒,主要表現在和硬體互動的時候。例如cat一個檔案時,從硬碟載入資料到記憶體中,在和硬體互動的那一小段時間一定是不可中斷的,否則在載入資料的時候突然被人為傳送的訊號手動喚醒,而被喚醒時和硬體互動的過程又還沒完成,所以即使喚醒了也沒辦法將cpu交給它運行,所以cat一個檔案的時候不可能只顯示一部分內容。而且,不可中斷睡眠若能被人為喚醒,更嚴重的後果是硬體崩潰。由此可知,不可中斷睡眠是為了保護某些重要進程,也是為了讓cpu不被浪費。一般不可中斷睡眠的存在時間極短,也極難透過非程式方式捕捉到。

其實只要發現進程存在,且非殭屍態進程,還不佔用cpu資源,那麼它就是睡眠的。包括後文中出現的暫停態、追蹤態,它們也都是睡眠態。


9.1.5 舉例分析進程狀態轉換過程

進程間狀態的轉換情況可能很複雜,這裡舉個例子,盡可能詳細地描述它們。

以在bash下執行cp指令為例。在目前bash環境下,處於可運行狀態(即就緒態)時,當執行cp指令時,首先fork出一個bash子程序,然後在子bash上exec載入cp程序,cp子程序進入等待佇列,由於在命令列下敲的命令,所以優先權較高,調度類別很快就選中它。在cp這個子進程執行過程中,父進程bash會進入睡眠狀態(不僅是因為cpu只有一顆的情況下一次只能執行一個進程,還因為進程等待),並等待被喚醒,此刻bash無法和人類交互。當cp指令執行完畢,它將自己的退出狀態碼告知父進程,此次複製是成功還是失敗,然後cp進程自己消逝掉,父進程bash被喚醒再次進入等待佇列,並且此時bash已經獲得了cp退出狀態碼。根據狀態碼這個"訊號",父行程bash知道了子行程已經終止,所以通告給內核,內核收到通知後將行程清單中的cp行程項目刪除。至此,整個cp進程正常完成。

假如cp這個子程序複製的是一個大文件,一個cpu時間片無法完成複製,那麼在一個cpu時間片消耗盡的時候它將進入等待佇列。

假如cp這個子程序複製文件時,目標位置已經有了同名文件,那麼默認會詢問是否覆蓋,發出詢問時它等待yes或no的信號,所以它進入了睡眠狀態(可中斷睡眠),當在鍵盤上敲入yes或no訊號給cp的時候,cp收到訊號,從睡眠態轉入就緒態,等待調度類別選取它完成cp進程。

在cp複製時,它需要和磁碟交互,在和硬體交互的短暫過程中,cp將處於不可中斷睡眠。

假如cp進程結束了,但是結束的過程出現了某種意外,使得bash這個父進程不知道它已經結束了(此例中是不可能出現這種情況的),那麼bash就不會通知內核回收程序清單中的cp表項,cp此時就成了殭屍行程。


9.1.6 進程結構和子shell

  • 前台進程:一般命令(如cp命令)在執行時都會fork子進程來執行,在子進程執行過程中,父進程會進入睡眠,這類是前台進程。 前台進程執行時,其父進程睡眠,因為cpu只有一顆,即使是多顆cpu,也會因為執行流(進程等待)的原因而只能執行一個進程,要想實現真正的多任務,應該使用進程內多執行緒實作多個執行流。

  • 後台程序:若在執行指令時,在指令的結尾加上符號"&",它會進入背景。將命令放入後台,會立即返回父進程,並傳回該後台進程的的jobid和pid,所以後台進程的父進程不會進入睡眠。當後台程序出錯,或執行完成,總之後台程序終止時,父進程會收到訊號。所以,透過在指令後面加上"&",再在"&"後給定另一個要執行的指令,可以實現"偽並行"執行的方式,例如"cp /etc/fstab /tmp & cat /etc/fstab"。

  • bash內建指令:bash內建指令是非常特殊的,父行程不會建立子行程來執行這些指令,而是直接在目前bash行程中執行。但如果將內建指令放在管道後,則此內建指令將和管道左邊的進程同屬於一個進程組,所以仍然會建立子進程。

說到這裡了,應該要解釋下子shell,這個特殊的子程序。

一般fork出來的子進程,內容和父進程是一樣的,包括變量,例如執行cp指令時也能取得到父進程的變數。但是cp指令是在哪裡執行的呢?在子shell中。執行cp指令敲入回車後,目前的bash進程fork出一個子bash,然後子bash透過exec載入cp程式取代子bash。請不要在此糾結子bash和子shell,如果搞不清它們的關係,就當它是同一種東西好了。

那是否可以這樣理解,所有命令其運行環境都是在子shell中呢?顯然,上面所說的bash內建指令不是在子shell中運行的。其他的所有方式,都是在子shell中完成,只不過方式不盡相同。完整的子shell請參考man bash,在其中非常多的地方都提到了子shell。以下列出幾種常見的方式。

  • (1).直接執行bash指令。這是一個很巧合的命令。 bash指令本身就是bash內建指令,在目前shell環境下執行內建指令本不會創建子shell,也就是說不會有獨立的bash行程出現,而實際結果則表現為新的bash是一個子程序。其中一個原因是執行bash指令會載入各種環境配置項,為了父bash的環境得到保護而不被覆蓋,所以應該讓其以子shell的方式存在。雖然fork出來的bash子程序內容完全繼承父shell,但因重新加載了環境配置項,所以子shell沒有繼承普通變量,更準確的說是覆蓋了從父shell中繼承的變量 。不妨試試在/etc/bashrc檔中定義一個變量,再在父shell中導出名稱相同值卻不同的環境變量,然後到子shell中看看該變量的值為何?

  • (2).執行shell腳本。因為腳本中第一行總是"#!/bin/bash"或直接"bash xyz.sh",所以這和上面的執行bash進入子shell其實是一回事,都是使用bash指令進入子shell。只不過執行腳本多了一個動作:指令執行完畢後自動退出子shell。也因此執行腳本時,腳本中不會繼承父shell的環境變數。

  • (3).非內建指令的指令替換。當命令中包含了命令替換部分時,將先執行這部分內容,如果這部分內容不是內建命令,將在子shell中完成,再將執行結果傳回給目前命令。因為這次的子shell不是透過bash指令進入的子shell,所以它會繼承父shell的所有變數內容。這也解釋了"$(echo $$)"中"$$"的結果是目前bash的pid號,而不是子shell的pid號,因為它不是使用bash指令進入的子shell。

還有兩個特殊的腳本呼叫方式:exec和source。

  • exec:exec是載入程式取代目前進程,所以它不會開啟子shell,而是直接在目前shell中執行指令或腳本,執行完exec後直接退出exec所在的shell。這就解釋了為何bash下執行cp指令時,cp執行完畢後會自動退出cp所在的子shell。

  • source:source一般用來載入環境設定類別腳本,無法直接載入指令。它也不會開啟子shell,直接在目前shell中執行呼叫腳本且執行腳本後不退出目前shell,所以腳本會繼承目前已有的變量,且腳本執行完畢後載入的環境變數會黏滯給目前shell ,在目前shell生效。


9.2 job任務

大部分行程都能放入後台,這時它就是一個後台任務,所以常稱為job,每個開啟的shell會維護一個job table,後台中的每個job都在job table中對應一個Job項。

手動將命令或腳本放入背景運行的方式是在命令列後面加上"&"符號。例如:

[root@server2 ~]# cp /etc/fstab  /tmp/ &[1] 8701

將進程放入後台後,會立即返回其父進程,一般對於手動放入後台的進程都是在bash下進行的,所以立即返回bash環境。在返回父進程的同時,也會傳回給父進程其jobid和pid。未來要引用jobid,都應該在jobid前加上百分號"%",其中"%%"表示當前job,例如"kill -9 %1"表示殺掉jobid為1的後台進程,如果不加百分號,完了,把Init進程殺了。

透過jobs指令可以查看後台job資訊。

jobs [--l:jobs默认不会列出后台工作的PID,加上---s:显示后台工作处于stopped状态的jobs

透過"&"放入後台的任務,在後台中仍會處於運行中。當然,對於那種互動式如vim類的命令,將轉入暫停運行狀態。

[root@server2 ~]# sleep 10 &[1] 8710[root@server2 ~]# jobs
[1]+  Running                 sleep 10 &

一定要注意,這裡看到的是running和ps或top顯示的R狀態,它們並不總是表示正在運行,處於等待佇列的程序也屬於running。它們都屬於task_running標識。

另一種手動加入後台的方式是按下CTRL+Z鍵,這可以將正在運行中的進程加入到後台,但這樣加入後台的進程會在後台暫停運行。

[root@server2 ~]# sleep 10^Z
[1]+  Stopped                 sleep 10[root@server2 ~]# jobs
[1]+  Stopped                 sleep 10

從jobs訊息也看到了在每個jobid的後面有個"+"號,還有"-",或不帶符號。

[root@server2 ~]# sleep 30&vim /etc/my.cnf&sleep 50&[1] 8915[2] 8916[3] 8917
[root@server2 ~]# jobs
[1]   Running                 sleep 30 &[2]+  Stopped                 vim /etc/my.cnf
[3]-  Running                 sleep 50 &

在發現vim的進程後面是加號,"+"表示執行中的任務,也就是說cpu正在它身上,"-"表示被調度類別選取的下個要執行的任務,從第三個任務開始不會再標註。從jobs的狀態可以分析出來,後台任務表中running但沒有"+"的表示處於等待隊列,running且帶有"+"的表示正在執行,stopped狀態的表示處於睡眠狀態。但不能認為job列表中任務一直是這樣的狀態,因為每個任務分配到的時間片實際上都很短,在很短的時間內執行完這次時間片長度的任務,立刻切換到下一個任務並執行。只不過實際過程中,因為切換速度和每個任務的時間片都極短,所以任務清單較小時,顯示出來的順序可能不怎麼會出現變動。

就上面的例子而言,下一個要執行的任務是vim,但它是stop的,難道因為這個第一順位的進程stop,其他進程就不執行嗎?顯然不是這樣的。事實上,過不了多久,會發現另外兩個sleep任務已經完成了,但vim仍處於stop狀態。

[root@server2 ~]# jobs
[1]   Done                    sleep 30[2]+  Stopped                 vim /etc/my.cnf
[3]-  Done                    sleep 50

透過這個job例子,是不是更深入的理解了一點核心調度進程的方式呢?

回歸正題。既然能手動將進程放入後台,那肯定能調回到前台,調到前台查看了下執行進度,又想調入後台,這肯定也得有方法,總不能使用CTRL+Z以暫停方式加到後台吧。

fg和bg指令分別是foreground和background的縮寫,也就是放入前台和放入後台,嚴格的說,是以運行狀態放入前台和後台,即使原來任務是stopped狀態的。

操作方式也很簡單,直接在命令後加上jobid即可(即[fg|bg] [%jobid]),不給定jobid時操作的將是當前任務,即帶有" +"的任務項。

[root@server2 ~]# sleep 20^Z                # 按下CTRL+Z进入暂停并放入后台
[3]+  Stopped                 sleep 20
[root@server2 ~]# jobs
[2]-  Stopped                 vim /etc/my.cnf
[3]+  Stopped                 sleep 20       # 此时为stopped状态
#
[root@server2 ~]# bg %3            # 使用bg或fg可以让暂停状态的进程变会运行态
[3]+ sleep 20 &
[root@server2 ~]# jobs
[2]+  Stopped                 vim /etc/my.cnf
[3]-  Running                 sleep 20 &     # 已经变成运行态

使用disown指令可以從job table直接移除一個job,只是只移出job table,並非是結束任務。而且移除job table後,任務將掛在init/systemd進程下,使其不依賴終端機

disown [-ar] [-h] [%jobid ...]
选项说明:-h:给定该选项,将不从job table中移除job,而是将其设置为不接受shell发送的sighup信号。具体说明见"信号"小节。-a:如果没有给定jobid,该选项表示针对Job table中的所有job进行操作。-r:如果没有给定jobid,该选项严格限定为只对running状态的job进行操作

如果不给定任何选项,该shell中所有的job都会被移除,移除是disown的默认操作,如果也没给定jobid,而且也没给定-a或-r,则表示只针对当前任务即带有"+"号的任务项。


9.3 终端和进程的关系

使用pstree命令查看下当前的进程,不难发现在某个终端执行的进程其父进程或上几个级别的父进程总是会是终端的连接程序。

例如下面筛选出了两个终端下的父子进程关系,第一个行是tty终端(即直接在虚拟机中)中执行的进程情况,第二行和第三行是ssh连接到Linux上执行的进程。

[root@server2 ~]# pstree -c | grep bash|-login---bash---bash---vim|-sshd-+-sshd---bash|      `-sshd---bash-+-grep

正常情况下杀死父进程会导致子进程变为孤儿进程,即其PPID改变,但是杀掉终端这种特殊的进程,会导致该终端上的所有进程都被杀掉。这在很多执行长时间任务的时候是很不方便的。比如要下班了,但是你连接的终端上还在执行数据库备份脚本,这可能会花掉很长时间,如果直接退出终端,备份就终止了。所以应该保证一种安全的退出方法。

一般的方法也是最简单的方法是使用nohup命令带上要执行的命令或脚本放入后台,这样任务就脱离了终端的关联。当终端退出时,该任务将自动挂到init(或systemd)进程下执行。如:

shell> nohup tar rf a.tar.gz /tmp/*.txt

另一种方法是使用screen这个工具,该工具可以模拟多个物理终端,虽然模拟后screen进程仍然挂在其所在的终端上的,但同nohup一样,当其所在终端退出后将自动挂到init/systemd进程下继续存在,只要screen进程仍存在,其所模拟的物理终端就会一直存在,这样就保证了模拟终端中的进程继续执行。它的实现方式其实和nohup差不多,只不过它花样更多,管理方式也更多。一般对于简单的后台持续运行进程,使用nohup足以。

另外,可能你已经发现了,很多进程是和终端无关的,也就是不依赖于终端,这类进程一般是内核类进程/线程以及daemon类进程,若它们也依赖于终端,则终端一被终止,这类进程也立即被终止,这是绝对不允许的。


9.4 信号

信号在操作系统中控制着进程的绝大多数动作,信号可以让进程知道某个事件发生了,也指示着进程下一步要做出什么动作。信号的来源可以是硬件信号(如按下键盘或其他硬件故障),也可以是软件信号(如kill信号,还有内核发送的信号)。不过,很多可以感受到的信号都是从进程所在的控制终端发送出去的。


9.4.1 需知道的信号

Linux中支持非常多种信号,它们都以SIG字符串开头,SIG字符串后的才是真正的信号名称,信号还有对应的数值,其实数值才是操作系统真正认识的信号。但由于不少信号在不同架构的计算机上数值不同(例如CTRL+Z发送的SIGSTP信号就有三种值18,20,24),所以在不确定信号数值是否唯一的时候,最好指定其字符名称。

以下是需要了解的信号。

      中断进程,可被捕捉和忽略,几乎等同于sigterm,所以也会尽可能的释放执行clean-up,释放资源,保存状态等(CTRL+      强制杀死进程,该信号不可被捕捉和忽略,进程收到该信号后不会执行任何clean-      杀死(终止)进程,可被捕捉和忽略,几乎等同于sigint信号,会尽可能的释放执行clean--      该信号是可被忽略的进程停止信号(CTRL+      发送此信号使得stopped进程进入running,该信号主要用于jobs,例如bg &      用户自定义信号2

 

只有SIGKILL和SIGSTOP这两个信号是不可被捕捉且不可被忽略的信号,其他所有信号都可以通过trap或其他编程手段捕捉到或忽略掉。

更多更详细的信号理解或说明,可以参考wiki的两篇文章:

jobs控制机制:(Unix)

信号说明:


9.4.2 SIGHUP

(1).当控制终端退出时,会向该终端中的进程发送sighup信号,因此该终端上行的shell进程、其他普通进程以及任务都会收到sighup而导致进程终止。

两种方式可以改变因终端中断发送sighup而导致子进程也被结束的行为:一是使用nohup命令启动进程,它会忽略所有的sighup信号,使得该进程不会随着终端退出而结束;二是使用disown,将任务列表中的任务移除出job table或者直接使用disown -h的功能设置其不接收终端发送的sighup信号。但不管是何种实现方式,终端退出后未被终止的进程将只能挂靠在init/systemd下。

(2).对于daemon类的程序(即服务性进程),这类程序不依赖于终端(它们的父进程都是Init或systemd),它们收到sighup信号时会重读配置文件并重新打开日志文件,使得服务程序可以不用重启就可以加载配置文件。


9.4.3 僵尸进程和SIGCHLD

一个编程完善的程序,在子进程终止、退出的时候,会发送SIGCHLD信号给父进程,父进程收到信号就会通知内核清理该子进程相关信息。

在子进程死亡的那一刹那,子进程的状态就是僵尸进程,但因为发出了SIGCHLD信号给父进程,父进程只要收到该信号,子进程就会被清理也就不再是僵尸进程。所以正常情况下,所有终止的进程都会有一小段时间处于僵尸态(发送SIGCHLD信号到父进程收到该信号之间),只不过这种僵尸进程存在时间极短(倒霉的僵尸),几乎是不可被ps或top这类的程序捕捉到的。

如果在特殊情况下,子进程终止了,但父进程没收到SIGCHLD信号,没收到这信号的原因可能是多种的,不管如何,此时子进程已经成了永存的僵尸,能轻易的被ps或top捕捉到。僵尸不倒霉,人类就要倒霉,但是僵尸爸爸并不知道它儿子已经变成了僵尸,因为有僵尸爸爸的掩护,僵尸道长即内核见不到小僵尸,所以也没法收尸。悲催的是,人类能力不足,直接发送信号(如kill)给僵尸进程是无效的,因为僵尸进程本就是终结了的进程,不占用任何运行资源,也收不到信号,只有内核从进程列表中将僵尸进程表项移除才能收尸。

要解决掉永存的僵尸有几种方法:

(1).杀死僵尸进程的父进程。没有了僵尸爸爸的掩护,小僵尸就暴露给了僵尸道长的直系弟子init/systemd,init/systemd会定期清理它下面的各种僵尸进程。所以这种方法有点不讲道理,僵尸爸爸是正常的啊,不过如果僵尸爸爸下面有很多僵尸儿子,这僵尸爸爸肯定是有问题的,比如编程不完善,杀掉是应该的。

(2).手动发送SIGCHLD信号给僵尸进程的父进程。僵尸道长找不到僵尸,但被僵尸祸害的人类能发现僵尸,所以人类主动通知僵尸爸爸,让僵尸爸爸知道自己的儿子死而不僵,然后通知内核来收尸。

当然,第二种手动发送SIGCHLD信号的方法要求父进程能收到信号,而SIGCHLD信号默认是被忽略的,所以应该显式地在程序中加上获取信号的代码。也就是人类主动通知僵尸爸爸的时候,默认僵尸爸爸是不搭理人类的,所以要强制让僵尸爸爸收到通知。不过一般daemon类的程序在编程上都是很完善的,发送SIGCHLD总是会收到,不用担心。


9.4.4 手动发送信号(kill命令)

使用kill命令可以手动发送信号给指定的进程。

kill [-s signal] pid...kill [-signal] pid...kill -l

使用kill -l可以列出Linux中支持的信号,有64种之多,但绝大多数非编程人员都用不上。

使用-s或-signal都可以发送信号,不给定发送的信号时,默认为TREM信号,即kill -15。

shell> kill -9 pid1 pid2...
shell> kill -TREM pid1 pid2...
shell> kill -s TREM pid1 pid2...


9.4.5 pkill和killall

这两个命令都可以直接指定进程名来发送信号,不指定信号时,默认信号都是TERM。

(1).pkill

pkill和pgrep命令是同族命令,都是先通过给定的匹配模式搜索到指定的进程,然后发送信号(pkill)或列出匹配的进程(pgrep),pgrep就不介绍了。

pkill能够指定模式匹配,所以可以使用进程名来删除,想要删除指定pid的进程,反而还要使用"-s"选项来指定。默认发送的信号是SIGTERM即数值为15的信号。

pkill [-signal] [-v] [-P ppid,...] [-s pid,...][-U uid,...] [-t term,...] [pattern]
选项说明:-P ppid,... :匹配PPID为指定值的进程-s pid,...  :匹配PID为指定值的进程-U uid,...  :匹配UID为指定值的进程,可以使用数值UID,也可以使用用户名称-t term,... :匹配给定终端,终端名称不能带上"/dev/"前缀,其实"w"命令获得终端名就满足此处条件了,所以pkill可以直接杀掉整个终端-v          :反向匹配-signal     :指定发送的信号,可以是数值也可以是字符代表的信号

在CentOS 7上,还有两个好用的新功能选项。

-F, --pidfile file:匹配进程时,读取进程的pid文件从中获取进程的pid值。这样就不用去写获取进程pid命令的匹配模式-L, --logpidfile  :如果"-F"选项读取的pid文件未加锁,则pkill或pgrep将匹配失败。

例如踢出终端:

shell> pkill -t pts/0

(2).killall

killall主要用于杀死一批进程,例如杀死整个进程组。其强大之处还体现在可以通过指定文件来搜索哪个进程打开了该文件,然后对该进程发送信号,在这一点上,fuser和lsof命令也一样能实现。

killall [-r,--regexp] [-s,--signal signal] [-u,--user user] [-v,--verbose] [-w,--wait] [-I,--ignore-case] [--] name ...
选项说明:-I           :匹配时不区分大小写-r           :使用扩展正则表达式进行模式匹配-s, --signal :发送信号的方式可以是-HUP或-SIGHUP,或数值的"-1",或使用"-s"选项指定信号-u, --user   :匹配该用户的进程-v,          :给出详细信息-w, --wait   :等待直到该杀的进程完全死透了才返回。默认killall每秒检查一次该杀的进程是否还存在,只有不存在了才会给出退出状态码。
               如果一个进程忽略了发送的信号、信号未产生效果、或者是僵尸进程将永久等待下去


9.5 fuser和lsof

fuser可以查看文件或目录所属进程的pid,即由此知道该文件或目录被哪个进程使用。例如,umount的时候提示the device busy可以判断出来哪个进程在使用。而lsof则反过来,它是通过进程来查看进程打开了哪些文件,但要注意的是,一切皆文件,包括普通文件、目录、链接文件、块设备、字符设备、套接字文件、管道文件,所以lsof出来的结果可能会非常多。

9.5.1 fuser

fuser [-ki] [-signal] file/dir-k:找出文件或目录的pid,并试图kill掉该pid。发送的信号是SIGKILL-i:一般和-k一起使用,指的是在kill掉pid之前询问。-signal:发送信号,如-1 -15,如果不写,默认-9,即kill -9不加选项:直接显示出文件或目录的pid

在不加选项时,显示结果中文件或目录的pid后会带上一个修饰符:

    c:在当前目录下

    e:可被执行的

    f:是一个被开启的文件或目录

    F:被打开且正在写入的文件或目录

    r:代表root directory

例如:

[root@xuexi ~]# fuser /usr/sbin/crond/usr/sbin/crond:      1425e

表示/usr/sbin/crond被1425这个进程打开了,后面的修饰符e表示该文件是一个可执行文件。

[root@xuexi ~]# ps aux | grep 142[5]
root       1425  0.0  0.1 117332  1276 ?        Ss   Jun10   0:00 crond

9.5.2 lsof

例如:

输出信息中各列意义:

  •     COMMAND:进程的名称

  •     PID:进程标识符

  •     USER:进程所有者

  •     FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等

  •     TYPE:文件类型,如DIR、REG等

  •     DEVICE:指定磁盘的名称

  •     SIZE/OFF:文件的大小或文件的偏移量(单位kb)(size and offset)

  •     NODE:索引节点(文件在磁盘上的标识)

  •     NAME:打开文件的确切名称

lsof的各种用法:

lsof  /path/to/somefile:显示打开指定文件的所有进程之列表;建议配合grep使用
lsof -c string:显示其COMMAND列中包含指定字符(string)的进程所有打开的文件;可多次使用该选项lsof -p PID:查看该进程打开了哪些文件lsof -U:列出套接字类型的文件。一般和其他条件一起使用。如lsof -u root -a -Ulsof -u uid/name:显示指定用户的进程打开的文件;可使用脱字符"^"取反,如"lsof -u ^root"将显示非root用户打开的所有文件lsof +d /DIR/:显示指定目录下被进程打开的文件
lsof +D /DIR/:基本功能同上,但lsof会对指定目录进行递归查找,注意这个参数要比grep版本慢
lsof -a:按"与"组合多个条件,如lsof -a -c apache -u apache
lsof -N:列出所有NFS(网络文件系统)文件
lsof -n:不反解IP至HOSTNAME
lsof -i:用以显示符合条件的进程情况lsof -i[46] [protocol][@host][:service|port]46:IPv4或IPv6
    protocol:TCP or UDP
    host:host name或ip地址,表示搜索哪台主机上的进程信息    service:服务名称(可以不只一个)
    port:端口号 (可以不只一个)

大概"-i"是使用最多的了,而"-i"中使用最多的又是服务名或端口了。

[root@www ~]# lsof -i :22COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
sshd     1390 root    3u  IPv4  13050      0t0  TCP *:ssh (LISTEN)
sshd     1390 root    4u  IPv6  13056      0t0  TCP *:ssh (LISTEN)
sshd    36454 root    3r  IPv4  94352      0t0  TCP xuexi:ssh->172.16.0.1:50018 (ESTABLISHED)

 

回到系列文章大纲:

转载请注明出处:

 

以上是linux進程和訊號的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn