一、信号介绍
kill -l 查看信号
1~31 普通信号,34~64 实时信号。
信号:Linux系统提供的一种向指定进程发送特定时间的方式,作识别处理。
信号的产生是异步的:进程间相互独立,不等待其他进程地执行。
信号的生命周期:信号的产生 -> 信号的保存 -> 信号的处理
二、信号处理
信号处理分类:默认处理,忽略处理,自定义处理(信号捕捉)
Action列记录信号处理默认操作。
1、默认处理
通常是停止,终止进程(Core, Term, Stop...),忽略。
2、自定义处理
捕捉信号进行处理 函数 signal()
signum:信号编号。
handler:处理信号的函数,返回值void, 参数信号编号。
3、举例
上述代码当OS发送2号信号给进程时,进程捕捉信号,不执行默认的2号信号操作(其实时 ctrl c 终止进程)而是打印 get a sig : 2
4、信号的发送
本质就是在进程的pcb中的信号位图 (uint32_t singals) 的指定信号编号位置的0变成1(1~31位)
由于修改的是内核数据结构,所以操作是由OS做的。
三、信号的五种产生方式
1、命令
kill -信号编号 pid 对指定进程发送信号
2、键盘输入
ctrl c(信号2 SIGINT) ctrl \(信号3 SIGQUT)
3、系统调用
int kill(pid_t pid, int sig) 给任意进程发送信号
对系统调用的封装 int raise(int sig) 对当前进程发送信号(不常用)
语言级 void abort(void) 终止进程,给进程发送6号信号 SIGABRT
比较特殊,即使被捕捉,依然会终止进程,不像2,3号信号
4、软件条件
例如当管道文件读端关闭,写端一直在写入,OS就会给进程发送 SIGPIPE 信号终止进程。
闹钟函数会在进程开始后 second 秒发送闹钟信号。由于闹钟存在很多,就要先描述再组织,用小根堆维护,超时就发送信号 pop()掉。
返回值是上一个闹钟的剩余时间。
alarm(0)表示取消闹钟。
闹钟在一个进程中默认只触发一次,但是可以捕捉闹钟信号之后再设一个。
5、异常
当进程做了非法访问操作时,OS会给进程发送信号,默认终止进程,释放进程在CPU中的上下文数据。
可以捕获信号不终止进程,但是这样就一直报错。
问题
(1)CPU怎么知道异常?
以 10 / 0 为例,CPU中存在 eflag ,当10 / 0 使结果溢出之后 eflag 中的溢出标记位变成1,CPU发现异常。
(2)为什么推荐终止程序?
若不终止进程就会一直调度进程,每调度一次就会报错。
6、细节
对比core, term
term:单纯异常终止进程。
core:异常终止进程,返回一个 debug 文件,可以调式发现错误,默认云服务器不会产生(防止错误文件打满磁盘)叫做核心转储,记录异常数据。
在前文中我们提到进程等待函数 waitpid(),里面的输出型参数 status 前八位是退出码,后七位是退出信号,中间一位就是 core dump 标志,当标志为1时就表示进程异常退出,core文件写入。
调试方法:gdb 错误文件 core-file core
四、信号保存
1、信号递达 Delivery
执行信号处理动作。
2、信号未决 Pending
信号从产生到递达的状态。
3、信号阻塞
进程可以阻塞信号,对应的信号就永远不会递达,一直未决,直到解除阻塞。
一个进程的阻塞与他是否未决无关。
在Linux中 sigset_t 是一个位图类型叫信号集,可以存储block, Pending位图
#include<signal.h>
位图清0 int sigempty(sigset_t* set)
位图全置1 int sigfillset(sigset_t* set)
把信号设置进位图(在对应信号编号处置1)int sigaddset(sigset_t* set, int signo)
把信号删除(在对应信号编号处置0) int sigdelet(sigset_t* set, int signo)
判断信号是否在位图 int sigismember(sigset_t* set, int signo)
获取信号屏蔽字
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset)
how:
SIG_BLOCK : 添加set中的屏蔽字,mask = mask | set
SIG_UBLOCK : 取消set中的屏蔽字,mask = mask &(~set)
SIG_SETMASK : 设置set内容,mask = set
set:输入型参数,传入指定信号屏蔽字的位图。
oldset:输出型参数,输出旧的信号屏蔽字。
获取Pending
int sigpending(sigset_t* set)
set:输出型参数,返回pending位图。
4、被阻塞的信号产生时将保持在未决状态,知道进程解除阻塞才能递达。
5、只要信号被阻塞就不会递达,忽略是递达的一种信号处理。
五、信号处理
其实signal函数中的handler参数不仅仅可以传自定义方法,SIG_IGN是忽略处理,SIG_DFL是默认处理。
上图可抽象成下图
六、重新理解虚拟地址空间
内核级页表只要维护一份,所以进程如何切换都能找到OS
七、键盘输入数据过程
所以信号就是模拟硬件中断来发送信号,同样是异步处理。
八、操作系统如何运行?
本质是死循环 + 时钟中断不停调度系统任务。
九、如何执行系统调用?
系统中维护了一批系统调用的函数指针数组,找到指定的系统调用号就能找到函数指针数组下标,找到函数,以fork()函数为例
pid_t fork()
{
mov 2 eax; // 把系统调用号放入eax寄存器
int 0x80 // 外部直接让CPU执行系统调用(陷阱或缺陷)
...
}
操作系统不相信任何人,用户无法直接跳到内核空间执行系统调用,只有CPU执行代码时从用户态(3)转到内核态(0)才能跳转。
十、信号的捕捉
与signal()函数类似的,int sigaction(int signum, const struct sigaction* act, struct sigaction* oact);
可以用于信号捕捉。
在sigaction结构体中,sa_handler是捕捉后的处理方法,sa_mask是在处理signum信号时,要屏蔽的信号集。
act:输入型参数,捕捉信号的处理方式。
oldact:输出型参数,旧的信号处理方式。
如果正在对信号进行处理,默认该信号被屏蔽,只有处理完之后自动解除屏蔽。
十一、补充知识
1、可重入函数
描述的是一个函数的特点。
一个执行流调用函数时,另一个执行流也调用该函数,称这个函数被重复进入。如果导致了问题,该函数就是不允许被重复进入,就是不可重入函数,这种函数占据绝大部分。
所以可以重复进入的函数就是可重入函数。
不可重入的条件:
调用了内存管理相关函数
调用了标准 I/O
库函数,因为其中很多实现都以不可重入的方式使用数据结构
2、volatile关键字
避免编译器优化,保证内存可见性。
在定义一个全局变量后,作为计算器的CPU会从内存中读入数据放进寄存器,而每一次从内存中读入数据的操作会因为编译器的优化而取消,这是如果内存中修改了全局变量,CPU寄存器中就不会改变,导致代码执行结果不在预期,此时加上volatile就可以避免问题。
3、SIGCHLD信号
子进程退出会给父进程发送SIGCHLD信号。
重谈进程等待
1、如果不关心子进程的退出情况,父进程可以 signal(SIGCHLD, SIG_IGN)收到子进程的退出信号后忽略信号,避免僵尸进程。
2、如果关心那就要等待,既然已经知道子进程退出会有信号,我们就可以不用在父进程的主程序中等待,可以在父进程收到信号后在处理函数中死循环等待子进程即可。