Linux内核中信号详解

电子说

1.3w人已加入

描述

 

  • 1 信号的角色

    • 1.1 x86/64架构信号定义

    • 1.2 ARM架构信号定义

    • 1.3 RISC-V架构信号定义

    • 1.4 信号的系统调用

    • 1.5 信号工作原理

  • 2 信号的响应行为

  • 3 POSIX信号和多线程程序

  • 4 与信号相关的数据结构

    • 4.2.1 x86/Linux2.6.11的定义

    • 4.2.2 x86-64/Linux2.6.11的定义

    • 4.2.3 x86-64/linux5.18.18的定义

    • 4.2.4 ARM/linux5.18.18的定义

    • 4.2.5 RISC-V/linux6.7

    • 4.1 信号描述符和信号处理程序描述符

    • 4.2 sigaction数据结构

    • 4.3 挂起信号队列

  • 5 信号数据结构的操作函数

    • 5.1 x86架构

    • 5.2 ARM和RISC-V架构

Unix最早引入了信号机制,允许用户进程间进行交互;内核也使用信号通知进程某些系统事件。信号机制已经存在了30年,期间只有一些细微的变化。

我们首先介绍Linux内核如何处理信号,其次讨论允许进程交换信号的系统调用。

1 信号的角色

信号是发送给进程,或一组进程的非常短的消息。通常,可能仅发送一个表示信号的编码。标准信号没有参数等其它信息。

信号的编码,在Linux中使用前缀SIG的宏表示。如前面提到的SIGCHLD宏,其展开的值是17,当子进程停止或终止时,发送给父进程的信号。SIGSEGV,等于11,当进程发生非法内存引用时发送给进程的信号。

信号两个主要作用:

  1. 使进程意识到发生了某个事件
  2. 让进程执行信号处理程序

当然,这两个目的不是相互排斥的,因为通常进程必须对某些事件做出响应(如执行服务例程)。

1.1 x86/64架构信号定义

11-1列出了Linux/i386的前31个信号(Unix系统定义的信号,x86架构,Linux2.6.11linux后续版本中32/64位的信号定义统一到了一个文件中)。某些信号,比如SIGCHLDSIGSTOP与架构相关;甚至,还有一些信号如SIGSTKFLT专门为某些架构定义的。

# 信号 默认动作 说明 POSIX
1 SIGHUP Terminate 挂起控制终端和进程 Yes
2 SIGINT Terminate 键盘中断 Yes
3 SIGQUIT Dump 键盘退出 Yes
4 SIGILL Dump 非法指令 Yes
5 SIGTRAP Dump 调试断点 No
6 SIGABRT Dump 异常终止 Yes
6 SIGIOT Dump 等价于SIGABRT No
7 SIGBUS Dump 总线错误 No
8 SIGFPE Dump 浮点异常 Yes
9 SIGKILL Terminate 杀死进程 Yes
10 SIGUSR1 Terminate 进程可用 Yes
11 SIGSEGV Dump 非法内存引用 Yes
12 SIGUSR2 Terminate 进程可用 Yes
13 SIGPIPE Terminate 管道没有读进程使用 Yes
14 SIGALRM Terminate 实时时钟 Yes
15 SIGTERM Terminate 进程终止 Yes
16 SIGSTKFLT Terminate 协处理器堆栈错误 No
17 SIGCHLD Ignore 子进程停止/终止/被跟踪时的信号 Yes
18 SIGCONT Continue 恢复执行 Yes
19 SIGSTOP Stop 停止进程执行 Yes
20 SIGTSTP Stop 停止tty发起的进程 Yes
21 SIGTTIN Stop 后台进程需要输入 Yes
22 SIGTTOU Stop 后台进程需要输出 Yes
23 SIGURG Ignore 套接字上的紧急条件 No
24 SIGXCPU Dump 超出CPU时间限制 No
25 SIGXFSZ Dump 超出文件大小限制 No
26 SIGVTALRM Terminate 用户态占用CPU时间定时器 No
27 SIGPROF Terminate 用户态和内核态占用CPU时间定时器 No
28 SIGWINCH Ignore 窗口大小改变 No
29 SIGIO Terminate 异步IO No
29 SIGPOLL Terminate 可轮询事件(poll) No
30 SIGPWR Terminate 电源失效/重启动 No
31 SIGSYS Dump 无效系统调用 No
31 SIGUNUSED Dump 等价于SIGSYS No

除了上表中的常规信号之外,POSIX标准还引入了一类新的信号,称为实时信号Linux中信号范围是32~64。实时信号具有以下特性:

  1. 增加了从SIGRTMINSIGRTMAX的实时信号,可以通过sysconf(_SC_RTSIG_MAX)系统函数获得当前操作系统支持的实时信号的个数。但是要注意,一般libc会对SIGRTMIN进行修改,保留几个预设的值用于pthread内部,比如glibc就保留了3个值。所以在使用实时信号的时候,应该使用SIGRTMIN+nSIGRTMAX-n的方式,而不是直接使用数值。

  2. 实时信号和常规信号不一样,它没有明确的含义,而是由使用者自己来决定如何使用。

  3. 进程可以接受多个相同的实时信号,而常规信号不能,在常规信号没有得到处理的时候,多个常规信号会被合为一个。

  4. 实时信号使用sigqueue发送的时候,可以携带附加的数据(int或者pointer)。

  5. 实时信号有时间顺序的概念,所以同样的实时信号会按次序被处理。

  6. 信号实质上是软中断,中断有优先级,信号也有优先级。实时信号具有优先的概念,数值越低的信号其优先级越高,也就是数值低的实时信号优先得到处理。实时信号和标准信号的优先级,在POSIX中是未定义的,一般来说会优先处理标准信号。

  7. 实时信号的默认行为都一样,都是结束当前的进程,这个和标准信号是不一样的。

尽管Linux内核不使用实时信号,但是它通过几个特殊的系统调用完整支持POSIX标准。

Ubuntu 18.04为例,查看Linux系统中使用的信号方法:

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

1.2 ARM架构信号定义

下图右边是ARM架构与x86架构信号定义的比较图(左边是x86架构,右边是ARM架构)。通过对比发现,ARM架构比x86架构多了一个SIGSWI信号。在对内核源代码进行进一步调查后,发现唯一提到SIGSWI(不包括声明本身)的是文件Linux 5.18.18中,位于tools/perf/trace/beauty/signum.c。具体代码中只有在打印信号的时候用,貌似已经从内核中移除。)。一些奇怪的基于ARM 的操作系统(RISCOS)使用这种方式与其模拟器进行通信。它被称为Arthur OS

内核

1.3 RISC-V架构信号定义

RISC-V架构信号定义如下面所示,Linux 6.7内核,文件位于/include/uapi/asm-generic/signal.h。信号的定义直接使用了标准的接口规范。

#define _NSIG       64
// ... 省略
#define SIGHUP       1
#define SIGINT       2
// ... 省略
#define SIGPWR      30
#define SIGSYS      31
#define SIGUNUSED   31

/* 用户进程不能认为这些是常数  */
#define SIGRTMIN    32
#ifndef SIGRTMAX
#define SIGRTMAX    _NSIG
#endif

所以说,对于Linux信号来说,不管是x86架构,ARM架构,还是RISC-V,都是统一的,没有什么变化。

1.4 信号的系统调用

Linux提供了一些系统调用,允许编程者发送信号,并决定如何响应接收到的信号。下表列出了这些系统调用:

系统调用 描述
kill() 发送信号给线程组
tkill() 发送信号给进程
tgkill() 发送信号给特定线程组中的进程
sigaction() 设定信号的行为
signal() 与sigaction()类似
sigpending() 检查是否为挂起信号
sigprocmask() 修改阻塞信号
sigsuspend() 等待信号
rt_sigaction() 设定实时信号的行为
rt_sigpending() 检查是否为挂起的实时信号
rt_sigprocmask() 修改阻塞的实时信号
rt_sigqueueinfo() 发送实时信号给线程组
rt_sigsuspend() 等待实时信号
rt_sigtimedwait() 与rt_sigsuspend()类似

1.5 信号工作原理

信号的一个重要特性是,可能会在任何时候传递给进程。发送给没有在执行状态的进程,就需要保存该信号,以便进程恢复执行时处理它。阻塞信号要求在解除阻塞之前延缓信号的传递。

因此,Linux将内核的传递分为了两个阶段:

  • 信号产生

    内核更新目标进程的数据结构,表达一个新信号要被发送。

  • 信号传递

    内核通过改变目标进程的状态,且执行指定信号处理程序,以强制其响应信号,

每个信号最多传递一次。信号是消耗性资源:一旦它们被传递,所有进程描述符中跟信号有关的数据引用都将取消。

产生还没有传递的信号,称为挂起信号。任何时候,一个进程只能存在一个给定类型的挂起信号;同一个进程的同类挂起信号会被抛弃。但是,实时信号与此不同:可以同时存在多个同类型的挂起信号。

信号产生还没有被传递这段时间,通常存在于以下时间段:

  • 信号通常只传递给当前正在运行的进程(current)。

  • 进程可以有选择地阻塞信号。这种情况下,进程不会接收信号,除非解除阻塞。

  • 执行信号处理程序时,进程通常屏蔽掉响应的信号(例如,在信号处理程序执行完之前自动阻塞该信号)。也就是说,信号处理程序不会被正在处理的信号打断,所以,信号处理程序不需要考虑可重入的问题。

尽管信号的概念非常简单,内核实现却相当复杂。内核必须:

  • 记住哪些信号被哪个进场阻塞。

  • 当从内核态切换到用户态时,检查该进程是否有信号需要处理。这通常发生在每次定时器中断时(大约几个毫秒一次)。

  • 判断该信号是否被忽略。满足忽略的条件如下:

    • 目标进程没有被其它进程追踪(也就是进程描述符中的PT_PTRACED标志等于0)。
    • 信号没有被目标进程阻塞。
    • 信号正在被目标忽略。(可以是进程显式忽略,也可以是信号的默认行为是忽略且进程没有更改它)
  • 处理信号,可能涉及到在进程执行的任何时候切换到信号处理程序,且需要在处理程序返回时恢复其原始执行上下文。

此外,Linux必须考虑到BSDSystem V信号采用的不同语义,它必须遵循相当复杂的POSIX要求。

2 信号的响应行为

信号的响应方式有3种:

  •  
  1. 忽略信号
  •  
  • Terminate

    杀死进程。

  • Dump

    杀死进程,如果可能的话创建一个包含其上下文的核心转储文件。该文件主要用于调试目的。

  • Ignore

    忽略信号。

  • Stop

    停止进程。例如将进程置为TASK_STOPPED状态。

  • Continue

    继续进程(TASK_STOPPED),将其置于TASK_RUNNING状态。

  1. 执行信号的默认行为。默认行为是内核预定义好的,如下所示:
  •  
  1. 捕获信号,执行自定义的信号处理程序。

注意,阻塞信号不同于忽略信号。只要信号被阻塞,就不会传递它。而忽略信号总是传递它,只是不执行响应动作。

SIGKILLSIGSTOP信号不能被忽略,捕获或阻塞。它们的默认动作总是会被执行。因此,SIGKILLSIGSTOP信号给予合适权限的用户可以终止、停止每个进程(这儿有两个例外:不能向进程0-swapper发送信号,并且发送给进程1-init的信号总是被丢弃。因此,进程0永远不会死亡,而进程1只有在init终止时才会死亡。)。

如果信号造成内核杀死进程,那么对于给定进程是非常致命的,比如SIGKILL。默认行为为Terminate的信号且未被进程捕获,对于该进程来说也是致命的。但是,如果信号被捕获,而其处理程序终止进程则不是致命的,因为进程自己选择的终止,不是内核杀死的。

3 POSIX信号和多线程程序

POSIX 1003.1标准对多线程应用程序的信号处理有严格的要求:

  • 多线程应用程序中所有线程共享信号处理程序;但是,每个线程必须具有自己的挂起信号和阻塞信号的位数组。

  • kill()sigqueue()POSIX库函数必须发送信号给整个多线程应用,而不是某个特定的线程。内核生成的所有信号(如SIGCHLDSIGINTSIGQUIT)都是如此。

  • 发送给多线程应用的信号只被传递给一个线程,由内核在没有阻塞该信号的线程中任意选择。

  • 如果致命信号发送给多线程应用,内核将杀死应用程序的所有线程,而不仅仅是信号传递给的那个线程。

为了遵循POSIX标准,Linux 2.6内核将多线程应用程序实现为属于同一线程组的一组轻量级进程。

本文中的线程组是广义的,甚至可以使传统意义上的单进程。术语进程表示传统意义上的进程或轻量级进程(线程组中的某个成员)。

此外,如果信号被发送给一个特定的进程,则是私有的;如果发送给整个线程组,则是共享的。

4 与信号相关的数据结构

为了追踪进程或线程组的信号状态,内核在进程描述符中提供了几个可访问的数据结构。重要的数据结构如下所示:

内核

其中,进程描述符中与信号处理相关的数据字段如下所示:

数据类型 名称 描述
struct signal_struct * signal 指向进程的信号描述符
struct sighand_struct * sighand 指向进程的信号处理程序描述符
sigset_t blocked 阻塞信号掩码
sigset_t real_blocked 阻塞信号临时掩码(rt_sigtimedwait())系统调用使用
struct sigpending pending 私有挂起信号
unsigned long sas_ss_sp 备选信号处理程序堆栈的地址

blocked存储了被进程屏蔽掉的信号。数据类型为sigset_t,是一个位数组,每一位代表一类信号:

typedef struct {
    unsigned long sig[2];
} sigset_t;

因为32位系统的unsigned long32位,信号最大数量是64(用_NSIG宏表示)。因为没有信号是0,所以,信号值等于sigset_t中位索引加1。具体可以参考前面列出的表。

4.1 信号描述符和信号处理程序描述符

进程描述符中的signal字段指向信号描述符,类型为signal_struct,用来记录共享挂起信号。此外,信号描述符还有一些与信号处理不太相关的字段,如rlim(进程资源限制),或pgrpsession字段,分别存储线程组领导者的PID和进程中会话领导者的PID。事实上,我们在学习clone(),fork(),和vfork()系统调用一节时了解到,同一线程组中的所有进程共享信号描述符,也就是说,通过clone()系统调用,并设置CLONE_THREAD标志,创建的所有进程,其信号描述符中所有字段必须相同。

信号描述符中与信号处理相关的字段,如下表所示:

类型 变量 描述
atomic_t count 信号描述符的使用计数器
atomic_t live 线程组中活动进程的数量
wait_queue_head_t wait_chldexit wait4()系统调用中休眠进程的等待队列
struct task_struct * curr_target 线程组中接收到信号的最后一个进程的描述符
struct sigpending shared_pending 共享挂起信号的数据结构
int group_exit_code 线程组的进程终止码
struct task_struct * group_exit_task 杀死整个线程组时使用
int notify_count 杀死整个线程组时使用
int group_stop_count 停止整个线程组时使用
unsigned int flags 传递修改进程状态的信号时使用的标志

除了信号描述符,每个进程还有一个信号处理描述符,数据结构为sighand_struct,其描述了线程组怎样处理信号。其字段如下所示:

类型 变量 描述
atomic_t count 信号处理程序描述符的使用计数器
struct k_sigaction[64] action 指定传递信号时要执行的动作的结构数组
spinlock_t siglock 包含信号和信号处理程序等描述符的自旋锁

正如先前提到的,使用clone()CLONE_SIGHAND标志创建的进程们共享信号处理描述符。所以,count字段记录了共享信号处理描述符的进程数。在POSIX多线程应用中,线程组中的所有轻量级进程引用相同的信号描述符和相同的信号处理描述符。

4.2 sigaction数据结构

有些架构可能会将信号的某些属性仅对内核可见。因此,存储在k_sigaction中的信号属性,既包含了对用户态隐藏的属性,也包含了用户态所有可见的属性。事实上,在x86平台上,所有的信号属性对用户态都是可见的。

因此,k_sigaction结构简化为一个类型为sigactionsa结构,它包含如下字段*:

用户态应用程序用来给signal()sigaction()系统调用传递参数的sigaction数据结构与内核使用的数据结构略有不同。

  • sa_handler

    该字段指定要执行的动作类型;可以是信号处理程序的指针,SIG_DFL(值为0,执行默认行为),或SIG_IGN(值为1,忽略信号)。

  • sa_flags

    如何处理处理信号的标志,下表列出了其中的一些。

    因为历史原因,这些标志和irqaction具有一样的前缀SA_;然而,两组标志没有任何关系。

  • sa_mask

    类型为sigset_t,用来指定在运行信号处理程序时屏蔽掉的信号。

sa_flags值和意义

标志名称 描述
SA_NOCLDSTOP 仅适用于SIGCHLD;当进程停止时不向父进程发送SIGCHLD
SA_NOCLDWAIT 仅适用于SIGCHLD;当进程终止时不会创建zombie僵尸进程
SA_SIGINFO 向信号处理程序提供额外的信息(查看稍后的改变信号动作)
SA_ONSTACK 为信号处理程序使用替代堆栈(查看稍后的捕捉信号一节)
SA_RESTART 中断的系统调用自动重启(查看稍后的`系统调用的重新执行)
SA_NODEFER,
SA_NOMASK
在执行信号处理程序时不屏蔽信号
SA_RESETHAND,
SA_ONESHOT
在执行信号处理程序后重置为默认操作
4.2.1 x86/Linux2.6.11的定义
#ifdef __KERNEL__
struct old_sigaction {
    __sighandler_t sa_handler;
    old_sigset_t sa_mask;
    unsigned long sa_flags;
    __sigrestore_t sa_restorer;
};

struct sigaction {
    __sighandler_t sa_handler;
    unsigned long sa_flags;
    __sigrestore_t sa_restorer;
    sigset_t sa_mask;       /* mask last for extensibility */
};

struct k_sigaction {
    struct sigaction sa;
};
#else
/* 这是为了迎合libc库的实现 */
struct sigaction {
    union {
        __sighandler_t _sa_handler;
        void (*_sa_sigaction)(int, struct siginfo *, void *);
    } _u;
    sigset_t sa_mask;
    unsigned long sa_flags;
    void (*sa_restorer)(void);
};

#define sa_handler      _u._sa_handler
#define sa_sigaction    _u._sa_sigaction
#endif /* __KERNEL__ */
4.2.2 x86-64/Linux2.6.11的定义
struct sigaction {
    __sighandler_t sa_handler;
    unsigned long sa_flags;
    __sigrestore_t sa_restorer;
    sigset_t sa_mask;       /* mask last for extensibility */
};

struct k_sigaction {
    struct sigaction sa;
};
4.2.3 x86-64/linux5.18.18的定义

较高版本中的内核中,将k_sigaction定义到了一个统一的文件中(include/linux/signal_types.h

struct sigaction {
#ifndef __ARCH_HAS_IRIX_SIGACTION
    __sighandler_t  sa_handler;
    unsigned long   sa_flags;
#else
    unsigned int    sa_flags;
    __sighandler_t  sa_handler;
#endif
#ifdef __ARCH_HAS_SA_RESTORER
    __sigrestore_t sa_restorer;
#endif
    sigset_t    sa_mask;    /* mask last for extensibility */
};

struct k_sigaction {
    struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER
    __sigrestore_t ka_restorer;
#endif
};

为了兼容libc库,需要根据架构进行一些定义:

ifndef __KERNEL__
/* 这是为了迎合libc库的实现 */
#ifdef __i386__

struct sigaction {
    union {
      __sighandler_t _sa_handler;
      void (*_sa_sigaction)(int, struct siginfo *, void *);
    } _u;
    sigset_t sa_mask;
    unsigned long sa_flags;
    void (*sa_restorer)(void);
};

#define sa_handler      _u._sa_handler
#define sa_sigaction    _u._sa_sigaction

#else /* __i386__ */

struct sigaction {
    __sighandler_t sa_handler;
    unsigned long sa_flags;
    __sigrestore_t sa_restorer;
    sigset_t sa_mask;       /* mask last for extensibility */
};

#endif /* !__i386__ */
endif /* ! __KERNEL__ */
4.2.4 ARM/linux5.18.18的定义

ARM架构下内核中数据结构与x86架构相同,但是,为了兼容libc,不得不定义一些特殊的结构:

#ifndef __KERNEL__
/* 这是为了迎合libc库的实现 */
struct sigaction {
    union {
      __sighandler_t _sa_handler;
      void (*_sa_sigaction)(int, struct siginfo *, void *);
    } _u;
    sigset_t sa_mask;
    unsigned long sa_flags;
    void (*sa_restorer)(void);
};

#define sa_handler      _u._sa_handler
#define sa_sigaction    _u._sa_sigaction

#endif /* __KERNEL__ */
4.2.5 RISC-V/linux6.7

最新版本内核中没有变化,RISC-V相关实现与ARM架构相同,只是,取消了联合体复杂的实现(这就是后发优势):

#ifndef __KERNEL__
struct sigaction {
    __sighandler_t  sa_handler;
    unsigned long   sa_flags;
#ifdef SA_RESTORER
    __sigrestore_t  sa_restorer;
#endif
    sigset_t sa_mask;       /* mask last for extensibility */
};
#endif

4.3 挂起信号队列

正如前面所述,某些系统可以产生信号:kill()rt_sigqueueinfo()发送信号到整个线程组,而tkill()tgkill()发送信号到某个特定的进程。

为了记录当前哪些信号被挂起,内核给每个进程提供了两个挂起信号队列:

  • 共享挂起信号队列,挂载到信号描述符的shared_pending字段,存储整个线程组的挂起信号。

  • 私有挂起信号队列,挂载到进程描述符的pending字段,存储进程(轻量级)自己的挂起信号。

挂起信号队列的元素是类型为sigpending的数据结构,定义如下:

struct sigpending {
    struct list_head    list;
    sigset_t            signal;
}

signal字段是一个位数组,每一位代表一个挂起信号,而list字段是双向链表的头,该表头指向sigqueue数据结构组成的链表,sigqueue字段定义如下表所示:

类型 变量 描述
struct list_head list 挂起信号队列的链表链接
spinlock_t * lock 信号处理描述符的siglock字段的指针
int flags sigqueue数据结构中的标志
siginfo_t info 描述发送信号的事件信息
struct user_struct * user 指向进程拥有者的用户数据结构

其中,siginfo_t的大小为128字节,描述特定信号事件的信息;它包含以下字段:

  • si_signo

    信号编码。

  • si_errno

    产生信号的指令的错误编码,如果是0则没有错误。

  • si_code

    标识发送信号方的编码(参加表11-8

    11-8。最重要的信号发送方编码

    编码名称 发送方
    SI_USER kill()raise()(查看稍后的与信号处理相关的系统调用)
    SI_KERNEL 通用内核函数产生的信号
    SI_QUEUE sigqueue()(查看稍后的与信号处理相关的系统调用)
    SI_TIMER 定时器到时
    SI_ASYNCIO 异步IO完成
    SI_TKILL tkill()tgkill()(查看稍后的与信号处理相关的系统调用)
  • _sifields

    一个联合体数据类型,根据信号类型存储信息。例如是SIGKILL信号,siginfo_t记录发送进程的PIDUID;如果是SIGSEGV,则记录产生信号时访问的内存地址。

5 信号数据结构的操作函数

为了方便处理信号,内核提供了几个函数和宏,如下所示。其中,set是指向sigset_t变量的指针,nsig是信号值,mask是一个unsigned long类型的位掩码。

  • sigemptyset(set)/sigfillset(set)

    设置sigset_t变量的位为01

  • sigaddset(set,nsig)/sigdelset(set,nsig)

    设置sigset_t变量中指定信号nsig10。事实上,sigaddset()可以简化为

    set->sig[(nsig - 1) / 32] |= 1UL << ((nsig - 1) % 32);
    

    sigdelset()简化为

    set->sig[(nsig - 1) / 32] &= ~(1UL << ((nsig - 1) % 32));
    
  • sigaddsetmask(set,mask)/sigdelsetmask(set,mask)

    设置sigset_t类型变量的位掩码为10

    set->sig[0] |= mask;
    

    and to:

    set->sig[0] &= ~mask;
    
  • sigismember(set,nsig)

    返回信号nsigsigset_t变量中的对应位。实际可以简化为:

    return 1 & (set->sig[(nsig-1) / 32] >> ((nsig-1) % 32));
    
  • sigmask(nsig)

    产生信号nsig的位索引。换句话说,如果内核需要设置、清除或测试sigset_t类型变量中的信号对应位,可以通过该宏可以导出正确的位。

  • sigandsets(d,s1,s2)/sigorsets(d,s1,s2)/signandsets(d,s1,s2)

    s1s2执行逻辑ANDORNAND操作,结果保存到d中。

  • sigtestsetmask(set,mask)

    如果变量的相应位掩码为1,则返回1;否则返回0。只有在信号1~32之间使用。

  • siginitset(set,mask)

    mask的位初始化sigset_t变量中1 ~ 32信号对应的低位,清除33 ~ 63信号对应位。

  • siginitsetinv(set,mask)

    mask的补码初始化sigset_t变量1~32信号对应的低位,并设置33~63信号对应的位。

  • signal_pending(p)

    判断由p指向的进程描述符是否具有非阻塞的挂起信号,如果有,返回1true);如果没有,则返回0false)。该函数是通过对进程的TIF_SIGPENDING标志进行检查实现的。

  • recalc_sigpending_tsk(t)/recalc_sigpending()

    第一个函数检查t指向的进程描述符中是否有挂起信号(通过检查t->pending->signal字段实现),或者检查该进程所属线程组是否有挂起信号(通过检查t-> signal->shared_pending->signal实现)。该函数随后设置t->thread_info->flags中的TIF_SIGPENDING标志位。recalc_sigpending()等价于recalc_sigpending_tsk(current)

  • rm_from_queue(mask,q)

    从挂起信号队列q中移除mask位掩码中对应的挂起信号。

  • flush_sigqueue(q)

    从挂起信号队列q中移除所有挂起信号。

  • flush_signals(t)

    删除发送给进程的所有信号(t指向进程描述符)。实现方式是清除t->thread_info->flagsTIF_SIGPENDING标志,并分别对t->pendingt->signal->shared_ pending队列调用flush_sigqueue()

5.1 x86架构

较新的内核版本(比如,v5.18.18v6.7)中,这些函数都已经作了统一处理,位于文件include/linux/signal.h中。但是,x86架构体系的i386它的实现使用了汇编指令(为了效率),比如:

static inline int __gen_sigismember(sigset_t *setint _sig)
{
    bool ret;
    asm("btl %2,%1" CC_SET(c)
        : CC_OUT(c) (ret) : "m"(*set), "Ir"(_sig-1));
    return ret;
}

__gen_sigismembersigismember的一个底层实现,其中用到了汇编指令btl(将寄存器的位进行比较,如果该位被设置则置为1,则CC_SET(c)条件码会被设置为真,否则为假)。所以,i386有一部分设置信号的函数定义独自一个文件(/arch/x86/include/asm/signal.h)。

5.2 ARM和RISC-V架构

x86-64ARMRISC-V架构的函数定义都位于include/linux/signal.h文件中,是统一实现。

 


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分