天天看點

system函數的總結

最近在看APUE第10章中關于system函數的POSIX.1的實作。關于POSIX.1要求system函數忽略SIGINT和SIGQUIT,并且阻塞信号SIGCHLD的論述,了解得不是很透徹,本文就通過實際的執行個體來一探究竟吧。

system函數的總結

一、為什麼要阻塞SIGCHLD信号

#include <stdlib.h>

int system(const char *command);

函數工作大緻流程:system()函數先fork一個子程序,在這個子程序中調用/bin/sh -c來執行command指定的指令。/bin/sh在系統中一般是個軟連結,指向dash或者bash等常用的shell,-c選項是告訴shell從字元串command中讀取要執行的指令(shell将擴充command中的任何特殊字元)。父程序則調用waitpid()函數來為變成僵屍的子程序收屍,獲得其結束狀态,然後将這個結束狀态傳回給system()函數的調用者。

知道了以上基本知識點,也就好了解為什麼偏偏是SIGCHLD信号了,而不是其他的信号:因為fork的子程序結束後,核心會向其父程序發送SIGHLD信号,即system()函數的調用者。

那麼為什麼在調用system()函數,運作command指定的指令時要阻塞SIGCHLD這個信号呢? 接下來我們就通過兩個不同的system版本對比運作的結果,進而找到阻塞SIGCHLD信号的真正原因。

先來具體看看這兩個不同的system函數實作版本:

system_without_signal.c:

system函數的總結

#include <stdio.h>  

#include <errno.h>  

#include <unistd.h>  

#include <stdlib.h>  

#include <string.h>  

#include "apue.h"  

/* version without signal handling */  

int system_without_signal(const char *cmd_string)  

{  

    pid_t pid;  

    int status = -1;  

    if (cmd_string == NULL)  

        return (1);     /* always a command processor with UNIX */  

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

        status = -1;    /* probably out of processes */  

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

        execl("/bin/sh", "sh", "-c", cmd_string, (char *)0);  

        _exit(127); /* execl error */  

    } else {                /* parent */  

//      sleep(1);  

        pid_t wait_pid;  

        while ((wait_pid = waitpid(pid, &status, 0)) < 0) {  

            printf("[in system_without_signal]: errno = %d(%s)\n",  

                                        errno, strerror(errno));  

            if (errno != EINTR) {  

                status = -1;    /* error other than EINTR form waitpid() */  

                break;  

            }  

        }  

        printf("[in system_without_signal]: pid = %ld, wait_pid = %ld\n",  

                                        (long)pid, (long)wait_pid);  

        pr_exit("[in system_without_signal]", status);  

    }  

    return (status);  

}  

system_with_signal.c

system函數的總結

#include <signal.h>  

#include <sys/types.h>  

#include <sys/wait.h>  

/* with appropriate signal handling */  

int system_with_signal(const char *cmd_string)  

    pid_t       pid;  

    int         status;  

    struct      sigaction ignore, saveintr, savequit;  

    sigset_t    chld_mask, save_mask;  

    /* ignore signal SIGINT and SIGQUIT */  

    ignore.sa_handler = SIG_IGN;  

    ignore.sa_flags = 0;  

    sigemptyset(&ignore.sa_mask);  

    if (sigaction(SIGINT, &ignore, &saveintr) < 0)   

        return (-1);  

    if (sigaction(SIGQUIT, &ignore, &savequit) < 0)  

    /* block SIGCHLD and save current signal mask */  

    sigemptyset(&chld_mask);  

    sigaddset(&chld_mask, SIGCHLD);  

    if (sigprocmask(SIG_BLOCK, &chld_mask, &save_mask) < 0)  

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

        /* restore previous signal actions & reset signal mask */  

        sigaction(SIGINT, &saveintr, NULL);  

        sigaction(SIGQUIT, &savequit, NULL);  

        sigprocmask(SIG_SETMASK, &save_mask, (sigset_t *)NULL);  

        _exit(127);  

    } else {                    /* parent */  

        int wait_pid;  

    //  sleep(10);  /* */  

            printf("[in system_with_signal]: errno = %d(%s)\n",   

                status = -1;    /* error other than EINTR from waitpid() */  

        printf("[in system_with_signal]: pid = %ld, wait_pid = %ld\n",   

        pr_exit("[in system_with_signal]", status);  

    /* in parent: restore previous signal action & reset signal mask */  

    if (sigaction(SIGINT, &saveintr, NULL) < 0)   

    if (sigaction(SIGQUIT, &savequit, NULL) < 0)  

    if (sigprocmask(SIG_SETMASK, &save_mask, (sigset_t *)NULL) < 0)  /* */  

好,接下來具體看一個例子:

system函數的總結

#define SETSIG(sa, sig, fun, flags) \  

do {                                \  

    sa.sa_handler = fun;            \  

    sa.sa_flags = flags;            \  

    sigemptyset(&sa.sa_mask);       \  

    sigaction(sig, &sa, NULL);      \  

} while (0)  

extern int system_without_signal(const char *cmd_string);  

static void sig_chld(int signo)  

    printf("\nenter SIGCHLD handler\n");  

    int exit_status = -1;  

    int errno_saved = errno;  

    pid = wait(&exit_status);  

    if (pid != -1) {  

        printf("[in sig_chld] reaped %ld child,", (long)pid);  

        pr_exit("wait: ", exit_status);  

        printf("\n");  

    } else {  

        printf("[in sig_chld] wait error: errno = %d(%s)\n\n",   

    errno = errno_saved;  

    printf("leave SIGCHLD handler\n");  

int main(int argc, const char *argv[])  

    struct sigaction sigchld_act;  

    SETSIG(sigchld_act, SIGCHLD, sig_chld, 0);  

    int status;  

    if ((status = system_without_signal("/bin/ls -l; exit 44")) < 0) {  

        err_sys("system() error(status = %d): ", status);  

    pr_exit("system() return:", status);  

    exit(EXIT_SUCCESS);  

在這個例子中,我們調用的是system_without_signal,即不處理信号的system實作版本,并且調用者還設定了SIGCHLD的信号處理函數。好,基于這些條件,接下來我們考慮兩種情形:

情形1:在子程序正在運作指定程式時,或者說在子程序結束之前,父程序中的waitpid阻塞在那裡。

這種情形下,一旦子程序結束,核心會向應用程式遞送SIGCHLD信号,運作信号處理函數,在信号處理函數中調用wait系列函數,那麼現在問題來了:究竟是信号處理函數中的wait系列函數還是system_without_signal中的waitpid為子程序收屍呢? 答案是未知的。因為信号本身是異步的,我們掌控不了(在我的系統中,waitpid還總能正确的擷取子程序退出狀态,而在信号處理函數中的wait卻傳回-1,errno設定為ECHLD,表明沒有可收屍的子程序,見下圖。但是,在你的系統中,結果也許就是相反的噢)。是以,在這種情形下,我們得出的結論是:盡管system函數完成了其任務(正确執行了我們指定的程式),但卻有可能傳回-1。很顯然,這不是我們希望發生的。

system函數的總結

情形2:在一個繁忙的系統中,很可能在調用waitpid之前子程序就已經結束了,此時核心會向父程序遞送SIGCHLD信号。

在這種情形下,問題就更明顯了。在調用waitpid之前就已經調用了SIGCHLD信号的信号處理函數,信号處理函數中的wait函數為子程序收了屍,那麼接下來的waitpid不就擷取不了子程序的退出狀态了嗎? 事實也的确如此!我們可以在waitpid之前調用加個sleep來模拟系統負荷重的情形,會發現waitpid會出錯,傳回-1,errno設定為ECHLD,表明沒有可收屍的子程序,最終system函數傳回-1。是以,在這種情形下,我們得出的結論是:盡管system函數完成了其任務(正确執行了我們指定的程式),但卻一直傳回-1。很顯然,這也不是我們希望發生的。

如果将上面例子中的system_without_signal替換成system_with_signal,那麼system函數在調用fork之前就已經阻塞了SIGCHLD信号的話,那麼就不會出現上述兩種情況了。因為阻塞了SIGCHLD信号,那麼不管system函數建立的子程序什麼時候結束,即不管SIGCHLD信号什麼時候來,在沒有解除阻塞之前,是不會處理該信号的,即SIGCHLD信号是未決的。是以,無論如何,waitpid都會正确擷取子程序的退出狀态。隻有在最後調用sigprocmask時,系統才會解除對SIGCHLD的阻塞。解除阻塞後,這才調用信号處理函數,不過這次信号處理函數中的wait會出錯,傳回-1,errno設定為ECHLD,表明沒有可收屍的子程序。那麼system函數就能正确的傳回子程序的退出狀态了。

system函數的總結

看到這裡,你可能會說,問題都是SIGCHLD信号處理函數中的wait惹的禍,如果去掉SIGCHLD信号處理函數中的wait函數,不就不會帶來上述的兩個問題了嗎? 我的答案是:的确可以避免上述兩個問題,即system函數可以正确的擷取子程序的退出狀态。但是這樣做還是會有問題的:我們先不管在SIGCHLD信号處理函數中不調用wait系列函數這種不正統的做法,我們在這裡考慮這樣一種情形:如果信号處理函數需要運作一分鐘的時間才傳回(實際程式設計中,信号處理函數要盡量短噢,這裡隻是一種極端的假設),那麼system函數豈不是也要阻塞一分鐘才能傳回?因為如果不阻塞SIGCHLD信号并且主程序注冊了SIGCHLD信号處理函數(未調用wait系列函數),那麼就需要等主程序的信号處理函數傳回後waitpid才能接受到子程序的退出狀态,也就是信号處理函數需要運作多長時間,那麼system也就需要這麼多時間才能傳回。一個函數的運作受到外界不确定因素的影響,這種情形還是應該避免的。是以在調用system函數的時候阻塞SIGCHLD,這樣在執行期間信号被阻塞就不會調用信号處理函數了,system中的waitpid就能"及時"地擷取到子程序的狀态。-- 但是仔細想想,其實system函數還是避免不了這種情形的,因為在最後調用sigprocmask解除阻塞時(一般在sigprocmask傳回之前,就至少遞送一個阻塞的信号),還是會調用信号處理函數,system依然會阻塞,唯一的不同是,這種情況下waitpid是在調用信号處理函數之前就擷取了子程序的退出狀态,避免了多線程的諸多影響。是以,在平時的程式設計實踐當中,信号處理函數要盡量的短,這樣才不會對其他函數造成不必要的未知影響。

好,稍微總結一下:

system函數之是以阻塞SIGCHLD,是為了保證system函數能夠正确擷取子程序的退出狀态,并傳回給system的調用者。

由此我們也可以引申出以下結論:

如果以後要寫一個函數,函數中fork了一個子程序,并且定義的函數要得到子程序的一些資訊,例如子程序的ID、子程序的終止狀态等,而該函數的調用者所注冊的SIGCHLD信号處理函數會影響這個函數擷取這些資訊,是以為了避免該函數在擷取這些資訊之前,由于子程序的終止觸發SIGCHLD信号而先調用信号處理函數,在fork之前應該将SIGCHLD信号阻塞,在函數正确擷取相關資訊後,才對SIGCHLD信号解除阻塞。

二、為什麼忽略SIGINT和SIGQUIT

關于這點,APUE的解釋已經很明白了:因為由system執行的指令可能是互動式指令(例如ed程式),以及因為system的調用者在指定的指令執行期間放棄了對程式的控制(waitpid阻塞在那裡),等待該執行程式的結束,是以system的調用者就不應該接收SIGINT和SIGQUIT信号,而隻由子程序接收,這也是在子程序中一開始恢複SIGINT和SIGQUIT信号的原因。其實說白了,還是因為希望擷取子程序的退出狀态不受到外界幹擾。

三、system函數的傳回值 

很多人不推薦使用system函數,是因為它的傳回值很多人沒有弄清楚。

(1)當參數command是NULL的時候

在參數為NULL的情況下,system函數的傳回值很簡單明了,隻有0和1。傳回1,表明系統的指令處理程式,即/bin/sh是可用的。相反,如果指令處理程式不可用,則傳回0。我們可以通過一個簡單的實驗來驗證下這個結論:

system函數的總結

<span style="font-family:Microsoft YaHei;">#include <stdio.h>  

    int ret = system(NULL);  

    printf("ret = %d\n", ret);  

    return 0;  

</span>  

 在我的系統上通過ls -l /bin/sh可以看出/bin/sh是個軟連結,指向/bin/dash這個SHELL,我們可以通過unlink指令先取消這個軟連結,會發現程式傳回0,如果再次建立這個軟連結,則system傳回1.

system函數的總結

(2)當參數command不是NULL的時候

當參數不為NULL的時候,情況有些小複雜,根據APUE這裡可以分為以下三種情況:

    (2.1)如果fork等系統調用失敗,或者waitpid函數發生除EINTR外的錯誤時,system傳回-1

    這種情況下,我們沒有辦法了,隻能檢測errno的值來判斷是哪個系統調用出錯以及出錯的原因!

     那麼為什麼要排除waitpid發生EIINTR呢? 對于這個問題,我們可以假設system函數的調用者設定了SIGUSR1信号的處理函數,那麼當waitpid阻塞在那裡時,向程式發送SIGUSR1信号,則waitpid會傳回-1,errno被設定為EINTR。是以應該排除EINTR錯誤值,否則就擷取不到/bin/sh的退出狀态了。

    (2.2)一切緻使execl失敗的情況下,system傳回127

    緻使execl失敗的原因應該隻有兩個:/bin/sh不存在,再者就是指定的shell指令是非法的。   

system函數的總結

    int ret = system("no_such_command");  

    pr_exit("", ret);  

測試結果:

system函數的總結

第一次傳回127是因為非法的指令,第二次卻是/bin/sh不存在導緻的。

那麼現在的問題是:如果指定的指令執行成功,且指令的傳回值正好也是127,那麼如何分辨是什麼原因呢(例如上述程式中的是system("exit 127"))? 貌似沒有辦法哦,是以我們在程式中盡量避免使用127作為傳回值。

    (2.3)除此之外,system傳回/bin/sh的終止狀态

          到這裡,要強調的一點是:system傳回的是/bin/sh的結束狀态,而不是我們指定的指令的傳回狀态,盡管大部分時間它們是一樣的。因為/bin/sh也有可能異常終止,例如人為的通過kill向其發送SIGKILL,那麼/bin/sh退出狀态就是9,而這跟指定的指令沒有任何關系。

     盡管有時參數command代表的指令執行過程中出了錯,但這不會影響/bin/sh的正常退出,看下面執行個體:

system函數的總結

其中的tsys請自行參考APUE。很明顯,xxx目錄是不存在的,ls執行過程中發生了錯誤,傳回值為2,shell接收到的就是512(為什麼是512,具體下篇文章),shell将該值轉換成2後,最後由waitpid接收到該終止狀态,即512,pr_exit列印的結果是2,正是ls傳回的終止狀态。

好了,通過之前的陳述我們知道system函數的傳回值即shell的終止狀态,這個終止狀态是通過waitpid獲得的,那麼怎麼解釋這個傳回值也就很明朗了 -- 使用檢查waitpid傳回值的那些宏就可以了,這也正式pr_exit實作的方式(參考APUE第8章)。

以上說的都是指令正常終止,那麼如果是異常終止了?system函數傳回值可以正确反映這種狀态嗎?我們通過實驗來驗證,先看信号SIGINT:

system函數的總結

再來看下信号SIGQUIT:

system函數的總結

可見通過system函數的傳回值是不可能知道程式是異常終止的,上面的傳回值之是以分别是130和131,是/bin/sh特殊處理的:當正在執行的指令是被信号終止的話,那麼終止狀态是128加上這個信号的編碼。

這裡提醒一下讀者,如果你照着APUE的實驗操作,即直接在終端鍵入Ctrl+C和Ctrl+\的話,你的結果可能與作者的是不一樣的。我的結果就與作者的不一樣:

system函數的總結

你的系統上的結果也許和我的也不一樣的,原因是不同的shell對信号的處理方式是不一樣的,APUE作者使用的shell對SIGINT和SIGQUI的處理應該都是忽略,從我上面的結果可以看出,dash忽略信号SIGQUIT。未完待續!

參考連結:

<a href="http://bbs.chinaunix.net/forum.php?mod=viewthread&amp;tid=2078496" target="_blank">http://bbs.chinaunix.net/forum.php?mod=viewthread&amp;tid=2078496</a>

<a href="http://blog.chinaunix.net/uid-24774106-id-3048281.html?page=3" target="_blank">http://blog.chinaunix.net/uid-24774106-id-3048281.html?page=3</a>