1. 程式人生 > >網路程式設計的三個重要訊號(SIGHUP ,SIGPIPE,SIGURG)

網路程式設計的三個重要訊號(SIGHUP ,SIGPIPE,SIGURG)

  對於訊號的介紹,我再前面的一篇部落格中做過專門的總結,感興趣的可以看看。本文主要介紹在網路程式設計中幾個密切相關的函式:SIGUP,SIGPIPE,SIGURG。

SIGHUP訊號

  在介紹SIGHUP訊號之前,先來了解兩個概念:程序組和會話。

程序組

  程序組就是一系列相互關聯的程序集合,系統中的每一個程序也必須從屬於某一個程序組;每個程序組中都會有一個唯一的 ID(process group id),簡稱 PGID;PGID 一般等同於程序組的建立程序的 Process ID,而這個進程序一般也會被稱為程序組先導(process group leader),同一程序組中除了程序組先導外的其他程序都是其子程序;
  程序組的存在,方便了系統對多個相關程序執行某些統一的操作,例如,我們可以一次性發送一個訊號量給同一程序組中的所有程序。

會話

  會話(session)是一個若干程序組的集合,同樣的,系統中每一個程序組也都必須從屬於某一個會話;一個會話只擁有最多一個控制終端(也可以沒有),該終端為會話中所有程序組中的程序所共用。一個會話中前臺程序組只會有一個,只有其中的程序才可以和控制終端進行互動;除了前臺程序組外的程序組,都是後臺程序組;和程序組先導類似,會話中也有會話先導(session leader)的概念,用來表示建立起到控制終端連線的程序。在擁有控制終端的會話中,session leader 也被稱為控制程序(controlling process),一般來說控制程序也就是登入系統的 shell 程序(login shell);
  這裡寫圖片描述


  執行睡眠後臺程序sleep 50 & 之後,通過 ps命令檢視該程序及shell資訊如上圖:

PPID 指父程序 id;PID 指程序 id;PGID 指程序組 id
SID 指會話 id;TTY 指會話的控制終端裝置;COMMAND 指程序所執行的命令
TPGID 指前臺程序組的 PGID。

SIGHUP訊號的觸發及預設處理

  在對會話的概念有所瞭解之後,我們現在開始正式介紹一下SIGHUP訊號,SIGHUP 訊號在使用者終端連線(正常或非正常)結束時發出, 通常是在終端的控制程序結束時, 通知同一session內的各個作業, 這時它們與控制終端不再關聯. 系統對SIGHUP訊號的預設處理是終止收到該訊號的程序

。所以若程式中沒有捕捉該訊號,當收到該訊號時,程序就會退出。
  
SIGHUP會在以下3種情況下被髮送給相應的程序:
  1、終端關閉時,該訊號被髮送到session首程序以及作為job提交的程序(即用 & 符號提交的程序);
  2、session首程序退出時,該訊號被髮送到該session中的前臺程序組中的每一個程序;
   3、若父程序退出導致程序組成為孤兒程序組,且該程序組中有程序處於停止狀態(收到SIGSTOP或SIGTSTP訊號),該訊號會被髮送到該程序組中的每一個程序。
  
  例如:在我們登入Linux時,系統會分配給登入使用者一個終端(Session)。在這個終端執行的所有程式,包括前臺程序組和後臺程序組,一般都屬於這個 Session。當用戶退出Linux登入時,前臺程序組和後臺有對終端輸出的程序將會收到SIGHUP訊號。這個訊號的預設操作為終止程序,因此前臺進 程組和後臺有終端輸出的程序就會中止。

此外,對於與終端脫離關係的守護程序,這個訊號用於通知它重新讀取配置檔案。 比如xinetd超級服務程式。
  當xinetd程式在接收到SIGHUP訊號之後呼叫hard_reconfig函式,它將迴圈讀取/etc/xinetd.d/目錄下的每個子配置檔案,並檢測其變化。如果某個正在執行的子服務的配置檔案被修改以停止服務,則xinetd主程序講給該子服務程序傳送SIGTERM訊號來結束它。如果某個子服務的配置檔案被修改以開啟服務,則xinetd將建立新的socket並將其繫結到該服務對應的埠上。

SIGPIPE

  在網路程式設計中,SIGPIPE這個訊號是很常見的。當往一個寫端關閉的管道或socket連線中連續寫入資料時會引發SIGPIPE訊號,引發SIGPIPE訊號的寫操作將設定errno為EPIPE。在TCP通訊中,當通訊的雙方中的一方close一個連線時,若另一方接著發資料,根據TCP協議的規定,會收到一個RST響應報文,若再往這個伺服器傳送資料時,系統會發出一個SIGPIPE訊號給程序,告訴程序這個連線已經斷開了,不能再寫入資料。
  測試程式如下:簡單的測試程式,函式未加錯誤判斷
  server.c:

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>

#define port 8888

void handle(int sig)
{
    printf("SIGPIPE : %d\n",sig);
}

void mysendmsg(int fd)
{

    // 寫入第一條訊息
    char* msg1 = "first msg"; 
    int n = write(fd, msg1, strlen(msg1));

    if(n > 0)  //成功寫入第一條訊息,server 接收到 client 傳送的 RST
    {
        printf("success write %d bytes\n", n);
    }

    // 寫入第二條訊息,觸發SIGPIPE
    char* msg2 = "second msg";
    n = write(fd, msg2, strlen(msg2));
    if(n < 0)
    {
        printf("write error: %s\n", strerror(errno));
    }
}
int main()
{
    signal(SIGPIPE , handle); //註冊訊號捕捉函式

    struct sockaddr_in server_addr;

    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);

    int listenfd = socket(AF_INET , SOCK_STREAM , 0);

    bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

    listen(listenfd, 128);

    int fd = accept(listenfd, NULL, NULL);
    if(fd < 0)
    {
        perror("accept");
        exit(1);
    }

    mysendmsg(fd);

    return 0;
}

  
client.c

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<unistd.h>

#define PORT 8888
#define MAX 1024

int main()
{

    char buf[MAX] = {'0'};
    int sockfd;
    int n;
    socklen_t slen;
    slen = sizeof(struct sockaddr);
    struct sockaddr_in seraddr;

    bzero(&seraddr,sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(PORT);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);


    //socket()
    if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        perror("socket");
        exit(-1);
    }
    //connect()
    if(connect(sockfd,(struct sockaddr *)&seraddr,slen) == -1)
    {
        perror("connect");
        exit(-1);
    }

    int ret = shutdown(sockfd , SHUT_RDWR);
    if(ret < 0)
    {
        perror("shutdown perror");
    }

    return 0;
}

執行結果
這裡寫圖片描述

  此外,因為SIGPIPE訊號的預設行為是結束程序,而我們絕對不希望因為寫操作的錯誤而導致程式退出,尤其是作為伺服器程式來說就更惡劣了。所以我們應該對這種訊號加以處理,在這裡,介紹兩種處理SIGPIPE訊號的方式:
  1 、給SIGPIPE設定SIG_IGN訊號處理函式,忽略該訊號:

signal(SIGPIPE, SIG_IGN);

  前文說過,引發SIGPIPE訊號的寫操作將設定errno為EPIPE,。所以,第二次往關閉的socket中寫入資料時, 會返回-1, 同時errno置為EPIPE. 這樣,便能知道對端已經關閉,然後進行相應處理,而不會導致整個程序退出.
  2、使用send函式的MSG_NOSIGNAL 標誌來禁止寫操作觸發SIGPIPE訊號。

send(sockfd , buf , size , MSG_NOSIGNAL);

   同樣,我們可以根據send函式反饋的errno來判斷socket的讀端是否已經關閉。
   此外,我們也可以通過IO複用函式來檢測管道和socket連線的讀端是否已經關閉。以POLL為例,當socket連線被對方關閉時,socket上的POLLRDHUP事件將被觸發。

SIGURG

  在介紹SIGURG訊號之前,先來說說什麼是帶外資料。

帶外資料

  帶外資料用於迅速告知對方本端發生的重要的事件。它比普通的資料(帶內資料)擁有更高的優先順序,不論傳送緩衝區中是否有排隊等待發送的資料,它總是被立即傳送。帶外資料的傳輸可以使用一條獨立的傳輸層連線,也可以對映到傳輸普通資料的連線中。實際應用中,帶外資料是使用很少見,有,telnet和ftp等遠端非活躍程式。
  UDP沒有沒有實現帶外資料傳輸,TCP也沒有真正的帶外資料。不過TCP利用頭部的緊急指標標誌和緊急指標,為應用程式提供了一種緊急方式,含義和帶外資料類似。TCP的緊急方式利用傳輸普通資料的連線來傳輸緊急資料。

SIGURG訊號的作用

  核心通知應用程式帶外資料到達的方式有兩種:一種就是利用IO複用技術的系統呼叫(如select)在接受到帶外資料時將返回,並嚮應用程式報告socket上的異常事件。
  另一種方法就是使用SIGURG訊號。

若對伺服器同時處理普通資料和帶外資料感興趣的話可以參考示例程式