淺析 Linux 進(jìn)程與線程

簡介

進(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ù)涩澡,分別是 forkexec妙同,如下代碼所示:

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 種:

  1. TASK_RUNNING:進(jìn)程是可執(zhí)行的志电,此時進(jìn)程要么是正在執(zhí)行曙咽,要么是在運(yùn)行隊(duì)列中等待被調(diào)度
  2. TASK_INTERRUPTIBLE:進(jìn)程正在睡眠(阻塞),等待條件達(dá)成挑辆。如果條件達(dá)成或者收到信號例朱,進(jìn)程會被喚醒并且進(jìn)入可運(yùn)行狀態(tài)
  3. TASK_UNINTERRUPTIBLE:進(jìn)程處于不可中斷狀態(tài),就算信號也無法喚醒鱼蝉,這種狀態(tài)用的比較少
  4. _TASK_TRACED:進(jìn)程正在被其它進(jìn)程追蹤茉继,通常是為了調(diào)試
  5. _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 中际跪,大致有以下幾步:

  1. 為新進(jìn)程創(chuàng)建一個內(nèi)核棧以及 task_struct商佛,此時它們的值與父進(jìn)程相同
  2. 將 task_struct 中某些變量,如統(tǒng)計(jì)信息姆打,設(shè)置為 0
  3. 將子進(jìn)程狀態(tài)設(shè)置為 TASK_UNINTERRUPTIBLE良姆,保證它不會被投入運(yùn)行
  4. 分配 pid
  5. 根據(jù)傳遞給 clone 的參數(shù),拷貝或者共享打開的文件幔戏、文件系統(tǒng)信息玛追、信號處理函數(shù)以及進(jìn)程的地址空間等。
  6. 返回指向子進(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é)果。vforkfork 沒有寫時拷貝的技術(shù)時是有著性能優(yōu)勢帘不,現(xiàn)在已經(jīng)沒有太大的意義说莫。

退出

進(jìn)程的運(yùn)行終有退出的時候,有 8 種方式使進(jìn)程終止寞焙,其中 5 中為正常終止:

  1. 從 main 返回
  2. 調(diào)用 exit
  3. 調(diào)用 _exit 或 _Exit
  4. 最后一個線程從其啟動例程返回
  5. 從最后一個線程調(diào)用 pthread_exit

異常終止方式有 3 種:

  1. 調(diào)用 abort
  2. 接收到一個信號
  3. 最后一個線程對取消請求作出響應(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ù)着茸,主要有以下步驟:

  1. 刪除進(jìn)程定時器
  2. 釋放進(jìn)程占用的頁表
  3. 遞減文件描述符的引用計(jì)數(shù)壮锻,如果某個引用計(jì)數(shù)為 0,則關(guān)閉文件
  4. 向父進(jìn)程發(fā)信號涮阔,給子進(jìn)程重新找養(yǎng)父猜绣,并且把進(jìn)程狀態(tài)設(shè)置為 EXIT_ZOMBIE
  5. 調(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)》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末有咨,一起剝皮案震驚了整個濱河市琐簇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖婉商,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件似忧,死亡現(xiàn)場離奇詭異,居然都是意外死亡丈秩,警方通過查閱死者的電腦和手機(jī)盯捌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蘑秽,“玉大人饺著,你說我怎么就攤上這事〕ι” “怎么了幼衰?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缀雳。 經(jīng)常有香客問我渡嚣,道長,這世上最難降的妖魔是什么肥印? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任识椰,我火速辦了婚禮,結(jié)果婚禮上竖独,老公的妹妹穿的比我還像新娘裤唠。我一直安慰自己,他們只是感情好莹痢,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布种蘸。 她就那樣靜靜地躺著,像睡著了一般竞膳。 火紅的嫁衣襯著肌膚如雪航瞭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天坦辟,我揣著相機(jī)與錄音刊侯,去河邊找鬼。 笑死锉走,一個胖子當(dāng)著我的面吹牛滨彻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挪蹭,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼亭饵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梁厉?” 一聲冷哼從身側(cè)響起辜羊,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后八秃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碱妆,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年昔驱,在試婚紗的時候發(fā)現(xiàn)自己被綠了疹尾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡骤肛,死狀恐怖航棱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情萌衬,我是刑警寧澤饮醇,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站秕豫,受9級特大地震影響朴艰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜混移,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一祠墅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧歌径,春花似錦毁嗦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茵肃,卻和暖如春腔长,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背验残。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工捞附, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人您没。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓鸟召,卻偏偏與公主長得像,于是被迫代替她去往敵國和親氨鹏。 傳聞我的和親對象是個殘疾皇子欧募,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容