【Linux網路程式設計-1】訊號

neon233發表於2024-06-26

1. 訊號


後臺服務程式要脫離shell執行,可用2種方法:

  • 後臺執行:demo &
  • fork一個子程序,再終止父程序,從而讓子程序被系統託管(ppid=1)

終止程序時,在shell下可以ctrl-ckill pidkillall demo

這三個都是給系統給demo程式傳送了訊號,ctrl-c是SIGINT訊號,kill pid和killall是SIGTERM訊號。

demo對這2個訊號的預設處理是程式終止。

在程式中,可以把訊號關聯到新的handler,那麼收到訊號後的處理將改變。

void handler(int sig);	//訊號處理函式

signal(SIGINT, handler);	//關聯訊號SIGINT和handler

(1)概念

  • 訊號是宏定義,一共64個訊號。SIGINT值是2,SIGTERM值是15。用訊號名、訊號值都行。

  • 訊號,也叫軟中斷,用來通知程序發生了事件。中斷,是說明程序哪怕在休眠sleep()也能中斷其休眠喚醒。

  • 但只能給程序通知發生了某個事件,不能給程序傳遞資料。

  • 程序之間可以透過kill庫函式互相傳送訊號。Linux核心也可給程序傳送訊號,通知程序發生了某個事件。

    int kill(pid_t pid, int sig);
    
    
    pid>0,將訊號sig傳送給程序號為pid的程序
    pid=0,訊號傳給和呼叫kill的程序同程序組中所有程序
    pid=-1,將訊號廣播給系統所有程序。如:系統關機時,給所有登入視窗廣播關機資訊。
    返回值:0表示成功,-1表示失敗,errno設定值
    
  • 訊號遞達(Delivery)、訊號未決(Pending)

    訊號遞達——訊號產生後到達程序並執行了訊號處理函式的過程

    訊號未決——訊號產生但沒有遞達

(2)程序對訊號的處理,有3種方式:

  • 忽略——對訊號不做任何處理,不產生中斷

    signal(SIGINT, SIG_IGN);	//忽略SIGINT訊號
    

    程式中設定中斷處理函式,那麼程式收到訊號時,中斷處理函式將被呼叫

  • Linux系統預設的訊號處理

  • signalsigaction函式重新設定處理函式

    //用這個
    //功能更強大。多執行緒中只能用這個,沒有signal()函式
    #include<signal.h>
    sigaction(int sigNum, const struct sigaction* act, struct sigaction* oldact);	
    
    struct sigaction
    {
        void (*sa_handler)(int);	//處理函式
        void (*sa_sigaction)(int, siginfo_t*, void*);	
        sigset_t sa_mask;	//訊號集合
        int sa_flags;	//標誌位   
    }
    /******************************************************/
    sa_flags標誌位關注:SA_RESTART
        標誌位設為SA_RESTART後,被訊號中斷的系統呼叫(如scanf這種)會在訊號處理後被重新自動呼叫
        不設定的話,被中斷的系統呼叫不會重新呼叫
        
    
    /******************************************************/
    struct sigaction stact;
    memset(&stact, 0, sizeof(stact));	//置0初始化
    stact.sa_handler=hdfunc;	//指定處理函式
    sigaddset(&stact.sa_mask, 15);	//訊號15設定阻塞,沒有手動解除阻塞,等2處理完畢緊接著處理15。要放在sigaction之前
    sigaction(2, &stact, NULL);	
    sigaction(15, &stact, NULL);	
    
    #include<signal.h>
    signal(int sigNum, sighandler);
    
    //返回值意義不大			
    //sighandler——訊號處理方式,可設為:
        SIG_IGN忽略
        void handler(int)處理
        SIG_DFL恢復為預設處理  
     
    /******************************************************/
    signal(2, hdfunc);	//訊號處理函式
    

(3)常用訊號

訊號名 訊號值 發出訊號 預設處理
SIGINT 2 鍵盤中斷ctrl-c 終止程序
SIGKILL 9 kill -9 終止程序、訊號不能被捕獲、訊號不能被忽略
SIGSEGV 11 無效的記憶體引用 記憶體越界時,Linux核心給程序發core dump,程式就報錯core dump
SIGTERM 15 kill和killall 終止程序
SIGCHLD 20,17,18 子程序結束 忽略

(4)實際用法

訊號常用,但工作中用法不多。

  • 服務程式體面、安全的退出

    服務程式執行在後臺,強行kill不是好辦法,因為kill不會釋放系統資源,會影響系統穩定。而且,對端也不知道你被kill掉了,還在等待通訊等情況。

    體面而安全的做法:編寫訊號處理函式,在裡面進行資源釋放(檔案IO關閉、資料庫連線關閉、通訊連線關閉等)、通知對端結束通訊等處理。程式就可以有計劃的退出。

    //demo.cpp
    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<signal.h>
    
    FILE* fp=0;
    void EXIT(int sig)
    {
        printf("get signal:%d\n",sig);
       	if(fp!=0)
            fclose(fp);	//釋放資源
        exit(0);
    }
    
    int main()
    {
        signal(SIGINT, EXIT);
        signal(SIGTERM, EXIT);
    
        if(fork() > 0)
            exit(0);
    
        while(100)
        {
            printf("execute..\n");
            sleep(1);
        }
        return 0;
    }
    
  • 網路服務程式抓包

(5)可靠訊號、不可靠訊號

132訊號為不可靠訊號,3464為可靠訊號。

  • 不可靠訊號:

    存在訊號丟失問題——程序收到訊號不做佇列處理,而是相同訊號會合併成一個。

    (早期版本Linux核心的signal函式會在對不可靠訊號進行一次處理後,handler會恢復成預設處理。現在的Linux核心版本。現在已經沒這個情況了。)

    #include<stdlib.h>
    #include<unistd.h>
    #include<signal.h>
    
    void handler(int sig)
    {
        printf("sig=%d",sig);
    	for(int j=0;j<10;j++)
        {
        	printf("j(%d)=%d",sig,j);
            sleep(1);
        }
    }
    
    int main()
    {
        //signal(2, handler);
        struct sigaction stact;
        memset(&stact,0,sizeof(stact));
        stact.sa_handler=handler;
        sigaction(2, stact, NULL);
        
        for(int i=0;i<100;i++)
        {
            printf("i=%d",i);
            sleep(1);
        }
        return 0;
    }
    
    //./demo執行
    //多次killall -2 demo傳送訊號2
    發現最後handler執行次數少於訊號2的傳送次數
    
  • 可靠訊號

    不會丟失

(6)訊號處理函式被中斷

一個訊號的處理函式正在執行時,若來了其他訊號,會中斷正在執行的處理函式,轉而執行新訊號的處理函式。待新的處理函式執行完,再繼續執行之前的處理。

若來的是相同的訊號,則會排隊阻塞等待當前處理函式執行完。(可靠訊號會出現合併現象)

#include<stdlib.h>
#include<unistd.h>
#include<signal.h>

void handler(int sig)
{
    printf("sig=%d",sig);
	for(int j=0;j<10;j++)
    {
    	printf("j(%d)=%d",sig,j);
        sleep(1);
    }
}

int main()
{
    //signal(2, handler);
    //signal(15, handler);
    struct sigaction stact;
    memset(&stact,0,sizeof(stact));
    stact.sa_handler=handler;
    sigaction(2, stact, NULL);
    sigaction(15, stact, NULL);
    
    for(int i=0;i<100;i++)
    {
        printf("i=%d",i);
        sleep(1);
    }
    return 0;
}

//demo執行
//killall -2 demo傳送訊號2
//在2的handler還未執行完之前,killall -15 demo傳送訊號15
可見訊號2的handler正在執行時,被訊號15的handler中斷
    
//多次killall -2 demo傳送訊號2
訊號2的handler不會被打斷,而是排隊,由於2不可靠訊號出現合併

(7)訊號忽略和阻塞

  • 忽略

    程序把遞達的訊號直接丟棄

    signal(sig, SIG_IGN);
    
  • 阻塞

    場景:為了讓訊號不被丟棄,同時也不會中斷當前正在執行的處理函式,將其阻塞,待當前中斷處理執行完,再手動結束阻塞sigprocmask(SIG_BLOCK, &set, NULL)會立即執行被阻塞的訊號的處理函式。

    訊號被阻塞在未決狀態,不是被丟棄。

    sigset_t set;	//建立訊號集合
    sigemptyset(&set);	//將訊號集合set置空
    sigfillset(&set);	//把全部訊號加到訊號集合
    sigaddset(&set, 15);	//向集合中新增訊號15
    sigdelset(&set, 15);	//從集合中刪除訊號15
    int sigismember(&set, 15);	//判斷訊號15是否在訊號集合
    sigprocmask(SIG_BLOCK, &set, NULL);	//將訊號集合set中所有訊號設定為阻塞SIG_BLOCK
    sigprocmask(SIG_UNBLOCK, &set, NULL);	//解除阻塞
    
    //demo
    #include<stdio.h>
    #include<stdlib.h>
    #include<signal.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<signal.h>
    
    void handler(int sig)
    {
        printf("sig=%d\n",sig);
    	for(int j=0;j<10;j++)
        {
        	printf("j(%d)=%d\n",sig,j);
            sleep(1);
        }
    }
    
    int main()
    {
        /*
        signal(2, handler);
        signal(15, handler);
        sigset_t set;	
    	sigemptyset(&set);	
    	sigaddset(&set, 15);	
    	sigprocmask(SIG_BLOCK, &set, NULL);	//訊號設定為阻塞SIG_BLOCK
       */
        
        struct sigaction stact;
        memset(&stact,0,sizeof(stact));
        stact.sa_handler=handler;
        sigaddset(&stact.sa_mask, 15);	//訊號15設定阻塞,沒有手動解除阻塞,等2處理完畢緊接著處理15。要放在sigaction之前
        stact.sa_flags=SA_RESTART;
        sigaction(2, &stact, NULL);
        sigaction(15, &stact, NULL);
        
        int a;
        scanf("%d", &a);	//在使用者不輸入的情況下,若不設定stact.sa_flags=SA_RESTART,訊號遞達被處理後,scanf等待輸入就跳過了
        
        for(int i=0;i<100;i++)
        {
            printf("i=%d\n",i);	
            fflush(stdout);	//printf 呼叫可能因為緩衝問題而不立即顯示輸出,而是迴圈完之後全部一起輸出
            sleep(1);
            /*
            if(i>20)	
                sigprocmask(SIG_UNBLOCK, &set, NULL);
             */
        }
        return 0;
    }
    
    //demo執行
    //killall -2 demo立即執行訊號2的handler
    //killall -9 demo時不會立即執行訊號15的handler,而是當i==21時才執行
    

相關文章