- (꒪ꇴ꒪ ),Hello我是祐言QAQ
- 我的博客主页:C/C++语言,数据结构,Linux基础,ARM开发板,网络编程等领域UP🌍
- 快上🚘,一起学习,让我们成为一个强大的攻城狮!
- 送给自己和读者的一句鸡汤🤔:集中起来的意志可以击穿顽石!
- 作者水平很有限,如果发现错误,请在评论区指正,感谢🙏
当谈及进程间通信(IPC),我们需要寻找途径来使不同进程能够交换数据和信息。在操作系统中,这一通信机制被称为IPC,它有多种方式可以实现。本文将着重探讨其中之一信号(Signal)以及相关的概念、分类和使用方式。
一、什么是信号
信号是一种操作系统层面对中断机制的软件模拟,作为一种异步通信方式,它允许进程在某些事件发生时向其他进程发送通知。信号的生命周期包括信号的产生、注册、响应和处理以及注销。
其中信号响应方式分为三类:
- 忽略信号:某些信号可以被进程忽略,但需要注意的是,特殊的信号 SIGKILL(9) 和 SIGSTOP (19)不能被忽略,也不能被捕捉,其默认操作无法修改。
- 捕捉信号响应函数:进程可以为特定信号注册自定义的信号处理函数。当进程接收到该信号时,会执行所注册的处理函数,从而实现特定的行为。
- 执行缺省操作:Linux系统下的每种信号都有其预定义的默认操作,如果没有指定特定的信号处理方式,系统将会执行该信号的默认操作。
换句话说,除了这两个信号之外的其他信号,接收信号的目标进程按照如下顺序来做出反应:
A :如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。否则进入 B。
B:如果该信号被捕捉,那么进一步判断捕捉的类型:
B1:如果设置了响应函数,那么执行该响应函数。
B2: 如果设置为忽略,那么直接丢弃该信号。
否则进入 C。
C:执行该信号的缺省动作(默认操作)。
注意:
信号的数值以及对应的名称可以通过命令 kill -l
查看。
信号 | 值 | 缺省动作 | 备注 |
SIGHUP | 1 | 终止 | 控制终端被关闭时产生 |
SIGINT | 2 | 终止 | 从键盘按键产生的中断信号(比如Ct+C) |
SIGQUIT | 3 | 终止并产生转储文件 | 从键盘按键产生的退出信号(比如Ct+\) |
SIGKILL | 9 | 终止 | 系统杀戮信号 |
SIGCONT | 18 | 恢复运行 | 系统恢复运行信号 |
SIGSTOP | 19 | 暂停 | 系统暂停信号 |
二、 信号分类
信号可以分为非实时信号(前31个)和实时信号(后31个)。这两者在响应方式上有所不同。
- 非实时信号(不可靠信号):非实时信号的响应不会排队,可能会发生嵌套。如果进程未及时响应一个非实时信号,之后的该信号将会被丢弃。每个非实时信号都与系统事件相关联,当事件发生时,对应的信号被产生。
- 实时信号(可靠信号):相比之下,实时信号的响应是按照接收顺序排队的,不会发生嵌套。即使同一种实时信号被多次发送,也不会被丢弃,而是依次响应。与实时信号相关联的没有特殊的系统事件。
三、信号的使用
1.发送信号
使用 kill(pid, sig) 函数向指定进程发送信号。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
2.响应方式注册
使用 signal(sig, func) 函数注册信号的响应方式,其中 func 可以是普通响应函数。
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
typedef void (*sighandler_t)(int);
SIG_IGN 捕捉动作为:忽略;
SIG_DFL 捕捉动作为:执行该信号的缺省动作;
void (*p)(int) 捕捉动作为:执行由p指向的信号响应函数。
练习:司机和售票员模拟
设计一个程序,通过父子进程模拟司机和售票员的互动。具体实现如下:
(1)售票员捕获到 SIGINT 信号时,向司机发送 SIGUSR1 信号,司机收到后打印 "开车了...";
(2)售票员捕获到 SIGQUIT 信号时,向司机发送 SIGUSR2 信号,司机收到后打印 "靠站...";
(3)司机捕获到 SIGTSTP 信号时,向售票员发送 SIGUSR1 信号,售票员收到后打印 "终点站到了,请所有乘客下车!"。
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
int childpid;
// 当售票员捕获到SIGINT信号时,发送信号10给父进程(司机)
void func1(int sig)
{
kill(getppid(), 10);
}
// 当司机捕获到SIGUSR1信号时,打印“开车了...”
void func2(int sig)
{
printf("开车了...\n");
}
// 当售票员捕获到SIGQUIT信号时,发送信号12给子进程(售票员)
void func3(int sig)
{
kill(getppid(), 12);
}
// 当司机捕获到SIGUSR2信号时,打印“靠站...”
void func4(int sig)
{
printf("靠站...\n");
}
// 当售票员捕获到SIGTSTP信号时,发送信号10给子进程(售票员)
void func5(int sig)
{
kill(childpid, 10);
}
// 当售票员捕获到SIGUSR1信号时,打印“终点站到了,请全部下车...”,然后退出进程
void func6(int sig)
{
printf("终点站到了,请全部下车...\n");
kill(getppid(), 9); // 给父进程(司机)发送SIGKILL信号
kill(getpid(), 9); // 给自己发送SIGKILL信号,终止进程
}
int main(int argc, char const *argv[])
{
pid_t x = fork();
if (x == 0) // 售票员进程
{
// 设置售票员的信号处理函数
signal(2, func1);
signal(3, func3);
signal(20, SIG_IGN); // 忽略SIGCHLD信号
signal(10, func6);
while(1); // 持续等待信号
}
if (x > 0) // 司机进程
{
childpid = x;
// 设置司机的信号处理函数
signal(2, SIG_IGN); // 忽略SIGINT信号
signal(10, func2);
signal(3, SIG_IGN); // 忽略SIGQUIT信号
signal(12, func4);
signal(20, func5);
while(1); // 持续等待信号
}
return 0;
}
3.给自己发送信号
使用 raise(sig) 函数向自己发送信号。
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("进程开始运行\n");
sleep(5);
raise(9);//给自己发送一个信号 9 杀死自己
printf("进程结束运行\n");
return 0;
}
4.等待信号
使用 pause() 函数使进程进入等待状态,直到收到一个信号。
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("进程%d开始运行\n", getpid());
pause();//多个pause只有一个有效
printf("进程%d结束运行\n", getpid());
return 0;
}
5.信号集和阻塞
使用 sigprocmask(how, set, oldset) 函数来设置信号的阻塞状态。
信号集:
sigset_t mysigset;//信号集
int sigemptyset(sigset_t *set);//清空信号集
int sigfillset(sigset_t *set);//将所有信号添加到信号集
int sigaddset(sigset_t *set, int signum);//添加指定的一个信号到信号集
int sigdelset(sigset_t *set, int signum);//将指定信号从信号集中删除
阻塞:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
下面我们就来用一用信号集与阻塞,设计两个进程:
(1)进程A负责不断向进程B发送信号(排除 SIGSTOP 和 SIGKILL);
(2)进程B接收信号,并将每个信号注册到同一个响应函数中,打印信号值。在发送信号之前,进程B阻塞了所有信号,然后进程A发送所有信号,延时5秒后,进程B解除对信号的阻塞。
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
// 信号处理函数,打印接收到的信号值
void func(int sig)
{
printf("sig = %d\n", sig);
}
int main(int argc, char const *argv[])
{
// 定义一个信号集:存放信号
sigset_t set;
// 清空信号集
sigemptyset(&set);
// 将信号添加到信号集中(排除一些特殊信号)
for (int i = 1; i < 65; ++i)
{
if (i == 9 || i == 19 || i == 32 || i == 33)
{
continue;
}
sigaddset(&set, i);
}
// 创建子进程
pid_t x = fork();
if (x > 0) // 父进程
{
sleep(1);
// 向子进程发送各种信号
for (int i = 1; i < 65; ++i)
{
if (i == 9 || i == 19 || i == 32 || i == 33)
{
continue;
}
kill(x, i);
}
// 等待子进程结束
wait(NULL);
}
if (x == 0) // 子进程
{
// 为每种信号注册信号处理函数
for (int i = 1; i < 65; ++i)
{
if (i == 9 || i == 19 || i == 32 || i == 33)
{
continue;
}
signal(i, func);
}
// 阻塞所有信号
sigprocmask(SIG_BLOCK, &set, NULL);
sleep(5); // 让子进程保持阻塞状态一段时间
// 解除对信号的阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
return 0;
}
6.发送信号带数据
int sigqueue(pid_t pid, int sig, const union sigval value);
kill(pid, sig);
union sigval
{
int sival_int;
void *sival_prt;
}
举例:
//定义一个联合体变量,用来存放要发送的数据
union sigval data;
data.sival_int = 100;
//发送信号,带数据
kill(atoi(argv[1]), SIGUSR1);
sigqueue(atoi(argv[1]), SIGUSR1, data);
7 捕捉信号,获取数据
捕捉一个指定的信号,且可以通过扩展响应函数来获取信号携带的额外数据。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
注意:
sigqueue()函数相当于扩展版的 kill 函数;
sigaction()函数相当于扩展版的 signal 函数。
举例:
//普通响应函数
void func(int sig)
{
printf("【%d】 普通响应函数\n", sig);
}
//扩展响应函数
void sac_func(int sig, siginfo_t *siginfo, void *ptr)
{
//如果是用sigqueue函数发送的信号,那么我们是可以拿到传过来的数据的
if (siginfo->si_code == SI_QUEUE)
{
printf("获取到信号【%d】传过来的数据:%d\n",
sig, siginfo->si_int);
}
}
int main(int argc, char const *argv[])
{
printf("pid = [%d]\n", getpid());
//扩展信号响应函数注册
struct sigaction act;
//设置普通响应函数
act.sa_handler = func;
//设置扩展响应函数
act.sa_sigaction = sac_func;
//设置掩码(可以不设置)
sigset_t myset;
sigaddset(&myset, SIGUSR2);
act.sa_mask = myset;
//设置响应方式是扩展响应函数方式
act.sa_flags |= SA_SIGINFO;
//普通信号响应函数的注册
// signal(SIGUSR1, func);
//扩展信号响应函数注册
sigaction(SIGUSR1, &act, NULL);
pause();
return 0;
}
8.信号的内核数据模型
信号在操作系统内核中有一个数据模型,用于表示和管理进程间通信的信号。这个数据模型包括信号的产生、传递、处理以及信号控制块等要素。下面就是信号的内核数据模型的介绍:
信号控制块(Signal Control Block,SCB): 信号控制块是操作系统内核中用于管理和维护信号信息的数据结构。每个进程都有一个关联的信号控制块,它存储了进程接收到的信号以及相关的处理和状态信息。这个数据结构通常包括以下字段:
-
信号位图(Signal Bitmap): 用于表示进程当前处于阻塞状态的信号。每个信号都对应一个位,如果某个信号的位为1,则表示该信号被阻塞。
-
信号队列(Signal Queue): 用于存储进程接收到但尚未处理的信号。信号队列采用队列的形式,其中每个节点存储了信号的类型、时间戳以及其他相关信息。
信号的产生和传递: 信号的产生通常是由特定事件触发,如硬件异常、软件条件等。当这些事件发生时,操作系统内核会将相应的信号发送给相应的进程。信号会按照进程的层次结构向父进程或子进程传递,或者向指定的进程传递。
信号的处理: 当进程接收到一个信号时,它可以按照事先注册的信号处理方式来响应。这可以通过调用 signal()
或 sigaction()
函数来实现。处理方式可以是忽略信号、执行默认操作、或执行自定义的信号处理函数。在执行信号处理函数期间,进程可以根据信号的类型和处理函数的内容来进行特定的操作,从而实现对信号的处理。
信号的阻塞和解除阻塞: 进程可以通过阻塞信号来暂时屏蔽某些信号的传递和处理。这在某些情况下很有用,比如在临界区代码中防止特定信号的干扰。进程可以使用 sigprocmask()
函数来设置信号的阻塞状态,以及使用 SIG_BLOCK
和 SIG_UNBLOCK
来分别添加和解除信号的阻塞。
信号的排队和处理顺序: 对于非实时信号,当多个信号被发送到同一个进程时,它们可能会排队等待被处理。对于实时信号,系统会保证信号按照发送顺序排队,不会发生嵌套。因此,进程需要按照信号排队的顺序来处理它们。
四、总结
信号作为一种进程间通信的手段,允许进程以异步的方式相互通知。通过掌握信号的基本概念、分类和使用方式,我们可以更好地实现进程间的通信和协调,从而提升系统的整体效率和稳定性。
更多C/C++语言、Linux系统、数据结构和ARM板实战相关文章,关注专栏:
手撕C语言
玩转linux
脚踢数据结构
系统、网络编程
探索C++
6818(ARM)开发板实战
📢写在最后
- 今天的分享就到这啦~
- 觉得博主写的还不错的烦劳
一键三连喔
~ - 🎉🎉🎉感谢关注🎉🎉🎉