從 0 開始學(xué)習(xí) Linux 系列之「17.進(jìn)程控制」

進(jìn)程控制

版權(quán)聲明:本文為 cdeveloper 原創(chuàng)文章晨仑,可以隨意轉(zhuǎn)載藕帜,但必須在明確位置注明出處意乓!

前言

這篇文章主要介紹 Linux 系統(tǒng)中進(jìn)程控制相關(guān)的 API樱调,包括創(chuàng)建,執(zhí)行届良,終止等操作笆凌。基本的進(jìn)程相關(guān)的概念在上一篇文章中已經(jīng)介紹過了士葫,不太熟悉的可以再回去了解了解乞而。

進(jìn)程標(biāo)識

每個進(jìn)程都有一個非負(fù)整數(shù)表示的唯一的進(jìn)程 ID,因為進(jìn)程 ID 標(biāo)識符總是唯一的慢显,所以常常把 ID 作為其他標(biāo)識符的一部分用來保證唯一性爪模,并且進(jìn)程的 ID 也是可以復(fù)用的,當(dāng)一個進(jìn)程終止后荚藻,其進(jìn)程 ID 就可能被其他剛創(chuàng)建的進(jìn)程所使用。

Linux 系統(tǒng)提供了一些獲取進(jìn)程 ID 的函數(shù):

#include <sys/types.h>
#include <unistd.h>

// 得到當(dāng)前進(jìn)程 ID
pid_t getpid(void);

// 得到父進(jìn)程 ID
pid_t getppid(void);

這兩個函數(shù)比較簡單,就是直接調(diào)用然后輸出即可刺下,可以自己嘗試輸出試試树埠。

進(jìn)程的 system 接口

我們在 Windows 下有 system 接口可以使用,例如打開記事本:

system("notepad");

同樣在 Linux 下也有這個接口疾呻,可以執(zhí)行相關(guān)的程序:

#include <stdlib.h>

// 調(diào)用 fork 函數(shù)執(zhí)行 command 命令
int system(const char *command);

例如除嘹,使用 system 接口來執(zhí)行 ls 命令:

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 調(diào)用 ls
    system("ls");
    return 0;
}

system 接口底層其實還是使用系統(tǒng)調(diào)用 fork,exec岸蜗,waitpid 來執(zhí)行程序尉咕,只是在上層又封裝了一次。

創(chuàng)建進(jìn)程 fork

fork 的定義

在 Linux 中散吵,我們使用 fork 來創(chuàng)建一個子進(jìn)程:

#include <unistd.h>

pid_t fork(void);

fork 的返回值

fork 函數(shù)有些特殊龙考,成功它返回 2 次蟆肆,失敗返回 -1,利用這個特性可以判斷當(dāng)前的進(jìn)程是子進(jìn)程還是父進(jìn)程

  1. 在子進(jìn)程中返回 0
  2. 在父進(jìn)程中返回子進(jìn)程的進(jìn)程 ID
// test_fork1.c

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (-1 == pid) 
        perror("fork fail");
    else if (0 == pid) // 子進(jìn)程返回 0
        printf("I'm child process: %d\n", getpid());
    else // 父進(jìn)程返回子進(jìn)程 PID
        printf("I'm parent process晦款,fork return is: %d\n", pid);

    return 0;
}

我們編譯運(yùn)行看看效果:

# 編譯
gcc test_fork1.c -o test_fork1

# 運(yùn)行
./test_fork1
I'm parent process炎功,fork return is: 12557
I'm child process: 12557

可以看到父進(jìn)程的返回值是子進(jìn)程的 PID:12557,子進(jìn)程的 PID 正是 12557缓溅,這也驗證了 fork 的返回值的特點蛇损。

fork 的寫時復(fù)制技術(shù)

通過執(zhí)行 fork,子進(jìn)程得到父進(jìn)程的一個副本坛怪,例如子進(jìn)程獲得父進(jìn)程的數(shù)據(jù)空間淤齐,堆和棧的副本,但是它們并不共享存儲空間袜匿,它們只共享代碼段更啄。但是在現(xiàn)在的系統(tǒng)實現(xiàn)中,并不執(zhí)行拷貝父進(jìn)程的副本居灯,作為替代方案祭务,而是使用寫時復(fù)制(Copy - On - Write)技術(shù)。

寫時復(fù)制:在 fork 之后怪嫌,這些區(qū)域由父子進(jìn)程共享义锥,而且內(nèi)核將它們的訪問權(quán)限改變?yōu)橹蛔x,如果父子進(jìn)程中的任何一個試圖修改這些區(qū)域岩灭,內(nèi)核只為修改區(qū)域的那片內(nèi)存制作一個副本給子進(jìn)程拌倍。

不管是哪種技術(shù)實現(xiàn),最后父子進(jìn)程的數(shù)據(jù)都是獨立的噪径,不會相互影響柱恤,我們來看一個實際的例子:

// test_fork2.c

#include <stdio.h>
#include <unistd.h>

int main() {
    int count = 0;

    // 創(chuàng)建子進(jìn)程
    pid_t pid = fork();
    
    // 父子進(jìn)程中都有這個變量
    count++;

    if (-1 == pid)
        perror("fork fail");
    else if (0 == pid) // 子進(jìn)程 count 變量
        printf("child process count: %d\n", count);
    else // 父進(jìn)程 count 變量
        printf("parent process count: %d\n", count);

    return 0;
}

編譯運(yùn)行:

# 編譯
gcc test_fork2.c -o test_fork2

# 運(yùn)行
./test_fork2
parent process count: 1
child process count: 1

結(jié)果是父子進(jìn)程中的 count = 1,如果父子進(jìn)程的 count 不獨立的話熄云,子進(jìn)程的 count 應(yīng)該等于 2膨更,但是實際上是等于 1,說明父子進(jìn)程的數(shù)據(jù)是獨立的缴允。

子進(jìn)程的執(zhí)行位置

fork 還有一個特點:子進(jìn)程不是從 main 函數(shù)開始執(zhí)行的荚守,而是從 fork 返回的地方開始,我們來看個實際的例子:

// test_fork3.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
    int ret_from_fork = 0;
    int mypid = getpid();

    printf("Before: my pid is %d\n", mypid);
    
    // 創(chuàng)建子進(jìn)程练般,子進(jìn)程從這里返回
    ret_from_fork = fork();

    sleep(1);

    printf("After: my pid is %d, fork() said %d\n", getpid(), ret_from_fork);
    return 0;
}

編譯運(yùn)行結(jié)果:

# 編譯
gcc test_fork3.c -o test_fork3

# 運(yùn)行
./test_fork3
Before: my pid is 9670
After: my pid is 9670, fork() said 9671
After: my pid is 9671, fork() said 0

看到只打印一個 Before 信息矗漾,沒有打印 2 個 Before 原因是:內(nèi)核通過復(fù)制父進(jìn)程 9670 來創(chuàng)建子進(jìn)程 9671,并將父進(jìn)程 9670 代碼和當(dāng)前運(yùn)行到的位置都復(fù)制到子進(jìn)程 9671薄料,所以新的子進(jìn)程 9671 從 fork 返回的地方開始運(yùn)行敞贡,而不是從頭開始,也就不會打印開頭的 Before 了摄职。

創(chuàng)建進(jìn)程 vfork

還有一個創(chuàng)建進(jìn)程的系統(tǒng)調(diào)用 vfork誊役,它跟 fork 很相似获列,但是也有幾點不同:

  1. vfork 的目的是創(chuàng)建一個子進(jìn)程來運(yùn)行一個程序
  2. vfork 并不復(fù)制父進(jìn)程地址空間,子進(jìn)程在父進(jìn)程地址空間中運(yùn)行蛔垢,并阻塞父進(jìn)程直到子進(jìn)程返回
  3. vfork 保證子進(jìn)程先運(yùn)行
  4. 子進(jìn)程需要調(diào)用 exec 或 exit 函數(shù)退出击孩,否則會帶來未知結(jié)果。

來看個實際的例子:

// test_vfork.c 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    int count = 0;
    pid_t pid = vfork();
    count++;

    if (-1 == pid) {
        perror("fork fail");
    } else if (0 == pid) {
        printf("child process count : %d\n", count);
        exit(0);
    } else {
        // 父進(jìn)程會阻塞
        printf("parent process count: %d\n", count);
    }
    
    return 0;
}

編譯運(yùn)行:

# 編譯
gcc test_vfork.c -o test_vfork

# 運(yùn)行
./test_vfork
child process count: 1
parent process count: 2

這個結(jié)果跟前面使用 fork 的例子是不同的鹏漆,使用 vfork 先打印的子進(jìn)程信息巩梢,再打印父進(jìn)程的信息,是因為父進(jìn)程被阻塞了艺玲,直到子進(jìn)程執(zhí)行完了才有機(jī)會執(zhí)行括蝠。并且由于子進(jìn)程在父進(jìn)程的地址空間中運(yùn)行,所以子進(jìn)程中對 count 加一的操作對父進(jìn)程也是有效的饭聚,因此最后父進(jìn)程的 count = 2忌警。

exec

fork 函數(shù)里面最后也是調(diào)用 exec 等函數(shù)來執(zhí)行程序的,我們有必須要了解這個函數(shù):

#include <unistd.h>

// path:程序名稱若治,argv:運(yùn)行參數(shù)
int execv(const char *path, char *const argv[]);

exec 有很多變種函數(shù)慨蓝,例如 execlp感混,execle端幼,等等,但基本的用法都是差不多的弧满,這里就以 execl 為例來看個程序:

#include <stdio.h>
#include <unistd.h>

int main() {
    char* argvs[] = {"ps", "-ef", NULL};
    execv("ps", argvs);
    return 0;
}

運(yùn)行結(jié)果就相當(dāng)與 shell 命令:ps - ef婆跑,其他的變種函數(shù)可以通過 man exec 來查看。

進(jìn)程等待 wait

父進(jìn)程可以使用 wait 系統(tǒng)調(diào)用主動等待子進(jìn)程或者指定進(jìn)程結(jié)束庭呜,并獲得子進(jìn)程的結(jié)束信息

#include <sys/types.h>
#include <sys/wait.h>

// 等待子進(jìn)程結(jié)束
pid_t wait(int *wstatus);

// 等待指定的 PID 進(jìn)程結(jié)束
pid_t waitpid(pid_t pid, int *wstatus, int options);

這個系統(tǒng)調(diào)用的過程如下:

  1. wait 暫停調(diào)用它的進(jìn)程直到子進(jìn)程結(jié)束
  2. wait 調(diào)用成功返回子進(jìn)程的 PID
  3. wstatus 存儲子進(jìn)程的返回信息(正常退出滑进,異常退出,被信號殺死)募谎,以此來知道子進(jìn)程是如何結(jié)束的

大致的流程如下:

F ---fork------> F -------- wait ----> F ------------->
        |                              |
        |                              |
        |                              |
        -------> C -------------exit() -

來看看 wait 是如何使用的:

// test_wait.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    int status = 0;
    pid_t pid = fork();
    if (0 == pid) {
        printf("child process\n");
        sleep(3);
        // 父進(jìn)程會得到 exit 的退出狀態(tài)碼
        exit(0);
    } else if(pid > 0) {
        // 等待子進(jìn)程返回扶关,wait 運(yùn)行成功返回子進(jìn)程 PID
        if (pid == wait(&status))
            printf("child has run ok status = %d, parent processing...\n", status);
    }

    return 0;
}

編譯運(yùn)行:

# 編譯
gcc test_wait.c -o test_wait

# 運(yùn)行
./test_wait
child process
child has run ok status = 0, parent processing...

可以看到父進(jìn)程成功等待了子進(jìn)程 3 s,并得到了存儲在 status 中的返回值数冬,這個 status 有 2 種狀態(tài):

  1. 如果子進(jìn)程調(diào)用 exit 退出节槐,那么內(nèi)核將 exit 的退出狀態(tài)碼放在 status 中
  2. 如果進(jìn)程被殺死,內(nèi)核將信號序列放在 status 中

實際使用時拐纱,wait 提供了相關(guān)的宏來判斷 status 的狀態(tài)铜异,詳情參考 man wait。另外 waitpidwait 幾乎的相同的秸架,作為鍛煉揍庄,就留給你自己去學(xué)習(xí)吧,參考 man waitpid东抹。

進(jìn)程結(jié)束

既然能夠創(chuàng)建進(jìn)程蚂子,那肯定能夠結(jié)束進(jìn)程沃测,在 Linux 中進(jìn)程退出又分為正常和異常退出,分別來了解了解食茎。

正常退出

有 5 種正常退出進(jìn)程的方法:

  1. 在 main 內(nèi)執(zhí)行 return芽突,等價于調(diào)用 exit
  2. 調(diào)用 exit
  3. 調(diào)用 _exit_Exit
  4. 進(jìn)程的最后一個線程在其啟動例程中執(zhí)行 return 語句
  5. 進(jìn)程的最后一個線程調(diào)用 pthread_exit 函數(shù)

異常終止

有 3 種異常終止的方法:

  1. 調(diào)用 abort,產(chǎn)生 SIGABRT 信號
  2. 當(dāng)進(jìn)程接受到某些信號時
  3. 最后一個線程對「取消」請求作出響應(yīng)

不管是哪種終止情況董瞻,我們都可以使用 wait 或者 waitpid 來得到子進(jìn)程的退出狀態(tài)寞蚌。

拓展:在 Linux 內(nèi)核中查看 fork 執(zhí)行流程

為了加深對 Linux 進(jìn)程的理解,下面就來簡單了解下 fork 在內(nèi)核中的具體調(diào)用過程钠糊。建議你用源碼查看工具來跟蹤源碼挟秤,我使用的是 Linux-2.6 的源碼,要跟蹤的文件是 kernel/fork.c抄伍,創(chuàng)建進(jìn)程的總體過程如下圖所示:

fork

總體的流程是創(chuàng)建一個新的任務(wù)(task_struct)艘刚,然后拷貝相關(guān)的進(jìn)程信息,最后喚醒這個進(jìn)程和后續(xù)的準(zhǔn)備工作截珍,其中最重要的是 copy_process 這個函數(shù)攀甚,來看看它的具體執(zhí)行過程:

copy_process

主要的流程就是先使用 dup_task_struct 復(fù)制一個進(jìn)程結(jié)構(gòu),然后初始化這個進(jìn)程信息岗喉,再拷貝父進(jìn)程的相關(guān)信息秋度,最后 sched_fork 調(diào)度進(jìn)程。

整個過程大體上就是這樣钱床,具體的細(xì)節(jié)有興趣可以深入的跟蹤荚斯,這里就介紹這些了。

結(jié)語

本次我們學(xué)習(xí)了 Linux 中非常重要的進(jìn)程控制查牌,其中非常重要的是 fork 這個函數(shù)事期,因為我們就是用這個函數(shù)來創(chuàng)建進(jìn)程的,另外我們也在內(nèi)核中分析了 fork 的具體實現(xiàn)過程纸颜,希望通過這篇文章能夠讓你對 Linux 的進(jìn)程有一個更加完整的了解和學(xué)習(xí)兽泣,希望你能認(rèn)真實踐。

最后胁孙,感謝你的閱讀唠倦,我們下次再見 :)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浊洞,隨后出現(xiàn)的幾起案子牵敷,更是在濱河造成了極大的恐慌,老刑警劉巖法希,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枷餐,死亡現(xiàn)場離奇詭異,居然都是意外死亡苫亦,警方通過查閱死者的電腦和手機(jī)毛肋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門怨咪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人润匙,你說我怎么就攤上這事诗眨。” “怎么了孕讳?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵匠楚,是天一觀的道長。 經(jīng)常有香客問我厂财,道長芋簿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任璃饱,我火速辦了婚禮与斤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘荚恶。我一直安慰自己撩穿,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布谒撼。 她就那樣靜靜地躺著食寡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嗤栓。 梳的紋絲不亂的頭發(fā)上冻河,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天,我揣著相機(jī)與錄音茉帅,去河邊找鬼。 笑死锭弊,一個胖子當(dāng)著我的面吹牛堪澎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播味滞,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼樱蛤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了剑鞍?” 一聲冷哼從身側(cè)響起昨凡,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚁署,沒想到半個月后便脊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡光戈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年哪痰,在試婚紗的時候發(fā)現(xiàn)自己被綠了遂赠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡晌杰,死狀恐怖跷睦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肋演,我是刑警寧澤抑诸,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站爹殊,受9級特大地震影響哼鬓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜边灭,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一异希、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绒瘦,春花似錦称簿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至该酗,卻和暖如春授药,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呜魄。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工悔叽, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爵嗅。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓娇澎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親睹晒。 傳聞我的和親對象是個殘疾皇子趟庄,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,562評論 2 349

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