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也没意思,这也是为什么叫不可靠信号的原因,后面序号的可靠信号都是通过队列来实现

发送信号

  1. 内核的异常处理程序

  2. /bin/kill发送

/bin/kill -signalid pid

pid为正数给pid进程 发送signalid信号

pid为负数给pid进程组所有进程发送signalid信号

  1. 键盘发送信号 ctrl c ctrl z等
  2. kill函数显式地要求内核发送一个信号给目的进程。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
成功返回0 否则返回-1;
pid >0 发送sig给进程pid;
pid =0 发送sig给调用进程所在进程组的每一个进程(包括自己);
pid <0 发送sig给|pid|进程组的每一个进程
  1. alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int secs);
在secs秒后 发送一个SIGALRM信号给调用进程 secs为0不会调度安排新闹钟,任何情况下都会取消待处理(pending)的闹钟,返回剩余秒数,没有待处理的闹钟返回0

接受信号

隐式阻塞机制

内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号。也就是信号处理程序执行时不接受正在处理类型的信号

显示阻塞机制

应用程序可以使用 sigprocmask 函数和它的辅助函数,来设置信号屏蔽字,明确地阻塞和解除阻塞选定的信号。SIGKILL 和 SIGSTOP 不能被阻塞

#include <signal.h>
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
setNULL,读取现在的屏蔽值

int sigemptyset(sigset_t *set);
初始化set0
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不能修改

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
返回:若成功则为指向前次处理程序的指针,若出错则为SIG_ERRC不设置errno);
handler 是SIG_IGN 忽略signum该类型信号;
handler 是SIG_DFL signum恢复默认行为;
handler 是个函数地址 设置该函数为异常处理函数
#include <stdio.h>
#include <signal.h>
#include <assert.h>
#include <stdlib.h>
void signal_handler(int sig) {
puts("ctrl c");
}
int main() {
__sighandler_t rs = signal(SIGINT, signal_handler);
assert(rs != SIG_ERR);
while (1);
}
grxer@Ubuntu22-wsl ~/s/test> gcc -g -O2 -o test test.c
grxer@Ubuntu22-wsl ~/s/test> ./test
^Cctrl c
^Cctrl c
^Cctrl c
^Cctrl c
^Cctrl c

处理程序结束后会返回到断点处继续

阻塞掉SIGINT信号

#include <assert.h>
#include <stdlib.h>
void signal_handler(int sig) {
puts("ctrl c");
}
int main() {
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK,&mask,NULL);
__sighandler_t rs = signal(SIGINT, signal_handler);
assert(rs != SIG_ERR);
while (1);
}
grxer@Ubuntu22-wsl ~/s/test> gcc -g -O2 -o test test.c
grxer@Ubuntu22-wsl ~/s/test> ./test
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^Z
fish: Job 6, './test' has stopped

非本地跳转

整数除0会SIGFPE

grxer@Ubuntu22-wsl ~/s/test> ./test
fish: Job 1, './test' terminated by signal SIGFPE (Floating point exception)

本来想着改掉SIGFPE的默认处理,看看整数除0后结果是什么样

#include <stdio.h>
#include <signal.h>
#include <assert.h>
#include <stdlib.h>
void signal_handler(int sig) {
puts("xxx");
}
int main() {
__sighandler_t rs = signal(SIGFPE, signal_handler);
assert(rs != SIG_ERR);
int z = 12;
int t = 0;
z = z / t;
printf("%d\n", z);
}

结果忘了信号处理函数过后会返回到断点处,也就是导致这个异常的语句处z = z / t;然后就会一直导致SIGFPE,goto只能实现函数内的跳转,其实goto在汇编层就是一个无条件的跳转jmp,他也没能力实现函数间的跳转,除非他能把栈给平了,寄存器修正了

使用setjmp就可以做到

#include <setjmp.h>
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);
void longjmp(jmp_buf env, int val);
void siglongjmp(sigjmp_buf env, int val);

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()
sigsetjmp() and siglongjmp() also perform nonlocal gotos, but provide
predictable handling of the process signal mask.

If, and only if, the savesigs argument provided to sigsetjmp() is non‐
zero, the process's current signal mask is saved in env and will be re‐
stored if a siglongjmp() is later performed with this env.

sigpending()用于获取当前进程已经被阻塞但尚未处理的信号集合

#include <stdio.h>
#include <signal.h>
#include <assert.h>
#include <setjmp.h>
#include <stdlib.h>
jmp_buf env;
int main() {
sigset_t mask, pendmask, blockmask;
sigemptyset(&mask);
sigemptyset(&pendmask);
sigemptyset(&blockmask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, NULL);
// if (sigsetjmp(env, 666) != 0) {
if (setjmp(env) != 0) {
sigpending(&pendmask);
sigprocmask(SIG_BLOCK, NULL, &blockmask);
if (sigismember(&blockmask, SIGINT))
puts("in blockmask");
else
puts("not in blockmask");
if (sigismember(&pendmask, SIGINT))
puts("in pendmask");
else
puts("not in pendmask");
exit(0);
}
sleep(2);//时间间隔我疯狂按ctrl c :)
// siglongjmp(env, 1);
longjmp(env, 1);
}

使用sigsetjmp时

grxer@Ubuntu22-wsl ~/s/test> ./test
^C^C^C^Cin blockmask
in pendmask

使用setjmp

grxer@Ubuntu22-wsl ~/s/test> ./test
^C^C^C^Cin blockmask
in pendmask

WTF 看来是我之前理解错了,他们都不会清除当前的信号屏蔽字和已经被阻塞但尚未处理的信号集合

gdb看下

setjmp(env) != 0

pwndbg> p/x env
$2 = {{
__jmpbuf = {0x0, 0xfa3c37809f472823, 0x7fffffffdfd8, 0x555555555269, 0x555555557d78, 0x7ffff7ffd040, 0xfa3c378098a72823, 0xaf6962d587272823},
__mask_was_saved = 0x0,
__saved_mask = {
__val = {0x0 <repeats 16 times>}
}
}}

sigsetjmp(env, 666) != 0

pwndbg> p/x env
$2 = {{
__jmpbuf = {0x0, 0x6cdae548d2b8f980, 0x7fffffffdfd8, 0x555555555269, 0x555555557d78, 0x7ffff7ffd040, 0x6cdae548d558f980, 0x398fb01dcad2f980},
__mask_was_saved = 0x1,
__saved_mask = {
__val = {0x2, 0x0 <repeats 15 times>}
}
}}

sigsetjmp确实保存了signal mask

那应该就是sigsetjmp最后会比setjmp多一个恢复过程

#include <stdio.h>
#include <signal.h>
#include <assert.h>
#include <setjmp.h>
#include <stdlib.h>
jmp_buf env;
sigset_t mask, pendmask, blockmask;
void signal_handler(int sig) {
printf("\nctrl+\\\n");
siglongjmp(env, 1);
// longjmp(env, 1);
}
int main() {
assert(sigemptyset(&mask)==0);
sigemptyset(&pendmask);
sigemptyset(&blockmask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, NULL);
__sighandler_t rs = signal(SIGQUIT, signal_handler);/*ctrl+\*/
assert(rs != SIG_ERR);
if (sigsetjmp(env, 1) != 0) {
// if (setjmp(env) != 0) {
sigpending(&pendmask);
sigprocmask(SIG_BLOCK, NULL, &blockmask);
if (sigismember(&blockmask, SIGINT))
puts("in blockmask");
else
puts("not in blockmask");
exit(0);
}
sigprocmask(SIG_UNBLOCK, &mask, NULL);//取消阻塞
sleep(2);/*按下ctrl+\*/

}

sigsetjmp时

grxer@Ubuntu22-wsl ~/s/test> ./test
^\
ctrl+\
in blockmask

setjmp时

grxer@Ubuntu22-wsl ~/s/test> ./test
^\
ctrl+\
not in blockmask

是这个意思了,这么简单的东西刚开始居然理解错了,艹

完成一下最初想做的事

#include <stdio.h>
#include <signal.h>
#include <assert.h>
#include <setjmp.h>
#include <stdlib.h>
jmp_buf env;
void signal_handler(int sig) {
puts("xxx");
siglongjmp(env, 1);
}
int main() {
__sighandler_t rs = signal(SIGFPE, signal_handler);
assert(rs != SIG_ERR);
int z = 12;
int t = 0;
if (sigsetjmp(env, 1) ==0 ) {
z = z / t;
}
printf("%d\n", z);
}
grxer@Ubuntu22-wsl ~/s/test> ./test
xxx
12