Linux 進程概念
概念的理解
首先程序與進程是什么?程序與進程又有什么區(qū)別?
程序(procedure):不太精確地說,程序就是執(zhí)行一系列有邏輯、有順序結(jié)構(gòu)的指令,幫我們達成某個結(jié)果。就如我們?nèi)ゲ宛^,給服務(wù)員說我要牛肉蓋澆飯,她執(zhí)行了做牛肉蓋澆飯這么一個程序,最后我們得到了這么一盤牛肉蓋澆飯。它需要去執(zhí)行,不然它就像一本武功秘籍,放在那里等人翻看。
進程(process):進程是程序在一個數(shù)據(jù)集合上的一次執(zhí)行過程,在早期的 UNIX、Linux 2.4 及更早的版本中,它是系統(tǒng)進行資源分配和調(diào)度的獨立基本單位。同上一個例子,就如我們?nèi)チ瞬宛^,給服務(wù)員說我要牛肉蓋澆飯,她執(zhí)行了做牛肉蓋澆飯這么一個程序,而里面做飯的是一個進程,做牛肉湯汁的是一個進程,把牛肉湯汁與飯混合在一起的是一個進程,把飯端上桌的是一個進程。它就像是我們在看武功秘籍這么一個過程,然后一個篇章一個篇章地去練。
簡單來說,程序是為了完成某種任務(wù)而設(shè)計的軟件,比如 vim 是程序。什么是進程呢?進程就是運行中的程序。
程序只是一些列指令的集合,是一個靜止的實體,而進程不同,進程有以下的特性:
動態(tài)性:進程的實質(zhì)是一次程序執(zhí)行的過程,有創(chuàng)建、撤銷等狀態(tài)的變化。而程序是一個靜態(tài)的實體。
并發(fā)性:進程可以做到在一個時間段內(nèi),有多個程序在運行中。程序只是靜態(tài)的實體,所以不存在并發(fā)性。
獨立性:進程可以獨立分配資源,獨立接受調(diào)度,獨立地運行。
異步性:進程以不可預(yù)知的速度向前推進。
結(jié)構(gòu)性:進程擁有代碼段、數(shù)據(jù)段、PCB(進程控制塊,進程存在的唯一標志)。也正是因為有結(jié)構(gòu)性,進程才可以做到獨立地運行。
并發(fā):在一個時間段內(nèi),宏觀來看有多個程序都在活動,有條不紊的執(zhí)行(每一瞬間只有一個在執(zhí)行,只是在一段時間有多個程序都執(zhí)行過)
并行:在每一個瞬間,都有多個程序都在同時執(zhí)行,這個必須有多個 CPU 才行
引入進程是因為傳統(tǒng)意義上的程序已經(jīng)不足以描述 OS 中各種活動之間的動態(tài)性、并發(fā)性、獨立性還有相互制約性。程序就像一個公司,只是一些證書,文件的堆積(靜態(tài)實體)。而當公司運作起來就有各個部門的區(qū)分,財務(wù)部,技術(shù)部,銷售部等等,就像各個進程,各個部門之間可以獨立運作,也可以有交互(獨立性、并發(fā)性)。
而隨著程序的發(fā)展越做越大,又會繼續(xù)細分,從而引入了線程的概念,當代多數(shù)操作系統(tǒng)、Linux 2.6 及更新的版本中,進程本身不是基本運行單位,而是線程的容器。就像上述所說的,每個部門又會細分為各個工作小組(線程),而工作小組需要的資源需要向上級(進程)申請。
線程(thread)是操作系統(tǒng)能夠進行運算調(diào)度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發(fā)多個線程,每條線程并行執(zhí)行不同的任務(wù)。因為線程中幾乎不包含系統(tǒng)資源,所以執(zhí)行更快、更有效率。
簡而言之,一個程序至少有一個進程,一個進程至少有一個線程。線程的劃分尺度小于進程,使得多線程程序的并發(fā)性高。另外,進程在執(zhí)行過程中擁有獨立的內(nèi)存單元,而多個線程共享內(nèi)存,從而極大地提高了程序的運行效率。就如下圖所示:

進程的屬性
進程的分類
大概明白進程是個什么樣的存在后,我們需要進一步了解的就是進程分類??梢詮膬蓚€角度來分:
以進程的功能與服務(wù)的對象來分;
以應(yīng)用程序的服務(wù)類型來分;
第一個角度來看,我們可以分為用戶進程與系統(tǒng)進程:
用戶進程:通過執(zhí)行用戶程序、應(yīng)用程序或稱之為內(nèi)核之外的系統(tǒng)程序而產(chǎn)生的進程,此類進程可以在用戶的控制下運行或關(guān)閉。
系統(tǒng)進程:通過執(zhí)行系統(tǒng)內(nèi)核程序而產(chǎn)生的進程,比如可以執(zhí)行內(nèi)存資源分配和進程切換等相對底層的工作;而且該進程的運行不受用戶的干預(yù),即使是 root 用戶也不能干預(yù)系統(tǒng)進程的運行。
第二角度來看,我們可以將進程分為交互進程、批處理進程、守護進程:
交互進程:由一個 shell 終端啟動的進程,在執(zhí)行過程中,需要與用戶進行交互操作,可以運行于前臺,也可以運行在后臺。
批處理進程:該進程是一個進程集合,負責按順序啟動其他的進程。
守護進程:守護進程是一直運行的一種進程,在 Linux 系統(tǒng)啟動時啟動,在系統(tǒng)關(guān)閉時終止。它們獨立于控制終端并且周期性的執(zhí)行某種任務(wù)或等待處理某些發(fā)生的事件。例如 httpd 進程,一直處于運行狀態(tài),等待用戶的訪問。還有經(jīng)常用的 cron(在 centOS 系列為 crond)進程,這個進程為 crontab 的守護進程,可以周期性的執(zhí)行用戶設(shè)定的某些任務(wù)。
進程的衍生
進程有這么多的種類,那么進程之間定是有相關(guān)性的,而這些有關(guān)聯(lián)性的進程又是如何產(chǎn)生的,如何衍生的?
就比如我們啟動了終端,就是啟動了一個 bash 進程,我們可以在 bash 中再輸入 bash 則會再啟動一個 bash 的進程,此時第二個 bash 進程就是由第一個 bash 進程創(chuàng)建出來的,他們之間又是個什么關(guān)系?
我們一般稱呼第一個 bash 進程是第二 bash 進程的父進程,第二 bash 進程是第一個 bash 進程的子進程,這層關(guān)系是如何得來的呢?
關(guān)于父進程與子進程便會提及這兩個系統(tǒng)調(diào)用 fork() 與 exec()
fork-exec是由 Dennis M. Ritchie 創(chuàng)造的
fork() 是一個系統(tǒng)調(diào)用(system call),它的主要作用就是為當前的進程創(chuàng)建一個新的進程,這個新的進程就是它的子進程,這個子進程除了父進程的返回值和 PID 以外其他的都一模一樣,如進程的執(zhí)行代碼段,內(nèi)存信息,文件描述,寄存器狀態(tài)等等
exec() 也是系統(tǒng)調(diào)用,作用是切換子進程中的執(zhí)行程序也就是替換其從父進程復制過來的代碼段與數(shù)據(jù)段
子進程就是父進程通過系統(tǒng)調(diào)用 fork() 而產(chǎn)生的復制品,fork() 就是把父進程的 PCB 等進程的數(shù)據(jù)結(jié)構(gòu)信息直接復制過來,只是修改了 PID,所以一模一樣,只有在執(zhí)行 exec() 之后才會不同,而早先的 fork() 比較消耗資源后來進化成 vfork(),效率高了不少,感興趣的同學可以查查為什么。
這就是子進程產(chǎn)生的由來。簡單的實現(xiàn)邏輯就如下方所示:
pid_t p;
p = fork();
if (p == (pid_t) -1)
/* ERROR */
else if (p == 0)
/* CHILD */
else
/* PARENT */
既然子進程是通過父進程而衍生出來的,那么子進程的退出與資源的回收定然與父進程有很大的相關(guān)性。當一個子進程要正常的終止運行時,或者該進程結(jié)束時它的主函數(shù) main() 會執(zhí)行 exit(n); 或者 return n,這里的返回值 n 是一個信號,系統(tǒng)會把這個 SIGCHLD 信號傳給其父進程,當然若是異常終止也往往是因為這個信號。
在將要結(jié)束時的子進程代碼執(zhí)行部分已經(jīng)結(jié)束執(zhí)行了,系統(tǒng)的資源也基本歸還給系統(tǒng)了,但若是其進程的進程控制塊(PCB)仍駐留在內(nèi)存中,而它的 PCB 還在,代表這個進程還存在(因為 PCB 就是進程存在的唯一標志,里面有 PID 等消息),并沒有消亡,這樣的進程稱之為僵尸進程(Zombie)。
如圖中第四列標題是 S,S 表示的是進程的狀態(tài),而在下屬的第三行的 Z 表示的是 Zombie 的意思。( ps 命令將在后續(xù)詳解)

正常情況下,父進程會收到兩個返回值:exit code(SIGCHLD 信號)與 reason for termination 。之后,父進程會使用 wait(&status) 系統(tǒng)調(diào)用以獲取子進程的退出狀態(tài),然后內(nèi)核就可以從內(nèi)存中釋放已結(jié)束的子進程的 PCB;而如若父進程沒有這么做的話,子進程的 PCB 就會一直駐留在內(nèi)存中,一直留在系統(tǒng)中成為僵尸進程(Zombie)。
雖然僵尸進程是已經(jīng)放棄了幾乎所有內(nèi)存空間,沒有任何可執(zhí)行代碼,也不能被調(diào)度,在進程列表中保留一個位置,記載該進程的退出狀態(tài)等信息供其父進程收集,從而釋放它。但是 Linux 系統(tǒng)中能使用的 PID 是有限的,如果系統(tǒng)中存在有大量的僵尸進程,系統(tǒng)將會因為沒有可用的 PID 從而導致不能產(chǎn)生新的進程。
另外如果父進程結(jié)束(非正常的結(jié)束),未能及時收回子進程,子進程仍在運行,這樣的子進程稱之為孤兒進程。在 Linux 系統(tǒng)中,孤兒進程一般會被 init 進程所“收養(yǎng)”,成為 init 的子進程。由 init 來做善后處理,所以它并不至于像僵尸進程那樣無人問津,不管不顧,大量存在會有危害。
進程 0 是系統(tǒng)引導時創(chuàng)建的一個特殊進程,也稱之為內(nèi)核初始化,其最后一個動作就是調(diào)用 fork() 創(chuàng)建出一個子進程運行 /sbin/init 可執(zhí)行文件,而該進程就是 PID=1 的進程 1,而進程 0 就轉(zhuǎn)為交換進程(也被稱為空閑進程),進程 1 (init 進程)是第一個用戶態(tài)的進程,再由它不斷調(diào)用 fork() 來創(chuàng)建系統(tǒng)里其他的進程,所以它是所有進程的父進程或者祖先進程。同時它是一個守護程序,直到計算機關(guān)機才會停止。
通過以下的命令我們可以很明顯的看到這樣的結(jié)構(gòu)
pstree
或者從此圖我們可以更加形象的看清子父進程的關(guān)系

通過以上的顯示結(jié)果我們可以看的很清楚,init 為所有進程的父進程或者說是祖先進程
我們還可以使用這樣一個命令來看,其中 pid 就是該進程的一個唯一編號,ppid 就是該進程的父進程的 pid,command 表示的是該進程通過執(zhí)行什么樣的命令或者腳本而產(chǎn)生的
ps -fxo user,ppid,pid,pgid,command

可以在圖中看見我們執(zhí)行的 ps 就是由 zsh 通過 fork-exec 創(chuàng)建的子進程而執(zhí)行的
使用這樣的一個命令我們也能清楚的看見 init 如上文所說是由進程 0 這個初始化進程來創(chuàng)建出來的子進程,而其他的進程基本是由 init 創(chuàng)建的子進程,或者是由它的子進程創(chuàng)建出來的子進程。所以 init 是用戶進程的第一個進程也是所有用戶進程的父進程或者祖先進程。(ps 命令將在后續(xù)課程詳解)
就像一個樹狀圖,而 init 進程就是這棵樹的根,其他進程由根不斷的發(fā)散,開枝散葉
進程組與 Sessions
每一個進程都會是一個進程組的成員,而且這個進程組是唯一存在的,他們是依靠 PGID(process group ID)來區(qū)別的,而每當一個進程被創(chuàng)建的時候,它便會成為其父進程所在組中的一員。
一般情況,進程組的 PGID 等同于進程組的第一個成員的 PID,并且這樣的進程稱為該進程組的領(lǐng)導者,也就是領(lǐng)導進程,進程一般通過使用 getpgrp() 系統(tǒng)調(diào)用來尋找其所在組的 PGID,領(lǐng)導進程可以先終結(jié),此時進程組依然存在,并持有相同的 PGID,直到進程組中最后一個進程終結(jié)。
與進程組類似,每當一個進程被創(chuàng)建的時候,它便會成為其父進程所在 Session 中的一員,每一個進程組都會在一個 Session 中,并且這個 Session 是唯一存在的,
Session 主要是針對一個 tty 建立,Session 中的每個進程都稱為一個工作(job)。每個會話可以連接一個終端(control terminal)。當控制終端有輸入輸出時,都傳遞給該會話的前臺進程組。Session 意義在于將多個 jobs 囊括在一個終端,并取其中的一個 job 作為前臺,來直接接收該終端的輸入輸出以及終端信號。 其他 jobs 在后臺運行。
前臺(foreground)就是在終端中運行,能與你有交互的
后臺(background)就是在終端中運行,但是你并不能與其任何的交互,也不會顯示其執(zhí)行的過程
工作管理
bash(Bourne-Again shell)支持工作控制(job control),而 sh(Bourne shell)并不支持。
并且每個終端或者說 bash 只能管理當前終端中的 job,不能管理其他終端中的 job。比如我當前存在兩個 bash 分別為 bash1、bash2,bash1 只能管理其自己里面的 job 并不能管理 bash2 里面的 job
我們都知道當一個進程在前臺運作時我們可以用 ctrl + c 來終止它,但是若是在后臺的話就不行了。
我們可以通過 & 這個符號,讓我們的命令在后臺中運行:
ls &

圖中所顯示的 [1] 236分別是該 job 的 job number 與該進程的 PID,而最后一行的 Done 表示該命令已經(jīng)在后臺執(zhí)行完畢。
我們還可以通過 ctrl + z 使我們的當前工作停止并丟到后臺中去

被停止并放置在后臺的工作我們可以使用這個命令來查看:
jobs

其中第一列顯示的為被放置后臺 job 的編號,而第二列的 + 表示最近(剛剛、最后)被放置后臺的 job,同時也表示預(yù)設(shè)的工作,也就是若是有什么針對后臺 job 的操作,首先對預(yù)設(shè)的 job,- 表示倒數(shù)第二(也就是在預(yù)設(shè)之前的一個)被放置后臺的工作,倒數(shù)第三個(再之前的)以后都不會有這樣的符號修飾,第三列表示它們的狀態(tài),而最后一列表示該進程執(zhí)行的命令。
我們可以通過這樣的一個命令將后臺的工作拿到前臺來:
# 后面不加參數(shù)提取預(yù)設(shè)工作,加參數(shù)提取指定工作的編號
# ubuntu 在 zsh 中需要 %,在 bash 中不需要 %
fg [%jobnumber]


之前我們通過 ctrl + z 使得工作停止放置在后臺,若是我們想讓其在后臺運作我們就使用這樣一個命令:
#與fg類似,加參則指定,不加參則取預(yù)設(shè)
bg [%jobnumber]

既然有方法將被放置在后臺的工作提至前臺或者讓它從停止變成繼續(xù)運行在后臺,當然也有方法刪除一個工作,或者重啟等等。
# kill的使用格式如下
kill -signal %jobnumber
# signal從1-64個信號值可以選擇,可以這樣查看
kill -l
其中常用的有這些信號值


若是在使用 kill +信號值然后直接加 pid,你將會對 pid 對應(yīng)的進程進行操作。
若是在使用 kill+信號值然后 %jobnumber,這時所操作的對象是 job,這個數(shù)字就是就當前 bash 中后臺的運行的 job 的 ID。