信号
信号
- 1、产生信号
- 2、信号的种类
- 3、阻塞信号
-
查看系统到底有哪些信号: kill -l
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在 signal.h 中找到。
1)SIGHUP 关闭终端
2)SIGINT Ctrl + C
3)SIGQUIT Ctrl + \
5)SIGTRAP 从用户空间陷入内核
6)SIGABRT C库的abort函数产生的,可以把一个进程不正常的终止掉
9)SIGKILL 杀死进程
11)SIGSEGV 段错误
13)SIGPIPE 管道破裂
14)SIGALRM 定时器
15)SIGTERM kill一个进程时,默认信号
17)SIGCHLD 子进程死亡, 给父进程发送的信号
18)SIGCONT 继续 (比如音乐的播放和暂停)
19)SIGSTOP 暂停
进程收到信号的三种响应方式:
1.缺省 (查看 man 7 signal)
2.忽略信号:来了信号,不采取任何操作,SIGKILL SIGSTOP 不能被忽略
3.捕获并处理:SIGKILL SIGSTOP 不能被捕获
1、产生信号
信号产生的条件:
1.按下终端按键 Ctrl + C Ctrl + \
2.硬件异常,除0,无效内存
3.kill
4.某种软条件发生,时间到,管道破裂
//信号注册函数
void (*signal(int signo, void (*handler)(int)))(int);
//参数:signo 要注册的信号
// handler 处理信号的函数
//返回值:旧的信号处理函数
//信号处理函数
typedef void (*handler_t)(int);
//信号处理函数只带有一个参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。
handler_t signal(int num, //要注册的信号
handler_t handler) //当信号到达,要执行的操作
//handler可以是3个值:SIG_IGN 忽略信号
//SIG_DFL 缺省处理方式
//自定义函数
例子:在屏幕上打印 . 产生SIGINT Ctrl + C 信号
当信号到达:
1.保护现场
2.转去执行信号处理程序
3.恢复现场
2、信号的种类
1.可靠信号和不可靠信号
不可靠信号表现在:(编号1~31不可靠)
- 执行完信号处理函数后,自动恢复成默认处理方式(Linux已解决)
- 当多个信号同时抵达时,可能出现信号丢失 。或者叫信号压缩
可靠信号(编号34~64) 解决不可靠信号的两个问题 (编号32~33的信号内核使用)
2.实时信号和非实时信号
- 非实时信号都是不可靠信号
- 实时信号都是可靠信号
3、阻塞信号
信号在内核里的表示:
信号递达(delivery):执行信号处理动作称之为信号递达
信号未决(pending):从信号产生到信号抵达之间的状态称之为信号未决
进程可以选择阻塞(block)某个信号,被阻塞的信号产生时保持未决状态,直到解除对此信号的阻塞,才执行抵达动作
信号在内核中的表示示意图:
每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
1. SIGHUP 信号未阻塞也未产生过,当它递达时执行默认处理动作。
2. SIGINT 信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
3. SIGQUIT 信号未产生过,一旦产生 SIGQUIT 信号将被阻塞,它的处理动作是用户自定义函数 sighandler。
信号集操作函数
sigset_t 类型对于每种信号用一个 bit 表示“有效”或“无效”状态,至于这个类型内部如何存储这些 bit 则依赖于系统实现。
信号集:
int sigemptyset(sigset_t *set);//清空信号集
int sigfillset(sigset_t *set);//填充
int sigaddset(sigset_t *set, int signum);//将某一个信号加入信号集
int sigdelset(sigset_t *set, int signum);//将某一信号从信号集中删除
int sigsmember(const sigset_t *set, int signum);//判断某一信号是否在信号集中
sigprocmask
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字。
int sigprocmask(int how, // SIG_BLOCK mask |= set 在整个进程里的信号屏蔽
// SIG_UNBLOCK mask &= ~set
// SIG_SETMASK mask = set
const sigset_t *set, //我们要设置的信号屏蔽
sigset_t *oldset); //返回旧的信号屏蔽
如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在 sigprocmask返回前,至少将其中一个信号递达。
sigpending
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它立即被进程接收到。
//获取内核的未决状态集
int sigpending(sigset_t *set)
sigpending 读取当前进程的未决信号集,通过 set 参数传出。调用成功则返回 0,出错则返回-1。
程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了 SIGINT 信号,按 Ctrl-C 将会使 SIGINT 信号处于未决状态,按 Ctrl-\仍然可以终止程序,因为 SIGQUIT 信号没有阻塞。
4、捕捉信号
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
内核实现的信号的捕捉
1. 用户程序注册了 SIGQUIT 信号的处理函数 sighandler。
2. 当前正在执行 main 函数,这时发生中断或异常切换到内核态。
3. 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT递达。
4. 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行sighandler 函数,sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
5. sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。
6. 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。
sigaction
int sigaction(int signo,//要捕获的信号类型
const struct sigaction *set, //设置信号处理
struct sigaction *old);//返回旧的信号
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;//在信号处理函数期间的信号屏蔽
int sa_flag; //0
}
例:使用sigaction
pause
int pause( void ); //将当前进程挂起,转存储调度,直到有信号抵达,才唤醒该进程
pause 函数使调用进程挂起直到有信号递达。如果信号的处理动作是终止进程,则进程终止,pause 函数没有机会返回;如果信号的处理动作是忽略,则进程继续处于挂起状态,pause 不返回;如果信号的处理动作是捕捉,则调用了信号处理函数之后 pause 返回-1,errno 设置为 EINTR,所以 pause 只有出错的返回值(想想以前还学过什么函数只有出错返回值?)。错误码 EINTR 表示“被信号中断”。
例:用alarm 和pause实现sleep函数 sleep会被信号打断
1. main 函数调用 mysleep 函数,后者调用 sigaction 注册了 SIGALRM 信号的处理函数 sig_alrm。
2. 调用 alarm(nsecs)设定闹钟。
3. 调用 pause 等待,内核切换到别的进程运行。
4. nsecs 秒之后,闹钟超时,内核发 SIGALRM 给这个进程。
5. 从内核态返回这个进程的用户态之前处理未决信号,发现有 SIGALRM 信号,其处理函数是 sig_alrm。
6. 切换到用户态执行 sig_alrm 函数,进入 sig_alrm 函数时 SIGALRM 信号被自动屏蔽,从 sig_alrm 函数返回时 SIGALRM 信号自动解除屏蔽。然后自动执行系统调用 sigreturn 再次进入内核,再返回用户态继续执行进程的主控制流程(main 函数调用的 mysleep 函数)。
7. pause 函数返回-1,然后调用 alarm(0)取消闹钟,调用 sigaction 恢复SIGALRM 信号以前的处理动作。
常见的信号处理:
SIGALRM(定时器)
int alarm(int sec) sec秒后,信号SIGALRM抵达
alarm(0) 清除闹钟,可用来做超时处理
例子:实现一个考试的案例
可重入函数和不可重入函数
可重入函数指的是可以放在信号处理函数中
不可重入函数的条件:
函数内部使用了 1.使用静态变量
2.malloc和free函数
3.标准IO
例子:可重入函数可以放在信号处理函数中,但不安全
sigsuspend
sigsuspend包含了pause的挂起的功能,同时解决了竞态条件的问题,在对时序要求严格的场合下都应该调用sigsuspend而不是pause。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
//sigsuspend = pause + 信号屏蔽
和pause一样,sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。
调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。
SIGCHID信号
子进程在终止时会给父进程发 SIGCHLD 信号,该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD 信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用 wait 清理子进程即可。