【进程间通信】信号

  • (꒪ꇴ꒪ ),Hello我是祐言QAQ
  • 我的博客主页:C/C++语言,数据结构,Linux基础,ARM开发板,网络编程等领域UP
  • 快上,一起学习,让我们成为一个强大的攻城狮!
  • 送给自己和读者的一句鸡汤:集中起来的意志可以击穿顽石!
  • 作者水平很有限,如果发现错误,请在评论区指正,感谢


        当谈及进程间通信(IPC),我们需要寻找途径来使不同进程能够交换数据和信息。在操作系统中,这一通信机制被称为IPC,它有多种方式可以实现。本文将着重探讨其中之一信号(Signal)以及相关的概念、分类和使用方式。

一、什么是信号

        信号是一种操作系统层面对中断机制的软件模拟,作为一种异步通信方式它允许进程在某些事件发生时向其他进程发送通知。信号的生命周期包括信号的产生、注册、响应和处理以及注销。

        其中信号响应方式分为三类

  1. 忽略信号:某些信号可以被进程忽略,但需要注意的是,特殊的信号 SIGKILL(9)SIGSTOP (19)不能被忽略,也不能被捕捉,其默认操作无法修改。
  2. 捕捉信号响应函数:进程可以为特定信号注册自定义的信号处理函数。当进程接收到该信号时,会执行所注册的处理函数,从而实现特定的行为
  3. 执行缺省操作:Linux系统下的每种信号都有其预定义的默认操作,如果没有指定特定的信号处理方式,系统将会执行该信号的默认操作。

        换句话说,除了这两个信号之外的其他信号,接收信号的目标进程按照如下顺序来做出反应:

A :如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。否则进入 B。

B:如果该信号被捕捉,那么进一步判断捕捉的类型:

        B1:如果设置了响应函数,那么执行该响应函数。

        B2: 如果设置为忽略,那么直接丢弃该信号。

        否则进入 C。

C:执行该信号的缺省动作(默认操作)。

注意:

        信号的数值以及对应的名称可以通过命令 kill -l 查看。

【进程间通信】信号_第1张图片

信号 缺省动作 备注
SIGHUP 1 终止 控制终端被关闭时产生
SIGINT 2 终止 从键盘按键产生的中断信号(比如Ct+C)
SIGQUIT 3 终止并产生转储文件 从键盘按键产生的退出信号(比如Ct+\)
SIGKILL 9 终止 系统杀戮信号
SIGCONT 18 恢复运行 系统恢复运行信号
SIGSTOP 19 暂停 系统暂停信号

二、 信号分类

        信号可以分为非实时信号(前31个)实时信号(后31个)。这两者在响应方式上有所不同。

  1. 非实时信号(不可靠信号):非实时信号的响应不会排队,可能会发生嵌套。如果进程未及时响应一个非实时信号,之后的该信号将会被丢弃。每个非实时信号都与系统事件相关联,当事件发生时,对应的信号被产生。
  2. 实时信号(可靠信号):相比之下,实时信号的响应是按照接收顺序排队的,不会发生嵌套。即使同一种实时信号被多次发送,也不会被丢弃,而是依次响应。与实时信号相关联的没有特殊的系统事件。

三、信号的使用

1.发送信号

        使用 kill(pid, sig) 函数向指定进程发送信号。

#include 
#include 
int kill(pid_t pid, int sig);


2.响应方式注册

        使用 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 信号,售票员收到后打印 "终点站到了,请所有乘客下车!"。

【进程间通信】信号_第2张图片

#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;
}


3.给自己发送信号

        使用 raise(sig) 函数向自己发送信号。

#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
	printf("进程开始运行\n");
	sleep(5);
	raise(9);//给自己发送一个信号 9 杀死自己
	printf("进程结束运行\n");
	return 0;
}


4.等待信号

        使用 pause() 函数使进程进入等待状态,直到收到一个信号。

#include 
#include 
#include 
#include 

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);

【进程间通信】信号_第3张图片

        下面我们就来用一用信号集与阻塞,设计两个进程:

        (1)进程A负责不断向进程B发送信号(排除 SIGSTOP 和 SIGKILL);
        (2)进程B接收信号,并将每个信号注册到同一个响应函数中,打印信号值。在发送信号之前,进程B阻塞了所有信号,然后进程A发送所有信号,延时5秒后,进程B解除对信号的阻塞。

【进程间通信】信号_第4张图片

#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;
}

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);
};

        标准信号响应函数指针 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;
}

8.信号的内核数据模型

        信号在操作系统内核中有一个数据模型,用于表示和管理进程间通信的信号。这个数据模型包括信号的产生、传递、处理以及信号控制块等要素。下面就是信号的内核数据模型的介绍:

        信号控制块(Signal Control Block,SCB): 信号控制块是操作系统内核中用于管理和维护信号信息的数据结构。每个进程都有一个关联的信号控制块,它存储了进程接收到的信号以及相关的处理和状态信息。这个数据结构通常包括以下字段:

  • 信号位图(Signal Bitmap): 用于表示进程当前处于阻塞状态的信号。每个信号都对应一个位,如果某个信号的位为1,则表示该信号被阻塞。

  • 信号队列(Signal Queue): 用于存储进程接收到但尚未处理的信号。信号队列采用队列的形式,其中每个节点存储了信号的类型、时间戳以及其他相关信息。

        信号的产生和传递: 信号的产生通常是由特定事件触发,如硬件异常、软件条件等。当这些事件发生时,操作系统内核会将相应的信号发送给相应的进程。信号会按照进程的层次结构向父进程或子进程传递,或者向指定的进程传递。

        信号的处理: 当进程接收到一个信号时,它可以按照事先注册的信号处理方式来响应。这可以通过调用 signal()sigaction() 函数来实现。处理方式可以是忽略信号、执行默认操作、或执行自定义的信号处理函数。在执行信号处理函数期间,进程可以根据信号的类型和处理函数的内容来进行特定的操作,从而实现对信号的处理。

        信号的阻塞和解除阻塞: 进程可以通过阻塞信号来暂时屏蔽某些信号的传递和处理。这在某些情况下很有用,比如在临界区代码中防止特定信号的干扰。进程可以使用 sigprocmask() 函数来设置信号的阻塞状态,以及使用 SIG_BLOCKSIG_UNBLOCK 来分别添加和解除信号的阻塞。

        信号的排队和处理顺序: 对于非实时信号,当多个信号被发送到同一个进程时,它们可能会排队等待被处理。对于实时信号,系统会保证信号按照发送顺序排队,不会发生嵌套。因此,进程需要按照信号排队的顺序来处理它们。

【进程间通信】信号_第5张图片

四、总结

         信号作为一种进程间通信的手段,允许进程以异步的方式相互通知。通过掌握信号的基本概念、分类和使用方式,我们可以更好地实现进程间的通信和协调,从而提升系统的整体效率和稳定性。

        更多C/C++语言Linux系统数据结构ARM板实战相关文章,关注专栏:

   手撕C语言

            玩转linux

                    脚踢数据结构

                            系统、网络编程

                                     探索C++

                                             6818(ARM)开发板实战

写在最后

  • 今天的分享就到这啦~
  • 觉得博主写的还不错的烦劳 一键三连喔~
  • 感谢关注

你可能感兴趣的:(系统编程,服务器,linux,运维,开发语言,网络协议,信号处理,信息与通信)