Linux基礎第六章 信號

6.1 前言

本章簡單描述信號。信號是Linux系統中,內核和進程通訊的一種方式。若是內核但願打斷進程的順序執行,那麼內核會在進程的PCB中記錄信號。而當這個進程被分配到CPU,進入執行狀態時,首先會檢查是否有信號,若是有信號,那麼進程會先嚐試執行信號處理函數。bash

內核須要打斷進程運行的時機:session

  • 進程沒法繼續了函數

int* p = NULL;
*p = 100;
// 此時代碼沒法再繼續運行,內核會發送SIGSEGV信號給進程,這是咱們常見的段錯誤
int a = 0;
int b = 1/a;
// 除0操做,發送SIGFPE給進程
  • 按下ctrl+c
    ctrl+c實際上是bash向前臺進程組發送SIGINTspa

int main()
{
    fork();
    fork(); // 四個進程
    while(1)
    {
        sleep(1);
    }
}

運行該程序後,再按Ctrl+c,結果是四個進程所有退出rest

int main()
{
    signal(SIGINT, SIG_IGN);
    fork();
    fork(); // 四個進程
    while(1)
    {
        sleep(1);
    }
}

有了signal的處理以後,ctrl+c發送的SIGINT不會致使進程退出。code

6.2 信號類型

經過kill -l命令能夠看到系統定義的信號類型,信號值小於32的是傳統的信號,稱之爲非實時信號,而大於32的稱之爲實時信號。這裏只討論非實時信號。blog

6.3 信號的處理

能夠經過signal函數,註冊信號處理函數。若是沒有註冊信號處理函數,那麼按照默認方式處理。
也能夠經過signal設置忽略信號。進程

信號 默認處理動做 發出信號的緣由
SIGHUP A 進程session leader退出時,同session的其餘進程會收到這個信號
SIGINT A Ctrl+C
SIGQUIT C Ctrl+D
SIGILL C 非法指令
SIGABRT C 調用abort函數產生的信號
SIGFPE C 浮點異常
SIGKILL AEF Kill信號
SIGSEGV C 無效的內存引用
SIGPIPE A 管道破裂: 寫一個沒有讀端口的管道
SIGALRM A 由alarm(2)發出的信號
SIGTERM A 終止信號
SIGUSR1 A 用戶自定義信號1
SIGUSR2 A 用戶自定義信號2
SIGCHLD B 子進程狀態變化會給父進程發送SIGCHLD信號
SIGCONT   進程繼續(曾被中止的進程)
SIGSTOP DEF 暫停進程
SIGTSTP D 控制終端(tty)上按下中止鍵
SIGTTIN D 後臺進程企圖從控制終端讀
SIGTTOU D 後臺進程企圖從控制終端寫

A 缺省的動做是終止進程
B 缺省的動做是忽略此信號
C 缺省的動做是終止進程並進行內核映像轉儲(dump core)
D 缺省的動做是中止進程
E 信號不能被捕獲
F 信號不能被忽略內存

由於SIGKILL不能被捕獲,那麼如下代碼是不正常
signal(SIGKILL, handle) //xxxx
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void signal_handle(int a)
{
    if(a == SIGINT)
        printf("signal_handle\n");
    else if(a == SIGABRT)
        printf("abrt\n");
    else if(a == SIGALRM)
        printf("alarm\n");
    else if(a == SIGCHLD)
        printf("child\n");
    else if(a == SIGUSR1)
        printf("usr1 signal\n");
}

int main()
{
    // SIGINT 2
    signal(SIGINT, signal_handle);
    signal(SIGABRT, signal_handle);
    signal(SIGALRM, signal_handle);
    signal(SIGCHLD, signal_handle);
    signal(SIGUSR1, signal_handle);

    pid_t pid  = fork();
    if(pid == 0)
        return 0;


    // 給本身發送一個abrt信號
    //abort();
    alarm(1);

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

 

6.4 不可靠信號

信號值小於32的都是不可靠信號,假如進程收到一個信號來不及處理,這時候又收到一個一樣的信號,那麼這兩個信號會合併成一個信號,這個緣由是由於進程保存該信號的值只有1位。rem

6.5 中斷系統調用(中斷阻塞)

假如一個進程調用了某系統調用致使該進行處於掛起狀態,而此時該進程接收到一個信號,那麼該系統調用被喚醒。一般該系統調用會返回-1,錯誤碼是EINTR

也有些系統調用,能夠設置打斷後重啓,這樣就不會被信號打斷,具體參考man 7 signal

若是使用signal函數註冊信號處理函數,默認被中斷的系統調用是自動重啓的。

// 阻塞
int ret = read(0, buf, sizeof(buf));
printf("read data\n");
#include <signal.h>
#include <stdio.h>
#include <errno.h>
void handle(int v){
    printf("ctrl+c\n");
}

int main()
{
    signal(SIGINT, handle);
    
    char buf;
    int ret = read(0, buf, sizeof(buf));  // read被中斷打斷了
    printf("ret = %d, errno=%d, EINTR=%d\n", ret, errno, EINTR); // EINTR
}

 

6.6 可重入問題

信號會致使可重入問題,好比一個全局鏈表。

std::vector<int> v;
void handler(int sig)
{
  v.push_back(0);
}
int main()
{
  signal(SIGINT, handler);
    signal(SIGUSR1, handler);
  while(1)
  {
        // 先屏蔽全部信號
    v.push_back(0);
        // 再去掉屏蔽
  }
}

以上代碼在必定狀況下會崩潰,在main函數中不停調用push_back,若是在push_back執行一半時,被中斷打斷,而後去執行中斷處理函數時,那麼該中斷處理函數的push_back會崩潰。

有些系統調用自己帶有局部靜態變量,所以那些函數不能在信號處理函數中調用,好比strtokreaddir等,對應的可重入的函數是strtok_rreaddir_r

6.7 發送信號

能夠經過kill函數發送信號。

調用kill
發送信號
進程1
內核
進程2

kill也能夠進程組發送信號

#include <sys/types.h>
#include <signal.h>

int main()
{
    kill(27054, SIGUSR1);    
}

 

補充:掩蓋信號

//掩蓋不可靠信號
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
// 掩蓋SIGINT
// 掩蓋一個可靠信號
void handle(int v)
{
    printf("sigint \n");
}

int main()
{
    signal(SIGINT, handle);
    sigset_t set;

    // 將set集合設置爲空
    sigemptyset(&set);
    // 將SIGINT信號加入集合
    sigaddset(&set, SIGINT);
    // 把這個集合表明的信號,加入信號掩碼
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 今後,該進程收到SIGINT,不會被處理
    
    sleep(5);
    printf("remove SIGINT from mask\n");

    // 去掉SIGINT的掩碼
    sigprocmask(SIG_UNBLOCK, &set, NULL);


    while(1)
    {
        sleep(1);
    }
}
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
// 掩蓋34
// 掩蓋一個可靠信號
void handle(int v)
{
    printf("hahahaha \n");
}

int main()
{
    signal(34, handle);
    sigset_t set;

    // 將set集合設置爲空
    sigemptyset(&set);
    // 將34信號加入集合
    sigaddset(&set, 34);
    // 把這個集合表明的信號,加入信號掩碼
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 今後,該進程收到34,不會被處理
    
    kill(getpid(), 34);
    kill(getpid(), 34);
    kill(getpid(), 34);
    kill(getpid(), 34);
    kill(getpid(), 34);

    sleep(5);
    printf("remove 34 from mask\n");

    // 去掉34的掩碼
    sigprocmask(SIG_UNBLOCK, &set, NULL);


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

 

6.8 忽略信號

signal(SIGPIPE, SIG_IGN);
#include <signal.h>

void handle(int v)
{

}

int main()
{
    // 表示忽略SIGINT
    // 忽略信號和掩蓋信號是有區別:
    //
    // 未決的信號:已經發出可是沒有被處理的信號叫未決的信號
    signal(SIGINT, SIG_IGN);

    // 從這裏開始就不忽略
    signal(SIGINT, handle); // 設置一個處理函數
    signal(SIGINT, SIG_DFL); // 恢復成默認處理
    while(1)
    {
        sleep(1);
    }
}

 

以上例子,忽略SIGPIPE信號,那麼進程收到SIGPIPE後,不會有任何反應。

6.9 屏蔽信號

屏蔽和忽略不一樣,忽略意味着在忽略期間,接收的信號就被忽略了。而屏蔽的意思,是暫時屏蔽,屏蔽期間收到的信號依舊在,若是某一時刻該信號再也不忽略時,該信號的處理程序會被調用。

設置屏蔽集合,使用sigprocmask

6.10 SIGCHLD

SIGCHLD信號產生於子進程退出和狀態變化,父進程一般在該信號的處理函數中,調用wait來回收子進程的PCB,這樣能夠避免阻塞。

#include <signal.h>

void chld_handle(int v)
{
    // 有子進程退出了
    // wait(NULL);
    //
    while(1) // 使用while(1)是避免有多個子進程同時退出,因爲SIGCHLD是不可靠信號,函數只會調用一次
    {
        int ret = waitpid(-1, NULL, WNOHANG); // 每次回收都應該用非阻塞方式去回收
        if(ret == -1)  // 沒有殭屍進程了,以前殭屍進程已經被回收了
            break;
    }
}

int main()
{
    // 等待子進程的結束,問題是:ddddd
    // 它阻塞主進程的執行,影響效率
    // wait(NULL);
    //
    signal(SIGCHLD, chld_handle);

    pid_t pid = fork();
    if(pid == 0) return 0;

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

 

6.11 sigaction和sigqueue

sigaction和signal同樣用來註冊信號處理函數,siqqueue和kill同樣,用來發送信號,可是sigaction比signal功能強大,signal比較簡單。

強大:

  1. 能夠傳遞參數

  2. 能夠得到發送信號的進程信息

  3. 能夠設置SA_RESTART

    #include <signal.h>
    #include <stdio.h>
    #include <errno.h>
    //void handle(int v){}
    //
    // 新的信號處理函數
    void handle(int v, siginfo_t* info, void* p)
    {
        printf("ctrl+c\n");
    }
    int main()
    {
        // 默認的signal已經有SA_RESTART這個flag了
        //signal(SIGINT, handle);
        
        struct sigaction sig;
        sig.sa_handler = NULL;
        sig.sa_sigaction = handle;
        sigemptyset(&sig.sa_mask);
     //   sig.sa_flags = 0;
        sig.sa_flags = SA_RESTART; // 讓阻塞的系統調用,被這個信號打斷以後,要重啓
        sig.sa_restorer = NULL; // 在Linux下沒用,直接填NULL就能夠了
        sigaction(SIGINT, &sig, NULL);
    
        char buf;
        int ret = read(0, buf, sizeof(buf));  // read被中斷打斷了
        printf("ret = %d, errno=%d, EINTR=%d\n", ret, errno, EINTR); // EINTR
    }
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    
    char buf[1024];
    
    void handle_data()
    {
        printf("user input is %s\n", buf);
    }
    
    void handle_chld1(int v)
    {
        while(1)
        {
            int ret = waitpid(-1, NULL, WNOHANG);
            if(ret < 0) break;
        }
    }
    
    void handle_chld(int v, siginfo_t* info, void* p)
    {
        handle_chld1(v);
    }
    
    int main()
    {
    //   signal(SIGCHLD, handle_chld);
    
        struct sigaction act;
        act.sa_handler = NULL;
        act.sa_sigaction = handle_chld;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        act.sa_restorer = NULL;
        sigaction(SIGCHLD, &act, NULL);
    
        while(1)
        {
        //    char* ret = fgets(buf, sizeof(buf), stdin);
        //    if(ret == NULL) break;
            
            int ret = read(0, buf, sizeof(buf));
            if(ret < 0)
            {
                // 說明read出錯,不是真正的出錯,而是被中斷打擾了
                // 那此時,應該從新調用read函數,去獲取信息
                if(errno == EINTR)
                {
                    continue;
                }
                // 若是是其餘錯誤緣由,就break
                break;
            }
    
            pid_t pid = fork();
            if(pid == 0)
            {
                handle_data();  // 建立一個子進程去處理數據
                exit(0);
            }
        }
    }
    #include <signal.h>
    #include <stdio.h>
    void handle(int v, siginfo_t* info, void* p)
    {
        printf("recv SIGINT, arg=%d\n", info->si_value.sival_int);
    }
    
    int main()
    {
        struct sigaction act;
        act.sa_handler = NULL;
        act.sa_sigaction = handle;
        sigemptyset(&act.sa_mask);
        act.sa_flags = SA_SIGINFO|SA_RESTART;
        act.sa_restorer = NULL;
    
        // 註冊信號處理函數
        sigaction(SIGINT, &act, NULL);
    
        union sigval v;
        v.sival_int = 99;
        // 至關於kill,可是它能夠傳遞一個參數
        sigqueue(getpid(), SIGINT, v);
    
        getchar();
    }
    #include <signal.h>
    #include <stdio.h>
    
    int main()
    {
        char* p = malloc(100);
        union sigval v;
      //  v.sival_int = 98;
        v.sival_ptr = p;
        // 至關於kill,可是它能夠傳遞一個參數
        sigqueue(28360, SIGINT, v);
    
        getchar();
    }

     

6.12 函數命令

signal:註冊信號處理函數
kill:發送信號
sigprocmask:設置信號掩碼
sigemptyset:清空信號集
sigfillset:設滿信號集
sigaddset:往信號集增長一個信號
sigdelset:從信號集刪除一個信號
sigismember:判斷信號不然在信號集

sigaction:註冊更增強大的處理函數
sigqueue:發送信號

abortalarmpause

相關文章
相關標籤/搜索