簡介
進(jìn)程與線程是所有的程序員都熟知的概念待诅,簡單來說進(jìn)程是一個執(zhí)行中的程序卑雁,而線程是進(jìn)程中的一條執(zhí)行路徑绪囱。進(jìn)程是操作系統(tǒng)中基本的抽象概念,本文介紹 Linux 中進(jìn)程和線程的用法以及原理扣甲,包括創(chuàng)建齿椅、消亡等涣脚。
進(jìn)程
創(chuàng)建與執(zhí)行
Linux 中進(jìn)程的創(chuàng)建與執(zhí)行分為兩個函數(shù)涩澡,分別是 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
從當(dāng)前進(jìn)程創(chuàng)建一個子進(jìn)程胰耗,此函數(shù)返回兩次柴灯,對于父進(jìn)程而言赠群,返回的是子進(jìn)程的進(jìn)程號查描,對于子進(jìn)程而言返回 0。子進(jìn)程是父進(jìn)程的副本冬三,擁有與父進(jìn)程一樣的數(shù)據(jù)空間匀油、堆和棧的副本勾笆,并且共享代碼段。
由于子進(jìn)程通常是為了調(diào)用 exec
裝載其它程序執(zhí)行窝爪,所以 Linux 采用了寫時拷貝技術(shù)钝侠,即數(shù)據(jù)段酸舍、堆和棧的副本并不會在 fork
之后就真的拷貝,只是將這些內(nèi)存區(qū)域的訪問權(quán)限變?yōu)橹蛔x里初,如果父子進(jìn)程中有任一個要修改這些區(qū)域,才會修改對應(yīng)的內(nèi)存頁生成新的副本双妨,這樣子是為了提高性能刁品。
fork
之后父進(jìn)程先執(zhí)行還是子進(jìn)程先執(zhí)行是不確定的挑随,所以如果要求父子進(jìn)程進(jìn)行同步兜挨,往往需要使用進(jìn)程間通信膏孟。fork
之后子進(jìn)程會繼承父進(jìn)程的很多東西,如:
- 打開的文件
- 實(shí)際用戶 ID噪舀、組用戶 ID 等
- 進(jìn)程組
- 當(dāng)前工作目錄
- 信號屏蔽和安排
- ...
父子進(jìn)程的區(qū)別在于:
- 進(jìn)程 ID 不同
- 子進(jìn)程不繼承父進(jìn)程的文件鎖
- 子進(jìn)程的未處理信號集為空
- ...
fork
之后魁淳,子進(jìn)程可以執(zhí)行不同的代碼段飘诗,也可以使用 exec
函數(shù)執(zhí)行其它的程序。
進(jìn)程描述符
進(jìn)程在運(yùn)行的時候界逛,除了加載程序昆稿,還會打開文件、占用一些資源仇奶,并且會進(jìn)入睡眠等其它狀態(tài)貌嫡。操作系統(tǒng)為了支持進(jìn)程的運(yùn)行,必然有一個數(shù)據(jù)結(jié)構(gòu)保存著這些東西该溯。在 Linux 中岛抄,一個名為 task_struct
的結(jié)構(gòu)保存了進(jìn)程運(yùn)行時的所有信息,稱為進(jìn)程描述符:
struct task_struct {
unsigned long state;
int prio;
pid_t pid;
...
}
進(jìn)程描述符完整描述了一個進(jìn)程:打開的文件狈茉、進(jìn)程的地址空間夫椭、掛起的信號以及進(jìn)程的信號等。系統(tǒng)將所有的進(jìn)程描述符放在一個雙端循環(huán)列表中:
進(jìn)程描述符具體存放在內(nèi)存的哪里呢氯庆?在內(nèi)核棧的末尾蹭秋。眾所周知,進(jìn)程中占用的內(nèi)存一部分是棧堤撵,主要用于函數(shù)調(diào)用仁讨,不過這里說的棧一般指的是用戶空間的棧,其實(shí)進(jìn)程還有內(nèi)核棧实昨。當(dāng)進(jìn)程調(diào)用系統(tǒng)調(diào)用的時候洞豁,進(jìn)程陷入內(nèi)核,此時內(nèi)核代表進(jìn)程執(zhí)行某個操作荒给,此時使用的是內(nèi)核空間的棧丈挟。
進(jìn)程狀態(tài)
進(jìn)程描述符中的 state
描述了進(jìn)程當(dāng)前的狀態(tài),有如下 5 種:
- TASK_RUNNING:進(jìn)程是可執(zhí)行的志电,此時進(jìn)程要么是正在執(zhí)行曙咽,要么是在運(yùn)行隊(duì)列中等待被調(diào)度
- TASK_INTERRUPTIBLE:進(jìn)程正在睡眠(阻塞),等待條件達(dá)成挑辆。如果條件達(dá)成或者收到信號例朱,進(jìn)程會被喚醒并且進(jìn)入可運(yùn)行狀態(tài)
- TASK_UNINTERRUPTIBLE:進(jìn)程處于不可中斷狀態(tài),就算信號也無法喚醒鱼蝉,這種狀態(tài)用的比較少
- _TASK_TRACED:進(jìn)程正在被其它進(jìn)程追蹤茉继,通常是為了調(diào)試
- _TASK_STOPPED:進(jìn)程停止運(yùn)行,通常是接收到 SIGINT蚀乔、SIGTSTP 信號的時候烁竭。
fork 與 vfork
在使用了寫時拷貝后,fork
的實(shí)際開銷就是復(fù)制父進(jìn)程的頁表以及給子進(jìn)程創(chuàng)建唯一的進(jìn)程描述符吉挣。fork
為了創(chuàng)建一個進(jìn)程到底做了什么呢派撕?fork
其實(shí)調(diào)用了 clone
婉弹,這是一個系統(tǒng)調(diào)用,通過給 clone
傳遞參數(shù)终吼,表明父子進(jìn)程需要共享的資源镀赌,clone
內(nèi)部會調(diào)用 do_fork
,而 do_fork
的主要邏輯在 copy_process
中际跪,大致有以下幾步:
- 為新進(jìn)程創(chuàng)建一個內(nèi)核棧以及 task_struct商佛,此時它們的值與父進(jìn)程相同
- 將 task_struct 中某些變量,如統(tǒng)計(jì)信息姆打,設(shè)置為 0
- 將子進(jìn)程狀態(tài)設(shè)置為 TASK_UNINTERRUPTIBLE良姆,保證它不會被投入運(yùn)行
- 分配 pid
- 根據(jù)傳遞給
clone
的參數(shù),拷貝或者共享打開的文件幔戏、文件系統(tǒng)信息玛追、信號處理函數(shù)以及進(jìn)程的地址空間等。 - 返回指向子進(jìn)程的指針
除了 fork
之外闲延,Linux 還有一個類似的函數(shù) vfork
痊剖。它的功能與 vfork
相同,子進(jìn)程在父進(jìn)程的地址空間運(yùn)行垒玲。不過陆馁,父進(jìn)程會阻塞,直到子進(jìn)程退出或者執(zhí)行 exec
合愈。需要注意的是叮贩,子進(jìn)程不能向地址空間寫入數(shù)據(jù)。如果子進(jìn)程修改數(shù)據(jù)想暗、進(jìn)行函數(shù)調(diào)用或者沒有調(diào)用 exec
那么會帶來未知的結(jié)果。vfork
在 fork
沒有寫時拷貝的技術(shù)時是有著性能優(yōu)勢帘不,現(xiàn)在已經(jīng)沒有太大的意義说莫。
退出
進(jìn)程的運(yùn)行終有退出的時候,有 8 種方式使進(jìn)程終止寞焙,其中 5 中為正常終止:
- 從 main 返回
- 調(diào)用 exit
- 調(diào)用 _exit 或 _Exit
- 最后一個線程從其啟動例程返回
- 從最后一個線程調(diào)用 pthread_exit
異常終止方式有 3 種:
- 調(diào)用 abort
- 接收到一個信號
- 最后一個線程對取消請求作出響應(yīng)
exit
函數(shù)會執(zhí)行標(biāo)準(zhǔn) I/O 庫的清理關(guān)閉操作:對所有打開的流調(diào)用 fclose
函數(shù)储狭,所有緩沖中的數(shù)據(jù)會被沖洗,而 _exit
會直接陷入內(nèi)核捣郊×杀罚看下面的代碼:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("line 1\n");
printf("line 2"); // 沒有換行符
// exit(0)
_exit(0);
}
其中第二行輸出沒有 \n
,如果末尾調(diào)用的是 _exit
呛牲,則只會輸出 line 1
刮萌,如果替換為 exit
,則第二行 line 2
也會輸出娘扩。
進(jìn)程退出最終會執(zhí)行到系統(tǒng)的 do_exit
函數(shù)着茸,主要有以下步驟:
- 刪除進(jìn)程定時器
- 釋放進(jìn)程占用的頁表
- 遞減文件描述符的引用計(jì)數(shù)壮锻,如果某個引用計(jì)數(shù)為 0,則關(guān)閉文件
- 向父進(jìn)程發(fā)信號涮阔,給子進(jìn)程重新找養(yǎng)父猜绣,并且把進(jìn)程狀態(tài)設(shè)置為 EXIT_ZOMBIE
- 調(diào)度其它進(jìn)程
此時,進(jìn)程的大部分資源都被釋放了敬特,并且不會進(jìn)入運(yùn)行狀態(tài)掰邢。不過還有些資源保持著,主要是 task_struct 結(jié)構(gòu)伟阔。之所以要留著是給父進(jìn)程提供信息辣之,讓父進(jìn)程知道子進(jìn)程的一些信息,如退出碼等减俏。
需要注意的是召烂,如果父進(jìn)程不進(jìn)行任何操作,那么這些信息會一直保留在內(nèi)存中娃承,成為僵尸進(jìn)程奏夫,占用系統(tǒng)資源,如下面的代碼:
int main() {
pid_t pid = fork();
if (pid == 0) {
exit(0);
} else {
sleep(10);
}
}
父進(jìn)程 fork 出子進(jìn)程后历筝,子進(jìn)程立刻退出酗昼,而父進(jìn)程則進(jìn)入睡眠。運(yùn)行程序梳猪,觀察進(jìn)程狀態(tài):
可以看到麻削,第一行進(jìn)程為父進(jìn)程,狀態(tài)為 S
春弥,表示其正在睡眠呛哟,而第二為子進(jìn)程,狀態(tài)為 Z
匿沛,表示僵尸狀態(tài)(zombie
)扫责,因?yàn)榇藭r子進(jìn)程已經(jīng)退出,然而 task_struct 還保存著逃呼,等待父進(jìn)程來處理鳖孤。
父進(jìn)程如何處理?調(diào)用 wait
函數(shù)抡笼,正如本文第一段代碼中所示苏揣。當(dāng)父進(jìn)程調(diào)用 wait
后,子進(jìn)程的 task_struct 才被釋放推姻。
如果父進(jìn)程先結(jié)束了呢平匈?在父進(jìn)程結(jié)束的時候,會為其子進(jìn)程找新的父進(jìn)程,一直往上找吐葱,最終成為 init
進(jìn)程的子進(jìn)程街望。init
子進(jìn)程會負(fù)責(zé)調(diào)用 wait
釋放子進(jìn)程的遺留信息。
線程
上面介紹了 Linux 中的進(jìn)程弟跑,那么線程又是怎么的灾前?網(wǎng)上一些說法是,Linux 中并沒有真正的內(nèi)核線程孟辑,線程是以進(jìn)程的方式實(shí)現(xiàn)的哎甲,只不過它們之間會共享內(nèi)存。這種說法有一定道理饲嗽,但并不完全準(zhǔn)確炭玫。
Linux 中剛開始是不支持線程的,后來出現(xiàn)了線程庫 LinuxThreads貌虾,不過它有很多問題吞加,主要是與 POXIS 標(biāo)準(zhǔn)不兼容。自 Linux 2.6 以來尽狠,Linux 中使用的就是新的線程庫衔憨,NPTL(Native POSIX Thread Library)。
NPTL 中線程的創(chuàng)建也是通過 clone
實(shí)現(xiàn)的袄膏,并且通過以下的參數(shù)表明了線程的特征:
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS |
CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
部分參數(shù)的含義如下:
- CLONE_VM:所有線程都共享同一個進(jìn)程地址空間
- CLONE_FILES:所有線程都共享進(jìn)程的文件描述符列表
- CLONE_THREAD:所有線程都共享同一個進(jìn)程 ID 以及 父進(jìn)程 ID
NPTL 所實(shí)現(xiàn)的線程庫是 1:1 的從用戶線程映射到內(nèi)核線程践图,并且內(nèi)核為了實(shí)現(xiàn) POSIX 的線程標(biāo)準(zhǔn)也做了一些改動,比如對于信號的處理等沉馆。所以說 Linux 內(nèi)核完全不區(qū)分進(jìn)程和線程码党,甚至不知道線程的存在這種說法現(xiàn)在是不準(zhǔn)確的。
線程間共享代碼段斥黑、堆以及打開的文件等揖盘,線程私有的部分有以下內(nèi)容:
- 線程 ID
- 寄存器
- 錯誤碼(errno)
- 棧
- 信號屏蔽
- ...
總結(jié)
Linux 中進(jìn)程與線程的使用是程序員必備的技能,而如果能了解一些實(shí)現(xiàn)的原理锌奴,則可以使用的更加得心應(yīng)手兽狭。本文介紹了 Linux 中進(jìn)程的創(chuàng)建、執(zhí)行以及消亡等缨叫,對于線程的實(shí)現(xiàn)及其與進(jìn)程的關(guān)系也進(jìn)行了簡單的說明椭符。進(jìn)程和線程還有更多的內(nèi)容可以研究荔燎,如進(jìn)程調(diào)度耻姥、進(jìn)程以及線程間的通信等。
參考
- 《UNIX 環(huán)境高級編程》
- 《Linux 內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》