Linux信号机制

信号的基本概念

信号可以理解为一种针对用户程序的软件中断, 可以用于异步通信. 信号的产生有多种方式:

  1. 进程调用kill系统调用向指定进程发信号
  2. 内核通过send_signal等函数向指定进程发信号

LInux支持0-64个信号, 其中0-31号信号为标准信号, 又称不可靠信号, 因为如果此时进程的信号处理队列中存在了某个信号, 当这个信号这时再次发生时, 则会被忽略;32-64为可靠信号, 所有收到的信号都会被处理。

image-20240110110129566
  • 信号的属性

阻塞: 如果一个信号被阻塞, 它还是会加入到进程的信号处理队列中, 但在没有取消阻塞前, 不会被处理。一旦取消阻塞, 就会被处理。所以又称为暂时屏蔽。通过sigaction和sigprocmask函数可以设定信号的阻塞。

忽略: 被忽略的信号, 还是会被处理, 不过信号处理函数是空的。

信号的处理流程

要处理信号,首先要知道有哪些信号要处理。下面的数据结构表示未决信号的集合,也就是还没有被处理的信号集合。

struct sigpending pending;
struct sigpending{
    struct sigqueue *head, **tail;
    sigset_t signal;
};
struct sigqueue{
    struct sigqueue *next;
    siginfo_t info;
}
  • sigset_t signal:未决信号集
  • sigqueue:一个链表,表示未决信号链

信号的注册:将信号加入到未决信号集sigset_t 中,并加入到未决信号链sigqueue。对于不可靠信号,如果未决信号集中已经存在该信号,那么就不会加入到未决信号链中;对于可靠信号,不管未决信号集中是否存在该信号,都会加入到未决信号链,因此这就是为什么可靠信号不会丢失了。

信号的处理时机:信号是在进程将要返回用户空间之前进行处理的, 可能是从系统调用返回, 也可能是从中断返回,返回之前,进程会检查未决信号集合,如果有信号,且没有被阻塞,那么就会调用对应的信号处理函数来处理。

信号的注销: 在要处理一个信号前,也就是在调用信号处理函数之前,进程会把信号在未决信号链中的表项删除。对于可靠信号来说,只会删除一个表项,如果删除后未决信号链中不存在该信号了,则从未决信号集中删除该信号,否则不处理未决信号集;对于不可靠信号,未决信号链肯定只有一个对应的表项,需要从未决信号链和未决信号集中都删除。

信号处理函数完成后:信号处理函数结束时, 会进入内核态, 再次检查是否有信号要处理。如果没有, 则恢复原来用户态的上下文。

用户态设置信号处理函数

设置信号处理方式的接口函数有两个,signal和sigaction。signal是早期的设置函数,适用于标准信号,比较简单。sigaction是后来新增的接口函数,功能比较强大,适用于实时信号,当然也可以用于标准信号。

  • signal()函数
sighandler_t signal(int signum, sighandler_t handler);

signal有两个参数,第一个是信号数值,第二个是信号处理函数。信号处理函数的接口如下所示:

typedef void (*sighandler_t)(int);

第二个参数可以传递特殊值SIG_IGN,代表忽略这个信号,还可以传递特殊值SIG_DFL,代表恢复信号的默认处理方式。

  • sigaction()函数
int sigaction(int signum, const struct sigaction *restrict act, struct sigaction *restrict oldact);

第一个参数是信号数值,第二个参数是要设置的情况,第三个参数会返回旧的设置情况,可以为NULL. 其中sigaction结构体为:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);// 
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

前2个参数为信号处理函数指针, 如果在字段sa_flags里面设置SA_SIGINFO的话就使用sa_sigaction, 否则是sa_handler.

sa_mask: 信号处理函数在执行时阻塞哪些信号。这里的阻塞指的是当信号处理函数执行过程中, 发生trap到内核并返回时,内核不会执行这些信号的处理函数, 而是继续执行当前的信号处理函数. 然而, 这些被阻塞的信号仍然会加入到进程的未决信号队列中。当前信号处理函数执行完后, 这些被阻塞的信号还会被处理.

信号的屏蔽

通过以下的函数可以设置信号掩码,也就是屏蔽的信号集合:

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set,int signo);
int sigdelset(sigset_t *set,int signo);
int sigismemeber(sigset_t* set,int signo);
  • sigprocmask()函数

sigaction()函数可以用来设置某个信号处理例程正在执行时忽略哪些信号, 而 sigprocmask 是设置这个进程会忽略哪些信号:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

  • how: 控制sigprocmask的行为:

    SIG_BLOCK: 阻塞信号集为参数set和当前set的并集

    SIG_UNBLOCK: 从当前set集中取消参数set指定的信号的阻塞

    SIG_SETMASK: 设置阻塞信号集为参数set.

多线程环境下,如果只想让某个线程忽略某些信号,那么要使用pthread_sigmask函数,它和sigprocmask非常像。

从内核向用户进程发信号

如果希望从内核的驱动,向指定的用户进程发送信号。可以采取以下步骤:

  1. 确定向哪个用户进程发送信号

可以通过get_current函数,获取当前陷入到内核中的进程。

  1. 发送信号

具体代码如下:

#include <linux/sched.h>
#include <linux/sched/signal.h>
static struct task_struct *task = NULL;

void init_func() {
    // 获取陷入到内核时运行的进程,其标识保存在task
	task = get_current();
}
void send_signal() {
    struct siginfo info;
    memset(&info, 0, sizeof(struct siginfo));
    info.si_signo = SIGNAL_NUM; // 指定发送的信号
    info.si_code = SI_QUEUE;    // 指定发送方式
    info.si_int = 1;            // 携带了一个int类型的数据
    // 向task进程发送信号
    if(send_sig_info(SIGNAL_NUM, (struct kernel_siginfo *)&info, task) < 0) {
        pr_err("Unable to send signal\n");
    }
}

多线程环境下的信号使用

多线程环境下,信号的处理有所不同。信号最开始,其实是面向进程的,而且通过sigaction等函数,导致信号的处理是异步的。但在多线程环境下,对信号的处理可以从异步变成同步,简化了信号处理的复杂度。信号的同步处理不再使用sigaction函数注册信号处理函数,而是通过一个线程使用sigwait等待信号的出现。

  • sigwait

sigwait函数会阻塞调用线程,直到pending queue里出现了参数set所指定的任意一个信号,函数返回时会将其从pending queue移除。如果pending queue此前已经有了要等待的信号,那么函数会立刻返回。

int sigwait(const sigset_t *set, int *sig);

参数:

  • set:要等待的信号集
  • sig:返回收到的信号值

返回值:成功则返回0,否则-1。

注意:使用sigwait前,需要提前阻塞掉要等待的信号,否则会调用对应的信号处理函数。在sigwait返回后,根据参数sig的内容,即可确定哪个信号发生了,接下来可以调用对应的处理函数。

问题

  • [x] 如果一个信号处理函数正在执行, 又有信号发来, 如何确保这些信号所表示的信息都被处理了呢

改成可靠信号, 但有一个问题, 正在处理信号的函数如果遇到中断,中断返回后这个信号处理函数咋办呢? 信号处理过程中, 通过sigaction已经设定了阻塞所有信号, 因此还是会执行这个信号处理函数。而且中断会保存上下文,因此返回后会接着执行未执行完的信号处理函数。

  • [x] 信号是在处理完之后从信号队列中取出吗

不是, 信号的注销是在调用信号处理函数之前. 调用信号处理函数之前,进程会把信号在未决信号链中的sigqueue结构卸掉。是否从未决信号集中把信号删除掉,对于实时信号与非实时信号是不相同的。参见: https://zhuanlan.zhihu.com/p/266427607

  • [x] 信号处理时发生了中断, 那么之后中断返回时还会执行信号处理函数吗?

会。sigaction可以屏蔽其他信号在信号处理函数未执行完时。

参考资料

  1. https://embetronicx.com/tutorials/linux/device-drivers/sending-signal-from-linux-device-driver-to-user-space/
  2. 在命令行中输入man sigaction
  3. https://zhuanlan.zhihu.com/p/266427607
  4. https://zhuanlan.zhihu.com/p/537431439
  5. Linux多线程编程-信号机制(sigwait、sigaction和pthread_sigmask、sigprocmask和pthread_kill、kill的区别)_sigwait sigaction-CSDN博客