1. 程式人生 > >linux socket程式設計 出現訊號SIGPIPE,分析及解決

linux socket程式設計 出現訊號SIGPIPE,分析及解決

在編寫一個仿QQ軟體,C/S模式。出現的問題:當客戶機關閉時,伺服器也隨著關閉,糾結很久之後,我gdb了下,出現下面提示資訊:

Program received signal SIGPIPE, Broken pipe.
0x0012e416 in __kernel_vsyscall ()

在 網上查了一下出現SIGPIPE的原因:如果嘗試send到一個已關閉的 socket上兩次,就會出現此訊號,也就是用協議TCP的socket程式設計,伺服器是不能知道客戶機什麼時候已經關閉了socket,導致還在向該已關 閉的socket上send,導致SIGPIPE。

而系統預設產生SIGPIPE訊號的措施是關閉程序,所以出現了伺服器也退出。

下面分析TCP協議的缺陷以至於伺服器無法及時判斷對方socket已關閉:

具 體的分析可以結合TCP的"四次握手"關閉. TCP是全雙工的通道, 可以看作兩條單工通道, TCP連線兩端的兩個端點各負責一條. 當對端呼叫close時, 雖然本意是關閉整個兩條通道, 但本端只是收到FIN包. 按照TCP協議的語義, 表示對端只是關閉了其所負責的那一條單工通道, 仍然可以繼續接收資料. 也就是說, 因為TCP協議的限制, 一個端點無法獲知對端的socket是呼叫了close還是shutdown.(此段網上抄來的)

解決方法:

重新定義遇到SIGPIPE的措施,signal(SIGPIPE,   SIG_IGN);具體措施在函式SIG_IGN裡面寫。

摘自:

當伺服器close一個連線時,若client端接著發資料。根據TCP協議的規定,會收到一個RST響應,client再往這個伺服器傳送資料時,系統會發出一個SIGPIPE訊號給程序,告訴程序這個連線已經斷開了,不要再寫了。

又或者當一個程序向某個已經收到RST的socket執行寫操作是,核心向該程序傳送一個SIGPIPE訊號。該訊號的預設學位是終止程序,因此程序必須捕獲它以免不情願的被終止。


根據訊號的預設處理規則SIGPIPE訊號的預設執行動作是terminate(終止、退出),所以client會退出。若不想客戶端退出可以把 SIGPIPE設為SIG_IGN

如:signal(SIGPIPE, SIG_IGN);
這時SIGPIPE交給了系統處理。

伺服器採用了fork的話,要收集垃圾程序,防止殭屍程序的產生,可以這樣處理:
signal(SIGCHLD,SIG_IGN);
交給系統init去回收。
這裡子程序就不會產生殭屍程序了。


在linux下寫socket的程式的時候,如果嘗試send到一個disconnected socket上,就會讓底層丟擲一個SIGPIPE訊號。
這個訊號的預設處理方法是退出程序,大多數時候這都不是我們期望的。因此我們需要過載這個訊號的處理方法。呼叫以下程式碼,即可安全的遮蔽SIGPIPE:
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction( SIGPIPE, &sa, 0 );

signal設定的訊號控制代碼只能起一次作用,訊號被捕獲一次後,訊號控制代碼就會被還原成預設值了。
sigaction設定的訊號控制代碼,可以一直有效,值到你再次改變它的設定。

struct sigaction action;
action.sa_handler = handle_pipe;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGPIPE, &action, NULL);
void handle_pipe(int sig)
{
//不做任何處理即可
}

RST的含義為“復位”,它是TCP在某些錯誤情況下所發出的一種TCP分節。有三個條件可以產生RST:

1), SYN到達某埠但此埠上沒有正在監聽的伺服器。
2), TCP想取消一個已有連線
3), TCP接收了一個根本不存在的連線上的分節。

1. Connect 函式返回錯誤ECONNREFUSED:
如果對客戶的SYN的響應是RST,則表明該伺服器主機在我們指定的埠上沒有程序在等待與之連線(例如伺服器程序也許沒有啟動),這稱為硬錯(hard error),客戶一接收到RST,馬上就返回錯誤ECONNREFUSED.

TCP為監聽套介面維護兩個佇列。兩個佇列之和不超過listen函式第二個引數backlog。

當一個客戶SYN到達時,若兩個佇列都是滿的,TCP就忽略此分節,且不傳送RST.這個因為:這種情況是暫時的,客戶TCP將重發SYN,期望不久就能 在佇列中找到空閒條目。要是TCP伺服器傳送了一個RST,客戶connect函式將立即傳送一個錯誤,強制應用程序處理這種情況,而不是讓TCP正常的 重傳機制來處理。還有,客戶區別不了這兩種情況:作為SYN的響應,意為“此埠上沒有伺服器”的RST和意為“有伺服器在此埠上但其佇列滿”的 RST.
Posix.1g允許以下兩種處理方法:忽略新的SYN,或為此SYN響應一個RST.歷史上,所有源自Berkeley的實現都是忽略新的SYN。

2.如果殺掉伺服器端處理客戶端的子程序,程序退出後,關閉它開啟的所有檔案描述符,此時,當伺服器TCP接收到來自此客戶端的資料時,由於先前開啟的那個套接字介面的程序已終止,所以以RST響應。

經常遇到的問題:
如果不判斷read , write函式的返回值,就不知道伺服器是否響應了RST, 此時客戶端如果向接收了RST的套介面進行寫操作時,核心給該程序發一個SIGPIPE訊號。此訊號的預設行為就是終止程序,所以,程序必須捕獲它以免不情願地被終止。

程序不論是捕獲了該訊號並從其訊號處理程式返回,還是不理會該訊號,寫操作都返回EPIPE錯誤。

3. 伺服器主機崩潰後重啟
如果伺服器主機與客戶端建立連線後崩潰,如果此時,客戶端向伺服器傳送資料,而伺服器已經崩潰不能響應客戶端ACK,客戶TCP將持續重傳資料分節,試圖從伺服器上接收一個ACK,如果伺服器一直崩潰客戶端會發現伺服器已經崩潰或目的地不可達,但可能需要比較長的時間; 如果伺服器在客戶端發現崩潰前重啟,伺服器的TCP丟失了崩潰前的所有連線資訊,所以伺服器TCP對接收的客戶資料分節以RST響應。

二、關於socket的recv:

對於TCP non-blocking socket, recv返回值== -1,但是errno == EAGAIN, 此時表示在執行recv時相應的socket buffer中沒有資料,應該繼續recv。

【If no messages are available at the socket and O_NONBLOCK is not set on the socket's file descriptor, recv() shall block until a message arrives. If no messages are available at the socket and O_NONBLOCK is set on thesocket's file descriptor, recv() shall fail and set errno to [EAGAIN] or [EWOULDBLOCK].】

對於UDP recv 應該一直讀取直到recv()==-1 && errno==EAGAIN,表示buffer中資料包被全部讀取。

接收資料時常遇到Resource temporarily unavailable的提示,errno程式碼為11(EAGAIN)。這表明你在非阻塞模式下呼叫了阻塞操作,在該操作沒有完成就返回這個錯誤,這個錯誤不會破壞socket的同步,不用管它,下次迴圈接著recv就可以。對非阻塞socket而言,EAGAIN不是一種錯誤。在VxWorks和 Windows上,EAGAIN的名字叫做EWOULDBLOCK。其實這算不上錯誤,只是一種異常而已。


while (res != 0)
{
//len = recv(sockfd, buff, MAXBUF, 0);


len = recv(sockfd, buff, 5, 0);
if (len <</span> 0 ) {
if(errno == EAGAIN) {
printf("RE-Len:%d errno EAGAIN\n", len);
continue;

}

if (errno == EINTR)

continue;
perror("recv error\n");
break;
} else if (len > 0) {
printf("Recved:%s, and len is:%d \n", buff, len);
len = send(sockfd, buff, len, 0);
if (len <</span> 0) {
perror("send error");
return -1;
}
memset(buff, 0, MAXBUF);
continue;
} else { //==0

printf("Disconnected by peer!\n");
res = 0;

return res;
}
}



外記:
accetp()是慢系統呼叫,在訊號產生時會中斷其呼叫並將errno變數設定為EINTR,此時應重新呼叫accept()。
所以使用時應這樣:


while(1) {
if ( (connfd = accept(....)) == -) {
if (errno == EINTR)
continue;
perror("accept()");
exit(1);
}



}



signal 與 sigaction 區別:
signal函式每次設定具體的訊號處理函式(非SIG_IGN)只能生效一次,每次在程序響應處理訊號時,隨即將訊號處理函式恢復為預設處理方式.所以如果想多次相同方式處理某個訊號,通常的做法是,在響應函式開始,再次呼叫signal設定。

int sig_int(); //My signal handler

...
signal(SIGINT, sig_int);
...

int sig_int()
{

signal(SIGINT, sig_int);
....
}

這種程式碼段的一個問題是:在訊號發生之後到訊號處理程式中呼叫s i g n a l函式之間有一個
時間視窗。在此段時間中,可能發生另一次中斷訊號。第二個訊號會造成執行預設動作,而對
中斷訊號則是終止該程序。這種型別的程式段在大多數情況下會正常工作,使得我們認為它們
正確,而實際上卻並不是如此。
另一個問題是:在程序不希望某種訊號發生時,它不能關閉該訊號

sigaction:
1.在訊號處理程式被呼叫時,系統建立的新訊號遮蔽字會自動包括正被遞送的訊號。因此保證了在處理一個
給定的訊號時,如果這種訊號再次發生,那麼它會被阻塞到對前一個訊號的處理結束為止
2.響應函式設定後就一直有效,不會重置
3.對除S I G A L R M以外的所有訊號都企圖設定S A _ R E S TA RT標誌,於是被這些訊號中斷
的系統呼叫(read,write)都能自動再起動。不希望再起動由S I G A L R M訊號中斷的系統呼叫的原因是希望對I / O操作可以設定時間限制。
 所以希望能用相同方式處理訊號的多次出現,最好用sigaction.訊號只出現並處理一次,可以用signal




   服務端關閉已連線客戶端,客戶端接著發資料產生問題,    1. 當伺服器close一個連線時,若client端接著發資料。根據TCP協議的規定,會收到一個RST響應,client再往這個伺服器傳送資料時,系統會發出一個SIGPIPE訊號給程序,告訴程序這個連線已經斷開了,不要再寫了。     根據訊號的預設處理規則SIGPIPE訊號的預設執行動作是terminate(終止、退出),所以client會退出。若不想客戶端退出可以把SIGPIPE設為SIG_IGN     如:    signal(SIGPIPE,SIG_IGN); 這時SIGPIPE交給了系統處理。    2. 客戶端write一個已經被伺服器端關閉的sock後,返回的錯誤資訊Broken pipe.      1)broken pipe的字面意思是“管道破裂”。broken pipe的原因是該管道的讀端被關閉。      2)broken pipe經常發生socket關閉之後(或者其他的描述符關閉之後)的write操作中    3)發生broken pipe錯誤時,程序收到SIGPIPE訊號,預設動作是程序終止。      4)broken pipe最直接的意思是:寫入端出現的時候,另一端卻休息或退出了,        因此造成沒有及時取走管道中的資料,從而系統異常退出;   伺服器採用了fork的話,要收集垃圾程序,防止殭屍程序的產生,可以這樣處理:           signal(SIGCHLD,SIG_IGN); 交給系統init去回收。    這裡子程序就不會產生殭屍程序了。