【Linux】进程信号

前言

本篇博客我们来总结下Linux下信号方面的知识

💓 个人主页:zkf

⏩ 文章专栏:Linux

若有问题 评论区见📝

🎉欢迎大家点赞👍收藏⭐文章

目录

1.信号的快速认识

1.1⽣活⻆度的信号

[1.2 技术应⽤⻆度的信号](#1.2 技术应⽤⻆度的信号)

[1.2.1 ⼀个样例](#1.2.1 ⼀个样例)

1.2.2⼀个系统函数

1.3信号概念

[1.3.1 查看信号](#1.3.1 查看信号)

1.3.2信号处理

2.产生信号

[2.1 通过终端按键产⽣信号](#2.1 通过终端按键产⽣信号)

[2.1.1 基本操作](#2.1.1 基本操作)

[2.1.2 理解OS如何得知键盘有数据](#2.1.2 理解OS如何得知键盘有数据)

[2.1.3 初步理解信号起源](#2.1.3 初步理解信号起源)

2.2使⽤函数产⽣信号

2.2.1kill

2.2.2raise

2.2.3abort

[2.3 由软件条件产⽣信号](#2.3 由软件条件产⽣信号)

[2.4 硬件异常产⽣信号](#2.4 硬件异常产⽣信号)

3.保存信号

3.1信号其他相关常⻅概念

3.2在内核中的表示

3.3sigset_t

3.4信号集操作函数

4.捕捉信号

[4.1 信号捕捉的流程](#4.1 信号捕捉的流程)

4.2sigaction

[4.3 穿插话题 - 操作系统是怎么运⾏的](#4.3 穿插话题 - 操作系统是怎么运⾏的)

4.3.1硬件中断

4.3.2时钟中断

4.3.3死循环

[4-3-4 软中断](#4-3-4 软中断)

5.可重入函数

6.volatile


1.信号的快速认识

1.1⽣活⻆度的信号

你在⽹上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临
时,你该怎么处理快递。也就是你能"识别快递"
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。
那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的⾏为
并不是⼀定要⽴即执⾏,可以理解成"在合适的时候去取"。
在收到通知,再到你拿到快递期间,是有⼀个时间窗⼝的,在这段时间,你并没有拿到快递,但是
你知道有⼀个快递已经来了。本质上是你"记住了有⼀个快递要去取"
当你时间合适,顺利拿到快递之后,就要开始处理快递了。⽽处理快递⼀般⽅式有三种:1. 执⾏默
认动作(幸福的打开快递,使⽤商品)2. 执⾏⾃定义动作(快递是零⻝,你要送给你你的⼥朋友)
3. 忽略快递(快递拿上来之后,扔掉床头,继续开⼀把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

基本结论:
你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
信号产⽣之后,你知道怎么处理吗?知道。如果信号没有产⽣,你知道怎么处理信号吗?
知道。所以,信号的处理⽅法,在信号产⽣之前,已经准备好了。
处理信号,⽴即处理吗?我可能正在做优先级更⾼的事情,不会⽴即处理?什么时候?合
适的时候。
信号到来 | 信号保存 | 信号处理
怎么进⾏信号处理啊?a.默认 b.忽略 c.⾃定义, 后续都叫做信号捕捉。

1.2 技术应⽤⻆度的信号

1.2.1 ⼀个样例

// sig.cc

include <iostream>

include <unistd.h>

int main ()
{
while ( true ){
std :: cout << "I am a process, I am waiting signal!" << std :: endl ;
sleep( 1 );
}
}
g++ sig.cc -o sig ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C
⽤⼾输⼊命令,在Shell下启动⼀个前台进程
⽤⼾按下 Ctrl+C ,这个键盘输⼊产⽣⼀个硬件中断,被OS获取,解释成信号,发送给⽬标前台进
程 , 前台进程因为收到信号,进⽽引起进程退出

1.2.2⼀个系统函数

NAME
signal - ANSI C signal handling
SYNOPSIS

include <signal.h>

typedef void (* sighandler_t )( int );
sighandler_t signal ( int signum, sighandler_t handler)
参数说明:
signum :信号编号 [ 后⾯解释,只需要知道是数字即可 ]
handler :函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏ handler ⽅法
⽽其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明⼀下,这⾥需要引⼊⼀
个系统调⽤函数
开始测试

include <iostream>

include <unistd.h>

include <signal.h>

void handler ( int signumber)
{
std::cout << " 我是 : " << getpid () << ", 我获得了⼀个信号 : " << signumber <<
std::endl;
}
int main ()
{
std::cout << " 我是进程 : " << getpid () << std::endl;
signal (SIGINT /*2*/ , handler);
while ( true ){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep ( 1 );
}
}
g++ sig.cc -o sig ./sig
我是进程 : 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C 我是 : 212569 , 我获得了⼀个信号 : 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C 我是 : 212569 , 我获得了⼀个信号 : 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
📌 思考:
这⾥进程为什么不退出?
这个例⼦能说明哪些问题?信号处理,是⾃⼰处理
请将⽣活例⼦和 Ctrl-C 信号处理过程相结合,解释⼀下信号处理过程?进程就是你,
操作系统就是快递员,信号就是快递,发信号的过程就类似给你打电话

📌 注意
要注意的是,signal函数仅仅是设置了特定信号的捕捉⾏为处理⽅式,并不是直接调⽤处
理动作。如果后续特定信号没有产⽣,设置的捕捉函数永远也不会被调⽤!
Ctrl-C 产⽣的信号只能发给前台进程。⼀个命令后⾯加个&可以放到后台运⾏,这样
Shell不必等待进程结束就可以接受新的命令,启动新的进程。
Shell可以同时运⾏⼀个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C
这种控制键产⽣的信号。
前台进程在运⾏过程中⽤⼾随时可能按下 Ctrl-C ⽽产⽣⼀个信号,也就是说该进程的⽤
⼾空间代码执⾏到任何地⽅都有可能收到 SIGINT 信号⽽终⽌,所以信号相对于进程的控
制流程来说是异步(Asynchronous)的。
关于进程间关系,我们在⽹络部分会专⻔来总结,现在就了解即可。
可以渗透 & 和 nohup

1.3信号概念

信号是进程之间事件异步通知的⼀种⽅式,属于软中断。

1.3.1 查看信号


每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义
#define SIGINT 2

1.3.2信号处理

( sigaction 函数稍后详细介绍),可选的处理动作有以下三种:
忽略此信号。

include <iostream>

include <unistd.h>

include <signal.h>

void handler ( int signumber)
{
std::cout << " 我是 : " << getpid () << ", 我获得了⼀个信号 : " << signumber
<< std::endl;
}
int main ()
{
std::cout << " 我是进程 : " << getpid () << std::endl;
signal (SIGINT /*2*/ , SIG_IGN); // 设置忽略信号的宏
while ( true ){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep ( 1 );
}
}
g++ sig.cc -o sig ./sig
我是进程 : 212681
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C^C^C^C^C^CI am a process, I am waiting signal! // 输⼊ ctrl+c 毫⽆反应
I am a process, I am waiting signal!
执⾏该信号的默认处理动作

include <iostream>

include <unistd.h>

include <signal.h>

void handler ( int signumber)
{
std::cout << " 我是 : " << getpid () << ", 我获得了⼀个信号 : " << signumber
<< std::endl;
}
int main ()
{
std::cout << " 我是进程 : " << getpid () << std::endl;
signal (SIGINT /*2*/ , SIG_DFL);
while ( true ){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep ( 1 );
}
}
g++ sig.cc -o sig ./sig
我是进程 : 212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C // 输⼊ ctrl+c, 进程退出,就是默认动作
提供⼀个信号处理函数,要求内核在处理该信号时切换到⽤⼾态执⾏这个处理函数,这种⽅式称为⾃
定义捕捉(Catch)⼀个信号。
上⾯的所有内容,我们都没有做⾮常多的解释,主要是先⽤起来,然后渗透部分概念和共识,下⾯我们从理论和实操两个层⾯,来进⾏对信号的详细学习、论证和理解。为了保证条理,我们采⽤如下思路来进⾏阐述:

2.产生信号

2.1 通过终端按键产⽣信号

2.1.1 基本操作

Ctrl+C (SIGINT) 已经验证过,这⾥不再重复
Ctrl+\(SIGQUIT)可以发送终⽌信号并⽣成core dump⽂件,⽤于事后调试(后⾯详谈)
Ctrl+Z(SIGTSTP)可以发送停⽌信号,将当前前台进程挂起到后台等。

2.1.2 理解OS如何得知键盘有数据

2.1.3 初步理解信号起源

📌 注意:
信号其实是从纯软件⻆度,模拟硬件中断的⾏为
只不过硬件中断是发给CPU,⽽信号是发给进程
两者有相似性,但是层级不同,这点我们后⾯的感觉会更加明显

2.2使⽤函数产⽣信号

2.2.1kill

kill 命令是调⽤ kill 函数实现的。 kill 函数可以给⼀个指定的进程发送指定的信号。

2.2.2raise

raise 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)。

2.2.3abort

abort 函数使当前进程接收到信号⽽异常终⽌。

2.3 由软件条件产⽣信号

SIGPIPE 是⼀种由软件条件产⽣的信号,在"管道"中已经介绍过了。本节主要介绍 alarm 函数
和 SIGALRM 信号。
调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发
SIGALRM 信号,该信号的默认处理动作是终⽌当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个⽐⽅,某⼈要⼩睡⼀觉,设定闹
钟为30分钟之后响,20分钟后被⼈吵醒了,还想多睡⼀会⼉,于是重新设定闹钟为15分钟之后响,"以
前设定的闹钟时间还余下的时间"就是10分钟。如果seconds值为0,表⽰取消以前设定的闹钟,函数
的返回值仍然是以前设定的闹钟时间还余下的秒数。
如何理解软件条件
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进⾏相应的处理。简⽽⾔之,软件条件是因操作系统内部或外部软件操作⽽触发的信号产⽣。

如何简单快速理解系统闹钟
系统闹钟,其实本质是OS必须⾃⾝具有定时功能,并能让⽤⼾设置这种定时功能,才可能实现闹钟这样的技术。
现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。
我们不在这部分进⾏深究,为了理解它,我们可以看到:定时器超时时间expires和处理⽅法
function。
操作系统管理定时器,采⽤的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为"堆结构"。

2.4 硬件异常产⽣信号

硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前
进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层⾯上,是被当成信号处理的。

注意:
通过上⾯的实验,我们可能发现:
发现⼀直有8号信号产⽣被我们捕获,这是为什么呢?上⾯我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS 会检查应⽤程序的异常情况,其实在CPU中有⼀些控制和状态寄存器,主要⽤于控制处理器的操作,通常由操作系统代码使⽤。状态寄存器可以简单理解为⼀个位图,对应着⼀些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调⽤对应的异常处理⽅法。
除零异常后,我们并没有清理内存,关闭进程打开的⽂件,切换进程等操作,所以CPU中还
保留上下⽂数据以及寄存器内容,除零异常会⼀直存在,就有了我们看到的⼀直发出异常信
号的现象。访问⾮法内存其实也是如此,⼤家可以⾃⾏实验。
⼦进程退出core dump

Core Dump
SIGINT的默认处理动作是终⽌进程,SIGQUIT的默认处理动作是终⽌进程并且Core Dump,现在我们
来验证⼀下。
⾸先解释什么是Core Dump。当⼀个进程要异常终⽌时,可以选择把进程的⽤⼾空间内存数据全部
保存到磁盘上,⽂件名通常是core,这叫做Core Dump。
进程异常终⽌通常是因为有Bug,⽐如⾮法内存访问导致段错误,事后可以⽤调试器检查core⽂件以
查清错误原因,这叫做 Post-mortem Debug (事后调试)。
⼀个进程允许 产⽣多⼤的 core ⽂件取决于进程的 Resource Limit (这个信息保存 在PCB
中)。默认是不允许产⽣ core ⽂件的, 因为 core ⽂件中可能包含⽤⼾密码等敏感信息,不安全。
在开发调试阶段可以⽤ ulimit 命令改变这个限制,允许产⽣ core ⽂件。 ⾸先⽤ ulimit 命令
改变 Shell 进程的 Resource Limit ,如允许 core ⽂件最⼤为 1024K: $ ulimit -c
1024
上⾯所说的所有信号产⽣,最终都要有OS来进⾏执⾏,为什么?OS是进程的管理者
信号的处理是否是⽴即处理的?在合适的时候
信号如果不是被⽴即处理,那么信号是否需要暂时被进程记录下来?记录在哪⾥最合适呢?
⼀个进程在没有收到信号的时候,能否能知道,⾃⼰应该对合法信号作何处理呢?
如何理解OS向进程发送信号?能否描述⼀下完整的发送处理过程?

3.保存信号

3.1信号其他相关常⻅概念

实际执⾏信号的处理动作称为信号递达(Delivery)
信号从产⽣到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动
作。

3.2在内核中的表示

每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动
作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤⼾⾃定义函数
sighandler。
如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?POSIX.1允许系统递送该信号⼀次或多次。Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。本章不讨论实时信号。

3.3sigset_t

从上图来看,每个信号只有⼀个bit的未决标志, ⾮0即1, 不记录该信号产⽣了多少次,阻塞标志也是这样表⽰的。因此, 未决和阻塞标志可以⽤相同的数据类型sigset_t来存储, , 这个类型
可以表⽰每个信号的"有效"或"⽆效"状态, 在阻塞信号集中"有效"和"⽆效"的含义是该信号
是否被阻塞, ⽽在未决信号集中"有 效"和"⽆效"的含义是该信号是否处于未决状态。下⼀篇将详
细介绍信号集的各种操作。阻塞信号集也叫做当前进程的 信号屏蔽字(Signal Mask), 这⾥的"屏
蔽"应该理解为阻塞⽽不是忽略。

3.4信号集操作函数

sigset_t类型对于每种信号⽤⼀个bit表⽰"有效"或"⽆效"状态, ⾄于这个类型内部如何存储这些
bit则依赖于系统实现, 从使⽤者的⻆度是不必关⼼的, 使⽤者只能调⽤以下函数来操作sigset_ t变量, ⽽不应该对它的内部数据做任何解释, ⽐如⽤printf直接打印sigset_t变量是没有意义的。

include <signal.h>

int sigemptyset ( sigset_t * set );
int sigfillset ( sigset_t * set );
int sigaddset ( sigset_t * set , int signo);
int sigdelset ( sigset_t * set , int signo);
int sigismember ( const sigset_t * set , int signo);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰ 该信号集的有效信号包括系统⽀持的所有信号。
注意,在使⽤sigset_ t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
调⽤函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。

include <signal.h>

int sigprocmask ( int how, const sigset_t * set , sigset_t *oset);
如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是⾮空指针,则 更改
进程的信 号屏蔽字,参数how指⽰如何更改。如果oset和set都是⾮空指针,则先将原来的信号 屏蔽字备份到oset⾥,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

如果调⽤sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀
个信号递达。

include <signal.h>

int sigpending ( sigset_t * set );
读取当前进程的未决信号集 , 通过 set 参数传出。
调⽤成功则返回 0 , 出错则返回 -1

4.捕捉信号

4.1 信号捕捉的流程


如果信号的处理动作是⽤⼾⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。
由于信号处理函数的代码是在⽤⼾空间的,处理过程⽐较复杂,举例如下:
⽤⼾程序注册了 SIGQUIT 信号的处理函数 sighandler 。
当前正在执⾏ main 函数,这时发⽣中断或异常切换到内核态。
在中断处理完毕后要返回⽤⼾态的 main 函数之前检查到有信号 SIGQUIT 递达。
内核决定返回⽤⼾态后不是恢复 main 函数的上下⽂继续执⾏,⽽是执⾏ sighandler 函
数, sighandler 和 main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个
独⽴的控制流程。
sighandler 函数返回后⾃动执⾏特殊的系统调⽤ sigreturn 再次进⼊内核态。
如果没有新的信号要递达,这次再返回⽤⼾态就是恢复 main 函数的上下⽂继续执⾏了。

4.2sigaction

include <signal.h>

int sigaction ( int signo, const struct sigaction *act, struct sigaction
*oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调⽤成功则返回0,出错则返回- 1。
signo是指定信号的编号。若act指针⾮空,则根据act修改该信号的处理动作。若oact指针⾮空, 则
通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表⽰忽略信号,赋值为常数SIG_DFL表⽰执⾏系统
默认动作,赋值为⼀个函数指针表⽰⽤⾃定义函数捕捉信号,或者说向内核注册了⼀个信号处理函
数,该函数返回值为void,可以带⼀个int参数,通过参数可以得知当前信号的编号,这样就可以⽤同⼀
个函数处理多种信号。显然,这也是⼀个回调函数,不是被main函数调⽤,⽽是被系统所调⽤。

4.3 穿插话题 - 操作系统是怎么运⾏的

4.3.1硬件中断

中断向量表就是操作系统的⼀部分,启动就加载到内存中了
通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
由外部设备触发的,中断系统运⾏流程,叫做硬件中断

4.3.2时钟中断

进程可以在操作系统的指挥下,被调度,被执⾏,那么操作系统⾃⼰被谁指挥,被谁推动执⾏呢
外部设备可以触发硬件中断,但是这个是需要⽤⼾或者设备⾃⼰触发,有没有⾃⼰可以定期触发的
设备?


这样,操作系统不就在硬件的推动下,⾃动调度了么!!!

4.3.3死循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统⾃⼰不做任何事情,需要什么功能,就向中断向量表⾥⾯添加⽅法即可.操作系统的本质:就是⼀个死循环!

这样,操作系统,就可以在硬件时钟的推动下,⾃动调度了.
所以,什么是时间⽚?CPU为什么会有主频?为什么主频越快,CPU越快?主频可以作为OS调度执⾏速度的参考之⼀

4-3-4 软中断

上述外部硬件中断,需要硬件设备触发。
有没有可能,因为软件原因,也触发上⾯的逻辑?有!
为了让操作系统⽀持进⾏系统调⽤,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内
部触发中断逻辑。
所以:


⽤⼾层怎么把系统调⽤号给操作系统? - 寄存器(⽐如EAX)
操作系统怎么把返回值给⽤⼾?- 寄存器或者⽤⼾传⼊的缓冲区地址
系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执
⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法
系统调⽤号的本质:数组下标!
可是为什么我们⽤的系统调⽤,从来没有⻅过什么 int 0x80 或者 syscall 呢?都是直接调⽤
上层的函数的啊?
那是因为Linux的gnu C标准库,给我们把⼏乎所有的系统调⽤全部封装了。
#define SYS_ify(syscall_name) _NR##syscall_name :是⼀个宏定义,⽤于将系
统调⽤的名称转换为对应的系统调⽤号。⽐如: SYS_ify(open) 会被展开为 __NR_open
⽽系统调⽤号,不是 glibc 提供的,是内核提供的,内核提供系统调⽤⼊⼝函数 man 2
syscall ,或者直接提供汇编级别软中断命令 int or syscall ,并提供对应的头⽂件或者开
发⼊⼝,让上层语⾔的设计者使⽤系统调⽤号,完成系统调⽤过程
缺⻚中断?内存碎⽚处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,
然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来
处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。

📌 所以:
操作系统就是躺在中断处理例程上的代码块!
CPU内部的软中断,⽐如int 0x80或者syscall,我们叫做陷阱
CPU内部的软中断,⽐如除零/野指针等,我们叫做异常。(所以,能理解"缺⻚异
常"为什么这么叫了吗?)

结论:
操作系统⽆论怎么切换进程,都能找到同⼀个操作系统!换句话说操作系统系统调⽤⽅法的执⾏,是在进程的地址空间中执⾏的!
关于特权级别,涉及到段,段描述符,段选择⼦,DPL,CPL,RPL等概念, ⽽现在芯⽚为了保证兼容性,已经⾮常复杂了,进⽽导致OS也必须得照顾它的复杂性,这块我们不做深究了。
⽤⼾态就是执⾏⽤⼾[0,3]GB时所处的状态
内核态就是执⾏内核[3,4]GB时所处的状态
区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
⼀般执⾏ int 0x80 或者 syscall 软中断,CPL会在校验之后⾃动变更
这样会不会不安全??

5.可重入函数

main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的时候,因为硬件中断使进程切换到内核,再次回⽤⼾态之前检查到有信号待处理,于是切换 到
sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的
两步都做完之后从sighandler返回内核态,再次回到⽤⼾态就从main函数调⽤的insert函数中继续往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后 向链表中插⼊两个节点,⽽最后只有⼀个节点真正插⼊链表中了。
像上例这样,insert函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函数,这称为重⼊,insert函数访问⼀个全局链表,有可能因为重⼊⽽造成错乱,像这样的函数称为 不可重⼊函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数,则称为可重⼊(Reentrant) 函数。想⼀下,为什么两个不同的控制流程调⽤同⼀个函数,访问它的同⼀个局部变量或参数就不会造成错乱?
如果⼀个函数符合以下条件之⼀则是不可重⼊的:
调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构

6.volatile

该关键字在C当中我们已经有所涉猎,今天我们站在信号的⻆度重新理解⼀下
volatile 作⽤:保持内存的可⻅性,告知编译器,被该关键字修饰的变量,不允许被优化,对该
变量的任何操作,都必须在真实的内存中进⾏操作


结束语

信号我认为涉及到的知识量比较多,这一块算是Linux上的难点