Linux - 进程间通信方式之信号
一、概述
1. 信号定义
信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。不能自定义信号,所有的信号都是系统预定义的。
二、信号的产生
1. 由shell终端根据当前发生的错误(段错误、非法指令等)和Ctrl+C而产生的信号。
例如:socket通信或管道通信,如果读端已经关闭,执行写操作(或发送数据)将导致执行写操作的进程收到SIGPIPE(表示管道破裂,该信号的默认行为是终止该进程)信号。
2. 在shell终端,使用kill或killall命令产生信号
示例1:
#include <unistd.h>
int main(int argc, char **argv) {
while(1) {
sleep(1);
}
return 0;
}
在终端1编译执行如上代码
在终端2shell下输入命令ps -ef | grep 示例1程序名 查询示例1程序的进程号
在终端2shell下输入命令kill -9 进程号
输入示例如下:
终端1:
gcc signal_kill.cc -o signal_kill.exe
./signal_kill.exe
终端2:
ps -ef | grep signal_kill.exe
kill -9 17601
3. 在代码中使用kill系统调用产生信号
- 有哪些信号?
信号名称 | 说明 |
---|---|
SIGABORT | 程序异常终止 |
SIGALRM | 超时告警 |
SIGFPE | 浮点运算异常 |
SIGHUP | 连接挂断 |
SIGILL | 非法指令 |
SIGINT | 终端中断(Ctrl + C 产生该信号) |
SIGKILL | 终止进程 |
SIGPIPE | 向没有读进程的管道写数据 |
SIGQUIT | 终端退出(Ctrl + \ 产生该信号) |
SIGSEGV | 无效内存段访问 |
SIGTERM | 终止 |
SIGUSR1 | 用户自定义信号1 |
SIGUSR2 | 用户自定义信号2 |
以上信号如果不捕获,则进程接收到后都会终止!
信号名称 | 说明 |
---|---|
SIGCHLD | 子进程已停止或退出 |
SIGCONT | 让暂停的进程继续执行 |
SIGSTOP | 停止执行(即暂停) |
SIGTSTP | 中断挂起 |
SIGTTIN | 后台进程尝试读操作 |
SIGTTOU | 后台进程尝试写操作 |
三、信号的处理
- 忽略信号
- 捕捉信号,指定信号处理函数进行处理
- 执行系统默认动作,大多数都是终止进程
示例代码 - 忽略和捕获信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void process_USR2(int signum) {
printf("接收到信号SIGUSR2:%d\n", signum);
}
int main(int argc, char *argv[]) {
// 忽略信号SIGUSR1
signal(SIGUSR1, SIG_IGN);
// 捕获信号SIGUSR2,并处理
signal(SIGUSR2, process_USR2);
while(1) {
sleep(1);
}
return 0;
}
/***********************************************************************
* 在终端1下编译执行
* 在终端2下使用kill命令发送相关信号
************************************************************************/
四、信号的捕获
1. 含义:指定接受到某种信号后,去执行指定的函数。
注:SIGKILL和SIGSTOP不能被捕获;即这两种信号的响应动作不能被改变。
2. 信号的安装
安装信号有两种方式,分别是:signal和sigaction。在实战中推荐使用sigaction。
- 使用signal
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal的参数2可取以下特殊值:
取值 | 含义 |
---|---|
SIG_IGN | 忽略信号 |
SIG_DFL | 恢复默认行为 |
注:signal的返回类型和第二个参数都是函数指针类型
注:使用SIG_DFL时,仅当第一次调用自定义的行为后马上使用SIG_DFL就可恢复,如果连续捕获多次后,就不确定。
示例代码 - 改变终端中断信号的行为:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void process_INT(int signum) {
printf("接收到中断信号:%d\n", signum);
}
int main(int argc, char *argv[]) {
signal(SIGINT, process_INT);
while(1) {
sleep(1);
}
return 0;
}
/***********************************************************************
* 1.编译运行
* 2.按下Ctrl + C 会输出如下内容:^C接收到中断信号:2
* 3.结束当前进程:开启另一个终端,使用ps命令获取进程号,通过kill -9 杀死进程
* 命令示例:
* ps -er | grep ./a.out
* kill -9 1011
************************************************************************/
示例代码 - 恢复信号的默认行为:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void process_INT(int signum) {
printf("Catch signal: %d\n", signum);
signal(SIGINT, SIG_DFL); // 等同于 signal(signum, SIG_DFL);
}
int main(int argc, char *argv[]) {
signal(SIGINT, process_INT);
while(1) {
sleep(1);
}
return 0;
}
/***********************************************************************
* 1. 编译执行
* 2. 第一次按下Ctrl + C 输出如下内容:^CCatch signal: 2
* 3. 第二次按下Ctrl + C 输出如下内容并退出程序:^C
************************************************************************/
注:使用SIG_DFL时,仅当第一次调用自定义的行为后马上使用SIG_DFL就可恢复,如果连续捕获多次后,就不确定。
- 使用sigaction
// 头文件
#include <signal.h>
// 原型
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// struct sigaction
struct sigaction {
void (*sa_handler)(int); // 信号的响应函数
sigset_t sa_mask; // 屏蔽信号集
int sa_flags; // 当sa_flags中包含 SA_RESETHAND时,接受到该信号并调用指定的信号处理函数执行之后,把该信号的响应行为重置为默认行为SIG_DFL
...
};
补充:
当sa_mask包含某个信号A时,则在信号处理函数执行期间,如果捕获到该信号A,则阻塞该信号A(即暂时不响应该信号),知道信号处理函数执行结束。即信号处理函数执行完之后,再响应该信号A。
示例代码 - sigaction函数的使用:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_response(int signum) {
printf("Catch a signal: %d\n", signum);
}
int main() {
struct sigaction action;
action.sa_handler = signal_response;
// sigemptyset(sigset_t *set) - 将set给出的信号集初始化为空,并将所有信号排除在该集之外。
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGINT, &action, 0);
while(1) {
sleep(1);
}
return 0;
}
示例代码 - 使用函数sigaction()实现只执行一次自定义信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_once_restore(int signum) {
printf("Catch a signal: %d\n", signum);
}
int main(int argc, char *argv[]) {
struct sigaction action;
action.sa_handler = signal_once_restore;
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &action, 0);
while(1) {
sleep(1);
}
return 0;
}
- signal 和 sigaction的区别:sigaction比signal更"健壮"。
五、信号的发送
1. 信号的发送方式
a. 在shell终端使用快捷键产生信号(例如:Ctrl + C)
b. 在shell终端使用kill,killall命令
c. 在代码中使用kill函数和alarm函数
2. 使用kill函数
/****************************************************************************
* 函数:int kill(pid_t pid, int sig)
* 功能:给指定的进程发送信号
* 用法:man 2 kill
* 参数:
* pid - 向pid发送信号
* pid > 0:信号sig被发送到具有PID指定的ID的进程。
* pid == 0:将sig发送到调用进程的进程组中的每个进程。
* pid == -1:将sig发送到调用进程有权为其发送信号的每个进程(进程1(Init)除外),但请参见下面的内容。
* pid < -1:将sig发送到进程组中ID为-pid的每个进程。
* sig - 要发送的信号。
* sig == 0:则不会发送信号,但仍会执行存在和权限检查;这可用于检查是否存在允许调用者发送信号的进程ID或进程组ID。
* 返回:
* 成功:返回0
* 失败:返回-1,并设置errno
* 说明: 给指定的程序发送信号需要"权限":普通用户的进程只能给该用户的其他进程发送信号,root用户可以给所有用户的进程发送信号。
*****************************************************************************/
示例程序 - 默认输出"child process work!",输入A后转大写,输入a后转小写:
#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
int flags = 0;
void signal_USR1(int signum) {
flags = 1;
}
void signal_USR2(int signum) {
flags = 0;
}
int main(int argc, char *argv[]) {
int pid = fork();
if(pid < 0) {
printf("fork() - failed! reason: %s\n", strerror(errno));
} else if(pid == 0) {
const char *msg = NULL;
struct sigaction act;
act.sa_handler = signal_USR1;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, 0);
act.sa_handler = signal_USR2;
sigaction(SIGUSR2, &act, 0);
while(1) {
if(flags) {
msg = "CHILD PROCESS WORK!";
} else {
msg = "child process work!";
}
printf("%s\n", msg);
sleep(1);
}
} else {
while(1) {
char c = getchar();
if(c == 'A') {
kill(pid, SIGUSR1);
} else if(c == 'a') {
kill(pid, SIGUSR2);
}
}
}
return 0;
}
示例程序 - 闹钟:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int alarm_flags = 0;
void signal_alarm(int signum) {
alarm_flags = 1;
}
int main(int argc, char *argv[]) {
int pid = fork();
if(pid < 0) {
fprintf(stderr, "fock() - failed! reason: %s\n", strerror(errno));
return -1;
} else if(pid == 0) {
sleep(5);
// 向父进程发送闹钟信号
// getppid() - 获取父进程id
kill(getppid(), SIGALRM);
} else {
struct sigaction action;
action.sa_handler = signal_alarm;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGALRM, &action, NULL);
// 暂时挂起该进程,直到收到任意信号
pause();
if(alarm_flags == 1) {
printf("The parent process has been awakened!\n");
}
}
return 0;
}
3. 使用alarm函数
/****************************************************************************
* 函数:unsigned int alarm(unsigned int seconds);
* 功能:在等待seconds秒时间之后给该进程本身发送一个SIGALRM信号
* 用法:man 2 alarm
* 参数:
* seconds - 等待的秒数
* seconds == 0:则取消所有挂起的alarm
* 返回:
* 失败:返回-1
* 成功:返回上次闹钟的使用时间(单位:秒)
* 注意:时间的单位是"秒"
* 实际闹钟时间比指定的时间要大一些
* 如果参数为0,则取消已设置的闹钟
* 如果闹钟时间还没到,再次调用alarm,则重新开始计时
* 每个进程最多只能使用1个闹钟
*****************************************************************************/
示例代码 - 闹钟:
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int alarm_flags = 0;
void signal_alarm(int signum) {
alarm_flags = 1;
}
int main() {
struct sigaction action;
action.sa_handler = signal_alarm;
action.sa_flags = 0;
sigemptyset(&action.sa_mask);
sigaction(SIGALRM, &action, NULL);
printf("time = %ld\n", time((time_t*)0));
int ret = alarm(5);
if(ret == -1) {
printf("alarm() - error!\n");
return -1;
}
// 挂起当前进程,直到收到任意信号
pause();
if(alarm_flags) {
printf("The process is awakened, time = %ld\n", time((time_t*)0));
}
return 0;
}
4. 使用raise
/****************************************************************************
* 函数:int raise(int sig);
* 功能:给本进程自身发送信号。
* 参数:sig - 需要发送的信号
* 返回:
* 成功 - 返回0
* 失败 - 返回非0值
*****************************************************************************/
六、发送多个信号
- 某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程又多次收到同一个信号(同一种信号值的信号),则:
如果该信号是不可靠信号(<32),则只响应一次;
如果该信号是可靠信号(>32),则可以响应多次(不会遗漏),但是都必须要等该次响应函数执行完成之后才能响应下一次。- 某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程收到另一个信号(不同信号值的信号),则:
如果该信号被包含在当前信号的signaction的sa_mask(信号屏蔽集)中,则不会立即处理该信号。直到当前的信号处理函数执行完之后,才去执行该信号的处理函数。
否则:立即中断当前执行过程(如果处于睡眠,比如sleep, 则立即被唤醒)而去执行这个新的信号响应。新的响应执行完之后,再在返回至原来的信号处理函数继续执行。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void myhandle(int sig)
{
printf("Catch a signal : %d\n", sig);
int i;
for (i=0; i<10; i++) {
sleep(1);
}
printf("Catch end.%d\n", sig);
}
int main(void)
{
struct sigaction act, act2;
act.sa_handler = myhandle;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGUSR1);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
act2.sa_handler = myhandle;
sigemptyset(&act2.sa_mask);
act2.sa_flags = 0;
sigaction(SIGUSR1, &act, 0);
while (1) {
}
return 0;
}
七、信号集
1. 概念:
信号集:用sigset_t(unsigned long int)类型表示。用来表示包含多个信号的集合。
2. 信号集的基本操作
函数 | 功能 |
---|---|
sigemptyset | 把信号集清空 |
sigfillset | 把所有已定义的信号填充到指定的信号集 |
sigdelset | 从指定的信号集中删除指定的信号 |
sigaddset | 从指定的信号集中添加指定的信号 |
sigismember | 判断指定信号是否在指定的信号集中。如果在,返回1;如果不在,返回0;信号无效,返回-1 |
3. 进程的"信号屏蔽字"
进程的"信号屏蔽字"是一个信号集
向目标进程发送某个信号时,如果这个信号在目标进程的信号屏蔽集中,则目标进程将不会响应该信号(即不会执行该信号的信号处理函数)。如果进程的信号屏蔽字中不再包含该信号时,则会立即响应这个信号(执行对应的函数,在其他进程向此进程发送过该信号的情况下)。
/****************************************************************************
* 函数:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
* 功能:修改进程的“信号屏蔽字”
* 参数:
* how - 取值如下:
* SIG_BLOCK - 把参数set中的信号添加到信号屏蔽字中
* SIG_UNBLOCK - 把参数set中的信号从信号屏蔽字中删除
* SIG_SETMASK - 把参数set中的信号设置为信号屏蔽字
* set - 信号屏蔽字
* oldset - 之前的信号屏蔽字
* 返回:
* 成功:返回0
* 失败:返回-1,并设置errno
*****************************************************************************/
示例代码 - 将信号SIGINT加入/移出信号屏蔽集:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_INT(int signum) {
printf("Catch a signal: %d\n", signum);
}
int main(int atgc, char *argv[]) {
struct sigaction action;
action.sa_handler = signal_INT;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGINT, &action, NULL);
sigset_t proc_sig_msk, old_mask;
sigemptyset(&proc_sig_msk);
sigaddset(&proc_sig_msk, SIGINT);
// 将proc_sig_msk添加到原信号屏蔽集中
sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);
printf("Blocked signal SIGINT.\n");
sleep(5);
printf("Unblocking signal SIGINT.\n");
// 将prorc_sig_msk从原信号屏蔽集中移除
sigprocmask(SIG_UNBLOCK, &proc_sig_msk, &old_mask);
printf("Unblocked signal SIGINT.\n");
while(1) {
sleep(1); // 休眠1秒
}
return 0;
}
4. 获取未处理的信号
当进程的信号屏蔽字中信号发生时,这些信号不会被该进程响应,可通过sigpending函数获取这些已经发生了但是没有被处理的信号
/****************************************************************************
* 函数:int sigpending(sigset_t *set);
* 功能:返回等待传递给调用线程的一组信号(即在阻塞时引发的信号)。
* 参数:
* set - 用于接收信号
* 返回:
* 成功 - 返回0
* 失败 - 返回-1,并设置errno
*****************************************************************************/
5. 阻塞式等待信号
- pause:阻塞进程,直到发生任意信号
- sigsuspend:用指定的参数设置信号屏蔽字,然后阻塞时等待信号的发生。即等待信号屏蔽字之外的信号