Linux系统编程-信号入门

嵌入式ARM 2020-07-08 00:00

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

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

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

一、概述

信号有时被称为提供处理异步事件机制的软件中断,与硬件中断的相似之处在于打断了程序执行的正常流程,很多比较重要的应用程序都需处理信号。事件可以来自于系统外部,例如用户按下 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指针 的云雾


免责声明:整理文章为传播相关技术,版权归原作者所有,如有侵权,请联系删除
嵌入式ARM 关注这个时代最火的嵌入式ARM,你想知道的都在这里。
评论
  • Matter加持:新世代串流装置如何改变智能家居体验?随着现在智能家庭快速成长,串流装置(Streaming Device,以下简称Streaming Device)除了提供更卓越的影音体验,越来越多厂商开始推出支持Matter标准的串流产品,使其能作为智能家庭中枢,连结多种智能家电。消费者可以透过Matter的功能执行多样化功能,例如:开关灯、控制窗帘、对讲机开门,以及操作所有支持Matter的智能家电。此外,再搭配语音遥控器与语音助理,打造出一个更加智能、便捷的居家生活。支持Matter协议
    百佳泰测试实验室 2025-01-03 10:29 141浏览
  • 本文继续介绍Linux系统查看硬件配置及常用调试命令,方便开发者快速了解开发板硬件信息及进行相关调试。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。查看系统版本信息查看操作系统版本信息root@ido:/# cat /etc/*releaseDISTRIB_ID=UbuntuDISTRIB_RELEASE=20.04DISTRIB_CODENAME=focalDIS
    Industio_触觉智能 2025-01-03 11:37 137浏览
  • 光耦合器,也称为光隔离器,是一种利用光在两个隔离电路之间传输电信号的组件。在医疗领域,确保患者安全和设备可靠性至关重要。在众多有助于医疗设备安全性和效率的组件中,光耦合器起着至关重要的作用。这些紧凑型设备经常被忽视,但对于隔离高压和防止敏感医疗设备中的电气危害却是必不可少的。本文深入探讨了光耦合器的功能、其在医疗应用中的重要性以及其实际使用示例。什么是光耦合器?它通常由以下部分组成:LED(发光二极管):将电信号转换为光。光电探测器(例如光电晶体管):检测光并将其转换回电信号。这种布置确保输入和
    腾恩科技-彭工 2025-01-03 16:27 155浏览
  • 前言近年来,随着汽车工业的快速发展,尤其是新能源汽车与智能汽车领域的崛起,汽车安全标准和认证要求日益严格,应用范围愈加广泛。ISO 26262和ISO 21448作为两个重要的汽车安全标准,它们在“系统安全”中扮演的角色各自不同,但又有一定交集。在智能网联汽车的高级辅助驾驶系统(ADAS)应用中,理解这两个标准的区别及其相互关系,对于保障车辆的安全性至关重要。ISO 26262:汽车功能安全的基石如图2.1所示,ISO 26262对“功能安全”的定义解释为:不存在由于电子/电气系统失效引起的危害
    广电计量 2025-01-02 17:18 218浏览
  • 在快速发展的能源领域,发电厂是发电的支柱,效率和安全性至关重要。在这种背景下,国产数字隔离器已成为现代化和优化发电厂运营的重要组成部分。本文探讨了这些设备在提高性能方面的重要性,同时展示了中国在生产可靠且具有成本效益的数字隔离器方面的进步。什么是数字隔离器?数字隔离器充当屏障,在电气上将系统的不同部分隔离开来,同时允许无缝数据传输。在发电厂中,它们保护敏感的控制电路免受高压尖峰的影响,确保准确的信号处理,并在恶劣条件下保持系统完整性。中国国产数字隔离器经历了重大创新,在许多方面达到甚至超过了全球
    克里雅半导体科技 2025-01-03 16:10 117浏览
  • 物联网(IoT)的快速发展彻底改变了从智能家居到工业自动化等各个行业。由于物联网系统需要高效、可靠且紧凑的组件来处理众多传感器、执行器和通信设备,国产固态继电器(SSR)已成为满足中国这些需求的关键解决方案。本文探讨了国产SSR如何满足物联网应用的需求,重点介绍了它们的优势、技术能力以及在现实场景中的应用。了解物联网中的固态继电器固态继电器是一种电子开关设备,它使用半导体而不是机械触点来控制负载。与传统的机械继电器不同,固态继电器具有以下优势:快速切换:确保精确快速的响应,这对于实时物联网系统至
    克里雅半导体科技 2025-01-03 16:11 164浏览
  • 国际标准IPC 标准:IPC-A-600:规定了印刷电路板制造过程中的质量要求和验收标准,涵盖材料、外观、尺寸、焊接、表面处理等方面。IPC-2221/2222:IPC-2221 提供了用于设计印刷电路板的一般原则和要求,IPC-2222 则针对高可靠性电子产品的设计提供了进一步的指导。IPC-6012:详细定义了刚性基板和柔性基板的要求,包括材料、工艺、尺寸、层次结构、特征等。IPC-4101:定义了印刷电路板的基板材料的物理和电气特性。IPC-7351:提供了元件封装的设计规范,包括封装尺寸
    Jeffreyzhang123 2025-01-02 16:50 198浏览
  • 从无到有:智能手机的早期探索无线电话装置的诞生:1902 年,美国人内森・斯塔布菲尔德在肯塔基州制成了第一个无线电话装置,这是人类对 “手机” 技术最早的探索。第一部移动手机问世:1938 年,美国贝尔实验室为美国军方制成了世界上第一部 “移动” 手机。民用手机的出现:1973 年 4 月 3 日,摩托罗拉工程师马丁・库珀在纽约曼哈顿街头手持世界上第一台民用手机摩托罗拉 DynaTAC 8000X 的原型机,给竞争对手 AT&T 公司的朋友打了一个电话。这款手机重 2 磅,通话时间仅能支持半小时
    Jeffreyzhang123 2025-01-02 16:41 167浏览
  • 车身域是指负责管理和控制汽车车身相关功能的一个功能域,在汽车域控系统中起着至关重要的作用。它涵盖了车门、车窗、车灯、雨刮器等各种与车身相关的功能模块。与汽车电子电气架构升级相一致,车身域发展亦可以划分为三个阶段,功能集成愈加丰富:第一阶段为分布式架构:对应BCM车身控制模块,包含灯光、雨刮、门窗等传统车身控制功能。第二阶段为域集中架构:对应BDC/CEM域控制器,在BCM基础上集成网关、PEPS等。第三阶段为SOA理念下的中央集中架构:VIU/ZCU区域控制器,在BDC/CEM基础上集成VCU、
    北汇信息 2025-01-03 16:01 173浏览
  • 在测试XTS时会遇到修改产品属性、SElinux权限、等一些内容,修改源码再编译很费时。今天为大家介绍一个便捷的方法,让OpenHarmony通过挂载镜像来修改镜像内容!触觉智能Purple Pi OH鸿蒙开发板演示。搭载了瑞芯微RK3566四核处理器,树莓派卡片电脑设计,支持开源鸿蒙OpenHarmony3.2-5.0系统,适合鸿蒙开发入门学习。挂载镜像首先,将要修改内容的镜像传入虚拟机当中,并创建一个要挂载镜像的文件夹,如下图:之后通过挂载命令将system.img镜像挂载到sys
    Industio_触觉智能 2025-01-03 11:39 113浏览
  • 影像质量应用于多个不同领域,无论是在娱乐、医疗或工业应用中,高质量的影像都是决策的关键基础。清晰的影像不仅能提升观看体验,还能保证关键细节的准确传达,例如:在医学影像中,它对诊断结果有着直接的影响!不仅如此,影像质量还影响了:▶ 压缩技术▶ 存储需求▶ 传输效率随着技术进步,影像质量的标准不断提高,对于研究与开发领域,理解并提升影像质量已成为不可忽视的重要课题。在图像处理的过程中,硬件与软件除了各自扮演着不可或缺的基础角色,有效地协作能够确保图像处理过程既高效又具有优异的质量。软硬件各扮演了什么
    百佳泰测试实验室 2025-01-03 10:39 137浏览
  • 【工程师故事】+半年的经历依然忧伤,带着焦虑和绝望  对于一个企业来说,赚钱才是第一位的,对于一个人来说,赚钱也是第一位的。因为企业要活下去,因为个人也要活下去。企业打不了倒闭。个人还是要吃饭的。企业倒闭了,打不了从头再来。个人失业了,面对的不仅是房贷车贷和教育,还有找工作的焦虑。企业说,一个公司倒闭了,说明不了什么,这是正常的一个现象。个人说,一个中年男人失业了,面对的压力太大了,焦虑会摧毁你的一切。企业说,是个公司倒闭了,也不是什么大的问题,只不过是这些公司经营有问题吧。
    curton 2025-01-02 23:08 289浏览
  • 自动化已成为现代制造业的基石,而驱动隔离器作为关键组件,在提升效率、精度和可靠性方面起到了不可或缺的作用。随着工业技术不断革新,驱动隔离器正助力自动化生产设备适应新兴趋势,并推动行业未来的发展。本文将探讨自动化的核心趋势及驱动隔离器在其中的重要角色。自动化领域的新兴趋势智能工厂的崛起智能工厂已成为自动化生产的新标杆。通过结合物联网(IoT)、人工智能(AI)和机器学习(ML),智能工厂实现了实时监控和动态决策。驱动隔离器在其中至关重要,它确保了传感器、执行器和控制单元之间的信号完整性,同时提供高
    腾恩科技-彭工 2025-01-03 16:28 159浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦