目录
信号
信号介绍
信号的概念
信号是信息的载体,Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段。
信号在我们的生活中随处可见,例如:
² 古代战争中摔杯为号;
² 现代战争中的信号弹;
² 体育比赛中使用的信号枪......
信号的特点
² 简单
² 不能携带大量信息
² 满足某个特点条件才会产生
信号的机制
进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕后再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。每个进程收到的所有信号,都是由内核负责发送的。
进程A给进程B发送信号示意图:
信号四要素
每个信号必备4要素,分别是:
1)编号
2)名称
3)事件
4)默认处理动作
可通过man 7 signal查看帮助文档获取:
在标准信号中,有一些信号是有三个“Value”,第一个值通常对alpha和sparc架构有效,中间值针对x86、arm和其他架构,最后一个应用于mips架构。一个‘-’表示在对应架构上尚未定义该信号。
不同的操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内,也出现在Linux中,而有的信号出现在FreeBSD或Mac OS中却没有出现在Linux下。这里我们只研究Linux系统中的信号。
Action为默认动作:
-
Term:终止进程
-
Ign: 忽略信号 (默认即时对该种信号忽略操作)
-
Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
-
Stop:停止(暂停)进程
-
Cont:继续运行进程
注意通过man 7 signal命令查看帮助文档,其中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
这里特别强调了9) SIGKILL 和19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!
信号的状态
1) 产生
a) 当用户按某些终端键时,将产生信号。
终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT
终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT
终端上按“Ctrl+z”键通常产生中断信号 SIGSTOP 等。
b) 硬件异常将产生信号。
除数为 0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。
c) 软件异常将产生信号。
当检测到某种软件条件已发生(如:定时器alarm),并将其通知有关进程时,产生信号。
d) 调用系统函数(如:kill、raise、abort)将发送信号。
注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
e) 运行 kill /killall命令将发送信号。
此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。
2) 未决状态:没有被处理
3) 递达状态:信号被处理了
阻塞信号集和未决信号集
信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。
Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
6.1 阻塞信号集(信号屏蔽字)
将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(处理发生在解除屏蔽后)。
6.2 未决信号集
信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号集概述
在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。
这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。
信号捕捉
信号处理方式
一个进程收到一个信号的时候,可以用如下方法进行处理:
1)执行系统默认动作
对大多数信号来说,系统默认动作是用来终止该进程。
2)忽略此信号(丢弃)
接收到此信号后没有任何动作。
3)执行自定义信号处理函数(捕获)
用用户定义的信号处理函数处理该信号。
【注意】:SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。
内核实现信号捕捉过程:
如何避免僵尸进程
-
最简单的方法,父进程通过 wait() 和 waitpid() 等函数等待子进程结束,但是,这会导致父进程挂起。
-
如果父进程要处理的事情很多,不能够挂起,通过 signal() 函数人为处理信号 SIGCHLD , 只要有子进程退出自动调用指定好的回调函数,因为子进程结束后, 父进程会收到该信号 SIGCHLD ,可以在其回调函数里调用 wait() 或 waitpid() 回收。
示例程序:
void sig_child(int signo) { pid_t pid; //处理僵尸进程, -1 代表等待任意一个子进程, WNOHANG代表不阻塞 while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) { printf("child %d terminated.\n", pid); } } int main() { pid_t pid; // 创建捕捉子进程退出信号 // 只要子进程退出,触发SIGCHLD,自动调用sig_child() signal(SIGCHLD, sig_child); pid = fork(); // 创建进程 if (pid < 0) { // 出错 perror("fork error:"); exit(1); } else if (pid == 0) { // 子进程 printf("I am child process,pid id %d.I am exiting.\n", getpid()); exit(0); } else if (pid > 0) { // 父进程 sleep(2); // 保证子进程先运行 printf("I am father, i am exited\n\n"); system("ps -ef | grep defunct"); // 查看有没有僵尸进程 } return 0; }
3)如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
示例程序:
int main()
{
pid_t pid;// 忽略子进程退出信号的信号
// 那么子进程结束后,内核会回收, 并不再给父进程发送信号
signal(SIGCHLD, SIG_IGN);pid = fork(); // 创建进程
if (pid < 0)
{ // 出错
perror("fork error:");
exit(1);
}
else if (pid == 0)
{ // 子进程
printf("I am child process,pid id %d.I am exiting.\n", getpid());
exit(0);}
else if (pid > 0)
{ // 父进程
sleep(2); // 保证子进程先运行
printf("I am father, i am exited\n\n");
system("ps -ef | grep defunct"); // 查看有没有僵尸进程
}return 0;
}