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

经历了大量的代码实践,每每我们在 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, writevioctl 被称为“低速”设备,所谓的“低速”设备,是指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 signal
  • ./restart -r 带参数运行,在这种情况下,read 函数会自动重启。

启动后,再开启一个终端,发送 SIGUSR1 或者 SIGALRM 信号给进程,结果如下:

I'm 3643
hello SIGUSR1
hello SIGALRM

5.3 结果分析

从上面的运行结果可以看到,当开启 SA_RESTART 选项时,read 函数不会返回错误。而关闭 SA_RESTART 选项时,read 函数会返回错误(-1),同时把 errno 置为 EINTR 。

很多时候,并不希望进程再接收到 SIGALRM 信号自动重启,APUE 给的解释是:

希望对 I/O 操作可以设置时间限制。

6. 总结

  • 低速设备的定义
  • 什么是慢速系统调用
  • 自动重启的含义
  • 回忆前面的那些程序,想想为什么 sleep 没结束就返回了。
  • 知道哪些函数支持自动重启,哪些函数不支持自动重启

在实际编程中,通常都不开启自动重启选项,目的是让程序被被信号打断后直接返回错误,这可以帮助我们不用再关心哪些函数支持自动重启,哪些函数不支持自动重启。

说白了,SA_RESTART 选项,尽量少用。

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值