abort函数的功能是使异常程序终止。
#include <stdlib.h> void abort(void); // 此函数不返回
此函数将SIGABRT信号发送给调用进程(进程不应忽略此信号)。ISO C规定,调用abort将向主机环境递送一个未成功终止的通知,其方法是调用raise(SIGABRT)函数。
ISO C要求若捕捉到此信号而且相应信号处理程序返回,abort仍不会返回到其调用者。如果捕捉到此信号,则信号处理程序不能返回的唯一方法是它调用exit、_exit、_Exit、longjmp或siglongjmp。POSIX.1也说明abort并不理会进程对此信号的忽略和阻塞。
让进程捕捉SIGABRT的意图是:在进程终止之前由其执行所需的清理操作。如果进程并不在信号处理程序中终止自己,POSIX.1声明当信号处理程序返回时,abort终止该进程。
ISO C针对此函数的规范将下列问题留由实现决定:是否要冲洗输出流以及是否要删除临时文件。POSIX.1的要求则更进一步,它要求如果abort调用终止进程,则它对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。因为大多数UNIX tempfile(临时文件)的实现在创建文件之后会立即调用unlink,所以ISO C关于临时文件的警告通常与我们无关。
《UNIX环境高级编程》P275:程序清单10-18 abort的POSIX.1实现
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> void abort(void) { sigset_t mask; struct sigaction action; // 调用者不能忽略SIGABRT sigaction(SIGABRT, NULL, &action); if (action.sa_handler == SIG_IGN) { action.sa_handler = SIG_DFL; sigaction(SIGABRT, &action, NULL); } if (action.sa_handler == SIG_DFL) ffluse(NULL); // 冲洗所有输出流 // 调用者不能阻塞SIGABRT sigfillset(&mask); sigdelset(&mask, SIGABRT); // 仅关闭SIGABRT掩码 sigprocmask(SIG_SETMASK, &mask, NULL); kill(getpid(), SIGABRT); // 发送信号 // 如果运行到此处,则进程捕获到SIGABRT并返回了 ffluse(NULL); // 冲洗输出流 action.sa_handler = SIG_DFL; sigaction(SIGABRT, &action, NULL); // 重置为默认 sigprocmask(SIG_SETMASK, &mask, NULL); kill(getpid(), SIGABRT); exit(1); }
首先查看是否执行默认动作,若是则冲洗所有标准I/O流。这并不等价于对所有打开的流调用fclose,但是当进程终止时,系统会关闭所有打开的文件。如果进程捕获此信号并返回,那么因为进程可能产生了更多的输出,所以再一次冲洗所有的流。不进行冲洗处理的唯一条件是如果进程捕捉此信号,然后调用_exit或_Exit。在这种情况下,内存中任何未冲洗的标准I/O缓冲区都被丢弃。
如果调用kill使其为调用者产生信号,并且如果该信号是不被阻塞的,则在kill返回前该信号(或某个未决的、为阻塞的信号)就被传送给了该进程。我们阻塞了除SIGABRT之外的所有信号,这样就可知如果对kill的调用返回了,则该进程一定已捕捉到了该信号,并且也从该信号处理程序返回。
POSIX.1要求system忽略SIGINT和SIGQUIT,阻塞SIGCHLD。
程序清单10-19使用8.13节中的system版本,用其调用ed(1)编辑器。这里调用ed的原因是:它是捕捉中断和退出信号的交互式程序。若从shell调用ed,并键入中断字符,则它捕捉中断信号并打印问号。它还将退出符的处理方式设置为忽略。
《UNIX环境高级编程》P276:程序清单10-19 用system调用ed编辑器(有改动)
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/wait.h> #include <errno.h> #include <unistd.h> int system(const char *command); static void sig_int(int signo) { printf("caught SIGINT\n"); } static void sig_chld(int signo) { printf("caught SIGCHLD\n"); } int main(void) { if (signal(SIGINT, sig_int) == SIG_ERR) fprintf(stderr, "signal(SIGINT) error\n"); if (signal(SIGCHLD, sig_chld) == SIG_ERR) fprintf(stderr, "signal(SIGCHLD) error\n"); if (system("/bin/ed") < 0) fprintf(stderr, "system() error\n"); exit(0); } int system(const char *command) { pid_t pid; int status; if (command == NULL) return(1); // UNIX中system总是可用的 if ((pid = fork()) < 0) { status = -1; // 进程创建出错 } else if (pid == 0) { // 子进程 execl("/bin/sh", "sh", "-c", command, (char *)0); _exit(127); } else { // 父进程 while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { status = -1; // 除了EINTR之外的出错 break; } } } return status; }
程序清单10-19用于捕捉SIGINT和SIGCHLD信号。若调用它则可得:
$ ./19 a 将正文添加至编辑器缓冲区 Here is one line of text . 行首的点停止添加方式 1,$p 打印缓冲区的第1行至最后1行 Here is one line of text w temp.foo 将缓冲区写至一个文件 25 编辑器称写了25个字符 q 离开编辑器 caught SIGCHLD
当编辑器终止时,系统向父进程发送SIGCHLD信号。父进程捕捉它,然后从信号处理程序返回。但是若父进程正在捕捉SIGCHLD信号,那么正在执行system函数时,应当阻塞对父进程递送SIGCHLD信号。否则,当system创建的子进程结束时,system的调用者可能错误地认为,它自己的一个子进程结束了。于是,调用者将会调用一种wait函数以获得子进程的终止状态,这样就阻止了system函数获得子进程的终止状态,并将其作为它的返回值。
如果再次执行该程序,在这次运行时将一个中断信号传送给编辑器,则可得:
$ ./19 a 将正文添加至编辑器缓冲区 hello, world . 行首的点停止添加方式 1,$p 打印缓冲区中的第1行至最后1行 hello, world w temp.foo 将缓冲区写至一个文件 13 编辑器称写了13个字节 ^C 键入中断符(Ctrl+C) ? 编辑器捕捉信号,打印问号 caught SIGINT 父进程执行同一操作 q 离开编辑器 caught SIGCHLD
键入中断字符可使中断信号传送给前台进程组中的所有进程。如图所示,显示了编辑程序正在运行时的各个进程的关系:
在这一实例中,SIGINT被送给三个前台进程(shell进程忽略此信号)。从输出可见,./19进程和ed进程捕捉该信号。但是,当用system运行另一个程序(如ed)时,不应使父、子进程两者都捕捉终端产生的两个信号:中断和退出。这两个信号只应送给正在运行的程序:子进程。因为由system执行的控制命令可能是交互式命令,以及因为system的调用者在执行程序时放弃了控制,等待该执行程序的结束,所以system的调用者就不应接收这两个终端产生的信号。
程序清单10-20显示了system函数的另一个实现,它进行了所要求的信号处理。(有改动)
#include <sys/wait.h> #include <errno.h> #include <signal.h> #include <unistd.h> int system(const char *cmdstring) { pid_t pid; int status; struct sigaction ignore, saveintr, savequit; sigset_t chldmask, savemask; if (cmdstring == NULL) return(1); // 忽略SIGINT和SIGQUIT ignore.sa_handler = SIG_IGN; sigemptyset(&ignore.sa_mask); ignore.sa_flags = 0; if (sigaction(SIGINT, &ignore, &saveintr) < 0) return(-1); if (sigaction(SIGQUIT, &ignore, &savequit) < 0) return(-1); // 屏蔽SIGCHLD sigemptyset(&chldmask); sigaddset(&chldmask, SIGCHLD); if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0) return(-1); if ((pid = fork()) < 0) { status = -1; } else if (pid == 0) { // 子进程 // 恢复之前的信号处理动作,恢复信号屏蔽字 sigaction(SIGINT, &saveintr, NULL); sigaction(SIGQUIT, &savequit, NULL); sigprocmask(SIG_SETMASK, &savemask, NULL); execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); } else { // 父进程 while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { status = -1; break; } } } if (sigaction(SIGINT, &saveintr, NULL) < 0) return(-1); if (sigaction(SIGQUIT, &savequit, NULL) < 0) return(-1); if (sigprocmask(SIG_SETMASK, &savemask, NULL) < 0) return(-1); return (status); }
与前一个的差别:
(1) 当我们键入中断或退出字符时,不向调用进程发送信号。
(2) 当ed命令终止时,不向调用进程发送SIGCHLD信号。作为替代,在程序末尾的sigprocmask调用对SIGCHLD信号阻塞之前,SIGCHLD信号一直被阻塞。而对sigprocmask函数的这一次调用是在waitpid取得了子进程的终止状态之后。
在fork之前改变对信号的配置的原因:fork之后不能保证是父进程还是子进程先运行。如果子进程先运行,父进程在一段时间后再运行,那么父进程将中断信号的配置更改为忽略之前,可能产生这种信号。
注意,子进程在调用execl之前要恢复这两个信号的配置,这就允许在调用者配置的基础上,execl可将它们的配置设置为默认值。
注意system的返回值,它是shell的终止状态,但shell的终止状态并不总是执行命令字符串的终止状态。
Bourne shell有一个特性,其终止状态是128加上一个信号编号,该信号终止了正在执行的命令。用交互式shell可以看到这一点。
$ sh -c "sleep 30" ^C $ echo $? 130 $ sh -c "sleep 30" ^\Quit (core dumped) $ echo $? 131
在所使用系统中,SIGINT的值为2,SIGQUIT的值为3,于是给出shell终止状态130、131。
在编写使用system函数的程序时,一定要正确地解释返回值。如果直接调用fork、exec和wait,则终止状态与调用system是不同的。