UNIX高級環境編程(9)進程控制(Process Control)- fork,vfork,殭屍進程,wait和waitpid

本章包含內容有:html

  • 建立新進程
  • 程序執行(program execution)
  • 進程終止(process termination)
  • 進程的各類ID

 

1 進程標識符(Process Identifiers)

每一個進程都有一個惟一的標識符,進程ID(process ID)。面試

進程的ID是可重用的,若是一個進程被終止,那麼它的進程ID會被系統回收,可是會延遲使用,防止該進程ID標識的新進程被誤認爲是之前的進程。緩存

三個特殊ID的進程:app

  • Process ID 0:調度者進程,內核進程。
  • Process ID 1:init進程,內核引導程序最後啓動,負責啓動Unix系統。對應系統文件/sbin/init。
  • Process ID 2:pagedaemon,負責虛擬內存的頁管理。

獲取進程各類ID的相關函數:函數

函數聲明:ui

#include <unistd.h>spa

pid_t getpid(void);     // Returns: process ID of calling processunix

pid_t getppid(void);        // Returns: parent process ID of calling process指針

uid_t getuid(void);        // Returns: real user ID of calling processhtm

uid_t geteuid(void);       // Returns: effective user ID of calling process

gid_t getgid(void);        // Returns: real group ID of calling process

gid_t getegid(void);        // Returns: effective group ID of calling process

這裏的各類ID在前面第三篇中有說明,http://www.cnblogs.com/suzhou/p/4295535.html

 

2 fork函數

fork函數用於一個已存在的進程建立一個新的進程。

函數聲明:

#include <unistd.h>

pid_t fork(void);

函數細節:

  1. 建立的新進程叫作子進程,子進程是父進程的一個拷貝,拷貝數據段,堆和棧,而共享文本段。
  2. 該函數調用一次,可是返回兩次(父進程和子進程各返回一次,子進程返回0,父進程返回子進程的進程號)。這樣設置的緣由是:父進程能夠有多個子進程,父進程沒有方法獲取子進程的進程號,而子進程只可能有一個父進程,而且能夠經過getppid方法獲取父進程的進程號。
  3. 寫時複製(copy-on-write)機制:子進程剛建立,在只讀的狀況下和父進程共享數據段、堆和棧。若是子進程或者父進程試着修改這些數據,內核會進程這些數據的拷貝。
  4. 咱們沒法判斷子進程和父進程的執行順序,這取決於系統的調度順序。

Example:

#include "apue.h"

 

int     globvar = 6;        /* external variable in initialized data */

char    buf[] = "a write to stdout\n";

 

int

main(void)

{

    int     var;        /* automatic variable on the stack */

    pid_t   pid;

 

    var = 88;

    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)

        err_sys("write error");

    printf("before fork\n");    /* we don't flush stdout */

 

    if ((pid = fork()) < 0) {

        err_sys("fork error");

    } else if (pid == 0) {      /* child */

        globvar++;              /* modify variables */

        var++;

    } else {

        sleep(2);               /* parent */

    }

 

    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,

      var);

    exit(0);

}

執行結果:

NewImage

pid爲12291的進程爲子進程,對變量glob和var進行了加1。

當把輸出重定向到一個文件時,咱們發現結果和直接輸出到終端中不太同樣:

NewImage

緣由: 

  • 函數write不使用緩存,因此在系統調用fork以前調用write,結果直接輸出到標準輸出上;
  • 而標準輸出若是鏈接到終端,則是行緩衝(line buffered),不然是全緩衝(full buffered);
  • 在第一個例子中,換行致使printf寫入到標準輸出中的數據flush到終端上(行緩衝,換新行,致使前面一行被打印);
  • 在第二個例子中,咱們將標準輸出重定向到文件,則使用全緩衝,printf的數據被緩存在buffer中沒有被打印,在fork時,buffer一樣被拷貝了一份,這樣父子進程都有了一個標準IO緩存(standard IO buffer);
  • 程序中的第二個printf將新的內從append到buffer中已有數據的後面,一同打印出,就看到了第二個例子中打印的結果。

 

文件共享(File Sharing)

當調用fork函數時,父進程的全部打開的文件描述符都會複製一份到子進程中,包括文件偏移量(file offset)。

因此當父子進程同時寫文件時,他們的操做都會更新同一個文件偏移量(file offset),加入子進程向文件中寫入了一部分數據,同時更新了file offset,那麼父進程進行寫入操做時,會使用跟新之後的offset,從而避免了覆蓋了子進程寫入的數據。

父子進程共享文件以下圖所示:

NewImage

咱們能夠發現,父子進程擁有相同的文件描述符,又沒有其餘的同步方式,因此他們的輸出可能會混起來(intermixed)。

fork以後,常見的處理父子進程擁有的文件描述符有兩種方式:

  • 父進程等待子進程完成。
  • 父子進程各自工做,關閉不須要的文件描述符。

除了打開的文件描述,其餘的子進程會繼承自父進程的內容包括:

NewImage

父子進程不一樣的地方包括:

  • fork的返回值不一樣
  • 進程ID不一樣
  • 進程的父進程ID不一樣
  • 子進程的tms_utime, tms_stime, itms_cutime和itms_cstime值被置爲0
  • 父進程的文件鎖不會被子進程繼承
  • 子進程的pending signals被置空

 

3 vfork

vfork和fork有相同的返回值。

vfork和fork的不一樣點:

  • 函數目的:vfork建立的子進程是爲了讓子進程執行一個新的程序
  • 複製操做:不復制父進程的地址空間,而是直接運行在父進程的地址空間中,直到子進程調用exec或者exit
  • 效率:因此vfork的執行效率比fork要高,由於它沒有copy操做
  • 不肯定的結果:可是若是子進程修改了數據、調用函數或者沒有調用exec和exit方法,則會形成不肯定的結果
  • 子進程先運行:vfork保證子進程先運行

 Example:

#include "apue.h"

 

int     globvar = 6;        /* external variable in initialized data */

 

int

main(void)

{

    int     var;        /* automatic variable on the stack */

    pid_t   pid;

 

    var = 88;

    printf("before vfork\n");   /* we don't flush stdio */

    if ((pid = vfork()) < 0) {

        err_sys("vfork error");

    } else if (pid == 0) {      /* child */

        globvar++;              /* modify parent's variables */

        var++;

        _exit(0);               /* child terminates */

    }

 

    /* parent continues here */

    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,

      var);

 

    exit(0);

}

運行結果:

NewImage

 

4 進程退出和殭屍進程

正常退出:三個函數exit, 

若是子進程不正常退出,則內核保證記錄該進程的異常退出狀態,該進程的父進程能夠經過調用wait或者waitpid函數獲取該子進程的異常退出狀態。

若是父進程在子進程以前終止,則init進程成爲該子進程的父進程。從而保證每一個進程都有父進程。

若是子進程先終止(異常終止或者正常退出),內核會保存該子進程的部分信息,包括進程pid,進程終止時的狀態和該進程佔用的CPU時間,同時內核會清除該進程佔用的內存,關閉全部已經打開的文件描述符。父進程能夠經過檢查該信息獲取子進程的終止狀況。

若是子進程先終止,而沒有父進程調用waitpid獲取該子進程的信息,那麼這種進程被成爲殭屍進程。使用ps命令能夠看到殭屍進程的相關信息。

若是父進程爲init進程,那麼子進程異常終止並不會成爲殭屍進程,由於init進程會對它的全部子進程調用wait函數獲取子進程的終止狀態。

 

5 wait和waitpid函數

子進程終止,內核會向父進程發送SIGCHLD信號。父進程默認的行爲是忽略該信號,父進程也能夠設置一個信號處理函數,當捕捉到該信號時,調用該處理函數,在後面的相關章節會介紹信號相關的概念。

本節介紹的wait和waitpid函數的做用是:

  • 若是子進程在運行,則阻塞;
  • 若是子進程終止,而且子進程的終止狀態被父進程獲取,則該函數馬上返回該終止狀態;
  • 若是該進程沒有任何子進程,則返回錯誤。

須要注意的一點是,若是咱們在接收到SIGCHLD信號後,調用wait函數,則該函數會馬上返回。在其餘狀況下調用wait函數,則會阻塞。

函數聲明:

#include <sys/wait.h>

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid, int *statloc, int options); 

// Both return: process ID if OK, 0,or -1 on error

兩個函數之間的區別:

  • wait函數會阻塞,一直到一個子進程終止;waitpid函數的參數options能夠指定不阻塞;
  • waitpid函數能夠選擇不阻塞,而且能夠指定等待某一個子進程終止。

函數細節:

  • 若是一個子進程終止併成爲了殭屍進程,wait函數馬上返回該子進程的狀態;
  • 若是一個進程調用wait()函數並阻塞,而且有多個子進程,則當有一個子進程終止時,wait()函數返回;
  • 參數statloc是一個整型指針,若是該參數不爲null,則子進程的終止狀態被保存在該參數指向的整型中;若是咱們不關心進程的終止狀態,statloc傳入null就行;

返回值檢查:

使用四個宏來檢查wait和waitpid函數來獲取子進程的終止狀態(terminated status),如退出狀態,信號值等信息。

四個宏的具體說明見下表所示:

NewImage

pid的取值對waitpid函數行爲的影響:

  • pid == -1:行爲和wait相同,等待任意一個子進程終止
  • pid > 0:等待進程號爲pid的進程終止
  • pid ==0:等待進程組號和調用進程的進程組號相同的任意一個子進程終止
  • pid < -1:等待進程組號等於pid的任意一個子進程終止

參數option的取值:

NewImage

waitpid函數提供了三個wait沒有的特性:

  • waitpid可讓咱們等待某一個特定的進程;
  • waitpid提供了不阻塞版本的wait函數;
  • option參數WCONTINUED和WUNTRACED爲系統的任務控制(job control)提供了支持。

 

Example:

#include "apue.h"

#include <sys/wait.h>

 

int

main(void)

{

    pid_t   pid;

 

    if ((pid = fork()) < 0) {

        err_sys("fork error");

    } else if (pid == 0) {      /* first child */

        if ((pid = fork()) < 0)

            err_sys("fork error");

        else if (pid > 0)

        {

            exit(0);    /* parent from second fork == first child */

        }

 

        /*

         * We're the second child; our parent becomes init as soon

         * as our real parent calls exit() in the statement above.

         * Here's where we'd continue executing, knowing that when

         * we're done, init will reap our status.

         */

        sleep(2);

        printf("second child, parent pid = %ld\n", (long)getppid());

        exit(0);

    }

 

    if (waitpid(pid, NULL, 0) != pid)   /* wait for first child */

        err_sys("waitpid error");

 

    /*

     * We're the parent (the original process); we continue executing,

     * knowing that we're not the parent of the second child.

     */

    exit(0);

}

執行結果:

NewImage

結果分析:

在這裏咱們fork了兩次,緣由是,當咱們想fork一個子進程出來,而咱們不但願父進程阻塞在wait函數,而且不但願因爲父進程沒有調用wait函數先退出致使子進程成爲殭屍進程,那麼fork兩次,而且退出第一個子進程,可使得父進程及時退出,而且第二個子進程的父進程變成init進程。

 

小結

本篇主要介紹了fork、vfork、殭屍進程、wait和waitpid函數,這些在unix環境中都是很重要的概念和函數,而且在面試中也常常問到。

下一篇的內容包括:

  • 解釋器文件(interpreter files)
  • 系統調用(system function)

 

參考資料:

《Advanced Programming in the UNIX Envinronment 3rd》

相關文章
相關標籤/搜索