程序訊號

有志者事竟成1019發表於2024-07-07

1. 訊號的產生

1.1 訊號概念

在生活中有很多的訊號在我們身邊圍繞,例如紅綠燈,發令槍,上課鈴等等

在接受到訊號,我們可以做出三種動作 1.立馬去做對應訊號的事情 2.等一會再做,有自己的事情或者更重要的事情 3.直接忽視,不做

訊號是給程序傳送的 eg: kill -9 pid

程序本身是程式設計師編寫的屬性和邏輯的集合:所以程序有識別訊號,認知到訊號的功能

對於訊號,程序必須要有儲存訊號的能力,就好比人記在腦子裡一樣

​ 由此可以引出對於訊號,可以做出三種反應:

1.預設動作 2.忽略動作 3.自定義動作

  • 程序收到的訊號儲存在哪裡呢 ?

如下圖,可以發現訊號有 1-31號(普通訊號) ,34-64號(實時訊號)

對於程序,收到一個訊號時候,只有兩種可能 : 是否收到訊號,所以可以用0和1來表示是否收到訊號

仔細觀察則會發現訊號有64個,那麼我們可以用點陣圖的形式用一個儲存

程序收到的訊號儲存在pcb的一個變數裡,用位元位來表示第幾個訊號

struct task_struct{
	......
	unsigned int singal;
	.......
}

給程序傳送訊號一定要經過系統呼叫,所以kill命令等都是OS提供的介面。

1.2 產生訊號的方式

    1. 透過鍵盤傳送

      在執行程序的時候,終止程序一般用Ctrl+c來終止當前程序,其實這個熱鍵是OS進行對這個操作進行識別,然後再對對應的程序傳送2號訊號(SIGINT),從而終止當前程序

    1. 系統呼叫向目標程序傳送訊號

    int kill(pid_t pid, int sig) ;

    標頭檔案#include <sys/types.h> #include <signal.h>

    功能: 對目標程序pid,傳送sig號訊號

    為此,我們可以實現一個自己的kill命令,eg:

    #include<iostream>
    #include<cstdio>
    #include<string>
    #include<unistd.h>
    #include<sys/types.h>
    #include<signal.h>
    void UsPage(const std::string& pro){
        //使用者手冊
        std::cout<<"\nplease input: "<<pro<<" pid signal"<<std::endl;
    }
    
    // 輸入格式為  ./mysignal  pid  signal
    int main(int argc,char* argv[]){
    
        if(argc!=3){
            UsPage(argv[0]);
            exit(0);
        }
        int id=atoi(argv[1]);       //id為要殺死的程序的pid即為傳的命令列的第二個引數
        int sign=atoi(argv[2]);     //對程序傳送的訊號sign
        int res=kill(id,sign);
        if(res==-1){
           perror("kill: ");//列印錯誤資訊
           exit(-1);
        }
        return 0;
    }
    
    1. 硬體產生訊號

(1) 除0操作

當在程式碼中有除0操作的時侯,底層實際上是硬體計算溢位,CPU會異常,此時會給程序傳送異常的訊號,傳送

發生除0作業系統會拋以下異常:Floating point exception

man 7 signal 檢視訊號時候可以發現為 : SIGFPE 8 Core Floating point exception,為8號訊號

(2) 野指標異常

如果發生對於一個無效指標的訪問,實際是程序地址空間發生對野指標的訪問異常,程序地址空間是由頁表對映到實體記憶體的,其中包括了一個MMU(記憶體管理單元),如果異常,那麼會給程序傳送訊號

野指標等越界錯誤會發生 Segmentation fault

man 7 signal 檢視:SIGSEGV 11 Core Invalid memory reference

..........例子不止這兩個,所以硬體也可以導致訊號的產生

    1. 軟體產生訊號

**(1)管道 **

在之前的管道學習中,假如有一個管道 : 關閉管道的讀端,但是寫端沒有關,那麼此時也會產生訊號 ,因為作業系統不允許任何的資源浪費,所以這種關閉讀,寫不關就會造成訊號的產生,從而終止程式(大部分訊號都會終止)

(2) 鬧鐘

int alarm(unsigned int num) ;//num秒後關閉程序

其實這個鬧鐘底層就是在num秒後給程序傳送一個14號訊號,然後num秒後終止

SIGALRM 14 Term Timer signal from alarm(2)

..........例子不止這兩個

1.3 signal ()函式

​ signal()函式:

sighandler_t signal(int signum, sighandler_t handler);

​ 其中第二個引數是一個函式指標型別,型別為 (*func)(int)

typedef void (*func)(int);  //其中這個函式的引數為int型

​ 設定處理訊號的功能

​ 指定使用sig指定的訊號編號處理訊號的方法。 引數func指定程式可以處理訊號的三種方式之一:

​ 這個就是自定義動作,我們自己定義改程序在遇到signnum的訊號的時候做什麼操作

eg:

void catchSign(int signal){
	std::cout<<"catach signal:"<<signal<<std::endl;
	sleep(1);
}
int main(){
    signal(11,catchSign);  
    int arr[100];
    arr[10000000]=666;
    return 0;
}

signal只是宣告,本來遇到第num號訊號應該是按系統指定的操作

signal函式之後就把遇到該訊號的時候,做出自己定義的操作了,即上邊的catchSign函式

所以當遇到改變操作的函式的訊號的時候,才會觸發自定義操作,並不是宣告signal就會觸發,如果沒有遇到該訊號甚至不會觸發自己的自定義操作。

1.4 核心轉儲問題

以上 31個普通訊號,CoreTerm型別的都會造成程序終止,但是不同的是,Core會產生核心轉儲

在雲伺服器上如果程序是Core退出的,沒有明顯的現象可以觀察到,這是因為雲伺服器關閉了Core file的檔案大小

如果要更改有效檔案大小可以使用命令:ulimit -c 1024,1024是設定的檔案大小,可以設定為其他,-c是corefile 其他的也可以用後邊那個-字母設定

將core file設定大小後,再次遇到造成core退出的程序,會在程序目錄下產生一個名為core.xxx的檔案

用gdb下輸入: core-file core.xxx會出現以下內容 要生成debug的可執行程式才可以

則gdb會顯示該程序異常的原因,該原因是野指標問題

2. 訊號的儲存

2.1 阻塞訊號

首先要理解訊號的儲存要引入以下概念:

  1. 實際執行訊號的處理過程稱為遞達
  2. 訊號從產生到遞達之間的狀態稱為未決
  3. 訊號可以被阻塞
  4. 被阻塞的訊號將處於未決狀態,直到解除阻塞,才會執行遞達的動作

所以訊號是可以被阻塞的 , 也可以說某個訊號可以被遮蔽

其實在pcb中訊號採用點陣圖的思想儲存,這些所有的訊號被稱為訊號集

pcb中訊號的儲存分為三個部分:

  1. 未決訊號集
  2. 訊號遮蔽集
  3. 訊號集操作函式表

  • pending : 表示到達pcb中的未決訊號
  • bolck : 表示當前程序遮蔽的訊號
  • handler :其實就是一個函式指標陣列,該陣列是訊號的處理方式,所以假設使用者自定義了訊號的處理方式,那麼這個函式表就會把自定義函式的指標替換到函式表的特定位置

當遮蔽訊號存在時,這個訊號將不會被遞達

pending : 00000000000000000000000

block : 00000000000000000000010 不會被遞達

當遮蔽訊號存在時,未決訊號也存在,這個訊號也不會被遞達

pending : 00000000000000000000010

block : 00000000000000000000010 不會被遞達

只有當未決訊號存在,並且沒有被遮蔽,才能被遞達

pending : 00000000000000000000010

block : 00000000000000000000000 遞達

2.2 訊號的捕捉

2.2.1 使用者態和核心態

首先要理解訊號的捕捉流程需要理解以下兩個內容:

使用者態和核心態

在作業系統中,記憶體以及硬體等資源是由作業系統所維護的,正所謂作業系統不相信任何人,當我們進行申請系統資源的時候,其實程序會切換到核心態去進行一系列操作,從而申請資源

實際執行系統呼叫的人是”程序“,但是身份是核心 , 系統呼叫往往比較費時間所以儘量少的進行系統呼叫

那麼切換狀態的過程是什麼呢?

  1. 在之前的程序地址空間中,瞭解到了使用者有0-3G的地址空間,程序地址空間透過頁表對映到實體記憶體中,從而實現cpu與程序互動

  2. 作業系統在開機時候也會載入到記憶體中,管理著整個電腦,整個問題毋庸置疑,所以作業系統也在記憶體中

  3. 對於每個程序,剩餘的3-4G地址空間,其實就是核心態的程序地址空間,稱為核心空間,整個程序地址空間對映到實體記憶體中的作業系統的實體記憶體區。

  4. 由於每個程序固定的3-4G都是核心空間,而且作業系統只有一個,所以對於每個程序都會透過核心級頁表對映到一個作業系統的實體記憶體處,核心級頁表一個就夠了。

  5. 當程序透過系統呼叫時候,系統呼叫會發出一個陷入指令,讓cpu的CR3暫存器(該暫存器記錄當前程序的執行級別0代表核心態,3代表使用者態)標記為核心態,從而變為核心態,所以在使用者態進行系統呼叫的時候,切換身份並且跳轉在核心空間中執行有關操作,執行完後會傳送指令再將CR3暫存器標記為使用者態

2.2.2 訊號的捕捉流程

訊號的捕捉處理是在核心態返回使用者態的時候進行的

首先我一定是進入了核心態,進入核心態的原因有很多 : 中斷,系統呼叫,程序切換等。

當要返回到使用者態時,既然進入核心態那麼不容易,所以索性去檢查一下程序是否收到了訊號,因為是在核心態,所以pcb訪問輕輕鬆鬆,然後檢查是否有需要遞達的訊號

對於訊號的處理有三種方式 : 1.預設動作 2.忽略動作 3.自定義動作

對於不同的處理方式也會有不同的流程

  • 預設動作和忽略動作

  • 自定義動作,假設這個自定義動作沒有使用程序退出,假設是一個自動修補漏洞並且繼續執行的處理

3. 訊號集操作函式

pending和bolck都是由一個訊號集組成的,也就是點陣圖的思想型別為sigset_t

  typedef struct {
  	unsigned long sig[_NSIG_WORDS];
  } sigset_t;

所以定義一個訊號集,可以 sigset block;

下邊介紹對訊號集的有關函式:

#include <signal.h>
 int sigemptyset(sigset_t *set);
//初始化訊號集,全置為0
 int sigfillset(sigset_t *set);
//初始化訊號集,使所有的訊號都有,即為全1
 int sigaddset (sigset_t *set, int signo);
//將訊號集中新增signo號訊號,即將set訊號集中signo訊號的位置置為1
 int sigdelset(sigset_t *set, int signo);
//將set訊號集中的signo號訊號由1置為0,即去掉
 int sigismember(const sigset_t *set, int signo);
//判斷signo訊號是否在set中出現,如果沒有返回0,有的話返回1

sigprocmask函式 :

呼叫sigpromask函式可以讀取或者更改當前程序中遮蔽訊號集

#include<signal.h>
int sigprocmask(int how , const sigset_t *set , sigset_t *oset);

引數:

  • how : 當前想進行的操作型別,1. SIG_BLOCK 2. SIG_UNBLOCK 3. SIG_SETMASK

​ SIG_BLOCK : 希望新增的遮蔽字的訊號,即 mask = mask|set

​ SIG_UNBLOCK : 希望解除遮蔽字的訊號,即可mask = mask | ~set

​ SIG_SETMASK : 將當前程序的遮蔽字設定為set所指向的值

  • set: 輸入型引數,根據how傳給函式的訊號集
  • oset : 輸出型引數 ,將沒有被更改之前的訊號集儲存到oset所指向的訊號集

sigpending函式:讀取當前程序的未決訊號集,透過set引數傳出

#include <signal.h>
int sigpending(sigset_t *set);
//讀取當前程序的未決訊號集,透過set引數傳出。呼叫成功則返回0,出錯則返回-1。 

下面用剛學的幾個函式做個實驗。程式如下:

#include<iostream>
#include<vector>
#include<string>
#include<unistd.h>
#include<signal.h>

using namespace std;

vector<int> Blocks={2};//想遮蔽多少,假如陣列中即可

string show_pending(sigset_t& pending){
    string s;
    int cnt=1;
    for(int i=31;i>=1;i--){
        //列印31個訊號集
        if(sigismember(&pending,i)){
            //sigismember比較傳參的訊號集是否有第i個訊號,有返回1,無返回0
            s+='1';
        }
        else s+='0';
        if(cnt++%4==0) s+=' ';
    }
    
    return s;
}


void hander(int sign){
    //自定義捕捉函式
    cout<<"捕捉到訊號"<<sign<<endl;
}

int main(){
    //1.初始化訊號集
    sigset_t block,oblock,pending;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    //2.阻塞訊號
    for(const auto& e:Blocks) sigaddset(&block,e);
    //3.設定遮蔽的訊號
    sigprocmask(SIG_SETMASK,&block,&oblock);
    int num=1;

    for(const auto&e:Blocks) signal(e,hander);

    while(true){
        //獲取當前程序的pending表
        sigpending(&pending);
        //列印當前程序表
        cout<<show_pending(pending)<<endl;
        sleep(1);

        if(num++==5){
            //假設5秒後取消阻塞
            sigprocmask(SIG_SETMASK,&oblock,&block);
            //oblock就是儲存的原來的,代表取消遮蔽,為了安全,將原來的block備份一下
        }
    }
    return 0;
}

相關文章