首先程序與進程是什麼?程序與進程又有什麼區別?nginx
程序(procedure):不太精確地說,程序就是執行一系列有邏輯、有順序結構的指令,幫咱們達成某個結果。就如咱們去餐館,給服務員說我要牛肉蓋澆飯,她執行了作牛肉蓋澆飯這麼一個程序,最後咱們獲得了這麼一盤牛肉蓋澆飯。它須要去執行,否則它就像一本武功祕籍,放在那裏等人翻看。shell
進程(process):進程是程序在一個數據集合上的一次執行過程,在早期的UNIX、Linux 2.4及更早的版本中,它是系統進行資源分配和調度的獨立基本單位。同上一個例子,就如咱們去了餐館,給服務員說我要牛肉蓋澆飯,她執行了作牛肉蓋澆飯這麼一個程序,而裏面作飯的是一個進程,作牛肉湯汁的是一個進程,把牛肉湯汁與飯混合在一塊兒的是一個進程,把飯端上桌的是一個進程。它就像是咱們在看武功祕籍這麼一個過程,而後一個篇章一個篇章地去練。apache
簡單來講,程序是爲了完成某種任務而設計的軟件,好比 vim 是程序。什麼是進程呢?進程就是運行中的程序。ubuntu
程序只是一些列指令的集合,是一個靜止的實體,而進程不一樣,進程有如下的特性:vim
併發:在一個時間段內,宏觀來看有多個程序都在活動,有條不紊的執行(每一瞬間只有一個在執行,只是在一段時間有多個程序都執行過)bash
並行:在每個瞬間,都有多個程序都在同時執行,這個必須有多個 CPU 才行session
引入進程是由於傳統意義上的程序已經不足以描述 OS 中各類活動之間的動態性、併發性、獨立性還有相互制約性。程序就像一個公司,只是一些證書,文件的堆積(靜態實體)。而當公司運做起來就有各個部門的區分,財務部,技術部,銷售部等等,就像各個進程,各個部門之間能夠獨立運作,也能夠有交互(獨立性、併發性)。數據結構
而隨着程序的發展越作越大,又會繼續細分,從而引入了線程的概念,當代多數操做系統、Linux 2.6及更新的版本中,進程自己不是基本運行單位,而是線程的容器。就像上述所說的,每一個部門又會細分爲各個工做小組(線程),而工做小組須要的資源須要向上級(進程)申請。多線程
線程(thread)是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運做單位。一條線程指的是進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。由於線程中幾乎不包含系統資源,因此執行更快、更有效率。併發
簡而言之,一個程序至少有一個進程,一個進程至少有一個線程。線程的劃分尺度小於進程,使得多線程程序的併發性高。另外,進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率。就以下圖所示:
大概明白進程是個什麼樣的存在後,咱們須要進一步瞭解的就是進程分類。能夠從兩個角度來分:
第一個角度來看,咱們能夠分爲用戶進程與系統進程:
第二角度來看,咱們能夠將進程分爲交互進程、批處理進程、守護進程
進程有這麼多的種類,那麼進程之間定是有相關性的,而這些有關聯性的進程又是如何產生的,如何衍生的?
就好比咱們啓動了終端,就是啓動了一個 bash 進程,咱們能夠在 bash 中再輸入 bash 則會再啓動一個 bash 的進程,此時第二個 bash 進程就是由第一個 bash 進程建立出來的,他們直接又是個什麼關係?
咱們通常稱呼第一個 bash 進程是第二 bash 進程的父進程,第二 bash 進程是第一個 bash 進程的子進程,這層關係是如何得來的呢?
關於父進程與子進程便會說起這兩個系統調用 fork()
與 exec()
fork-exec是由 Dennis M. Ritchie 創造的
fork() 是一個系統調用(system call),它的主要做用就是爲當前的進程建立一個新的進程,這個新的進程就是它的子進程,這個子進程除了父進程的返回值和 PID 之外其餘的都如出一轍,如進程的執行代碼段,內存信息,文件描述,寄存器狀態等等
exec() 也是系統調用,做用是切換子進程中的執行程序也就是替換其從父進程複製過來的代碼段與數據段
子進程就是父進程經過系統調用 fork()
而產生的複製品,fork()
就是把父進程的 PCB 等進程的數據結構信息直接複製過來,只是修改了 PID,因此如出一轍,只有在執行 exec()
以後纔會不一樣,而早先的 fork()
比較消耗資源後來進化成 vfork()
,效率高了很多,感興趣的同窗能夠查查爲何。
這就是子進程產生的由來。簡單的實現邏輯就以下方所示【註釋1】
pid_t p; p = fork(); if (p == (pid_t) -1) /* ERROR */ else if (p == 0) /* CHILD */ else /* PARENT */
既然子進程是經過父進程而衍生出來的,那麼子進程的退出與資源的回收定然與父進程有很大的相關性。當一個子進程要正常的終止運行時,或者該進程結束時它的主函數 main()
會執行 exit(n);
或者 return n
,這裏的返回值 n 是一個信號,系統會把這個 SIGCHLD 信號傳給其父進程,固然如果異常終止也每每是由於這個信號。
在將要結束時的子進程代碼執行部分已經結束執行了,系統的資源也基本歸還給系統了,但如果其進程的進程控制塊(PCB)仍駐留在內存中,而它的 PCB 還在,表明這個進程還存在(由於 PCB 就是進程存在的惟一標誌,裏面有 PID 等消息),並無消亡,這樣的進程稱之爲殭屍進程(Zombie)。
如圖中第四列標題是 S,S 表示的是進程的狀態,而在下屬的第三行的 Z 表示的是 Zombie 的意思。( ps 命令將在後續詳解)
正常狀況下,父進程會收到兩個返回值:exit code(SIGCHLD 信號)與 reason for termination
。以後,父進程會使用 wait(&status)
系統調用以獲取子進程的退出狀態,而後內核就能夠從內存中釋放已結束的子進程的 PCB;而如若父進程沒有這麼作的話,子進程的 PCB 就會一直駐留在內存中,一直留在系統中成爲殭屍進程(Zombie)。
雖然殭屍進程是已經放棄了幾乎全部內存空間,沒有任何可執行代碼,也不能被調度,在進程列表中保留一個位置,記載該進程的退出狀態等信息供其父進程收集,從而釋放它。可是 Linux 系統中能使用的 PID 是有限的,若是系統中存在有大量的殭屍進程,系統將會由於沒有可用的 PID 從而致使不能產生新的進程。
另外若是父進程結束(非正常的結束),未能及時收回子進程,子進程仍在運行,這樣的子進程稱之爲孤兒進程。在 Linux 系統中,孤兒進程通常會被 init 進程所「收養」,成爲 init 的子進程。由 init 來作善後處理,因此它並不至於像殭屍進程那樣無人問津,無論不顧,大量存在會有危害。
進程 0 是系統引導時建立的一個特殊進程,也稱之爲內核初始化,其最後一個動做就是調用 fork()
建立出一個子進程運行 /sbin/init
可執行文件,而該進程就是 PID=1 的進程 1,而進程 0 就轉爲交換進程(也被稱爲空閒進程),進程 1 (init 進程)是第一個用戶態的進程,再由它不斷調用 fork() 來建立系統裏其餘的進程,因此它是全部進程的父進程或者祖先進程。同時它是一個守護程序,直到計算機關機纔會中止。
經過如下的命令咱們能夠很明顯的看到這樣的結構
pstree
或者今後圖咱們能夠更加形象的看清子父進程的關係
經過以上的顯示結果咱們能夠看的很清楚,init 爲全部進程的父進程或者說是祖先進程
咱們還可使用這樣一個命令來看,其中 pid 就是該進程的一個惟一編號,ppid 就是該進程的父進程的 pid,command 表示的是該進程經過執行什麼樣的命令或者腳本而產生的
ps -fxo user,ppid,pid,pgid,command
能夠在圖中看見咱們執行的 ps 就是由 zsh 經過 fork-exec 建立的子進程而執行的
使用這樣的一個命令咱們也能清楚的看見 init 如上文所說是由進程 0 這個初始化進程來建立而出的子進程,而其餘的進程基本是由 init 建立的子進程,或者是由它的子進程建立出來的子進程。因此 init 是用戶進程的第一個進程也是全部用戶進程的父進程或者祖先進程。(ps 命令將在後續課程詳解)
就像一個樹狀圖,而 init 進程就是這棵樹的根,其餘進程由根不斷的發散,開枝散葉
每個進程都會是一個進程組的成員,並且這個進程組是惟一存在的,他們是依靠 PGID(process group ID)來區別的,而每當一個進程被建立的時候,它便會成爲其父進程所在組中的一員。
通常狀況,進程組的 PGID 等同於進程組的第一個成員的 PID,而且這樣的進程稱爲該進程組的領導者,也就是領導進程,進程通常經過使用 getpgrp()
系統調用來尋找其所在組的 PGID,領導進程能夠先終結,此時進程組依然存在,並持有相同的PGID,直到進程組中最後一個進程終結。
與進程組相似,每當一個進程被建立的時候,它便會成爲其父進程所在 Session 中的一員,每個進程組都會在一個 Session 中,而且這個 Session 是惟一存在的,
Session 主要是針對一個 tty 創建,Session 中的每一個進程都稱爲一個工做(job)。每一個會話能夠鏈接一個終端(control terminal)。當控制終端有輸入輸出時,都傳遞給該會話的前臺進程組。Session 意義在於將多個 jobs 囊括在一個終端,並取其中的一個 job 做爲前臺,來直接接收該終端的輸入輸出以及終端信號。 其餘 jobs 在後臺運行。
前臺(foreground)就是在終端中運行,能與你有交互的
後臺(background)就是在終端中運行,可是你並不能與其任何的交互,也不會顯示其執行的過程
bash(Bourne-Again shell)支持工做控制(job control),而 sh(Bourne shell)並不支持。
而且每一個終端或者說 bash 只能管理當前終端的中的 job,不能管理其餘終端中的 job。好比我當前存在兩個 bash 分別爲 bash一、bash2,bash1 只能管理其本身裏面的 job 並不能管理 bash2 裏面的 job
咱們都知道當一個進程在前臺運做時咱們能夠用 ctrl + c
來終止它,可是如果在後臺的話就不行了。
咱們能夠經過 &
這個符號,讓咱們的命令在後臺中運行
ls &
圖中所顯示的 [1] 236
分別是該 job 的 job number 與該進程的 PID,而最後一行的 Done 表示該命令已經在後臺執行完畢。
咱們還能夠經過 ctrl + z
使咱們的當前工做中止並丟到後臺中去
被中止並放置在後臺的工做咱們可使用這個命令來查看
jobs
其中第一列顯示的爲被放置後臺 job 的編號,而第二列的 +
表示最近(剛剛、最後)被放置後臺的 job,同時也表示預設的工做,也就是如果有什麼針對後臺 job 的操做,首先對預設的 job,-
表示倒數第二(也就是在預設以前的一個)被放置後臺的工做,倒數第三個(再以前的)之後都不會有這樣的符號修飾,第三列表示它們的狀態,而最後一列表示該進程執行的命令
咱們能夠經過這樣的一個命令將後臺的工做拿到前臺來
#後面不加參數提取預設工做,加參數提取指定工做的編號 #ubuntu 在 zsh 中須要 %,在 bash 中不須要 % fg [%jobnumber]
以前咱們經過 ctrl + z
使得工做中止放置在後臺,如果咱們想讓其在後臺運做咱們就使用這樣一個命令
#與fg相似,加參則指定,不加參則取預設 bg [%jobnumber]
既然有方法將被放置在後臺的工做提至前臺或者讓它從中止變成繼續運行在後臺,固然也有方法刪除一個工做,或者重啓等等
#kill的使用格式以下 kill -signal %jobnumber #signal從1-64個信號值能夠選擇,能夠這樣查看 kill -l
其中經常使用的有這些信號值
信號值 | 做用 |
---|---|
-1 | 從新讀取參數運行,相似與restart |
-2 | 如同 ctrl+c 的操做退出 |
-9 | 強制終止該任務 |
-15 | 正常的方式終止該任務 |
注意
如果在使用kill+信號值而後直接加 pid,你將會對 pid 對應的進程進行操做
如果在使用kill+信號值而後
%jobnumber
,這時所操做的對象是 job,這個數字就是就當前 bash 中後臺的運行的 job 的 ID