當前位置:首頁 > 公眾號精選 > 嵌入式微處理器
[導讀]人們很容易高估某個決定性時刻的重要性,也很容易低估每天進行微小改進的價值。以前我也以為大規(guī)模的成功需要大規(guī)模的行動,現(xiàn)在我不這么認為了。長期來看,由于復利效果,一點小小的改進就能產(chǎn)生驚人的變化。 還有一點值得注意的情況,大多數(shù)人有了家庭和子

人們很容易高估某個決定性時刻的重要性,也很容易低估每天進行微小改進的價值。以前我也以為大規(guī)模的成功需要大規(guī)模的行動,現(xiàn)在我不這么認為了。長期來看,由于復利效果,一點小小的改進就能產(chǎn)生驚人的變化。

還有一點值得注意的情況,大多數(shù)人有了家庭和子女后,并且現(xiàn)在國內盛行加班文化,很難再集中精力能抽出大塊的時間進行學習了,部分還能堅持學習的人幾乎都是以犧牲睡眠時間為代價的,我個人不太認為這種做法,我始終認為有更合理健康的方法能形成一個工作、生活、學習、娛樂的有效循環(huán),或許認識到 微進步 的重要性就是一個很好的開始吧。

本文就是我的微進步,歡迎閱讀。

一、概述

信號有時被稱為提供處理異步事件機制的軟件中斷,與硬件中斷的相似之處在于打斷了程序執(zhí)行的正常流程,很多比較重要的應用程序都需處理信號。事件可以來自于系統(tǒng)外部,例如用戶按下 Ctrl+C,或者來自程序或者內核的某些操作。作為一種進程間通信 (IPC) 的基本形式,進行可以給另一個進程發(fā)送信號。

信號很早就是 Unix 的一部分。隨著時間的推移,信號有了很大的改進。比如在可靠性方面,之前的信號可能會出現(xiàn)丟失的情況。在功能方面,現(xiàn)在信號可以攜帶用戶定義的附加信息。最初,不同的 Unix 系統(tǒng)對信號的修改,后來,POSIX 標準的到來挽救并且標準化了信號機制。

  • 用術語 raise 表示一個信號的產(chǎn)生,catch 表示接收到一個信號。

  • 事件的發(fā)生是異步的,程序對信號的處理也是異步的。

  • 信號可以被生成、捕獲、響應或忽略。有兩種信號不能被忽略:SIGKILL 和 SIGSTOP。不能被忽略的原因是:它們向內核和超級用戶提供了使進程終止或停止的可靠方法。

1. 簡單概念

信號類型:

$ man 7 signal
DESCRIPTION
   Standard signals
          First the signals described in the original POSIX.1-1990 standard.

       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
       SIGKILL       9       Term    Kill signal
       SIGSEGV      11       Core    Invalid memory reference
       SIGPIPE      13       Term    Broken pipe: write to pipe with no
                                     readers
       SIGALRM      14       Term    Timer signal from alarm(2)
       SIGTERM      15       Term    Termination signal
       SIGUSR1   30,10,16    Term    User-defined signal 1
       SIGUSR2   31,12,17    Term    User-defined signal 2
       SIGCHLD   20,17,18    Ign     Child stopped or terminated

       SIGCONT   19,18,25    Cont    Continue if stopped
       SIGSTOP   17,19,23    Stop    Stop process
       SIGTSTP   18,20,24    Stop    Stop typed at terminal
       SIGTTIN   21,21,26    Stop    Terminal input for background process
       SIGTTOU   22,22,27    Stop    Terminal output for background process

       The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

       Next the signals not in the POSIX.1-1990 standard but described in SUSv2 and POSIX.1-2001.

       Signal       Value     Action   Comment
       ────────────────────────────────────────────────────────────────────
       SIGBUS      10,7,10     Core    Bus error (bad memory access)
       SIGPOLL                 Term    Pollable event (Sys V).
                                       Synonym for SIGIO
       SIGPROF     27,27,29    Term    Profiling timer expired
       SIGSYS      12,31,12    Core    Bad argument to routine (SVr4)
       SIGTRAP        5        Core    Trace/breakpoint trap
       SIGURG      16,23,21    Ign     Urgent condition on socket (4.2BSD)
       SIGVTALRM   26,26,28    Term    Virtual alarm clock (4.2BSD)
       SIGXCPU     24,24,30    Core    CPU time limit exceeded (4.2BSD)
       SIGXFSZ     25,25,31    Core    File size limit exceeded (4.2BSD)

        ...

       Next various other signals.

       Signal       Value     Action   Comment
       ────────────────────────────────────────────────────────────────────
       SIGIOT         6        Core    IOT trap. A synonym for SIGABRT
       SIGEMT       7,-,7      Term
       SIGSTKFLT    -,16,-     Term    Stack fault on coprocessor (unused)
       SIGIO       23,29,22    Term    I/O now possible (4.2BSD)
       SIGCLD       -,-,18     Ign     A synonym for SIGCHLD
       SIGPWR      29,30,19    Term    Power failure (System V)
       SIGINFO      29,-,-             A synonym for SIGPWR
       SIGLOST      -,-,-      Term    File lock lost (unused)
       SIGWINCH    28,28,20    Ign     Window resize signal (4.3BSD, Sun)
       SIGUNUSED    -,31,-     Core    Synonymous with SIGSYS

       (Signal 29 is SIGINFO / SIGPWR on an alpha but SIGLOST on a sparc.)

發(fā)送信號:

  • 如果想發(fā)送一個信號給進程,而該進程并不是當前的前臺進程,就需要使用kill 命令。

  • kill 命令有一個有用的變體叫 killall,它可以給運行著某一命令的所有進程發(fā)送信號。

處理信號:
Unix 系統(tǒng)提供了兩種方法來改變信號處置:signal() 和 sigaction()。signal()系統(tǒng)調用是設置信號處置的原始 API,所提供的接口比sigaction() 簡單。另一方面,sigaction() 提供了 signal() 所不具備的功能。進一步而言,signal() 的行為在不同 Unix 實現(xiàn)間存在差異,這意味著對可移植性有所追求的程序絕不能使用此調用來建立信號處理函數(shù) (signal handler)。故此,sigaction()是建立信號處理器的首選API。

由于可能會在許多老程序中看到 signal() 的應用,我們先了解如何用 signal() 函數(shù)來處理信號。

signal() 的定義:

$ man 2 signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 參數(shù)1 signum 指定希望修改 handler 的信號編號,參數(shù)2 handler,則指定信號抵達時所調用的 signal handler 函數(shù)的地址。

  • 成功,返回以前的信號處理函數(shù);出錯,返回 SIG_ERR;

2. 入門實驗

簡單試用 signal()。

分解代碼:

static void ouch(int sig) {
    printf("OUCH! - I got signal %d\n", sig);
    (void) signal(SIGINT, SIG_DFL);
}
int main() {
    (void) signal(SIGINT, ouch);

    while(1) {
        printf("Hello World!\n");
        sleep(1);
    }
}

運行效果:

$ ./ctrlc1 
Hello World!
Hello World!
^COUCH! - I got signal 2
Hello World!
Hello World!

相關要點:

  • 在信號處理函數(shù)中,調用如 printf 這樣的函數(shù)是不安全的。一般的做法是:在信號處理函數(shù)中設置一個標志,然后在主程序中檢查該標志,如需要就打印一條消息。

  • 如果想保留信號處理函數(shù),讓它繼續(xù)響應用戶的 Ctrl+C 組合鍵,我們就需要再次調用 signal 函數(shù)來重新建立它。這會使信號在一段時間內無法得到處理,這段時間從調用中斷函數(shù)開始,到信號處理函數(shù)的重建為止。如果在這段時間內程序接收到第二個信號,它就會違背我們的意愿終止程序的運行。

  • 不推薦使用 signal 接口。之所以介紹它,是因為可能會在許多老程序中看到它的應用。更清晰、執(zhí)行更可靠的函數(shù): sigaction(),在所有的新程序中都應該使用這個函數(shù),暫不做深入介紹。

二、發(fā)送信號

1. 如何發(fā)送信號

進程可以通過調用 kill 函數(shù)向包括它本身在內的其他進程發(fā)送一個信號。

kill():

$ man 2 kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

把參數(shù) sig 給指定的信號發(fā)送給由參數(shù) pid 指定的進程號所指定的進程。

kill 調用會在失敗時返回 -1 并設置 errno 變量,失敗的原因:

  • 給定的信號無效(errno設置為EINVAL);

  • 發(fā)送進程權限不夠(errno設置為EPERM);

  • 目標進程不存在(errno設置為ESRCH);

關于權限:
要想發(fā)送一個信號,發(fā)送進程必須擁有相應的權限,包括2種情況:

  • 兩個進程必須擁有相同的用戶 ID,即你只能發(fā)送信號給屬于自己的進程;

  • 超級用戶可以發(fā)送信號給任何進程;

2. 鬧鐘功能

進程可以通過調用 alarm() 函數(shù)在經(jīng)過預定時間后發(fā)送一個 SIGALRM 信號。

alarm():

$ man 2 alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 在 seconds 秒之后發(fā)送一個 SIGALRM 信號。

  • 返回值是以前設置的鬧鐘時間的余留秒數(shù),如果調用失敗則返回 -1。

相關要點:

  • 由于處理的延時和時間調度的不確定性,實際鬧鐘時間將比預先安排的要稍微拖后一點兒。

  • 把參數(shù) seconds 設置為 0 將取消所有已設置的鬧鐘請求。

  • 如果在接收到 SIGALRM 信號之前再次調用 alarm() 函數(shù),則鬧鐘重新開始計時

  • 每個進程只能有一個鬧鐘時間。

3. 入門實驗

用 kill() 模擬鬧鐘。

分解代碼:
設置 signal handler:

int main()
{
    pid_t pid;

    printf("alarm application starting\n");

    pid = fork();
    switch(pid) {
    case -1:
      /* Failure */
      perror("fork failed");
      exit(1);
    case 0:
      /* child */
        sleep(5);
        kill(getppid(), SIGALRM);
        exit(0);
    }

    /* parent */
    printf("waiting for alarm to go off\n");
    (void) signal(SIGALRM, ding);

    pause();
    if (alarm_fired)
        printf("Ding!\n");

    printf("done\n");
    exit(0);
}

定義 signal handler:

static int alarm_fired = 0;
static void ding(int sig)
{
    alarm_fired = 1;
}

通過 fork 調用啟動新的進程:子進程休眠 5 秒后向其父進程發(fā)送一個 SIGALRM 信號。父進程在安排好捕獲 SIGALRM 信號后暫停運行,直到接收到一個信號為止。

運行效果:

$ ./alarm 
alarm application starting
waiting for alarm to go off
<等待5 秒鐘>
Ding!
done

相關要點:

  • pause() 把程序的執(zhí)行掛起直到有一個信號出現(xiàn)為止。使用信號并掛起程序的執(zhí)行是 Unix 程序設計中的一個重要部分。

    $ man 2 pause
    #include <unistd.h>
    int pause(void);
  • 當它被一個信號中斷時,將返回 -1(如果下一個接收到的信號沒有導致程序終止的話)并把 errno 設置為 EINTR。

  • 更常見的方法是使用 sigsuspend() 函數(shù),暫不做介紹。

  • 在信號處理函數(shù)中沒有調用 printf,而是通過設置標志,然后在main函數(shù)中檢查該標志來完成消息的輸出。

  • 如果信號出現(xiàn)在系統(tǒng)調用的執(zhí)行過程中會怎么樣?

    • 一般只需要考慮“慢”系統(tǒng)調用,例如從終端讀數(shù)據(jù),如果在這個系統(tǒng)調用等待數(shù)據(jù)時出現(xiàn)一個信號,它就會返回錯誤 EINTR。
      $ man 3 errno
      EINTR
      Interrupted function call (POSIX.1); see signal(7).
  • 如果你開始在自己的程序中使用信號,就需要注意一些系統(tǒng)調用會因為接收到了一個信號而失敗。

  • 我們需要更健壯的信號接口:

    • 在編寫程序中處理信號部分的代碼時必須非常小心,因為在使用信號的程序中會出現(xiàn)各種各樣的“競態(tài)條件”。例如,如果想調用pause等待一個信號,可信號卻出現(xiàn)在調用 pause() 之前,就會使程序無限期地等待一個不會發(fā)生的事件。

    • POSIX 標準推薦了一個更新和更健壯的信號編程接口:sigaction。

三、信號集 (Signal Set)

多個信號可使用一個稱之為信號集的數(shù)據(jù)結構來表示,POSIX.1 定義了數(shù)據(jù)類型 sigset_t 以表示一個信號集,并且定義了下列 5 個處理信號集的函數(shù):

$ man 3 sigemptyset
NAME
       sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations

SYNOPSIS
       #include <signal.h>

       int sigemptyset(sigset_t *set);
       int sigfillset(sigset_t *set);
       int sigaddset(sigset_t *setint signum);
       int sigdelset(sigset_t *setint signum);
       int sigismember(const sigset_t *setint signum);
  • 函數(shù) sigemptyset() 初始化由參數(shù) set 指向的信號集,清除其中所有信號。

  • 函數(shù) sigfillset() 初始化由參數(shù) set 指向的信號集,使其包括所有信號。

  • 必須使用 sigemptyset() 或者 sigfillset() 來初始化信號集。這是因為 C 語言不會對自動變量進行初始化,并且,借助于將靜態(tài)變量初始化為 0 的機制來表示空信號集的作法在可移植性上存在問題,因為有可能使用位掩碼之外的結構來實現(xiàn)信號集。

  • 函數(shù) sigaddset() 將一個信號添加到已有的信號集中,sigdelset() 則從信號集中刪除一個信號。

  • sigismember() 函數(shù)用來測試信號 sig 是否是信號集 set 的成員。

四、信號屏蔽字 (Signal Mask)

4.1 基礎概念

每個進程都有一個信號屏蔽字(或稱信號掩碼,signal mask),它規(guī)定了當前要阻塞遞送到該進程的信號集。對于每種信號,屏蔽字中都有一位與之對應。對于某種信號,若其對應位被設置,則它當前是被阻塞的。進程可以調用 sigprocmask() 檢測或更改,或同時進行檢測和更改進程的信號屏蔽字。

向信號屏蔽字中添加信號的3種方式:

  • 當調用信號處理器 (signal handler) 時,可能會引發(fā)信號自動添加到信號屏蔽字中的行為,暫不作深入介紹。

  • 使用 sigaction() 函數(shù)建立信號處理器時,可以指定一組信號集,當調用該處理器時會將該信號集里的信號阻塞,暫不作深入介紹。

  • 使用sigprocmask()系統(tǒng)調用,可以隨時顯式地向信號屏蔽字中添加或移除信號。

先來了解 sigprocmask():

$ man 2 sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

相關知識點:

  • sigprocmask() 既可用于修改 進程的信號屏蔽字,也可用于獲取現(xiàn)有的屏蔽字,或者同時執(zhí)行這2個操作。

  • 參數(shù) how 指定了 sigprocmask() 該如何操作信號屏蔽字。

    • SIG_BLOCK: 將參數(shù) set 信號集內的信號添加到信號屏蔽字中;
    • SIG_UNBLOCK: 將參數(shù) set 信號集內的信號從信號屏蔽字中移除;
    • SIG_SETMASK: 將參數(shù) set 信號集賦給信號屏蔽字。
  • 若 set 參數(shù)不為空,則其指向一個 sigset_t 緩沖區(qū),用于返回之前的信號屏蔽字。

  • SUSv3 規(guī)定,如果有任何正在等待的信號 (pending signals) 因調用了 sigprocmask() 解除了鎖定,那么在此調用返回前至少會傳遞一次這些信號。

  • 系統(tǒng)將忽略試圖阻塞 SIGKILL 和 SIGSTOP 信號的請求。如果試圖阻塞這些信號,sigprocmask() 既不會予以關注,也不會產(chǎn)生錯誤。

  • 常見的使用方法:

sigset_t blockSet, prevMask;
sigemptyset(&blockSet);

/* 1. Block SIGINT, save previous signal mask */
sigaddset(&blockSet, SIGINT);
if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1)
    errExit("sigprocmask1");

/* 2. Code that should not be interrupted by SIGINT */

/* 3. Restore previous signal mask, unblocking SIGINT */
if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1)
    errExit("sigprocmask2");

4.2 實驗 demo

main() 函數(shù):

1> 為所有信號注冊同一個信號處理函數(shù),用于驗證信號集是否被成功屏蔽:

static void handler(int sig)
{
    if (sig == SIGINT)
        gotSigint = 1;
    else
        sigCnt[sig]++;
}

int main(int argc, char *argv[])
{
    int n, numSecs;
    sigset_t fullMask, emptyMask;

    printf("%s: PID is %ld\n", argv[0], (long) getpid());

    for (n = 1; n < NSIG; n++)
        (void) signal(n, handler); // UNSAFE
    ...
}

注意:siganl() 是不可靠的,這里為了簡化程序而采用該接口。

2> 初始化信號集,然后屏蔽所有信號:

sigfillset(&fullMask);
if (sigprocmask(SIG_SETMASK, &fullMask, NULL) == -1) {
    perror("sigprocmask");
    exit(EXIT_FAILURE);
}

printf("%s: sleeping for %d seconds\n", argv[0], numSecs);
sleep(numSecs);

先屏蔽所有的信號,然后睡眠。睡眠期間,進程無法響應除 SIGSTOP 和 SIGKILL 之外的任何信號。

3> 睡眠結束后,用空信號集來解除所有的信號屏蔽:

sigemptyset(&emptyMask);   /* Unblock all signals */
if (sigprocmask(SIG_SETMASK, &emptyMask, NULL) == -1) {
    perror("sigprocmask");
    exit(EXIT_FAILURE);
}

while (!gotSigint)  /* Loop until SIGINT caught */
        continue;

for (n = 1; n < NSIG; n++)
    if (sigCnt[n] != 0)
        printf("%s: signal %d caught %d time%s\n", argv[0], n,
                sigCnt[n], (sigCnt[n] == 1) ? "" : "s");

exit(EXIT_SUCCESS);
}

解除了對某個等待信號的屏蔽后,系統(tǒng)會立刻將該信號傳遞一次給進程。

打印信號集 printSigset():

void printSigset(FILE *of, const char *prefix, const sigset_t *sigset)
{
    int sig, cnt;

    cnt = 0;
    for (sig = 1; sig < NSIG; sig++) {
        if (sigismember(sigset, sig)) {
            cnt++;
            fprintf(of, "%s%d (%s)\n", prefix, sig, strsignal(sig));
        }
    }

    if (cnt == 0)
        fprintf(of, "%s<empty signal set>\n", prefix);
}

3. 運行效果:
屏蔽期間多次按下 ctrl + c (發(fā)送 SIGINT):

$ ./signal_set 5
./signal_set: PID is 18375
blocked:1 (Hangup)
blocked:2 (Interrupt)
blocked:3 (Quit)
...
blocked:64 (Real-time signal 30)
./signal_set: sleeping for 5 seconds
^C^C^Cblocked:<empty signal set>
./signal_set: signal 2 caught 1 time

在信號被屏蔽的 5 秒期間,連續(xù)按下 3 次 ctrl + c,所有信號都不會被處理。當過了 5 秒后,解除信號屏蔽,僅僅有一次 SIGINT 信號被成功地傳遞并處理。

五、等待中的信號 (Pending Signals)

如果某進程接受了一個該進程正在阻塞的信號,那么會將該信號填加到進程的等待信號集中。當解除對該信號的鎖定時,會隨之將信號傳遞給此進程。為了確定進程中處于等待狀態(tài)的是哪些信號,可以使用 sigpending()。

$ man 2 sigpending
NAME
       sigpending, rt_sigpending - examine pending signals
SYNOPSIS
       #include <signal.h>

       int sigpending(sigset_t *set);
DESCRIPTION
       sigpending() returns the set of signals that are pending for delivery to the calling thread (i.e., the signals
       which have been raised while blocked)
.  The mask of pending signals is returned in set.

sigpending() 為調用進程返回處于等待狀態(tài)的信號集,并將其置于 set 指向的sigset_t 中。

相關知識點:

  • 如果修改了對等待信號的處置 (術語disposition),那么當后來解除對信號的鎖定時,將根據(jù)新的處置來處理信號。

六、待處理的信號 (Pending Signals)

如果某進程接受了一個該進程正在阻塞 (blocking) 的信號,那么會將該信號填加到進程的 等待信號集 (set of pending signals) 中。當解除對該信號的阻塞時,會隨之將信號傳遞給此進程??梢允褂?sigpending() 確定進程中處于等待狀態(tài)的是哪些信號。

$ man 2 sigpending
    #include <signal.h>

    int sigpending(sigset_t *set);

sigpending() 為調用進程返回處于等待狀態(tài)的信號集,并將其置于參數(shù) set 指向的 sigset_t 中。

1. 一個簡單的例子 (sig_pending.c)

1) 分解代碼:
1> main():

int main(void)
{
 sigset_t newmask, oldmask, pendmask;

 if (signal(SIGQUIT, sig_quit) == SIG_ERR)
  err_sys("can't catch SIGQUIT");

 /* Block SIGQUIT and save current signal mask. */
 sigemptyset(&newmask);
 sigaddset(&newmask, SIGQUIT);
 if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
  err_sys("SIG_BLOCK error");

    /* SIGQUIT here will remain pending */
 sleep(5);

 if (sigpending(&pendmask) < 0)
  err_sys("sigpending error");
 if (sigismember(&pendmask, SIGQUIT))
  printf("\nSIGQUIT pending\n");

 /* Restore signal mask which unblocks SIGQUIT. */
 if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
  err_sys("SIG_SETMASK error");
 printf("SIGQUIT unblocked\n");

    /* SIGQUIT here will terminate with core file */
 sleep(5);

 exit(0);
}

main() 做了 5 件事:

  • 設置 SIGQUIT 的信號處理函數(shù);
  • 屏蔽 SIGQUIT;
  • 睡眠 5 秒,用于等待 SIGQUIT 信號;
  • 睡眠結束,檢測 SIGQUIT 是否處于 pending;
  • 解除屏蔽 SIGQUIT;

注意:在設置 SIGQUIT 為阻塞時,我們保存了老的屏蔽字。為了解除對該信號的阻塞,用老的屏蔽字重新設置了進程信號屏蔽字。另一種方法是用 SIG_UNBLOCK 使阻塞的信號不再阻塞。如果編寫一個可能由其他人使用的函數(shù),而且需要在函數(shù)中阻塞一個信號,則不能用 SIG_UNBLOCK 簡單地解除對此信號的阻塞,這是因為此函數(shù)的調用者在調用本函數(shù)之前可能也阻塞了此信號。

2> 信號處理函數(shù) sig_quit():

static void sig_quit(int signo)
{
 printf("caught SIGQUIT\n");
 if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
  err_sys("can't reset SIGQUIT");
}

2) 運行效果:

$ ./sig_pending 
^\                      // 按下 1 次 ctrl + \ (在5s之內)
SIGQUIT pending         // 從 sleep(5) 返回后
caught SIGQUIT          // 在信號處理程序中
SIGQUIT unblocked       // 從sigprocmask() 返回
^\Quit (core dumped)

2 個值得注意的點:

  • 信號處理函數(shù)是在 sigprocmask() unblock 信號返回之前被調用;

  • 用 signal() 設置信號處理函數(shù),信號被處理時,會將信號處置重置為其默認行為。要想在同一信號“再度光臨”時再次調用該信號處理器函數(shù),程序員必須在信號處理器內部調用signal(),以顯式重建處理器函數(shù),但是這種處理方式是不安全的,真實的項目里應使用 sigaction(),后續(xù)的文章會舉例講解。

、不對待處理的信號進行排隊處理

等待信號集只是一個掩碼,僅表明一個信號是否發(fā)生,而未表明其發(fā)生的次數(shù)。換言之,如果同一信號在阻塞狀態(tài)下產(chǎn)生多次,那么會將該信號記錄在等待信號集中,并在稍后僅傳遞一次。后面會介紹實時信號,對實時信號所采取的是隊列化管理。如果將某一實時信號的多個實例發(fā)送給一進程,那么將會多次傳遞該實時信號,暫不做深入介紹。

1. 仍是那個簡單的例子 (sig_pending.c)

為了降低學習難度,跟前面的 Pending Signals 章節(jié)使用同一個例子,修改一下測試步驟:

$ ./sig_pending 
^\^\^\                  // 按下 3 次 ctrl + \ (在5s之內)
SIGQUIT pending         // 從 sleep(5) 返回后
caught SIGQUIT          // 只調用了一次信號處理程序
SIGQUIT unblocked       // 從sigprocmask() 返回
^\Quit (core dumped)

第二次運行該程序時,在進程休眠期間產(chǎn)生了 3 次 SIGQUIT 信號,但是取消對該信號的阻塞后,系統(tǒng)只向進程傳送了一次 SIGQUIT,從中可以看出在 Linux 系統(tǒng)上沒有對信號進行排隊處理。

2. 查看 Linux 內核里 Signal Pending 相關的實現(xiàn) (非重點)

1) 相關數(shù)據(jù)結構
內核用 struct task_struct 來描述一個進程,struct task_struct 中信號相關的成員 (Linux-4.14):

<sched.h>
struct task_struct {
...
 /* Signal handlers: */
 struct signal_struct  *signal;
 struct sighand_struct  *sighand;
 sigset_t   blocked;
 sigset_t   real_blocked;
 /* Restored if set_restore_sigmask() was used: */
 sigset_t   saved_sigmask;
 struct sigpending  pending;
 unsigned long   sas_ss_sp;
 size_t    sas_ss_size;
 unsigned int   sas_ss_flags;
...
};

我們將注意力集中中 struct sigpending pending 上。struct sigpending pending 建立了一個鏈表,該鏈表包含了所有已經(jīng)產(chǎn)生、且有待內核處理的信號,其定義如下:

struct sigpending {
 struct list_head list;
 sigset_t signal;
};
  • 成員 struct list_head list 通過雙向鏈表管理所有待處理信號,每一種待處理的信號對應雙向鏈表中的 1 個 struct sigqueue 節(jié)點。

  • 成員 sigset_t signal 是位圖 (bit mask,或稱位掩碼),它指定了仍然有待處理的所有信號的編號。某 1 bit = 1 表示該 bit 對應的信號待處理。sigset_t 所包含的比特位數(shù)目要 >= 所支持的信號數(shù)目。因此,內核使用了 unsigned long 數(shù)組來定義該數(shù)據(jù)類型:

typedef struct {
 unsigned long sig[_NSIG_WORDS];
sigset_t;
  • struct sigqueue 的定義如下:
struct sigqueue {
 struct list_head list;
 int flags;
 siginfo_t info;
 ...
};
  • siginfo_t 用于保存信號的額外信息,暫時不用關心。

注意:在 struct sigpending 鏈表中,struct sigqueue 對應的是一種類型的待處理信號,而不是某一個具體的信號。

示意圖:

2) 信號的產(chǎn)生
當給進程發(fā)送一個信號時,這個信號可能來自內核,也可能來自另外一個進程。

內核里有多個 API 能產(chǎn)生信號,這些 API 最終都會調用 send_signal()。我們重點關注信號是何時被設置為 pending 狀態(tài)的。

linux/kernel/signal.c:

send_signal()
 __send_signal()
  struct sigqueue *q = __sigqueue_alloc();
  list_add_tail(&q->list, &pending->list); // 將待處理信號添加到 pending 鏈表中
  sigaddset(&pending->signal, sig); // 在位圖中將信號對應的 bit 置 1
  complete_signal(sig, t, group);
   signal_wake_up();

send_signal() 會分配一個新的 struct sigqueue 實例,然后為其填充信號的額外信息,并添加到目標進程的 sigpending 鏈表且設置位圖。

如果信號成功發(fā)送,沒有被阻塞,就可以用 signal_wake_up() 喚醒目標進程,使得調度器可以選擇目標進程運行。

3) 信號的傳遞:
這些知識放在這篇文章里已經(jīng)完全超綱了,如果將所有的細節(jié)都暴露出來會讓初學者感到極度的困惑。

所以,我們只邁出一小步,將僅剩的一點注意力集中在內核在執(zhí)行信號處理函數(shù)前是如何處理 pending 信號的。

在每次由內核態(tài)切換到用戶態(tài)時,內核都會進行信號處理,最終的效果就是調用 do_signal() 函數(shù)。

linux/kernel/signal.c:

do_signal()
 get_signal()
  dequeue_signal(current, &current->blocked, &ksig->info);
    handle_signal()
  signal_setup_done();
   signal_delivered();
  • dequeue_signal() 是關鍵點:
dequeue_signal()
 int sig = next_signal(pending, mask);
 collect_signal(sig, pending, info, resched_timer);
  sigdelset(&list->signal, sig); // 取消信號的 pending 狀態(tài)
  list_del_init(&first->list); // 刪除 pending 鏈表中的 struct sigqueue 節(jié)點
  copy_siginfo(info, &first->info);
  • handle_signal() 會操作進程在用戶態(tài)下的棧,使得在從內核態(tài)切換到用戶態(tài)之后運行信號處理程序,而不是正常的程序代碼。

  • do_signal() 返回時,信號處理函數(shù)就會被執(zhí)行。

、相關參考

  • 《Unix 環(huán)境高級編程-第10章 信號》
  • 《Linux/Unix 系統(tǒng)編程手冊-第20章 信號:基本概念》
  • 《Linux 系統(tǒng)編程-第10章 信號》
  • 《Linux 程序設計-第11章 進程和信號》
  • 《深入理解 Linux 內核 第11章 信號》
  • 《深入 Linux 內核架構 5.4.1信號》
  • 《Linux 內核源代碼情景分析 6.4信號

你和我各有一個蘋果,如果我們交換蘋果的話,我們還是只有一個蘋果。但當你和我各有一個想法,我們交換想法的話,我們就都有兩個想法了。如果你也對 嵌入式系統(tǒng)和開源軟件 感興趣,并且想和更多人互相交流學習的話,請關注我的公眾號:嵌入式系統(tǒng)磚家,一起來學習吧,無論是 關注或轉發(fā) ,還是賞賜,都是對作者莫大的支持,謝謝 各位的大拇指 ,祝工作順利,家庭和睦~


本文授權轉載自公眾號“嵌入式Hacker” ,作者吳偉東Jack


-END-




推薦閱讀



【01】為什么要使用二級指針?
【02】指針和引用有什么區(qū)別?分別什么時候引用?
【03】“懸空指針”和“野指針”究竟是什么意思?標準答案來了
【04】用指針實現(xiàn)高低位倒序,瘋了吧?
【05】再談指針:大佬給你撥開 C指針 的云霧


免責聲明:整理文章為傳播相關技術,版權歸原作者所有,如有侵權,請聯(lián)系刪除

免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

嵌入式ARM

掃描二維碼,關注更多精彩內容

本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內容真實性等。需要轉載請聯(lián)系該專欄作者,如若文章內容侵犯您的權益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或將催生出更大的獨角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉型技術解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關鍵字: 汽車 人工智能 智能驅動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質量流程IT總裁陶景文發(fā)表了演講。

關鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權最終是由生態(tài)的繁榮決定的。

關鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領增長 以科技創(chuàng)新為引領,提升企業(yè)核心競爭力 堅持高質量發(fā)展策略,塑強核心競爭優(yōu)勢...

關鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術學會聯(lián)合牽頭組建的NVI技術創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術創(chuàng)新聯(lián)...

關鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關鍵字: BSP 信息技術
關閉
關閉