系统编程学习笔记第四章:信号通信
大家好,我是小智!
1. 进程间通信方式的含义与作用
进程间通信(Inter Process Communication,简称IPC)指的是进程之间的信息交换,进程间通信的方式有很多,比如管道通信、信号通信、共享内存、消息队列、信号量组、POSIX信号量等。
进程间通信可以达到数据传输、共享资源、控制进程等目的,方便用户对进程进行控制和管理。
2. 信号的概念
信号(英文翻译为signal)是Unix系统、类Unix系统(比如Linux系统)以及其他POSIX兼容的操作系统中用于实现进程间通信的一种方式。信号采用的是一种异步通信机制。
思考:请问什么是异步通信?异步通信和同步通信的区别是什么?相比于同步通信,异步通信的优势是什么?
回答:同步指的是当进程发起一个请求,但是该请求并未马上响应,则进程就会阻塞等待,直到请求被响应。举个例子:双方进行交易的时候遵循“一手交钱,一手交货”的原则。
而异步指的是当进程发起一个请求,如果该请求并未马上响应,则进程会继续执行其他的任务,过来一段时间请求得到了响应,则会通知该进程,该进程得到通知再去对请求做出处理。
举个例子:买东西忘带钱来了,商店老板说“没事,下次带来就行”,过段时间你把钱交给老板,老板对账目重新计算,老板并不会因为你买东西忘带钱一直等你回家取钱就不做其他人的生意,而是继续做其他人的生意,因为这样可以提高老板的盈利。也就是说相比于同步通信,异步通信可以提高程序的执行效率。

思考:如果进程A向进程B发起一个请求事件,但是进程B并未及时对进程A的请求进行响应,此时进程A又不打算一直等待进程B的响应,请问过段时间后如果进程B已经完成请求响应,应该如何通知进程A?
回答:信号采用的是一种异步通信机制,可以用于通知接收信号的进程有哪些事件发生了。当一个信号发送给一个某进程,操作系统就会中断接收到信号的进程,此时进程中任何非原子性的操作都将被中断。
另外,如果用户在进程中定义了接收到某信号要执行的处理接口,则进程收到该信号后就会执行对应的函数,否则就执行默认的处理函数。
举个例子:比如王者荣耀逆风局,队友可能觉得胜利无望,则发起投降,但是此时如果你发送“稳住,我们能赢”的信号,则大家重拾信心之后拖到后期可能会逆风翻盘,如果发起投降之后大家没有任何的操作,则默认点击投降按钮。
3. 信号的类型
思考:既然Linux系统下提供了信号作为进程间的通信方式,那请问Linux系统下提供了多少种信号,以及如何查看这些信号?
回答:Linux系统中提供了shell命令:kill - l,该命令的作用是给某个进程发送信号,参数-l可以列出信号的名称
3.1 kill函数

3.2 信号种类

由上图可以发现,Linux系统中的信号编号为1到64,其中编号为1到31的信号为普通信号,编号为34到64的信号为实时信号。
3.2.1 普通信号
Linux系统中的普通信号也被称为不可靠信号,指的是当进程接收到了很多的信号请求但是又不能及时处理时,不会把信号形成队列,而是把其余未被处理的信号直接丢弃,只留下一个信号。Linux系统中的普通信号是从Unix系统继承过来的。
3.2.2 实时信号
Linux系统中的实时信号也被称为可靠信号,指的是当进程接收到了很多信号请求但是又不能及时处理时,会把未处理的信号形成队列,然后按照顺序依次处理,不会丢弃信号。Linux系统中的实时信号是新增加的。
4. Linux系统中信号的含义
思考:既然Linux系统下提供了这些信号,但是用户无法通过信号编号和信号名称知道信号的作用,请问如何得到这些信号的含义是什么?
回答:关于Linux系统中信号的相关含义,可以通过man手册的第7章查找signal进行了解,如下图。

上图的中文解释如下方的两幅图:


注意:通过
kill -l输出的信号名称都是大写的,按照编程规则而言这些信号都是宏定义,另外,可以看到信号前面有一个编号,这个编号就是指信号宏定义的替换列表的值。这些宏定义都被定义在signal.h头文件中,头文件的路径:/usr/include/x86_64-linux-gnu/asm/signal.h
5. 在本地Linux模拟验证码实现思路图

6.信号的产生
Linux系统下信号产生的条件是较复杂的,一般可以分为以下几种情况,具体区别如下所示:
6.1 按键触发
按键触发指的是用户按下某个快捷键,然后由内核发送指定的信号给进程,比如用户准备在Linux系统的终端输入shell命令,则会先开启一个Terminal终端,然后在终端中执行了一个可执行文件,此时操作系统会创建一个进程,并把可执行文件的代码段和数据段加载到进程空间中,并分配CPU时间片给到该进程,此时进程会从就绪态进入运行态。
在进程执行过程中,因为用户打算提前结束进程,所以用户在终端按下快捷键Ctrl+C,键盘就产生了一个硬件中断,操作系统会把Ctrl+C解释为SIGINT信号并记录在进程的PCB中。
此时CPU会暂停执行用户空间中的代码,然后去执行内核空间中的硬件中断,当硬件中断执行完成后CPU会返回用户空间,但是CPU需要先处理记录在进程PCB中的信号,而CPU发现此时进程PCB中的信号是SIGINT,该信号含义是终止进程,所以进程会切换到终止态,相当于结束进程。
注意:在终端正在运行的进程叫做前台进程,用户使用快捷键Ctrl+C只能结束前台进程,无法结束后台进程,如果想把一个进程转为后台执行,则可以在运行程序的时候添加符号 &,例如
./program &。
6.2 硬件异常
硬件异常产生的信号会由系统硬件进行检测,比如进程中执行除以0的指令会导致ALU异常,或者进程中访问了非法内存地址会导致MMU异常,此时内核会发送给进程相关的信号,例如下图。

6.3 调用接口
Linux系统中提供了一个名字叫做kill()的函数接口,用户利用该接口可以实现主动向指定进程发送信号,用户可以通过man 2 kill查阅函数的使用规则,如下图。

可以看到kill函数具有两个参数,第一个参数指的是目标进程的PID,第二个参数则是要发送的信号名称。kill函数调用成功则返回0,调用失败则返回-1。
除了kill()函数之外,Linux系统还提供了一个名称叫做raise()的函数接口,两者的区别是kill()函数可以向指定的进程发送信号,而raise()函数只能向当前进程发送信号,如下图。

6.4 发送指令
用户除了在程序中调用kill()函数发送信号之外,还可以直接在终端中使用shell命令:kill 给指定PID的进程发送信号,其实kill命令也是调用kill函数来实现信号的发送。如果kill命令没有指定信号名称,则默认发送SIGTERM信号,该信号表示终止进程。

6.5 内核检测
当内核检测到某种软件条件发生时也可以通过信号通知进程,例如内核检测到闹钟超时则会产生SIGALRM信号,或者当内核检测到进程向读端已关闭的管道写数据时就产生SIGPIPE信号。
Linux系统下提供了一个叫做alarm()的函数接口,alarm翻译为中文是闹钟的意思,也就是说该函数可以设置内核定时器的时间,时间是以秒为单位的,当设置的秒数到达时,就相当于闹钟时间达到,此时内核检测到闹钟到达之后就会向调用该函数的进程发送SIGALRM,该信号的默认含义是终止进程。

7. 信号的处理
当进程接收到信号之后,可以分为三种情况来对信号进行处理,分别是默认、捕捉和忽略。
7.1 默认处理
由于Linux系统中已经对普通信号的含义进行了规定,也就是当进程接收到某个信号后,如果用户没有自定义信号的执行动作,则会采用默认处理的方式对信号进行响应,如下图,比如进程接收SIGTERM信号后则会被终止。

7.2 捕捉信号
信号捕捉指的是在进程接收到某个指定信号之前,先设计好该信号响应函数,并把该信号和该响应接口进行关联,这样当进程接收到信号之后,就不会执行信号的默认响应动作,而是执行用户指定的响应动作。

Linux系统中提供了一个名称叫做signal()的函数,用户可以通过man 2 signal了解函数的使用规则。

可以看到,该函数有两个参数,第一个参数指的是目标信号的编号,第二个参数指的是信号的处理函数的地址,是一个函数指针类型,void (*sighandler_t)(int),用户需要按照该类型定义信号处理接口。
void function(int signal) //信号处理函数没有返回值,有一个int型参数,名称为signal即可
{
//响应动作
}
当捕捉到信号时,不论进程的主控制流程当前执行到哪儿,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。
信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。引入了信号处理函数使得一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局变量、硬件资源等),就有可能出现冲突。
注意:并不是所有的信号都可以自定义信号响应动作,对于SIGKILL信号和SIGSTOP信号而言,用户不可以自定义这两个信号的响应接口。
另外,可以看到signal()函数提供了两个宏,SIG_IGN指的是信号可以被忽略,SIG_DFL指的是信号关联默认的响应动作。
7.3 忽略信号
忽略信号指的是当进程接收到某个信号后,并不打算执行该信号的相关动作,而选择直接丢弃该信号。用户可以通过调用signal()函数,只不过函数的第二个参数设置为SIG_IGN即可。
8. 信号的阻塞
有时进程会接收到很多来自其他进程的信号,但是该进程暂时不打算对某些指定信号做出响应,所以需要暂时“屏蔽”某些信号。比如程序在执行过程中不打算受到用户Ctrl+C的强制结束进程的影响,所以进程需要阻塞该信号,当该信号到达时对它进行屏蔽,当做“看不见”。
Linux系统中提供了一个名称叫做sigprocmask()的函数接口来设置信号集的属性,使用规则如下图:

可以看到,该函数可以对信号集进行设置,比如对信号集进行阻塞、解除阻塞等相关操作,当然,用户需要创建信号集,并把相关信号添加到信号集中,或者从信号集中删除某些信号。

注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
9. 信号的挂起
当进程被系统调度程序进行调度,得到CPU资源进入运行态时,才有能力处理其他进程发送过来的信号,当进程处于其他状态时,就算其他进程发送信号过来,该进程也无法处理。
所以进程中就提供了一个挂起信号集,所有被发送到这个进程的信号首先被放入这个信号集,挂起信号集存储了进程的待处理信号,这些信号必须要等到进程被系统调度的时候才能被进一步响应。
10. 练习
-
用户设计两个程序,其中程序A正在执行一个死循环,要求程序B发送一个结束信号给程序A,当程序A接收到信号之后则结束。
-
用户设计两个程序,要求进程A中自定义信号SIGUSR1的响应接口,要求进程B每隔一段时间向进程A就发送SIGUSR1信号,测试进程A是否可以执行关联的响应接口。

-
用户设计一个程序,要求程序每隔1s就获取当前系统时间并输出到终端,但是用户不打算让其他用户通过快捷键Ctrl+C来强制结束该程序,所以要求现在设计该程序。
-
根据上面的接口,设计一个程序,要求把快捷键Ctrl+C的对应信号进行阻塞,需要创建一个信号集,把该信号添加到信号集,对信号集属性进行设置(阻塞),然后测试发送该信号是否会被进程响应。


浙公网安备 33010602011771号