進程與線程是全部的程序員都熟知的概念,簡單來講進程是一個執行中的程序,而線程是進程中的一條執行路徑。進程是操做系統中基本的抽象概念,本文介紹 Linux 中進程和線程的用法以及原理,包括建立、消亡等。程序員
Linux 中進程的建立與執行分爲兩個函數,分別是 fork
和 exec
,以下代碼所示:編程
int main() { pid_t pid; if ((pid = fork() < 0) { printf("fork error\n"); } else if (pid == 0) { // child if (execle("/home/work/bin/test1", "test1", NULL) < 0) { printf("exec error\n"); } } // parent if (waitpid(pid, NULL) < 0) { printf("wait error\n"); } }
fork
從當前進程建立一個子進程,此函數返回兩次,對於父進程而言,返回的是子進程的進程號,對於子進程而言返回 0。子進程是父進程的副本,擁有與父進程同樣的數據空間、堆和棧的副本,而且共享代碼段。數據結構
因爲子進程一般是爲了調用 exec
裝載其它程序執行,因此 Linux 採用了寫時拷貝技術,即數據段、堆和棧的副本並不會在 fork
以後就真的拷貝,只是將這些內存區域的訪問權限變爲只讀,若是父子進程中有任一個要修改這些區域,纔會修改對應的內存頁生成新的副本,這樣子是爲了提升性能。函數
fork
以後父進程先執行仍是子進程先執行是不肯定的,因此若是要求父子進程進行同步,每每須要使用進程間通訊。fork
以後子進程會繼承父進程的不少東西,如:性能
父子進程的區別在於:spa
fork
以後,子進程能夠執行不一樣的代碼段,也可使用 exec
函數執行其它的程序。操作系統
進程在運行的時候,除了加載程序,還會打開文件、佔用一些資源,而且會進入睡眠等其它狀態。操做系統爲了支持進程的運行,必然有一個數據結構保存着這些東西。在 Linux 中,一個名爲 task_struct
的結構保存了進程運行時的全部信息,稱爲進程描述符:線程
struct task_struct { unsigned long state; int prio; pid_t pid; ... }
進程描述符完整描述了一個進程:打開的文件、進程的地址空間、掛起的信號以及進程的信號等。系統將全部的進程描述符放在一個雙端循環列表中:設計
進程描述符具體存放在內存的哪裏呢?在內核棧的末尾。衆所周知,進程中佔用的內存一部分是棧,主要用於函數調用,不過這裏說的棧通常指的是用戶空間的棧,其實進程還有內核棧。當進程調用系統調用的時候,進程陷入內核,此時內核表明進程執行某個操做,此時使用的是內核空間的棧。3d
進程描述符中的 state
描述了進程當前的狀態,有以下 5 種:
在使用了寫時拷貝後,fork
的實際開銷就是複製父進程的頁表以及給子進程建立惟一的進程描述符。fork
爲了建立一個進程到底作了什麼呢?fork
其實調用了 clone
,這是一個系統調用,經過給 clone
傳遞參數,代表父子進程須要共享的資源,clone
內部會調用 do_fork
,而 do_fork
的主要邏輯在 copy_process
中,大體有如下幾步:
clone
的參數,拷貝或者共享打開的文件、文件系統信息、信號處理函數以及進程的地址空間等。除了 fork
以外,Linux 還有一個相似的函數 vfork
。它的功能與 vfork
相同,子進程在父進程的地址空間運行。不過,父進程會阻塞,直到子進程退出或者執行 exec
。須要注意的是,子進程不能向地址空間寫入數據。若是子進程修改數據、進行函數調用或者沒有調用 exec
那麼會帶來未知的結果。vfork
在 fork
沒有寫時拷貝的技術時是有着性能優點,如今已經沒有太大的意義。
進程的運行終有退出的時候,有 8 種方式使進程終止,其中 5 中爲正常終止:
異常終止方式有 3 種:
exit
函數會執行標準 I/O 庫的清理關閉操做:對全部打開的流調用 fclose
函數,全部緩衝中的數據會被沖洗,而 _exit
會直接陷入內核。看下面的代碼:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { printf("line 1\n"); printf("line 2"); // 沒有換行符 // exit(0) _exit(0); }
其中第二行輸出沒有 \n
,若是末尾調用的是 _exit
,則只會輸出 line 1
,若是替換爲 exit
,則第二行 line 2
也會輸出。
進程退出最終會執行到系統的 do_exit
函數,主要有如下步驟:
此時,進程的大部分資源都被釋放了,而且不會進入運行狀態。不過還有些資源保持着,主要是 task_struct 結構。之因此要留着是給父進程提供信息,讓父進程知道子進程的一些信息,如退出碼等。
須要注意的是,若是父進程不進行任何操做,那麼這些信息會一直保留在內存中,成爲殭屍進程,佔用系統資源,以下面的代碼:
int main() { pid_t pid = fork(); if (pid == 0) { exit(0); } else { sleep(10); } }
父進程 fork 出子進程後,子進程馬上退出,而父進程則進入睡眠。運行程序,觀察進程狀態:
能夠看到,第一行進程爲父進程,狀態爲 S
,表示其正在睡眠,而第二爲子進程,狀態爲 Z
,表示殭屍狀態(zombie
),由於此時子進程已經退出,然而 task_struct 還保存着,等待父進程來處理。
父進程如何處理?調用 wait
函數,正如本文第一段代碼中所示。當父進程調用 wait
後,子進程的 task_struct 才被釋放。
若是父進程先結束了呢?在父進程結束的時候,會爲其子進程找新的父進程,一直往上找,最終成爲 init
進程的子進程。init
子進程會負責調用 wait
釋放子進程的遺留信息。
上面介紹了 Linux 中的進程,那麼線程又是怎麼的?網上一些說法是,Linux 中並無真正的內核線程,線程是以進程的方式實現的,只不過它們之間會共享內存。這種說法有必定道理,但並不徹底準確。
Linux 中剛開始是不支持線程的,後來出現了線程庫 LinuxThreads,不過它有不少問題,主要是與 POXIS 標準不兼容。自 Linux 2.6 以來,Linux 中使用的就是新的線程庫,NPTL(Native POSIX Thread Library)。
NPTL 中線程的建立也是經過 clone
實現的,而且經過如下的參數代表了線程的特徵:
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
部分參數的含義以下:
NPTL 所實現的線程庫是 1:1 的從用戶線程映射到內核線程,而且內核爲了實現 POSIX 的線程標準也作了一些改動,好比對於信號的處理等。因此說 Linux 內核徹底不區分進程和線程,甚至不知道線程的存在這種說法如今是不許確的。
線程間共享代碼段、堆以及打開的文件等,線程私有的部分有如下內容:
Linux 中進程與線程的使用是程序員必備的技能,而若是能瞭解一些實現的原理,則可使用的更加駕輕就熟。本文介紹了 Linux 中進程的建立、執行以及消亡等,對於線程的實現及其與進程的關係也進行了簡單的說明。進程和線程還有更多的內容能夠研究,如進程調度、進程以及線程間的通訊等。
參考