信號的捕捉與可重入函數

  • 信號的捕捉

  • 在信號的相關概念中曾提到如果一個信號沒有被Block,但被Pending,但不會立即遞達,而是在合適的時候,這裏的合適的時候是指:當進程從內核態返回用戶態時,會對信息進行檢測處理。

  • 正如下圖所示,是系統在對信號進行捕捉時經歷的過程:
    這裏寫圖片描述

可以簡化爲:

這裏寫圖片描述

  • 首先先來介紹一下什麼是信號捕捉:
    如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱爲信號捕捉

  • 那麼內核是如何實現信號捕捉的呢??
    由於信號處理函數的代碼是在用戶空間的,處理過程比較複雜,舉例如下:用戶程序註冊了SIGQUIT信號的處理函數sighandler。當前正在執行main函數,這是發生中斷或異常切換到內核態。在中斷處理完畢後要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。內核決定返回用戶態後不是恢復main函數的上下文繼續執行,而是執行sighandler函數,sighandler和main函數使用不同的堆棧空間,他們之間不存在調用和被調用的關係,是兩個獨立的控制流程。sighandler函數返回後自動執行特殊的系統調用sigreturn再次進入內核態。如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。

  • sigaction函數

頭文件:#include<signal.h>
函數原型:int sigaction(int signo,const struct sigaction                    *act,struct sigaction *oact);
函數功能:可以讀取和修改與指定信號相關聯的處理動作。
返回值:調用成功返回0,出錯返回-1。
參數:signo是指定信號的編號。若act指針非空,則根據act修改該信號的處理動作。若oact指針非空,則通過oact傳出該信號原來的處理動作。act和oact指向sigaction結構體
struct sigaction結構體
    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_restorer:該元素是過時的,不應該使用,POSIX.1標準將不指定該元素。(棄用)
sa_sigaction:當sa_flags被指定爲SA_SIGINFO標誌時,使用該信號處理程序。(很少使用)
重點掌握:
① sa_handler:指定信號捕捉後的處理函數名(即註冊函數),用於指向原型爲void handler(int)的信號處理函數地址。也可賦值爲SIG_IGN表忽略 或 SIG_DFL表執行默認動作
② sa_mask: 調用信號處理函數時,所要屏蔽的信號集合(信號屏蔽字)。注意:僅在處理函數被調用期間屏蔽生效,是臨時性設置。
③ sa_flags:通常設置爲0,表使用默認屬性。

  • pause函數
頭文件:#include<unsitd.h>
函數原型:int pause(void);
函數功能:使調用進程掛起直到有信號遞達

如果信號的處理動作是終止進程,則進程終止,pause函數沒有機會返回;如果信號的處理動作默認是忽略,則進程繼續處於掛起狀態,pause不返回;如果信號的處理動作是捕捉,則調用了信號處理函數後pause返回-1,errno設置爲EINTR(被信號中斷),所以pause只有出錯的返回值
什麼叫把進程(PCB)掛起??
把進程狀態設置爲T狀態,把進程放到等待隊列裏
掛起不能被調度,只有R狀態才能被調度

  • 下面我們用alarm和pause實現mysleep函數
    思想:
    1.首先main函數調用mysleep函數,後者調用sigaction註冊了SIGALAM信號的處理函數sig_alrm
    2.調用alarm(n)設定鬧鐘
    3.調用pause等待,內核切換到別的進程運行
    4.n秒後,鬧鐘超時,內核發送SIGALRM信號給該進程
    5.從內核態返回用戶態之前處理未決信號,發現有SIGALRM信號,處理函數是sig_alrm
    6.切換到用戶態執行sig_alrm函數,進入sig_alrm函數時SIGALRM信號被自動屏蔽,從sig_alrm函數返回時SIGALRM信號自動解除屏蔽。然後自動執行系統調用sigreturn再次進入內核,再返回用戶態繼續執行進程的主控制流程
    7.pause函數返回-1,然後調用alarm(0)取消鬧鐘,調用sigaction恢復SIGALRM信號以前的處理動作

  • 代碼實現如下

#include <stdio.h>
#include<signal.h>
#include<unistd.h>
void sig_alarm(int signo)
{
    (void)signo;
}
unsigned int mysleep(unsigned int n)
{
    struct sigaction New,Old;
    unsigned int unslept = 0;
    New.sa_handler = sig_alarm;
    sigemptyset(&New.sa_mask);
    New.sa_flags = 0;
    sigaction(SIGALRM,&New,&Old);//註冊信號處理函數
    alarm(n);//設置鬧鐘
    pause();
    unslept = alarm(0);//清空鬧鐘
    sigaction(SIGALRM,&Old,NULL);//恢復默認信號處理動作
    return unslept;
}
int main()
{
    while(1)
    {
        mysleep(3);
        printf("hello world!\n");
    }
    return 0;
}
  • 運行結果如圖:

這裏寫圖片描述

這裏寫圖片描述

但是系統運行的時序並不像我們寫程序時所設想的那樣。雖然alarm(n)緊接進行pause(),但是無法保證pause()一定會在調用alarm(n)之後的n秒之內被調用。由於異步事件在任何時候都有可能發生,如果我們寫程序時考慮不周全,就可能由於時序問題而導致錯誤,這叫做競態條件
解決以上問題的方法就是將“解除信號屏蔽”和“掛起等待信號”這兩步合併成一個原子操作,這是就要使用sigsuspend函數了。
sigsuspend函數

頭文件:#include<signal.h>
函數原型:int sigsuspend(const sigset_t *sigmask)
函數功能:解除對信號的屏蔽並掛起
  • 可重入函數

  • 首先解釋一下什麼是重入:

當一個函數被不同的執行流程控制,有可能在第一次調用還沒返回時就再次進入此函數,就稱爲重入

  • 那什麼樣的函數是可重入的呢??

如果一個函數只訪問自己的局部變量或參數,數據和狀態不會被破壞,則稱爲可重入函數。反知,如果一個函數訪問全局鏈表,有可能因爲重入而發生錯亂,不能保證函數的行爲一致和結果相同,像這樣的函數就是不可重入的

  • 可重入和線程安全是兩個不同的概念:可重入函數一定是線程安全的;線程安全的函數可能是重入的,也可能是不重入的;線程不安全的函數一定是不可重入的

  • 爲什麼兩個不同的控制流程調用同一個函數,訪問它的同一個局部變量或參數就不會造成錯亂?

    在線程之中,線程雖然強調資源共享,但是他們的棧卻是獨有的,所以訪問它的同一個局部變量或參數就不會造成錯亂。

  • 如果一個函數滿足一下條件之一就是不可重入的:
    1.調用了malloc或free,因爲malloc也是用全局鏈表來管理堆的
    2.調用了標準I/O庫函數,標準I/O庫的實現都以不可重入的方式使用全局數據結構
    3.進行了浮點運算,許多處理器/編譯器,浮點運算都是不可重入的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章