程序員成長之旅——進程信號

信號的概念

信號就是軟中斷。
信號提供異步處理事件的一種方式。例如:用戶在終端按下結束進程鍵,使一個進程提前終止。
每一個信號都有一個名字,它們的名字都以SIG打頭。例如,每當進程調用了abort函數時,都會產生一個SIGABRT信號。
每一個信號對應一個正整數,定義在頭文件

信號產生的場景

  • 當用戶在終端按下特定的鍵時,會產生信號。例如,當用戶按下DELETE按鍵(或Control-C)時,會產生一箇中斷信號(interrupt signal,SIGINIT),該信號使得一個運行中的程序終止。
  • 硬件異常可以產生信號。會引發硬件異常的情況如除以0,非法內存引用(invalid memory reference)等。這種情況會被硬件檢測到,並通知內核,然後內核產生相應的信號通知對應的運行進程。例如,當一個進程執行了一個非法的內存引用,會觸發SIGSEGV信號。
  • kill函數允許當前進程向其他的進程或者進程組發送任意的信號。當然,這種方法存在限制:我們必須是信號接收進程的所有者,或者我們必須是超級用戶(superuser)。
  • kill命令的作用和kill函數類似。這個命令多用戶殺死後臺進程。
  • 軟件異常可以根據不同的條件產生不同的信號。例如:網絡連接中接受的數據超出邊界時,會觸發SIGURG信號。

對於進程來說,信號是隨機產生的,所以進程不能簡單地根據檢測某個變量是否改變來判斷信號是否發生,而應該告訴內核“當這個信號發生時,做下面的這些事情”。

信號的處理

對於進程來說,不能判別是否出現一個信號,而是必須要告訴內核信號出現的時候,執行下列操作。
信號的處理方式有三種:

  1. 忽略此信號
  2. 執行信號的默認處理動作。
  3. 提供自定義行爲,要求處理該信號的時候切換到用戶態執行這個處理函數,也叫做捕捉一信號。

注:捕捉信號的時候需要注意不能捕捉SIGKILL信號和SIGSTOP信號。當捕捉到SIGCHLD信號,這個時候標識一個子進程已經終止,所以這個時候我們可以調用waitpid函數來取得該子進程的進程ID以及它的終止狀態。

對於一些信號發生時,會造成進程終止,同時生成一個core文件,該core文件記錄了該進程終止時的內存情況,可以幫助調試和調查進程的終止狀態。

有幾種情況不會生成core文件

  • 如果進程設置了suid位(chmod u+s file),並且當前用戶不是程序文件的所有者;
  • 如果進程設置了guid位(set-group-ID),並且當前用戶不是程序文件的組所有者;
  • 如果過戶沒有當前工作目錄的寫權限;
  • 如果core文件已經存在,並且用戶沒有該文件的寫權限;
    該core文件太大(由參數RLIMIT_CORE限制)

產生信號

(1)終端產生信號
首先提出一個概念叫做 core dump,我想在linux下寫c,肯定不少發現錯誤的時候報這個錯誤接下來我們先來看看這個東西到底是個什麼。
core dump叫做核心轉儲,也叫做核心文件(core file),是操作系統在進程收到某些信號而終止運行時,將此時進程的地址空間的內容以及有關進程狀態的其他信息寫出的一個磁盤文件,這個信息我們常常用於調試程序。
默認的linux系統當中是不生成這個文件的,我們可以使用 ulimit -a 查看系統中這個文件的大小。
在這裏插入圖片描述
我們可以使用命令 ulimit -c xxxx 設置生成的core dump的大小。
在這裏插入圖片描述
默認情況下,生成的core dump文件的格式是core.xxx,後面一般都是pid。並且生成在當前目錄下。
現在我們模擬生成一下這樣的core dump文件,我們首先寫出一個死循環。

int main()
{
    printf("hello world\n");
    while(1);

    return 0;
}

我們運行這個程序,然後操作,Ctrl+\,這樣就會出現:
在這裏插入圖片描述
從上圖我們可以看到我們操作過程中從鍵盤Ctrl+,這樣就會產生一個信號SIGQUIT,這個信號傳遞給運行的進程,然後進程得到這個信號引發終止進程並引發核心轉儲。

接下來我們來看看如何利用這個coredump文件進行調試

我們直接gdb tese文件和core文件就好,在終端輸入 gdb tese core.4622 得到:
在這裏插入圖片描述
可以很快定位到錯誤之處。
(2)通過系統調用產生信號
我們可以通過系統調用來產生信號。這裏我們先來看一下kill函數。

int kill(pid_t pid, int sig);

kill函數可以給指定的進程發送信號。
這個函數當中一個參數是進程的pid,第二個參數是我們需要發送給pid進程的一個信號的序號,比如sig = 9 ,那我們就發送SIGKILL。

再來介紹一個raise函數

int raise(int sig);

這個函數是用來給當前進程發送信號的。
abort函數使得當前進程接收到信號而異常終止。

void abort(void);

這個函數會產生SIGABRT信號,這個信號是夭折信號。
(3)軟件產生信號
軟件產生信號這裏我們首先來說一個函數alarm函數:

unsigned int alarm(unsigned int seconds);

裏面的變量seconds所給的是一個時間,單位是秒。這個函數的意思就是類似鬧鐘的形式,alarm(1)的意思讓操作系統在1秒鐘以後結束這個進程alarm的默認行爲動作就是終止這個進程。alarm函數的信號SIGALRM信號,這個信號的默認動作就是終止這個進程,當使用alarm(0)的意思就是取消以前設定的鬧鐘,返回值就是所剩餘的時間。調用alarm函數會產生SIGALRM信號。
(4)硬件異常產生信號
硬件異常產生信號,這些條件由硬件檢測並通知內核,然後內核向當前進程發送適當的信號,例如當前進程執行了除以0的指令,CPU運算單元會產生異常,內核將這個異常解釋爲SIGFPE信號發送給該進程,再比如當前進程訪問了非法內存地址,MMU會產生異常,內核將這個異常解釋爲SIGSEGV信號發送給該進程。

阻塞信號

(1)阻塞的概念
信號遞達:正在執行信號處理的動作
信號產生與信號遞達之間叫做信號未決,也叫pending。
當信號阻塞的時候不會遞達,接觸阻塞,信號才能遞達。
關於信號,我們首先需要從內核的角度來看看信號。
在內核當中,當一個進程接收到信號,會對應的在進程的pcb當中有三個相關的結構
在這裏插入圖片描述
因爲我們現在有31個普通信號,所以這個時候我們可以想下我們前期所說的位圖,我們也就可以利用一個整形就夠了,每一個信號對應一個比特位。

另外因爲是bit位,所以這裏注意,即使你產生了多個信號,這裏的信號位也只是從0變爲1,不記錄信號產生了多少次。

pending表標識信號未決表,表示信號是否產生,block阻塞表,表示當前進程與信號屏蔽相關內容。我們也把阻塞信號集叫做當前進程的信號屏蔽字。

注意阻塞和忽略是兩回事,阻塞只是屏蔽了信號,而忽略是對信號的一種處理方式。
(2)信號集相關的函數
在linux下信號我們定義成爲sigset_t類型的,sigset_t我們叫做信號集,這種類型經過我的測試大小是128個字節。
信號集下面有一些函數。

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 sigismember(const sigset_t *set, int signo); 

這裏的函數都放在signal.h當中,sigemptyset函數用來初始化set所指向的信號集,使得信號集所有信號的對應的bit位清空。
sigfillset函數標識對set所指向的信號集的所有位進行置位操作。
注意,使用信號集之前一定得先試用sigemptyset或者是sigfillset進行初始化信號集。
sigaddset是對set所指向的信號集進行進行添加一個信號signo。
sigdelset函數是對信號集進行刪除有效的信號。
sigismember函數是用來判斷是否在set所指向的信號集當中包含signo信號。

說完看這些函數我們再說一個和信號屏蔽字相關的函數,sigprocmask函數,

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

這個函數是用來進行讀取或者修改進程的信號屏蔽字這裏的how說的是如何進行更改,set指向你要修改的當前信號屏蔽字,oldset指向修改前你的信號屏蔽字。
在這裏插入圖片描述
注意:如果調用了sigprocmask解除了對當前若干個未決信號的阻塞,則在sigprocmask返回前,至少會將其中的一個信號遞達。

接下來說另外的一個函數叫做sigpending,它用來輸出pending表中的內容。

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

void printfspending(sigset_t *set)
{
    int i=0;
    for(i=0;i<32;i++)
    {
        if(sigismember(set,i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
int main()
{
    sigset_t set,oset;
    sigemptyset(&set);
    printfspending(&set);
    sigaddset(&set,SIGINT);
    sigprocmask(SIG_BLOCK,&set,NULL);
    while(1)
    {
        sigpending(&oset);
        printfspending(&oset);
        sleep(1);
    }

    return 0;
}

在這裏插入圖片描述

捕捉信號

先來提出一個函數就叫做sigaction函數,這個函數可以修改和信號相關聯的動作,實現信號的捕捉。

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

struct 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);
};

在這裏插入圖片描述
我們也可以使用signal函數可以實現這個功能。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

它的第一個參數是信號的編號,第二個參數是指向自定義函數的指針,就是當你捕捉到這個信號,不讓它去做它的默認操作,而是去做你想要讓它做函數,這個參數是一個返回值爲void,參數爲int的一個函數指針。

signal是C標準庫提供的信號處理函數,

接下來說一說信號捕捉的時候的狀態轉換:
在這裏插入圖片描述
從上面這張圖就可以看出整個狀態的轉換,

1.首先當你遇到中斷、異常或者系統調用的時候進入內核態。
2.然後產生信號,這樣由內核態切換用戶態,這個過程當中需要去PCB檢查那三張表,然後發現有遞達的信號,然後這個時候就去處理信號對應的操作。也就是信號處理函數。
3.處理信號處理函數的時候,這個時候爲了安全的問題,這個時候爲用戶態。
4.信號處理函數結束後,然後從用戶態切換到內核態。
5.然後由內核態切換到中斷異常執行處的用戶態。

所以總共有4次狀態的切換。

可重入信號

可重入函數主要用於多任務環境中,一個可重入的函數簡單來說就是可以被中斷的函數,也就是說,可以在這個函數執行的任何時刻中斷它,轉入OS調度下去執行另外一段代碼,而返回控制時不會出現什麼錯誤;而不可重入的函數由於使用了一些系統資源,比如全局變量區,中斷向量表等,所以它如果被中斷的話,可能會出現問題,這類函數是不能運行在多任務環境下的。
注意事項
編寫可重入函數時,若使用全局變量,則應通過關中斷、信號量(即P、V操作)等手段對其加以保護。

若對所使用的全局變量不加以保護,則此函數就不具有可重入性,即當多個進程調用此函數時,很有可能使有關全局變量變爲不可知狀態。

如何將一個不可重入的函數改寫成可重入的函數?

答:把一個不可重入函數變成可重入的唯一方法是用可重入規則來重寫它。其實很簡單,只要遵守了幾條很容易理解的規則,那麼寫出來的函數就是可重入的。

  1. 不要使用全局變量。因爲別的代碼很可能覆蓋這些變量值。

  2. 在和硬件發生交互的時候,切記執行類似disinterrupt()之類的操作,就是關閉硬件中斷。完成交互記得打開中斷,在有些系列上,這叫做"進入/退出核心"。

  3. 不能調用其它任何不可重入的函數。

  4. 謹慎使用堆棧。最好先在使用前先OS_ENTER_KERNAL。

堆棧操作涉及內存分配,稍不留神就會造成益出導致覆蓋其他任務的數據,所以,請謹慎使用堆棧!最好別用!很多黑客程序就利用了這一點以便系統執行非法代碼從而輕鬆獲得系統控制權。還有一些規則,總之,時刻記住一句話:保證中斷是安全的!
參考網址

SIGCHLD

最後我們來說一個信號,是SIGCHLD信號,這個信號是我們子進程終止的時候會給父進程傳送這個信號。
SIGCHLD信號產生的條件:
1.子進程終止時
2.子進程收到SIGSTOP信號停止的時候。
3.子進程處在停止狀態,接受到SIGCONT後喚醒。

父進程接收到了SIGCHLD信號,這個時候的默認動作是忽略,當然你可以去進行信號捕捉。我們能通過信號捕捉可以去處理其他。

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