1.前言
生活当中我们无时不刻都在接触外界给予我们各种各样的信号,比如穿越马路时看到红灯就得停下来,在比如听到手机铃声就得接电话,那么生活中如果很多重要的信号同时发生了,你会先做哪个事情?换句化说你会如何处理?我们的操作系统就这种情况给出了一系列的设计
2.系统的介绍信号
2.1什么是linux信号
信号是一种用于通知进程发生某种事件或异常情况的机制。当发生特定的事件或异常时,操作系统会检测到并向目标进程发送一个信号,以便进程能够做出相应的处理。
2.2信号处理的常见方式
1.默认(进程自带的,程序员已经写好的逻辑)
2.忽略
3.自定义动作(捕捉信号)
2.3进程的保存
进程pcb内部保存了信号位图字段,比如说0000 0100 信号收到了就把某一个0改成1
3.信号的产生
3.1由软件条件下产生的信号
在我们讲解这个概念之前先来回顾和学习几个接口函数
//在Linux下,signal函数用于设置信号的处理方式。其原型如下:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
下面是对Linux下signal函数的详细解释:
1.signum参数是要设置处理函数的信号的编号,表示要处理的具体信号类型。常见的信号编号可以在 <signal.h> 头文件中找到,例如SIGINT、SIGTERM、SIGSEGV等。
2.handler参数是一个函数指针,指向用户定义的信号处理函数。该函数接收一个整型参数,表示接收到的信号编号。信号处理函数的返回类型为void。
3.signal函数的返回类型是一个函数指针,它指向之前的信号处理函数。如果之前没有设置过处理函数,则返回值为SIG_DFL(默认处理方式)或SIG_IGN(忽略信号)。
如果将handler参数设置为SIG_DFL,表示将信号的处理方式恢复为默认操作。对于大多数信号,这将导致进程终止或生成核心转储文件。
如果将handler参数设置为SIG_IGN,表示忽略接收到的信号。这意味着当进程接收到该信号时,不会采取任何操作。
如果将handler参数设置为用户自定义的信号处理函数,则在接收到信号时,将调用该函数来执行相应的操作。
//例子:
#include <stdio.h>
#include <signal.h>
void sigintHandler(int signum) {
printf("Received SIGINT signal. Exiting...\n");
// 执行一些清理操作或其他逻辑
exit(0);
}
int main() {
// 注册SIGINT信号的处理函数
signal(SIGINT, sigintHandler);
// 进入主循环
while (1) {
// 执行主要逻辑
}
return 0;
}
在上述示例中,signal(SIGINT, sigintHandler)将SIGINT信号的处理方式设置为sigintHandler函数,该函数在接收到SIGINT信号时打印一条消息并退出程序。
//在Linux下,alarm函数用于设置定时器信号,即在指定的时间间隔后发送一个SIGALRM信号给当前进程。该函数的原型如下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
1.seconds参数表示定时器的时间间隔,以秒为单位。当定时器到期时,将发送一个SIGALRM信号给当前进程。
2.alarm函数返回一个无符号整数,表示之前剩余的定时器时间。如果之前已设置了定时器,该值表示剩余的时间;如果之前没有设置定时器,则返回值为0。
tips:
当seconds为0时,表示取消之前设置的定时器。如果之前已设置了定时器,将返回剩余的时间;如果之前没有设置定时器,返回值为0。
只能有一个定时器处于活动状态。如果在调用alarm函数之前已经设置了定时器,新的调用将覆盖前一个定时器,并重新开始计时。
当定时器到期时,会产生一个SIGALRM信号。可以通过注册SIGALRM信号的处理函数来捕获和处理该信号。(表示定时器到期的信号,用于通知进程已经达到了预定的时间间隔。)
其实我们还有很多种与信号相关的函数,当操作系统识别到某种软件(软件是一堆程序,各种逻辑框架是程序员自己写的)条件符合发送某种信号的标准时,操作系统构建信号发送给指定的进程。
3.2硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了
以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非
法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
我们就以上两种情况来具体谈谈这个问题。
1.除以0指令
在计算机中,处理运算的是cpu这个硬件,在cpu内部的状态寄存器(位图)中有对应的状态标记位和溢出标记位,如果溢出标记位为1,那么操作系统会立刻找到当前运行进程的pid,接着操作系统对对应进程发送对应信号。
(正常情况下来讲硬件异常会默认退出)
2.野指针或越界问题
当虚拟地址转换成实际地址的时候需要用到页表和mmu(mmu是硬件),如果越界了或者是非法访问地址,在转换时一定会报错。
总结:所有信号有它的来源,但最终都被操作系统识别解释并发送。
3.3知识点补充----核心转储
这幅图想必大家都不陌生吧,在我们用函数获取子进程的status(位图)曾经给出了这样的一副图,而我们今天讲解的核心转储的是否启用就和这个core dump标志位有关。
那么究竟什么是核心转储呢?
核心转储(Core Dump)是指在程序运行期间发生严重错误或崩溃时,操作系统将进程的内存状态保存到一个文件中的过程。
一般而言云服务器(生产环境)的核心转储功能是被默认关闭的,因为在生产环境中保持磁盘空间的充足和高效利用是非常重要的,而禁用核心转储功能可以避免不必要的磁盘空间占用,确保系统的正常运行。
4.信号的保存和信号的处理
因为这一块的内容有相似处可以用来进行比较好的对比所以放在一块讲。
4.1信号存储的内容-------位图表的理解
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号
产生时,内核在进程控制块中设置该信号为未决状态(1)以指示进程有一个未决信号等待处理,直到信号递达,并通过函数指针调用所对应的函数进行处理,才开始清除该标志(改成0)。
tips:
(信号掩码(Signal Mask)是一个位图,用于指定进程当前阻塞(屏蔽)哪些信号的递送。通过设置信号掩码,进程可以控制是否接收特定类型的信号。信号标志位中的阻塞部分是进程的运行时状态的一部分,指示某个特定的信号是否被阻塞,这个标志位间接受到信号掩码的影响。)
- 当进程接收到一个信号时,如果该信号在信号掩码中被阻塞(signal mask),那么该信号会保持未决状态(1)。
- 在信号存储未决标志位为未决状态(阻塞情况下),即使进程接收到相同类型的信号,它们也不会立即处理,而是继续保持未决状态。
- 当进程解除对某个信号的阻塞时,内核会将该信号从未决状态转变为"待处理"(ready )状态,并将信号存储未决标志位相应地更新为"非未决"(0)状态。
- 一旦信号存储未决标志位被设置为非未决状态,进程就可以根据信号的处理方式(例如,执行默认操作、调用信号处理函数)来处理该信号。
4.2信号集操作函数
前置小知识sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号
的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有
**效”和“无效”的含义是该信号是否处于未决状态。**下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当
前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
#include <signal.h>
int sigemptyset(sigset_t *set);
``sigemptyset` 函数用于将信号掩码设置为空集,即将所有位都清零,表示不阻塞任何信号。
int sigfillset(sigset_t *set);
``sigfillset` 函数用于将信号掩码设置为满集,即将所有位都置为1,表示阻塞所有信号。
int sigaddset (sigset_t *set, int signo);
``sigaddset` 函数用于将指定的信号添加到信号掩码中,将对应位设置为1,表示阻塞该信号。
int sigdelset(sigset_t *set, int signo);
``sigdelset` 函数用于从信号掩码中移除指定的信号,将对应位设置为0,表示解除对该信号的阻塞。
int sigismember(const sigset_t *set, int signo)
``sigismember` 函数用于检查指定的信号是否在信号掩码中。如果该信号被阻塞(信号掩码对应位为1),则返回1;否则,返回0。
//sigprocmask 函数用于修改进程的信号掩码(Signal Mask)。它可以用于阻塞或解除阻塞指定的信号,并提供了对信号掩码的原子操作。
//下面是关于 sigprocmask 函数的详细解释:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
1.//how 参数指定了如何修改信号掩码,它可以采用以下三个值之一:
//SIG_BLOCK:将 set 参数中指定的信号添加到当前信号掩码中,即将这些信号阻塞。在此模式下,新的信号掩码是当前信号掩码和 set 的并集。
//SIG_UNBLOCK:从当前信号掩码中移除 set 参数中指定的信号,即解除对这些信号的阻塞。在此模式下,新的信号掩码是当前信号掩码和 set 的补集。
//SIG_SETMASK:将当前信号掩码替换为 set 参数中指定的信号掩码,即设置新的信号掩码为 set。在此模式下,新的信号掩码直接被 set 替换。
2.set 参数是一个指向 sigset_t 类型的指针,表示要添加、移除或替换的信号集。sigset_t 是一个用于存储信号掩码的数据结构,它通常是一个位图,每个位对应一个特定的信号。
3.oldset 参数是一个指向 sigset_t 类型的指针,用于存储调用 sigprocmask 函数之前的旧信号掩码。如果不需要获取旧的信号掩码,可以将该参数设置为 NULL。
4.sigprocmask 函数的返回值为整型,表示函数调用的结果。如果成功,返回值为 0,如果出错,返回值为 -1,并设置相应的错误码。
使用 sigprocmask 函数,您可以对当前进程的信号掩码进行修改,控制信号的阻塞和解除阻塞。通过合理设置信号掩码,您可以选择性地阻塞或允许某些信号的递送,以满足程序的需求。
请注意,sigprocmask 函数是一个原子操作,它保证在修改信号掩码期间不会被中断。这是确保在多线程或并发环境中正确处理信号掩码的重要机制之一。
//sigpending 函数用于获取当前被阻塞的未决信号集(Pending Signal Set),即已经发送给进程但由于信号阻塞而暂时未能处理的信号。
int sigpending(sigset_t *set);
1.set 参数是一个指向 sigset_t 类型的指针,用于存储被阻塞的未决信号集。sigset_t 是一个用于存储信号掩码的数据结构,它通常是一个位图,每个位对应一个特定的信号。
2.sigpending 函数的返回值为整型,表示函数调用的结果。如果成功,返回值为 0,如果出错,返回值为 -1,并设置相应的错误码。
5.信号的捕捉
5.1前置小知识
首先在了解信号捕捉之前先为大家普及一些额外概念。
正如我们小标题所说在,在这会为大家介绍一些函数来捕捉相应的信号。
想象以下这样一段场景:
我们写了一个程序捕捉了所有的信号,我们是不是就写了一个不会异常不会被杀掉的进程?
显然操作系统考虑到了这样一种问题,kill - 9 命令就能无视这种信号捕捉杀死进程。
5.2状态间变化
在信号进行一系列处理的期间,我们的操作系统会在内核态和用户态直接相互转换。
下面先正式介绍一下这两种状态
- 用户态(User Mode):
- 用户态是指操作系统中的一种执行模式,其中用户程序在较低的特权级别下运行。
- 在用户态下,用户程序只能执行受限的操作,无法直接访问操作系统核心和底层硬件资源。
- 用户程序可以执行常规的计算任务、访问用户空间的内存和设备、进行文件操作等。
- 用户态下的程序执行受到操作系统的保护,无法直接影响其他程序或操作系统本身的稳定性和安全性。
- 内核态(Kernel Mode):
- 内核态是指操作系统中的一种执行模式,其中操作系统内核在较高的特权级别下运行。
- 在内核态下,操作系统内核具有更高的特权级别和更广泛的访问权限,可以执行特权指令和访问底层硬件资源。
- 内核态下的操作系统内核可以执行关键的系统任务,如管理内存、调度进程、访问设备驱动程序等。
- 内核态具有更高的特权级别,可以执行更底层和敏感的操作,但也需要小心处理,以确保操作系统的稳定性和安全性。
实际上在我们的内核状态下使用的也是进程地址空间那一套知识体系,下面我给大家一副图,就能很清晰的得到一个直观理解:
这里我们在提出一个问题:当我公用一个cpu来执行这一份实际内存时,如何知道我应该是内核态的执行权限还是内核态。
实际上cpu存在一套特殊的寄存器,比如CR3,上面记录的信息可以表示当前cpu的执行权限。
5.3信号捕捉具体方案以及内核态,用户态转变
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码
是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行
main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号
SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler
和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返
回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复
main函数的上下文继续执行了。
5.4相关函数的使用
//sigaction 函数用于设置和修改信号处理程序,它提供了更为灵活和可靠的信号处理机制。下面是关于 sigaction 函数的详细介绍:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
1.signum 参数是要设置或修改的信号编号,它可以是标准的 POSIX 信号编号(如 SIGINT、SIGTERM)或自定义的信号编号。
2.act 参数是一个指向 struct sigaction 结构的指针,用于指定新的信号处理程序。
3.oldact 参数是一个指向 struct sigaction 结构的指针,用于存储之前的信号处理程序(如果需要备份)。
4.sigaction 函数的返回值为整型,表示函数调用的结果。如果成功,返回值为 0,如果出错,返回值为 -1,并设置相应的错误码。
//struct sigaction 结构定义如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
1.sa_handler 是一个函数指针,用于指定简单的信号处理函数。当信号到达时,将调用该函数进行处理。
2.sa_sigaction 是一个函数指针,用于指定复杂的信号处理函数。与 sa_handler 不同,它接收三个参数:信号编号、指向 siginfo_t 结构的指针(包含关于信号的详细信息)和一个指向 ucontext_t 结构的指针(描述进程上下文的结构)。
3.sa_mask 是一个 sigset_t 类型的信号掩码,用于指定在信号处理程序执行期间要阻塞的信号集。阻塞这些信号可以防止它们在处理程序执行期间再次触发。
4.sa_flags 是一个标志位,用于指定附加的行为选项。常见的标志包括 SA_RESTART(指定系统调用在信号处理程序返回后自动重启)和 SA_SIGINFO(指示使用 sa_sigaction 而不是 sa_handler 进行处理)。
5.sa_restorer 是一个指向恢复函数的指针,用于在信号处理程序返回后进行其他处理。在大多数情况下,它被设置为 NULL。
6.可重入函数,不可重入函数
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因
为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函
数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从
sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步
之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只
有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称
为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,
如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
7.volatile关键字
该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
[hb@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c #-O2
.PHONY:clean
clean:
rm -f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal
[hb@localhost code_test]$ cat sig.c
标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循
环,进程退出
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
[hb@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1
优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进
程继续运行!但是很明显flflag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flflag,
并不是内存中最新的flflag,这就存在了数据二异性的问题。 while 检测的flflag其实已经因为优化,被放在了
CPU寄存器当中。如何解决呢?很明显需要 volatile
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
[hb@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量
的任何操作,都必须在真实的内存中进行操作。