Linux中断的系统调用

早期UNIX系统的一个特性是:如果在进程执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。这样处理的理由是:因为一个信号发生了,进程捕捉到了它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。

在这里,我们必须区分系统调用和函数。当捕捉到某个信号时,被中断的是内核中的执行的系统调用。

为了支持这种特性,将系统调用分成两类:低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,它们包括:

  • 在读某些类型的文件时,如果数据并不存在则可能会使调用者永远阻塞(管道、终端设备以及网络设备)。

  • 在写这些类型的文件时,如果不能立即接受这些数据,则也可能会使调用者永远阻塞。
  • 打开文件,在某种条件发生之前也可能会使调用者阻塞(例如,打开终端设备,它要等待直到所连接的调制解调器回答了电话)。

  • pause(按照定义,它使调用进程睡眠直至捕捉到一个信号)和wait。
  • 某种ioctl操作。
  • 某些进程间通信函数

在这些低速系统调用中一个值得注意的例外是与磁盘I / O有关的系统调用。虽然读、写一个磁盘文件可能暂时阻塞调用者(在磁盘驱动程序将请求排入队列,然后在适当时间执行请求期间),但是除非发生硬件错误,I / O操作总会很快返回,并使调用者不再处于阻塞状态。

慢系统调用(slow system call):此术语适用于那些可能永远阻塞的系统调用。永远阻塞的系统调用是指调用有可能永远无法返回,多数网络支持函数都属于这一类。如:若没有客户连接到服务器上,那么服务器的accept调用就没有返回的保证。

EINTR错误的产生:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可 能返回一个EINTR错误。例如:在socket服务器端,设置了信号捕获机制,有子进程,当在父进程阻塞于慢系统调用时由父进程捕获到了一个有效信号 时,内核会致使accept返回一个EINTR错误(被中断的系统调用)。

当碰到EINTR错误的时候,可以采取有一些可以重启的系统调 用要进行重启,而对于有一些系统调用是不能够重启的。例如:accept、read、write、select、和open之类的函数来说,是可以进行重 启的。不过对于套接字编程中的connect函数我们是不能重启的,若connect函数返回一个EINTR错误的时候,我们不能再次调用它,否则将立即 返回一个错误。针对connect不能重启的处理方法是,必须调用select来等待连接完成

为了帮助应用程序使其不必处理被中断的系统调用,4.2BSD引进了某些被中断的系统调用的自动再起动。自动再起动的系统调用包括:ioctl、 read、readv、write、writev、wait和waitpid。正如前述,其中前五个函数只有对低速设备进行操作时才会被信号中断。而 wait和waitpid在捕捉到信号时总是被中断。某些应用程序并不希望这些函数被中断后再起动,因为这种自动再起动的处理方式也会带来问题,为此 4.3BSD允许进程在每个信号各别处理的基础上不使用此功能。

怎么看哪些系统条用会产生EINTR错误呢?用man啊!

http://man7.org/linux/man-pages/man7/signal.7.html

 

复制代码
Interruption of system calls and library functions by signal handlers
       If a signal handler is invoked while a system call or library
       function call is blocked, then either:

       * the call is automatically restarted after the signal handler
         returns; or

       * the call fails with the error EINTR.

       Which of these two behaviors occurs depends on the interface and
       whether or not the signal handler was established using the
       SA_RESTART flag (see sigaction(2)).  The details vary across UNIX
       systems; below, the details for Linux.

       If a blocked call to one of the following interfaces is interrupted
       by a signal handler, then the call will be automatically restarted
       after the signal handler returns if the SA_RESTART flag was used;
       otherwise the call will fail with the error EINTR:

           * 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, for example, a terminal, pipe, or
             socket.  (A disk is not a slow device according to this
             definition.)  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).

           * open(2), if it can block (e.g., when opening a FIFO; see
             fifo(7)).

           * wait(2), wait3(2), wait4(2), waitid(2), and waitpid(2).

           * Socket interfaces: accept(2), connect(2), recv(2), recvfrom(2),
             recvmmsg(2), recvmsg(2), send(2), sendto(2), and sendmsg(2),
             unless a timeout has been set on the socket (see below).

           * File locking interfaces: flock(2) and fcntl(2) F_SETLKW.

           * POSIX message queue interfaces: mq_receive(3),
             mq_timedreceive(3), mq_send(3), and mq_timedsend(3).

           * futex(2) FUTEX_WAIT (since Linux 2.6.22; beforehand, always
             failed with EINTR).

           * POSIX semaphore interfaces: sem_wait(3) and sem_timedwait(3)
             (since Linux 2.6.22; beforehand, always failed with EINTR).

       The following interfaces are never restarted after being interrupted
       by a signal handler, regardless of the use of SA_RESTART; they always
       fail with the error EINTR when interrupted by a signal handler:

           * "Input" socket interfaces, when a timeout (SO_RCVTIMEO) has
             been set on the socket using setsockopt(2): accept(2), recv(2),
             recvfrom(2), recvmmsg(2) (also with a non-NULL timeout
             argument), and recvmsg(2).

           * "Output" socket interfaces, when a timeout (SO_RCVTIMEO) has
             been set on the socket using setsockopt(2): connect(2),
             send(2), sendto(2), and sendmsg(2).

           * Interfaces used to wait for signals: pause(2), sigsuspend(2),
             sigtimedwait(2), and sigwaitinfo(2).

           * File descriptor multiplexing interfaces: epoll_wait(2),
             epoll_pwait(2), poll(2), ppoll(2), select(2), and pselect(2).

           * System V IPC interfaces: msgrcv(2), msgsnd(2), semop(2), and
             semtimedop(2).

           * Sleep interfaces: clock_nanosleep(2), nanosleep(2), and
             usleep(3).

           * read(2) from an inotify(7) file descriptor.

           * io_getevents(2).

       The sleep(3) function is also never restarted if interrupted by a
       handler, but gives a success return: the number of seconds remaining
       to sleep.

   Interruption of system calls and library functions by stop signals
       On Linux, even in the absence of signal handlers, certain blocking
       interfaces can fail with the error EINTR after the process is stopped
       by one of the stop signals and then resumed via SIGCONT.  This
       behavior is not sanctioned by POSIX.1, and doesn't occur on other
       systems.

       The Linux interfaces that display this behavior are:

           * "Input" socket interfaces, when a timeout (SO_RCVTIMEO) has
             been set on the socket using setsockopt(2): accept(2), recv(2),
             recvfrom(2), recvmmsg(2) (also with a non-NULL timeout
             argument), and recvmsg(2).

           * "Output" socket interfaces, when a timeout (SO_RCVTIMEO) has
             been set on the socket using setsockopt(2): connect(2),
             send(2), sendto(2), and sendmsg(2), if a send timeout
             (SO_SNDTIMEO) has been set.

           * epoll_wait(2), epoll_pwait(2).

           * semop(2), semtimedop(2).

           * sigtimedwait(2), sigwaitinfo(2).

           * read(2) from an inotify(7) file descriptor.

           * Linux 2.6.21 and earlier: futex(2) FUTEX_WAIT,
             sem_timedwait(3), sem_wait(3).

           * Linux 2.6.8 and earlier: msgrcv(2), msgsnd(2).

           * Linux 2.4 and earlier: nanosleep(2).
复制代码

 

 

 

处理被中断的系统调用

既然系统调用会被中断,那么别忘了要处理被中断的系统调用。有三种处理方式:

◆ 人为重启被中断的系统调用

◆ 安装信号时设置 SA_RESTART属性(该方法对有的系统调用无效)

◆  忽略信号(让系统不产生信号中断)

 

1. 人为重启被中断的系统调用

人为当碰到EINTR错误的时候,有一些可以重启的系统调用要进行重启,而对于有一些系统调用是不能够重启的。例如:accept、read、 write、select、和open之类的函数来说,是可以进行重启的。不过对于套接字编程中的connect函数我们是不能重启的,若connect 函数返回一个EINTR错误的时候,我们不能再次调用它,否则将立即返回一个错误。针对connect不能重启的处理方法是,必须调用select来等待 连接完成。

这里的“重启”怎么理解?

一些IO系统调用执行时,如 read 等待输入期间,如果收到一个信号,系统将中断read, 转而执行信号处理函数. 当信号处理返回后, 系统遇到了一个问题: 是重新开始这个系统调用, 还是让系统调用失败?早期UNIX系统的做法是, 中断系统调用,并让系统调用失败, 比如read返回 -1, 同时设置 errno 为EINTR中断了的系统调用是没有完成的调用,它的失败是临时性的,如果再次调用则可能成功,这并不是真正的失败,所以要对这种情况进行处理, 典型的方式为:

again:
          if ((n = read(fd, buf, BUFFSIZE)) < 0) {
             if (errno == EINTR)
                  goto again;     /* just an interrupted system call */
            /* handle other errors */
          }
复制代码
for( ; ;) {
     if (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) 
    {
    if (errno == EINTR)
        continue;
    }
    else
    {
        errsys("accept error");
    }
}
复制代码

2. 安装信号时设置 SA_RESTART属性

我们还可以从信号的角度来解决这个问题,  安装信号的时候, 设置 SA_RESTART属性,那么当信号处理函数返回后, 不会让系统调用返回失败,而是让被该信号中断的系统调用将自动恢复。

复制代码
struct sigaction action;
 
action.sa_handler = handler_func;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
/* 设置SA_RESTART属性 */
action.sa_flags |= SA_RESTART;
 
sigaction(SIGALRM, &action, NULL);
复制代码

但注意,并不是所有的系统调用都可以自动恢复。如msgsnd喝msgrcv就是典型的例子,msgsnd/msgrcv以block方式发送/接 收消息时,会因为进程收到了信号而中断。此时msgsnd/msgrcv将返回-1,errno被设置为EINTR。且即使在插入信号时设置了 SA_RESTART,也无效。在man msgrcv中就有提到这点:

(msgsnd and msgrcv are never automatically restarted after being interrupted by a signal handler, regardless of the setting of the SA_RESTART flag when establishing a signal handler.)

3. 忽略信号

当然最简单的方法是忽略信号,在安装信号时,明确告诉系统不会产生该信号的中断。

struct sigaction action;
 
action.sa_handler = SIG_IGN;
sigemptyset(&action.sa_mask);
 
sigaction(SIGALRM, &action, NULL);

测试代码

测试代码一

闹钟信号SIGALRM中断read系统调用。安装SIGALRM信号时如果不设置SA_RESTART属性,信号会中断read系统过调用。如果设置了SA_RESTART属性,read就能够自己恢复系统调用,不会产生EINTR错误。

复制代码
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
#include <unistd.h>
 
void sig_handler(int signum)
{
    printf("in handler\n");
    sleep(1);
    printf("handler return\n");
}
 
int main(int argc, char **argv)
{
    char buf[100];
    int ret;
    struct sigaction action, old_action;
 
    action.sa_handler = sig_handler;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    /* 版本1:不设置SA_RESTART属性
     * 版本2:设置SA_RESTART属性 */
    //action.sa_flags |= SA_RESTART;
 
    sigaction(SIGALRM, NULL, &old_action);
    if (old_action.sa_handler != SIG_IGN) {
        sigaction(SIGALRM, &action, NULL);
    }
    alarm(3);
   
    bzero(buf, 100);
 
    ret = read(0, buf, 100);
    if (ret == -1) {
        perror("read");
    }
 
    printf("read %d bytes:\n", ret);
    printf("%s\n", buf);
 
    return 0;
}
复制代码

测试代码二

闹钟信号SIGALRM中断msgrcv系统调用。即使在插入信号时设置了SA_RESTART,也无效。

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
 
void ding(int sig)
{
    printf("Ding!\n");
}
 
struct msgst
{
    long int msg_type;
    char buf[1];
};
 
int main()
{
    int nMsgID = -1;
 
    // 捕捉闹钟信息号
    struct sigaction action;
    action.sa_handler = ding;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    // 版本1:不设置SA_RESTART属性
    // 版本2:设置SA_RESTART属性
    action.sa_flags |= SA_RESTART;
    sigaction(SIGALRM, &action, NULL);
   
    alarm(3);
    printf("waiting for alarm to go off\n");
 
    // 新建消息队列
    nMsgID = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    if( nMsgID < 0 )
    {
        perror("msgget fail" );
        return;
    }
    printf("msgget success.\n");
 
    // 阻塞 等待消息队列
    //
    // msgrcv会因为进程收到了信号而中断。返回-1,errno被设置为EINTR。
    // 即使在插入信号时设置了SA_RESTART,也无效。man msgrcv就有说明。
    //
    struct msgst msg_st;
    if( -1 == msgrcv( nMsgID, (void*)&msg_st, 1, 0, 0 ) )
    {
        perror("msgrcv fail");
    }
 
    printf("done\n");
 
    exit(0);
}
复制代码

总结

慢系统调用(slow system call)会被信号中断,系统调用函数返回失败,并且errno被置为EINTR(错误描述为“Interrupted system call”)。

处理方法有以下三种:①人为重启被中断的系统调用;②安装信号时设置 SA_RESTART属性;③忽略信号(让系统不产生信号中断)。

有时我们需要捕获信号,但又考虑到第②种方法的局限性(设置 SA_RESTART属性对有的系统无效,如msgrcv),所以在编写代码时,一定要“人为重启被中断的系统调用”。

 

非阻塞(nonblock)socket接口会否出现EINTR错误

对于socket接口(指connect/send/recv/accept..等等后面不重复,不包括不能设置非阻塞的如select),在阻塞模式下有可能因为发生信号,返回EINTR错误,由用户做重试或终止。 

但是,在非阻塞模式下,是否出现这种错误呢? 
对此,重温了系统调用、信号、socket相关知识,得出结论是:不会出现。 

首先, 
1.信号的处理是在用户态下进行的,也就是必须等待一个系统调用执行完了才会执行进程的信号函数,所以就有了信号队列保存未执行的信号 
2.用户态下被信号中断时,内核会记录中断地址,信号处理完后,如果进程没有退出则重回这个地址继续执行 

socket接口是一个系统调用,也就是即使发生了信号也不会中断,必须等socket接口返回了,进程才能处理信号。
也就是,EINTR错误是socket接口主动抛出来的,不是内核抛的。socket接口也可以选择不返回,自己内部重试之类的.. 

那阻塞的时候socket接口是怎么处理发生信号的? 

举例 
socket接口,例如recv接口会做2件事情, 
1.检查buffer是否有数据,有则复制清除返回 
2.没有数据,则进入睡眠模式,当超时、数据到达、发生错误则唤醒进程处理 

socket接口的实现都差不了太多,抽象说 
1.资源是否立即可用,有则返回 
2.没有,就等... 

对于 
1.这个时候不管有没信号,也不返回EINTR,只管执行自己的就可以了 
2.采用睡眠来等待,发生信号的时候进程会被唤醒,socket接口唤醒后检查有无未处理的信号(signal_pending)会返回EINTR错误。 

所以 
socket接口并不是被信号中断,只是调用了睡眠,发生信号睡眠会被唤醒通知进程,然后socket接口选择主动退出,这样做可以避免一直阻塞在那里,有退出的机会。非阻塞时不会调用睡眠。 

我们看看linux内核里的实现 
linux kernel 3.5.5 
 

 

posted on 2018-02-13 16:32  AlanTu  阅读(2970)  评论(0编辑  收藏  举报

导航