早期的UNIX版本中,信号是不可靠的。不可靠在这里指的是,信号可能会丢失:一个信号发生了,但进程却可能一直不知道这一点。同时,进程对信号的控制能力也很差,它能捕捉信号或忽略它。有时用户希望通知内核阻塞一个信号:不要忽略该信号,在其发生时记住它,然后在进程做好准备时再通知它。这种阻塞信号的能力当时是不具备的。
早期版本中的一个问题进程每次接到信号对其进行处理时,随即将该信号动作复位为默认值。
这些早期系统的另一个问题是:进程不希望某种信号发生时,它不能关闭该信号。进程所做的一切就是忽略该信号。
早期UNIX系统的一个特性是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno被设置为EINTR。这要处理的理由是:因为一个信号确实发生了,进程捕捉到了它,这意味着已经发生了某种事情,所以是个应当唤醒阻塞的系统调用的好机会。
为了支持这种特性,将系统调用分为两类:低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,它们包括:
(1) 在读某些类型的文件(管道、终端设备以及网络设备)时,如果数据并不存在则可能会使调用者永远阻塞。
(2) 在写这些类型的文件时,如果不能立即接受这些数据,则也可能会使调用者永远阻塞。
(3) 打开某些类型文件,在某种条件发生之前也可能会使调用者阻塞。
(4) pause(按照定义,它使调用进程休眠直至捕捉到一个信号)和wait函数。
(5) 某些ioctl操作。
(6) 某些进程间通信函数。
在这些低速系统调用中,一个值得注意的例外是与磁盘I/O有关的系统调用。虽然读、写一个磁盘文件可能暂时阻塞调用者(在磁盘驱动器将请求排入队列,然后在适当时间执行请求期间),但是除非发生硬件错误,I/O操作总会很快返回,并使调用者不再处于阻塞状态。
对于中断的read、write系统调用。如若read系统调用已接收并传送数据至应用程序缓冲区,但尚未接收到应用程序全部数据时被中断,操作系统可以认为该系统调用失败,并将errno设置为EINTR;另一种处理方式是允许该系统调用成功返回,返回已接收到的部分数据量。write与此类似。
与被中断的系统调用相关的问题是必须显示地处理出错返回。
为了帮助应用程序使其不必处理被中断的系统调用,某些被中断系统调用自动重启动。自动重启动的系统调用包括ioctl、read、readv、write、writev、wait和waitpid。前5个函数只对低速设备进行操作时才会被信号中断,而wait和waitpid在捕捉到信号时总是被中断。因为这种自动重启动的处理方式也会带来问题,所以某些应用程序并不希望这些函数被中断后重启动。为此,允许进程基于每个信号禁用此功能。
进程捕捉到信号并对其进程处理时,进程正在执行的指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回,则继续执行在捕捉到信号时进程正在执行的正常指令序列。但在信号处理程序中,不能判断捕捉到信号时进程在何处执行。
不可重入函数,其原因为:(a) 已知它们使用静态数据结构,(b) 它们调用malloc或free,(c) 它们是标准I/O函数。标准I/O库的很多实现都以不可重入方式使用全局数据结构。
信号处理程序可能会修改其原先值。作为一个通用的规则,当在信号处理程序中调用某些可重入函数时,应当在其前保存errno,在其后恢复errno。
在程序清单10-2中,信号处理程序my_alarm调用了不可重入函数getpwnam,而my_alarm每秒钟调用一次。
《UNIX环境高级编程》P247:程序清单10-2 在信号处理程序中调用不可重入函数(有改动)
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <string.h> #include <pwd.h> static void my_alarm(int signo) { struct passwd *rootptr; printf("in signal handler\n"); if ((rootptr = getpwnam("root")) == NULL) fprintf(stderr, "getpwnam(root) error\n"); alarm(1); } int main(void) { struct passwd *ptr; signal(SIGALRM, my_alarm); alarm(1); for ( ; ; ) { if ((ptr = getpwnam("user")) == NULL) fprintf(stderr, "getpwnam error\n"); // pause(); if (strcmp(ptr->pw_name, "user") != 0) printf("return value corrupted!, pw_name = %s\n", ptr->pw_name); } exit(0); }
该程序在我的电脑上(Ubuntu 14.04)没要按照预期运行,信号处理程序调用一次之后,就不再调用。(其中原因?)。如果去掉pasue()那行注释,该程序会每秒调用一次信号处理程序。
SIGCLD和SIGCHLD这两个信号很容易混淆。SIGCLD是System V的一个信号名,其语义与名为SIGCHLD的BSD信号不同。POSIX.1 则采用BSD的SIGCHLD信号。
由于历史原因,System V处理SIGCLD信号的方式不同于其他信号。省略掉……
如果SIGCHLD(没打错,有H)被忽略,4.4BSD总是产生僵死进程,如果要避免僵死进程,则必须等待子进程。但Mac OS X 10.3在SIGCHLD被忽略时,并不创建僵死进程。在SVR4中,如果调用signal或sigset将SIGCHLD的配置设置为忽略,则绝不会产生僵死进程。Linux2.4.22和Solaris 9在此方面追随SVR4.
使用sigaction可设置SA_NOCLDWAIT标志以避免僵死进程。
《UNIX环境高级编程》P249:程序清单10-3 不能正常工作的System V SIGCLD处理程序
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <sys/wait.h> static void sig_cld(int signo); int main() { pid_t pid; if (signal(SIGCLD, sig_cld) == SIG_ERR) perror("signal error"); if ((pid = fork()) < 0) { perror("fork error"); } else if (pid == 0) { sleep(2); _exit(0); } pause(); exit(0); } static void sig_cld(int signo) { pid_t pid; int status; printf("SIGCLD received"); if (signal(SIGCLD, sig_cld) == SIG_ERR) perror("signal error"); if ((pid = wait(&status)) < 0) perror("wait error\n"); printf("pid = %d\n", pid); }
在本人系统(Ubuntu 14.04 )上,工作正常。其原因是,当一进程安排捕捉SIGCHLD,并且已经有进程准备好由其父进程等待时,该系统调用并不调用SIGCHLD(SIGCLD)信号的处理程序。
在System V中产生问题是:在信号处理程序开始处调用signal,内核会立即检查是否有需要等待的子进程(因为我们正在处理一个SIGCLD,所以确实有这种子进程),所以它产生另一个对信号处理程序的调用。信号处理程序调用signal,整个过程再次重复。为了解决这一问题,应当在调用wait取到子进程的终止状态后再调用signal。此时仅当其他子进程终止时,内核才会再次产生此种信号。
如果为SIGCHLD建立了一个信号处理程序,又存在一个已终止但父进程尚未等待它的进程,则是否会产生信号?POSIX.1没有对此作说明。但POSIX.1在信号发生时并没有将信号配置复位为默认值,于是在SIGCHLD处理程序中也就不必再为该信号建立一个信号处理程序。
在Linux中SIGCLD等价与SIGCHLD。