system函數的總結


最近在看APUE第10章中關於system函數的POSIX.1的實現。關於POSIX.1要求system函數忽略SIGINT和SIGQUIT,並且阻塞信號SIGCHLD的論述,理解得不是很透徹,本文就通過實際的實例來一探究竟吧。

 

一、為什么要阻塞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:

 

[cpp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. #include <stdio.h>  
  2. #include <errno.h>  
  3. #include <unistd.h>  
  4. #include <stdlib.h>  
  5. #include <string.h>  
  6. #include "apue.h"  
  7.   
  8. /* version without signal handling */  
  9. int system_without_signal(const char *cmd_string)  
  10. {  
  11.     pid_t pid;  
  12.     int status = -1;  
  13.   
  14.     if (cmd_string == NULL)  
  15.         return (1);     /* always a command processor with UNIX */  
  16.   
  17.     if ((pid = fork()) < 0) {  
  18.         status = -1;    /* probably out of processes */  
  19.     } else if (pid == 0) {  /* child */  
  20.         execl("/bin/sh", "sh", "-c", cmd_string, (char *)0);  
  21.         _exit(127); /* execl error */  
  22.     } else {                /* parent */  
  23. //      sleep(1);  
  24.         pid_t wait_pid;  
  25.         while ((wait_pid = waitpid(pid, &status, 0)) < 0) {  
  26.             printf("[in system_without_signal]: errno = %d(%s)\n",  
  27.                                         errno, strerror(errno));  
  28.             if (errno != EINTR) {  
  29.                 status = -1;    /* error other than EINTR form waitpid() */  
  30.                 break;  
  31.             }  
  32.         }  
  33.         printf("[in system_without_signal]: pid = %ld, wait_pid = %ld\n",  
  34.                                         (long)pid, (long)wait_pid);  
  35.         pr_exit("[in system_without_signal]", status);  
  36.     }  
  37.   
  38.     return (status);  
  39. }  

 

system_with_signal.c

 

[cpp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <errno.h>  
  4. #include <string.h>  
  5. #include <unistd.h>  
  6. #include <signal.h>  
  7. #include <sys/types.h>  
  8. #include <sys/wait.h>  
  9.   
  10. /* with appropriate signal handling */  
  11. int system_with_signal(const char *cmd_string)  
  12. {  
  13.     pid_t       pid;  
  14.     int         status;  
  15.     struct      sigaction ignore, saveintr, savequit;  
  16.     sigset_t    chld_mask, save_mask;  
  17.       
  18.     if (cmd_string == NULL)  
  19.         return (1);     /* always a command processor with UNIX */  
  20.   
  21.     /* ignore signal SIGINT and SIGQUIT */  
  22.     ignore.sa_handler = SIG_IGN;  
  23.     ignore.sa_flags = 0;  
  24.     sigemptyset(&ignore.sa_mask);  
  25.     if (sigaction(SIGINT, &ignore, &saveintr) < 0)   
  26.         return (-1);  
  27.     if (sigaction(SIGQUIT, &ignore, &savequit) < 0)  
  28.         return (-1);  
  29.   
  30.     /* block SIGCHLD and save current signal mask */  
  31.     sigemptyset(&chld_mask);  
  32.     sigaddset(&chld_mask, SIGCHLD);  
  33.     if (sigprocmask(SIG_BLOCK, &chld_mask, &save_mask) < 0)  
  34.         return (-1);  
  35.   
  36.     if ((pid = fork()) < 0) {  
  37.         status = -1;    /* probably out of processes */  
  38.     } else if (pid == 0) {      /* child */  
  39.         /* restore previous signal actions & reset signal mask */  
  40.         sigaction(SIGINT, &saveintr, NULL);  
  41.         sigaction(SIGQUIT, &savequit, NULL);  
  42.         sigprocmask(SIG_SETMASK, &save_mask, (sigset_t *)NULL);  
  43.   
  44.         execl("/bin/sh", "sh", "-c", cmd_string, (char *)0);  
  45.         _exit(127);  
  46.     } else {                    /* parent */  
  47.         int wait_pid;  
  48.     //  sleep(10);  /* */  
  49.         while ((wait_pid = waitpid(pid, &status, 0)) < 0) {  
  50.             printf("[in system_with_signal]: errno = %d(%s)\n",   
  51.                                         errno, strerror(errno));  
  52.             if (errno != EINTR) {  
  53.                 status = -1;    /* error other than EINTR from waitpid() */  
  54.                 break;  
  55.             }  
  56.         }  
  57.         printf("[in system_with_signal]: pid = %ld, wait_pid = %ld\n",   
  58.                                         (long)pid, (long)wait_pid);  
  59.         pr_exit("[in system_with_signal]", status);  
  60.     }  
  61.   
  62.     /* in parent: restore previous signal action & reset signal mask */  
  63.     if (sigaction(SIGINT, &saveintr, NULL) < 0)   
  64.         return (-1);  
  65.     if (sigaction(SIGQUIT, &savequit, NULL) < 0)  
  66.         return (-1);  
  67.     if (sigprocmask(SIG_SETMASK, &save_mask, (sigset_t *)NULL) < 0)  /* */  
  68.         return (-1);  
  69.   
  70.     return (status);  
  71. }  

 

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

 

[cpp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. #include <stdio.h>  
  2. #include <errno.h>  
  3. #include <stdlib.h>  
  4. #include <signal.h>  
  5. #include "apue.h"  
  6.   
  7. #define SETSIG(sa, sig, fun, flags) \  
  8. do {                                \  
  9.     sa.sa_handler = fun;            \  
  10.     sa.sa_flags = flags;            \  
  11.     sigemptyset(&sa.sa_mask);       \  
  12.     sigaction(sig, &sa, NULL);      \  
  13. while (0)  
  14.   
  15. extern int system_without_signal(const char *cmd_string);  
  16.   
  17. static void sig_chld(int signo)  
  18. {  
  19.     printf("\nenter SIGCHLD handler\n");  
  20.       
  21.     pid_t pid;  
  22.     int exit_status = -1;  
  23.     int errno_saved = errno;  
  24.     pid = wait(&exit_status);  
  25.     if (pid != -1) {  
  26.         printf("[in sig_chld] reaped %ld child,", (long)pid);  
  27.         pr_exit("wait: ", exit_status);  
  28.         printf("\n");  
  29.     } else {  
  30.         printf("[in sig_chld] wait error: errno = %d(%s)\n\n",   
  31.                                         errno, strerror(errno));  
  32.     }  
  33.   
  34.     errno = errno_saved;  
  35.     printf("leave SIGCHLD handler\n");  
  36. }  
  37.   
  38. int main(int argc, const char *argv[])  
  39. {  
  40.     pid_t pid;  
  41.     struct sigaction sigchld_act;  
  42.   
  43.     SETSIG(sigchld_act, SIGCHLD, sig_chld, 0);  
  44.   
  45.     int status;  
  46.     if ((status = system_without_signal("/bin/ls -l; exit 44")) < 0) {  
  47.         err_sys("system() error(status = %d): ", status);  
  48.     }  
  49.     pr_exit("system() return:", status);  
  50.   
  51.     exit(EXIT_SUCCESS);  
  52. }  

 

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

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

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

 

情形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函數就能正確的返回子進程的退出狀態了。

 

看到這里,你可能會說,問題都是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。我們可以通過一個簡單的實驗來驗證下這個結論:

[cpp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-family:Microsoft YaHei;">#include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int main(int argc, const char *argv[])  
  5. {  
  6.     int ret = system(NULL);  
  7.     printf("ret = %d\n", ret);  
  8.   
  9.     return 0;  
  10. }  
  11. </span>  

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

 

(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命令是非法的。   

[cpp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-family:Microsoft YaHei;">#include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include "apue.h"  
  4.   
  5. int main(int argc, const char *argv[])  
  6. {  
  7.     int ret = system("no_such_command");  
  8.     pr_exit("", ret);  
  9.   
  10.     return 0;  
  11. }  
  12. </span>  

測試結果:

第一次返回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的正常退出,看下面實例:

其中的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:

再來看下信號SIGQUIT:

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

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

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

參考鏈接:

http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=2078496

http://blog.chinaunix.net/uid-24774106-id-3048281.html?page=3


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM