中断系统调用与自动重启动

经历了大量的代码实践,每每我们在 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 选项,尽量少用。


原文地址:https://blog.51cto.com/u_15766689/5623168

posted @ 2023-08-23 15:20  HelloMarsMan  阅读(101)  评论(0)    收藏  举报