Linux Signal 示例

jouyouyun發表於2018-11-18

訊號是系統響應某些條件而產生的一個事件,接收到該信的程式做出相應的處理。通常信是由錯誤產生的,如段錯誤(SIGSEGV)。 但信還可以作為程式間通訊的一種方式,由一個程式傳送給另一個程式。

訊號定義在 signal.h 檔案中,以 SIG 作為開頭,可用 kill -l 命令檢視,詳細資訊參見 man 7 signal

訊號處理

訊號可以通過 signalsigaction 函式來註冊處理, signal 函式是 struct sigactionsa_handler 的一種便捷實現。

signal 函式

原型:

void (*signal(int sig, void (*func)(int)))(int);

其中 sig 是需要捕獲的 signal number, 後一個是捕獲到訊號後的處理函式指標,所以處理函式的原型必須是 void func(int) ,簡單的程式碼示例如下:

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

static void
handler(int sig)
{
        printf("Recieved signal: %d
", sig);
}

int
main(int argc, char *argv[])
{
        signal(SIGINT, handler);

        printf("Caught SIGINT, input `quit` to exit...
");
        // wait signal caught
        char buf[1024] = {0};
        while (1) {
                printf("Please input: ");
                scanf("%s", buf);
                if (strcmp(buf, "quit") == 0) {
                        break;
                }
        }
        printf("Exit...
");
        return 0;
}

另外 api 中也提供了下面 2 個特殊的 handler:

  • SIG_IGN

    忽略此訊號

  • SIG_DFL

    恢復此訊號的預設行為

sigaction 函式

原型:

int sigaction(int sig, const struct sigaction *restrict act,
           struct sigaction *restrict oact);

其中 sigsignal number, act 指定訊號的處理行為, oact 如果不為 NULL 則返回訊號之前的處理行為。

struct sigaction 的主要成員如下:

型別 名稱 描述
void(*) (int) sa_handler 處理函式指標,同 signal 函式中的 func 引數
sigset_t sa_mask 訊號遮蔽字,是指當前被阻塞的一組訊號,不能被當前程式收到
int sa_flags 處理行為修改器,指明哪種處理函式生效,詳見下文
void(*) (int, siginfo_t *, void *) sa_sigaction 處理函式指標,僅 sa_flags == SA_SIGINFO 時有效

其中 sa_flags 主要可以設定為以下值:

  • SA_NOCLDSTOP

    子程式停止時不產生 SIGCHLD 訊號

  • SA_RESETHAND

    將訊號的處理函式在處理函式的入口重置為 SIG_DFL

  • SA_RESTART

    重啟可中斷的函式而不是給出 EINTR 錯誤

  • SA_SIGINFO

    使用 sa_sigaction 做為訊號的處理函式

  • SA_NODEFER

    捕獲到訊號時不將它新增到訊號遮蔽字中

簡單的程式碼示例如下:

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

#define SIG SIGINT

static void
sig_handler(int sig, siginfo_t *si, void *data)
{
        printf("Caught signal: %d
", sig);
        printf("Sender pid: %d
", si->si_pid);
        printf("Sender uid: %d
", si->si_uid);
}

static int
sig_caught(int sig)
{
        printf("Start caught signal: %d
", sig);
        struct sigaction sa;
        sa.sa_flags = SA_SIGINFO;
        sa.sa_sigaction = sig_handler;
        sigemptyset(&sa.sa_mask);
        int ret = sigaction(sig, &sa, NULL);
        if (ret == -1) {
                printf("Failed to caught signal: %d
", sig);
                return -1;
        }

        return 0;
}

int
main(int argc, char *argv[])
{
        if (sig_caught(SIG) == -1) {
                return -1;
        }

        printf("Caught signal(%d), input `quit` to exit...
", SIG);
        char buf[1024] = {0};
        while(1) {
                printf("Please input: ");
                scanf("%s", buf);
                if (strcmp(buf, "quit") == 0) {
                        break;
                }
        }
        printf("Exit...
");
        return 0;
}

訊號遮蔽字

考慮一下這種情況:在 signal()/sigaction() 返回之前程式就已經收到了需要處理的訊號,此時程式會以預設行為來處理,這顯然不符合我們的期望。 這時就需要用到訊號遮蔽字了,在程式啟動時就將需要處理的訊號加入的遮蔽字中,等 signal()/sigaction() 返回後再解除遮蔽,解除遮蔽後至少會將收到的待處理訊號傳送一個給程式。

遮蔽字用到一下函式:

int sigemptyset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigprocmask(int how, const sigset_t *restrict set,
           sigset_t *restrict oset);

sigprocmaskset 為需要設定的遮蔽字集, oset 為之前的遮蔽字集, how 控制著 set 如何生效,可設定為以下值:

  • SIG_BLOCK

    該程式的遮蔽字集將為當期遮蔽字集與 set 的並集, set 中包含了需要遮蔽的訊號集

  • SIG_UNBLOCK

    該程式的遮蔽字集將為當期遮蔽字集與 set 的補集的交集, set 中包含了需要解除遮蔽的訊號集

  • SIG_SETMASK

    該程式的遮蔽字集將設定為 set 的值

簡單的設定流程如下:

int
sig_block(int sig, int how)
{
        sigset_t mask;
        sigemptyset(&mask)
        sigaddset(&mask, sig);
        sigprocmask(how, &mask, NULL);
}

訊號傳送

訊號可以通過 kill 函式傳送給指定程式,也可以通過 raise 或者 alarm 函式傳送給當前執行的執行緒或程式,下面來分別說說這幾個函式。

kill

原型:

int kill(pid_t pid, int sig);

kill 函式向指定程式傳送指定的訊號,如果訊號為 0 將執行錯誤檢查,訊號並不會傳送,可以用來檢查 pid 的有效性。

pid 大於 0 時訊號將傳送給此程式, pid 小於等於 0 時,如下:

  • 等於 0

    訊號將傳送給傳送者所在組裡的所有程式

  • 等於 -1

    訊號將傳送給所有程式

  • 小於 -1

    訊號將傳送給程式組為 pid 絕對值的所有組內程式

alarm

原型:

unsigned alarm(unsigned seconds);

alarm 函式將在指定的 seconds 之後傳送一個 SIGALRM 訊號,如果 seconds 為 0, 則取消之前的定時器請求。如果不為 0 則取消之前的請求,重新設定為 seconds 。 如果在等待結束之前有其他的事件產生,那定時器請求也將被取消。

簡單的程式碼示例如下:

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

static void
handler(int sig)
{
        printf("alarm arrived: %d
", sig);
}

int
main(int argc, char *argv[])
{
        signal(SIGALRM, handler);

        alarm(2);

        sleep(2);
        printf("alarm 5s over
");

        alarm(10);
        sleep(1);

        unsigned int remaining = alarm(3);
        printf("alarm 10s remain: %u, reset to 3
", remaining);
        sleep(3);
        printf("alarm 3s over
");

        alarm(20);
        sleep(3);

        remaining = alarm(0);
        printf("cancel alarm 20s, remian: %u, exit...
", remaining);
}

raise

原型:

int raise(int sig);

raise 函式將給當前執行的執行緒或程式傳送訊號,如果訊號處理函式已經被呼叫, raise 函式將等待訊號處理函式呼叫結束才返回。

結語

訊號處理函式是會被重複呼叫的,所以必要儲存其是可重入的,注意處理邏輯。

另外本文中的程式碼都在 signal 中,這個 repo 也有其它的示例,有興趣的可以看看。

附錄

訊號表

/* ISO C99 signals.  */
#define    SIGINT        2    /* Interactive attention signal.  */
#define    SIGILL        4    /* Illegal instruction.  */
#define    SIGABRT        6    /* Abnormal termination.  */
#define    SIGFPE        8    /* Erroneous arithmetic operation.  */
#define    SIGSEGV        11    /* Invalid access to storage.  */
#define    SIGTERM        15    /* Termination request.  */

/* Historical signals specified by POSIX. */
#define    SIGHUP        1    /* Hangup.  */
#define    SIGQUIT        3    /* Quit.  */
#define    SIGTRAP        5    /* Trace/breakpoint trap.  */
#define    SIGKILL        9    /* Killed.  */
#define SIGBUS        10    /* Bus error.  */
#define    SIGSYS        12    /* Bad system call.  */
#define    SIGPIPE        13    /* Broken pipe.  */
#define    SIGALRM        14    /* Alarm clock.  */

/* New(er) POSIX signals (1003.1-2008, 1003.1-2013).  */
#define    SIGURG        16    /* Urgent data is available at a socket.  */
#define    SIGSTOP        17    /* Stop, unblockable.  */
#define    SIGTSTP        18    /* Keyboard stop.  */
#define    SIGCONT        19    /* Continue.  */
#define    SIGCHLD        20    /* Child terminated or stopped.  */
#define    SIGTTIN        21    /* Background read from control terminal.  */
#define    SIGTTOU        22    /* Background write to control terminal.  */
#define    SIGPOLL        23    /* Pollable event occurred (System V).  */
#define    SIGXCPU        24    /* CPU time limit exceeded.  */
#define    SIGXFSZ        25    /* File size limit exceeded.  */
#define    SIGVTALRM    26    /* Virtual timer expired.  */
#define    SIGPROF        27    /* Profiling timer expired.  */
#define    SIGUSR1        30    /* User-defined signal 1.  */
#define    SIGUSR2        31    /* User-defined signal 2.  */

/* Nonstandard signals found in all modern POSIX systems
   (including both BSD and Linux).  */
#define    SIGWINCH    28    /* Window size change (4.3 BSD, Sun).  */

/* Archaic names for compatibility.  */
#define    SIGIO        SIGPOLL    /* I/O now possible (4.2 BSD).  */
#define    SIGIOT        SIGABRT    /* IOT instruction, abort() on a PDP-11.  */
#define    SIGCLD        SIGCHLD    /* Old System V name */

/* Not all systems support real-time signals.  bits/signum.h indicates
   that they are supported by overriding __SIGRTMAX to a value greater
   than __SIGRTMIN.  These constants give the kernel-level hard limits,
   but some real-time signals may be used internally by glibc.  Do not
   use these constants in application code; use SIGRTMIN and SIGRTMAX
   (defined in signal.h) instead.  */
#define __SIGRTMIN    32
#define __SIGRTMAX    __SIGRTMIN

/* Biggest signal number + 1 (including real-time signals).  */
#define _NSIG        (__SIGRTMAX + 1)

相關文章