Linux 进程信号之信号的捕捉

发布于:2025-09-15 ⋅ 阅读:(25) ⋅ 点赞:(0)

序言:

在前面的博客中我们详细介绍了进程信号的产生和保存的话题,这篇博客我们开始了解信号处理和操作系统是如何运行的话题。

前面博客很精彩,如果没有看过之前的博客快去看吧!!!我相信你会收获很多!!!

《Linux 进程信号之信号的产生》

《Linux 进程信号之信号的保存》

一、信号的处理过程

我们在之前的博客说过:进程对于普通信号会选择一个合适的时间处理,那么其实信号捕捉的主要话题都是围绕这个话题来展开的。

下面我们来看一幅图片:

1.1、信号捕捉的过程:

1、由于系统调用,异常,中断等陷入内核。

2、当处理完中断,异常,系统调用之后准备返回时会调用do_signal函数去检查。

3、如果do_signal信号检查到有信号要且还是自定义捕捉就会去转换回用户态去执行自定义捕捉代码(信号处理方法有三种后面我们会详细说每一个的过程)。

4、进程转换成用户态后去执行自定义捕捉代码在结束的时候再变成内核态调用sys_sigreturn函数。

5、当执行完成sys_sigreturn 函数之后,进程返回用户态,执行下面的代码。

内核态和用户态我会在下面的内容逐步解释清楚。

1.2、对信号捕捉的流程我有几个问题:

1.2.1、如何实现在调用完自定义捕捉的时候去调用sigreturn函数的?

在我们学习函数的时候知道调用函数的时候会先开辟函数栈帧把相关数据也压入进去,这些相关数据就包含当函数调用结束之后要返回的地址,那么如果我们把这个地址改成sigreturn函数的函数地址那么当函数结束之后不就自动调用sigreturn函数了。

下面有一副图片去理解一下(当然图片的例子并不是进程信号的处理的代码但也可以理解这个问题):

1.2.2、当处理完自定义捕捉并调用完sigreturn函数之后返回执行接下来的代码时候,还需要去调用do_signal函数去检查是否有新的信号到来吗?

答案是不是,简单一点说当调用完sigreturn函数之后就已经返回用户态了而do_signal函数是内核态的代码,如果想要了解更多是因为要保证信号处理的原子性。

do_signal的调用时机被严格限制在 “进程从内核态返回用户态前”(如系统调用结束、时钟中断返回), sigreturn是一个特殊的 “上下文恢复流程”,其设计目标是:

1、确保信号处理函数执行完毕后,无缝恢复到原用户态逻辑(就像信号从未发生过一样)。

2、若sigreturn后再调用do_signal,可能导致 “信号嵌套处理”(新信号打断原上下文恢复),破坏信号处理的原子性,甚至引发栈帧混乱。

只有当进程再次进入内核态(如执行新的系统调用、触发新的中断),并从内核态返回用户态时,才会再次调用do_signal检查是否有新的未处理信号。

1.2.3、当执行自定义捕捉的时候为什么要变成用户态不用内核态?

为了实现操作系统的安全性,因为内核态的权限高如果有恶意的程序去执行一些恶意代码的话,操作系统的安全性就无法保证。

1.2.4、我们的代码没有调用系统调用也会陷入内核?

当我们的进程时间片耗尽,将会执行调度代码将进程换下,这个时候就需要去执行操作系统的代码就需要陷入内核。

在整个过程中一共有四次身份的转变和一次检查,这幅图可以帮你快速记忆。

1.3、三种捕捉的处理过程

默认捕捉:

对于默认动作,由于进程处于内核态可以直接执行操作系统对应的代码如果默认动作是杀死进程那就直接杀死,如果是暂停进程那就改变进程的运行状态把PCB链入等待队列中(如果看到这可能你不是很理解但往下看我会详细讲解内核态和用户态看完回来再看你就会理解)。

忽略捕捉:

对于忽略动作,由于进程处于内核状态权限高可以直接去更改进程的pending表,并直接返回用户态去执行下面的代码。

自定义捕捉:

会跳转去到用户态去执行用户的自定义捕捉代码,然后在返回内核态。

二、硬件中断

我们先想一个问题:

操作系统是如何知道我们的硬件设备准备好的?

难道是操作系统进行轮询检查?要是这样说的话那么操作系统既要去进行进行进程的调度,还需要对我们的硬件设备进行检查,那么操作系统的工作量也太大了吧,操作系统当然不会进行轮询式的检查,那么怎么实现?答案是操作系统通过中断的方式。

下面我们来看一副图:

硬件中断流程:

1、当硬件设备准备好了数据以后他就会向中断管理器(比如8259芯片)发送一个信号。

2、收到信号之后会把中断号记录在自己的寄存器中。

3、然后中断控制器再向CPU发送一个信号。

4、CPU去读取中断管理器的寄存器中的中断号。

5、CPU停止进程代码,并保存上下文。

6、CPU去查中断向量表再用中断号去执行对应的中断方法(中断方法是操作系统提供的在开机的时候操作系统起来的时候就加载进来)。

7、当把中断方法处理完了以后恢复上下文数据CPU继续执行进程下面的代码。

中断控制器是如何知道对应硬件设备的中断号的?

8259芯片通过不同的引脚与不同的硬件设备间接或者直接的相连,设备准备好了以后通过高低电平通知芯片,芯片再通过中断号注册表去查找对应的中断号。

硬件中断和信号的关系?

发中断 <==>发信号

保存中断号 <==> 记录信号

中断号 <==> 信号编号

处理中断 <==>  处理信号

有了硬件中断以后计算机有了对外界信号的感知能力再结合中断处理方法从此计算机有了对外界信号的处理能力。有了信号以后进程有了对某种事件发送的感知能力再结合信号处理方法有了对事件发生的处理能力。

三、时钟中断

1、什么是时钟中断?

如果我们在加上一个时钟源作为外部信号,定时向CPU发送信号让它去执行对应的代码。因为时钟源的频率很高是nm级别的如果把它放在外边会和其他的外部设备发的信号产生冲突,所以工程师们把时钟源集成在了CPU上。

其实时钟源的频率就是我们买电脑CPU时关注的主频。

2、操作系统是怎么运行起来的?

void main(void) 
{ 
...

for (;;)
pause();
} // end main

上面的代码就是Linux操作系统的main函数,我们可以看见如果没有信号的话操作系统只会一直暂停,只有当时钟信号到来的时候CPU会执行对应的中断方法(对应的调度代码),从而操作系统才能运行起来。

四、软中断

软件中断?是软件也能造成中断吗?答案是是的。

我们的系统调用就是依靠软件中断实现的,这个可能和我们以前的理解不一样,我们之前一直说的都是操作系统给我们提供对应的系统调用吗?这个句话也不能说完全错,操作系统确实向我们提供了对应的系统调用号但我们看见的系统调用函数是被glibc库进行封装的。

CPU有很多指令(syscall ,int 0X80)可以使CPU中断,CPU再去对应的中断方法中去调用。

下面我们看一下glibc是如何去封装系统调用的?

操作系统提供了系统调用的调用号和函数的具体实现,glibc首先将系统调用号先放在eax寄存器中,再用指令syscall陷入内核执行对应的中断方法,再从寄存器中拿出系统调用号再去执行系统函数调用表中的方法,当执行完了以后恢复上下文继续向下执行。

void sched_init(void)
{
    ...
// 设置系统调⽤中断⻔。
set_system_gate(0x80, &system_call);
} 


system_call:
cmp eax,nr_system_calls-1 ;
ja bad_sys_call
push ds ;
push fs
push edx ;// ebx,ecx,edx 中放着系统调⽤相应的C 语⾔函数的调⽤参数。
push ecx ;// push %ebx,%ecx,%edx as parameters
push ebx ;// to the system call
mov edx,10h ;// set up ds,es to kernel space
mov ds,dx ;
mov es,dx
mov edx,17h ;
mov fs,dx ;
call [_sys_call_table+eax*4] //确定调用函数的指针地址
push eax ;
mov eax,_current ;
cmp dword ptr [state+eax],0 ;// state
jne reschedule
cmp dword ptr [counter+eax],0 ;// counter
je reschedule;
ret_from_sys_call:

什么是软中断?

由于软件的原因引起进程由用户态进入内核态,这就是软终端,对于由于系统调用的软中断又叫做陷阱,由于软件引起的硬件出错从而引起的中断的软中断又叫做异常(比如缺页中断)。

那么缺页中断是怎么回事?

在CPU中集成了一个状态寄存器(EFLAGS),它的功能是记录在CPU在计算的时候是否出现了错误,如果发现ELFLAGS出错将会触发中断,CPU保护现场去执行对应的中断方法,在中断方法内会开辟内存和页表并做好页表和内存的映射,当这些工作做完了以后CPU恢复现场继续向下执行进程的代码,所以像缺页中断这种是由软件引起再由硬件异常造成的中断的软中断叫做异常。

系统调用函数表是什么东西?

系统调用函数表本质就是函数指针表,再这里变存储了全部的系统调用的实现。

/ 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(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
}

五、内核态和用户态

在内核中有两个页表,内核页表指向的是操作系统的代码和数据全部进程共用,而用户页表是指向进程的代码和数据每个进程有一个。

为什么内核页表只有一个而用户页表有多个?

用户页表有多个是因为为了进程的独立性,如果页表不是独立的另个进程可以访问我的数据如果改动会导致进程崩溃

内核页表只有一个因为CPU要中断而中断向量表和中断函数实现都是在内核的代码和数据区如果有多份,这些页表全部都是一样的会浪费内存资源。如果页表大家共用的话,CPU无论在执行哪个进程的代码在中断的时候都可以快速跳转到内核的代码去执行。

那这样说的话我们可以直接跳转到内核去,这和以前我们说的操作系统不相信任何人只能操作系统去访问内核的数据和代码吗?

当然为了保证操作系统的安全性,操作系统设置了很多办法来区分用户的权限,下面我们说一个方法用CRL判定进程的权限:

在CS寄存器中会有两位比特位来记录进程当前的权限有0 内核,3用户这就是CRL,通过比较想要访问的地址的权限和现在进程的权限来判断当前的动作是否合法。

所以什么是内核态?

以内核的身份,允许通过系统调用的方式访问内核的数据和代码(操作系统只允许用户通过系统调用的方式进入内核)。

用户态:以用户的身份只能去访问自己的代码和数据。

内核态由谁决定?

进程的内核态还是用户态由CPU的CS寄存器来决定。

=========================================================================下面是一些补充理解:

当去要去进入内核去处理信号的时候进程需要被换下吗?

答案是不要,进程的PCB依旧在CPU中但PC指针指向的内核区的代码和数据,CRL由0变成3等等。

如何理解进程变成内核态?

当进程进入内核就这个时候进程就相当于操作系统可以执行操作系统的代码,去暂停杀死进程。

=========================================================================

本篇关于命名管道的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持和修正!!!