Linux 進程概念

Linux 中也難免遇到某個程序無響應的情況,可以通過一些命令來幫助我們讓系統能夠更流暢的運行。 而在此之前,我們需要對進程的基礎知識有一定的瞭解,才能更好、更有效率的使用Linux 提供的工具。

一、概念理解

1.1 程序與進程

程序(procedure):不太精確地說,程序就是執行一系列有邏輯、有順序結構的指令,幫我們達成某個結果。就如我們去餐館,給服務員說我要牛肉蓋澆飯,她執行了做牛肉蓋澆飯這麼一個程序,最後我們得到了這麼一盤牛肉蓋澆飯。它需要去執行,不然它就像一本武功祕籍,放在那裏等人翻看。

進程(process):進程是程序在一個數據集合上的一次執行過程,在早期的UNIX、Linux 2.4及更早的版本中,它是系統進行資源分配和調度的獨立基本單位。同上一個例子,就如我們去了餐館,給服務員說我要牛肉蓋澆飯,她執行了做牛肉蓋澆飯這麼一個程序,而裏面做飯的是一個進程,做牛肉湯汁的是一個進程,把牛肉湯汁與飯混合在一起的是一個進程,把飯端上桌的是一個進程。它就像是我們在看武功祕籍這麼一個過程,然後一個篇章一個篇章地去練。

簡單來說,程序是爲了完成某種任務而設計的軟件,比如vim程序。而進程就是運行中的程序。

程序只是一些列指令的集合,是一個靜止的實體,而進程不同,進程有以下特點:

  • 動態性: 進程的實質是一次程序執行的過程,有創建、撤銷等狀態的變化。而程序是一個靜態的實體。

  • 併發性: 進程可以做到一個時間段內,有多個進程在運行。程序只是靜態的實體,所以不存在併發性。

  • 獨立性: 進程可以獨立分配資源,獨立接受調查,獨立地運行。

  • 異步性: 進程以不可預知的速度向前推進。

  • 結構性: 進程擁有代碼段、數據段、PCB(進程控制塊,進程存在的唯一標識)。也正是因爲有結構性,進程纔可以做到獨立地運行。

併發:在一個時間段內,宏觀來看有多個程序都在活動,有條不紊的執行(每一瞬間只有一個在執行,只是在一段時間有多個程序都在執行過)

並行:在每一個瞬間,都有多個程序在同時執行,這個必須多個CPU才行

引入進程是因爲傳統意義上的程序已經不足以描述 OS 中各種活動之間的動態性、併發性、獨立性還有相互制約性。程序就像一個公司,只是一些證書,文件的堆積(靜態實體)。而當公司運作起來就有各個部門的區分,財務部,技術部,銷售部等等,就像各個進程,各個部門之間可以獨立運做,也可以有交互(獨立性、併發性)。

而隨着程序的發展越做越大,又會繼續細分,從而引入了線程的概念,當代多數操作系統、Linux 2.6及更新的版本中,進程本身不是基本運行單位,而是線程的容器。就像上述所說的,每個部門又會細分爲各個工作小組(線程),而工作小組需要的資源需要向上級(進程)申請。

線程(thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。因爲線程中幾乎不包含系統資源,所以執行更快、更有效率。

二、進程的屬性

2.1 進程分類

進程的分類,可以從兩個角度來分:

  • 以進程的功能與服務的對象來分;
  • 以應用程序的服務類型來分。

第一個角度來看,可以分爲用戶進程與系統進程:

  • 用戶進程:通過執行用戶程序、應用程序或稱之爲內核之外的系統程序而產生的進程,此來進程可以在用戶的控制下運行或關閉。
  • 系統進程: 通過執行系統內核程序而產生的進程,比如可以執行內存資源分配和進程切換等相對底層的工作;而且該進程的運行不受用戶的干預,即使是root用戶也不能干預系統進程的運行。

第二個角度來看,可以將進程分爲交換進程、批處理進程、守護進程

  • 交換進程:由一個shell終端啓動的進程,在執行過程中,需要與用戶進行交互操作,可以運行於前臺,也可以運行在後臺。
  • 批處理進程:該進程是一個進程集合,負責按順序啓動其他的進程。
  • 守護進程:守護進程是一直運行的一種進程,在Linux系統啓動時啓動,在系統關閉時終止。它們獨立於控制終端並且週期性的執行某種任務或等待處理某些發生的事件。例如httpd進程,一直處於運行狀態,等待用戶的訪問。還有進程用的cron(在centos系列爲crond)進程,這個進程爲crontab的守護進程,可以週期性的執行用戶設定的某些任務。

2.2 進程的衍生

進程有這麼多的種類,那麼進程之間定是有相關性的,而這些有關聯性的進程又是如何產生的,如何衍生的?

就比如我們啓動了終端,就是啓動了一個 bash 進程,我們可以在 bash 中再輸入 bash 則會再啓動一個 bash 的進程,此時第二個 bash 進程就是由第一個 bash 進程創建出來的,他們之間又是個什麼關係?

我們一般稱呼第一個 bash 進程是第二 bash 進程的父進程,第二 bash 進程是第一個 bash 進程的子進程,這層關係是如何得來的呢?

fork-exec是由 Dennis M. Ritchie 創造的

fork() 是一個系統調用(system call),它的主要作用就是爲當前的進程創建一個新的進程,這個新的進程就是它的子進程,這個子進程除了父進程的返回值和 PID 以外其他的都一模一樣,如進程的執行代碼段,內存信息,文件描述,寄存器狀態等等

exec() 也是系統調用,作用是切換子進程中的執行程序也就是替換其從父進程複製過來的代碼段與數據段

子進程就是父進程通過系統調用 fork() 而產生的複製品,fork() 就是把父進程的 PCB 等進程的數據結構信息直接複製過來,只是修改了 PID,所以一模一樣,只有在執行 exec() 之後纔會不同,而早先的 fork() 比較消耗資源後來進化成 vfork(),效率高了不少,感興趣的同學可以查查爲什麼。

既然子進程是通過父進程而衍生出來的,那麼子進程的退出與資源的回收定然與父進程有很大的相關性。當一個子進程要正常的終止運行時,或者該進程結束時它的主函數 main() 會執行 exit(n); 或者 return n,這裏的返回值 n 是一個信號,系統會把這個 SIGCHLD 信號傳給其父進程,當然若是異常終止也往往是因爲這個信號。

在將要結束時的子進程代碼執行部分已經結束執行了,系統的資源也基本歸還給系統了,但若是其進程的進程控制塊(PCB)仍駐留在內存中,而它的 PCB 還在,代表這個進程還存在(因爲 PCB 就是進程存在的唯一標誌,裏面有 PID 等消息),並沒有消亡,這樣的進程稱之爲殭屍進程(Zombie)。

如圖中第四列標題是 S,S 表示的是進程的狀態,而在下屬的第三行的 Z 表示的是 Zombie 的意思。

正常情況下,父進程會收到兩個返回值: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

還可以使用這樣一個命令來看,其中 pid 就是該進程的一個唯一編號,ppid 就是該進程的父進程的 pid,command 表示的是該進程通過執行什麼樣的命令或者腳本而產生的

ps -fxo user,ppid,pid,pgid,command

可以在圖中看見我們執行的 ps 就是由 zsh 通過 fork-exec 創建的子進程而執行的

使用這樣的一個命令我們也能清楚的看見 init 如上文所說是由進程 0 這個初始化進程來創建出來的子進程,而其他的進程基本是由 init 創建的子進程,或者是由它的子進程創建出來的子進程。所以 init 是用戶進程的第一個進程也是所有用戶進程的父進程或者祖先進程。(ps 命令將在後續課程詳解)

就像一個樹狀圖,而 init 進程就是這棵樹的根,其他進程由根不斷的發散,開枝散葉

2.3 進程組於Sessons

每一個進程都會是一個進程組的成員,而且這個進程組是唯一存在的,他們是依靠 PGID(process group ID)來區別的,而每當一個進程被創建的時候,它便會成爲其父進程所在組中的一員。

一般情況,進程組的 PGID 等同於進程組的第一個成員的 PID,並且這樣的進程稱爲該進程組的領導者,也就是領導進程,進程一般通過使用 getpgrp() 系統調用來尋找其所在組的 PGID,領導進程可以先終結,此時進程組依然存在,並持有相同的PGID,直到進程組中最後一個進程終結。

與進程組類似,每當一個進程被創建的時候,它便會成爲其父進程所在 Session 中的一員,每一個進程組都會在一個 Session 中,並且這個 Session 是唯一存在的,

Session 主要是針對一個 tty 建立,Session 中的每個進程都稱爲一個工作(job)。每個會話可以連接一個終端(control terminal)。當控制終端有輸入輸出時,都傳遞給該會話的前臺進程組。Session 意義在於將多個 jobs 囊括在一個終端,並取其中的一個 job 作爲前臺,來直接接收該終端的輸入輸出以及終端信號。 其他 jobs 在後臺運行。

前臺(foreground)就是在終端中運行,能與你有交互的

後臺(background)就是在終端中運行,但是你並不能與其任何的交互,也不會顯示其執行的過程

2.4 工作管理

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 表示該命令已經在後臺執行完畢。

我們還可以通過 ctrl + z 使我們的當前工作停止並丟到後臺中去

其中第一列顯示的爲被放置後臺 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章