[單刷APUE系列]第八章——進程控制[1]

原文來自靜雅齋,轉載請註明出處。javascript

進程標識

在平常的開發使用過程中,以及以往的開發經驗,都應該知道進程是存在一個ID的,也就是進程ID(process ID),進程ID是惟一的,用以保證進程是惟一存在而且能被惟一得到。可是,在Unix系統中,進程ID是惟一的,可是在進程退出後,系統很是有可能將這個pid交付給新啓動的進程,可是這樣會致使新進程被誤認爲是以前存在的進程,因此現有的Unix系統有一個隊列,用於pid的延時複用。
Linux系統是類Unix系統,它的實現也是很具備表明性的,你們都知道,Linux實際上應該叫GNU/Linux,並且Linux只是一個內核,當將這個內核和軟件集合打包,就造成了一個Linux發行版,固然其中不包含各個發行商的修改內容,內核是啓動後得到控制權,內核有一部分就造成了pid爲0的進程,也叫做調度進程,沒有什麼卵用,而後pid爲1的進程被啓動,也就是其餘進程的父進程,通常都是init程序,它負責啓動整個Unix系統,並將系統根據配置文件引導到一個可以使用的狀態,init和前面的調度進程不同,調度進程其實是內核的一部分,而init是內核啓動的一個普通進程,可是它擁有root權限,在蘋果系統中,init進程被launchd進程替代,可是其做用也是差很少的。
除了上面的兩個進程之外,還有不少和系統密切相關的內核進程,這些進程都以守護進程的形式常駐。系統提供了一系列函數用於獲取當前進程的各項屬性。java

pid_t getpid(void);
pid_t getppid(void);

uid_t getuid(void);
uid_t geteuid(void);

gid_t getgid(void);
gid_t getegid(void);複製代碼

上面6個函數,分別是獲取當前進程pid、父進程pid、真實用戶ID、有效用戶ID、真實組ID和有效組ID。至於這些屬性的解釋,前面幾章已經都提到過了,因此這裏就再也不解釋。shell

進程派生

進程能夠派生出子進程,這是一個很廣泛的行爲,Unix系統也爲此提供了函數網絡

pid_t fork(void);

Fork() causes creation of a new process.  The new process (child process) is an exact copy of the calling process (parent process) except for the following:

o   The child process has a unique process ID.

o   The child process has a different parent process ID (i.e., the process ID of the parent process).

o   The child process has its own copy of the parent's descriptors.  These descriptors reference the same underlying objects, so that, for instance,file pointers in file objects are  shared between the child and the parent, so that an lseek(2) on a descriptor in the child process can affect a subsequent read or write by the parent.  This descriptor copying is also used by the shell to establish standard input and output for newly cre-ated processes as well as to set up pipes.

o   The child processes resource utilizations are set to 0; see setrlimit(2).複製代碼

這是一個很重要的函數,前面也使用過fork函數派生子進程,fork函數建立一個新進程,新進程是父進程的完整複製,固然,子進程的pid是從新生成的,子進程也擁有父進程pid做爲ppid屬性,子進程擁有父進程描述符的一份拷貝,可是實際上引用了相同的底層對象,因此實際上父進程子進程都是共享一樣的文件對象,就像第三章裏面講到的,進程只維護了文件描述符和文件指針的映射,內核爲全部的打開的文件維護了一個文件表,每一個文件表項包含了文件狀態標誌、文件偏移量等等,在學習文件共享這塊的時候,筆者還提到一個重點就是內核維護的文件表是爲全部打開的文件,同一個文件被不一樣進程打開是兩個文件表項,可是父子進程其實是拷貝了完整的進程空間,因此說子進程擁有父進程的文件描述符和文件指針的映射,因此說,父子進程的文件描述符指向了同一個文件表項目,因此lseek這樣的修改偏移量的函數會影響到父子進程,這個特性也被shell用於創建標準輸入輸出錯誤給新啓動的進程。固然,子進程的資源限制和父進程是不一樣的,將被重置。
前面幾章中提到,fork函數被調用一次,可是返回兩次,子進程和父進程都會獲得返回值,可是子進程獲得的返回值是0,父進程的返回值則是子進程的pid。子進程複製了整個父進程的進程空間,例如堆和棧等,固然,這只是個副本,父子進程實際共享的只有正文段,這樣能夠節約空間,而且前面提到正文段其實是隻讀的。
在實際的Unix系統實現中,經常使用差分存儲的技術,也就是說,原來的堆棧不會被複制,兩個進程以只讀的形式共享同一個堆棧區域,當須要修改區域內容的時候,則在新的區域製做差分存儲。運維

#include "include/apue.h"

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

int main(int argc, char *argv[])
{
    int var;
    pid_t pid;

    var = 80;
    if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1)
        err_sys("write error");
    printf("before fork\n");

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        ++globalVar;
        ++var;
    } else {
        sleep(2);
    }

    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globalVar, var);
    exit(0);
}複製代碼

而後執行的結果以下函數

~/Development/Unix » ./a.out
a write to stdout
before fork
pid = 8905, glob = 7, var = 81
pid = 8904, glob = 6, var = 80
~/Development/Unix » ./a.out > temp.out
~/Development/Unix » cat temp.out
a write to stdout
before fork
pid = 8916, glob = 7, var = 81
before fork
pid = 8915, glob = 6, var = 80複製代碼

很明顯的能夠看到兩個現象,就是子進程修改了變量不會致使父進程的變化,還有就是輸出到文件和輸出到終端發生了區別。看上面的代碼,能夠發現使用write函數向標準輸出寫的時候,使用了sizeof(buf) - 1,這是由於strlen函數計算長度的時候是不計算終止的null字節的,可是sizeof則會包括null字節,這個其實很好理解,strlen函數其實是一個函數調用,爲了保證開發者使用的便捷,因此默認認爲字符串長度實際上不該該包含null字節,可是sizeof則是一個單目運算符,它和其餘的運算符同樣都不是函數,sizeof操做符以字節形式給出了其操做數的存儲大小。操做數能夠是一個表達式或括在括號內的類型名。操做數的存儲大小由操做數的類型決定。換言之,這是一個編譯時計算。
在第三章中講到,write函數是不帶緩衝的IO,而標準C庫提供的則是帶有緩衝的,在前面的章節中也提到了緩衝的不一樣狀況,若是標準輸出是鏈接到終端設備,那麼它是行緩衝的,不然就是全緩衝的,在標準輸出是終端設備的狀況下,咱們只看到了一行輸出,由於換行符沖洗了緩衝區,而當標準輸出重定向到文件的時候,輸出是全緩衝的,這樣換行符不會致使系統的自動寫入,當fork函數執行的時候,這行輸出依舊被存儲在緩衝區中,而後隨着fork函數被共享給了子進程,隨着後續繼續的寫入,兩個進程都同時寫入了before fork字符串。
實際上,文件共享一直是很重要的概念,咱們知道,用戶啓動的進程通常都是shell啓動的,也就是說是shell的子進程,因此shell將進程的輸入輸出能夠進行重定向,當父進程的標準輸入輸出被重定向的時候,因爲子進程繼承了父進程的文件描述符,因此子進程也被重定向了,前面也說過,父子進程相同的文件描述符是指向同一個文件表項的,因爲這個緣由,二者任意一個進程修改了偏移量,下一個進程會跟在這個偏移量後,能夠變相的實現一種交互。固然咱們知道,因爲多進程操做系統調度,進程之間的切換是很頻繁的,若是沒有父子進程的同步措施,二者的輸出頗有可能混合,因此對於派生子進程,有如下兩種方式處理文件描述符學習

  1. 父進程使用函數等待子進程完成。這個很是簡單,因爲共享同一個文件表項,子進程的輸出也會更新父進程的偏移量,因此等待子進程完成後直接就能讀寫。
  2. 父進程和子進程各自執行不一樣的程序段。在這種狀況下,在fork之後,父子進程各自只使用不衝突的文件描述符。

前面也提到過不少關於父子進程的繼承,例如各類用戶組ID,當前工做目錄,資源限制環境變量等,一般狀況下,使用fork函數有兩種緣由優化

  1. 父進程複製自身,各自執行不一樣的代碼段,也就是網絡服務中典型的多進程模型。
  2. 一個進程想要執行不一樣的程序。shell就是這樣的,因此子進程能夠在fork後馬上使用exec,讓新程序運行。

進程派生變體

pid_t vfork(void);

Vfork() can be used to create new processes without fully copying the address space of the old process, which is horrendously inefficient in a paged envi-ronment.  It is useful when the purpose of fork(2) would have been to create a new system context for an execve.  Vfork() differs from fork in that the child borrows the parent's memory and thread of control until a call to execve(2) or an exit (either by a call to exit(2) or abnormally.)  The parent process is suspended while the child is using its resources.

Vfork() returns 0 in the child's context and (later) the pid of the child in the parent's context.

Vfork() can normally be used just like fork.  It does not work, however, to return while running in the childs context from the procedure that called vfork() since the eventual return from vfork() would then return to a no longer existent stack frame.  Be careful, also, to call _exit rather than exit if you can't execve, since exit will flush and close standard I/O channels, and thereby mess up the parent processes standard I/O data structures.  (Even with fork it is wrong to call exit since buffered data would then be flushed twice.)複製代碼

vfork函數也是建立一個新進程,可是不徹底拷貝父進程地址空間,這個函數spawn new process in a virtual memory efficient way,實際上這個函數主要用於spawn一個新進程而作的優化,不須要複製父進程的地址空間,從而加快了函數的執行,除此之外,vfork函數還會保證子進程先運行,ui

#include "include/apue.h"

int globalVar = 6;

int main(int argc, char *argv[])
{
    int var;
    pid_t pid;

    var = 88;
    printf("before vfork\n");
    if ((pid = vfork()) < 0) {
        err_sys("vfork error");
    } else if (pid == 0) {
        ++globalVar;
        ++var;
        _exit(0);
    } else {
        printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globalVar, var);
    }
    exit(0);
}複製代碼

而後運行這個程序lua

~/Development/Unix » ./a.out
before vfork
pid = 10616, glob = 7, var = 89複製代碼

從運行結果能夠很清楚的看到,子進程對變量的改變確實更改了父進程的變量值,其次,在上面的代碼中,子進程使用了_exit函數關閉進程,在前面咱們能夠了解到,exit函數會在關閉進程以前進行一系列的操做,而子進程其實是和父進程共享同一個內存空間,因此極可能會致使沒有任何輸出,因此在vfork函數只是用於spawn一個進程的前置操做,而不是正常的派生子進程,這個也在Unix系統手冊中給予了警告。

退出進程

就像前面一章講的,有5種正常退出和3種異常終止,下面是5中正常退出

  1. main函數返回,實際上等效於調用exit
  2. exit函數。exit函數其實是ISO C定義的函數,在前面也有過詳細的工做流程描述
  3. 調用_exit和_Exit函數。二者能夠當作等價,只是一個是ISO C庫函數,一個是Unix系統函數
  4. 進程的最後一個線程執行return語句
  5. 進程的最後一個函數使用pthread_exit函數

3種異常終止以下

  1. 調用abort函數產生SIGABRT信號
  2. 進程接收到信號
  3. 進程接收到取消請求

不管是如何退出進程,實際上在最後都須要內核進行執行清理工做,包括打開的描述符什麼的,對於上面5中正常退出,都會有一個退出狀態能夠傳遞,對於3種異常終止,內核一樣會產生一個終止狀態,最終,都會變成退出狀態,這樣父進程就能獲得子進程的退出狀態。
在正常的使用過程當中,子進程都是先於父進程退出,可是在某些特殊狀況下,父進程會先於子進程結束,可是實際上在終止每一個進程的時候,內核會檢查全部現有的進程,若是是正在終止的進程的子進程,就將其父進程修改成init進程,也就是pid爲1的進程。
在Unix系統運維中,會碰到殭屍進程,在開發的概念上來講,就是子進程已經終止,可是父進程還沒有對其進行善後處理。

wait函數族

pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);複製代碼

實際上,當一個進程退出,不管是正常仍是異常,內核都會向父進程發送SIGCHLD信號,在默認狀況下,都是選擇忽略這個信號,這裏只須要知道使用wait函數族會發生什麼

The wait() function suspends execution of its calling process until stat_loc information is available for a terminated child process, or a signal is received. On return from a successful wait() call, the stat_loc area contains termination information about the process that exited as defined below.複製代碼

wait函數會阻塞父進程知道子進程終止或者受到SIGCHLD信號,當wait函數返回時,stat_loc將會包含進程結束信息。
waitwaitpid函數就在於參數的區別,waitpid能夠傳入一個options選項用於行爲的改變,還有就是能夠指定進程ID。wait函數則是等待直到第一個子進程退出.

The options parameter contains the bitwise OR of any of the following options.  The WNOHANG option is used to indi-cate that the call should not block if there are no processes that wish to report status.  If the WUNTRACED option is set, children of the current process that are stopped due to a SIGTTIN, SIGTTOU, SIGTSTP, or SIGSTOP signal also have their status reported.複製代碼

WNOHANG參數指示沒有進程報告狀態則當即返回,WUNTRACED選項則是子進程因爲SIGTTINSIGTTOUSIGTSTPSIGSTOP信號進入暫停狀態,還有一個是WCONTINUED,頭文件中存在,可是說明手冊上不存在,由POSIX1.x規定。

The following macros may be used to test the manner of exit of the process.  One of the first three macros will evaluate to a non-zero (true) value:
WIFEXITED(status)
        True if the process terminated normally by a call to _exit(2) or exit(3).

WIFSIGNALED(status)
        True if the process terminated due to receipt of a signal.

WIFSTOPPED(status)
        True if the process has not terminated, but has stopped and can be restarted.  This macro can be true only if the wait call specified the WUNTRACED option or if the child process is being traced (see ptrace(2)).

Depending on the values of those macros, the following macros produce the remaining status information about the child process:

WEXITSTATUS(status)
        If WIFEXITED(status) is true, evaluates to the low-order 8 bits of the argument passed to _exit(2) or exit(3) by the child.

WTERMSIG(status)
        If WIFSIGNALED(status) is true, evaluates to the number of the signal that caused the termination of the process.

WCOREDUMP(status)
        If WIFSIGNALED(status) is true, evaluates as true if the termination of the process was accompanied by the creation of a core file containing an image of the process when the signal was received.

WSTOPSIG(status)
        If WIFSTOPPED(status) is true, evaluates to the number of the signal that caused the process to stop.複製代碼

上面就是蘋果系統支持的一系列宏,原著中的WIFCONTINUED宏沒有出如今說明手冊中,可是實際上在頭文件中是存在的。

#include "include/apue.h"
#include <sys/wait.h>

void pr_exit(int status)
{
    if (WIFEXITED(status))
        printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
        printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),
#ifdef WCOREDUMP
        WCOREDUMP(status) ? " (core file generated)" : "");
#else
        "");
#endif
    else if (WIFSTOPPED(status))
        printf("child stopped, signal number = %d\n", WSTOPSIG(status));
}複製代碼

上面是原著提供的打印終端終止狀態的函數,能夠按照之前的方法將其打包爲靜態庫。須要注意的是,你須要指定-D_DARWIN_C_SOURCE來保證編譯添加上WCOREDUMP支持。

#include "include/apue.h"
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    pid_t pid;
    int status;

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        exit(7);

    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        abort();

    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        status /= 0;

    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);

    exit(0);
}複製代碼

編譯運行

> ./a.out
normal termination, exit status = 7
abnormal termination, signal number = 6
abnormal termination, signal number = 8複製代碼

並無像原著同樣出現(core file generated)字樣,多是由於系統雖然支持這個宏,可是隻對少數錯誤會進行轉儲。
waitpid函數的options選項的三個可選值實際上起到了兩種做用,WNOHANG是非阻塞,而其餘兩個參數則是做業控制。

相關文章
相關標籤/搜索