先备知识:fork()系统调用、wait()系统调用
信号是软件生成的中断,比如用户按下 Ctrl-C
或一个进程想要通知另一个进程某些内容,此时 OS 会将该信号发送到指定的进程。
存在可以发送到进程的固定信号集。信号由整数标识,其具有指定符号名称。例如,SIGCHLD
是当子进程终止时发送到父进程的信号的编号。
以下是一些常用的信号
#define SIGHUP 1 /* 挂起进程 */
#define SIGINT 2 /* 中断进程 */
#define SIGQUIT 3 /* 退出进程 */
#define SIGILL 4 /* 非法指令 */
#define SIGTRAP 5 /* 追踪陷阱 */
#define SIGABRT 6 /* 中止 */
信号的 OS 结构
对于每个进程,操作系统会维护2个整数,它们的每一位对应于信号的编号
两个整数分别跟踪的是:挂起的信号和阻塞的信号。
使用32位整数,可以表示多达32个不同的信号
例如
在下面的示例中,SIGINT(=2) 信号被阻塞,没有信号挂起
向进程发送某个信号时,操作系统会根据信号编号,来修改挂起整数中对应的位值来达到将该信号挂起的目的。每次OS选择要在处理器上运行的某个进程时,都会检查挂起整数和阻塞整数。
- 如果没有挂起的信号,则正常重新启动进程,并在下一条指令处继续执行。
- 如果有 1 个或多个信号挂起,但每个信号都被阻塞,则进程也会正常重新启动,但信号仍标记为挂起。
- 如果有 1 个或多个信号挂起且未被阻塞,则OS执行进程代码中的信号处理例程来处理信号。处理完的信号,它对应的挂起位被清除。
默认对信号的处理
有几个默认的信号程序处理例程。每个信号都与这些默认处理程序例程之一相关联。不同的默认处理程序例程通常具有以下操作之一:
- Ign:忽略信号;即什么都不做,只需返回
- Term:终止进程
- Cont:取消阻止已停止的进程
- Stop:阻止进程
示例代码
// 演示默认信号处理例程的C程序
#include<stdio.h>
#include <unistd.h>
int main()
{
while (1) {
printf(“hello world\n”);
sleep(1);
}
return 0;
}
输出
hello world
hello world
hello world
terminated
上面代码中,输出将无限次打印hello world
如果用户按 Ctrl-C 来发送 SIGINT 信号,则其默认处理程序将终止进程。
用户定义的信号处理程序
除了 SIGKILL 以外,用户可以用自己定义的信号处理例程来替换几乎所有的默认处理例程。
信号处理例程可以有任何名称,但必须具有返回类型void和一个int参数。
例如:您可以为 SIGCHLD 信号(子进程的终止)的信号处理程序选择名称 sigchld_handler 。那么声明将是:
void sigchld_handler(int sig);
当信号处理程序执行时,传递给它的参数是信号的编号。程序员可以使用相同的信号处理函数来处理多个信号。在这种情况下,处理程序需要检查参数以查看发送了哪个信号。另一方面,如果一个信号处理函数只处理一个信号,那么就没有必要费心检查参数,因为它总是那个信号编号。
示例代码
// 演示用户自定义信号处理例程的C程序
#include<stdio.h>
#include<signal.h>
#include <stdlib.h>
#include <unistd.h>
// SIGINT的例程,由键盘上的 Ctrl-C 触发
void handle_sigint(int sig)
{
printf("Caught signal %d\n", sig);
}
int main()
{
signal(SIGINT, handle_sigint);
while(1) {
sleep(1);
}
return 0;
}
输出
^CCaught signal 2 // 当用户按下 Ctrl-C
^CCaught signal 2
发送信号
我们可以使用kill()向进程发送信号,语法如下:
// pid: 目标进程的 id
// signal: 要发送的信号类型
// Return value: 如果信号发送成功,则返回值为 0
int kill(pid_t pid, int signal);
例如
pid_t mypid = getpid(); // 进程自己的 pid
kill(mypid, SIGINT); // 进程向自身发送SIGINT信号,自杀
// 因为SIGINT信号默认处理程序是终止进程
练习
1. 下面程序将输出什么?
#include <stdio.h>
#include <wait.h>
#include <signal.h>
#include <unistd.h>
int main()
{
int stat;
pid_t pid;
if ((pid = fork()) == 0)
while(1) ;
else {
kill(pid, SIGINT);
wait(&stat);
if (WIFSIGNALED(stat))
psignal(WTERMSIG(stat), "Child term due to");
}
}
输出:
Child term due to: Interrupt
这个程序中,子进程什么都没做就陷入了死循环,主进程在创建完子进程后就发送终止信号给子进程,由于子进程没有设置处理该信号的回调例程,所以调用的默认信号处理例程,即终止该进程。主进程发送完信号等待子进程退出,由于上面原因则获取子进程退出的原因必然是 Interrupt。
2. 预测下面程序的执行结果?
#include <stdio.h>
#include <signal.h>
#include <wait.h>
#include <unistd.h>
#include <stdlib.h>
int val = 10;
void handler(int sig)
{
val += 5;
}
int main()
{
pid_t pid;
signal(SIGCHLD, handler);
if ((pid = fork()) == 0) {
val -= 3;
exit(0);
}
waitpid(pid, NULL, 0);
printf("val = %d\n", val);
exit(0);
}
输出:
val = 15
这个程序中父进程设置了一个 SIGCHLD 信号的回调函数 handler 用来处理子进程退出时的情况,然后就创建了一个子进程。子进程执行完 val -= 3 就退出了。 同时另一方面父进程创建完子进程后就等待子进程退出,因为上面父进程设置了子进程一旦退出就调用的 handler 函数,所以在执行 printf 之前,程序先进入执行 handler,val 值变成了15,至于在子进程中对 val 的修改并不会影响父进程,所以最终值是15。
3. 考虑以下代码,输出是什么?
#include <stdio.h>
#include <wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
pid_t pid;
int counter = 0;
void handler1(int sig)
{
counter++;
printf("counter = %d\n", counter);
/* 将打印的字符串刷新到stdout */
fflush(stdout);
kill(pid, SIGUSR1);
}
void handler2(int sig)
{
counter += 3;
printf("counter = %d\n", counter);
exit(0);
}
int main()
{
pid_t p;
int status;
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while(1) ;
}
if ((p = wait(&status)) > 0) {
counter += 4;
printf("counter = %d\n", counter);
}
}
输出:
counter = 1 // parent’s handler
counter = 3 // child’s handler
counter = 5 // parent’s main
该程序中,父进程注册了信号回调函数 handler1,而子进程注册了信号回调函数 handler2。一方面子进程注册完后向父进程发送 SIGUSR1 信号,然后进入死循环。同时另一方面,父进程创建完子进程就挂起等待子进程退出。那么这个过程父进程的回调函数 handler1 最先被调用,一开始 counter = 0,所以后打印 1;之后父进程在 handler1 函数里也向子进程发送 SIGUSR1 信号,所以接着子进程调用回调函数 handler2,因为子进程此时自己的 counter 变量还是 0,所以打印 3,打印完子进程退出,系统检查到子进程退出,解除父进程的挂起,向下执行,所以打印 5。
网友评论