Linux 进程间通信(三)信号通信
1 信号概述
信号是在软件层次上对中断机制的一种模拟。 在原理上, 一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。 信号是异步的, 一个进程不必通过任何操作来等待信号的到达, 事实上, 进程也不知道信号到底什么时候到达。 信号可以直接进行用户空间进程和内核进程之间的交互, 内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
它可以在任何时候发给某一进程, 而无须知道该进程的状态。 如果该进程当前并未处于执行态, 则该信号就由内核保存起来, 直到该进程恢复执行再传递给它为止; 如果一个信号被进程设置为阻塞, 则该信号的传递被延迟, 直到其阻塞被取消时才被传递给进程。
信号是进程间通信机制中唯一的异步通信机制, 可以看做是异步通知, 通知接收信号的进程有哪些事情发生了。 信号机制经过 Posix 实时扩展后, 功能更加强大, 除了基本通知功能外, 还可以传递附加信息。
信号事件的发生有两个来源:
硬件来源:如我们按下了键盘上的按钮或者出现其他硬件故障;
软件来源:最常用发送信号的系统函数有 kill()、raise()、alarm()、setitimer()和 sigqueue()等, 软件来源还包括一些非法运算等操作。
进程可以通过 3 种方式来响应一个信号
1. 忽略信号
忽略信号即对信号不做任何处理, 其中, 有两个信号不能忽略: SIGKILL 和 SIGSTOP。
2. 捕捉信号
定义信号处理函数, 当信号发生时, 执行相应的处理函数。
3. 执行默认操作
Linux 对每种信号都规定了默认操作,
如表所示。
信号名 信号id 含义
(1)SIGINT 2 Ctrl+C时OS送给前台进程组中每个进程
(2)SIGABRT 6 调用abort函数,进程异常终止
(3)SIGPOLL SIGIO 8 指示一个异步IO事件,在高级IO中提及
(4)SIGKILL 9 杀死进程的终极办法
(5)SIGSEGV 11 无效存储访问时OS发出该信号
(6)SIGPIPE 13 涉及管道和socket
(7)SIGALARM 14 涉及alarm函数的实现
(8)SIGTERM 15 kill命令发送的OS默认终止信号
(9)SIGCHLD 17 子进程终止或停止时OS向其父进程发此信号
(10)
SIGUSR1 10 用户自定义信号,作用和意义由应用自己定义
SIGUSR2 12
一个完整的信号生命周期可以分为 3 个重要阶段,这 3 个阶段由 4 个重要事件来刻画的:
信号产生、 信号在进程中注册、 信号在进程中注销、 执行信号处理函数。
这里信号的产生、注册、 注销等是指信号的内部实现机制, 而不是信号的函数实现。 因此, 信号注册与否与本节后面讲到的发送信号函数(如 kill()等) 及信号安装函数(如 signal()等) 无关, 只与信号值有关。相邻两个事件的时间间隔构成信号生命周期的一个阶段。要注意这里的信号处理有多种方式, 一般是由内核完成的, 当然也可以由用户进程来完成, 故在此没有明确指出。
信号的处理包括信号的发送、 捕获及信号的处理, 它们有各自相对应的常见函数。
发送信号的函数: kill()、 raise()。
捕捉信号的函数: alarm()、 pause()。
处理信号的函数: signal()、 sigaction()。
2 信号发送与捕捉
2.1 信号发送: kill()和 raise()
kill()函数同读者熟知的 kill 系统命令一样, 可以发送信号给进程或进程组(实际上, kill系统命令只是 kill()函数的一个用户接口)。 这里需要注意的是, 它不仅可以中止进程(实际上发出 SIGKILL 信号), 也可以向进程发送其他信号。
与 kill()函数不同的是, raise()函数只允许进程向自身发送信号。
kill()函数的语法要点
raise()函数的语法要点。 只能进程向自身发送信号
下面的示例首先使用 fork()创建了一个子进程, 接着为了保证子进程不在父进程调用kill()之前退出, 在子进程中使用 raise()函数向自身发送 SIGSTOP 信号, 使子进程暂停。 接下来在父进程中调用 kill()向子进程发送信号, 在该示例中使用的是 SIGKILL, 读者可以使用其他信号进行练习。
/* kill_raise.c */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int ret;
/* 创建一个子进程 */
if ((pid = fork()) < 0)
{
printf("Fork error\n");
exit(1);
}
if (pid == 0)
{
/* 在子进程中使用 raise()函数发出 SIGSTOP 信号, 使子进程暂停 */
sleep(1);
printf("Child(pid : %d) is waiting for any signal\n", getpid());
raise(SIGSTOP);
exit(0);
}
else
{
/* 在父进程中收集子进程发出的信号, 并调用 kill()函数进行相应的操作 */
if ((waitpid(pid, NULL, WNOHANG)) == 0)
{
if ((ret = kill(pid, SIGKILL)) == 0)
{
printf("Parent kill %d\n",pid);
}
}
waitpid(pid, NULL, 0);
exit(0);
}
}
该
程序运行结果如下:
$ ./kill_raise
Child(pid : 4877) is waiting for any signal
Parent kill 4877
2. 信号捕捉: alarm()、 pause()
alarm()也称为闹钟函数, 它可以在进程中设置一个定时器, 当定时器指定的时间到时,它就向进程发送 SIGALARM 信号。 要注意的是,一个进程只能有一个闹钟时内核只为每个进程配置一个时钟,一个闹钟没结束,又定义一个闹钟,是不会定义成功,且会返回上次定义后剩下的时间,如果在调用 alarm()之前已设置过闹钟时间,且签约个已经结束, 则任何以前的闹钟时间都被新值所代替。
pause()函数用于将调用进程挂起直至捕捉到信号为止。 这个函数很常用, 通常可以用于判断信号是否已到。
alarm()函数的语法要点。
alarm发出的信号默认是结束进程SIGALARM
pause()函数的语法要点。
以下实例实际上已完成了一个简单的 sleep()函数的功能, 由于 SIGALARM 默认的系统动作为终止该进程, 因此程序在打印信息前就会被结束了, 代码如下:
/* alarm_pause.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
/* 调用 alarm 定时器函数 */
int ret = alarm(5);
pause();
printf("I have been waken up.\n",ret); /* 此语句不会被执行 */
}
结果:
$./alarm_pause
Alarm clock
3 信号的处理
信号处理的方法主要有两种, 一种是使用 signal()函数, 另一种是使用信号集函数组。
下面分别介绍这两种处理方式。
3.1 使用 signal()函数
使用 signal()函数处理时, 只需指出要处理的信号和处理函数即可。 它主要用于前 32 种非实时信号的处理, 不支持信号传递信息, 但是由于使用简单、 易于理解, 因此也受到很多程序员的欢迎。 Linux 还支持一个更健壮更新的信号处理函数 sigaction(), 推荐使用该函数。
signal()函数的语法。
这里需要对该函数原型进行说明。 这个函数原型有点复杂: 首先该函数原型整体指向一个无返回值并且带一个整型参数的函数指针, 也就是信号的原始配置函数; 接着该原型又带有两个参数, 其中第 2 个参数可以是用户自定义的信号处理函数的函数指针。
sigaction()函数的语法要点。
这里要说明的是 sigaction()函数中第 2 和第 3 个参数用到的 sigaction 结构, 这是一个看似非常复杂的结构, 希望读者能够慢慢阅读此段内容。
sigaction 结构体的定义, 代码如下:
struct sigaction {
void (*sa_handler)(int); /* addr of signal handler, or SIG_IGN, or SIG_DFL */
sigset_t sa_mask; /* additional signals to block */
int sa_flags; /* signal options */
/* alternate handler */
void (*sa_sigaction)(int, siginfo_t *, void *);
};
sa_handler 是一个函数指针, 指定信号处理函数, 这里除可以是用户自定义的处理函数外, 还可以为 SIG_DFL(采用默认的处理方式) 或 SIG_IGN(忽略信号)。 它的处理函数只有一个参数, 即信号值。
sa_mask 是一个信号集, 它可以指定在信号处理程序执行过程中哪些信号应当被屏蔽,在调用信号捕获函数前, 该信号集要加入到信号的信号屏蔽字中。
sa_flags 中包含了许多标志位, 是对信号进行处理的各个选择项。 它的常见可选值如表
第1个实例表明了如何使用 signal()函数捕捉相应信号, 并做出给定的处理。这里, my_func就是信号处理的函数指针, 读者还可以将其改为 SIG_IGN 或 SIG_DFL 查看运行结果。
第 2个实例是用 sigaction()函数实现同样的功能。
以下是使用 signal()函数的示例:
/* signal.c */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/* 自定义信号处理函数 */
void my_func(int sign_no)
{
if (sign_no == SIGINT)
{
printf("I have get SIGINT\n");
}
else if (sign_no == SIGQUIT)
{
printf("I have get SIGQUIT\n");
}
}
int main()
{
printf("Waiting for signal SIGINT or SIGQUIT...\n");
/* 发出相应的信号, 并跳转到信号处理函数处 */
signal(SIGINT, my_func);
signal(SIGQUIT, my_func);
pause();
exit(0);
}
$ ./signal
Waiting for signal SIGINT or SIGQUIT...
I have get SIGINT //(按 Ctrl+c 组合键)
$ ./signal
Waiting for signal SIGINT or SIGQUIT...
I have get SIGQUIT //(按 Ctrl+\ 组合键)
以下是用 sigaction()函数实现同样的功能,
/* sigaction.c */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/* 自定义信号处理函数 */
void my_func(int sign_no)
{
if (sign_no == SIGINT)
{
printf("I have get SIGINT\n");
}
else if (sign_no == SIGQUIT)
{
printf("I have get SIGQUIT\n");
}
}
int main()
{
struct sigaction action;
printf("Waiting for signal SIGINT or SIGQUIT...\n");
/* sigaction 结构初始化 */
action.sa_handler = my_func;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
/* 发出相应的信号, 并跳转到信号处理函数处 */
sigaction(SIGINT, &action, 0);
sigaction(SIGQUIT, &action, 0);
pause();
exit(0);
}
3.2 信号集函数组
使用信号集函数组处理信号时涉及一系列的函数, 这些函数按照调用的先后次序可分为以下几大功能模块:
创建信号集、 注册信号处理函数及检测信号。
1.创建信号集主要用于处理用户感兴趣的一些信号, 其函数包括以下几个。
sigemptyset(): 将信号集初始化为空。
sigfillset(): 将信号集初始化为包含所有已定义的信号集。
sigaddset(): 将指定信号加入到信号集中。
sigdelset(): 将指定信号从信号集中删除。
sigismember(): 查询指定信号是否在信号集中。
2.注册信号处理函数主要用于决定进程如何处理信号。 这里要注意的是, 信号集里的信号并不是真正可以处理的信号, 只有当信号的状态处于非阻塞状态时才会真正起作用。 因此,首先使用 sigprocmask()函数检测并更改信号屏蔽字(信号屏蔽字是用来指定当前被阻塞的一组信号, 它们不会被进程接收), 然后使用 sigaction()函数来定义进程接收到特定信号后的行为。检测信号是信号处理的后续步骤, 因为被阻塞的信号不会传递给进程, 所以这些信号就处于“未处理” 状态(也就是进程不清楚它的存在)。 sigpending()函数允许进程检测“未处理” 信号, 并进一步决定对它们做何处理。
首先介绍创建信号集的函数格式, 表列举了这一组函数的语法要点。
表 4.15 列举了 sigprocmask()函数的语法要点。
此处, 若 set 是一个非空指针, 则参数 how 表示函数的操作方式; 若 how 为空, 则表示忽略此操作。
表 4.16 列举了 sigpending()函数的语法要点。
总之, 在处理信号时, 一般遵循如图 4.6 所示的操作流程。
4. 信号处理实例
该实例首先把 SIGQUIT、 SIGINT 两个信号加入信号集, 然后将该信号集设为阻塞状态,并进入用户输入状态。 用户只需按任意键, 就可以立刻将信号集设置为非阻塞状态, 再对这两个信号分别操作, 其中 SIGQUIT 执行默认操作, 而 SIGINT 执行用户自定义函数的操作。
源代码如下:
/* sigset.c */
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/* 自定义的信号处理函数 */
void my_func(int signum)
{
printf("If you want to quit,please try SIGQUIT\n");
}
int main()
{
sigset_t set,pendset;
struct sigaction action1,action2;
/* 初始化信号集为空 */
if (sigemptyset(&set) < 0)
{
perror("sigemptyset");
exit(1);
}
/* 将相应的信号加入信号集 */
if (sigaddset(&set, SIGQUIT) < 0)
{
perror("sigaddset");
exit(1);
}
if (sigaddset(&set, SIGINT) < 0)
{
perror("sigaddset");
exit(1);
}
if (sigismember(&set, SIGINT))
{
sigemptyset(&action1.sa_mask);
action1.sa_handler = my_func;
action1.sa_flags = 0;
sigaction(SIGINT, &action1, NULL);
}
if (sigismember(&set, SIGQUIT))
{
sigemptyset(&action2.sa_mask);
action2.sa_handler = SIG_DFL;
action2.sa_flags = 0;
sigaction(SIGQUIT, &action2,NULL);
}
/* 设置信号集屏蔽字, 此时 set 中的信号不会被传递给进程, 暂时进入待处理状态 */
if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
{
perror("sigprocmask");
exit(1);
}
else
{
printf("Signal set was blocked, Press any key!");
getchar();
}
/* 在信号屏蔽字中删除 set 中的信号 */
if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
{
perror("sigprocmask");
exit(1);
}
else
{
printf("Signal set is in unblock state\n");
}
while(1);
exit(0);
}
该程序的运行结果如下, 可以看见, 在信号处于阻塞状态时, 所发出的信号对进程不起作用, 并且该信号进入待处理状态。 读者按任意键, 并且信号脱离了阻塞状态后, 用户发出的信号才能正常运行。 这里 SIGINT 已按照用户自定义的函数运行, 请读者注意阻塞状态下SIGINT 的处理和非阻塞状态下 SIGINT 的处理有何不同。
$ ./sigset
Signal set was blocked, Press any key! /* 此时按任何键可以解除阻塞屏蔽字 */
If you want to quit,please try SIGQUIT /* 阻塞状态下 SIGINT 的处理 */
Signal set is in unblock state /* 从信号屏蔽字中删除 set 中的信号 */
If you want to quit,please try SIGQUIT /* 非阻塞状态下 SIGINT 的处理 */
If you want to quit,please try SIGQUIT
Quit /* 非阻塞状态下 SIGQUIT 的处理 */