中断系统调用与自动重启动
经历了大量的代码实践,每每我们在 main 函数中都有这么类似的一句:
while(1) {
write(STDOUT_FILENO, ".", 1);
sleep(...); // read(...), pause(...)
}
有时候,只要发现信号一来,这后面的 sleep 或者 pause 被信号中断后都会失效。不过你还没见过 read 也失效的情况,那是因为之前我们一直用的 signal 信号注册函数。或者说,signal 默认情况下设置了自动重启动属性。
其实按照正常的逻辑,它们在中断后,本应该就直接返回,不是吗?(如果不理解,速速对照上一篇博文来理解 《打通你的任督二脉-信号处理函数的执行期》 ),不正常的是 read 才对,read 如果被信号打断,难道不应该直接返回吗?它是如何做到的?上一节我们提到,只要进程接收到了信号,即使请求的某些资源还没到来,进程照样会被调度到。这很可能导致 read 在没读取到数据就直接返回了。
接下来,我们一探究竟。
1. 低速系统调用与其它系统调用
下面这段话引用片 man page:
read(2), readv(2), write(2), writev(2), and ioctl(2) calls on “slow” devices. A “slow” device is one where the I/O call may block for an indefinite time.
意思是说,read, readv, write, writev 和 ioctl 被称为“低速”设备,所谓的“低速”设备,是指I/O 调用可能会被永远阻塞。
for example, a terminal, pipe, or socket. If an I/O call on a slow device has already transferred some data by the time it is interrupted by a signal handler, then the call will return a success status (normally, the number of bytes transferred).
例如,终端,管道或者套接字。如果低速设备上的 I/O 调用正在传输数据的过程中被信号打断,则返回传输的字节数。
Note that a (local) disk is not a slow device according to this definition; I/O operations on disk devices are not interrupted by signals.
需要注意的是:根据定义,本地磁盘不是慢设备!磁盘设备上的 I/O 操作是不会被信号打断的!
对于上面这句,APUE 给的解释是这样的:
虽然读写磁盘文件可能会暂时阻塞调用者(磁盘将驱动程序将请求保存到队列,最后会在适当的时期执行该请求),除非发生硬件错误,否则 I/O 操作总是很快返回,并使调用者不在处于阻塞状态。
综合以上的论述,我们可以认为,只要可能导致 I/O 永远 阻塞的,就是慢速系统调用。(关键词:可能,永远)
按照定义,pause 函数是慢速的,而 sleep 不是(仔细体会)。
2. 再谈信号处理函数执行期
按照 APUE 的说法,只有对低速设备进行操作的时候,才会被信号中断!!!
回到篇首语,其中讲到只要进程接收到了信号(未被阻塞),即使请求的资源还没到来,进程照样会被调度到,这句话就得修正为:
只要进程接收到了信号(未被阻塞),同时执行 I/O 操作位于低速设备上,即使请求的资源还没到来,进程照样会被调度到
3. 低速系统调用被信号中断
这里有两种情况:
低速系统调用已经收到 n 字节的数据时被信号中断,按照 POSIX 语义,成功返回已读取的字节数 n!(System V 语义是返回错误,而 linux 是遵守 POSIX 标准的)
低速系统调用尚未收到数据,被信号中断,返回错误(-1),同时 errno 变量置为 EINTR (error interrupt)
4. 什么是自动重启
有些慢速系统调用,被信号中断后,本应该返回错误的,但是通过开启 struct sigaction 成员 sa_flags 的 SA_RESTART 选项,这些慢速系统调用就不会返回错误,而是重新执行一次!!!
如果你使用了 signal 信号注册函数,SA_RESTART 选项默认就是开启的(大多数时候,我们并不希望开启此选项)。
4.1 能够自动重启的系统调用
read(2), readv(2), write(2), writev(2), ioctl(2).
open(2)(在打开 FIFO 文件时).
wait(2), wait3(2), wait4(2), waitid(2), waitpid(2).
socket 接口accept(2), connect(2), recv(2), recvfrom(2), recvmmsg(2), recvmsg(2), send(2), sendto(2), and sendmsg(2).(未设置超时时间的情况下)
文件锁接口flock(2), 以及fcntl(2) 在使用 F_SETLKW 和 F_OFD_SETLKW 时.
消息队列mq_receive(3), mq_timedreceive(3), mq_send(3), mq_timedsend(3).
futex(3) FUTEX_WAIT (2.6.22 内核以前不支持自动重启)
getrandom(2).
pthread_mutex_lock(3), pthread_cond_wait(3) 和相关 api.
信号量相关的函数sem_wait(3), sem_timedwait(3) (2.6.22 内核以前不支持自动重启).
4.2 不能自动重启的系统调用
不能自动重启的系统调用无视 SA_RESTART 开关。
socket 读相关的接口, 在使用了setsockopt(2) 函数设置了 SO_RCVTIMEO 的情况下:accept(2), recv(2), recvfrom(2), recvmmsg(2) recvmsg(2).
socket 写相关的接口,在使用了setsockopt(2) 函数设置了 SO_RCVTIMEO 的情况下:connect(2), send(2), sendto(2), and sendmsg(2)
等待信号的函数:pause(2), sigsuspend(2), sigtimedwait(2), and sigwaitinfo(2)
多路复用:epoll_wait(2), epoll_pwait(2), poll(2), ppoll(2), select(2), pselect(2).
System V 进程间通信接口:msgrcv(2), msgsnd(2), semop(2), semtimedop(2).
sleep 相关接口:clock_nanosleep(2), nanosleep(2), usleep(3).
read(2) 读取inotify(7) 返回的描述符.
io_getevents(2)
以上函数被信号中断都会返回失败,同时 errno 置 EINTR.
另外,还有一个比较奇葩的函数 sleep(3),要单独挑出来打一顿,它不支持自动重启,但是被信号中断了它能够成功返回剩余时间的秒数。
5. 实例
看了如此多的概念,相信你也烦了。下面这段代码就演示 read 从终端读取数据时自动重启和不自动重启两种情况。
5.1 程序清单
代码
// restart.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void handler(int sig) {
switch(sig) {
case SIGUSR1:
printf("hello SIGUSR1\n");break;
case SIGALRM:
printf("hello SIGALRM\n");break;
}
}
int main(int argc, char* argv[]) {
char buf[16] = { 0 };
int n = 0;
printf("I'm %d\n", getpid());
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 如果进程带参数 -r,则开启自动重启选项
if (argc >= 2 && strcmp(argv[1], "-r") == 0) {
act.sa_flags |= SA_RESTART;
}
if (sigaction(SIGUSR1, &act, NULL) < 0) {
perror("signal SIGUSR1");
}
if (sigaction(SIGALRM, &act, NULL)) {
perror("signal SIGALRM");
}
while(1) {
if ((n = read(STDIN_FILENO, buf, 15)) < 0) {
if (errno == EINTR) { // 如果 read 返回错误,检查 errno,判断是否被信号中断
printf("Inuterrupted by signal\n");
}
}
else {
buf[n] = 0;
printf("%s", buf);
}
}
return 0;
}
编译
$ gcc restart.c -o restart
5.2 运行
该程序有两种运行方式:
./restart 不带参数运行,在这种情况下,read 函数不自动重启。
启动后,再开启一个终端,发送 SIGUSR1 或者 SIGALRM 信号给进程,结果如下:
I'm 3626
hello SIGUSR1
Inuterrupted by signal
hello SIGALRM
Inuterrupted by
./restart -r 带参数运行,在这种情况下,read 函数会自动重启。
启动后,再开启一个终端,发送 SIGUSR1 或者 SIGALRM 信号给进程,结果如下:
I'm 3643
5.3 结果分析
从上面的运行结果可以看到,当开启 SA_RESTART 选项时,read 函数不会返回错误。而关闭 SA_RESTART 选项时,read 函数会返回错误(-1),同时把 errno 置为 EINTR 。
很多时候,并不希望进程再接收到 SIGALRM 信号自动重启,APUE 给的解释是:
希望对 I/O 操作可以设置时间限制。
6. 总结
低速设备的定义
什么是慢速系统调用
自动重启的含义
回忆前面的那些程序,想想为什么 sleep 没结束就返回了。
知道哪些函数支持自动重启,哪些函数不支持自动重启
在实际编程中,通常都不开启自动重启选项,目的是让程序被被信号打断后直接返回错误,这可以帮助我们不用再关心哪些函数支持自动重启,哪些函数不支持自动重启。
说白了,SA_RESTART 选项,尽量少用。

浙公网安备 33010602011771号