1. 程式人生 > >Linux中斷的系統調用

Linux中斷的系統調用

例子 off ipc perror depend 重新開始 stat 適用於 data

早期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
技術分享圖片

技術分享圖片

技術分享圖片

Linux中斷的系統調用