文章目录

一、信号的基本概念

1.常见信号的使用场景

(第33章)Linux系统编程之信号_其他

  • 注意, Ctrl-C产生的信号只能发给前台进程。 “wait和waitpid函数”中我们看到一个命令后面加个 & 可以放到后台运行, 这样Shell不必等待进程结束就可以接受新的命令, 启动新的进程。
  • Shell可以同时运行一个前台进程和任意多个后台进程, 只有前台进程才能接到像Ctrl-C这种控制键产生的信号。
  • 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号, 也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止, 所以信号相对于进程的控制流程来说是异步( Asynchronous) 的。

2.kill -l:查看系统定义 的信号列表

(第33章)Linux系统编程之信号_其他_02

  • 这些信号各自在什么条件下产生, 默认的处理动作是什么, 在 signal(7) 中都有详细说明
    (第33章)Linux系统编程之信号_僵尸进程_03

3.产生信号的条件

(第33章)Linux系统编程之信号_其他_04
(第33章)Linux系统编程之信号_僵尸进程_05

4.用户程序可以调用 sigaction(2) 函数告诉内核如何处理某种信号

可选的处理动作有以下三种:

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数, 要求内核在处理该信号时切换到用户态执行这个处理函数, 这种方式称为捕捉( 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
  • 然后写一个死循环程序:
    (第33章)Linux系统编程之信号_信号处理_06
    (第33章)Linux系统编程之信号_信号处理_07

3.调用系统函数向进程发信号

(1)kill -SIGSEGV 7940等价于kill -11 7940

  • 仍以上一节的死循环程序为例, 首先在后台执行这个程序, 然后用 kill 命令给它发 SIGSEGV 信号
    (第33章)Linux系统编程之信号_僵尸进程_08

(2)kill 命令是调用 kill 函数实现的;raise函数;abort函数

  • kill 函数可以给一个指定的进程发送指定的信号。 raise 函数可以给当前进程发送指定的信号
    (第33章)Linux系统编程之信号_信号处理_09

4.由软件条件产生信号:alarm和SIGALRM

(第33章)Linux系统编程之信号_其他_10

三、阻塞信号

1.信号在内核中的表示

  • 实际执行信号的处理动作称为信号递达( Delivery) ;
  • 信号从产生到递达之间的状态, 称为信号未决( Pending)
  • 进程可以选择阻塞( Block) 某个信号。 被阻塞的信号产生时将保持在未决状态, 直到进程解除对此信号的阻塞, 才执行递达的动作。 注意, 阻塞和忽略是不同的, 只要信号被阻塞就不会递达, 而忽略是在递达之后可选的一种处理动作

(1)信号在内核中的示意图

(第33章)Linux系统编程之信号_僵尸进程_11

  • 每个信号都有两个标志位分别表示阻塞和未决, 还有一个函数指针表示处理动作。
  • 信号产生时, 内核在进程控制块中设置该信号的未决标志, 直到信号递达才清除该标志
  • 在上图的例子中
    (第33章)Linux系统编程之信号_其他_12

(2)如果在进程解除对某信号的阻塞之前这种信号产生过多次, 将如何处理?

  • POSIX.1允许系统递送该信号一次或多次。 Linux是这样实现的: 常规信号在递达之前产生多次只计一次, 而实时信号在递达之前产生多次可以依次放在一个队列里。 这里不讨论实时信号。
  • 因此, 未决和阻塞标志可以用相同的数据类型 sigset_t 来存储, sigset_t 称为信号集, 这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞, 而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
  • 阻塞信号集也叫做当前进程的信号屏蔽字( SignalMask) , 这里的“屏蔽”应该理解为阻塞而不是忽略

2.信号集操作函数

printf 直接打印 sigset_t 变量是没有意义的

  • sigset_t 类型对于每种信号用一个bit表示“有效”或“无效”状态, 至于这个类型内部如何存储这些bit则依赖于系统实现, 从使用者的角度是不必关心的, 使用者只能调用以下函数来操作 sigset_t 变量, 而不应该对它的内部数据做任何解释
    (第33章)Linux系统编程之信号_僵尸进程_13
  • 函数 sigemptyset 初始化 set 所指向的信号集, 使其中所有信号的对应bit清零, 表示该信号集不包含任何有效信号。
  • 函数 sigfillset 初始化 set 所指向的信号集, 使其中所有信号的对应bit置位, 表示该信号集的有效信号包括系统支持的所有信号。
  • 在使用 sigset_t 类型的变量之前, 一定要调用 sigemptyset 或 sigfillset 做初始化, 使信号集处于确定的状态。
  • 初始化 sigset_t 变量之后,就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
  • 这四个函数都是成功返回0, 出错返回-1。
  • sigismember 是一个布尔函数, 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回1, 不包含则返回0, 出错返回-1。

3.sigprocmask

(1) 调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字
(第33章)Linux系统编程之信号_其他_14

  • 返回值: 若成功则为0, 若出错则为-1
  • 如果 oset 是非空指针, 则读取进程的当前信号屏蔽字通过 oset 参数传出
  • 如果 set 是非空指针, 则更改进程的信号屏蔽字, 参数 how 指示如何更改
  • 如果 oset 和 set 都是非空指针, 则先将原来的信号屏蔽字备份到 oset 里, 然后根据 set 和 how 参数更改信号屏蔽字

(2)假设当前的信号屏蔽字为 mask , 下表说明了 how 参数的可选值
(第33章)Linux系统编程之信号_僵尸进程_15

4. sigpending

(第33章)Linux系统编程之信号_其他_16

  • sigpending 读取当前进程的未决信号集, 通过 set 参数传出。 调用成功则返回0, 出错则返回-1。

5.eg

  • 把s和p都当作去,操作同一信号集
    (第33章)Linux系统编程之信号_控制流_17
    (第33章)Linux系统编程之信号_僵尸进程_18
四、捕捉信号

1.捕捉信号的含义

  • 如果信号的处理动作是用户自定义函数, 在信号递达时就调用这个函数, 这称为捕捉信号。
  • 由于信号处理函数的代码是在用户空间的, 处理过程比较复杂, 举例如下:
    (第33章)Linux系统编程之信号_信号处理_19
    (第33章)Linux系统编程之信号_信号处理_20

(第33章)Linux系统编程之信号_信号处理_21

2.sigaction

  • sigaction 函数可以读取和修改与指定信号相关联的处理动作。 调用成功则返回0, 出错则返回-1
    (第33章)Linux系统编程之信号_僵尸进程_22

  • signo 是指定信号的编号。

  • 若 act 指针非空, 则根据 act 修改该信号的处理动作。

  • 若 oact 指针非空, 则通过 oact 传出该信号原来的处理动作。

  • act 和 oact 指向 sigaction 结构体
    (第33章)Linux系统编程之信号_控制流_23
    (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 函数使调用进程挂起直到有信号递达
    (第33章)Linux系统编程之信号_其他_24

  • 如果信号的处理动作是终止进程, 则进程终止, 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;
}

(第33章)Linux系统编程之信号_僵尸进程_25

4.可重入函数

  • 当捕捉到信号时, 不论进程的主控制流程当前执行到哪儿, 都会先跳到信号处理函数中执行, 从信号处理函数返回后再继续执行主控制流程。
  • 信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的, 二者不存在调用和被调用的关系, 并且使用不同的堆栈空间。
  • 引入了信号处理函数使得一个进程具有多个控制流程, 如果这些控制流程访问相同的全局资源( 全局变量、 硬件资源等) , 就有可能出现冲突
  • eg
    (第33章)Linux系统编程之信号_信号处理_26
    (第33章)Linux系统编程之信号_僵尸进程_27
    (第33章)Linux系统编程之信号_僵尸进程_28

5.sig_atomic_t类型与volatile限定符

(第33章)Linux系统编程之信号_其他_29
(第33章)Linux系统编程之信号_信号处理_30
(第33章)Linux系统编程之信号_信号处理_31
(第33章)Linux系统编程之信号_控制流_32
(第33章)Linux系统编程之信号_信号处理_33
(第33章)Linux系统编程之信号_控制流_34
(第33章)Linux系统编程之信号_信号处理_35

6.竞态条件与sigsuspend函数

(第33章)Linux系统编程之信号_信号处理_36
(第33章)Linux系统编程之信号_控制流_37
(第33章)Linux系统编程之信号_信号处理_38
(第33章)Linux系统编程之信号_控制流_39
(第33章)Linux系统编程之信号_僵尸进程_40
(第33章)Linux系统编程之信号_僵尸进程_41

7.SIGCHLD信号

(1)wait和waitpid函数清理僵尸进程

(第33章)Linux系统编程之信号_信号处理_42

(2)不产生僵尸进程的其它办法

(第33章)Linux系统编程之信号_控制流_43