從內核角度,聊聊進程是什麼

從內核角度,聊聊進程是什麼

來自專欄程天寫代碼4 人贊了文章

我曾經參加過很多面試,其中有三分之二的公司都會問到我一個問題,就是:進程和線程的區別。這個問題網路上有很多標準答案,很多人都能聊得風生水起,但有趣的是,當被問及「進程是什麼」的時候,可能很多人拎不清。

從內核角度來講,可以認為它並沒有線程這個概念(其實並不是)。無論是我們所說的線程,還是進程,對於 Linux 而言,都屬於 task,因此無論是進程還是線程,都擁有唯一屬於自己的 task_struct 。實際上我們所謂的線程,更像是 task 這個概念,有的進程有一個task,就叫做單線程進程;有的進程有多個線程,就叫做多線程進程。

我們先看,進程是什麼,或者說,我們感性里「認為」的進程,在 Linux 內核層面是什麼樣的過程呢?很容易理解的是,在靜態的時候,程序是存儲在磁碟上面的,進程是把程序的邏輯載入到內存的運行過程。我們需要把程序從磁碟讀取到內存,通過操作寄存器,使用CPU執行程序,這個時候程序就變成了進程(進行中的程序)。一般而言,一個進程會擁有4G的虛擬內存,其中3G交付用戶使用,1G用於存儲內核相關的資源,而所謂的內核資源,就是前面提到的 task_struct 中的數據,這一部分資源呢,中文名字是進程式控制制塊,也就是 PCB。

了解進程式控制制塊都包含什麼,只需要看一下 task_struct 的結構就好了。下面列舉我們常見的一些概念在 task_struct 裡面的對應。

long state; /* 表示進程的狀態 -1表示不可執行 0表示可執行 >0表示停止 */

long counter; /* 運行時間片 以jiffs遞減計數 */

long priority; /* 運行優先數,開始時,counter = priority,值越大,表示優先數越高,等待時間越長. */

long signal; /* 信號.是一組點陣圖,每一個bit代表一種信號. */

struct sigaction sigaction[32]; /* 信號響應的數據結構, 對應信號要執行的操作和標誌信息 */

long blocked; /* 進程信號屏蔽碼(對應信號點陣圖) */

int exit_code; /* 任務執行停止的退出碼 其父進程會取 */

unsigned long start_code,end_code,end_data,brk,start_stack; /* start_code代碼段地址 end_code代碼長度(byte),end_data 代碼長度+數據長度(byte) brk總長度(byte) start_stack堆棧段地址 */

long pid,father,pgrp,session,leader; /* 進程號 父進程號 父進程組號 會話號 會話頭(發起者)*/

unsigned short uid,euid,suid; /* 用戶id 號 有效用戶 id 號 保存用戶 id 號*/

unsigned short gid,egid,sgid; /* 組標記號 (組id) 有效組 id 保存的組id */

long alarm; /* 報警定時值 (jiffs數) */

long utime,stime,cutime,cstime,start_time; /* 用戶態運行時間 (jiffs數) 系統態運行時間 (jiffs數) 子進程 用戶態運行時間 子進程系統態運行時間 進程開始運行時刻 */

int tty; /* 進程使用tty的子設備號 -1表示設有使用 */

unsigned short umask; /* 文件創建屬性屏蔽位 */

struct m_inode * pwd; /* 當前工作目錄 i節點結構 */

struct m_inode * root; /* 根目錄i節點結構 */

struct m_inode * executable; /* 執行文件i節點結構 */

unsigned long close_on_exec; /* 執行時關閉文件句柄點陣圖標誌 */

struct file * filp[NR_OPEN]; /* 文件結構指針表 最多32項 表項號即是文件描述符的值 */

struct desc_struct ldt[3]; /* 任務局部描述符表 0-空 1-cs段,2-Ds和Ss段 */

struct tss_struct tss; /* 進程的任務狀態段信息結構 */

進程創建的是通過 sys_fork 函數觸發 0x80 中斷實現的。如果看源碼,會看到 sys_fork 中執行兩個比較重要的函數: find_empty_process 和 copy_process 。前者負責為進程分配一個進程號,該值創建的依據是通過內核全局變數 last_pid 存放的最新進程 id,來指定新進程的進程號,這也是我們看到進程號隨著進程的不斷創建時循環趨勢遞增的原因。後者主要負責為新進程創建 task_struct ,並將父進程的 task_struct 複製給新進程,並更新一些新進程的 task_struct 的私有信息,之後,為新進程創建一個頁表(通過調用 get_free_page 函數),同樣,也需要把父進程的頁表內容複製給新進程,接下來設置新進程共享父進程的文件,並設置新進程的全局描述符表(也就是 GDT) 的項目,例如內存基址等,這樣才能載入代碼、設置數據段基址、限長,也是複製頁表的條件,最後把新進程設置為就緒態,就可以參與到進程的系統調度。

以上就是進程創建的過程,想必介紹到這裡,你就了解了網路上介紹進程和線程區別的時候提到的頁表、數據堆棧之類的概念都大概是什麼,進程對於你也不應該再是一個「玄學」概念。

那麼線程又是什麼呢?實際上線程的概念最早出現在上世紀八十年代,遠遠早於 linux 的誕生。為什麼要引入線程呢?我覺得可以思考一個問題,計算機為什麼要有多個進程?是的,提升處理效率,使得用戶可以在聽音樂的同時寫公眾號文章。那為什麼要有多個線程呢,也大概是這個原因,舉個例子,在我用 office word 寫文章的時候,跟蹤游標輸入的是一個線程,而語法糾錯又會是另外一個線程。一個進程的多個線程會共享絕大部分該進程的數據資源,這也就是網路上所謂的「進程有獨立的數據資源,線程幾乎沒有」的來源,實際上這句話有點誤導,它會讓你認為進程和線程是並列的兩個概念,實際上線程從屬於進程而已。

引入進程的原因,是因為同一個進程,內部可能存在多個不同的 task,這些 task 需要復用進程的數據,但是呢這些 task 操作的數據又有一定的獨立性(或者可以通過鎖實現安全修改),因此多個 task 並不需要按照時序執行,可以同時執行,因此就產生了線程的概念,線程的並行是隨著多核處理器的產生而產生的,多個線程可以通過調度在不同的處理器核心上來實現並行處理。

可以這樣講,多進程實現了操作系統級別的並發,多線程實現了進程內部的並發。

在 linux 層面,線程的創建實際上是調用 clone 函數。我之所以最初說 linux 層面沒有線程這個概念,是因為 linux 創建初期內核就一直不存在「線程」這回事,後來因為實際需求,產生了 LinuxThreads 這個項目。線程的創建「看起來」和進程的創建很像,所以你也能看到線程的 pid,區別在於父子進程會共享地址空間、文件系統資源、文件描述符、信號處理以及被阻斷的信號等,這對於常規進程是不可能的事兒。因此說 linux 內核不存在線程的概念是有道理的,因為所謂的線程是一種特殊的進程,進程和線程是用戶態的區分而不是內核態,內核處理線程的時候,會完全把它和進程當做相同的事物去處理。

這裡解釋一個問題,既然在 linux 內核眼中它們是相同事物,為什麼 kill 掉線程的時候不會影響進程的運行,但是 kill 掉進程的時候也會消滅掉線程呢?這個可以參考 POSIX 標準:

  • 查看進程列表的時候,相關的一組 task_struct 應當被展現為列表中的一個節點;

  • 發送給這個」進程」的信號(對應 kill 系統調用),將被對應的這一組 task_struct 所共享, 並且被其中的任意一個」線程」處理;

  • 發送給某個」線程」的信號(對應 pthread_kill ),將只被對應的一個 task_struct 接收,並且由它自己來處理;

  • 當」進程」被停止或繼續時(對應 SIGSTOP/SIGCONT 信號), 對應的這一組 task_struct 狀態將改變;

  • 當」進程」收到一個致命信號(比如由於段錯誤收到 SIGSEGV 信號),對應的這一組 task_struct 將全部退出;

接下來介紹 linux 中三個著名的進程。

idle_task 進程(pid=0,又稱0號進程),是由操作系統自動創建,運行在內核態,它其實是操作系統創建的第一個進程,最初用於操作系統的載入,後來用於進程的調度管理。

init_task(pid=1) 進程通過idle的內核線程創建,在內核空間完成初始化後,被釋放在用戶空間,它是操作系統一切用戶進程的祖先進程。正如前面所說的 sys_fork 函數,一個用戶進程的創建,實際上是從 init_task 進程 fork 出來的,除此之外,init_task 進程的任務就是守護監視其他進程。

kthreadd_task 進程(pid=2)也是idle通過內核線程創建的,它和1號線程的區別是始終運行在內核空間,負責內核線程的調度和管理。

再介紹兩個常見的進程類型的概念,孤兒進程和殭屍進程。

孤兒進程,指的是在父進程退出的時候,它的子進程(線程)因為各種原因還在運行,這種進程就被成為孤兒進程,孤兒進程會被init_task進程收養,由init_task代替退出的父進程對它們進行狀態收集。

殭屍進程,則是指的那種被fork出來的子進程已經退出,但是父進程並沒有調用 wait 函數或者 waitpid 函數獲取子進程的狀態信息,導致子進程的進程描述符仍然保存在操作系統中,這種實際上已經退出但是在操作系統上還能看到的進程就被稱為殭屍進程。

這兩種進程都涉及到進程退出的概念。進程如何退出呢,前面沒有介紹。linux 中進程退出的機制是,進程接收到操作系統發出的退出信號,內核釋放該進程的所有資源,包括內存和打開的文件,但仍然保存進程號和退出狀態等信息,直到父進程通過 wait 或者 waitpid 函數來採集才會釋放。聯繫到孤兒進程和殭屍進程,實際上孤兒進程對用戶的可見性較低,因為孤兒進程會被init_task進程收養,init_task可以做到安全退出孤兒進程,殭屍進程則不然,它們會佔用進程號等稀缺資源,當一個系統產生大量殭屍進程的時候,則無法創建新進程。


所以,關於進程,你現在理解了嗎?

請關注公眾號 程天寫代碼(pod1024)隨時獲取最新技術掃盲文章。

推薦閱讀:

Windows(x86)頁表與虛擬空間之我見
實測 目前最流行的PE啟動盤哪家最純凈?
Win10最高級版本,win10專業版工作站版本最新密鑰分享
windows操作系統安裝圖文詳解(四)----xp和windows7雙系統安裝(windows7下安裝xp)
Android和iOS操作系統區別的視頻

TAG:Linux內核 | 計算機 | 操作系統 |