SIGPIPE導致程序無故中止

最近在開發新項目,使用的是舊的代碼,因此要進行一些代碼的刪減和優化。在win下開發時,一切正常,但在Linux下,發現其中一個模塊在啓動時會偶發性無緣無故中止,沒有異常日誌,沒有core dump,內核那邊也沒有異常,進程直接就消失了。

我以爲是系統環境變量沒設置好,但使用命令ulimit -a查看各種參數沒有問題,在終端執行sleep 30,然後按ctrl+\也能正常產生core dump。

bug是偶發性的,使用腳本啓動,有一定的概率觸發。但假如我手動去啓動那個進程,反反覆覆試了半個多鍾,也沒有重現,更別說把gdb掛上去調試。我甚至一度懷疑是腳本有問題,查了半天也沒結果。最終在調試腳本時,一次偶然的啓動,重現了這個問題。查看gdb的堆棧,發現是觸發了SIGPIPE信號,程序直接退出。

這一下就理清了問題:整個程序是多進程架構,使用socket進行通信。模塊啓動時,會連接另一個模塊進行握手,但就在發送握手數據時觸發了SIGPIPE。仔細跟一下代碼邏輯,發現是異常沒處理好,導致連接沒有成功時也開始發送了數據。使用腳本啓動時,啓動各個進程的時間比較接近,進行握手時另一個模塊還未來得及監聽端口,因此會連接失敗,有一定概率重現。手動啓動的話另一個模塊已初始化完成,早就在監聽端口,直接就連接上了,重現不了。使用gdb -ex=r --args myprogram arg1 arg2在腳本里用gdb啓動進程也會導致這個進程啓動慢一點,重現的概率也很小。

這個bug暴露了兩個問題。

問題一

信號一般都會產生core dump,但是SIGPIPE這個信號比較特殊,它不會。它是直接終止程序,也沒有內核日誌,查問題的時候根本無從下手。所以程序中除了謹慎地防止這個信號的產生以後,一般還會直接處理掉這個信號

// 直接忽略掉
sigaction(SIGPIPE, &(struct sigaction){SIG_IGN}, NULL);

// 讓程序產生core dump
void sigpipe_handler(int unused)
{
	assert(false);
}
sigaction(SIGPIPE, &(struct sigaction){sigpipe_handler}, NULL);

如果忽略掉信號,則對socket進行send/write操作時一般會返回錯誤,errnoEPIPE,這時要處理一下異常。

問題二

getsockopt(fd, SOL_SOCKET, SO_ERROR, &error_s, &len_s) 第二次調用不會返回錯誤。我簡化了一下代碼,提供一個Linux下的小程序來分析

#include <assert.h>
#include <sys fcntl.h="">
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys select.h="">
#include <unistd.h>
#include <netinet in.h="">
#include <sys socket.h="">
#include <netinet tcp.h="">
#include <arpa inet.h="">

int main()
{
    int fd = -1;
    int ret = 0;
    int error_s = 0;
    struct sockaddr_in serv_addr;

    int len_s = sizeof(int);
    int try = 30;
    
    fd = socket(PF_INET, SOCK_STREAM, 0);
    fcntl(fd, F_SETFL, O_NONBLOCK);

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(55555);
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.88");
    
    ret = connect(fd, (struct sockaddr *)&amp;serv_addr, sizeof(struct sockaddr_in));
    printf("connect ret=%d, errno=%d\n", ret, errno);

try_connect_again:

    printf("try_connect_again\n");
    printf("attempt left %d ...\n", try);
    if (!try)
        goto fin;
        
    usleep(2000000);

    struct timeval tm;
    tm.tv_sec = 0;
    tm.tv_usec = 0;
            
    fd_set set;
    FD_ZERO(&amp;set);
    FD_SET(fd, &amp;set);

    ret = select(fd+1, NULL, &amp;set, NULL, &amp;tm);
    if (ret)
    {
        ret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &amp;error_s, &amp;len_s);
        printf("getsockopt, ret=%d, error_s=%d \n", ret, error_s);
        if (error_s == 0)
            printf("connected\n");
        else
        {
            try--;
            goto try_connect_again;
        }
    }
    else if (ret == 0)
    {
        try--;
        goto try_connect_again;
    }

fin:
    printf("end\n");
    return 0;
}

整個程序的邏輯比較簡單,發起一個連接,然後定時用select來監聽可寫事件,再用getsockopt來判斷是否連接成功。但是在不同的系統下,它表現是不一樣的:

xzc@debian:~/local_code$ gcc test_gso.c 
xzc@debian:~/local_code$ ./a.out 
connect ret=-1, errno=115
try_connect_again
attempt left 30 ...
getsockopt, ret=0, error_s=111 
try_connect_again
attempt left 29 ...
getsockopt, ret=0, error_s=0 
connected
end
xzc@debian:~/local_code$ cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 10 (buster)"

[root@localhost test]# gcc test_gso.c 
[root@localhost test]# ./a.out 
connect ret=-1, errno=115
try_connect_again
attempt left 30 ...
getsockopt, ret=0, error_s=111 
try_connect_again
attempt left 29 ...
try_connect_again
attempt left 28 ...
try_connect_again
attempt left 27 ...
^C
[root@localhost test]# cat /etc/centos-release 
CentOS release 6.5 (Final)

可以看到在Debian 10下,select每次都會返回可寫事件,getsockopt第一次返回錯誤碼111,第二次返回了0,由於沒有處理第一次的異常,第二次就會誤以爲連接成功從而發送了數據引發SIGPIPE。而在CentOS 6.5下,select只返回了一次可寫事件,沒有再次調用getsockopt

經過測試,發現新系統(測試了Debian 10、CentOS 7、CentOS 6.10)的表現都是select每次都會返回可讀事件,getsockopt第一次返回錯誤碼111,第二次返回了0。而我這新的項目使用了老項目的代碼,系統也從CentOS 6.5遷移到CentOS 7,因此觸發了這個bug。

不太清楚這個差異的原因,可能是內核的bug,也可能是修改了特性。但也有不少人遇到:
https://github.com/dotnet/runtime/issues/17260
https://www.unix.com/programming/254646-strange-getsockopt-solaris-behavior.html

另外還有一個值得注意的點。win的select和linux下不同,判斷一個連接是否成功,如果成功是在writefds參數,但如果失敗是在exceptfds參數。參考:https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select

我這項目代碼沒有處理exceptfds參數,因此win下不會監聽到連接失敗,所以平時在win下開發沒有發現問題。</unistd.h></stdlib.h></stdio.h></errno.h></assert.h>

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