linux學習——信號

信號

標籤(空格分隔): 未分類


今天我們來說一說信號,linux當中有一個頭文件signal.h其中提供了62個信號。信號是用於向一個進程來通知發生一部時間的機制。信號類似於一個硬件終端,但是信號沒有優先級,操作系統看待信號都是平等的。對一個進程,一次只能給一個信號。

所以信號我們也叫做軟中斷信號,通知進程發生異步時間,使用信號我們可以通過

上述的信號,1-31我們叫做普通信號,34-64我們叫做實時信號,實時信號主要用於實時系統。

產生信號的條件:


  1. 用戶在終端按下鍵盤的某些鍵的時候。例如:Ctrl+C終止進程。Ctrl+\ 停止進程,引發進程停止並且產生信息轉儲。Ctrl+Z停止進程執行,只是暫停執行,不能被阻塞,處理或忽略
  2. 硬件產生信號,例如:除數爲0,無效的內存引用,通常是由硬件檢測到傳給內核,然後內核通知進程。
  3. 通過系統調用kill將信號傳送給另外的進程。

  4. 可以在終端下使用kill命令將信號發送給其他進程。

  5. 一些軟件條件發生的時候,也可能產生信號,例如:在網絡連接上傳來外來數據的時候產生SIGURG。

信號的處理方式


對於進程來說,不能判別是否出現一個信號,而是必須要告訴內核信號出現的時候,執行下列操作。
信號的處理方式有三種:
1. 忽略此信號
2. 執行信號的默認處理動作。
3. 提供自定義行爲,要求處理該信號的時候切換到用戶態執行這個處理函數,也叫做捕捉一信號。

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

產生信號


1. 終端產生信號
首先提出一個概念叫做 core dump,我想在linux下寫c,肯定不少發現錯誤的時候報這個錯誤接下來我們先來看看這個東西到底是個什麼。

core dump叫做核心轉儲,也叫做核心文件(core file),是操作系統在進程收到某些信號而終止運行時,將此時進程的地址空間的內容以及有關進程狀態的其他信息寫出的一個磁盤文件,這個信息我們常常用於調試程序。

默認的linux系統當中是不生成這個文件的,我們可以使用ulimit -a查看系統中這個文件的大小。
![enter description here][1]
我們可以使用命令ulimit -c xxxx設置生成的core dump的大小。
![enter description here][2]
默認情況下,生成的core dump文件的格式是core.xxx,後面一般都是pid。並且生成在當前目錄下。

現在我們模擬生成一下這樣的core dump文件,我們首先寫出一個死循環。

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

    return 0;
}

我們運行這個程序,然後操作,Ctrl+\,這樣就會出現:
![enter description here][3]
從上圖我們可以看到我們操作過程中從鍵盤Ctrl+,這樣就會產生一個信號SIGQUIT,這個信號傳遞給運行的進程,然後進程得到這個信號引發終止進程並引發核心轉儲。

接下來我們來看看如何利用這個coredump文件進行調試
我們直接gdb test文件和core文件就好8,在終端輸入gdb test core.6437得到:
![enter description here][4]

是不是很快就定位到了錯誤之處!

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信號。

阻塞信號


1.關於阻塞概念
阻塞信號我們首先提出一些概念,

信號遞達:正在執行信號處理的動作,

信號產生與信號遞達之間叫做信號未決,也叫做pending。

當信號阻塞的時候不會遞達,接觸阻塞,信號才能遞達。

關於信號,我們首先需要從內核的角度來看看信號。
在內核當中,當一個進程接收到信號,會對應的在進程的PCB當中有三個相關的結構,

![enter description here][5]

因爲我們現在有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指向修改前你的信號屏蔽字。
how參數:

參數 含義
SIG_BLOCK set包含了我們希望添加到當前信號屏蔽字的信號
SIG_UNBLOCK set包含了我們希望從當前信號屏蔽字中移除阻塞的信號
SIG_SETMASK 設置當前信號屏蔽字爲set所指向的值

注意:如果調用了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;
}

我們可以從圖片當中看到當我們按下Ctrl+c產生SIGINT信號的時候,這個時候就會在未決表改了對應的比特位。SIGINT信號是2號信號,修改了下標爲2的位置的比特位。

捕捉信號


接下來我們來着重講一講關於信號捕捉的問題。

這裏先來提出一個函數就叫做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);
};
參數 含義
sa_handler 早期的捕捉函數
sa_sigaction 新添加的捕捉函數,通過sa_flags選擇哪種捕捉函數。
sa_mask 在執行捕捉函數時,設置阻塞其它信號,sa_mask,進程阻塞信號集,退出捕捉函數後,還原回原有的阻塞信號集
sa_flags SA_SIGINFO 或者 0
sa_restorer 保留,已過時

我們也可以使用signal函數可以實現這個功能。

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

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

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

接下來說一說信號捕捉的時候的狀態轉換:
![enter description here][6]

從上面這張圖就可以看出整個狀態的轉換,

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

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

可重入函數


有了信號以後,會去調用喜好處理函數,這個時候你的程序就是異步執行,這個時候就引入了一個問題就是可重入函數的問題,

對於一個函數,當多個執行流進入函數,運行期間會出現問題的就叫做不可重入函數,不會出現的問題就是可重入函數。

信號捕捉函數內部禁止調入不可重入函數。

另外可重入函數還會和線程安全有聯繫:
1.線程安全不一定是可重入的,可重入的一定是線程安全的。
2.對全局變量或者公共資源進行多線程的進行訪問的時候,則這個就既不是線程安全的也不是可重入。
3.如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個重入函數若鎖還未釋放則會產生死鎖,因此是不可重入的。

四類不可重入函數u:
第一類:不保護共享變量的函數
第二類:保持跨越多個調用的狀態函數
第三類:返回指向靜態變量指針的函數。
第四類:調用線程不安全的函數。

可重入函數是線程安全函數的一種,特點在於它們被多個線程調用的時候,不會引用任何共享數據。

對於不可重入函數的處理,我們通常採用的方法就是重寫函數。

另外就是有些以_r結尾的函數就是那個函數的可重入版本。

競態條件


我們先來介紹一個pause函數。

 int pause(void);

關於pause函數,是用來使得調用進程掛起直到有信號遞達。如果信號的處理動作是終止進程,則進程終止,pause函數不返回,如果處理動作是忽略,pause函數也不返回。如果處理動作是信號捕捉,則調用捕捉函數,然後返回-1。

然後這裏我們使用alarm和pause模擬實現一個sleep函數。

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

void sig_alarm(int signo)
{

}
void mysleep(int seconds)
{
    struct sigaction set,oset;
    set.sa_handler=sig_alarm;
    sigemptyset(&set.sa_mask);
    set.sa_flags=0;
    sigaction(SIGALRM,&set,&oset);
    //設置鬧鐘
    alarm(seconds);
    //這裏鬧鐘到時間發送信號SIGALRM,然後執行信號處理函數,然後pause返回錯誤碼-1,
    pause();
    unsigned int unslept=alarm(0);
    sigaction(SIGALRM,&oset,NULL);
}
int main()
{
    while(1)
    {
        mysleep(2);
        printf("2 seconds success\n");
    }
    return 0;
}

我們這個函數mysleep模擬了sleep函數。但是,我們需要思考一個問題就是在這裏存在一個時序竟態的問題,當我們執行完alarm之後,別的進程會競爭奪走了CPU,奪走n秒後,SIGALRM遞達了,然後n秒過後,這個時候就去執行pause,這樣沒有了信號,這樣最終就是一直掛起。

所以我們要讓alarm和pause的操作是原子的纔行。

linux在這裏給出了一個函數sigsuspend函數。

 int sigsuspend(const sigset_t *mask);

1.通過mask來臨時解除對某個信號的屏蔽
2.掛起等待
3.然後當sigsuspend返回的時候,這個時候恢復爲原來的值

所以我們應該對這一段代碼這樣操作纔行

    //首先屏蔽SIGALRM信號,不讓它遞達
    alarm(seconds);
    //解除屏蔽字,SIGALRM遞達,
    pause();

所以我們先阻塞信號,保存當前信屏蔽字,然後直到最後進程回到我當前進程,然後我解除SIGALRM信號的屏蔽,這樣信號就會遞達這樣就確保了alarm和pause之間的操作都是原子的。

而對於sigsuspend函數來說:sigsuspend用於在接收到某個信號之前,臨時用mask替換進程的信號掩碼,並暫停進程執行,直到收到信號爲止。

SIGCHLD


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

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

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