信号产生
使用终端按键产生信号
Ctrl-c, Ctrl-z, Ctrl-\都是常用的信号,但是他们只能作用于前台进程
一个bash中,只能有一个前台进程,但是可以有若干个后台进程
使用命令
cpp
kill -n pid_t
使用函数
- kill
cpp
int kill(pid_t pid, int sig);
向指定进程发送信号
- raise
cpp
int raise(int sig);
用于向进程自己发送信号
软件条件满足时发送信号
alarm函数
cpp
unsigned int alarm(unsigned int seconds);
一个计时器,当时间结束后,会发送SIGALRM信号终止程序
coredump
- 默认不开启,使用ulimit -c size设置允许的coredump文件大小,当程序出现段错误时,会产生coredump文件,以方便调试
- 自定义信号捕捉
cpp
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
自定义捕捉信号指示定义了如何处理,并不等于处理动作
硬件异常
当硬件运行出现错误时,比如除零错误,会触发SIGFPE信号;访问野指针,会触发SIGSEGV信号
cpp
void handler(int signo)
{
prinrf("received: %d\n", signo);
}
signal(SIGFPE, handler);
int a = 10;
a /= 0;
使用signal自定义信号捕捉之后,发现一直在循环打印自定义捕捉内容,原因是在cpu中存在控制和状态寄存器,可以理解其起到位图的作用,记录产生的异常信号,
当发生异常信号,执行完我们的处理函数之后,并没有发生cpu上下文的切换,寄存器中仍然保存着出错误的信息,所以会一直打印接收到的信号
信号的保存
- 信号状态
- 信号递达,信号实际进行处理的状态
- 信号未决,信号产生到信号递达这中间的状态叫做信号未决
- 信号阻塞和信号忽略,信号阻塞是进程可选的,只有进程先解决信号阻塞状态到信号未决状态,才能有机会进入信号递达状态;信号忽略是在信号递达之后,进程可选的一种信号处理方式
-
信号在内核中的表示
在task_struct中,存放着三个表,分别是handler表、pending表、block表,handler中保存着函数处理方式的函数指针,如果有自定义就指向自定义的处理函数;
pending表是未决信号的记录表,当信号产生时,会从0被置为1;block记录的是是否阻塞这个信号,之后pending表为1并且block相应记录为0时,才能够进入信号递达状态 -
普通信号,对于多次接受的情况,只记录一次;但是对于实时信号,会通过队列的方式多次记录
信号集
- 前面提到的pending表和block表,结构一样,都是用1、0表示一个信号是否是当前自己的有效信号。我们用sigset_t来表示当前信号的有效情况,例如pending表的
sigset_t表示是否处于未决状态,而block表的sigset_t表示是否处于阻塞状态。sigset_t的信息、操作都要通过函数进行,直接操作例如使用printf打印是没有意义的 - 初始化信号集
cpp
int sigemptyset(sigset_t* sig);
int sigfillset(sigset_t* sig);
信号集要先初始化在使用,emptyset将其初始化为空集,而fillset会根据当前进程信号集的有效情况进行初始化
- 增减有效元素
cpp
int sigaddset(sigset_t* sig, int signum);
int sigdelset(sigset_t* sig, int signum);
通过这两个接口去增加后者删除信号集中的有效信号
- 获取未决信号集
通过一个输出型参数带出信息
cpp
int sigpending(sigset_t* sig);
- 获取或者修改当前进程的block或者pending信号集
cpp
int sigprocmask(int how, const sigset_t* set, sigset_t* oset)
当oset不为空时,将当前进程的信号集信息传入到oset中带出;当set不为空时,将当前进程信号集设置为oset的描述;若两者均不为空,则将执行前面两个过程,既拷贝带出,也进行当前进程信号集设置
信号捕捉
- 执行主控制流程的某条指令时,因为异常、中断、系统调用等原因,从用户态进入内核态
- 执行完内核态中的处理流程时,检查进程的pending表,如果有能够处理的信号,操作系统会再次进入用户态。不过这次不是进入主控制流程,而是去执行用户自定义
的处理函数(如果没有就直接进行处理,然后返回主控制流程的上下文,继续往下执行),这里并不是返回了主控制流程,而是另外开辟了新的堆栈,去执行用户的自定义信号处理
处理完成后,返回内核态,随后返回用户态,从主控制流程之前的状态往下运行
中断
硬件中断
当硬件设备就绪时(比如移动鼠标、按动键盘),会向中断控制器发送中断。随后,中断控制器会通知cpu,cpu随后向中断器发送信号,获取中断号。随后cpu会将当前进程在寄存器中的上下文
拷贝到进程在内存的相应空间之中,这一过程称作cpu保护现场。之后,操作系统会根据拿到的中断号去查阅中断向量表,中断向量表中记录着对于各种硬件、软中断等等的中断服务
所需进行的处理。完成相应处理函数之后,操作系统返回用户态,恢复cpu现场,继续往下执行被中断的流程
时钟中断
上面理解了操作系统如何既进行用户代码的处理也处理硬件中断,那进程调度呢?现代cpu中继承了一个时钟源,其每个一段时间就会向cpu中发送时钟中断。cpu获得
时钟中断之后,就会到中断向量表中去执行时钟中断进程调度的处理,从而完成进程的切换。
这里明白了,其实所谓进程的时间片,就是一个计时器
软中断
为了让操作系统支持系统调用,cpu设计了syscall和int 0x80这两个汇编指令,当这两条指令被执行时,cpu会触发软中断,去根据中断号查阅系统调用表,完成相应的处理
缺页中断、内存碎片处理、除零野指针错误,这些都会通过软中断,通知操作系统进行处理
陷阱和异常
cpu中的软中断,比如syscall和int 0x80,这些被称作陷阱;除零错误、野指针,这些被称作异常
内核态和用户态
在进程的地址空间当中[0,3]GB是用户的代码(代码段、数据段、堆、栈、共享区等),[3, 4]GB是内核的代码。运行用户的代码时,就是用户态;运行内核的代码时,就是内核态
页表
如同国家、省、市、区、街道、小区、家这样从大到小,一层一层检索出具体地址的过程,从虚拟地址到物理地址的映射也是类似进行的。
对于64位机器,高16位不用来查询页表,低48位被分成五部分使用。
前三个高九位,分别用来查询三级页目录,接下来根据再低9位查询叶子页表。这就是高36位
虚拟地址和物理地址都以4kb为单位进行划分,虚拟内存的叫做分页,物理内存的叫做粉叶框,一个字节在页和叶框中的偏移量是相同的。
接着上面剩下的最后低12位虚拟地址,这里正好和4kb的分页一个字节一个字节的对应,就通过这些位去查询叶子页表,获得虚拟地址到物理地址的具体映射
如果没有找到页表中想要查询的字节,就说明相应内容还没有被加载到物理内存当中,这时就会出发缺页中断,cpu由用户态转入内核态,完成从硬盘加载相应内容的操作
cpu中,mmu负责完成这一过程,其中有一块高速缓存------块表(tlb),里面存放着最近用过的映射