進程信號(信號產生、註冊、註銷、處理),信號阻塞和volatile關鍵字

進程信號

信號:信號是一個軟件中斷;
作用:操作系統通過信號告訴進程發生了某個事件,打斷進程當前操作,去處理這個事情
操作系統中的信號:通過 kill -l 命令可以查看系統中的信號種類(62種)

1~31好信號:從Uinux借而來,每個信號都有具體對應的系統事件;(非可靠信號,有可能丟失信號);
34~64號信號:後期補充的,因爲沒有具體對應的事件,因此命名比較草率;(可靠信號,不會丟失信號);

信號的生命週期:1、產生;2、在進程中註冊;3、在進程中註銷;4、信號處理;

信號產生

硬件:ctrl+c / ctrl+z /ctrl + |
軟件:kill -signum pid命令 kill默認發送15號進程 /kill(int pid, int signum) / raise(int signum) / abort( ) /alarm(int seconds)
kill殺死一個進程的原理:向進程發送一個信號,信號有對應的事件,進程放下手頭工作去處理這個事件,然而事件的處理結果就是讓進程退出。fg的功能是將一個暫停的後臺程序調到前臺運行

  • kill(進程id,信號值)
  • kill(getpid(),SIGHUP); // 給指定進程發送指定的信號
  • raise(SIGTERM); //給進程自己發送指定的信號
  • abort( ); //給自己發送SIGABRT信號通常用於異常通知
  • alarm(3); //3秒之後給進程自己發送SIGALRM信號(相當於定時器)

信號在進程中註冊

如何讓進程知道自己收集了某個信號;pcb -> struct sigpending -> struct sigset_t
sigset_t這個結構體中只有一個數據成員;這個數組用於實現一個位圖 - - - 稱之爲未決信號集合(收到了但是沒有被處理的信號集合);
給一個進程發送一個信號,就會將這個位圖中對應位置爲1,表示進程當前收到了這個信號;
信號的註冊其實不僅會修改位圖,還會爲信號組織一個sigqueue節點添加到pcb的sigqueue鏈表中;

  • 1~31號非可靠信號註冊:若信號註冊的時候位圖爲0,則互創建一個sigqueue節點並修改位圖爲1,但是若位圖爲1,則什麼也不做;
  • 34~64號可靠信號的註冊:不管位圖當前是否爲0,都會創建一個節點,添加到鏈表中,並修改位圖;

信號在進程的註銷

爲了保證一個信號只會被處理一次,因此先註銷再處理;在pcb中刪除當前信號信息;將pending位圖置爲0;刪除信號節點;
非可靠信號註銷:因爲非可靠信號只會有一個節點,因此刪除節點後,位圖直接置爲0;
可靠信號註銷:因爲可靠信號有可能註冊多次,有多個節點,因此刪除節點後,需要判斷是否還有相同節點,若沒有才會將位圖置爲0;

爲什麼要註銷?(先註銷後處理)
答:因爲有這個信號需要處理,並且取出了這個信號的信息,因此註銷之後就立即去處理;這樣保證一個信號只會被處理一次。

信號的處理

信號表示一個事件的到來,處理事件就是完成功能,(在C語言中完成一個功能的最小模塊爲- - -函數)。
其實每一個信號都對應有自己的事件處理函數,信號到來,去處理這個事件就是去執行這個處理函數;執行完畢事件就處理完了。

信號的處理方式:

  1. 默認處理方式:操作系統中原定義好的每個信號的處理方式;
  2. 忽略處理方式:處理方式就是忽略,什麼也不做;
  3. 自定義處理方式:自己定義一個回調函數,使用這個函數替換內核中默認的處理函數;信號到來就會調用我們定義的函數了。
    在這裏插入圖片描述
    typedef void(*sighandler_t)(int signo); - - - - 定義了一個名稱爲sighandler_t的函數指針類型。
    sighandler_t signal(int signum,sighandler_t handler);
    handler :SIG_DFL- - - 默認處理方式 / SIG_IGN - - - 忽略處理方式 / 用戶自己定義的一個沒有返回值,有個int型參數的函數地址。

自定義信號的捕捉流程

1、主控流程因爲中斷/異常/系統調用切換到內核態運行;
2、在返回用戶態之前處理信號;
3、返回用戶態運行自定義回調函數;
4、返回內核態;
5、返回用戶態主控流程;

在這裏插入圖片描述
默認處理方式調用的函數與忽略處理方式調用的函數都是系統中已經實現的 - - -內核中直接處理。
自定義信號處理方式- - - -用戶自己寫一個事件處理函數;

信號阻塞

並不是不接收信號。信號依然可以註冊,只是標識哪些信號暫時不處理。
在pcb中有一個位圖,位圖叫block位圖 - - - 阻塞信號集合,這個集合中的信號如果來了(添加到pending位圖中)則暫時不處理。
在這裏插入圖片描述

如何阻塞一個信號?

所有信號中,有兩個信號比較特殊:SIGKILL -9 / SIGSTOP -19,這兩個信號不可被阻塞,不可被忽略,不可被自定義。

int sigprocmask(int how, sigset_t *set, sigset_t *old);

how

SIG_BLOCK - - -將set集合中的信號添加到內核中的block阻塞信號集合中,使用old保存原來的阻塞信息以便於還原- - -(阻塞set集合中的信號);
SIG_UNBLOCK - - - 將set集合中的信號從內核中的block阻塞信號集合中移除- - - (對set集合中的信號解除阻塞);
SIG_SETMASK - - - 將內核中的block阻塞信號集合內容設置爲set集合中的信息 - - -(阻塞set集合中的信號);
SIG_BLOCK - - -set | block;
SIG_UNBLOCK - - - set & block;
SIG_SETMASK - - - block = set;

0.將一些信號的處理函數自定義;1.將所有的信號都給阻塞;2.在解除阻塞之前,給進程發送信號;3.解除阻塞,查看信號的處理情況。

int sigemptyset(sigset_t *set); //清空set信號集合 - - - 使用一個變量時的初始化過程
int sigaddset(sigset_t *set, int signum); //向set集合中添加指定的信號
int sigdelset(sigset_t *set,int signum); //從set集合中移除指定的信號
int sigismember(const sigset_t *set,int signum); //判斷指定信號是否在set集合中
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void sigcb(int signo)
{
    printf("recv a signal:%d\n", signo);
}
int main()
{
    signal(SIGINT, sigcb);
    signal(SIGRTMIN+4, sigcb);

    sigset_t set;
    sigemptyset(&set);//清空集合,防止未知數據造成影響
    sigfillset(&set);//向集合中添加所有信號

    sigprocmask(SIG_BLOCK, &set, NULL);//阻塞set集合中的所有信號

    printf("press enter coninue\n");
    getchar();//等待一個回車,如果不按回車就一直卡在這裏

    sigprocmask(SIG_UNBLOCK, &set, NULL);//解除set集合中的信號阻塞

    while(1) {
        sleep(1);
    }
    return 0;
}

殭屍進程:子進程退出後會向父進程發送SIGCHLD信號通知父進程,子進程的狀態改變;但是因爲SIGCHLD信號默認 處理方式是忽略;因此之前的程序中若不進行進程等待則不通知子進程退出。
如果進行進程等待,而且不想讓父進程阻塞,就可以自定義SIGCHLD信號的處理方式;
在自定義回調函數中調用waipid,處理殭屍進程,父進程就不用一直等待;
但是SIGCHLD信號是一個非可靠信號,如果有多個子進程同時退出,有可能造成信號丟失。

while(waitpid(-1,NULL,WNOHANG)>0);//非阻塞循環在一個回調中將所有的殭屍進程全部處理
waitpid(int pid,int *status, int options)
options:WNOHANG- - -將waipid設置爲非阻塞,沒有子進程退出則立即報錯返回;(0 - - - 默認阻塞等待子進程退出);
返回值:>0(退出的子進程pid);==0(有子進程但是沒有退出); <0 (出錯了,比如當前沒有子進程);

關鍵字volatile

用於修飾一個變量,保持變量的內存可見性(cpu在處理的時候每次都重新從內存獲取數據),防止編譯器過度優化。
cpu處理一個數據的過程時從內存中將數據加載到寄存器上進行處理;
gcc編譯器在編譯的程序的時候,如果使用了代碼優化 -Olevel選項,發現某個變量使用頻率非常高,爲了提高效率,則直接將變量值設置爲某個寄存器的值,以後訪問的時候直接從寄存器訪問,則減少了內存訪問的過程,提高效率。(但是這種優化有時候會造成代碼的邏輯混亂)。
因此使用volatile關鍵字修飾變量,讓cpu無論如何每次都重新到內存中獲取數據。

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

volatile long long  a = 1;

void sigcb(int no)
{
    a=0;
    printf("a=%d\n", a);
}
int main()
{
    signal(SIGINT, sigcb);
    while(a) {
    }
    printf("exited a=%d\n", a);
    return 0;
}

函數的可重入和不可重入

函數的重入:在多個執行流程中,同時進入一個函數運行。
函數可重入:函數重入之後,不會造成數據二義或者邏輯混亂;
函數不可重入:函數重入之後,有可能會造成數據二義或者邏輯混亂。
在這裏插入圖片描述

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

int a = 1, b = 1;

int test() {
    a++;
    sleep(3);
    b++;
    return a+b;
}

void sigcb(int no) {
    printf("signal sum:%d\n", test());
}
int main()
{
    signal(SIGINT, sigcb);
    printf("main sum:%d\n", test());
    return 0;
}

函數是否可重入的判斷基準:這個函數中是否對全局變量進行了非原子的操作,若有則不可重入。
操作的原子性:操作一次完成,中間不會被打斷;
原子操作:操作要麼一次完成,要麼就不操作;
一個函數如果根本沒有操作全局數據,則肯定是可重入的,因爲每個函數調用的時候都有獨立的函數棧。
一個函數若對全局數據進行操作,但是操作是原子性的,則也是可重入的。

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