当谈及进程间通信(IPC),我们需要寻找途径来使不同进程能够交换数据和信息。在操作系统中,这一通信机制被称为IPC,它有多种方式可以实现。本文将着重探讨其中之一信号(Signal)以及相关的概念、分类和使用方式。
信号是一种操作系统层面对中断机制的软件模拟,作为一种异步通信方式,它允许进程在某些事件发生时向其他进程发送通知。信号的生命周期包括信号的产生、注册、响应和处理以及注销。
其中信号响应方式分为三类:
换句话说,除了这两个信号之外的其他信号,接收信号的目标进程按照如下顺序来做出反应:
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个)。这两者在响应方式上有所不同。
使用 kill(pid, sig) 函数向指定进程发送信号。
#include
#include
int kill(pid_t pid, int sig);
使用 signal(sig, func) 函数注册信号的响应方式,其中 func 可以是普通响应函数。
#include
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
#include
#include
#include
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;
}
使用 raise(sig) 函数向自己发送信号。
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
printf("进程开始运行\n");
sleep(5);
raise(9);//给自己发送一个信号 9 杀死自己
printf("进程结束运行\n");
return 0;
}
使用 pause() 函数使进程进入等待状态,直到收到一个信号。
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
printf("进程%d开始运行\n", getpid());
pause();//多个pause只有一个有效
printf("进程%d结束运行\n", getpid());
return 0;
}
使用 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
#include
#include
#include
#include
// 信号处理函数,打印接收到的信号值
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;
}
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);
捕捉一个指定的信号,且可以通过扩展响应函数来获取信号携带的额外数据。
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);
};
标准信号响应函数指针 sa_handler 和扩展信号响应函数指针 sa_sigaction 所指向的 函数接口是不同的,sa_sigaction 指向的函数接口要复杂得多,事实上如果选择扩展接口的 话,信号的接收进程不仅可以接收到 int 型的信号,还会接收到一个 siginfo_t 型的结构体指针,还有一个 void 型的指针。
如果需要使用扩展信号响应函数,则 sa_flags 必须设置 SA_SIGINFO,此时,结构体 act 中的成员 sa_sigaction 将会替代 sa_handler(事实上他们是联合体里面的两个成员, 是非此即彼的关系),扩展的响应函数接口如下:
void (*sa_sigaction)(int, siginfo_t *, void *);
该函数的参数列表详情:
第一个参数:int 型,就是触发该函数的信号;
第二个参数:siginfo_t 型指针,指向如下结构体:
siginfo_t
{
int si_signo; // si_signo、si_errno 和 si_code 对所有信号都有效
int si_errno;
int si_code;
int si_trapno; // 以下成员只对部分情形有效,详情见下面的注解
pid_t si_pid;
uid_t si_uid;
int si_status;
clock_t si_utime;
clock_t si_stime;
sigval_t si_value;
int si_int;
void *si_ptr;
int si_overrun;
int si_timerid;
void *si_addr;
long si_band;
int si_fd;
short si_addr_lsb;
}
第三个参数:一个 void 型指针,该指针指向一个上下文环境,一般很少使用。
发送者进程使用 kill( )/sigqueue( )发送信号时 si_pid 和 si_uid 将会被填充为其 PID 及其实际用户 ID,另外如果使用的是 sigqueue( )发送信号,那么 si_int 和 si_ptr 为其发送额外数据。
注意:
sigqueue()函数相当于扩展版的 kill 函数;
sigaction()函数相当于扩展版的 signal 函数。
举例:
sigaction.c
#include
#include
#include
#include
#include
#include
//扩展信号响应函数
void func(int sig, siginfo_t *value, void *p)
{
printf("sig = %d\n", sig); //触发扩展信号响应函数的信号
printf("接收数据: %d\n", value->si_int); //value.si_int 对应发送数据联合体里面的int
}
int main(int argc, char const *argv[])
{
printf("进程%d开始运行\n", getpid());
struct sigaction act;
act.sa_flags = SA_SIGINFO; //使用扩展信号响应函数而不是标准响应函数
act.sa_sigaction = func; //将扩展信号响应函数地址给到sa_sigaction变量赋值
//接收传过来的数据,信号响应函数就得要使用扩展信号响应函数
sigaction(10, &act, NULL);
return 0;
}
sigquene.c
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
//发送信号和数据给某个进程
int sig, pid;
scanf("%d %d", &pid, &sig);
//发送的数据联合体变量
union sigval data;
data.sival_int = 1000; //发送的数据1000
sigqueue(pid, sig, data);
while(1);
return 0;
}
信号在操作系统内核中有一个数据模型,用于表示和管理进程间通信的信号。这个数据模型包括信号的产生、传递、处理以及信号控制块等要素。下面就是信号的内核数据模型的介绍:
信号控制块(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)开发板实战
一键三连喔
~