【Linux篇】信号机制深度剖析:从信号捕捉到SIGCHLD信号处理

📌 个人主页: 孙同学_

🔧 文章专栏: Liunx

💡 关注我,分享经验,助你少走弯路!

文章目录

    • [一. 信号捕捉](#一. 信号捕捉)
      • [1.1 信号捕捉的流程](#1.1 信号捕捉的流程)
      • [1.2 穿插话题 - 操作系统是怎么运行起来的](#1.2 穿插话题 - 操作系统是怎么运行起来的)
        • [1.2.1 硬件中断](#1.2.1 硬件中断)
        • [1.2.2 时钟中断](#1.2.2 时钟中断)
        • [1.2.3 死循环](#1.2.3 死循环)
        • [1.2.4 软中断](#1.2.4 软中断)
      • [1.2.5 缺页中断,内存碎片化,除零野指针](#1.2.5 缺页中断,内存碎片化,除零野指针)
      • [1.3 内核态和用户态](#1.3 内核态和用户态)
      • [1.4 信号捕捉sigaction](#1.4 信号捕捉sigaction)
    • [二. 可重入函数](#二. 可重入函数)
    • [三. volatile](#三. volatile)
    • [四. SIGHILD信号](#四. SIGHILD信号)

一. 信号捕捉

我们对于信号的了解目前到了当前阶段

1.1 信号捕捉的流程

1.当我们执行main函数,执行到一定程度的时候,我们的进程可能由于某些原因导致进入操作系统内部,比如说系统调用。2.在系统调用内部把需要执行的程序执行完,正常情况下再返回到主执行流继续向下执行,3.但是操作系统在返回之前会检查当前的进程是否收到对应的信号,对应的信号是否被block,如果收到了信号并且没有被block,4.操作系统反而会进入信号处理的流程,如果信号处理函数是自定义的,操作系统会由内核态转变为用户态,执行对应的handler方法,执行完毕之后,再使用特定的系统调用(sigreturn)返回到内核处,再由内核态返回到用户态,继续执行主执行流下面的方法。

在内核态返回到用户态做信号检查,检查到特定信号的处理动作是忽略呢?

操作系统只需要把pending比特位由1改回0,然后返回到用户层,继续向后运行

在内核态返回到用户态做信号检查,检查到特定信号的处理动作是默认呢?

大部分信号的默认处理动作都是终止进程,我们此时在内核态,发现次信号的默认处理动作就是终止,操作系统就会直接把我们的进程杀掉,释放PCB,释放地址空间。

总结:

信号的执行时间:进程从内核态返回到用户态的时候。

处理的信号是忽略信号:操作系统只需要把pending表中的1->0即可,然后返回到用户层。

处理的信号是默认信号:大多数信号的默认处理动作是终止进程,如果处理的信号是默认信号,操作系统就会直接终止掉当前进程,释放pcb,释放地址空间等资源

💦所以信号处理默认动作和忽略动作要比处理自定义捕捉简单。

当我们处理自定义捕捉时,从内核态转换到用户态,那么执行handler的人是谁呢?是用户还是操作系统?

答案是用户执行。

不是操作系统执行的原因是如果用户在自己的代码中有一个非法操作呢。
因此在执行自定义方法时(用户写的),操作系统要做一次身份切换,以用户的身份执行自定义的方法。

当自定义方法执行完后,又要执行特殊的系统调用sigreturn再次进入内核这是怎么做到的呢?可以通过类似栈帧技术来完成。

我们和进程凭什么进入内核?
while(true){}这种代码运行时也是一个进程,它也会进入内核吗?

答案是是的,因为这中代码运行起来也是一个进程,它也会被调度,Linux中的进程持有cpu不是说把这个进程跑完才这个进程才结束,每一个进程都有它自己的时间片,时间片到了操作系统就会把当前进程剥离下来,这个进程就不会被调度了,当下次调度到你时你才重新上,看起来我们的while循环一直被打印,其实我们的while循环在一直消耗时间片,时间片消耗完了就把此进程从cpu上拿下来了。进程的时间片到了之后操作系统就强制性的不让此进程执行,强制不让此进程执行就是操作系统强制介入,最后强制进入内核态。

1.2 穿插话题 - 操作系统是怎么运行起来的

1.2.1 硬件中断

当我们按下硬件设备时,操作系统就会向CPU的针脚发送一个叫做硬件中断的东西。

我们之前讲的冯诺依曼体系结构中,输入设备必须将数据从外设先搬到内存中,然后CPU只去访问内存就可以,这叫做数据信号,但并不意味着CPU不能和外设相连接,我们的外设可以和CPU通过线路连接到一起,只不过不以拷贝数据为目的,主要是为了传递信息。

CPU上面的针脚可以间接的和外部设备做信息沟通。其实外部设备它的这些中断信息并没有和CPU直接相连,在硬件上会有一个叫做中断控制器 的东西。

寄存器不一定在CPU中才有,在外部设备中也有。

硬件设备是让CPU知道某个外部设备"准备好了",但是是写还是读CPU并不知道,所以我们又来引申出来一个概念叫做中断向量表(IDT),其实就是一个函数指针数组。这个数组的内容是提取中断的中断方法,数组的下标表示提取中断的中断号。IDT是操作系统的一部分。

所以总的流程就是外部设备"准备好了",CPU通过中断控制器来获取中断号,然后CPU执行内核代码访问中断向量表,拿着获取的中断号,找到对应的中断方法。

📌:中断和信号在思想上是打通的,信号是纯软件上的工作,是用软件来模拟硬件中断的。

在没有中断到来时操作系统什么都不干,处于暂停状态。

1.2.2 时钟中断

进程可以在操作系统的指挥下进行被调度,被执行,那么操作系统又是在谁的驱动下执行呢?

中断向量表中有一个进程调度的中断服务

时钟源: 会以特定的频率,向CPU发送特定的中断。

时钟源以特定的频率向CPU发送中断,从此以后操作系统就在硬件时钟中断的驱动下在中断向量表中找到进程调度中断服务进行调度了。

所以操作系统就是基于中断进行工作的软件

后来硬件设计者发现时钟源和外部设备放在一起和外部设备竞争中断的效率太低了,所以最后将时钟源集成在了CPU的内部。所以CPU自己会每隔一定的时间向CUP自己触发中断,然后由CPU自己调度对应的注册的中断方法叫做schedule()

至此CPU内部就产生了一个概念叫做主频

这样操作系统就会在硬件的推动下自动调度了。

cpp 复制代码
// Linux 内核0.11 
// main.c
sched_init(); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c) 
// 调度程序的初始化⼦程序。 
void sched_init(void)
{
 ...
 set_intr_gate(0x20, &timer_interrupt);
 // 修改中断控制器屏蔽码,允许时钟中断。 
 outb(inb_p(0x21) & ~0x01, 0x21);
 // 设置系统调⽤中断⻔。 
 set_system_gate(0x80, &system_call);
 ...
}
// system_call.s
_timer_interrupt:
 ...
;// do_timer(CPL)执⾏任务切换、计时等⼯作,在kernel/shched.c,305 ⾏实现。 
 call _do_timer ;// 'do_timer(long CPL)' does everything from
// 调度⼊⼝ 
void do_timer(long cpl)
{
 ...
 schedule();
}
void schedule(void)
{
 ...
 switch_to(next); // 切换到任务号为next 的任务,并运⾏之。 
}
1.2.3 死循环

如果是这样的话操作系统不就可以躺平了,操作系统自己不需要干什么事情,需要什么功能就向中断向量表中添加对应的方法即可。
操作系统的本质:就是一个死循环

cpp 复制代码
void main(void) /* 这⾥确实是void,并没错。 */ 
{ /* 在startup 程序(head.s)中就是这样假设的。 */ 
 ...
 /*
 * 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返 
 * 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任 
 * 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时), 
 * 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没 
 * 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。 
 */
 for (;;)
 pause();
} // end main
1.2.4 软中断

上述的外部硬件中断,需要硬件设备触发,也有可能是软件的原因也触发上面的逻辑。为了让操作系统支持系统调用,CPU也涉及到了对应的汇编指令(int或者syscall),可以让CPU内部触发中断逻辑。

假设cpu的主频为1ns,那么cpu经过1ns就触发一次中断,在这1ns里,我们的进程在执行自己的代码,如果今天我们的代码里面有一个除零错误,在cpu的内部有一个标志寄存器EFLAGS,它会发现我们的计算结果发生了cpu硬件上的溢出。以前cpu出错了cpu就不知道该怎么办,后来我们的程序设计者规定成为一种由cpu内部触发的中断,一旦检测出cpu内部出现错误,cpu自己就会生成出一个中断号。它自己生成的中断一旦产生了,cpu就要停下来处理这个中断,中断号一来,我们就要带着中断号在中断向量表里查找对应的中断处理方法。比如说异常处理,给目标进程发送信号。

操作系统是怎么知道硬件出异常了呢?

答案是通过中断,操作系统会自动注册中断处理方法。

比如我们以前所说的虚拟地址和物理地址找不到对应的映射关系就会产生缺页中断page_fault()

我们把这种没有外部设备驱动的由CPU内部,由软件触发的错误我们称作为软中断

我们上面所说的除0操作,野指针操作都是软件问题导致cpu硬件出错而产生中断,有没有一种可能让CPU内部,直接通过软件让CPU主动产生中断呢?

答案是有的,CPU在自己的"指令集"中引入了两个新指令

在32位下叫做int,在64位下叫做syscall

指令集:比如说000,001各自代表了什么操作

C/C++代码:本质就是编译成了指令集+数据

cup一旦触发中断,就要在中断向量表里面查找对应的中断处理方法。那么中断处理方法就必须要有对应的方法和编号,那么int和syscall的放法和编号是多少呢?

当我们进行系统调用的时候具体是怎么进去操作系统,完成系统调用过程的,毕竟cpu只有一个!

我们的系统调用全都在一张叫做系统调用表的函数指针里

从此往后每一个系统调用都有一个下标,这个下标我们叫做系统调用号(在内核中)

我们在中断向量表里,比如说0x80这个位置注册一个方法,比如说CallSystem

我们给0x80直接注册一个软中断,当未来触发这个软中断,执行0x80对应的中断处理方法,会获取系统调用号,执行系统调用。

上面都是内核的实现,那么用户层面呢?怎么把系统调用调起来呢?

比如说 open,open的底层实现也是用汇编写的,move eax 5,它把数字5移动到了eax寄存器中,并且0x80。open最核心的代码片段只需要做上面两步。

那下层获取系统调用号是怎么获取的呢?

在CallSystem把eax寄存器中的内容move到n里面,此时通过eax寄存器就可以让把用户设置的系统调用号让内核拿到了,然后根据系统调用调用底层的方法。

抛出一个问题:当我们进行系统调用的时候,具体是怎么进入操作系统,完成系统调用过程的?
Linux中所有的系统调用是被写在一张Linux系统调用表的函数指针里的 ,从此以后每一个系统调用都有一个下标,这个下标叫做系统调用号

比如在调用一个open系统调用的时候,首先会move eax 5,将编号5放入到eax寄存器中,然后再调用int或者syscall陷入内核,本质是触发软中断,CPU就会根据系统调用号自动查表,执行对应的处理方法。

所以:为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(intsyscall),可以让CPU内部触发中断逻辑。

💦OS不提供任何的系统调用接口,OS只提供系统调用号 ,我们用的系统调用接口都是被glibc封装的。

cpp 复制代码
// 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_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};
// 调度程序的初始化⼦程序。
void sched_init(void)
{
    ...
        // 设置系统调⽤中断⻔。
        set_system_gate(0x80, &system_call);
}
_system_call : cmp eax, nr_system_calls - 1; // 调⽤号如果超出范围的话就在eax 中置-1 并退出。
ja bad_sys_call
    push ds; // 保存原段寄存器值。
push es
    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;                               // ds,es 指向内核数据段(全局描述符表中数据段描述符)。
mov es, dx mov edx, 17h;                  // fs points to local data space
mov fs, dx;                               // fs 指向局部数据段(局部描述符表中数据段描述符)。
;                                         // 下⾯这句操作数的含义是:调⽤地址 = _sys_call_table + %eax * 4。参⻅列表后的说明。
;                                         // 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了⼀个包括72 个
;                                         // 系统调⽤C 处理函数的地址数组表。
call[_sys_call_table + eax * 4] push eax; // 把系统调⽤号⼊栈。
mov eax, _current;                        // 取当前任务(进程)数据结构地址??eax。
;                                         // 下⾯97-100 ⾏查看当前任务的运⾏状态。如果不在就绪状态(state 不等于0)就去执⾏调度程
序。;                                     // 如果该任务在就绪状态但counter[??]值等于0,则也去执⾏调度程序。
cmp dword ptr[state + eax], 0;            // state
jne reschedule
    cmp dword ptr[counter + eax],
    0;         // counter
je reschedule; // 以下这段代码执⾏从系统调⽤C 函数返回后,对信号量进⾏识别处理。
ret_from_sys_call:

可是我们用的系统调用并没有使用int或者syscall呀,那是因为Linux的glibC标准库,给我们把几乎所有的系统调用全部封装了。

把vfork转化成系统调用号,放到eax寄存器中,然后直接调用syscall,完成系统调用。

下图是32位下的

  • #define SYS_ify(syscall_name) __NR_##syscall_name:是一个宏定义,用于将系统调用的名称转换为对应的系统调用号。比如SYS_ify(open)会被展开为__NR_open
  • __NR_open系统调用号,不是glibc提供的,而是内核提供的,内核提供系统调用入口函数man 2 syscall,或者直接提供汇编级别软中断命令int 或者 syscall,并提供对应的头文件或者开发入口,让上层需要的设计者使用系统调用号,完成系统调用过程

🌵系统调用的过程也是在进程地址空间上进行的,所有的函数调用都是地址空间之间的跳转。

1.2.5 缺页中断,内存碎片化,除零野指针

缺页中断,内存碎片化,除零野指针错误,都会被转化成cpu内部的软中断,然后走中断处理例程。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片化的,有的是用来给目标进程发送信号,杀掉进程等等。

所以:

  • 操作系统就是躺在中断处理例程上的代码块!
  • cpu内部的软中断,比如int 0x80或者syscall,我们叫做陷阱
  • cpu内部的软中断,比如除零/野指针等,我们叫做异常。

系统调用表的数据结构是属于操作系统的,所以在操作系统的3-4GB内核区就会存在一个sys_fork()的系统调用。未来我们在我们的代码区,把一个系统调用号move到eax,我们的进程就会触发中断陷入内核,当前的进程运行就被暂停下来了(因为有cpu现场保护),中断处理方法本质就属于操作系统,所以才会陷入内核,陷入内核以后在自己的地址空间上跳转到内核区执行对应的sys_fork()。

  • 系统调用的过程,也是在进程地址空间上进行的
  • 所有的函数调用,都是地址空间之间的跳转

1.3 内核态和用户态

在计算机整个进程的地址空间中,0 ~ 3GB为用户区,3 ~ 4GB为内核态区。用户访问0 ~ 3GB的时候不需要任何的系统调用,在自己的代码里面访问全局变量,堆区,动静态库中的方法,访问栈区,访问命令行参数,环境变量等所有的过程只需要拿到虚拟地址就能直接访问0 ~ 3GB的所有的代码和数据了。

我们的虚拟地址空间,当前用户所有的代码和数据,包括动静态库,栈区,命令行参数以及环境变量,要么代码要么数据最终一定会在物理内存中保存。

操作系统也是软件,也一定在内存中。 所以我们就引出了一个新的概念,内核页表

内核页表只有一份,所有进程共享。

这就意味着无论如何调度,我们总能找到操作系统。

📙用户和内核都在0~4GB的地址空间上了,如果用户随便拿一个虚拟地址[3-4]GB,用户不就可以随便访问内核中的代码和数据了?

答案是:OS为了保护自己,不相信任何人,只能通过系统调用的方式访问。

📕在系统中,用户或者OS自己是怎么知道当前是处于内核态还是用户态的呢?

答案是:存在一个叫做CS的段寄存器,这个段寄存器的低两个比特位表示的就是是在内核态还是用户态,如果是3代表的是用户态,如果是0代表的就是在内核态。CPL(Current Privilege Level),即当前特权级别。

到目前我们就知道了int 0x80syscall的作用就是让cs段寄存器指向操作系统的代码区,同时将权限标志位将11设置为0,此时就标志着陷入内核了。

1.4 信号捕捉sigaction

检查并改变信号的处理动作

cpp 复制代码
       #include <signal.h>

       int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

参数说明:

  • signum:指定要设置或者获取处理程序的信号编码
  • act:注册方法,指向一个sigaction的结构体指针,该结构体包含了新信号的处理信息。如果为NULL,则表示获取当前信号处理程序而不进行修改。
  • oldcat:输出型参数,如果为非NULL,则储存之前的信号处理信息,以便在需要时恢复。

sigaction结构体:

cpp 复制代码
struct sigaction {
	void (*sa_handler)(int);
	void (*sa_sigaction)(int,siginfo_t *,void *);
	sigset_t sa_mask;
	int sa_flags;
	void (*sa_restorer)(void);
};
  • sa_handler:指定信号处理程序,为兼容旧式信号处理的函数指针。
  • sa_sigaction:新的信号处理函数指针,允许接收更多信号相关信息。
  • sa_mask:一个信号集,指定在处理信号时哪些信号应被阻塞。
  • sa_flags:一些标志位,用于控制信号处理的行为,如 SA_RESTART(自动重启被信号中断的系统调用)等。
  • sa_restorer:用于恢复处理程序的上下文,通常不使用。

返回值

成功时返回 0。

失败时返回 -1,并设置 errno 以指示错误类型。

sa_mask:当某个信号的处理函数被调用时,内核自动将当前信号加入到进程的信号屏蔽字,当信号处理函数返回时,自动恢复原来的信号屏蔽字,这样就保证了如果处理某个信号时,如果这种信号再次产生,它就又去执行这种信号了,形成了递归式的处理(说白了就是操作系统不允许同一个信号被重复抵达)。如果在调用信号处理函数时,除了当前信号被自动屏蔽外,还希望屏蔽另外的信号,则用sa_mask字段说明这些额外屏蔽的信号。当信号处理函数返回时,自动恢复为原来的信号屏蔽字。

demo代码:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>

void handler(int signum)
{
    std::cout << "hello signal:" << signum << std::endl;
    while(true)
    {
        //不断获取pending表
        sigset_t pending;
        sigpending(&pending);
        for(int i = 31; i >= 1; i--)
        {
            if(sigismember(&pending,i))
            {
                std::cout << "1" ;
            }
            else
            {
                std::cout << "0" ;
            }
        }
        std::cout << std::endl;
        sleep(1);
    }
    exit(0); // 收到让进程直接退出
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGINT,&act,&oact); //对2号信号进行捕捉

    while (true)
    {
        std::cout << "hello world" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

除了当前信号被自动屏蔽外,还希望屏蔽另外的信号,则用sa_mask字段说明这些额外屏蔽的信号。

总结:从现在开始我们知道的信号捕捉的方法有两种,一种叫做signal一种是sigactionsigaction对普通信号来讲为我们提供了能够捕捉信号时屏蔽其他信号的能力(默认也会屏蔽自己),当信号处理函数完成时它会解除屏蔽。因此就不存在递归式的信号处理了。当解除屏蔽的时候此信号会被立即抵达。

二. 可重入函数

main函数调用insert函数向一个链表head中插入节点Node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换至内核,再次回到用户态之前发现有信号要处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回到内核,再次回到用户态就从main函数调用的insert函数中继续进行,先前做的第一步后被打断,现在做第二步。结果是main函数和sighandler都向链表中插入了两个节点,而最后只有一个节点插入到链表中了,Node2节点丢失了,这就会造成内存泄漏。

像这样的main执行流和sighandler执行流insert方法被两个执行流重复进入了,函数被重复进入代码出了问题则称此函数为不可重入函数,函数被重复进入了代码没有出问题则称此函数为可重入函数。

如果一个函数用了全局变量,那么这个函数大概率是不可重入的,如果一个函数内部只用了自己的临时变量,那么这个函数大概率是可重入的。

三. volatile

volatile是c语言中的关键字,保证内存的可见性

cpp 复制代码
#include <stdio.h> 
#include <signal.h> 
int flag = 0; 
void handler(int sig) 
{ 
 printf("chage flag 0 to 1\n"); 
 flag = 1; 
} 
int main() 
{ 
 signal(2, handler); 
 while(!flag); 
 printf("process quit normal\n"); 
 return 0; 
} 

上述代码的执行逻辑是按理来说代码会一直在while循环这里,但是当我们发送二号信号,将flag从0置为1,当while再次判断时!1就为0,while执行结束,就打印process quit normal

在我们的main函数的执行流里面,并不会对flag做修改,那么就有人会说handler修改了,编译器中没有执行流的概念。编译器发现在整个main执行流的范畴内,编译器没有对flag进行修改,编译器默认main只对flag做检查,编译器是识别不到flag被修改的。比如说编译器在优化级别较高的情况下,编译器就会把我们的flag变量直接优化到register寄存器当中。

我们的cpu一般有两种计算模式:逻辑计算和物理计算。我们的数据是保存在内存中的,当我们要对某个数进行计算,1先要把这个数从内存导入到cpu当中,2在cpu内部进行对应的计算,3如果需要就写回到内存中,如果不需要就不写回。总之少不了把数从物理内存导到cpu中才能进行计算。

我们的while(!flag)起始就是不断地把数从物理内存导到cpu中进行判断,我们来了一个handler方法把flag0 -> 1了,while就循环结束了。可是我们的编译器有可能把我们的1 ,2两步进行优化了,因为flag只在while循环中做判读,并不被修改,所以编译器就会给flag加一个建议性的关键字register,并且flag为0,所以在将来编译运行时直接把0加载到cpu寄存器中,从此while循环只需要检查寄存器中的值是否为真就行。

标准情况下,编译器不会优化我们的代码,当我们编译时加入-o选项时编译器就会对我们的代码进行优化,-o1-o更高的优化,-o2是比-o1更高的优化。

我们的CPU通常会处理两种逻辑,计算逻辑和条件逻辑,执行的过程是:

从内存中将数据加载到CPU中 ---> 在CPU中计算 ---> 若需要时则返回。

当我们将我们的上述代码编译时加入-o选项,编译器发现我们的flag在while循环中只是充当判断条件,而并没有做任何的修改,就会把我们的flag优化到寄存器中,这样当CPU加载flag时就会把flag从内存中加载到CPU这一步骤省略了,直接从寄存器中读取,这样读取到的flag就会一直为0,while判断就会为真,发送2号信号就不会打印了。

如果我们不想被编译器优化呢?很明显需要volatile

cpp 复制代码
#include <stdio.h> 
#include <signal.h> 

volatile int flag = 0; 
void handler(int sig) 
{ 
 printf("chage flag 0 to 1\n"); 
 flag = 1; 
} 
int main() 
{ 
 signal(2, handler); 
 while(!flag); 
 printf("process quit normal\n"); 
 return 0; 
} 
  • volatile作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

四. SIGHILD信号

SIGHILD信号编号为17

我们用wait或者waitpid函数来清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞的查询是否有子进程结束等待清理(也就是轮询的方式),采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式父进程在处理自己的工作的时候还有时不时的轮询一下,程序实现复杂。

其实子进程在退出的时候会给父进程发送SIGCHLD信号,我们为什么看不到呢?原因是父进程接收到这个信号的默认动作是忽略。父进程可以自定义SIGCHLD函数,这样父进程就可以专心处理自己的工作,子进程在结束时会通知父进程,父进程在信号处理函数中只需调用wait函数清理子进程即可。

测试代码:

cpp 复制代码
#include <stdio.h> 
#include <stdlib.h> 
#include <signal.h> 
void handler(int sig) 
{ 
 pid_t id; 
 while( (id = waitpid(-1, NULL, WNOHANG)) > 0) { 
 printf("wait child success: %d\n", id); 
 } 
 printf("child is quit! %d\n", getpid()); 
} 
int main() 
{ 
 signal(SIGCHLD, handler); 
 pid_t cid; 
 if((cid = fork()) == 0){//child 
 printf("child : %d\n", getpid()); 
 sleep(3); 
 exit(1); 
 } 
 while(1){ 
 printf("father proc is doing some thing!\n"); 
 sleep(1); 
 } 
 return 0; 
}
相关推荐
jimy120 小时前
安卓里运行Linux
linux·运维·服务器
爱凤的小光21 小时前
Linux清理磁盘技巧---个人笔记
linux·运维
耗同学一米八1 天前
2026年河北省职业院校技能大赛中职组“网络建设与运维”赛项答案解析 1.系统安装
linux·服务器·centos
知星小度S1 天前
系统核心解析:深入文件系统底层机制——Ext系列探秘:从磁盘结构到挂载链接的全链路解析
linux
2401_890443021 天前
Linux 基础IO
linux·c语言
智慧地球(AI·Earth)1 天前
在Linux上使用Claude Code 并使用本地VS Code SSH远程访问的完整指南
linux·ssh·ai编程
老王熬夜敲代码1 天前
解决IP不够用的问题
linux·网络·笔记
zly35001 天前
linux查看正在运行的nginx的当前工作目录(webroot)
linux·运维·nginx
QT 小鲜肉1 天前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
问道飞鱼1 天前
【Linux知识】Linux 虚拟机磁盘扩缩容操作指南(按文件系统分类)
linux·运维·服务器·磁盘扩缩容