电子说
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
内核如何处理信号,其次讨论允许进程交换信号的系统调用。
信号是发送给进程,或一组进程的非常短的消息。通常,可能仅发送一个表示信号的编码。标准信号没有参数等其它信息。
信号的编码,在Linux
中使用前缀SIG
的宏表示。如前面提到的SIGCHLD
宏,其展开的值是17
,当子进程停止或终止时,发送给父进程的信号。SIGSEGV
,等于11
,当进程发生非法内存引用时发送给进程的信号。
信号两个主要作用:
当然,这两个目的不是相互排斥的,因为通常进程必须对某些事件做出响应(如执行服务例程)。
表11-1
列出了Linux/i386
的前31
个信号(Unix
系统定义的信号,x86
架构,Linux2.6.11
,linux
后续版本中32/64
位的信号定义统一到了一个文件中)。某些信号,比如SIGCHLD
或SIGSTOP
与架构相关;甚至,还有一些信号如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
。实时信号具有以下特性:
增加了从SIGRTMIN
到SIGRTMAX
的实时信号,可以通过sysconf(_SC_RTSIG_MAX)
系统函数获得当前操作系统支持的实时信号的个数。但是要注意,一般libc
会对SIGRTMIN
进行修改,保留几个预设的值用于pthread
内部,比如glibc
就保留了3
个值。所以在使用实时信号的时候,应该使用SIGRTMIN+n
、SIGRTMAX-n
的方式,而不是直接使用数值。
实时信号和常规信号不一样,它没有明确的含义,而是由使用者自己来决定如何使用。
进程可以接受多个相同的实时信号,而常规信号不能,在常规信号没有得到处理的时候,多个常规信号会被合为一个。
实时信号使用sigqueue
发送的时候,可以携带附加的数据(int
或者pointer
)。
实时信号有时间顺序的概念,所以同样的实时信号会按次序被处理。
信号实质上是软中断,中断有优先级,信号也有优先级。实时信号具有优先的概念,数值越低的信号其优先级越高,也就是数值低的实时信号优先得到处理。实时信号和标准信号的优先级,在POSIX
中是未定义的,一般来说会优先处理标准信号。
实时信号的默认行为都一样,都是结束当前的进程,这个和标准信号是不一样的。
尽管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
下图右边是ARM
架构与x86
架构信号定义的比较图(左边是x86
架构,右边是ARM
架构)。通过对比发现,ARM
架构比x86
架构多了一个SIGSWI
信号。在对内核源代码进行进一步调查后,发现唯一提到SIGSWI
(不包括声明本身)的是文件(
Linux 5.18.18
中,位于tools/perf/trace/beauty/signum.c
。具体代码中只有在打印信号的时候用,貌似已经从内核中移除。)。一些奇怪的基于ARM
的操作系统(RISCOS)
使用这种方式与其模拟器进行通信。它被称为Arthur OS
。
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
,都是统一的,没有什么变化。
Linux
提供了一些系统调用,允许编程者发送信号,并决定如何响应接收到的信号。下表列出了这些系统调用:
系统调用 | 描述 |
---|---|
kill() |
发送信号给线程组 |
tkill() |
发送信号给进程 |
tgkill() |
发送信号给特定线程组中的进程 |
sigaction() |
设定信号的行为 |
signal() |
与sigaction()类似 |
sigpending() |
检查是否为挂起信号 |
sigprocmask() |
修改阻塞信号 |
sigsuspend() |
等待信号 |
rt_sigaction() |
设定实时信号的行为 |
rt_sigpending() |
检查是否为挂起的实时信号 |
rt_sigprocmask() |
修改阻塞的实时信号 |
rt_sigqueueinfo() |
发送实时信号给线程组 |
rt_sigsuspend() |
等待实时信号 |
rt_sigtimedwait() |
与rt_sigsuspend()类似 |
信号的一个重要特性是,可能会在任何时候传递给进程。发送给没有在执行状态的进程,就需要保存该信号,以便进程恢复执行时处理它。阻塞信号要求在解除阻塞之前延缓信号的传递。
因此,Linux
将内核的传递分为了两个阶段:
信号产生
内核更新目标进程的数据结构,表达一个新信号要被发送。
信号传递
内核通过改变目标进程的状态,且执行指定信号处理程序,以强制其响应信号,
每个信号最多传递一次。信号是消耗性资源:一旦它们被传递,所有进程描述符中跟信号有关的数据引用都将取消。
产生还没有传递的信号,称为挂起信号。任何时候,一个进程只能存在一个给定类型的挂起信号;同一个进程的同类挂起信号会被抛弃。但是,实时信号与此不同:可以同时存在多个同类型的挂起信号。
信号产生还没有被传递这段时间,通常存在于以下时间段:
信号通常只传递给当前正在运行的进程(current
)。
进程可以有选择地阻塞信号。这种情况下,进程不会接收信号,除非解除阻塞。
执行信号处理程序时,进程通常屏蔽掉响应的信号(例如,在信号处理程序执行完之前自动阻塞该信号)。也就是说,信号处理程序不会被正在处理的信号打断,所以,信号处理程序不需要考虑可重入的问题。
尽管信号的概念非常简单,内核实现却相当复杂。内核必须:
记住哪些信号被哪个进场阻塞。
当从内核态切换到用户态时,检查该进程是否有信号需要处理。这通常发生在每次定时器中断时(大约几个毫秒一次)。
判断该信号是否被忽略。满足忽略的条件如下:
PT_PTRACED
标志等于0
)。处理信号,可能涉及到在进程执行的任何时候切换到信号处理程序,且需要在处理程序返回时恢复其原始执行上下文。
此外,Linux
必须考虑到BSD
和System V
信号采用的不同语义,它必须遵循相当复杂的POSIX
要求。
信号的响应方式有3
种:
Terminate
杀死进程。
Dump
杀死进程,如果可能的话创建一个包含其上下文的核心转储文件。该文件主要用于调试目的。
Ignore
忽略信号。
Stop
停止进程。例如将进程置为TASK_STOPPED
状态。
Continue
继续进程(TASK_STOPPED
),将其置于TASK_RUNNING
状态。
注意,阻塞信号不同于忽略信号。只要信号被阻塞,就不会传递它。而忽略信号总是传递它,只是不执行响应动作。
SIGKILL
和SIGSTOP
信号不能被忽略,捕获或阻塞。它们的默认动作总是会被执行。因此,SIGKILL
和SIGSTOP
信号给予合适权限的用户可以终止、停止每个进程(这儿有两个例外:不能向进程0
-swapper
发送信号,并且发送给进程1
-init
的信号总是被丢弃。因此,进程0
永远不会死亡,而进程1
只有在init
终止时才会死亡。)。
如果信号造成内核杀死进程,那么对于给定进程是非常致命的,比如SIGKILL
。默认行为为Terminate
的信号且未被进程捕获,对于该进程来说也是致命的。但是,如果信号被捕获,而其处理程序终止进程则不是致命的,因为进程自己选择的终止,不是内核杀死的。
POSIX 1003.1
标准对多线程应用程序的信号处理有严格的要求:
多线程应用程序中所有线程共享信号处理程序;但是,每个线程必须具有自己的挂起信号和阻塞信号的位数组。
kill()
和sigqueue()
等POSIX
库函数必须发送信号给整个多线程应用,而不是某个特定的线程。内核生成的所有信号(如SIGCHLD
、SIGINT
或SIGQUIT
)都是如此。
发送给多线程应用的信号只被传递给一个线程,由内核在没有阻塞该信号的线程中任意选择。
如果致命信号发送给多线程应用,内核将杀死应用程序的所有线程,而不仅仅是信号传递给的那个线程。
为了遵循POSIX
标准,Linux 2.6
内核将多线程应用程序实现为属于同一线程组的一组轻量级进程。
本文中的
线程组
是广义的,甚至可以使传统意义上的单进程。术语进程
表示传统意义上的进程或轻量级进程(线程组中的某个成员)。
此外,如果信号被发送给一个特定的进程,则是私有的;如果发送给整个线程组,则是共享的。
为了追踪进程或线程组的信号状态,内核在进程描述符中提供了几个可访问的数据结构。重要的数据结构如下所示:
其中,进程描述符中与信号处理相关的数据字段如下所示:
数据类型 | 名称 | 描述 |
---|---|---|
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 long
是32
位,信号最大数量是64
(用_NSIG
宏表示)。因为没有信号是0
,所以,信号值等于sigset_t
中位索引加1
。具体可以参考前面列出的表。
进程描述符中的signal
字段指向信号描述符,类型为signal_struct
,用来记录共享挂起信号。此外,信号描述符还有一些与信号处理不太相关的字段,如rlim
(进程资源限制),或pgrp
和session
字段,分别存储线程组领导者的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
多线程应用中,线程组中的所有轻量级进程引用相同的信号描述符和相同的信号处理描述符。
有些架构可能会将信号的某些属性仅对内核可见。因此,存储在k_sigaction
中的信号属性,既包含了对用户态隐藏的属性,也包含了用户态所有可见的属性。事实上,在x86
平台上,所有的信号属性对用户态都是可见的。
因此,k_sigaction
结构简化为一个类型为sigaction
的sa
结构,它包含如下字段*:
用户态应用程序用来给
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 |
在执行信号处理程序后重置为默认操作 |
#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__ */
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;
};
较高版本中的内核中,将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__ */
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__ */
最新版本内核中没有变化,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
正如前面所述,某些系统可以产生信号: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
记录发送进程的PID
和UID
;如果是SIGSEGV
,则记录产生信号时访问的内存地址。
为了方便处理信号,内核提供了几个函数和宏,如下所示。其中,set
是指向sigset_t
变量的指针,nsig
是信号值,mask
是一个unsigned long
类型的位掩码。
sigemptyset(set)/sigfillset(set)
设置sigset_t
变量的位为0
或1
。
sigaddset(set,nsig)/sigdelset(set,nsig)
设置sigset_t
变量中指定信号nsig
为1
或0
。事实上,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
类型变量的位掩码为1
或0
。
set->sig[0] |= mask;
and to:
set->sig[0] &= ~mask;
sigismember(set,nsig)
返回信号nsig
在sigset_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)
对s1
和s2
执行逻辑AND
、OR
和NAND
操作,结果保存到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
指向的进程描述符是否具有非阻塞的挂起信号,如果有,返回1
(true
);如果没有,则返回0
(false
)。该函数是通过对进程的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->flags
的TIF_SIGPENDING
标志,并分别对t->pending
和t->signal->shared_ pending
队列调用flush_sigqueue()
。
x86
架构
较新的内核版本(比如,v5.18.18
和v6.7
)中,这些函数都已经作了统一处理,位于文件include/linux/signal.h
中。但是,x86
架构体系的i386
它的实现使用了汇编指令(为了效率),比如:
static inline int __gen_sigismember(sigset_t *set, int _sig)
{
bool ret;
asm("btl %2,%1" CC_SET(c)
: CC_OUT(c) (ret) : "m"(*set), "Ir"(_sig-1));
return ret;
}
__gen_sigismember
是sigismember
的一个底层实现,其中用到了汇编指令btl
(将寄存器的位进行比较,如果该位被设置则置为1
,则CC_SET(c)
条件码会被设置为真,否则为假)。所以,i386
有一部分设置信号的函数定义独自一个文件(/arch/x86/include/asm/signal.h
)。
x86-64
、ARM
和RISC-V
架构的函数定义都位于include/linux/signal.h
文件中,是统一实现。
全部0条评论
快来发表一下你的评论吧 !