经历了大量的代码实践,每每我们在 main 函数中都有这么类似的一句:
while(1) {
write(STDOUT_FILENO, ".", 1);
sleep(...); // read(...), pause(...)
}
- 1
- 2
- 3
- 4
有时候,只要发现信号一来,这后面的 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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 编译
$ gcc restart.c -o restart
- 1
5.2 运行
该程序有两种运行方式:
./restart
不带参数运行,在这种情况下,read 函数不自动重启。
启动后,再开启一个终端,发送 SIGUSR1 或者 SIGALRM 信号给进程,结果如下:
I'm 3626
hello SIGUSR1
Inuterrupted by signal
hello SIGALRM
Inuterrupted by signal
- 1
- 2
- 3
- 4
- 5
./restart -r
带参数运行,在这种情况下,read 函数会自动重启。
启动后,再开启一个终端,发送 SIGUSR1 或者 SIGALRM 信号给进程,结果如下:
I'm 3643
hello SIGUSR1
hello SIGALRM
- 1
- 2
- 3
5.3 结果分析
从上面的运行结果可以看到,当开启 SA_RESTART 选项时,read 函数不会返回错误。而关闭 SA_RESTART 选项时,read 函数会返回错误(-1),同时把 errno 置为 EINTR 。
很多时候,并不希望进程再接收到 SIGALRM 信号自动重启,APUE 给的解释是:
希望对 I/O 操作可以设置时间限制。
6. 总结
- 低速设备的定义
- 什么是慢速系统调用
- 自动重启的含义
- 回忆前面的那些程序,想想为什么 sleep 没结束就返回了。
- 知道哪些函数支持自动重启,哪些函数不支持自动重启
在实际编程中,通常都不开启自动重启选项,目的是让程序被被信号打断后直接返回错误,这可以帮助我们不用再关心哪些函数支持自动重启,哪些函数不支持自动重启。
说白了,SA_RESTART 选项,尽量少用。