Linux 信号(Signal)
Linux对异常的处理
陷入内核调用异常处理程序,对于故障(Fault)
如果处理程序可以修复的故障,比如正常的缺页,修复后回到断点处继续执行
如果不能修复的,比如越级,越权,越界,除0等,linux只会给进程发送一个信号,回到用户态,进程去调度相应的信号处理程序,这样做是快速在内核态完成异常处理,减小嵌套执行异常的可能性
linux里可以用kill -l查看信号列表,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。
信号值 | 信号名称 | 说明 | 默认行为 |
---|---|---|---|
1 | SIGHUP |
终端断开连接 | 终止进程 |
2 | SIGINT |
终端中断符(Ctrl+C) | 终止进程 |
3 | SIGQUIT |
终端退出符(Ctrl+\) | 生成core文件并终止进程 |
4 | SIGILL |
非法指令 | 生成core文件并终止进程 |
5 | SIGTRAP |
跟踪断点 | 生成core文件并终止进程 |
6 | SIGABRT |
异常终止 | 生成core文件并终止进程 |
7 | SIGBUS |
总线错误 | 生成core文件并终止进程 |
8 | SIGFPE |
浮点异常 | 生成core文件并终止进程 |
9 | SIGKILL |
无条件终止 | 终止进程 |
10 | SIGUSR1 |
用户定义信号1 | 终止进程 |
11 | SIGSEGV |
段错误 | 生成core文件并终止进程 |
12 | SIGUSR2 |
用户定义信号2 | 终止进程 |
13 | SIGPIPE |
写入管道或套接字时,读取端已关闭 | 终止进程 |
14 | SIGALRM |
定时器信号 | 终止进程 |
15 | SIGTERM |
终止请求 | 终止进程 |
16 | SIGSTKFLT |
协处理器堆栈错误 | 生成core文件并终止进程 |
17 | SIGCHLD |
子进程状态变化 | 忽略信号 |
18 | SIGCONT |
继续执行停止的进程 | 忽略信号 |
19 | SIGSTOP |
停止进程,不能被捕捉或忽略 | 停止进程 |
20 | SIGTSTP |
终端停止符(Ctrl+Z) | 停止进程 |
21 | SIGTTIN |
后台进程试图从终端读取 | 停止进程 |
22 | SIGTTOU |
后台进程试图向终端写入 | 停止进程 |
23 | SIGURG |
套接字上接收到紧急数据 | 忽略信号 |
24 | SIGXCPU |
CPU时间限制超时 | 生成core文件并终止进程 |
25 | SIGXFSZ |
文件大小限制超时 | 生成core文件并终止进程 |
26 | SIGVTALRM |
虚拟定时器信号 | 终止进程 |
27 | SIGPROF |
分时定时器信号 | 终止进程 |
28 | SIGWINCH |
窗口大小变化 | 忽略信号 |
29 | SIGIO |
异步I/O事件 | 忽略信号 |
30 | SIGPWR | 电源故障 | 终止 |
31 | SIGSYS | 非法系统调用 | 终止 |
这种信号机制,给了用户一种进程间通信机制,自定义信号处理程序的权力
linux 1 ~ 31里一种类型的信号最多只有一个待处理信号(一个发出而没有被接收处理的信号叫做待处理信号),因为传统UNIX支持的信号是用bitflag实现的,已经置1再置1也没意思,这也是为什么叫不可靠信号的原因,后面序号的可靠信号都是通过队列来实现
发送信号
内核的异常处理程序
/bin/kill发送
/bin/kill -signalid pid
pid为正数给pid进程 发送signalid信号
pid为负数给pid进程组所有进程发送signalid信号
- 键盘发送信号 ctrl c ctrl z等
- kill函数显式地要求内核发送一个信号给目的进程。
int kill(pid_t pid, int sig);
成功返回0 否则返回-1;
pid >0 发送sig给进程pid;
pid =0 发送sig给调用进程所在进程组的每一个进程(包括自己);
pid <0 发送sig给|pid|进程组的每一个进程
- alarm函数
unsigned int alarm(unsigned int secs);
在secs秒后 发送一个SIGALRM信号给调用进程 secs为0不会调度安排新闹钟,任何情况下都会取消待处理(pending)的闹钟,返回剩余秒数,没有待处理的闹钟返回0
接受信号
隐式阻塞机制
内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号。也就是信号处理程序执行时不接受正在处理类型的信号
显示阻塞机制
应用程序可以使用 sigprocmask 函数和它的辅助函数,来设置信号屏蔽字,明确地阻塞和解除阻塞选定的信号。SIGKILL 和 SIGSTOP 不能被阻塞
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how 值:
SIG_BLOCK:把set里信号添加到blocked列表
SIG_UNBLOCK:从blocked里删除set里的信号
SIG_SETMASK:block=set
把旧的blocked保存到oldset
set 为NULL,读取现在的屏蔽值
int sigemptyset(sigset_t *set);
初始化set为0
int sigfillset(sigset_t *set);
所有信号填入set
int sigaddset(sigset_t *set, int signum);
填入signum
int sigdelset(sigset_t *set, int signum);
删除signum
返回:如果成功则为 o, 若出错则为-1 。
int sigismember(const sigset_t *set, int signum);
返回:若 signum 是 set 的成员则为 1, 如果不是则为 0 若出错则为-1
显示阻塞并不是被直接丢弃了,在恢复阻塞后,还可以接收到
修改和信号相关联的默认行为
IGSTOP和SIGKILL不能修改
|
|
grxer@Ubuntu22-wsl ~/s/test> gcc -g -O2 -o test test.c |
处理程序结束后会返回到断点处继续
阻塞掉SIGINT信号
|
grxer@Ubuntu22-wsl ~/s/test> gcc -g -O2 -o test test.c |
非本地跳转
整数除0会SIGFPE
grxer@Ubuntu22-wsl ~/s/test> ./test |
本来想着改掉SIGFPE的默认处理,看看整数除0后结果是什么样
|
结果忘了信号处理函数过后会返回到断点处,也就是导致这个异常的语句处z = z / t;然后就会一直导致SIGFPE,goto只能实现函数内的跳转,其实goto在汇编层就是一个无条件的跳转jmp,他也没能力实现函数间的跳转,除非他能把栈给平了,寄存器修正了
使用setjmp就可以做到
|
setjmp把当前程序各种信息保存到env里(typically, the stack pointer, the instruction pointer, possibly the values of other registers and the signal mask),直接调用返回0,
longjmp利用最近一次保存的env来恢复之前的环境,成功调用后不返回,原来的setjmp会再次返回第二个参数val的值
sigsetjmp和setjmp差不多只是,sigsetjmp只要第二个参数不为0就会保存信号屏蔽字,jmp过来时恢复,sigset不会
手册原话
sigsetjmp() and siglongjmp() |
sigpending()用于获取当前进程已经被阻塞但尚未处理的信号集合
|
使用sigsetjmp时
grxer@Ubuntu22-wsl ~/s/test> ./test |
使用setjmp
grxer@Ubuntu22-wsl ~/s/test> ./test |
WTF 看来是我之前理解错了,他们都不会清除当前的信号屏蔽字和已经被阻塞但尚未处理的信号集合
gdb看下
setjmp(env) != 0
pwndbg> p/x env |
sigsetjmp(env, 666) != 0
pwndbg> p/x env |
sigsetjmp确实保存了signal mask
那应该就是sigsetjmp最后会比setjmp多一个恢复过程
|
sigsetjmp时
grxer@Ubuntu22-wsl ~/s/test> ./test |
setjmp时
grxer@Ubuntu22-wsl ~/s/test> ./test |
是这个意思了,这么简单的东西刚开始居然理解错了,艹
完成一下最初想做的事
|
grxer@Ubuntu22-wsl ~/s/test> ./test |