博客 / 詳情

返回

Linux系統編程-(四)信號

一.信號概述

1.1 中斷

中斷就是字面的意思,譬如正在打遊戲,手機響了,這時後中斷遊戲,去接手機,回來再打遊戲,這就是中斷。

1.2 什麼是信號

信號是軟件中斷,是在軟件層次上對中斷機制的一種模擬,是一種異步通信的方式 。信號是 Linux 進程間通信的最古老的方式,也是最常用的通信方式。

1.3 信號機制

進程A給進程B發送信號,進程B收到信號之前執行自己的代碼,收到信號後,不管執行到程序的什麼位置,都要暫停運行,去處理信號,處理完畢後再繼續執行,是一種異步模式。

1.4 信號狀態

三種狀態,產生、未決和遞達。
1 產生
a)按鍵產生,如:Ctrl+c、Ctrl+z
b)系統調用產生,如:kill、raise、abort
3)軟件條件產生,如:定時器alarm
4)硬件異常產生,如:非法訪問內存(段錯誤)、除0(浮點數例外)、內存對齊出錯(總線錯誤)
5)命令產生,如:kill命令
2 未決
沒有被處理,產生和遞達之間的狀態。主要由於阻塞(屏蔽)導致該狀態
3 遞達
遞送並且到達進程,信號被處理了

1.5 查看信號

1)kill -l

(base) zhaow@zhaow-610:~$ kill -l
 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX
//  信號編號)信號名字    

2)man 7 signal

SIGNAL(7)                  Linux Programmer's Manual                 SIGNAL(7)

NAME
       signal - overview of signals

DESCRIPTION
       Linux  supports both POSIX reliable signals (hereinafter "standard sig‐
       nals") and POSIX real-time signals.

......
Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGHUP        1       Term    Hangup detected on controlling terminal
                                     or death of controlling process
       SIGINT        2       Term    Interrupt from keyboard
       SIGQUIT       3       Core    Quit from keyboard
       SIGILL        4       Core    Illegal Instruction
       SIGABRT       6       Core    Abort signal from abort(3)
       SIGFPE        8       Core    Floating-point exception
......

信號的四個要素:
1) 信號的編號

使用kill -l命令可以查看當前系統有哪些信號,不存在編號為0的信號。其中1-31號信號稱之為常規信號(也叫普通信號或標準信號),34-64稱之為實時信號,驅動編程與硬件相關。

2) 信號的名稱
3) 產生信號的事件
4) 信號的默認處理動作

Term:終止進程
Ign:忽略信號 (默認即時對該種信號忽略操作)
Core:終止進程,生成Core文件。(查驗死亡原因,用於gdb調試)
Stop:停止(暫停)進程
Cont:繼續運行進

1.6 阻塞信號集和未決信號集

Linux內核的進程控制塊PCB是一個結構體,這個結構體裏面包含了信號相關的信息,主要有阻塞信號集和未決信號集。

信號集 説明
阻塞信號集 將某些信號加入集合,對他們設置屏蔽,當屏蔽信號後,再收到該信號,該信號的處理將推後(處理髮生在解除屏蔽後)。
未決信號集 信號產生後由於某些原因(主要是阻塞)不能抵達。這類信號的集合稱之為未決信號集。在屏蔽解除前,信號一直處於未決狀態。信號產生,未決信號集中描述該信號的位立刻翻轉為1,表示信號處於未決狀態。當信號被處理對應位翻轉回為0。

二.信號相關函數

2.1 signal函數

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

函數作用:註冊信號捕捉函數
函數參數
    signum:信號編號
    handler:信號處理函數

代碼示例

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

/*signal函數測試---註冊信號處理函數*/

void sighandler(int signo){
    printf("singo = %d\n",signo);
}

int main(){
    signal(SIGPIPE,sighandler);
    //沒給讀端的管道寫數據產生SIGPIPE信號。
    int fd[2];
    int ret = pipe(fd);
    if(ret < 0){
        perror("pipe");
        return -1;
    }

    close(fd[0]);

    write(fd[1],"hello",strlen("hello")); 
    // 信號產生後,內核調用註冊的sighandler
    return 0;
    
}

2.2 kill函數


#include <sys/types.h>
#include <signal.h>
​
int kill(pid_t pid, int sig);
功能:給指定進程發送指定信號(不一定殺死)
​
參數:
    pid : 取值有 4 種情況 :
        pid > 0:  將信號傳送給進程 ID 為pid的進程。
        pid = 0 :  將信號傳送給當前進程所在進程組中的所有進程。
        pid = -1 : 將信號傳送給系統內所有的進程。
        pid < -1 : 將信號傳給指定進程組的所有進程。這個進程組號等於 pid 的絕對值。
    sig : 信號的編號,這裏可以填數字編號,也可以填信號的宏定義,可以通過命令 kill - l("l" 為字母)進行相應查看。不推薦直接使用數字,應使用宏名,因為不同操作系統信號編號可能不同,但名稱一致。
​
返回值:
    成功:0
    失敗:-1

代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

int main(){
    kill(getpid(),SIGKILL); // 給當前進程發送信號SIGKILL
    printf("helloworld\n");
    return 0;
}

2.3 raise函數

#include <signal.h>
​
int raise(int sig);
功能:給當前進程發送指定信號(自己給自己發),等價於 kill(getpid(), sig)
參數:
    sig:信號編號
返回值:
    成功:0
    失敗:非0值

代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信號處理函數
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
}


int main(){
    // 註冊信號處理函數
    signal(SIGINT,sighandler);

    // 給當前進程發送SIGINT信號
    raise(SIGINT);
    return 0;

}

2.4 abort

#include <stdlib.h>
​
void abort(void);
功能:給自己發送異常終止信號 6) SIGABRT,併產生core文件,等價於kill(getpid(), SIGABRT);
​
參數:無
​
返回值:無
​

代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信號處理函數
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
}


int main(){
    // 註冊信號處理函數
    signal(SIGINT,sighandler);

    // 給當前進程發送SIGINT信號
    raise(SIGINT);
    // abort 給自己發送異常終止信號 6) SIGABRT,併產生core文件,等價於kill(getpid(), SIGABRT);
    abort();
    return 0;

}

2.5 alarm 函數

#include <unistd.h>
​
unsigned int alarm(unsigned int seconds);
功能:
    設置定時器(鬧鐘)。在指定seconds後,內核會給當前進程發送14)SIGALRM信號。進程收到該信號,默認動作終止。每個進程都有且只有唯一的一個定時器。
    取消定時器alarm(0),返回舊鬧鐘餘下秒數。
參數:
    seconds:指定的時間,以秒為單位
返回值:
    返回0或剩餘的秒數

代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

int main(){
    int n = alarm(5);
    printf("n = %d\n",n);
    sleep(1);
    n = alarm(5);
    printf("n = %d\n",n);
    return 0;
}

2.6 setitimer函數

#include <sys/time.h>
​
int setitimer(int which,  const struct itimerval *new_value, struct itimerval *old_value);
功能:
    設置定時器(鬧鐘)。 可代替alarm函數。精度微秒us,可以實現週期定時。
參數:
    which:指定定時方式
        a) 自然定時:ITIMER_REAL → 14)SIGALRM計算自然時間
        b) 虛擬空間計時(用户空間):ITIMER_VIRTUAL → 26)SIGVTALRM  只計算進程佔用cpu的時間
        c) 運行時計時(用户 + 內核):ITIMER_PROF → 27)SIGPROF計算佔用cpu及執行系統調用的時間
    new_value:struct itimerval, 負責設定timeout時間
        struct itimerval {
            struct timerval it_interval; // 鬧鐘觸發週期
            struct timerval it_value;    // 鬧鐘觸發時間
        };
        struct timeval {
            long tv_sec;            // 秒
            long tv_usec;           // 微秒
        }
        itimerval.it_value: 設定第一次執行function所延遲的秒數 
        itimerval.it_interval:  設定以後每幾秒執行function
​
    old_value: 存放舊的timeout值,一般指定為NULL
返回值:
    成功:0
    失敗:-1

代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

//信號處理函數
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
}

int main(){
    signal(SIGALRM,sighandler);

    struct itimerval tm;
    // 週期性時間賦值
    tm.it_interval.tv_sec = 1;
    tm.it_interval.tv_usec = 0;
    // 第一次出發的時間
    tm.it_value.tv_sec = 3;
    tm.it_value.tv_usec = 0;

    setitimer(ITIMER_REAL,&tm,NULL);

    while (1)
    {
        sleep(1);
    }
    

    return 0;
}

三 信號集函數

3.1 信號集

為了方便對多個信號進行處理,一個用户進程常常需要對多個信號做出處理,在 Linux 系統中引入了信號集。
信號集是一個能表示多個信號的數據類型,sigset_t set,set即一個信號集。
由於信號集屬於內核的一塊區域,用户不能直接操作內核空間,為此,內核提供了一些信號集相關的接口函數,使用這些函數用户就可以完成對信號集的相關操作。

函數 函數説明 參數與返回值
int sigemptyset(sigset_t *set); 函數説明:將某個信號集清0 函數返回值:成功:0;失敗:-1,設置errno
int sigfillset(sigset_t *set); 函數説明:將某個信號集置1 函數返回值:成功:0;失敗:-1,設置errno
int sigaddset(sigset_t *set, int signum); 函數説明:將某個信號加入信號集合中 函數返回值:成功:0;失敗:-1,設置errno
int sigdelset(sigset_t *set, int signum); 函數説明:將某信號從信號清出信號集 函數返回值:成功:0;失敗:-1,設置errno
int sigismember(const sigset_t *set, int signum); 函數説明:判斷某個信號是否在信號集中 函數返回值:在:1;不在:0;出錯:-1,設置errno

代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>


int main(){
    // 定義個信號集變量
    sigset_t set;
    int ret = 0;

    // 初始化信號集內容,也就是清空信號集
    sigemptyset(&set);

    // 判斷SIGINT 是否在信號集set中,在返回1,不在返回0
    ret = sigismember(&set,SIGINT);
    if(ret == 0){
        printf("SIGINT not in set\n");
    }

    // 將SIGINT和SIGQUIT添加到信號集set
    sigaddset(&set,SIGINT);
    sigaddset(&set,SIGQUIT);

    // 判斷SIGINT 是否在信號集set中,在返回1,不在返回0
    ret = sigismember(&set,SIGINT);
    if(ret == 1){
        printf("SIGINT  in set\n");
    } 
    // 把 SIGQUIT 從信號集 set 移除
    sigdelset(&set, SIGQUIT); 

    // 判斷SIGQUI 是否在信號集set中,在返回1,不在返回0
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT not in set \n");
    }
    return 0;
}

3.2 sigprocmask函數

用來屏蔽信號、解除屏蔽也使用該函數。其本質,讀取或修改進程控制塊中的信號屏蔽字(阻塞信號集)。

#include <signal.h>
​
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
    檢查或修改信號阻塞集,根據 how 指定的方法對進程的阻塞集合進行修改,新的信號阻塞集由 set 指定,而原先的信號阻塞集合由 oldset 保存。
​
參數:
    how : 信號阻塞集合的修改方法,有 3 種情況:
        SIG_BLOCK:向信號阻塞集合中添加 set 信號集,新的信號掩碼是set和舊信號掩碼的並集。相當於 mask = mask|set。
        SIG_UNBLOCK:從信號阻塞集合中刪除 set 信號集,從當前信號掩碼中去除 set 中的信號。相當於 mask = mask & ~ set。
        SIG_SETMASK:將信號阻塞集合設為 set 信號集,相當於原來信號阻塞集的內容清空,然後按照 set 中的信號重新設置信號阻塞集。相當於mask = set。
    set : 要操作的信號集地址。
        若 set 為 NULL,則不改變信號阻塞集合,函數只把當前信號阻塞集合保存到 oldset 中。
    oldset : 保存原先信號阻塞集地址
​
返回值:
    成功:0,
    失敗:-1,失敗時錯誤代碼只可能是 EINVAL,表示參數 how 不合法。

3.3 sigpending函數

#include <signal.h>
​
int sigpending(sigset_t *set);
功能:讀取當前進程的未決信號集
參數:
    set:未決信號集
返回值:
    成功:0
    失敗:-1

代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

//信號處理函數
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
}


int main(){
    //註冊SIGINT和SIGQUIT的信號處理函數
    signal(SIGINT, sighandler);
    signal(SIGQUIT, sighandler);

    // 定義個信號集變量
    sigset_t myset,old;
    // 初始化信號集
    sigemptyset(&myset);

    // 添加信號集到阻塞中
    sigaddset(&myset, SIGINT);
    sigaddset(&myset, SIGQUIT);
    sigaddset(&myset, SIGKILL);

    // 自定義信號集設置到內核中的阻塞信號集
    sigprocmask(SIG_BLOCK, &myset, &old);

    sigset_t pend;
    int i = 0;
    while (1)
    {
        // 讀取內核中的未決信號集
        sigpending(&pend);

        for(int i=1; i< 32;i++){
            if (sigismember(&pend, i))
            {
                printf("1");
            }
            else if (sigismember(&pend, i) == 0)
            {
                printf("0");
            }
            printf("\n");
        }
        printf("\n");
        sleep(5);
        i++;

        if(i<5){
             sigprocmask(SIG_SETMASK, &old, NULL);
        
        }


    }
    
    return 0;
}

四.信號捕捉

一個進程收到一個信號的時候,可以用如下方法進行處理:
1)執行系統默認動作:對大多數信號來説,系統默認動作是用來終止該進程。
2)忽略此信號(丟棄)
3)執行自定義信號處理函數(捕獲)

SIGKILL 和 SIGSTOP 不能更改信號的處理方式,因為它們向用户提供了一種使進程終止的可靠方法。

信號捕捉函數常用的有兩個 signal函數和sigaction函數。
signal函數上述已有説明。

4.1 sigaction函數

#include <signal.h>
​
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
    檢查或修改指定信號的設置(或同時執行這兩種操作)。
​
參數:
    signum:要操作的信號。
    act:   要設置的對信號的新處理方式(傳入參數)。
    oldact:原來對信號的處理方式(傳出參數)。
​
    如果 act 指針非空,則要改變指定信號的處理方式(設置),如果 oldact 指針非空,則系統將此前指定信號的處理方式存入 oldact。
​
返回值:
    成功:0
    失敗:-1


struct sigaction {
       void  (*sa_handler)(int);    // 舊的信號處理函數
       void  (*sa_sigaction)(int, siginfo_t *, void *); //新的信號處理函數
       sigset_t  sa_mask; //信號處理函數執行期間需要阻塞的信號
       int      sa_flags; //通常為0,表示使用默認標識
       void     (*sa_restorer)(void);
};

代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信號處理函數
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
    sleep(4);
}

int main()
{
    //註冊信號處理函數
    struct sigaction act;
    act.sa_handler = sighandler;
    sigemptyset(&act.sa_mask);  //在信號處理函數執行期間, 不阻塞任何信號
    sigaddset(&act.sa_mask, SIGQUIT);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, NULL);

    
    signal(SIGQUIT, sighandler);    

    while(1)
    {
        sleep(10);
    }

    return 0;
}

4.2 內核實現信號捕捉過程

如果信號的處理動作是用户自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。
以如下例子説明
1.用户程序註冊了SIGQUIT信號的處理函數sighandler。
2.當前正在執行main函數,這時發生中斷或異常切換到內核態。
3.在中斷處理完畢後要返回用户態的main函數之前檢查到有信號SIGQUIT遞達。
4.內核決定返回用户態後不是恢復main函數的上下文繼續執行,而是執行sighandler函數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關係,是兩個獨立的控制流程。
5.sighandler函數返回後自動執行特殊的系統調用sigreturn再次進入內核態。
如果沒有新的信號要遞達,這次再返回用户態就是恢復main函數的上下文繼續執行了

五.SIGCHLD信號

5.1 產生SIGCHLD信號的條件

1) 子進程終止時

2) 子進程接收到SIGSTOP信號停止時

3) 子進程處在停止態,接受到SIGCONT後喚醒時

5.2 SIGCHLD信號的作用

子進程退出後,內核會給它的父進程發送SIGCHLD信號,父進程收到這個信號後可以對子進程進行回收。
使用SIGCHLD信號完成對子進程的回收可以避免父進程阻塞等待而不能執行其他操作,只有當父進程收到SIGCHLD信號之後才去調用信號捕捉函數完成對子進程的回收,未收到SIGCHLD信號之前可以處理其他操作。

5.3 避免殭屍進程

父進程通過 wait() 和 waitpid() 等函數等待子進程結束,但是,這會導致父進程掛起。
如果父進程要處理的事情很多,不能夠掛起,可以通過 signal() 函數人為處理信號 SIGCHLD , 只要有子進程退出自動調用指定好的回調函數,因為子進程結束後, 父進程會收到該信號 SIGCHLD ,可以在其回調函數裏調用 wait() 或 waitpid() 回收。
代碼示例

//父進程使用SICCHLD信號完成對子進程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void waitchild(int signo)
{
    pid_t wpid;

    //回收子進程
    while(1)
    {
        wpid = waitpid(-1, NULL, WNOHANG);
        if(wpid>0)
        {
            printf("child is quit, wpid==[%d]\n", wpid);
        }
        else if(wpid==0)
        {
            printf("child is living, wpid==[%d]\n", wpid);
            break;
        }
        else if(wpid==-1)
        {
            printf("no child is living, wpid==[%d]\n", wpid);
            break;
        }
    }
}

int main()
{
    pid_t pid;

    signal(SIGCHLD,waitchild);

    pid = fork();
    if(pid < 0){
        perror("fork");
        exit(1);
    }else if(pid == 0){
        printf("child process [%d]\n",getpid());
        exit(0);
    }else{
        sleep(2);
        printf("father process [%d]\n",getpid());
        system("ps -ef | grep defunct"); // 檢查有無殭屍進程
    }
    return 0;

    return 0;
}

5.4 父進程忽略子進程,子進程結束後,由內核回收

用signal(SIGCHLD, SIG_IGN)通知內核,父進程忽略此信號,那麼子進程結束後,內核會回收, 並不再給父進程發送信號。

//父進程使用SICCHLD信號完成對子進程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;

    signal(SIGCHLD,SIG_IGN);

    pid = fork();
    if(pid < 0){
        perror("fork");
        exit(1);
    }else if(pid == 0){
        printf("child process [%d]\n",getpid());
        exit(0);
    }else{
        sleep(2);
        printf("father process [%d]\n",getpid());
        system("ps -ef | grep defunct");
    }
    return 0;

    return 0;
}
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.