当前位置:首页 > 公众号精选 > 嵌入式微处理器
[导读]人们很容易高估某个决定性时刻的重要性,也很容易低估每天进行微小改进的价值。以前我也以为大规模的成功需要大规模的行动,现在我不这么认为了。长期来看,由于复利效果,一点小小的改进就能产生惊人的变化。 还有一点值得注意的情况,大多数人有了家庭和子

人们很容易高估某个决定性时刻的重要性,也很容易低估每天进行微小改进的价值以前我也以为大规模的成功需要大规模的行动,现在我不这么认为了。长期来看,由于复利效果,一点小小的改进就能产生惊人的变化。

还有一点值得注意的情况,大多数人有了家庭和子女后,并且现在国内盛行加班文化,很难再集中精力能抽出大块的时间进行学习了,部分还能坚持学习的人几乎都是以牺牲睡眠时间为代价的,我个人不太认为这种做法,我始终认为有更合理健康的方法能形成一个工作、生活、学习、娱乐的有效循环,或许认识到 微进步 的重要性就是一个很好的开始吧。

本文就是我的微进步,欢迎阅读。

一、概述

信号有时被称为提供处理异步事件机制的软件中断,与硬件中断的相似之处在于打断了程序执行的正常流程,很多比较重要的应用程序都需处理信号。事件可以来自于系统外部,例如用户按下 Ctrl+C,或者来自程序或者内核的某些操作。作为一种进程间通信 (IPC) 的基本形式,进行可以给另一个进程发送信号。

信号很早就是 Unix 的一部分。随着时间的推移,信号有了很大的改进。比如在可靠性方面,之前的信号可能会出现丢失的情况。在功能方面,现在信号可以携带用户定义的附加信息。最初,不同的 Unix 系统对信号的修改,后来,POSIX 标准的到来挽救并且标准化了信号机制。

  • 用术语 raise 表示一个信号的产生,catch 表示接收到一个信号。

  • 事件的发生是异步的,程序对信号的处理也是异步的。

  • 信号可以被生成、捕获、响应或忽略。有两种信号不能被忽略: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.)

发送信号:

  • 如果想发送一个信号给进程,而该进程并不是当前的前台进程,就需要使用kill 命令。

  • kill 命令有一个有用的变体叫 killall,它可以给运行着某一命令的所有进程发送信号。

处理信号:
Unix 系统提供了两种方法来改变信号处置:signal() 和 sigaction()。signal()系统调用是设置信号处置的原始 API,所提供的接口比sigaction() 简单。另一方面,sigaction() 提供了 signal() 所不具备的功能。进一步而言,signal() 的行为在不同 Unix 实现间存在差异,这意味着对可移植性有所追求的程序绝不能使用此调用来建立信号处理函数 (signal handler)。故此,sigaction()是建立信号处理器的首选API。

由于可能会在许多老程序中看到 signal() 的应用,我们先了解如何用 signal() 函数来处理信号。

signal() 的定义:

$ man 2 signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 参数1 signum 指定希望修改 handler 的信号编号,参数2 handler,则指定信号抵达时所调用的 signal handler 函数的地址。

  • 成功,返回以前的信号处理函数;出错,返回 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!

相关要点:

  • 在信号处理函数中,调用如 printf 这样的函数是不安全的。一般的做法是:在信号处理函数中设置一个标志,然后在主程序中检查该标志,如需要就打印一条消息。

  • 如果想保留信号处理函数,让它继续响应用户的 Ctrl+C 组合键,我们就需要再次调用 signal 函数来重新建立它。这会使信号在一段时间内无法得到处理,这段时间从调用中断函数开始,到信号处理函数的重建为止。如果在这段时间内程序接收到第二个信号,它就会违背我们的意愿终止程序的运行。

  • 不推荐使用 signal 接口。之所以介绍它,是因为可能会在许多老程序中看到它的应用。更清晰、执行更可靠的函数: sigaction(),在所有的新程序中都应该使用这个函数,暂不做深入介绍。

二、发送信号

1. 如何发送信号

进程可以通过调用 kill 函数向包括它本身在内的其他进程发送一个信号。

kill():

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

把参数 sig 给指定的信号发送给由参数 pid 指定的进程号所指定的进程。

kill 调用会在失败时返回 -1 并设置 errno 变量,失败的原因:

  • 给定的信号无效(errno设置为EINVAL);

  • 发送进程权限不够(errno设置为EPERM);

  • 目标进程不存在(errno设置为ESRCH);

关于权限:
要想发送一个信号,发送进程必须拥有相应的权限,包括2种情况:

  • 两个进程必须拥有相同的用户 ID,即你只能发送信号给属于自己的进程;

  • 超级用户可以发送信号给任何进程;

2. 闹钟功能

进程可以通过调用 alarm() 函数在经过预定时间后发送一个 SIGALRM 信号。

alarm():

$ man 2 alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 在 seconds 秒之后发送一个 SIGALRM 信号。

  • 返回值是以前设置的闹钟时间的余留秒数,如果调用失败则返回 -1。

相关要点:

  • 由于处理的延时和时间调度的不确定性,实际闹钟时间将比预先安排的要稍微拖后一点儿。

  • 把参数 seconds 设置为 0 将取消所有已设置的闹钟请求。

  • 如果在接收到 SIGALRM 信号之前再次调用 alarm() 函数,则闹钟重新开始计时

  • 每个进程只能有一个闹钟时间。

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 秒后向其父进程发送一个 SIGALRM 信号。父进程在安排好捕获 SIGALRM 信号后暂停运行,直到接收到一个信号为止。

运行效果:

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

相关要点:

  • pause() 把程序的执行挂起直到有一个信号出现为止。使用信号并挂起程序的执行是 Unix 程序设计中的一个重要部分。

    $ man 2 pause
    #include <unistd.h>
    int pause(void);
  • 当它被一个信号中断时,将返回 -1(如果下一个接收到的信号没有导致程序终止的话)并把 errno 设置为 EINTR。

  • 更常见的方法是使用 sigsuspend() 函数,暂不做介绍。

  • 在信号处理函数中没有调用 printf,而是通过设置标志,然后在main函数中检查该标志来完成消息的输出。

  • 如果信号出现在系统调用的执行过程中会怎么样?

    • 一般只需要考虑“慢”系统调用,例如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回错误 EINTR。
      $ man 3 errno
      EINTR
      Interrupted function call (POSIX.1); see signal(7).
  • 如果你开始在自己的程序中使用信号,就需要注意一些系统调用会因为接收到了一个信号而失败。

  • 我们需要更健壮的信号接口:

    • 在编写程序中处理信号部分的代码时必须非常小心,因为在使用信号的程序中会出现各种各样的“竞态条件”。例如,如果想调用pause等待一个信号,可信号却出现在调用 pause() 之前,就会使程序无限期地等待一个不会发生的事件。

    • POSIX 标准推荐了一个更新和更健壮的信号编程接口:sigaction。

三、信号集 (Signal Set)

多个信号可使用一个称之为信号集的数据结构来表示,POSIX.1 定义了数据类型 sigset_t 以表示一个信号集,并且定义了下列 5 个处理信号集的函数:

$ 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);
  • 函数 sigemptyset() 初始化由参数 set 指向的信号集,清除其中所有信号。

  • 函数 sigfillset() 初始化由参数 set 指向的信号集,使其包括所有信号。

  • 必须使用 sigemptyset() 或者 sigfillset() 来初始化信号集。这是因为 C 语言不会对自动变量进行初始化,并且,借助于将静态变量初始化为 0 的机制来表示空信号集的作法在可移植性上存在问题,因为有可能使用位掩码之外的结构来实现信号集。

  • 函数 sigaddset() 将一个信号添加到已有的信号集中,sigdelset() 则从信号集中删除一个信号。

  • sigismember() 函数用来测试信号 sig 是否是信号集 set 的成员。

四、信号屏蔽字 (Signal Mask)

4.1 基础概念

每个进程都有一个信号屏蔽字(或称信号掩码,signal mask),它规定了当前要阻塞递送到该进程的信号集。对于每种信号,屏蔽字中都有一位与之对应。对于某种信号,若其对应位被设置,则它当前是被阻塞的。进程可以调用 sigprocmask() 检测或更改,或同时进行检测和更改进程的信号屏蔽字。

向信号屏蔽字中添加信号的3种方式:

  • 当调用信号处理器 (signal handler) 时,可能会引发信号自动添加到信号屏蔽字中的行为,暂不作深入介绍。

  • 使用 sigaction() 函数建立信号处理器时,可以指定一组信号集,当调用该处理器时会将该信号集里的信号阻塞,暂不作深入介绍。

  • 使用sigprocmask()系统调用,可以随时显式地向信号屏蔽字中添加或移除信号。

先来了解 sigprocmask():

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

相关知识点:

  • sigprocmask() 既可用于修改 进程的信号屏蔽字,也可用于获取现有的屏蔽字,或者同时执行这2个操作。

  • 参数 how 指定了 sigprocmask() 该如何操作信号屏蔽字。

    • SIG_BLOCK: 将参数 set 信号集内的信号添加到信号屏蔽字中;
    • SIG_UNBLOCK: 将参数 set 信号集内的信号从信号屏蔽字中移除;
    • SIG_SETMASK: 将参数 set 信号集赋给信号屏蔽字。
  • 若 set 参数不为空,则其指向一个 sigset_t 缓冲区,用于返回之前的信号屏蔽字。

  • SUSv3 规定,如果有任何正在等待的信号 (pending signals) 因调用了 sigprocmask() 解除了锁定,那么在此调用返回前至少会传递一次这些信号。

  • 系统将忽略试图阻塞 SIGKILL 和 SIGSTOP 信号的请求。如果试图阻塞这些信号,sigprocmask() 既不会予以关注,也不会产生错误。

  • 常见的使用方法:

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() 函数:

1> 为所有信号注册同一个信号处理函数,用于验证信号集是否被成功屏蔽:

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

解除了对某个等待信号的屏蔽后,系统会立刻将该信号传递一次给进程。

打印信号集 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 (发送 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 秒期间,连续按下 3 次 ctrl + c,所有信号都不会被处理。当过了 5 秒后,解除信号屏蔽,仅仅有一次 SIGINT 信号被成功地传递并处理。

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

如果某进程接受了一个该进程正在阻塞的信号,那么会将该信号填加到进程的等待信号集中。当解除对该信号的锁定时,会随之将信号传递给此进程。为了确定进程中处于等待状态的是哪些信号,可以使用 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() 为调用进程返回处于等待状态的信号集,并将其置于 set 指向的sigset_t 中。

相关知识点:

  • 如果修改了对等待信号的处置 (术语disposition),那么当后来解除对信号的锁定时,将根据新的处置来处理信号。

六、待处理的信号 (Pending Signals)

如果某进程接受了一个该进程正在阻塞 (blocking) 的信号,那么会将该信号填加到进程的 等待信号集 (set of pending signals) 中。当解除对该信号的阻塞时,会随之将信号传递给此进程。可以使用 sigpending() 确定进程中处于等待状态的是哪些信号。

$ man 2 sigpending
    #include <signal.h>

    int sigpending(sigset_t *set);

sigpending() 为调用进程返回处于等待状态的信号集,并将其置于参数 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 的信号处理函数;
  • 屏蔽 SIGQUIT;
  • 睡眠 5 秒,用于等待 SIGQUIT 信号;
  • 睡眠结束,检测 SIGQUIT 是否处于 pending;
  • 解除屏蔽 SIGQUIT;

注意:在设置 SIGQUIT 为阻塞时,我们保存了老的屏蔽字。为了解除对该信号的阻塞,用老的屏蔽字重新设置了进程信号屏蔽字。另一种方法是用 SIG_UNBLOCK 使阻塞的信号不再阻塞。如果编写一个可能由其他人使用的函数,而且需要在函数中阻塞一个信号,则不能用 SIG_UNBLOCK 简单地解除对此信号的阻塞,这是因为此函数的调用者在调用本函数之前可能也阻塞了此信号。

2> 信号处理函数 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 个值得注意的点:

  • 信号处理函数是在 sigprocmask() unblock 信号返回之前被调用;

  • 用 signal() 设置信号处理函数,信号被处理时,会将信号处置重置为其默认行为。要想在同一信号“再度光临”时再次调用该信号处理器函数,程序员必须在信号处理器内部调用signal(),以显式重建处理器函数,但是这种处理方式是不安全的,真实的项目里应使用 sigaction(),后续的文章会举例讲解。

、不对待处理的信号进行排队处理

等待信号集只是一个掩码,仅表明一个信号是否发生,而未表明其发生的次数。换言之,如果同一信号在阻塞状态下产生多次,那么会将该信号记录在等待信号集中,并在稍后仅传递一次。后面会介绍实时信号,对实时信号所采取的是队列化管理。如果将某一实时信号的多个实例发送给一进程,那么将会多次传递该实时信号,暂不做深入介绍。

1. 仍是那个简单的例子 (sig_pending.c)

为了降低学习难度,跟前面的 Pending Signals 章节使用同一个例子,修改一下测试步骤:

$ ./sig_pending 
^\^\^\                  // 按下 3 次 ctrl + \ (在5s之内)
SIGQUIT pending         // 从 sleep(5) 返回后
caught SIGQUIT          // 只调用了一次信号处理程序
SIGQUIT unblocked       // 从sigprocmask() 返回
^\Quit (core dumped)

第二次运行该程序时,在进程休眠期间产生了 3 次 SIGQUIT 信号,但是取消对该信号的阻塞后,系统只向进程传送了一次 SIGQUIT,从中可以看出在 Linux 系统上没有对信号进行排队处理。

2. 查看 Linux 内核里 Signal Pending 相关的实现 (非重点)

1) 相关数据结构
内核用 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 建立了一个链表,该链表包含了所有已经产生、且有待内核处理的信号,其定义如下:

struct sigpending {
 struct list_head list;
 sigset_t signal;
};
  • 成员 struct list_head list 通过双向链表管理所有待处理信号,每一种待处理的信号对应双向链表中的 1 个 struct sigqueue 节点。

  • 成员 sigset_t signal 是位图 (bit mask,或称位掩码),它指定了仍然有待处理的所有信号的编号。某 1 bit = 1 表示该 bit 对应的信号待处理。sigset_t 所包含的比特位数目要 >= 所支持的信号数目。因此,内核使用了 unsigned long 数组来定义该数据类型:

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) 信号的产生
当给进程发送一个信号时,这个信号可能来自内核,也可能来自另外一个进程。

内核里有多个 API 能产生信号,这些 API 最终都会调用 send_signal()。我们重点关注信号是何时被设置为 pending 状态的。

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 链表且设置位图。

如果信号成功发送,没有被阻塞,就可以用 signal_wake_up() 唤醒目标进程,使得调度器可以选择目标进程运行。

3) 信号的传递:
这些知识放在这篇文章里已经完全超纲了,如果将所有的细节都暴露出来会让初学者感到极度的困惑。

所以,我们只迈出一小步,将仅剩的一点注意力集中在内核在执行信号处理函数前是如何处理 pending 信号的。

在每次由内核态切换到用户态时,内核都会进行信号处理,最终的效果就是调用 do_signal() 函数。

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 状态
  list_del_init(&first->list); // 删除 pending 链表中的 struct sigqueue 节点
  copy_siginfo(info, &first->info);
  • handle_signal() 会操作进程在用户态下的栈,使得在从内核态切换到用户态之后运行信号处理程序,而不是正常的程序代码。

  • do_signal() 返回时,信号处理函数就会被执行。

、相关参考

  • 《Unix 环境高级编程-第10章 信号》
  • 《Linux/Unix 系统编程手册-第20章 信号:基本概念》
  • 《Linux 系统编程-第10章 信号》
  • 《Linux 程序设计-第11章 进程和信号》
  • 《深入理解 Linux 内核 第11章 信号》
  • 《深入 Linux 内核架构 5.4.1信号》
  • 《Linux 内核源代码情景分析 6.4信号

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。如果你也对 嵌入式系统和开源软件 感兴趣,并且想和更多人互相交流学习的话,请关注我的公众号:嵌入式系统砖家,一起来学习吧,无论是 关注或转发 ,还是赏赐,都是对作者莫大的支持,谢谢 各位的大拇指 ,祝工作顺利,家庭和睦~


本文授权转载自公众号“嵌入式Hacker” ,作者吴伟东Jack


-END-




推荐阅读



【01】为什么要使用二级指针?
【02】指针和引用有什么区别?分别什么时候引用?
【03】“悬空指针”和“野指针”究竟是什么意思?标准答案来了
【04】用指针实现高低位倒序,疯了吧?
【05】再谈指针:大佬给你拨开 C指针 的云雾


免责声明:整理文章为传播相关技术,版权归原作者所有,如有侵权,请联系删除

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

嵌入式ARM

扫描二维码,关注更多精彩内容

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

上海2023年9月4日 /美通社/ -- 2023年8月8日,成都大运会正式落幕。来自113个国家和地区的6500名大学生运动员,在12个比赛日里同台竞技,展现青春与体育的力量。与此同时,在大运会的各个赛场、运动员村以及...

关键字: 信号 TV PS CE

瓦努阿图维拉港2023年8月28日 /美通社/ -- Vantage(或"Vantage Markets")欣然宣布在Vantage应用程序上推...

关键字: GE AN 信号 应用程序

(全球TMT2023年7月19日讯)三星电子宣布已完成其业内首款GDDR7的研发工作,年内将首先搭载于主要客户的下一代系统上验证。继2022年三星开发出速度为每秒24千兆比特(Gbps)的GDDR6 16Gb之后,GD...

关键字: DDR 三星电子 信号 GBPS

频谱分析仪是用于分析信号频谱结构的设备,可以用于测量信号的频率、幅度、功率、谱线宽度等参数,是现代电子测量和通信领域中不可或缺的工具。

关键字: 频谱分析仪 信号

频谱分析仪是用于分析信号的频率成分和功率的电子设备。分辨率是频谱分析仪的一个重要指标,它决定了频谱分析仪能够分辨的信号细节和频谱特征。在频谱分析仪的使用过程中,分辨率受到多种因素的影响,这些因素包括信号频率、信号强度、分...

关键字: 频谱分析仪 信号 分辨率

频谱分析仪是用于测量信号频率、幅度和功率谱等特性的重要工具,广泛应用于电子工程、通信、生物医学等领域。本文将介绍频谱分析仪的基本原理、使用方法和技巧,帮助读者更好地理解和使用频谱分析仪。

关键字: 频谱分析仪 信号

频谱分析仪是一种用于分析信号频率成分的重要仪器,能够在复杂信号中识别出各个频率成分,以及它们在不同时间段的强度和带宽。在科学研究、工业生产、通信网络、电子对抗等领域,频谱分析仪发挥着重要作用。

关键字: 频谱分析仪 信号

脉冲变压器是一种用于将电源信号转换为所需电压和电流的特殊变压器。它通常被用于直流电源或高频交流电源中,可用于稳压、反接保护、防过载等应用。当我们需要选型脉冲变压器时,需考虑以下几个方面:输出电压、最大输出电流、工作频率、...

关键字: 脉冲变压器 信号 电源

集成运算放大器是一种常见的电子元器件,它广泛应用于模拟电路、信号处理、控制系统等领域。集成运算放大器主要的功能是放大电压信号。它可以将微弱的输入信号放大成为符合实际需要的信号大小,同时也可以对信号进行滤波、积分、微分等操...

关键字: 运算放大器 电路 信号

Matlab和C语言的区别是:1、用途不同;2、语法不同;3、运行速度不同;4、可移植性不同;5、代码管理不同。Matlab是一种数值计算和科学计算工具

关键字: matlab语言 C语言 系统编程
关闭
关闭