Linux之信號第一談


 

信號的基本概念


    在Linux環境下,有一重要概念信號(signal),說它重要是因爲它在進程管理中佔有着相當重要的低位。整個進程之間的管理切換,都是通過信號的方式實現的。首先舉個簡單例子。

    生活中,當我們過馬路時,總是可以看到交通燈,從小耳熟能詳的一局童謠“紅燈停,綠燈行,黃燈亮了等一等”,遇到紅燈,我們都會下意識地等待,綠燈亮起,我們會選擇穿過馬路,這就是一種信號。暫且不討論闖紅燈的行爲i_f18.gif。考慮一下,爲什麼我們會這麼做?紅綠燈給我們的只有顏色上的信號,我們卻做出了一系列的反應,並不是因爲紅燈有多麼大的能力,而是在我們腦海中有這樣的一種意識,看到它之後我們就理所當然的要這樣。

    信號也是這樣,在Linux操作系統中,進程的控制大都需要信號來實現,並不是信號指使進程進行某一項動作,而是進程在信號來臨之前就已經知道了,如果遇到某個信號該怎麼做。有了這個概念之後,我們從操作系統的角度來看看,信號是如何工作的。

    首先要得到一個信號,這由終端的命令或程序代碼可以實現,不是我們討論的重點,這些命令或函數被執行之後,根據馮諾依曼體系結構,首先輸入的信息要傳遞給內存,這時CPU從原來的用戶態切換爲內核態,分析解釋該信息,得到信號,接下來這些信號會保存在該進程的PCB當中(注意,這個很重要!!!)。等待CPU從內核態轉換爲用戶態,重新處理該進程時,會首先處理PCB中記錄的信號,這個和線程切換有點類似,發現待處理的信號,接下來就會去執行它,從而使進程產生相應的行爲。

    瞭解了這些東西之後,我們需要從宏觀角度來認識一下信號,信號是一種通知機制,告訴進程某些事情發生了,進程針對信號產生特定的行爲。(進程對這些信號會產生的默認行爲是已知的)同時信號的產生是異步產生的,完全隨機,可能在進程運行過程中的任何時間  

    之前談進程的時候,說過kill命令,這個後面會用到

kill -l        # 查看當前系統所有可用信號

wKioL1itjK2wdTXwAAEgCRDxTSg187.png    


信號產生的條件

     1、終端組合鍵,只能產生少量信號,僅適用於前臺進程

     2、硬件異常,硬件檢測並通知內核

     3、軟件方式,指令或函數接口。kill命令(例鬧鐘超時SIGALRM信號)

    關於條件2,要多說一點的是,硬件異常產生的信號,由操作系統解釋,例如除0操作產生SIGFPE,訪問非法內存地址;還有就是MMU內存管理單元異常。MMU是用來結合頁表進行虛擬地址到物理地址轉化,與MMU搭配使用的還有TLB, 做後備緩存,用來緩存映射之後的硬件緩存結果。


    那當進程得到信號之後會怎麼處理呢?還是當我們遇到交通燈一樣,遇到紅燈,每個人都知道要停下來,默認的動作應該是在原地等待,但依舊會有些人闖紅燈,同時,有些人沒有老老實實地在等,而是在打電話,進程也一樣,有三種處理方式:

信號處理方式:

     1、忽略信號

     2、執行信號默認動作

     3、自定義動作, 提供一個信號處理函數,也叫作信號捕捉(catch)【使用signal函數,後面會提到】  

    關於如何去實現,後面會提到。


信號的產生

    

    在Linux下信號的產生主要有三種方式。

    1、終端通過按鍵組合鍵產生

        常見的組合鍵有以下幾個:

ctrl+c:SIGINT(2)        終止前臺進程
ctrl+z:SIGSTOP(19)     停止前臺進程,同時將該進程放到後臺
ctrl+\:SIGQUIT(3)       終止進程並Core Dump

    這裏多說一點關於core dump的東西。Core Dump, 即核心轉儲,當一個進程被異常終止時,可以選擇性的將用戶空間的內存數據全部保存到磁盤上,默認在當前路徑下生成一個文件名爲 core.****的文件,****通常爲進程ID,在運行結束之後,可以使用gdb進行調試,找到異常所在。默認情況下是不會產生core文件的,原因有兩個,一是容易將用戶的私人信息也保存到了磁盤上,造成信息的不安全,二就是如果在用戶不知情的情況下,產生core文件,會佔用磁盤大量空間,因此即使是在實際開發中,core dump的使用也很少見。接下來給出兩天命令,關於查看和調整core文件的屬性

# 查看系統資源上限:
[root@localhost mySemaphore]# ulimit -a
# 更改core文件大小上限:
[root@localhost mySemaphore]# ulimit -c 1024     # 以block爲基本單位
     使用gdb調試,-g編譯選項
         (gdb) core-file core文件

     使用ulimit命令改變了shell的ResourceLimit,而當前進程的PCB有shell複製而來,所以也就具有了和shell相同的Resource Limit值,從而產生了core dump。

core測試代碼:

//test.c
#include <stdio.h>
#include <unistd.h>
int main()
{
    int count = 0;
    while(1)
    {
        printf("hello world\n");
        if(count == 5){
            int c = 3/0;
        }
        count++;
        sleep(1);
    }
    return 0;
}

wKioL1iuLc3wtuE4AALAB8J60Hw291.png


2、 調用系統函數想進程發送信號

    這裏我們會提到三個函數 kill,raise, bort

#include <sys/types.h>
#include <signal.h>
       int kill(pid_t pid, int sig);
               # 給一個指定的進程發送指定信號(成功返回0, 失敗返回-1)
              
#include <signal.h>
     int raise(int sig);
             # 給自己發送指定信號,用父進程wait驗證(成功返回0, 失敗返回-1)
             
#include <stdlib.h>
     void bort(void);
               # 使當前進程異常終止,abort函數總是成功的,所以沒有返回值

關於kill 函數中的進程ID,和信號,我們可以通過命令行參數的方式獲得,代碼測試如下:


kill測試代碼:

// 終端1
// mysignal.c
#include <stdio.h>
#include <sys/types.h>
int main(int argc, char* argv[])
{
    if(argc != 3){
        printf("Isn't form: ./file pid signo \n");
        return 1;
    }
    else{
        int pid = atoi(argv[1]);
        int sig = atoi(argv[2]);
        kill(pid, sig);
    }
    return 0;
}
//終端2
// test.c
#include <stdio.h>
int main()
{
    while(1);
    return 0;
}
// 終端3
[muhui@bogon ~]$ ps aux | grep test

// 執行順序
先執行test,終端3使用命令查看test進程id
然後執行mysignal,
     [muhui@bogon mysignal]$ ./mysignal 3773 9
觀察終端2的顯示結果,並再次運行終端3的執行


raise測試代碼:

// mysignal.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
    int count = 0;
    while(1){
        printf("hello world\n");
        if(count == 5)
            raise(9);
        sleep(1);
        count++;
    }
    return 0;
}

wKiom1iuL8Hj-BVQAAAkEYXnyxM944.png

abort測試代碼:

// mysignal.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
    int count = 0;
    while(1){
        printf("hello world\n");
        if(count == 5)
            abort();
        sleep(1);
        count++;
    }
    return 0;
}

wKioL1iuMA_C3T_9AAAmkxmbSoY386.png


3、軟件條件產生信號

    關於這種信號的產生方式,我們以SIGALRM信號爲例。

    SIGALRM是14號信號,也叫鬧鐘信號,軟件通過alarm函數產生,可以以秒爲單位進行定時,當時間到達之後,終止進程,alarm函數定義如下:

#include <unistd.h>
     unsigned int alarm(unsigned int seconds);
               # 在seconds秒之後給當前進程發送SIGALRM信號
               # 傳入0,停止鬧鐘
               # 返回值爲0,或剩餘秒數
               # 默認處理動作是終止當前進程



alarm測試代碼:   

// myalarm.c
// 測試一秒鐘能打印多少次
#include <stdio.h>
#include <unistd.h>
int main()
{
    int count = 0;
    alarm(1);
    while(1){
        printf("count = %d\n", count++);
    }
    return 0;
}

wKiom1iuMMaQPDdDAAAoMAmSZrE326.png

    然後呢,這裏就要提到上面的一個函數,signal函數,用來自定義進程對信號的動作,網上很多人都把這個函數當做一個單獨的模塊來講,這裏我用一種比較簡單的方式,儘量最容易地說清楚這個函數。

    首先看函數的定義:

#include <signal.h>
     typedef void (*sighandler_t)(int);
     sighandler_t signal(int signum, sighandler_t handler);     # 接收信號之後自定義動作
           # 參數1,要處理的信號
           # 參數2,通常有三種形式
                SIG_ING,表示忽略前面的信號,即沒有動作。
                SIG_DFL,表示執行該信號的默認動作,換句話說,如果使用這個選項,完全可以 不適用這個函數,因爲本身就是執行默認動作
                自定義函數名,即函數指針

接下來看一段基於上面alarm函數的測試代碼。

// my_alarm.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int count = 0;
void my_sig(int)
{
    alarm(1);
    printf("count = %d\n", count);
}
int main()
{
    signal(SIGALRM, my_sig);    // 首先要聲明該信號的處理方式,這是使用自定義函數
    alarm(1);
    while(1){
        count++;
    }
    return 0;
}

wKiom1iuM02RytVfAAAkUQq5HAU627.png    、

    我們可以發現,alarm是一次性定時鬧鐘,一次結束之後,如果沒有特殊聲明,進程直接終止。

對比兩次輸出的count,會發現相差數萬被,這裏就體現出了I/O速度和內存的速度之間的差距。


    我們可以試着將上面自定義函數中的alarm(1);去掉,代碼如下:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int count = 0;
void my_sig(int i)
{
    printf("count = %d\n", count);
}
int main()
{
    signal(SIGALRM, my_sig);
    alarm(1);
    while(1)
    {
        count++;
    }
    return 0;
}

    運行結果我們會發現,打印一次之後,進程會卡住不再運行,當我們在另一個終端下執行ps命令,會看到


[muhui@bogon my_alarm]$ ps aux | grep myalarm

muhui     3024 87.0  0.0   1872   376 pts/0    R+   09:03   0:04 ./myalarm

muhui     3026  0.0  0.0   5984   728 pts/1    S+   09:03   0:00 grep myalarm


    進程一直在運行!!

    不要想的太多,這裏自定義行爲之後,並不是就卡在了自定義函數中,而是執行完畢自定義函數,就又調回原來程序跳出的地方,繼續執行原來的while(1),打破了默認的退出當前進程的行爲。

    關於本文中所有的源碼,全部打包上傳,下載連接:

https://github.com/muhuizz/Linux/tree/master/Linux%E7%B3%BB%E7%BB%9F%E7%BC%96%E7%A8%8B/%E4%BF%A1%E5%8F%B7/code

    --------muhuizz整理

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