socket中的函數遇見EINTR的處理

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 .h>  

  2. #include .h>  

  3. #include .h>  

  4. #include .h>  

  5. #include .h>  

  6. #include .h>  

  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),所以在編寫代碼時,一定要“人爲重啓被中斷的系統調用”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章