Linux操作系统之信号:保存与处理信号
目录
前言:
前文回顾与补充:
信号的保存
PCB里的信号保存
sigset_t
信号集操作函数
信号的处理
信号捕捉的流程:编辑
操作系统的运行原理
硬件中断
时钟中断
死循环
软中断
总结:
前言:
在上一篇文章,我已经为大家详细的阐述了信号产生的物种方式,相信大家对于信号已经有了较为深刻的印象与理解了。
那么今天我们就来继续谈论信号的另外两个重要话题:信号的保存与捕捉。
前文回顾与补充:
我们之前说, 捕捉信号一共有三种方式,分别是:默认,忽略,与自定义。
在使用signal时,有两个宏分别代表默认与忽略的处理:
#include <iostream>
#include <unistd.h>
#include <signal.h>int main()
{::signal(2,SIG_IGN);//ignore 忽略:本身就是一种处理方法,什么也不用做直接忽略掉::signal(2,SIG_DFL);//default:执行该信号默认的处理方法return 0;
}
我们之前说的所有信号的产生,最后都要由OS执行,这是为什么呢?
:因为OS是进程的管理者
信号的处理是否会被立即处理?
:不会,会在合适的时候进行处理
一个信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适?
:需要,会被记录在进程的PCB中
我们先补充一点信号的其他知识:
实际执行信号的处理动作被称为信号递达(Delivery)
信号从产生到递达之间的状态,被称为信号未决(Pending)
进程可以选择阻塞(block)某个信号
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才会执行递达的动作。
这里要注意,阻塞与忽略是不同的,只要信号被阻塞就不会递达,而忽略时在递达之后我们选择的一种处理信号的动作
信号的保存
PCB里的信号保存
我们刚刚说,由于信号的处理不是立即处理,所以我们需要保存好进程,就保存在进程的PCB中,我们先来看一下具体保存信号的结构:
这个pending就是我们之前说的信号位图,保存这个进程是否收到了相应的信号。
这个blcok也是一个位图,保存的是对应位置的信号是否被我们阻塞/屏蔽了。
而这个handler,则是一个函数指针数组,里面存储的就是对应信号的默认处理方法。我们信号的编号-1,就是对应的数组下标。
这也就是为什么我们只需要signal一次,就能永久改变处理方法的原因:因为我们是直接把方法拷贝替换成了自己的处理函数。
sigset_t
信号集操作函数
由于保管信号的是位图结构,我们不好每次都进行位操作来修改位图,所以有对应的系统调用:

如果我们想要恢复成老的数据,该怎么办?
所以有输出型参数oldset供我们使用。

#include <iostream>
#include <string>
#include <functional>
#include <vector>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>int main()
{//初始化,把指定的位图全部清0sigset_t block,oblock;sigemptyset(&block);sigemptyset(&oblock);//设置信号,此时我们有没有把对2号信号的屏蔽,设置进入内核中?:只是在用户栈上设置了block的位图结构// 并没有设置进入内核中!sigaddset(&block,2);//把我们对2号信号的屏蔽,设置进入内核中sigprocmask(SIG_SETMASK,&block,&oblock);while(true){//获取该进程的pending表并打印sigset_t pending;sigpending(&pending);std::cout<<getpid()<<":";for(int i=31;i>=0;i--){if(sigismember(&pending,i))//挨个挨个检查是否存在在该位图里{std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;sleep(1);}return 0;
}
运行代码,我们打开另外一个bash给该进程发送信号
那我们此时在增加一段代码,使得他在一定时间后自动解除屏蔽试试??
#include <iostream>
#include <string>
#include <functional>
#include <vector>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>void handler(int signo)
{std::cout<<"我已经解除屏蔽"<<signo<<std::endl;
}int main()
{signal(2,handler);//初始化,把指定的位图全部清0sigset_t block,oblock;sigemptyset(&block);sigemptyset(&oblock);//设置信号,此时我们有没有把对2号信号的屏蔽,设置进入内核中?:只是在用户栈上设置了block的位图结构// 并没有设置进入内核中!sigaddset(&block,2);//把我们对2号信号的屏蔽,设置进入内核中sigprocmask(SIG_SETMASK,&block,&oblock);int count=0;while(true){//获取该进程的pending表并打印sigset_t pending;sigpending(&pending);std::cout<<getpid()<<":";for(int i=31;i>0;i--){if(sigismember(&pending,i))//挨个挨个检查是否存在在该位图里{std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;count++;if(count>=10){sigprocmask(SIG_SETMASK, &oblock, nullptr);//解除屏蔽}sleep(1);}return 0;
}
运行代码,可以看见
信号的处理
我们可以先说出结论,之前所说的合适的时候,指的就是,进程在从内核态切换为用户态时,会检测当前进程的pending与blcok,根据这两个位图是决定是否执行处理方法handler。
这里引出了两个新概念,用户态与内核态。
大家不用急,且听我慢慢道来:
信号捕捉的流程:

信号处理函数的代码是在⽤⼾空间的,处理过程还是比较复杂的,我们可以举例如下:
用户程序注册了SIGQUIT 信号的处理函数 sighandler 。
当前正在执行main函数,这个时候会发生中断或者异常切换到内核态(为什么一定会发生中断我后面会解释)
在中断处理完毕后,要返回用户态的main函数前会检查到有信号SIGQUIT 递达。
内核决定返回用户态后不是恢复main函数的上下文继续执行代码,而是执行sighhandler函数(此时sighandler 和 main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个独⽴的控制流程)
操作系统的运行原理
我们这里不得不插一句嘴说一下操作系统的运行原理,只有这样才能让大家理解内核态与用户态更加深入。
这一内容还是很重要的,我们会填上之前说的很多坑。
硬件中断
我们之前在键盘产生信号时一直在说硬件中断,这个到底是怎么一回事呢?
当我们的硬件准备就绪时,就会开始中断,每一个硬件都有自己的一个中断号,并且进程会通过高电压的形式通知CPU,我已经准备就绪了。当我们的CPU知道硬件中断后,会去获取这个硬件对应的中断号。
此时,CPU会保护现场,包括存储此时运行进程代码数据的CPU的数据,保存在PCB中(之前讲页表时我们提到过保护现场这个现象) 。
在这之后,会根据中断号来进行处理的方法,即:
这个中断向量表IDT,本质是还是一个函数指针数组,每一个硬件对应的中断号,就对应了下标。
所以每一个硬件产生中断后,他的处理方法我们一开始就知道了,该如何处理。
这里的中断处理例程,一共有四步:
1、保存现场
2、根据中断号,查中断向量表
3、调用对应的处理方法
4、恢复现场
而这个中断向量表,是操作系统的⼀部分,启动就加载到内存中了。
通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
时钟中断
但是我们还是有一个疑问,进程可以在操作系统的指挥下,被调度,被执⾏,那么操作系统⾃⼰被谁指挥,被谁推动执⾏呢?
外部设备可以触发硬件中断,但是这个是需要⽤⼾或者设备⾃⼰触发,有没有⾃⼰可以定期触发的 设备?
这里就要引出我们时钟中断的概念了
我们规定了一个时间,固定的触发硬件中断,这个中断,被我们称为时钟中断,负责定期帮我们实现中断!!
而这个中断在中断向量表的处理方法只有一个,那就是去调度进程!!
注意,调度进程不代表一定要进行进程切换。
还记得时间片这个概念吗?
其实就是一个整数count。
我们初始规定count=1000;
那么每一次进行调度,就回让count--,当count为0时,就会进行进程的切换。
所以我们还有主频这个概念,就是指 CPU 内部时钟信号的工作频率,通常以 赫兹(Hz) 为单位。你的CPU主频越高,价格越贵,同一个时间进行进程调度越多,性能越好。
死循环
void main(void) /* 这⾥确实是void,并没错。 */{ /* 在startup 程序(head.s)中就是这样假设的。 */.../** 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返* 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任* 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没* 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。*/for (;;)pause();} // end main
软中断

// sys.h
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
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_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};
总结:
由于时间关系。
今天的博客就写到这里,明天我们将会进行最后的结尾,为大家更加具体的说一下什么是内核态与用户态。
相信今天的知识已经把大家之前所学的内容串联起来,大家对操作系统也有了更加深刻的理解!
明天我们将会完成信号部分的内容,并给大家讲一些信号done的相关内容,之后我们将会开始线程的学习!!