Linux定時器處理之實時信號使用,消息隊列阻塞模型,避免超時等待

man msgrcv翻到msgrcv函數英文有段話說明了,意思是這樣,當msgrcv所在的進程捕獲到一個信號的時候,該函數會調用失敗並且把errno設置爲EINTR,也就是說這個時候msgrcv就不會繼續阻塞了,會直接返回,如果在這之前啓動了定時器,這個時候就可以進行超時判斷,判斷是否還需要阻塞等待。這個時候問題來了,定時器是通過信號通知機制實現的,timer_t, sigevent

the calling process catches a signal. In this case the system call fails with the errno set to EINTR.(msgrcv is never automatically restarted after being interrupted by a signal handler, regardless of the setting the SA_RESTART flag when establshing a signal handler)

開始使用SIGALARM(14),作爲定時器的到時通知標誌,一般情況下是不會有問題的,但是當tps壓力加大,併發請求增多的時候,msgrcv等待的內容始終沒有返回回來,導致線程一直阻塞,使用沒有退出來判斷超時,系統直接就停止對外提供響應了,必須重啓才能恢復。。。

原因在於信號分爲可靠信號(實時信號)和不可靠信號(非實時信號),前者會排隊等待,不會被丟棄,後者卻會被丟棄

另外實時信號SIGRTMIN,有可能並不是一個常量的宏定義,在SUSE環境下是一個函數定義,所以這個宏此時在switch case語句裏面的時候就會編譯報錯。。。只能使用if判斷了。。


還有就是上面的英文括號裏面的話,如果信號註冊函數設置了SA_RESTART標誌的話,那麼信號的捕獲終端對原來的程序邏輯就不會產生影響,原有的阻塞型系統調用如read、msgrcv不會被調用失敗返回,會繼續阻塞等待。。


關於SA_RESTART信號

http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=28852942&id=3754478

#include
int sigaction(int sigon,const struct sigaction &restrict act,

            struct sigaction &restrict oact);
            成功返回0,出錯返回-1
此函數使用下列結構:
struct sigaction{
 void    ( *sa_handler)(int); //信號處理函數地址 sigset_t sa_mask;       //一個調用信號捕捉函數之前要加到進程信號屏蔽字中的信號集
 int    sa_flags;       //信號處理選項
 
 void   (*sa_sigaction)(int,siginfo_t *,void *); 
}

對於sigaction 函數本身我們不做多介紹。我們重點是在 sa_flags 的不同值的情況下,sigaction函數的處理方式


SA_INTERRUPT:由此信號中斷的系統調用不會自動重啓動(針對sigaction的XSI默認處理方式不會自動重啓) 

SA_RESTART:  由此信號中斷的系統調用會自動重啓。
   
   我們看下面這段程序:
 4 void sig_int(int signo){
  5         printf("\nint sig_int\n");
  6 }
  7 
  8 int main(void){
  9         struct sigaction new_action;
 10         new_action.sa_handler=sig_int;
 11         if(sigemptyset(&new_action.sa_mask)==-1){
 12                 printf("set new action mask error\n");
 13                 exit(1);
 14         }
 15         new_action.sa_flags=0;
 16 
 17         if(sigaction(SIGINT,&new_action,NULL)==-1){
 18                 printf("set SIGINT process error\n");
 19                 exit(1);
 20         }
 21         char c;
 22         read(0,&c,1);
 23         printf("read :%c\n",c);
 24         exit(0);
 25 }

第十五行中我們沒有使用任何標誌。設置捕捉 SIGINT 信號,然後讀標準輸入

輸出如下:
   feng@ubuntu:~/learn_linux_c_second/chapter_10$ ./a.out
 ^C
 int sig_int
 read :?
從輸出我們看到 的確是一樣的。

現在 我們將 第十五行改爲  new_action.sa_flags=SA_RESTART; 再看看輸出情況
<a href="mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2><span style=" font-size:16px;"="" style="word-wrap: break-word; text-decoration: none; color: rgb(25, 89, 155);">feng@ubuntu:~/learn_linux_c_second/chapter_10$ ./a.out
^C
int sig_int
^C
int sig_int
^C
int sig_int
^\Quit (core dumped)
 從輸出中我們看到  當多次按下 中斷鍵後 進程捕捉到信號並打印消息,但是進程並未結束,而是繼續等待輸入。按下退出鍵時進程才退出
 也就是說 使用了 SA_RESTART標誌後 read調用被中斷後 會自動重啓 繼續等待中斷輸入。



http://www.cnblogs.com/cobbliu/p/5592659.html

linux 多線程信號總結(一)

1. 在多線程環境下,產生的信號是傳遞給整個進程的,一般而言,所有線程都有機會收到這個信號,進程在收到信號的的線程上下文執行信號處理函數,具體是哪個線程執行的難以獲知。也就是說,信號會隨機發個該進程的一個線程。

2 signal函數BSD/Linux的實現並不在信號處理函數調用時,恢復信號的處理爲默認,而是在信號處理時阻塞此信號,直到信號處理函數返回。其他實現可能在調用信號處理函數時,恢復信號的處理爲默認方式,因而需要在信號處理函數中重建信號處理函數爲我們定義的處理函數,在這些系統中,較好的方法是使用sigaction來建立信號處理函數。

3 發送信號給進程,哪個線程會收到?APUE說,在多線程的程序中,如果不做特殊的信號阻塞處理,當發送信號給進程時,由系統選擇一個線程來處理這個信號。

4 如果進程中,有的線程可以屏蔽了某個信號,而某些線程可以處理這個信號,則當我們發送這個信號給進程或者進程中不能處理這個信號的線程時,系統會將這個信號投遞到進程號最小的那個可以處理這個信號的線程中去處理。

5 如果我們同時註冊了信號處理函數,同時又用sigwait來等待這個信號,誰會取到信號?經過實驗,Linux上sigwait的優先級高。 

6 在Linux中的posix線程模型中,線程擁有獨立的進程號,可以通過getpid()得到線程的進程號,而線程號保存在pthread_t的值中。而主線程的進程號就是整個進程的進程號,因此向主進程發送信號只會將信號發送到主線程中去。如果主線程設置了信號屏蔽,則信號會投遞到一個可以處理的線程中去。

7 當調用SYSTEM函數去執行SHELL命令時,可以放心的阻塞SIGCHLD,因爲SYSTEM會自己處理子進程終止的問題。 

8 使用sleep()時,要以放心的去阻塞SIGALRM信號,目前sleep函數都不會依賴於ALRM函數的SIGALRM信號來工作。

 

 

linux 多線程信號總結(二)

1. 默認情況下,信號將由主進程接收處理,就算信號處理函數是由子線程註冊的

2. 每個線程均有自己的信號屏蔽字,可以使用sigprocmask函數來屏蔽某個線程對該信號的響應處理,僅留下需要處理該信號的線程來處理指定的信號。

3. 對某個信號處理函數,以程序執行時最後一次註冊的處理函數爲準,即在所有的線程裏,同一個信號在任何線程裏對該信號的處理一定相同

4. 可以使用pthread_kill對指定的線程發送信號

APUE的說法:每個線程都有自己的信號屏蔽字,但是信號的處理是進程中所有的線程共享的,這意味着儘管單個線程可以阻止某些信號,但當線程修改了與某個信號相關的處理行爲後,所有的線程都共享這個處理行爲的改變。這樣如果一個線程選擇忽略某個信號,而其他線程可以恢復信號的默認處理行爲,或者爲信號設置一個新的處理程序,從而可以撤銷上述線程的信號選擇。

進程中的信號是送到單個線程的,如果信號與硬件故障或者計時器超時有關,該型號就被髮送到引起該事件的線程中去,而其他的信號則被髮送到任意一個線程。

sigprocmask的行爲在多線程的進程中沒有定義,線程必須使用pthread_sigmask

總結:一個信號可以被沒屏蔽它的任何一個線程處理,但是在一個進程內只有一個多個線程共用的處理函數。......

 

linux 多線程信號總結(三)

1 Linux 多線程應用中,每個線程可以通過調用pthread_sigmask() 設置本線程的信號掩碼。一般情況下,被阻塞的信號將不能中斷此線程的執行,除非此信號的產生是因爲程序運行出錯如SIGSEGV;另外不能被忽略處理的信號SIGKILL 和SIGSTOP 也無法被阻塞。

2 當一個線程調用pthread_create() 創建新的線程時,此線程的信號掩碼會被新創建的線程繼承。

信號安裝最好採用sigaction方式,sigaction,是爲替代signal 來設計的較穩定的信號處理,signal的使用比較簡單。signal(signalNO,signalproc);

不能完成的任務是:1.不知道信號產生的原因;

2.處理信號中不能阻塞其他的信號

而signaction,則可以設置比較多的消息。尤其是在信號處理函數過程中接受信號,進行何種處理。

sigaction函數用於改變進程接收到特定信號後的行爲。

4 sigprocmask函數只能用於單線程,在多線程中使用pthread_sigmask函數。

5 信號是發給進程的特殊消息,其典型特性是具有異步性。

6 信號集代表多個信號的集合,其類型是sigset_t。

7 每個進程都有一個信號掩碼(或稱爲信號屏蔽字),其中定義了當前進程要求阻塞的信號集。

所謂阻塞,指Linux內核不向進程交付在掩碼中的所有信號。於是進程可以通過修改信號掩碼來暫時阻塞特定信號的交付,被阻塞的信號不會影響進程的行爲直到該信號被真正交付。 

忽略信號不同於阻塞信號,忽略信號是指Linux內核已經嚮應用程序交付了產生的信號,只是應用程序直接丟棄了該信號而已。

10 sleep和nanosleep,如果沒有在它調用之前設置信號屏蔽字的話,都可能會被信號處理函數打斷。參見sleep和nanosleep的mannual。



http://www.cnblogs.com/blueyunchao0618/p/5953862.html

轉自:http://blog.sina.com.cn/s/blog_636a55070101vs2d.html

轉自:http://blog.csdn.net/tiany524/article/details/17048069

首先感謝上述兩位博主的詳細講解。

雖然內容有點長,但是分析的很全面,各種實例應用基本都考慮到了。

 

本文將從以下幾個方面來闡述信號:

(1)信號的基本知識

(2)信號生命週期與處理過程分析

(3) 基本的信號處理函數

(4) 保護臨界區不被中斷

(5) 信號的繼承與執行

(6)實時信號中鎖的研究

 

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

 

1.信號本質:

 

信號的本質是軟件層次上對中斷的一種模擬(軟中斷)。它是一種異步通信的處理機制,事實上,進程並不知道信號何時到來。

 

2.信號來源

(1)程序錯誤,如非法訪問內存

(2)外部信號,如按下了CTRL+C

(3)通過kill或sigqueue向另外一個進程發送信號

 

3.信號種類

信號分爲可靠信號與不可靠信號,可靠信號又稱爲實時信號,非可靠信號又稱爲非實時信號。

信號代碼從1到32是不可靠信號,不可靠信號主要有以下問題:

(1)每次信號處理完之後,就會恢復成默認處理,這可能是調用者不希望看到的(早期的signal函數,linux2.6.35.6內核經驗證已經不再恢復默認動作)。

(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)處理過程:

程序運行在用戶態時->進程由於系統調用或中斷進入內核->轉向用戶態執行信號處理函數->信號處理函數完畢後進入內核->返回用戶態繼續執行程序

 

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

 

信號處理函數的過程大概是這樣了。

 

具體的可參考http://www.spongeliu.com/165.html Linux內核信號處理機制介紹

 

                 http://blog.csdn.net/tiany524/article/details/17048069

 

參考圖:

 

 

 

第三部分: 基本的信號處理函數

首先看一個兩個概念: 信號未決與信號阻塞

信號未決: 指的是信號的產生到信號處理之前所處的一種狀態。確切的說,是信號的產生到信號註銷之間的狀態。

信號阻塞: 指的是阻塞信號被處理,是一種信號處理方式。

 

 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後將集合從阻塞集合中移除。
[root@localhost 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;

}

運行結果:


[root@localhost 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函數使用了系統全局內存分配表。

 

2. 保護臨界區不被中斷 (參考 APUE sigsuspend章節)

由於臨界區的代碼是關鍵代碼,是非常重要的部分,因此,有必要對臨界區進行保護,不希望信號來中斷臨界區操作。這裏通過信號屏蔽字來阻塞信號的發生。

 

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

 

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對臨界區的保護就不會產生上述問題了。

 

 

 

 

 3. 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函數處。

 

 

2. 利用測試鎖解決死鎖

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時,主函數已經釋放鎖,此時就可以在信號處理函數中對臨界資源加鎖了。

但這種方法明顯丟失了一個信號,不是很好的解決方法。

 

 

 

3. 利用雙線程來解決主函數與信號處理函數死鎖

我們知道,當進程收到一個信號時,會選擇其中的某個線程進行處理,前提是這個線程沒有屏蔽此信號。因此,可以在主線程中屏蔽信號,另選一個線程去處理這個信號。由於主線程與另外一個線程是平行執行的,因此,等待主線程執行完臨界區時,釋放鎖,這個線程去執行信號處理函數,直到執行完畢釋放臨界資源。

 

這裏用到一個線程的信號處理函數: 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後,信號處理函數得到鎖,然後進行臨界資源的訪問。這就解決了主函數與信號處理函數之間的死鎖問題。

 

擴展: 如果有多個信號到達時,還可以用多線程來處理多個信號,從而達到並行的目的,這個很好實現的,可以嘗試一下。


發佈了94 篇原創文章 · 獲贊 28 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章