我们执行一个进程的时候可以直接使用Ctrl + C来停止其运行,就是今天要说的信号起的作用,我们就来看看信号是怎么执行这个过程的,信号的种类等等
信号是什么
举例
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能"识别快递"
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成"在合适的时候去取"。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你"记住了有一个快递要去取"
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1.执行默认动作 (幸福的打开快递,使用商品)2.执行自定义动作 (快递是零食,你要送给你你的女朋友)3.忽略快递 (快递拿上来之后,扔掉床头,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
初步结论
你怎么能识别信号呢?识别信号是内置的 ,进程识别信号,是内核程序员写的内置特性。
信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。
处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。
信号到来 信号保存 信号处理
怎么进行信号处理啊?a.默认b.忽略c.自定义,后续都叫做信号捕捉。
系统函数
cpp
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum是信号的编号,signal能对这个信号编号的信号执行方法重写。这就叫自定义信号的处理。
返回函数是返回旧的信号处理函数
这里我告诉大家Ctrl + C是2号信号,我们给二号信号注册新的函数试试:
cpp
#include<iostream>
#include<signal.h>
using namespace std;
void handle(int id){
cout<<"我是"<<id<<"号信号"<<endl;
}
int main(){
signal(2,handle);
while(true){
sleep(1);
}
return 9;
}

我们发现执行ctrl + C已经杀不死进程了,因为进程的2号信号已经不是原来的默认杀死进程的方式了。但是我们可以使用ctrl \来杀死进程
信号概念
信号是进程之间事件异步通知的⼀种方式,属于软中断。
信号查看
使用kill -l查看所有信号
这些信号也可在signal.h里面找到宏定义
信号的产生

raise
可以通过raise函数主动发送信号给自己
cpp
#include <signal.h>
int raise(int sig);
abort
会给自己进程发送一个信号 6 ,即使重写了处理方式,但是处理后依然会让进程退出
cpp
#include<iostream>
#include<signal.h>
using namespace std;
void handle(int id){
cout<<"我是"<<id<<"号信号"<<endl;
}
int main(){
signal(2,handle);
signal(SIGABRT,handle);
while(true){
sleep(1);
raise(2);
}
return 9;
}

alarm
cpp
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
注册一个计时器,只能在秒级,计时器到点了会发送一个SIG_ALARM的信号
cpp
#include<iostream>
#include<signal.h>
using namespace std;
void handle(int id){
cout<<"我是"<<id<<"号信号"<<endl;
}
void alarmHandle(int id){
cout<<"计时过了,进程结束"<<endl;
abort();
}
int main(){
signal(2,handle);
signal(SIGABRT,handle);
signal(SIGALRM,alarmHandle);
alarm(4);
while(true){
sleep(1);
raise(2);
}
return 9;
}

alarm(0)是取消之前所有的定时器
硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以O的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
cpp
#include<iostream>
#include<signal.h>
using namespace std;
void handle(int id){
cout<<"我是"<<id<<"号信号"<<endl;
}
void alarmHandle(int id){
cout<<"计时过了,进程结束"<<endl;
abort();
}
int main(){
for(int x=1;x<=31;++x)signal(x,handle);
while(true){
sleep(4);
int* p =nullptr;
*p =100;
}
return 9;
}
例如我们吧所有信号重写了,看看最后会给进程发送什么信号:
发现是发送的11号信号,但是进程要怎么停下来呢?就只能通过查找进程pid,用9号信号来杀死进程了,只有9号信号SIGKILL和19号信号SIGSTOP不能被重写,防止进程无法停止和杀死。
因此我们知道了,硬件的一些异常现象也会作为信号发送给进程。
子进程退出coredump
我们再进程控制模块讲述了waitpid可以获取子进程退出的status,可以通过WIFEXITED WEXITSTATUS等操作获取退出的相关信息,其中WIFSIGNALED判断是否信号终止和WTERMSIG获取信号编号也就知道是什么了。

另外还有一个WCOREDUMP又是什么呢?它是判断是否生成了coredump文件
除了ctrl c的SIGINT SIGKILL 9号信号以及SIGSTOP19号信号(因为这个是人为结束的,因此不需要给出coredump,而9号信号和19号信号可以用户调用,可以系统调用,而系统调用时是比较紧急的情况,因此不能此时还留时间让进程生成coredump文件),其他让进程终止的信号会生成一个终止的core文件,这个文件会保存寄存器上下文,堆栈等信息,使用gdb查看coredump文件时就能精准定位哪里除了错误,从而快速排查并修复。
coredump文件默认是不会生成的,只有使用使用ulimit主动打开,因为coredump生成的文件是有隐私性的,有一些隐私数据。
我们需要调用ulimit -c (大小单位是KB) ulimit -c 1024(KB)
ulimit -c unlimited可以设置无限大小

信号的保存

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

因此进程中有阻塞表和未决表,阻塞表是处理某个信号是否要被阻塞暂时不处理,只要用户解除阻塞,这个信号就会在合适的时机立马执行;而未决就是将信号记录,等待处理,在未决期间如果有相同的信号再次进入,那么未决表并不会计数增加,依然会当成是一个信号进行后续递达处理。
sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1 ,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t 来存储,sigset_t称为信号集 ,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。
信号集的操作函数
我们不必关系sigset具体是什么,只要能操作它就行
cpp
#include <signal.h>
int sigemptyset(sigset_t *set);//所有位 置0
int sigfillset(sigset_t *set);//所有位 置1
int sigaddset(sigset_t *set, int signo);//某位设置为1
int sigdelset(sigset_t *set, int signo);//某位设置为0
int sigismember(const sigset_t *set, int signo);//判断是否在信号集里面
使用前需要使用sigemptyset/sigfillset来初始化
sigprocmask
这个函数就是用来设置阻塞信号集的
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
set是传需要更改的信号集,oset是可以接受当前更改前的信号集,两个都可以为空,但是同时为空无意义。set空表示只想知道当前信号集,oset为空表示不想知道旧信号集并且设置新的信号集,两个都不为空就是接受旧的然后放置新的。how就是放置新的信号集的操作:

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
第一个和第二个how用于只增加减少几个信号的情况,而第三更适用于设置整体的信号集。
sigpending
cpp
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
sigaction
cpp
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction
*oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回o,出错则返回-1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
cpp
struct sigaction {
// 方式1:信号处理函数(最常用)
void (*sa_handler)(int);
// 方式2:更灵活的处理函数(可获取信号详细信息)
void (*sa_sigaction)(int, siginfo_t *, void *);
// 信号掩码:处理该信号时,临时屏蔽哪些其他信号
sigset_t sa_mask;
// 标志位:控制信号处理的行为(如SA_SIGINFO、SA_RESTART)
int sa_flags;
// 废弃字段,无需关注
void (*sa_restorer)(void);
};
sa_hander/sa_sigaction可以传自定义的函数,可以传SIG_IGN(默认处理) SIG_DFL(忽略)

sa_mask是sigset_t类型因此可以使用一下函数来操作,这个屏蔽是短暂的,处理完信号就失效了 
sa_flags可以按位或上面的三个操作符,按需要处理
捕捉信号

信号捕捉的具体流程:

整个流程是,当在用户态执行某一行代码时(操作系统可能在处理其他中断,或者等待),此时调用了系统调用或者中断(中断一般是操作系统可以处理的,除非严重的问题。它是告诉操作系统有新的中断,操作系统会按优先级来处理哪个)了或者异常(一般是错误问题,也是一种中断)了,需要陷入内核来处理这些情况,因此保存寄存器上下文,然后进入内核,操作系统会先处理异常和中断以及系统调用,如果处理这个中断和异常时发现是错误无法恢复当前进程,那么就会设置对应的未决信号到进程的pending表,然后等这些工作忙完,就会去检查当前信号的pending表,如果有就会去执行对应信号,如果信号是默认的或者忽略的,这个只用在内核进行执行,如果是用户自己注册的信号函数,那么就要恢复到用户态低权限级来处理代码,等执行完再做收尾工作,比如继续执行未递达的信号,或者处理完了就恢复进程上下文,返回用户态去执行代码。

操作系统运行原理
硬件中断

中断向量表就是操作系统的一部分,启动就加载到内存中了
通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
由外部设备触发的,中断系统运行流程,叫做硬件中断
时钟中断
操作系统自己又是被谁指挥执行推动的呢?

通过时钟中断,定期让操作系统陷入内核执行相关的处理和信号递达
死循环
操作系统本质就是一个死循环,操作系统无限执行着这个死循环,通过中断执行相应的处理
软中断
操作系统提供汇编指令(int 或者 syscall)实现软中断,它为系统调用提供了支持
系统调用底层
系统调用也是一个封装,它会先使用汇编指令(int 或者 syscall),并传一个系统调用号(通过寄存器来传递给操作系统),陷入内核后,操作系统就会根据系统调用号和注册的系统调用表来找到对应函数并执行。返回值依然是用寄存器拷回
cpp
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208)
extern int sys_read (); // 读⽂件。 (fs/read_write.c, 55)
extern int sys_write (); // 写⽂件。 (fs/read_write.c, 83)
extern int sys_open (); // 打开⽂件。 (fs/open.c, 138)
extern int sys_close (); // 关闭⽂件。 (fs/open.c, 192)
extern int sys_waitpid (); // 等待进程终⽌。 (kernel/exit.c, 142)
extern int sys_creat (); // 创建⽂件。 (fs/open.c, 187)
extern int sys_link (); // 创建⼀个⽂件的硬连接。 (fs/namei.c, 721)
extern int sys_unlink (); // 删除⼀个⽂件名(或删除⽂件)。 (fs/namei.c, 663)
extern int sys_execve (); // 执⾏程序。 (kernel/system_call.s, 200)
extern int sys_chdir (); // 更改当前⽬录。 (fs/open.c, 75)
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102)
extern int sys_mknod (); // 建⽴块/字符特殊⽂件。 (fs/namei.c, 412)
extern int sys_chmod (); // 修改⽂件属性。 (fs/open.c, 105)
extern int sys_chown (); // 修改⽂件宿主和所属组。 (fs/open.c, 121)
extern int sys_break (); // (-kernel/sys.c, 21)
extern int sys_stat (); // 使⽤路径名取⽂件的状态信息。 (fs/stat.c, 36)
extern int sys_lseek (); // 重新定位读/写⽂件偏移。 (fs/read_write.c, 25)
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348)
extern int sys_mount (); // 安装⽂件系统。 (fs/super.c, 200)
extern int sys_umount (); // 卸载⽂件系统。 (fs/super.c, 167)
extern int sys_setuid (); // 设置进程⽤⼾id。 (kernel/sys.c, 143)
extern int sys_getuid (); // 取进程⽤⼾id。 (kernel/sched.c, 358)
extern int sys_stime (); // 设置系统时间⽇期。 (-kernel/sys.c, 148)
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26)
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338)
extern int sys_fstat (); // 使⽤⽂件句柄取⽂件的状态信息。(fs/stat.c, 47)
extern int sys_pause (); // 暂停进程运⾏。 (kernel/sched.c, 144)
extern int sys_utime (); // 改变⽂件的访问和修改时间。 (fs/open.c, 24)
extern int sys_stty (); // 修改终端⾏设置。 (-kernel/sys.c, 31)
extern int sys_gtty (); // 取终端⾏设置信息。 (-kernel/sys.c, 36)
extern int sys_access (); // 检查⽤⼾对⼀个⽂件的访问权限。(fs/open.c, 47)
extern int sys_nice (); // 设置进程执⾏优先权。 (kernel/sched.c, 378)
extern int sys_ftime (); // 取⽇期和时间。 (-kernel/sys.c,16)
extern int sys_sync (); // 同步⾼速缓冲与设备中数据。 (fs/buffer.c, 44)
extern int sys_kill (); // 终⽌⼀个进程。 (kernel/exit.c, 60)
extern int sys_rename (); // 更改⽂件名。 (-kernel/sys.c, 41)
extern int sys_mkdir (); // 创建⽬录。 (fs/namei.c, 463)
extern int sys_rmdir (); // 删除⽬录。 (fs/namei.c, 587)
extern int sys_dup (); // 复制⽂件句柄。 (fs/fcntl.c, 42)
extern int sys_pipe (); // 创建管道。 (fs/pipe.c, 71)
extern int sys_times (); // 取运⾏时间。 (kernel/sys.c, 156)
extern int sys_prof (); // 程序执⾏时间区域。 (-kernel/sys.c, 46)
extern int sys_brk (); // 修改数据段⻓度。 (kernel/sys.c, 168)
extern int sys_setgid (); // 设置进程组id。 (kernel/sys.c, 72)
extern int sys_getgid (); // 取进程组id。 (kernel/sched.c, 368)
extern int sys_signal (); // 信号处理。 (kernel/signal.c, 48)
extern int sys_geteuid (); // 取进程有效⽤⼾id。 (kenrl/sched.c, 363)
extern int sys_getegid (); // 取进程有效组id。 (kenrl/sched.c, 373)
extern int sys_acct (); // 进程记帐。 (-kernel/sys.c, 77)
extern int sys_phys (); // (-kernel/sys.c, 82)
extern int sys_lock (); // (-kernel/sys.c, 87)
extern int sys_ioctl (); // 设备控制。 (fs/ioctl.c, 30)
extern int sys_fcntl (); // ⽂件句柄操作。 (fs/fcntl.c, 47)
extern int sys_mpx (); // (-kernel/sys.c, 92)
extern int sys_setpgid (); // 设置进程组id。 (kernel/sys.c, 181)
extern int sys_ulimit (); // (-kernel/sys.c, 97)
extern int sys_uname (); // 显⽰系统信息。 (kernel/sys.c, 216)
extern int sys_umask (); // 取默认⽂件创建属性码。 (kernel/sys.c, 230)
extern int sys_chroot (); // 改变根系统。 (fs/open.c, 90)
extern int sys_ustat (); // 取⽂件系统信息。 (fs/open.c, 19)
extern int sys_dup2 (); // 复制⽂件句柄。 (fs/fcntl.c, 36)
extern int sys_getppid (); // 取⽗进程id。 (kernel/sched.c, 353)
extern int sys_getpgrp (); // 取进程组id,等于getpgid(0)。(kernel/sys.c, 201)
extern int sys_setsid (); // 在新会话中运⾏程序。 (kernel/sys.c, 206)
extern int sys_sigaction (); // 改变信号处理过程。 (kernel/signal.c, 63)
extern int sys_sgetmask (); // 取信号屏蔽码。 (kernel/signal.c, 15)
extern int sys_ssetmask (); // 设置信号屏蔽码。 (kernel/signal.c, 20)
extern int sys_setreuid (); // 设置真实与/或有效⽤⼾id。 (kernel/sys.c,118)
extern int sys_setregid (); // 设置真实与/或有效组id。 (kernel/sys.c, 51)
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
}

Linux的gnu C标准库,给我们把几乎所有的系统调用全部封装了
可重入函数

以一上作为例子:
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函
数,这称为重入 ,inser函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自已的局部变量或参数,则称为可重入(Reentrant)函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准1/0库函数。标准I/0库的很多实现都以不可重入的方式使用全局数据结构。
SIGCHILD
当子进程退出时,会给父进程发送一个SIGCHILD信号,SIGCHILD的SIG_DFL是忽略,但是会产生僵尸进程等待父进程回收;但是SIG_IGN时,也是忽略,但是会交给操作系统自动回收,不产生僵尸进程。这种方式可以不用父进程非阻塞轮询或者阻塞等待。
可以过父进程阻塞waitpid/非阻塞轮询来检查子进程是否真正释放了(阻塞waitpid/非阻塞轮询都会持续检测子进程是否被回收,如果回收了就会返回这个进程的pid)
用户态和内核态
CPU指令集:是CPU实现软件指挥硬件执行的媒介,具体来说每一条汇编语句都对应了一条CPU指令,而非常非常多的CPU指令在一起,可以组成一个、甚至多个集合,指令的集合CPU指令集
CPU指令集有权限分级,大家试想,CPU指令集可以直接操作硬件的,要是因为指令操作的不规范,造成的错误会影响整个计算机系统的。好比你写程序,因为对硬件操作不熟悉,导致操作系统内核、及其他所有正在运行的程序,都可能会因为操作失误而受到不可挽回的错误,最后只能重启计算机才行。
针对上面的需求,硬件设备商直接提供硬件级别的支持,做法就是对CPU指令集设置了权限,不同级别权限能使用的CPU指令集是有限的,以Inter CPU 为例,Inter把CPU指令集操作的权限由高到低划为4级:
ring0:权限最高,可以使用所有CPU指令集
ring 1
ring2
ring3:权限最低,仅能使用常规如CPU指令集,不能使用操作硬件资源的CPU指令集,比IO读写、网卡访问、申请内存都不行
ring0被叫做内核态,完全在操作系统内核中运行
执行内核空间的代码,具有ring0保护级别,有对硬件的所有操作权限,可以执行所有CPU指令集,访问任意地址的内存,在内核模式下的任何异常都是灾难性的,将会导致整台机器停机
ring3被叫做用户态,在应用程序中运行
在用户模式下,具有ring3保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调用系统接(SystemCallAPIs)来达到访问硬件和内存,在这种保护模式下,即时程序发生崩溃也是可以恢复的,在电脑上大部分程序都是在,用户模式下运行的
低权限的资源范围较小,高权限的资源范围更大,所以用户态与内核态的概念就是CPU指令集权限的区别。
在内存资源上的使用,操作系统对用户态与内核态也做了限制,每个进程创建都会分配虚拟空间地址,以Linux32位操作系统为例,它的寻址空间范围是4G(2的32次方),而操作系统会把虚拟控制地址划分为两部分,一部分为内核空间,另一部分为用户空间,高位的1G(从虚拟地址OxC0000000到OxFFFFFFFF)由内核使用,而低位的3G(从虚拟地址0x00000000到OxBFFFFFFF)由各个进程使用。

用户态:只能操作0-3G 范围的低位虚拟空间地址
内核态:0-4G范围的虚拟空间地址都可以操作,尤其是对3-4G范围的高位虚拟空间地址必须由内核态去操作