1. 程式人生 > >socket中的函式遇見EINTR的處理

socket中的函式遇見EINTR的處理

轉載 : https://blog.csdn.net/benkaoya/article/details/17262053

http://blog.chinaunix.net/uid-21501855-id-4490453.html

這幾天,寫伺服器程式碼過程當中,遇見EINRT訊號的問題,我是借鑑 《unp 》,採用continue或者goto again迴圈解決的。但是感覺這個還是很有必要記錄一下。網路上查詢到的資訊很多。下面是我查詢到的和EINTR有關的介紹:
1  http://blog.csdn.net/yanook/article/details/7226019  慢系統呼叫函式如何處理中斷訊號EINTR


2  http://blog.csdn.net/benkaoya/article/details/17262053 訊號中斷 與 慢系統呼叫
3  http://1.guotie.sinaapp.com/?p=235    socket,accept,connect出現EINTR錯誤的解決方法
個人認為2說的比較明確,建議大家多看看。
我的記錄基本上是對2的抄襲:

慢系統呼叫:可能永遠阻塞的系統呼叫這很關鍵,不適用於非諸塞的情況。永遠阻塞的系統呼叫是指呼叫永遠無法返回,多數網路支援函式都屬於這一類。如:若沒有客戶連線到伺服器上,那麼伺服器的accept呼叫就會一直阻塞。
(以下為抄襲2原文)
EINTR說明:如果程序在一個慢系統呼叫(slow system call)中阻塞時,當捕獲到某個訊號且相應訊號處理函式返回時,這個系統呼叫被中斷,呼叫返回錯誤,設定errno為EINTR(相應的錯誤描述為“Interrupted system call”)。

怎麼看哪些系統條用會產生EINTR錯誤呢?man 7 signal,在ubuntu 10.04上可以檢視,哪些系統呼叫會產生 EINTR錯誤。

如何處理被中斷的系統呼叫

既然系統呼叫會被中斷,那麼別忘了要處理被中斷的系統呼叫。有三種處理方式:

◆ 人為重啟被中斷的系統呼叫

◆ 安裝訊號時設定 SA_RESTART屬性(該方法對有的系統呼叫無效)

◆  忽略訊號(讓系統不產生訊號中斷)

人為重啟被中斷的系統呼叫

人為當碰到EINTR錯誤的時候,有一些可以重啟的系統呼叫要進行重啟,而對於有一些系統呼叫是不能夠重啟的。例如:accept、read、write、select、和open之類的函式來說,是可以進行重啟的。不過對於套接字程式設計中的connect函式我們是不能重啟的

,若connect函式返回一個EINTR錯誤的時候,我們不能再次呼叫它,否則將立即返回一個錯誤。針對connect不能重啟的處理方法是,必須呼叫select來等待連線完成。

這裡的“重啟”怎麼理解?

一些IO系統呼叫執行時,如 read 等待輸入期間,如果收到一個訊號,系統將中斷read, 轉而執行訊號處理函式. 當訊號處理返回後, 系統遇到了一個問題: 是重新開始這個系統呼叫, 還是讓系統呼叫失敗?早期UNIX系統的做法是, 中斷系統呼叫,並讓系統呼叫失敗, 比如read返回 -1, 同時設定 errno 為EINTR中斷了的系統呼叫是沒有完成的呼叫,它的失敗是臨時性的,如果再次呼叫則可能成功,這並不是真正的失敗,所以要對這種情況進行處理, 典型的方式為:

另外,原文建議上去github上看看別人怎麼處理EINTR錯誤的,下面2個處理方面均是截圖過來的:

 

connect處理方式,抄襲3原文,沒有測試過,處理方法是對的。
connect的問題,當connect遇到EINTR錯誤時,不能向上面那樣重新進入迴圈處理,原因是,connect的請求已經發送向對方,正在等待對方迴應,這是如果重新呼叫connect,而對方已經接受了上次的connect請求,這一次的connect就會被拒絕,因此,需要使用select或poll呼叫來檢查socket的狀態,如果socket的狀態就緒,則connect已經成功,否則,視錯誤原因,做對應的處理。

#include poll.h

int check_conn_is_ok(socket_t sock) {
	struct pollfd fd;
	int ret = 0;
	socklen_t len = 0;

	fd.fd = sock;
	fd.events = POLLOUT;

	while ( poll (&fd, 1, -1) == -1 ) {
		if( errno != EINTR ){
			perror("poll");
			return -1;
		}
	}

	len = sizeof(ret);
	if ( getsockopt (sock, SOL_SOCKET, SO_ERROR,
                     &ret,
                     &len) == -1 ) {
    	        perror("getsockopt");
		return -1;
	}

	if(ret != 0) {
		fprintf (stderr, "socket %d connect failed: %s\n",
                 sock, strerror (ret));
		return -1;
	}

	return 0;
}

在呼叫connect時,這樣使用:

#include erron.h

....
if(connnect()) {
    if(errno == EINTR) {
        if(check_conn_is_ok() < 0) {
              perror();
              return -1;
        }
        else {
             printf("connect is success!\n");
        }
    }
    else {
         perror("connect");
         return -1;
    }
}

我一般使用continue或者goto來處理。

安裝訊號時設定 SA_RESTART屬性

我們還可以從訊號的角度來解決這個問題,  安裝訊號的時候, 設定 SA_RESTART屬性,那麼當訊號處理函式返回後, 不會讓系統呼叫返回失敗,而是讓被該訊號中斷的系統呼叫將自動恢復。

[cpp] view plaincopy在CODE上檢視程式碼片派生到我的程式碼片

  1. struct sigaction action;  
  2.    
  3. action.sa_handler = handler_func;  
  4. sigemptyset(&action.sa_mask);  
  5. action.sa_flags = 0;  
  6. /* 設定SA_RESTART屬性 */  
  7. action.sa_flags |= SA_RESTART;  
  8.    
  9. 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.

 

忽略訊號

當然最簡單的方法是忽略訊號,在安裝訊號時,明確告訴系統不會產生該訊號的中斷。

[cpp] view plaincopy在CODE上檢視程式碼片派生到我的程式碼片

  1. struct sigaction action;  
  2.    
  3. action.sa_handler = SIG_IGN;  
  4. sigemptyset(&action.sa_mask);  
  5.    
  6. sigaction(SIGALRM, &action, NULL);  

測試程式碼1

  1. #include   
  2. #include   
  3. #include   
  4. #include   
  5. #include   
  6. #include   
  7.    
  8. void sig_handler(int signum)  
  9. {  
  10.     printf("in handler\n");  
  11.     sleep(1);  
  12.     printf("handler return\n");  
  13. }  
  14.    
  15. int main(int argc, char **argv)  
  16. {  
  17.     char buf[100];  
  18.     int ret;  
  19.     struct sigaction action, old_action;  
  20.    
  21.     action.sa_handler = sig_handler;  
  22.     sigemptyset(&action.sa_mask);  
  23.     action.sa_flags = 0;  
  24.     /* 版本1:不設定SA_RESTART屬性 
  25.      * 版本2:設定SA_RESTART屬性 */  
  26.     //action.sa_flags |= SA_RESTART;  
  27.    
  28.     sigaction(SIGALRM, NULL, &old_action);  
  29.     if (old_action.sa_handler != SIG_IGN) {  
  30.         sigaction(SIGALRM, &action, NULL);  
  31.     }  
  32.     alarm(3);  
  33.      
  34.     bzero(buf, 100);  
  35.    
  36.     ret = read(0, buf, 100);  
  37.     if (ret == -1) {  
  38.         perror("read");  
  39.     }  
  40.    
  41.     printf("read %d bytes:\n", ret);  
  42.     printf("%s\n", buf);  
  43.    
  44.     return 0;  
  45. }  

在ubuntu 10.04 上測試結果:
不設定SA_RESTART,執行結果如下:

 

說明接受訊號處理完成以後,主函式收到EINTR訊號,read函式返回-1,退出
設定SA_RESTART,執行結果如下:

說明設定SA_RESTART引數以後,自動重新呼叫read函式,沒有體現在應用層程式碼中,在應用層看來,這個EINTR沒有造成任何影響。

個人認為下面的總結很重要:

慢系統呼叫(slow system call)會被訊號中斷,系統呼叫函式返回失敗,並且errno被置為EINTR(錯誤描述為“Interrupted system call”)。

處理方法有以下三種:①人為重啟被中斷的系統呼叫;②安裝訊號時設定 SA_RESTART屬性;③忽略訊號(讓系統不產生訊號中斷)。

有時我們需要捕獲訊號,但又考慮到第②種方法的侷限性(設定 SA_RESTART屬性對有的系統無效,如msgrcv),所以在編寫程式碼時,一定要“人為重啟被中斷的系統呼叫”