1. 程式人生 > >訊號 —— Linux 程式設計

訊號 —— Linux 程式設計

一、簡介

A 給 B 傳送訊號,B 收到訊號之前執行自己的程式碼,收到訊號後,不管執行到程式的什麼位置都要暫停執行,去處理訊號,處理完畢再繼續執行,與硬體中斷類似 —— 非同步模式   ,但訊號是軟體層面上實現的中斷,早期被稱為“軟中斷”。

每個程序收到的所有訊號,都是由核心負責傳送的,核心處理。

1、訊號的產生

(1)、按鍵產生,如: Ctrl+c :2) SIGINT (終止/中斷)    Ctrl + z : 20)SIGTSTP(暫停/停止)    Ctrl+\ : 3)SIGQUIT(退出)

           (2)、系統呼叫產生,如: kill 、raise、abort

           (3)、軟體條件產生,如:  定時器 alarm

           (4)、硬體異常產生,如: 非法訪問記憶體(段錯誤)、除 0 (浮點數除外)、記憶體對齊(匯流排錯誤 )

           (5)、命令產生,       如: kill  命令

2、訊號的狀態

訊號遞達(Delivery),我們將實際執行訊號的處理動作稱為訊號遞達。  - 訊號未決(Pending),我們將訊號從產生到遞達之間的狀態稱為訊號未決。  - 訊號阻塞(Block),程序可以選擇阻塞某個訊號,也可以理解為遮蔽某個訊號。就像我們將討厭的人拉入黑名單,某一天我們不討厭了,還可以拉出來。

注意,如果收到被阻塞的訊號,那麼該訊號將一直保持在未決狀態,不會被遞達。還有,阻塞和忽略是不同的,阻塞是程序沒有收到該訊號,而忽略是程序收到訊號後的一種處理方式。  程序將收到的訊號存放在PCB中,PCB中有三個與上面三個狀態相對應的位圖表。(因為用於表示訊號的狀態,並且狀態只有是與否兩個概念,我們中0,1來表示方便且節省空間,所以用點陣圖。)  每個訊號都在阻塞表(block)和未決表(pending)中有一個0或1的狀態。還有一個handler表,類似於一個函式指標陣列,每個指標都指向指定訊號的處理方式。

3、訊號的處理方式:

①忽略,將pending表中的訊號位置由1置0,返回使用者態,恢復main函式上下文。

②執行預設動作,預設動作一般為終止程式(stop或destroy),此時不返回,執行終止程序流程。

③執行自定義動作(訊號捕捉),這個最重要,如果訊號的處理動作是使用者自定義的動作,核心會先切換到使用者態執行訊號處理函式,訊號處理函式返回時執行系統呼叫sigreturn再次返回核心態。然後再從核心態返回使用者態從上次異常或中斷的地方繼續執行。此種方式最為繁瑣,共有四次核心使用者之間的切換。可以參照下圖理解:

 4、阻塞訊號集(訊號遮蔽字)

將某些訊號,假如集合,對它們設定遮蔽,當遮蔽 x 訊號後,在收到該訊號,該訊號的處理將退後(解除遮蔽後)

 5、未決訊號集

(1)、訊號產生,未決訊號集中描述該訊號的位立刻翻轉為1,表訊號處於未決狀態。當訊號別處理對應位翻轉會為 0 。這一時刻往往非常短暫。

(2)、訊號產生後由於某些原因(主要阻塞)不能抵達,這類訊號的 集合稱之為未決訊號集。在遮蔽解除之前,訊號一直處於未決狀態。

6、訊號的編號

kill -L

1) SIGHUP         2) SIGINT          3) SIGQUIT            4) SIGILL         5) SIGTRAP
6) SIGABRT        7) SIGBUS          8) SIGFPE             9) SIGKILL        10) SIGUSR1
11) SIGSEGV       12) SIGUSR2       13) SIGPIPE            14) SIGALRM       15) SIGTERM
16) SIGSTKFLT     17) SIGCHLD       18) SIGCONT            19) SIGSTOP       20) SIGTSTP
21) SIGTTIN       22) SIGTTOU       23) SIGURG             24) SIGXCPU       25) SIGXFSZ
26) SIGVTALRM     27) SIGPROF       28) SIGWINCH           29) SIGIO         30) SIGPWR
31) SIGSYS        34) SIGRTMIN      35) SIGRTMIN+1       36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6       41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10   45) SIGRTMIN+11      46) SIGRTMIN+12   47) SIGRTMIN+13
48) SIGRTMIN+14   49) SIGRTMIN+15   50) SIGRTMAX-14      51) SIGRTMAX-13   52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10  55) SIGRTMAX-9       56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6     59) SIGRTMAX-5   60) SIGRTMAX-4       61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1     64) SIGRTMAX               

     不存在編號為 0 的訊號,其中 1 - 31 稱之為常規訊號(也叫普通訊號或標準訊號),34-64 稱之為實時訊號,驅動程式設計與硬體 相關。名字上區別不大。而前 32 個名字不盡相同。
     可通過    man 7 signal 檢視幫助文件獲取。也可檢視 /usr/src/linux-headers-4.4.0-31/arch/s390/include/uapi/asm/signal.h

 二、訊號操作函式

1、訊號集的設定

#include <signal.h>
       
       sigset_t  set;
       
       int sigemptyset(sigset_t *set);                    // 將某個訊號集清 0              成功:0    失敗 :-1
 
       int sigfillset(sigset_t *set);                     // 將某個訊號集置 1              成功:0    失敗 :-1
 
       int sigaddset(sigset_t *set, int signum);          // 將某個訊號加入訊號集          成功:0    失敗 :-1
 
       int sigdelset(sigset_t *set, int signum);          // 將某個訊號清除訊號集          成功:0    失敗 :-1
 
       int sigismember(const sigset_t *set, int signum);  // 判斷某個訊號是否在訊號集中          返回值:在集合 1; 不在集合:0;   出錯: -1
  
       sigset_t   型別的本質是點陣圖,但不應該直接使用位操作,而應該使用上述函式,保證跨系統操作有效。
 

 2、sigprocmask函式

一個程序的Block的訊號遮蔽字(阻塞訊號集,後面我們都稱訊號遮蔽字)規定了此程序遮蔽的訊號,我們可以呼叫函式sigprocmask對訊號遮蔽字程序檢測和修改。

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);


引數:
    how引數指定了這個函式工作的方式,有以下三種:
    SIG_BLOCK:使當前的訊號遮蔽字與引數set指標指向的訊號遮蔽字組成並集,構成新的訊號遮蔽字。set包含了希望阻塞的新訊號。 
    SIG_UNBLOCK:使當前的訊號遮蔽字與引數set指標指向的訊號遮蔽字補集的交集,構成新的訊號遮蔽字。set包含了希望希望解除阻塞的訊號。 
    SIG_SETMASK:將當前程序的訊號遮蔽字設定成set所指向的值。

引數:
    set指標,指向一個合適的訊號遮蔽字。

引數:
    oldset指標,當我們修改了當前的訊號遮蔽字之後,需要儲存之前的訊號遮蔽字,以便回覆之前的工作狀態。

如果呼叫sigprocmask解除了對當前若干個未決訊號的阻塞,則在sigprocmask函式返回前,至少有一個訊號遞達(非實時訊號,最後一個到達的訊號將被處理,實時訊號具有排隊機制,都會被處理)。

  3、sigpending 函式

讀取當前程序的未決訊號集。 

#include <signal.h>

int sigpending(sigset_t *set);

set       傳出引數
 
返回值: 成功 0;   失敗: -1 ,設定 errno
/*
    列印未決訊號集 
*/ 
#include<signal.h>
#include<stdio.h>
#include<string.h>
 
void printped(sigset_t *ped)
{
    int i;
    for( i=1;i<32;i++ )
    {
        if( sigismember(ped,i) == 1 )
        {
            putchar('1');
        }
        else
        {
            putchar('0');
        }
    }
    printf("\n");
}
  
int main(void)
{
    sigset_t myset,oldset,ped;
    sigemptyset(&myset);
    sigaddset(&myset,SIGQUIT);
   // sigaddset(&myset,SIGINT);
//    sigaddset(&myset,SIGTSTP);
 
    sigprocmask(SIG_BLOCK,&myset,&oldset);
 
    while(1)
    {
        sigpending(&ped);
        printped(&ped);
        sleep(1); 
    }
 
    return 0;
}

4、訊號捕捉     

1)signal  函式

       #include <signal.h>
 
            typedef void (*sighandler_t)(int);
 
            sighandler_t signal(int signum, sighandler_t handler);
 

2)sigaction  函式

此函式的功能是檢查或修改指定訊號的處理動作。與我們之前講的signal功能相似

       #include <signal.h>
 
            int sigaction(int signum, const struct sigaction *act,
                                        struct sigaction *oldact);
 
         返回值:
       
                成功:0           失敗: -1 ,設定 errno
 
         引數:
         
              act : 傳入引數,新的處理方式。
 
              oldact : 傳出引數,舊的處理方式。
 
             struct sigaction {
              
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };
 
              (1)、sa_handler : 指定訊號捕捉後的處理函式名(即註冊函式)。也可賦值為 SIG_IGN表忽略 或 SIG_DFL 表示執行預設動作。該函式可有一個int引數,通過此引數可以獲得當前訊號的編號。
 
              (2)、sa_mask    :  呼叫訊號處理函式時,所要遮蔽的訊號集合(訊號遮蔽字)。注意:僅在處理函式被呼叫期間遮蔽生效,是零時性設定。
 
              (3)、sa_flags   :  通常設定為 0 ,表使用預設屬性。
 
                    
//訊號捕獲處理。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>

   void sig_proc(int signo)
   {
       printf("%d siganl is catched\n",signo); 
       sleep(10);
       printf("finish\n");

   }

    int main(void)
    {
        int ret;
        struct sigaction act;

        act.sa_handler = sig_proc;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask,SIGQUIT);  
        act.sa_flags = 0;        // 預設屬性訊號捕捉函式執行期間,自動遮蔽本訊號

       ret = sigaction(SIGINT,&act,NULL);
       // signal(SIGINT,sig_proc);
        if( ret < 0  )
        {
            perror("sigaction error");
            exit(1);

        }
        while(1);  

        return 0;
    }

 1、程序正常執行時,預設 PCB 中有一個訊號遮蔽字,假定為 *,它決定了程序自動遮蔽哪些訊號。當註冊了某個訊號捕捉函式,捕捉到該訊號以後,要呼叫該函式。而該函式有可能執行很長時間。在這期間所遮蔽的訊號。不由 * 來指定。而是用 sa_mask 來指定。呼叫完訊號處理函式,再恢復為 * 。

2、xxx  訊號捕捉函式執行期間,XXX 訊號被自動遮蔽。

3、阻塞的常規訊號不支援排隊,產生多次只記錄一次。(後 32 個實時訊號支援排隊)

三、測試舉例

1. 阻塞非實時訊號和實施訊號,然後各發送三次。最後傳送自定義訊號,解除阻塞。觀察非實時訊號和實時訊號的特點。

/*************************************************************************
	> File Name: signal.c
	> Author: 
	> Mail: 
	> Created Time: Sun 29 Jul 2018 04:02:53 PM CST
 ************************************************************************/

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

//自定義訊號處理程式
int myhandle(int num)
{
    printf("myhandle running..\n");
    printf("signal num is = %d \n",num);

    //解除阻塞
    if(num==SIGUSR1)
    {
        //解除阻塞非實時訊號和實時訊號
        sigset_t set;
        sigemptyset(&set);
        sigaddset(&set,SIGINT);
        sigaddset(&set,SIGRTMIN);
        sigprocmask(SIG_UNBLOCK,&set,NULL);
        printf("收到自定義訊號,解除阻塞:%d\n",num);
    }
    if(num==SIGINT)
    {
        printf("收到非實時訊號:%d\n",num);
    }
    if(num==SIGRTMIN)
    {
        printf("收到實時訊號:%d\n",num);
    }
}

int main()
{
    pid_t  pid;

    int ret = 0;

    struct sigaction act;
    act.sa_handler = myhandle;
    //act.sa_flags = SA_SIGINFO;

    //註冊非實時訊號
    if(sigaction(SIGINT,&act,NULL)<0)
    {
        perror("sigaction err:");
        exit(-1);
    }

    //註冊實時訊號 kill -l 顯示所有訊號
    if(sigaction(SIGRTMIN,&act,NULL)<0)
    {
        perror("sigaction err:");
        exit(-1);
    }

    //註冊使用者自定義訊號
    if(sigaction(SIGUSR1,&act,NULL)<0)
    {
        perror("sigaction err:");
        exit(-1);
    }

    //阻塞非實時訊號和實時訊號
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set,SIGINT);
    sigaddset(&set,SIGRTMIN);
    sigprocmask(SIG_BLOCK,&set,NULL);

    pid =  fork();
    if(pid==0)
    {
        //子程序向父程序傳送訊號
        printf("子程序傳送訊號\n");
        union sigval value;
        //引數是子程序的pid
        value.sival_int = getpid();
        int i=0;
        for(i=0;i<3;++i)
        {
            //傳送3次非實時訊號
            if(sigqueue(getppid(),SIGINT,value)<0)
            {
                perror("sigqueue SIGINT error:");
                exit(-3);
            }
            printf("傳送非實時訊號\n");
        }
       for(i=0;i<3;++i)
        {
            //傳送3次實時訊號
            if(sigqueue(getppid(),SIGRTMIN,value)<0)
            {
                perror("sigqueue SIGINT error:");
                exit(-3);
            }
            printf("傳送實時訊號\n");
        }
        //傳送自定義訊號解除阻塞,kill同sigqueue
        kill(getppid(),SIGUSR1);
    }
    else if(pid > 0)
    {
        //父程序
        while(1)
        {
            sleep(1);
        }
    }
    else
    {
        //error
        perror("fork error:");
        exit(-2);
    }

    return 0;
}