1. 程式人生 > >Linux訊號透徹分析與理解

Linux訊號透徹分析與理解

本文將從以下幾個方面來闡述訊號:
(1)訊號的基本知識
(2)訊號生命週期與處理過程分析
(3) 基本的訊號處理函式
(4) 保護臨界區不被中斷
(5) 訊號的繼承與執行
(6)實時訊號中鎖的研究

第一部分: 訊號的基本知識

1.訊號本質:

訊號的本質是軟體層次上對中斷的一種模擬。它是一種非同步通訊的處理機制,事實上,程序並不知道訊號何時到來。

2.訊號來源

(1)程式錯誤,如非法訪問記憶體
(2)外部訊號,如按下了CTRL+C
(3)通過kill或sigqueue向另外一個程序傳送訊號

3.訊號種類

訊號分為可靠訊號與不可靠訊號,可靠訊號又稱為實時訊號,非可靠訊號又稱為非實時訊號。
訊號程式碼從1到32是不可靠訊號,不可靠訊號主要有以下問題:
(1)每次訊號處理完之後,就會恢復成預設處理,這可能是呼叫者不希望看到的
(2)存在訊號丟失的問題
現在的Linux對訊號機制進行了改進,因此,不可靠訊號主要是指訊號丟失。

訊號程式碼從SIGRTMIN到SIGRTMAX之間的訊號是可靠訊號。可靠訊號不存在丟失,由sigqueue傳送,可靠訊號支援排隊。
可靠訊號註冊機制:
核心每收到一個可靠訊號都會去註冊這個訊號,在訊號的未決訊號鏈中分配sigqueue結構,因此,不會存在訊號丟失的問題。
不可靠訊號的註冊機制:
而對於不可靠的訊號,如果核心已經註冊了這個訊號,那麼便不會再去註冊,對於程序來說,便不會知道本次訊號的發生。
可靠訊號與不可靠訊號與傳送函式沒有關係,取決於訊號程式碼,前面的32種訊號就是不可靠訊號,而後面的32種訊號就是可靠訊號。

4.訊號響應的方式

(1)採用系統預設處理SIG_DFL,執行預設操作
(2)捕捉訊號處理,即使用者自定義的訊號處理函式來處理
(3)忽略訊號SIG_IGN ,但有兩種訊號不能被忽略SIGKILL,SIGSTOP

第二部分: 訊號的生命週期與處理過程分析

1. 訊號的生命週期

訊號產生->訊號註冊->訊號在程序中登出->訊號處理函式執行完畢
(1)訊號的產生是指觸發訊號的事件的發生
(2)訊號註冊
指的是在目標程序中註冊,該目標程序中有未決訊號的資訊:

struct sigpending pending:
struct sigpending{
    struct sigqueue *head, **tail;
    sigset_t signal;
};
struct sigqueue{
    struct sigqueue *next;
    siginfo_t info;
}

其中 sigqueue結構組成的鏈稱之為未決訊號鏈,sigset_t稱之為未決訊號集。
*head,**tail分別指向未決訊號鏈的頭部與尾部。
siginfo_t info是訊號所攜帶的資訊。

訊號註冊的過程就是將訊號值加入到未決訊號集siginfo_t中,將訊號所攜帶的資訊加入到未決訊號鏈的某一個sigqueue中去。

因此,對於可靠的訊號,可能存在多個未決訊號的sigqueue結構,對於每次訊號到來都會註冊。而不可靠訊號只註冊一次,只有一個sigqueue結構。
只要訊號在程序的未決訊號集中,表明程序已經知道這些訊號了,還沒來得及處理,或者是這些訊號被阻塞。

(3)訊號在目標程序中登出
在程序的執行過程中,每次從系統呼叫或中斷返回使用者空間的時候,都會檢查是否有訊號沒有被處理。如果這些訊號沒有被阻塞,那麼就呼叫相應的訊號處理函式來處理這些訊號。則呼叫訊號處理函式之前,程序會把訊號在未決訊號鏈中的sigqueue結構卸掉。是否從未決訊號集中把訊號刪除掉,對於實時訊號與非實時訊號是不相同的。
非實時訊號:由於非實時訊號在未決訊號鏈中只有一個sigqueue結構,因此將它刪除的同時將訊號從未決訊號集中刪除。
實時訊號:由於實時訊號在未決訊號鏈中可能有多個sigqueue結構,如果只有一個,也將訊號從未決訊號集中刪除掉。如果有多個那麼不從未決訊號集中刪除訊號,登出完畢。

(4)訊號處理函式執行完畢
執行處理函式,本次訊號在程序中響應完畢。

在第4步,只簡單的描述了訊號處理函式執行完畢,就完成了本次訊號的響應,但這個訊號處理函式空間是怎麼處理的呢? 核心棧與使用者棧是怎麼工作的呢? 這就涉及到了訊號處理函式的過程。

訊號處理函式的過程:

(1)註冊訊號處理函式

訊號的處理是由核心來代理的,首先程式通過sigal或sigaction函式為每個訊號註冊處理函式,而核心中維護一張訊號向量表,對應訊號處理機制。這樣,在訊號在程序中登出完畢之後,會呼叫相應的處理函式進行處理。

(2)訊號的檢測與響應時機
在系統呼叫或中斷返回使用者態的前夕,核心會檢查未決訊號集,進行相應的訊號處理。

(3)處理過程:
程式執行在使用者態時->程序由於系統呼叫或中斷進入核心->轉向使用者態執行訊號處理函式->訊號處理函式完畢後進入核心->返回使用者態繼續執行程式

首先程式執行在使用者態,在程序陷入核心並從核心返回的前夕,會去檢查有沒有訊號沒有被處理,如果有且沒有被阻塞就會呼叫相應的訊號處理程式去處理。首先,核心在使用者棧上建立一個層,該層中將返回地址設定成訊號處理函式的地址,這樣,從核心返回使用者態時,就會執行這個訊號處理函式。當訊號處理函式執行完,會再次進入核心,主要是檢測有沒有訊號沒有處理,以及恢復原先程式中斷執行點,恢復核心棧等工作,這樣,當從核心返回後便返回到原先程式執行的地方了。

訊號處理函式的過程大概是這樣了。

第三部分: 基本的訊號處理函式
首先看一個兩個概念: 訊號未決與訊號阻塞
訊號未決: 指的是訊號的產生到訊號處理之前所處的一種狀態。確切的說,是訊號的產生到訊號登出之間的狀態。
訊號阻塞: 指的是阻塞訊號被處理,是一種訊號處理方式。

1. 訊號操作

訊號操作最常用的方法是訊號的遮蔽,訊號遮蔽主要用到以下幾個函式:

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set,int signo);
int sigdelset(sigset_t *set,int signo);
int sigismemeber(sigset_t* set,int signo);
int sigprocmask(int how,const sigset_t*set,sigset_t *oset);

訊號集,訊號掩碼,未決集
訊號集: 所有的訊號阻塞函式都使用一個稱之為訊號集的結構表明其所受到的影響。
訊號掩碼:當前正在被阻塞的訊號集。
未決集: 程序在收到訊號時到訊號在未被處理之前訊號所處的集合稱為未決集。
可以看出,這三個概念沒有必然的聯絡,訊號集指的是一個泛泛的概念,而未決集與訊號掩碼指的是具體的訊號狀態。

對於訊號集的初始化有兩種方法: 一種是用sigemptyset使訊號集中不包含任何訊號,然後用sigaddset把訊號加入到訊號集中去。
另一種是用sigfillset讓訊號集中包含所有訊號,然後用sigdelset刪除訊號來初始化。

sigemptyset()函式初始化訊號集set並將set設定為空。
sigfillset()函式初始化訊號集,但將訊號集set設定為所有訊號的集合。 sigaddset()將訊號signo加入到訊號集中去。
sigdelset()從訊號集中刪除signo訊號。
sigprocmask()將指定的訊號集合加入到程序的訊號阻塞集合中去。如果提供了oset,那麼當前的訊號阻塞集合將會儲存到oset集全中去。
引數how決定了操作的方式: SIG_BLOCK 增加一個訊號集合到當前程序的阻塞集合中去 SIG_UNBLOCK
從當前的阻塞集合中刪除一個訊號集合 SIG_SETMASK 將當前的訊號集合設定為訊號阻塞集合

下面看一個例子:

#include
#include
#include
#include
#include
int main(){
    sigset_t initset;
    int i;
    sigemptyset(&initset);//初始化訊號集合為空集合
    sigaddset(&initset,SIGINT);//將SIGINT訊號加入到此集合中去
    while(1){
        sigprocmask(SIG_BLOCK,&initset,NULL);//將訊號集合加入到程序的阻塞集合中去
        fprintf(stdout,"SIGINT singal blocked/n");
        for(i=0;i<10;i++){
            sleep(1);//每1秒輸出
            fprintf(stdout,"block %d/n",i);
        }
//在這時按一下Ctrl+C不會終止
        sigprocmask(SIG_UNBLOCK,&initset,NULL);//從程序的阻塞集合中去刪除訊號集合
        fprintf(stdout,"SIGINT SINGAL unblokced/n");
        for(i=0;i<10;i++){
            sleep(1);
            fprintf(stdout,"unblock %d/n",i);
        }

    }
    exit(0);
}

執行結果:

SIGINT singal blocked
block 0
block 1
block 2
block 3
block 4
block 5
block 6
block 7
block 8
block 9

在執行到block 3時按下了CTRL+C並不會終止,直到執行到block9後將集合從阻塞集合中移除。

[[email protected] C]# ./s1
SIGINT singal blocked
block 0
block 1
block 2
block 3
block 4
block 5
block 6
block 7
block 8
block 9
SIGINT SINGAL unblokced
unblock 0
unblock 1

由於此時已經解除了阻塞,在unblock1後按下CTRL+C則立即終止。

2. 訊號處理函式

#include
int sigaction(
    int signo,
    const struct sigaction *act,
    struct sigaction *oldact
);

這個函式主要是用於改變或檢測訊號的行為。

第一個引數是變更signo指定的訊號,它可以指向任何值,SIGKILL,SIGSTOP除外
第二個引數,第三個引數是對訊號進行細粒度的控制。
如果*act不為空,*oldact不為空,那麼oldact將會儲存訊號以前的行為。如果act為空,*oldact不為空,那麼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);
}

引數含義:
sa_handler是一個函式指標,主要是表示接收到訊號時所要採取的行動。此欄位的值可以是SIG_DFL,SIG_IGN.分別代表預設操作與核心將忽略程序的訊號。這個函式只傳遞一個引數那就是訊號程式碼。
當SA_SIGINFO被設定在sa_flags中,那麼則會使用sa_sigaction來指示訊號處理函式,而非sa_handler.
sa_mask設定了掩碼集,在程式執行期間會阻擋掩碼集中的訊號。
sa_flags設定了一些標誌, SA_RESETHAND當該函式處理完成之後,設定為為系統預設的處理模式。SA_NODEFER 在處理函式中,如果再次到達此訊號時,將不會阻塞。預設情況下,同一訊號兩次到達時,如果此時處於訊號處理程式中,那麼此訊號將會阻塞。
SA_SIGINFO表示用sa_sigaction指示的函式。
sa_restorer已經被廢棄。

sa_sigaction所指向的函式原型:
void my_handler(int signo,siginfo_t *si,void *ucontext);
第一個引數: 訊號編號
第二個引數:指向一個siginfo_t結構。
第三個引數是一個ucontext_t結構。
其中siginfo_t結構體中包含了大量的訊號攜帶資訊,可以看出,這個函式比sa_handler要強大,因為前者只能傳遞一個訊號程式碼,而後者可以傳遞siginfo_t資訊。

typedef struct siginfo_t{
    int si_signo;//訊號編號
    int si_errno;//如果為非零值則錯誤程式碼與之關聯
    int si_code;//說明程序如何接收訊號以及從何處收到
    pid_t si_pid;//適用於SIGCHLD,代表被終止程序的PID
    pid_t si_uid;//適用於SIGCHLD,代表被終止程序所擁有程序的UID
    int si_status;//適用於SIGCHLD,代表被終止程序的狀態
    clock_t si_utime;//適用於SIGCHLD,代表被終止程序所消耗的使用者時間
    clock_t si_stime;//適用於SIGCHLD,代表被終止程序所消耗系統的時間
    sigval_t si_value;
    int si_int;
    void * si_ptr;
    void* si_addr;
    int si_band;
    int si_fd;
};


sigqueue(pid_t pid,int signo,const union sigval value)
union sigval{int sival_int, void*sival_ptr};

sigqueue函式類似於kill,也是一個程序向另外一個程序傳送訊號的。
但它比kill函式強大。
第一個引數指定目標程序的pid.
第二個引數是一個訊號程式碼。
第三個引數是一個共用體,每次只能使用一個,用來程序傳送訊號傳遞的資料。
或者傳遞整形資料,或者是傳遞指標。
傳送的資料被sa_sigaction所指示的函式的siginfo_t結構體中的si_ptr或者是si_int所接收。

sigpending的用法
sigpending(sigset_t set);
這個函式的作用是返回未決的訊號到訊號集set中。即未決訊號集,未決訊號集不僅包括被阻塞的訊號,也可能包括已經到達但沒有被處理的訊號。

示例1: sigaction函式的用法

#include
#include
void signal_set1(int);//訊號處理函式,只傳遞一個引數訊號程式碼
void signal_set(struct sigaction *act)
{
    switch(act->sa_flags){
        case (int)SIG_DFL:
            printf("using default hander/n");
            break;
        case (int)SIG_IGN:
            printf("ignore the signal/n");
            break;
        default:
            printf("%0x/n",act->sa_handler);
    }

}
void signal_set1(int x){//訊號處理函式
    printf("xxxxx/n");
    while(1){}
}
int main(int argc,char** argv)
{
    int i;
    struct sigaction act,oldact;
    act.sa_handler = signal_set1;
    act.sa_flags = SA_RESETHAND;
    //SA_RESETHANDD 在處理完訊號之後,將訊號恢復成預設處理
    //SA_NODEFER在訊號處理程式執行期間仍然可以接收訊號
    sigaction (SIGINT,&act,&oldact) ;//改變訊號的處理模式
    for (i=1; i<12; i++)
    {
        printf("signal %d handler is : ",i);
        sigaction (i,NULL,&oldact) ;
        signal_set(&oldact);//如果act為NULL,oldact會儲存訊號當前的行為
        //act不為空,oldact不為空,則oldact會儲存訊號以前的處理模式
    }
    while(1){
    //等待訊號的到來
    }
    return 0;
}

執行結果:

[[email protected] C]# ./s2
signal 1 handler is : using default hander
signal 2 handler is : 8048437
signal 3 handler is : using default hander
signal 4 handler is : using default hander
signal 5 handler is : using default hander
signal 6 handler is : using default hander
signal 7 handler is : using default hander
signal 8 handler is : using default hander
signal 9 handler is : using default hander
signal 10 handler is : using default hander
signal 11 handler is : using default hander
xxxxx

解釋:
sigaction(i,NULL,&oldact);
signal_set(&oldact);
由於act為NULL,那麼oldact儲存的是當前訊號的行為,當前的第二個訊號的行為是執行自定義的處理程式。
當按下CTRL+C時會執行訊號處理程式,輸出xxxxxx,再按一下CTRL+C會停止,是由於SA_RESETHAND恢復成預設的處理模式,即終止程式。
如果沒有設定SA_NODEFER,那麼在處理函式執行過程中按一下CTRL+C將會被阻塞,那麼程式會停在那裡。

示例2: sigqueue向本程序傳送資料的訊號

#include
#include
#include
#include
#include
void myhandler(int signo,siginfo_t *si,void *ucontext);
int main(){
    union sigval val;//定義一個攜帶資料的共用體
    struct sigaction oldact,act;
    act.sa_sigaction=myhandler;
    act.sa_flags=SA_SIGINFO;//表示使用sa_sigaction指示的函式,處理完恢復預設,不阻塞處理過程中到達下在被處理的訊號
    //註冊訊號處理函式
    sigaction(SIGUSR1,&act,&oldact);
    char data[100];
    int num=0;
    while(num<10){
        sleep(2);
        printf("等待SIGUSR1訊號的到來/n");
        sprintf(data,"%d",num++);
        val.sival_ptr=data;
        sigqueue(getpid(),SIGUSR1,val);//向本程序傳送一個訊號
    }
}

void myhandler(int signo,siginfo_t *si,void *ucontext){
    printf("已經收到SIGUSR1訊號/n");
    printf("%s/n",(char*)(si->si_ptr));
}

程式執行的結果是:

等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
0
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
1
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
2
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
3
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
4
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
5
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
6
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
7
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
8
等待SIGUSR1訊號的到來
已經收到SIGUSR1訊號
9

解釋: 本程式用sigqueue不停的向自身傳送訊號,並且攜帶資料,資料被放到處理函式的第二個引數siginfo_t結構體中的si_ptr指標,當num<10時不再發。

一般而言,sigqueue與sigaction配合使用,而kill與signal配合使用。

示例3: 一個程序向另外一個程序傳送訊號,並攜帶資訊

傳送端:

#include
#include
#include
#include
#include
int main(){
    union sigval value;
    value.sival_int=10;
    if(sigqueue(4403,SIGUSR1,value)==-1){//4403是目標程序pid
        perror("訊號傳送失敗/n");
    }
    sleep(2);
}

接收端:


#include
#include
#include
#include
#include
void myhandler(int signo,siginfo_t*si,void *ucontext);
int main(){
    struct sigaction oldact,act;
    act.sa_sigaction=myhandler;
    act.sa_flags=SA_SIGINFO|SA_NODEFER;
    //表示執行後恢復,用sa_sigaction指示的處理函式,在執行期間仍然可以接收訊號
    sigaction(SIGUSR1,&act,&oldact);
    while(1){
        sleep(2);
        printf("等待訊號的到來/n");
    }
}
void myhandler(int signo,siginfo_t *si,void *ucontext){
    printf("the value is %d/n",si->si_int);
}

示例4: sigpending的用法
sigpending(sigset_t *set)將未決訊號放到指定的set訊號集中去,未決訊號包括被阻塞的訊號和訊號到達時但還沒來得及處理的訊號

#include
#include
#include
#include
#include
void myhandler(int signo,siginfo_t *si,void *ucontext);
int main(){
    struct sigaction oldact,act;
    sigset_t oldmask,newmask,pendingmask;
    act.sa_sigaction=myhandler;
    act.sa_flags=SA_SIGINFO;
    sigemptyset(&act.sa_mask);//首先將阻塞集合設定為空,即不阻塞任何訊號
    //註冊訊號處理函式
    sigaction(SIGRTMIN+10,&act,&oldact);
    //開始阻塞
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGRTMIN+10);
    printf("SIGRTMIN+10 blocked/n");
    sigprocmask(SIG_BLOCK,&newmask,&oldmask);
    sleep(20);//為了發出訊號
    printf("now begin to get pending mask/n");
    if(sigpending(&pendingmask)<0){
        perror("pendingmask error");
    }
    if(sigismember(&pendingmask,SIGRTMIN+10)){
        printf("SIGRTMIN+10 is in the pending mask/n");
    }
    sigprocmask(SIG_UNBLOCK,&newmask,&oldmask);
    printf("SIGRTMIN+10 unblocked/n");
}
//訊號處理函式
void myhandler(int signo,siginfo_t *si,void *ucontext){

printf("receive signal %d/n",si->si_signo);

}

程式執行:

在另一個shell傳送訊號:
kill -44 4579

SIGRTMIN+10 blocked
now begin to get pending mask
SIGRTMIN+10 is in the pending mask
receive signal 44
SIGRTMIN+10 unblocked
可以看到SIGRTMIN由於被阻塞所以處於未決訊號集中。

關於基本的訊號處理函式就介紹到這了。

第四部分: 保護臨界區不被中斷
1. 函式的可重入性
函式的可重入性是指可以多於一個任務併發使用函式,而不必擔心資料錯誤。相反,不可重入性是指不能多於一個任務共享函式,除非能保持函式互斥(或者使用訊號量,或者在程式碼的關鍵部分禁用中斷)。可重入函式可以在任意時刻被中斷,稍後繼續執行,而不會丟失資料。

可重入函式:
* 不為連續的呼叫持有靜態資料。
* 不返回指向靜態資料的指標;所有資料都由函式的呼叫者提供。
* 使用本地資料,或者通過製作全域性資料的本地拷貝來保護全域性資料。
* 絕不呼叫任何不可重入函式。

不可重入函式可能導致混亂現象,如果當前程序的操作與訊號處理程式同時對一個檔案進行寫操作或者是呼叫malloc(),那麼就可能出現混亂,當從訊號處理程式返回時,造成了狀態不一致。從而引發錯誤。
因此,訊號的處理必須是可重入函式。
簡單的說,可重入函式是指在一個程式中呼叫了此函式,在訊號處理程式中又呼叫了此函式,但仍然能夠得到正確的結果。
printf,malloc函式都是不可重入函式。printf函式如果列印緩衝區一半時,又有一個printf函式,那麼此時會造成混亂。而malloc函式使用了系統全域性記憶體分配表。

  1. 保護臨界區不被中斷
    由於臨界區的程式碼是關鍵程式碼,是非常重要的部分,因此,有必要對臨界區進行保護,不希望訊號來中斷臨界區操作。這裡通過訊號遮蔽字來阻塞訊號的發生。

    下面介紹兩個與保護臨界區不被訊號中斷的相關函式。

int pause(void);
int sigsuspend(const sigset_t *sigmask);

pause函式掛起一個程序,直到一個訊號發生。
sigsuspend函式的執行過程如下:
(1)設定新的mask去阻塞當前程序
(2)收到訊號,呼叫訊號的處理函式
(3)將mask設定為原先的掩碼
(4)sigsuspend函式返回

可以看出,sigsuspend函式是等待一個訊號發生,當等待的訊號發生時,執行完訊號處理函式後就會返回。它是一個原子操作。

保護臨界區的中斷:
(1)首先用sigprocmask去阻塞訊號
(2)執行後關鍵程式碼後,用sigsuspend去捕獲訊號
(3)然後sigprocmask去除阻塞
這樣訊號就不會丟失了,而且不會中斷臨界區。

使用pause函式對臨界區的保護:
這裡寫圖片描述

上面的程式是用pause去保護臨界區,首先用sigprocmask去阻塞SIGINT訊號,執行臨界區程式碼,然後解除阻塞。最後呼叫pause()函式等待訊號的發生。但此時會產生一個問題,如果訊號在解除阻塞與pause之間發生的話,訊號就可能丟失。這將是一個不可靠的訊號機制。
因此,採用sigsuspend可以避免上述情況發生。

使用sigsuspend對臨界區的保護:

這裡寫圖片描述

使用sigsuspend對臨界區的保護就不會產生上述問題了。

  1. sigsuspend函式的用法
    sigsuspend函式是等待的訊號發生時才會返回。
    sigsuspend函式遇到結束時不會返回,這一點很重要。
    示例:
    下面的例子能夠處理訊號SIGUSR1,SIGUSR2,SIGSEGV,其它的訊號被遮蔽,該程式輸出對應的訊號,然後繼續等待其它訊號的出現。
#include
#include
#include
#include
void myhandler(int signo);
int main(){
    struct sigaction action;
    sigset_t sigmask;
    sigemptyset(&sigmask);
    sigaddset(&sigmask,SIGUSR1);
    sigaddset(&sigmask,SIGUSR2);
    sigaddset(&sigmask,SIGSEGV);
    action.sa_handler=myhandler;
    action.sa_mask=sigmask;
    sigaction(SIGUSR1,&action,NULL);
    sigaction(SIGUSR2,&action,NULL);
    sigaction(SIGSEGV,&action,NULL);
    sigfillset(&sigmask);
    sigdelset(&sigmask,SIGUSR1);
    sigdelset(&sigmask,SIGUSR2);
    sigdelset(&sigmask,SIGSEGV);
    while(1){
        sigsuspend(&sigmask);//不斷的等待訊號到來
    }
    return 0;
}
void myhandler(int signo){
    switch(signo){
    case SIGUSR1:
        printf("received sigusr1 signal./n");
        break ;
    case SIGUSR2:
        printf("received sigusr2 signal./n");
        break;
    case SIGSEGV:
        printf("received sigsegv signal/n");
        break;
    }
}

程式執行結果:

received sigusr1 signal
received sigusr2 signal
received sigsegv signal
received sigusr1 signal
已終止

另一個終端用於傳送訊號:
先得到當前程序的pid, ps aux|grep 程式名

kill -SIGUSR1 4901
kill -SIGUSR2 4901
kill -SIGSEGV 4901
kill -SIGTERM 4901
kill -SIGUSR1 4901

解釋:
第一行傳送SIGUSR1,則呼叫訊號處理函式,打印出結果。
第二,第三行分別列印對應的結果。
第四行傳送一個預設處理為終止程序的訊號。
但此時,但不會終止程式,由於sigsuspend遇到終止程序訊號並不會返回,此時並不會打印出”已終止”,這個訊號被阻塞了。當再次傳送SIGURS1訊號時,程序的訊號阻塞恢復成預設的值,因此,此時將會解除阻塞SIGTERM訊號,所以程序被終止。

第五部分: 訊號的繼承與執行

當使用fork()函式時,子程序會繼承父程序完全相同的訊號語義,這也是有道理的,因為父子程序共享一個地址空間,所以父程序的訊號處理程式也存在於子程序中。

示例: 子程序繼承父程序的訊號處理函式

#include
#include
#include
#include
#include
void myhandler(int signo,siginfo_t *si,void *vcontext);
int main(){
    union sigval val;
    struct sigaction oldact,newact;
    newact.sa_sigaction=myhandler;
    newact.sa_flags=SA_SIGINFO|SA_RESETHAND;//表示採用sa_sigaction指示的函式以及執行完處理函式後恢復預設操作
    //註冊訊號處理函式
    sigaction(SIGUSR1,&newact,&oldact);
    if(fork()==0){
        val.sival_int=10;
        printf("子程序/n");
        sigqueue(getpid(),SIGUSR1,val);
    }
    else {
        val.sival_int=20;
        printf("父程序/n");
        sigqueue(getpid(),SIGUSR1,val);
    }

}

void myhandler(int signo,siginfo_t *si,void *vcontext){
    printf("訊號處理/n");
    printf("%d/n",(si->si_int));
}

輸出的結果為:

子程序
訊號處理
10
父程序
訊號處理
20

可以看出來,子程序繼承了父程序的訊號處理函式。

第六部分: 實時訊號中鎖的研究

  1. 訊號處理函式與主函式之間的死鎖
    當主函式訪問臨界資源時,通常需要加鎖,如果主函式在訪問臨界區時,給臨界資源上鎖,此時發生了一個訊號,那麼轉入訊號處理函式,如果此時訊號處理函式也對臨界資源進行訪問,那麼訊號處理函式也會加鎖,由於主程式持有鎖,訊號處理程式等待主程式釋放鎖。又因為訊號處理函式已經搶佔了主函式,因此,主函式在訊號處理函式結束之前不能執行。因此,必然造成死鎖。
    示例1: 主函式與訊號處理函式之間的死鎖
#include
#include
#include
#include
#include
#include
int value=0;
sem_t sem_lock;//定義訊號量
void myhandler(int signo,siginfo_t *si,void *vcontext);//程序處理函式宣告
int main(){
    union sigval val;
    val.sival_int=1;
    struct sigaction oldact,newact;
    int res;
    res=sem_init(&sem_lock,0,1);
    if(res!=0){
        perror("訊號量初始化失敗");
    }
    newact.sa_sigaction=myhandler;
    newact.sa_flags=SA_SIGINFO;
    sigaction(SIGUSR1,&newact,&oldact);
    sem_wait(&sem_lock);
    printf("xxxx/n");
    value=1;
    sleep(10);
    sigqueue(getpid(),SIGUSR1,val);//sigqueue傳送帶引數的訊號
    sem_post(&sem_lock);
    sleep(10);
    exit(0);
}

void myhandler(int signo,siginfo_t *si,void *vcontext){
    sem_wait(&sem_lock);
    value=0;
    sem_post(&sem_lock);
}

此程式將一直阻塞在訊號處理函式的sem_wait函式處。

  1. 利用測試鎖解決死鎖
    sem_trywait(&sem_lock);是非阻塞的sem_wait,如果加鎖失敗或者是超時,則返回-1。

示例2: 用sem_trywait來解決死鎖

#include
#include
#include
#include
#include
#include
int value=0;
sem_t sem_lock;//定義訊號量
void myhandler(int signo,siginfo_t *si,void *vcontext);//程序處理函式宣告
int main(){
    union sigval val;
    val.sival_int=1;
    struct sigaction oldact,newact;
    int res;
    res=sem_init(&sem_lock,0,1);
    if(res!=0){
        perror("訊號量初始化失敗");
    }
    newact.sa_sigaction=myhandler;
    newact.sa_flags=SA_SIGINFO;
    sigaction(SIGUSR1,&newact,&oldact);
    sem_wait(&sem_lock);
    printf("xxxx/n");
    value=1;
    sleep(10);
    sigqueue(getpid(),SIGUSR1,val);//sigqueue傳送帶引數的訊號
    sem_post(&sem_lock);
    sleep(10);
    sigqueue(getpid(),SIGUSR1,val);
    exit(0);
}
void myhandler(int signo,siginfo_t *si,void *vcontext){
    if(sem_trywait(&sem_lock)==0){
        value=0;
        sem_post(&sem_lock);
    }
}

第一次傳送sigqueue時,由於主函式持有鎖,因此,sem_trywait返回-1,當第二次傳送sigqueue時,主函式已經釋放鎖,此時就可以在訊號處理函式中對臨界資源加鎖了。
但這種方法明顯丟失了一個訊號,不是很好的解決方法。

  1. 利用雙執行緒來解決主函式與訊號處理函式死鎖
    我們知道,當程序收到一個訊號時,會選擇其中的某個執行緒進行處理,前提是這個執行緒沒有遮蔽此訊號。因此,可以在主執行緒中遮蔽訊號,另選一個執行緒去處理這個訊號。由於主執行緒與另外一個執行緒是平行執行的,因此,等待主執行緒執行完臨界區時,釋放鎖,這個執行緒去執行訊號處理函式,直到執行完畢釋放臨界資源。

這裡用到一個執行緒的訊號處理函式: pthread_sigmask
int pthread_sigmask(int how,const sigset_t *set,sigset_t *oldset);
這個函式與sigprocmask很相似。
how:
SIG_BLOCK 將訊號集加入到執行緒的阻塞集中去
SIG_UNBLOCK 將訊號集從阻塞集中刪除
SIG_SETMASK 將當前集合設定為執行緒的阻塞集

示例: 利用雙執行緒來解決主函式與訊號處理函式之間的死鎖

#include
#include
#include
#include
#include
#include
#include
void*thread_function(void *arg);//執行緒處理函式
void myhandler(int signo,siginfo_t *si,void *vcontext);//訊號處理函式
int value;
sem_t semlock;
int main(){
    int res;
    pthread_t mythread;
    void *thread_result;
    res=pthread_create(&mythread,NULL,thread_function,NULL);//建立一個子執行緒
    if(res!=0){
        perror("執行緒建立失敗");
    }

    //在主執行緒中將訊號遮蔽
    sigset_t empty;
    sigemptyset(&empty);
    sigaddset(&empty,SIGUSR1);
    pthread_sigmask(SIG_BLOCK,&empty,NULL);

    //主執行緒中對臨界資源的訪問
    if(sem_init(&semlock,0,1)!=0){
        perror("訊號量建立失敗");
    }

    sem_wait(&semlock);
    printf("主執行緒已經執行/n");
    value=1;
    sleep(10);
    sem_post(&semlock);
    res=pthread_join(mythread,&thread_result);//等待子執行緒退出
    exit(EXIT_SUCCESS);
}
void *thread_function(void *arg){
    struct sigaction oldact,newact;
    newact.sa_sigaction=myhandler;
    newact.sa_flags=SA_SIGINFO;
    //註冊訊號處理函式
    sigaction(SIGUSR1,&newact,&oldact);
    union sigval val;
    val.sival_int=1;
    printf("子執行緒睡眠3秒/n");
    sleep(3);
    sigqueue(getpid(),SIGUSR1,val);
    pthread_exit(0);//執行緒結束

}

void myhandler(int signo,siginfo_t *si,void *vcontext){
    sem_wait(&semlock);
    value=0;
    printf("訊號處理完畢/n");
    sem_post(&semlock);
}

執行結果如下:

主執行緒已經執行
子執行緒睡眠3秒
訊號處理完畢

解釋一下:

在主線執行緒中阻塞了SIGUSR1訊號,首先讓子執行緒睡眠3秒,目的讓主執行緒先執行,然後當主執行緒訪問臨界資源時,讓執行緒sleep(10),在這期間,子執行緒傳送訊號,此時子執行緒會去處理訊號,而主執行緒依舊平行的執行,子執行緒被阻止訊號處理函式的sem_wait處,等待主執行緒10後,訊號處理函式得到鎖,然後進行臨界資源的訪問。這就解決了主函式與訊號處理函式之間的死鎖問題。

擴充套件: 如果有多個訊號到達時,還可以用多執行緒來處理多個訊號,從而達到並行的目的,這個很好實現的,可以嘗試一下。

相關推薦

Linux訊號透徹分析理解

本文將從以下幾個方面來闡述訊號: (1)訊號的基本知識 (2)訊號生命週期與處理過程分析 (3) 基本的訊號處理函式 (4) 保護臨界區不被中斷 (5) 訊號的繼承與執行 (6)實時訊號中鎖的研究 第一部分: 訊號的基本知識 1.訊號本質

linux下 signal訊號機制的透徹分析各種例項講解

首先感謝上述兩位博主的詳細講解。 雖然內容有點長,但是分析的很全面,各種例項應用基本都考慮到了。 本文將從以下幾個方面來闡述訊號: (1)訊號的基本知識 (2)訊號生命週期與處理過程分析 (3) 基本的訊號處理函式 (4) 保護臨界區不被中斷 (5)

linux系統故障分析排查

使用 權限 建立 shel 自動識別 了解 緊急 rhel5 1.4 在處理Linux系統出現的各種故障時,故障的癥狀是最先發現的,而導致這以故障的原因才是最終排除故障的關鍵。熟悉Linux系統的日誌管理,了解常見故障的分析與解決辦法,將有助於管理員快速定位故障點。“對癥下

Linux系統故障分析排查--日誌分析

獲得 cat cron stl 文本格式 etc 服務的啟動 網絡 調試   處理Linux系統出現的各種故障時,故障的癥狀是最先發現的,而導致這以故障的原因才是最終排除故障的關鍵。熟悉Linux系統的日誌管理,了解常見故障的分析與解決辦法,將有助於管理員快速定位故障點,“

對01揹包的分析理解(圖文)

  首先謝謝Christal_R的文章(點選轉到連結)讓我學會01揹包 本文較長,但是長也意味著比較詳細,希望您可以耐心讀完。 題目: 現在有一個揹包(容器),它的體積(容量)為V,現在有N種物品(每個物品只有一個),每個物品的價值W[i]和佔用空間C[i]都會由輸入給出,現在問這個揹包最多

Bookshelf 2(poj3628,01揹包,dp遞推) 對01揹包的分析理解(圖文)

題目連結:Bookshelf 2(點選進入) 題目解讀: 給n頭牛,給出每個牛的高度h[i],給出一個書架的高度b(所有牛的高度相加>書架高度b),現在把一些牛疊起來(每頭牛隻能用一次,但不同的牛可能身高相同),在這些疊起來的牛的總高度>書架b的基礎上,找出最小的差距(由於輸入的資料會保證所有

linux 訊號signal和sigaction理解

今天看到unp時發現之前對signal到理解實在淺顯,今天拿來單獨學習討論下。   signal,此函式相對簡單一些,給定一個訊號,給出訊號處理函式則可,當然,函式簡單,其功能也相對簡單許多,簡單給出個函式例子如下:     1 #incl

Linux I2C驅動分析實現--例子

通過上篇《Linux I2C驅動分析與實現(二)》,我們對Linux子系統已經不陌生,那麼如何實現I2C驅動呢? 編寫客戶驅動的方法 在核心中有兩種方式的i2c客戶驅動的編寫方法,一種叫legacy傳統方式,另一種是newstyle方式. 前 一種legacy是一種舊式的方法,在2.

Android java層音訊相關的分析理解(二)音量控制相關

上一篇我們簡單地說了一下Android java層的基本框架。接下來我們就來聊一下在android中音量控制的相關內容。 1.音量定義 在Android中,音量的控制與流型別是密不可分的,每種流型別都獨立地擁有自己的音量設定,各種流型別的音量是互不干擾的,例如音樂音量、通話

有關 Python 2 和 Sublime Text 中文 Unicode 編碼問題的分析理解

問題背景: 相信很多用 Sublime Text 來寫 Python 2 的同學都遇到過以下這個問題(例如這位同學 /t/100435 和這位同學 /t/163012 ): 在 Sublime Text 裡用 Cmd (Ctrl) + B 執行程式碼 print u'中文',想要打印出 unicode 型

Linux系統日誌分析管理(14)

當你的 Linux 系統出現不明原因的問題時,你需要查閱一下系統日誌才能夠知道系統出了什麼問題了,所以說了解系統日誌是很重要的事情,系統日誌可以記錄系統在什麼時間、哪個主機、哪個服務、出現了什麼資訊等,這些資訊也包括使用者識別資料、系統故障排除等,如果你能夠善用這些日誌檔案資訊的話,你的系統出現錯誤時,你將可

Linux:sk_buff完全剖析理解【轉】

sk_buff 目錄 1 sk_buff介紹 2 sk_buff組成 3 struct sk_buff 結構體 4 sk_buff成員變數 4.1 Layout佈局 4.2 General通用 4.3 Feature-specific功能相關 5 sk_buff管理和

效能結果分析理解(關於90%響應時間、圖表等)

描述性統計與效能結果分析——《LoadRunner 沒有告訴你的》之一LoadRunner中的90%響應時間是什麼意思?這個值在進行效能分析時有什麼作用?本文爭取用最簡潔的文字來解答這個問題,並引申出“描述性統計”方法在效能測試結果分析中的應用。為什麼要有90%使用者響應時間

黑馬程式設計師 交通燈管理系統的分析理解

----------android培訓、java培訓、java學習型技術部落格、期待與您交流!---------- 交通燈管理系統 需求說明 非同步隨機生成按照各個路線行駛的車輛。 例如: 由南向而來去往北向的車輛----直行車輛 由西向而來去往南向的車輛----右轉車

Linux訊號機制分析訊號處理函式

【摘要】本文分析了Linux核心對於訊號的實現機制和應用層的相關處理。首先介紹了軟中斷訊號的本質及訊號的兩種不同分類方法尤其是不可靠訊號的原理。接著分析了核心對於訊號的處理流程包括訊號的觸發/註冊/執行及登出等。最後介紹了應用層的相關處理,主要包括訊號處理函式的安裝、訊號

Android java層音訊相關的分析理解(一)基本框架

最近在整理之前在公司寫的一些文件,於是決定將部分適用比較廣的文件整理在部落格中,供大家參考。第一個系列是AudioService相關的。這個可以算是《深入理解Android 卷Ⅲ》的一個讀書筆記吧。整體的思路基本上與《深入理解Android 卷Ⅲ》的Audio部分差不多。只

關於程式設計正規化的分析理解

 隨著程式設計(programming、偶不喜歡說程式設計)方法學和軟體工程研究的深入,特別是OO思想的普及,正規化(paradigm)以及程式設計正規化等術語漸漸出現在人們面前。 面向物件程式設計(OOP)常常被譽為是一種革命性的思想,正因為它不同於其他的各種程式設計

oracle 遊標分析理解(基礎)

分支 屬性變量 打印 一次 vsa 分享圖片 如果 number for --------------堅持寫一點 慢慢成長 希望對大家有所幫助(小白的理解) 也是自己學習後的理解(只是一小部分,需要更深沈的還需日後成長) 接下來就是我們的重點 --遊標 提供了一種對

Linux input子系統編程、分析模板

linux輸入設備都有共性:中斷驅動+字符IO,基於分層的思想,Linux內核將這些設備的公有的部分提取出來,基於cdev提供接口,設計了輸入子系統,所有使用輸入子系統構建的設備都使用主設備號13,同時輸入子系統也支持自動創建設備文件,這些文件采用阻塞的IO讀寫方式,被創建在"/dev/input/"下。如下

Linux內核哈希表分析應用

構造方法 init lis 個數 無需 表示 字節 div 擴展 目錄(?)[+] Linux內核哈希表分析與應用 Author:tiger-johnTime:2012-12-20mail:[email protected]/* */Blog