1、什么是慢系統(tǒng)調用?
該術語適用于那些可能永遠阻塞的系統(tǒng)調用。永遠阻塞的系統(tǒng)調用是指調用永遠無法返回,多數(shù)網(wǎng)絡支持函數(shù)都屬于這一類。如:若沒有客戶連接到服務器上,那么服務器的accept調用就會永遠阻塞。
慢系統(tǒng)調用可以被永久阻塞,包括以下幾個類別:
(1)讀寫‘慢’設備(包括pipe,終端設備,網(wǎng)絡連接等)。讀時,數(shù)據(jù)不存在,需要等待;寫時,緩沖區(qū)滿或其他原因,需要等待。
(2)當打開某些特殊文件時,需要等待某些條件,才能打開。例如:打開中斷設備時,需要等到連接設備的modem響應才能完成。
(3)pause和wait函數(shù)。pause函數(shù)使調用進程睡眠,直到捕獲到一個信號。wait等待子進程終止。
(4)某些ioctl操作。
(5)某些IPC操作。
2、EINTR錯誤產生的原因-(阻塞的系統(tǒng)調用、或者非阻塞的系統(tǒng)調用)
如果進程在一個慢系統(tǒng)調用(slow system call)中阻塞時,當捕獲到某個信號且相應信號處理函數(shù)返回時,這個系統(tǒng)調用不再阻塞而是被中斷,就會調用返回錯誤(一般為-1)&&設置errno為EINTR(相應的錯誤描述為“Interrupted system call”)。
如下表所示的系統(tǒng)調用就會產生EINTR錯誤,當然不同的函數(shù)意義也不同。
3、解決辦法
既然系統(tǒng)調用會被中斷,那么別忘了要處理被中斷的系統(tǒng)調用。有三種處理方式:
解決方法1:重啟被中斷的系統(tǒng)調用
當碰到EINTR錯誤的時候,有一些可以重啟的系統(tǒng)調用要進行重啟,而對于有一些系統(tǒng)調用是不能夠重啟的。例如:accept、read、write、select、和open之類的函數(shù)來說,是可以進行重啟的。不過對于套接字編程中的connect函數(shù)是不能重啟的,若connect函數(shù)返回一個EINTR錯誤的時候,我們不能再次調用它,否則將立即返回一個錯誤。針對connect不能重啟的處理方法是,必須調用select來等待連接完成。
理解“重啟”?一些IO系統(tǒng)調用執(zhí)行時,如 read 等待輸入期間,如果收到一個信號,系統(tǒng)將中斷read, 轉而執(zhí)行信號處理函數(shù). 當信號處理返回后, 系統(tǒng)遇到了一個問題: 是重新開始這個系統(tǒng)調用? 還是讓系統(tǒng)調用失敗?早期UNIX系統(tǒng)的做法是:中斷系統(tǒng)調用,并讓系統(tǒng)調用失敗, 比如read返回 -1, 同時設置 errno 為EINTR中斷了的系統(tǒng)調用是沒有完成的調用,它的失敗是臨時性的,如果再次調用則可能成功,這并不是真正的失敗,所以要對這種情況進行處理, 典型的方式為“重啟”,采用accept函數(shù)為例子,代碼如下
需要C/C++ linux服務器架構師學習資料私信“資料”(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協(xié)程,DPDK,ffmpeg等),免費分享
ACCEPT:
clifd = accept(srvfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
if (clifd == -1) {
if (errno == EINTR) {
goto ACCEPT;
} else {
fprintf(stderr, "accept fail,error:%sn", strerror(errno));
return -1;
}
}
解決方法2:安裝信號時設置 SA_RESTART屬性(該方法對有的系統(tǒng)調用無效)
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);
解決方法3: 忽略信號(讓系統(tǒng)不產生信號中斷)
struct sigaction action;
action.sa_handler = SIG_IGN;
sigemptyset(&action.sa_mask);
sigaction(SIGALRM, &action, NULL);
EAGAIN-(一般用于非阻塞的系統(tǒng)調用)
非阻塞的系統(tǒng)調用,由于資源限制/不滿足條件,導致返回值為EAGAIN
在Linux環(huán)境下開發(fā)經(jīng)常會碰到很多錯誤(設置errno),其中EAGAIN是其中比較常見的一個錯誤(比如用在非阻塞操作中)。
如:首先是把套接字設置為異步的了,然后在使用write發(fā)送數(shù)據(jù)時采取的方式是循環(huán)發(fā)送大量的數(shù)據(jù);由于是異步的,writesend將要發(fā)送的數(shù)據(jù)提交到發(fā)送緩沖區(qū)后是立即返回的,并不需要對端確認數(shù)據(jù)已接收。在這種情況下是很有可能出現(xiàn)發(fā)送緩沖區(qū)被填滿,導致writesend無法再向緩沖區(qū)提交要發(fā)送的數(shù)據(jù)。因此就產生了Resource temporarily unavailable的錯誤(資源暫時不可用),EAGAIN 的意思也很明顯,就是要你再次嘗試。
從字面上來看,是提示再試一次。這個錯誤經(jīng)常出現(xiàn)在當應用程序進行一些非阻塞(non-blocking)操作(對文件或socket)的時候。
如:以 O_NONBLOCK的標志打開文件/socket/FIFO,如果連續(xù)做read操作而沒有數(shù)據(jù)可讀。此時程序不會阻塞起來等待數(shù)據(jù)準備就緒返回,read函數(shù)會返回一個錯誤EAGAIN,提示你的應用程序現(xiàn)在沒有數(shù)據(jù)可讀請稍后再試。
又例如,當一個系統(tǒng)調用(比如fork)因為沒有足夠的資源(比如虛擬內存)而執(zhí)行失敗,返回EAGAIN提示其再調用一次(也許下次就能成功)。
Linux - 非阻塞socket編程處理EAGAIN錯誤
在linux進行非阻塞的socket接收數(shù)據(jù)時經(jīng)常出現(xiàn)Resource temporarily unavailable,errno代碼為11(EAGAIN),這是什么意思? ⇒ ⇒ ⇒ 這表明在非阻塞模式下調用了阻塞操作,在該操作沒有完成就返回這個錯誤,這個錯誤不會破壞socket的同步,不用管它,下次循環(huán)接著recv就可以。對非阻塞socket而言,EAGAIN不是一種錯誤。在VxWorks和windows上,EAGAIN的名字叫做EWOULDBLOCK。
iReadSizeOnce=read(iOpenCom,RxBuf+iReadSize,1024);
if (iReadSizeOnce != ZERO)
{
if (iReadSizeOnce != EAGAIN)
{
continue;
}
else
{
//stCComApiLog.LogError("讀串口操作錯誤");
return(FUN_ERROR);
}
}
慢系統(tǒng)調用:可能永遠阻塞的系統(tǒng)調用,這很關鍵,不適用于非諸塞的情況。永遠阻塞的系統(tǒng)調用是指調用永遠無法返回,多數(shù)網(wǎng)絡支持函數(shù)都屬于這一類。如:若沒有客戶連接到服務器上,那么服務器的accept調用就會一直阻塞。
EINTR說明:如果進程在一個慢系統(tǒng)調用(slow system call)中阻塞時,當捕獲到某個信號且相應信號處理函數(shù)返回時,這個系統(tǒng)調用被中斷,調用返回錯誤,設置errno為EINTR(相應的錯誤描述為“Interrupted system call”)。
怎么看哪些系統(tǒng)條用會產生EINTR錯誤呢?man 7 signal,在ubuntu 10.04上可以查看,哪些系統(tǒng)調用會產生 EINTR錯誤。
如何處理被中斷的系統(tǒng)調用
既然系統(tǒng)調用會被中斷,那么別忘了要處理被中斷的系統(tǒng)調用。有三種處理方式:
◆ 人為重啟被中斷的系統(tǒng)調用
◆ 安裝信號時設置 SA_RESTART屬性(該方法對有的系統(tǒng)調用無效)
◆ 忽略信號(讓系統(tǒng)不產生信號中斷)
人為重啟被中斷的系統(tǒng)調用
人為當碰到EINTR錯誤的時候,有一些可以重啟的系統(tǒng)調用要進行重啟,而對于有一些系統(tǒng)調用是不能夠重啟的。例如:accept、read、write、select、和open之類的函數(shù)來說,是可以進行重啟的。不過對于套接字編程中的connect函數(shù)我們是不能重啟的,若connect函數(shù)返回一個EINTR錯誤的時候,我們不能再次調用它,否則將立即返回一個錯誤。針對connect不能重啟的處理方法是,必須調用select來等待連接完成。
這里的“重啟”怎么理解?
一些IO系統(tǒng)調用執(zhí)行時,如 read 等待輸入期間,如果收到一個信號,系統(tǒng)將中斷read, 轉而執(zhí)行信號處理函數(shù). 當信號處理返回后, 系統(tǒng)遇到了一個問題: 是重新開始這個系統(tǒng)調用, 還是讓系統(tǒng)調用失敗?早期UNIX系統(tǒng)的做法是, 中斷系統(tǒng)調用,并讓系統(tǒng)調用失敗, 比如read返回 -1, 同時設置 errno 為EINTR中斷了的系統(tǒng)調用是沒有完成的調用,它的失敗是臨時性的,如果再次調用則可能成功,這并不是真正的失敗,所以要對這種情況進行處理, 典型的方式為:
connect處理方式,抄襲3原文,沒有測試過,處理方法是對的。
connect的問題,當connect遇到EINTR錯誤時,不能向上面那樣重新進入循環(huán)處理,原因是,connect的請求已經(jīng)發(fā)送向對方,正在等待對方回應,這是如果重新調用connect,而對方已經(jīng)接受了上次的connect請求,這一次的connect就會被拒絕,因此,需要使用select或poll調用來檢查socket的狀態(tài),如果socket的狀態(tài)就緒,則connect已經(jīng)成功,否則,視錯誤原因,做對應的處理。
#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: %sn",
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屬性,那么當信號處理函數(shù)返回后, 不會讓系統(tǒng)調用返回失敗,而是讓被該信號中斷的系統(tǒng)調用將自動恢復。
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);
但注意,并不是所有的系統(tǒng)調用都可以自動恢復。如msgsnd喝msgrcv就是典型的例子,msgsnd/msgrcv以block方式發(fā)送/接收消息時,會因為進程收到了信號而中斷。此時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.
忽略信號
當然最簡單的方法是忽略信號,在安裝信號時,明確告訴系統(tǒng)不會產生該信號的中斷。
struct sigaction action;
action.sa_handler = SIG_IGN;
sigemptyset(&action.sa_mask);
sigaction(SIGALRM, &action, NULL);
#include
#include
#include
#include
#include
#include
void sig_handler(int signum)
{
printf("in handlern");
sleep(1);
printf("handler returnn");
}
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("%sn", buf);
return 0;
}
在linux測試結果:
不設置SA_RESTART,執(zhí)行結果如下:
說明接受信號處理完成以后,主函數(shù)收到EINTR信號,read函數(shù)返回-1,退出
設置SA_RESTART,執(zhí)行結果如下:
說明設置SA_RESTART參數(shù)以后,自動重新調用read函數(shù),沒有體現(xiàn)在應用層代碼中,在應用層看來,這個EINTR沒有造成任何影響。
總結:
慢系統(tǒng)調用(slow system call)會被信號中斷,系統(tǒng)調用函數(shù)返回失敗,并且errno被置為EINTR(錯誤描述為“Interrupted system call”)。
處理方法有以下三種:①人為重啟被中斷的系統(tǒng)調用;②安裝信號時設置 SA_RESTART屬性;③忽略信號(讓系統(tǒng)不產生信號中斷)。
有時我們需要捕獲信號,但又考慮到第②種方法的局限性(設置 SA_RESTART屬性對有的系統(tǒng)無效,如msgrcv),所以在編寫代碼時,一定要“人為重啟被中斷的系統(tǒng)調用”。