1.常见信号的使用场景
- 注意, Ctrl-C产生的信号只能发给前台进程。 “wait和waitpid函数”中我们看到一个命令后面加个 & 可以放到后台运行, 这样Shell不必等待进程结束就可以接受新的命令, 启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程, 只有前台进程才能接到像Ctrl-C这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号, 也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止, 所以信号相对于进程的控制流程来说是异步( Asynchronous) 的。
2.kill -l:查看系统定义 的信号列表
- 这些信号各自在什么条件下产生, 默认的处理动作是什么, 在 signal(7) 中都有详细说明
3.产生信号的条件
4.用户程序可以调用 sigaction(2) 函数告诉内核如何处理某种信号
可选的处理动作有以下三种:
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数, 要求内核在处理该信号时切换到用户态执行这个处理函数, 这种方式称为捕捉( Catch) 一个信号。
1.Core Dump文件的含义
- 当一个进程要异常终止时, 可以选择把进程的用户空间内存数据全部保存到磁盘上, 文件名通常是 core , 这叫做Core Dump。
- 进程异常终止通常是因为有Bug, 比如非法内存访问导致段错误, 事后可以用调试器检查 core 文件以查清错误原因, 这叫做Post-mortem Debug。
- 一个进程允许产生多大的 core 文件取决于进程的Resource Limit( 这个信息保存在PCB中) 。 默认是不允许产生 core 文件的, 因为 core 文件中可能包含用户密码等敏感信息, 不安全。
- 在开发调试阶段可以用 ulimit 命令改变这个限制, 允许产生 core 文件
2.ulimit -c 1024
- 首先用 ulimit 命令改变Shell进程的Resource Limit, 允许 core 文件最大为1024K:
ulimit -c 1024
- 然后写一个死循环程序:
3.调用系统函数向进程发信号
(1)kill -SIGSEGV 7940等价于kill -11 7940
- 仍以上一节的死循环程序为例, 首先在后台执行这个程序, 然后用 kill 命令给它发 SIGSEGV 信号
(2)kill 命令是调用 kill 函数实现的;raise函数;abort函数
- kill 函数可以给一个指定的进程发送指定的信号。 raise 函数可以给当前进程发送指定的信号
4.由软件条件产生信号:alarm和SIGALRM
三、阻塞信号1.信号在内核中的表示
- 实际执行信号的处理动作称为信号递达( Delivery) ;
- 信号从产生到递达之间的状态, 称为信号未决( Pending)
- 进程可以选择阻塞( Block) 某个信号。 被阻塞的信号产生时将保持在未决状态, 直到进程解除对此信号的阻塞, 才执行递达的动作。 注意, 阻塞和忽略是不同的, 只要信号被阻塞就不会递达, 而忽略是在递达之后可选的一种处理动作
(1)信号在内核中的示意图
- 每个信号都有两个标志位分别表示阻塞和未决, 还有一个函数指针表示处理动作。
- 信号产生时, 内核在进程控制块中设置该信号的未决标志, 直到信号递达才清除该标志
- 在上图的例子中
(2)如果在进程解除对某信号的阻塞之前这种信号产生过多次, 将如何处理?
- POSIX.1允许系统递送该信号一次或多次。 Linux是这样实现的: 常规信号在递达之前产生多次只计一次, 而实时信号在递达之前产生多次可以依次放在一个队列里。 这里不讨论实时信号。
- 因此, 未决和阻塞标志可以用相同的数据类型 sigset_t 来存储, sigset_t 称为信号集, 这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞, 而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
- 阻塞信号集也叫做当前进程的信号屏蔽字( SignalMask) , 这里的“屏蔽”应该理解为阻塞而不是忽略
2.信号集操作函数
printf 直接打印 sigset_t 变量是没有意义的
-
sigset_t 类型对于每种信号用一个bit表示“有效”或“无效”状态, 至于这个类型内部如何存储这些bit则依赖于系统实现, 从使用者的角度是不必关心的, 使用者只能调用以下函数来操作 sigset_t 变量, 而不应该对它的内部数据做任何解释
- 函数 sigemptyset 初始化 set 所指向的信号集, 使其中所有信号的对应bit清零, 表示该信号集不包含任何有效信号。
- 函数 sigfillset 初始化 set 所指向的信号集, 使其中所有信号的对应bit置位, 表示该信号集的有效信号包括系统支持的所有信号。
- 在使用 sigset_t 类型的变量之前, 一定要调用 sigemptyset 或 sigfillset 做初始化, 使信号集处于确定的状态。
- 初始化 sigset_t 变量之后,就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
- 这四个函数都是成功返回0, 出错返回-1。
- sigismember 是一个布尔函数, 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回1, 不包含则返回0, 出错返回-1。
3.sigprocmask
(1) 调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字
- 返回值: 若成功则为0, 若出错则为-1
- 如果 oset 是非空指针, 则读取进程的当前信号屏蔽字通过 oset 参数传出
- 如果 set 是非空指针, 则更改进程的信号屏蔽字, 参数 how 指示如何更改
- 如果 oset 和 set 都是非空指针, 则先将原来的信号屏蔽字备份到 oset 里, 然后根据 set 和 how 参数更改信号屏蔽字
(2)假设当前的信号屏蔽字为 mask , 下表说明了 how 参数的可选值
4. sigpending
- sigpending 读取当前进程的未决信号集, 通过 set 参数传出。 调用成功则返回0, 出错则返回-1。
5.eg
- 把s和p都当作去,操作同一信号集
1.捕捉信号的含义
- 如果信号的处理动作是用户自定义函数, 在信号递达时就调用这个函数, 这称为捕捉信号。
-
由于信号处理函数的代码是在用户空间的, 处理过程比较复杂, 举例如下:
2.sigaction
-
sigaction 函数可以读取和修改与指定信号相关联的处理动作。 调用成功则返回0, 出错则返回-1
-
signo 是指定信号的编号。
-
若 act 指针非空, 则根据 act 修改该信号的处理动作。
-
若 oact 指针非空, 则通过 oact 传出该信号原来的处理动作。
-
act 和 oact 指向 sigaction 结构体
(1) -
将 sa_handler 赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号, 赋值为常数 SIG_DFL 表示执行系统默认动作, 赋值为一个函数指针表示用自定义函数捕捉信号, 或者说向内核注册了一个信号处理函数, 该函数返回值为 void , 可以带一个 int 参数, 通过参数可以得知当前信号的编号, 这样就可以用同一个函数处理多种信号。
-
显然, 这也是一个回调函数, 不是被 main 函数调用, 而是被系统所调用
-
当某个信号的处理函数被调用时, 内核自动将当前信号加入进程的信号屏蔽字, 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时, 如果这种信号再次产生, 那么它会被阻塞到当前处理结束为止
-
如果在调用信号处理函数时, 除了当前信号被自动屏蔽之外, 还希望自动屏蔽另外一些信号, 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字
(2)sa_flags 字段包含一些选项, 本章的代码都把 sa_flags 设为0, sa_sigaction 是实时信号的处理函数
3.pause
(1)
-
pause 函数使调用进程挂起直到有信号递达
-
如果信号的处理动作是终止进程, 则进程终止, pause 函数没有机会返回;
-
如果信号的处理动作是忽略, 则进程继续处于挂起状态, pause 不返回;
-
如果信号的处理动作是捕捉, 则调用了信号处理函数之后 pause 返回-1, errno 设置为 EINTR , 所以 pause 只有出错的返回值
-
错误码 EINTR 表示“被信号中断”。
(2)eg:下面我们用 alarm 和 pause 实现 sleep(3) 函数, 称为 mysleep
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void sig_alrm(int signo)
{
/* nothing to do */
}
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
unsigned int unslept;
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
alarm(nsecs);
pause();
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL);
return unslept;
}
int main(void)
{
while(1){
mysleep(2);
printf("Two seconds passed\n");
}
return 0;
}
4.可重入函数
- 当捕捉到信号时, 不论进程的主控制流程当前执行到哪儿, 都会先跳到信号处理函数中执行, 从信号处理函数返回后再继续执行主控制流程。
- 信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的, 二者不存在调用和被调用的关系, 并且使用不同的堆栈空间。
- 引入了信号处理函数使得一个进程具有多个控制流程, 如果这些控制流程访问相同的全局资源( 全局变量、 硬件资源等) , 就有可能出现冲突
- eg