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

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

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

本文就是我的微進(jìn)步,歡迎閱讀。

一、概述

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

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

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

  • 事件的發(fā)生是異步的,程序?qū)π盘柕奶幚硪彩钱惒降摹?/p>

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

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ā)送一個信號給進(jìn)程,而該進(jìn)程并不是當(dāng)前的前臺進(jìn)程,就需要使用kill 命令。

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

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

由于可能會在許多老程序中看到 signal() 的應(yīng)用,我們先了解如何用 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,則指定信號抵達(dá)時所調(diào)用的 signal handler 函數(shù)的地址。

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

2. 入門實(shí)驗(yàn)

簡單試用 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);
    }
}

運(yùn)行效果:

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

相關(guān)要點(diǎn):

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

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

  • 不推薦使用 signal 接口。之所以介紹它,是因?yàn)榭赡軙谠S多老程序中看到它的應(yīng)用。更清晰、執(zhí)行更可靠的函數(shù): sigaction(),在所有的新程序中都應(yīng)該使用這個函數(shù),暫不做深入介紹。

二、發(fā)送信號

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

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

kill():

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

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

kill 調(diào)用會在失敗時返回 -1 并設(shè)置 errno 變量,失敗的原因:

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

  • 發(fā)送進(jìn)程權(quán)限不夠(errno設(shè)置為EPERM);

  • 目標(biāo)進(jìn)程不存在(errno設(shè)置為ESRCH);

關(guān)于權(quán)限:
要想發(fā)送一個信號,發(fā)送進(jìn)程必須擁有相應(yīng)的權(quán)限,包括2種情況:

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

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

2. 鬧鐘功能

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

alarm():

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

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

相關(guān)要點(diǎn):

  • 由于處理的延時和時間調(diào)度的不確定性,實(shí)際鬧鐘時間將比預(yù)先安排的要稍微拖后一點(diǎn)兒。

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

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

  • 每個進(jìn)程只能有一個鬧鐘時間。

3. 入門實(shí)驗(yàn)

用 kill() 模擬鬧鐘。

分解代碼:
設(shè)置 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 調(diào)用啟動新的進(jìn)程:子進(jìn)程休眠 5 秒后向其父進(jìn)程發(fā)送一個 SIGALRM 信號。父進(jìn)程在安排好捕獲 SIGALRM 信號后暫停運(yùn)行,直到接收到一個信號為止。

運(yùn)行效果:

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

相關(guān)要點(diǎn):

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

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

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

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

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

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

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

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

    • POSIX 標(biāo)準(zhǔn)推薦了一個更新和更健壯的信號編程接口:sigaction。

三、信號集 (Signal Set)

多個信號可使用一個稱之為信號集的數(shù)據(jù)結(jié)構(gòu)來表示,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() 來初始化信號集。這是因?yàn)?C 語言不會對自動變量進(jìn)行初始化,并且,借助于將靜態(tài)變量初始化為 0 的機(jī)制來表示空信號集的作法在可移植性上存在問題,因?yàn)橛锌赡苁褂梦谎诖a之外的結(jié)構(gòu)來實(shí)現(xiàn)信號集。

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

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

四、信號屏蔽字 (Signal Mask)

4.1 基礎(chǔ)概念

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

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

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

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

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

先來了解 sigprocmask():

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

相關(guān)知識點(diǎn):

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

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

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

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

  • 系統(tǒng)將忽略試圖阻塞 SIGKILL 和 SIGSTOP 信號的請求。如果試圖阻塞這些信號,sigprocmask() 既不會予以關(guān)注,也不會產(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 實(shí)驗(yàn) demo

main() 函數(shù):

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

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);

先屏蔽所有的信號,然后睡眠。睡眠期間,進(jìn)程無法響應(yīng)除 SIGSTOP 和 SIGKILL 之外的任何信號。

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

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)會立刻將該信號傳遞一次給進(jìn)程。

打印信號集 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. 運(yùn)行效果:
屏蔽期間多次按下 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,所有信號都不會被處理。當(dāng)過了 5 秒后,解除信號屏蔽,僅僅有一次 SIGINT 信號被成功地傳遞并處理。

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

如果某進(jìn)程接受了一個該進(jìn)程正在阻塞的信號,那么會將該信號填加到進(jìn)程的等待信號集中。當(dāng)解除對該信號的鎖定時,會隨之將信號傳遞給此進(jìn)程。為了確定進(jìn)程中處于等待狀態(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() 為調(diào)用進(jìn)程返回處于等待狀態(tài)的信號集,并將其置于 set 指向的sigset_t 中。

相關(guān)知識點(diǎn):

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

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

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

$ man 2 sigpending
    #include <signal.h>

    int sigpending(sigset_t *set);

sigpending() 為調(diào)用進(jìn)程返回處于等待狀態(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 件事:

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

注意:在設(shè)置 SIGQUIT 為阻塞時,我們保存了老的屏蔽字。為了解除對該信號的阻塞,用老的屏蔽字重新設(shè)置了進(jìn)程信號屏蔽字。另一種方法是用 SIG_UNBLOCK 使阻塞的信號不再阻塞。如果編寫一個可能由其他人使用的函數(shù),而且需要在函數(shù)中阻塞一個信號,則不能用 SIG_UNBLOCK 簡單地解除對此信號的阻塞,這是因?yàn)榇撕瘮?shù)的調(diào)用者在調(diào)用本函數(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) 運(yùn)行效果:

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

2 個值得注意的點(diǎn):

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

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

、不對待處理的信號進(jìn)行排隊(duì)處理

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

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

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

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

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

2. 查看 Linux 內(nèi)核里 Signal Pending 相關(guān)的實(shí)現(xiàn) (非重點(diǎn))

1) 相關(guān)數(shù)據(jù)結(jié)構(gòu)
內(nèi)核用 struct task_struct 來描述一個進(jìn)程,struct task_struct 中信號相關(guān)的成員 (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)生、且有待內(nèi)核處理的信號,其定義如下:

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

  • 成員 sigset_t signal 是位圖 (bit mask,或稱位掩碼),它指定了仍然有待處理的所有信號的編號。某 1 bit = 1 表示該 bit 對應(yīng)的信號待處理。sigset_t 所包含的比特位數(shù)目要 >= 所支持的信號數(shù)目。因此,內(nèi)核使用了 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 用于保存信號的額外信息,暫時不用關(guān)心。

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

示意圖:

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

內(nèi)核里有多個 API 能產(chǎn)生信號,這些 API 最終都會調(diào)用 send_signal()。我們重點(diǎn)關(guān)注信號是何時被設(shè)置為 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); // 在位圖中將信號對應(yīng)的 bit 置 1
  complete_signal(sig, t, group);
   signal_wake_up();

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

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

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

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

在每次由內(nèi)核態(tài)切換到用戶態(tài)時,內(nèi)核都會進(jìn)行信號處理,最終的效果就是調(diào)用 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() 是關(guān)鍵點(diǎn):
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é)點(diǎn)
  copy_siginfo(info, &first->info);
  • handle_signal() 會操作進(jìn)程在用戶態(tài)下的棧,使得在從內(nèi)核態(tài)切換到用戶態(tài)之后運(yùn)行信號處理程序,而不是正常的程序代碼。

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

、相關(guān)參考

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

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


本文授權(quán)轉(zhuǎn)載自公眾號“嵌入式Hacker” ,作者吳偉東Jack


-END-




推薦閱讀



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


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

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

嵌入式ARM

掃描二維碼,關(guān)注更多精彩內(nèi)容

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

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

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

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

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

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

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

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

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

關(guān)鍵字: 騰訊 編碼器 CPU

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

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

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

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

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

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

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

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

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

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉