跳到主要内容 Linux 信号机制深度剖析:从信号捕捉到 SIGCHLD 处理 | 极客日志
C
Linux 信号机制深度剖析:从信号捕捉到 SIGCHLD 处理 Linux 信号机制涉及进程间通信及中断处理。文章解析了信号捕捉流程,包括内核态与用户态切换、硬件中断与软中断机制。重点介绍了 sigaction 函数配置信号处理,以及可重入函数和 volatile 关键字在信号处理中的关键作用。最后阐述了 SIGCHLD 信号用于处理僵尸进程,通过自定义处理函数配合 waitpid 实现非阻塞子进程状态检查,避免父进程阻塞。
晚风告白 发布于 2026/2/6 更新于 2026/4/18 8.6K 浏览Linux 信号机制深度剖析
一、信号捕捉
1.1 信号捕捉的流程
当我们执行 main 函数,执行到一定程度的时候,进程可能由于某些原因导致进入操作系统内部,比如系统调用。
在系统调用内部把需要执行的程序执行完,正常情况下再返回到主执行流继续向下执行。
但是操作系统在返回之前会检查当前的进程是否收到对应的信号,对应的信号是否被 block。如果收到了信号并且没有被 block,操作系统反而会进入信号处理的流程。
如果信号处理函数是自定义的,操作系统会由内核态转变为用户态,执行对应的 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 是操作系统的一部分。
所以总的流程就是外部设备'准备好了', 通过中断控制器来获取中断号,然后 执行内核代码访问中断向量表,拿着获取的中断号,找到对应的中断方法。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
CPU
CPU
📌:中断和信号在思想上是打通的,信号是纯软件上的工作,是用软件来模拟硬件中断的。
在没有中断到来时操作系统什么都不干,处于暂停状态。
1.2.2 时钟中断 进程可以在操作系统的指挥下进行被调度,被执行,那么操作系统又是在谁的驱动下执行呢?
中断向量表中有一个进程调度的中断服务。
时钟源: 会以特定的频率,向 CPU 发送特定的中断。
时钟源以特定的频率向 CPU 发送中断,从此以后操作系统就在硬件时钟中断的驱动下 在中断向量表中找到进程调度中断服务进行调度了。
后来硬件设计者发现时钟源和外部设备放在一起和外部设备竞争中断的效率太低了,所以最后将时钟源集成在了 CPU 的内部。所以 CPU 自己会每隔一定的时间向 CPU 自己触发中断,然后由 CPU 自己调度对应的注册的中断方法叫做 schedule()。
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/sched.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 死循环 如果是这样的话操作系统不就可以躺平了,操作系统自己不需要干什么事情,需要什么功能就向中断向量表中添加对应的方法即可。
操作系统的本质:就是一个死循环
void main (void ) {
...
for (;;) pause();
}
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++ 代码:本质就是编译成了指令集 + 数据
cpu 一旦触发中断,就要在中断向量表里面查找对应的中断处理方法。那么中断处理方法就必须要有对应的方法和编号,那么 int 和 syscall 的放法和编号是多少呢?
当我们进行系统调用的时候具体是怎么进去操作系统,完成系统调用过程的,毕竟 cpu 只有一个!
我们的系统调用全都在一张叫做系统调用表的函数指针里
从此往后每一个系统调用都有一个下标,这个下标我们叫做系统调用号 (在内核中)
我们在中断向量表里,比如说 0x80 这个位置注册一个方法,比如说 CallSystem
我们给 0x80 直接注册一个软中断,当未来触发这个软中断,执行 0x80 对应的中断处理方法,会获取系统调用号,执行系统调用。
上面都是内核的实现,那么用户层面呢?怎么把系统调用调起来呢?
比如说 open,open 的底层实现也是用汇编写的,mov eax 5,它把数字 5 移动到了 eax 寄存器中,并且 0x80。open 最核心的代码片段只需要做上面两步。
那下层获取系统调用号是怎么获取的呢?
在 CallSystem 把 eax 寄存器中的内容 move 到 n 里面,此时通过 eax 寄存器就可以让把用户设置的系统调用号让内核拿到了,然后根据系统调用调用底层的方法。
抛出一个问题:当我们进行系统调用的时候,具体是怎么进入操作系统,完成系统调用过程的?
Linux 中所有的系统调用是被写在一张Linux 系统调用表的函数指针里的 ,从此以后每一个系统调用都有一个下标,这个下标叫做系统调用号 。
比如在调用一个 open 系统调用的时候,首先会 mov eax 5,将编号 5 放入到 eax 寄存器中,然后再调用 int 或者 syscall 陷入内核,本质是触发软中断,CPU 就会根据系统调用号自动查表,执行对应的处理方法。
所以:为了让操作系统支持进行系统调用,CPU 也设计了对应的汇编指令 (int 或 syscall),可以让 CPU 内部触发中断逻辑。
💦OS 不提供任何的系统调用接口,OS 只提供系统调用号 ,我们用的系统调用接口都是被 glibc 封装的。
extern int sys_setup () ;
extern int sys_exit () ;
extern int sys_fork () ;
extern int sys_read () ;
extern int sys_write () ;
extern int sys_open () ;
extern int sys_close () ;
extern int sys_waitpid () ;
extern int sys_creat () ;
extern int sys_link () ;
extern int sys_unlink () ;
extern int sys_execve () ;
extern int sys_chdir () ;
extern int sys_time () ;
extern int sys_mknod () ;
extern int sys_chmod () ;
extern int sys_chown () ;
extern int sys_break () ;
extern int sys_stat () ;
extern int sys_lseek () ;
extern int sys_getpid () ;
extern int sys_mount () ;
extern int sys_umount () ;
extern int sys_setuid () ;
extern int sys_getuid () ;
extern int sys_stime () ;
extern int sys_ptrace () ;
extern int sys_alarm () ;
extern int sys_fstat () ;
extern int sys_pause () ;
extern int sys_utime () ;
extern int sys_stty () ;
extern int sys_gtty () ;
extern int sys_access () ;
extern int sys_nice () ;
extern int sys_ftime () ;
extern int sys_sync () ;
extern int sys_kill () ;
extern int sys_rename () ;
extern int sys_mkdir () ;
extern int sys_rmdir () ;
extern int sys_dup () ;
extern int sys_pipe () ;
extern int sys_times () ;
extern int sys_prof () ;
extern int sys_brk () ;
extern int sys_setgid () ;
extern int sys_getgid () ;
extern int sys_signal () ;
extern int sys_geteuid () ;
extern int sys_getegid () ;
extern int sys_acct () ;
extern int sys_phys () ;
extern int sys_lock () ;
extern int sys_ioctl () ;
extern int sys_fcntl () ;
extern int sys_mpx () ;
extern int sys_setpgid () ;
extern int sys_ulimit () ;
extern int sys_uname () ;
extern int sys_umask () ;
extern int sys_chroot () ;
extern int sys_ustat () ;
extern int sys_dup2 () ;
extern int sys_getppid () ;
extern int sys_getpgrp () ;
extern int sys_setsid () ;
extern int sys_sigaction () ;
extern int sys_sgetmask () ;
extern int sys_ssetmask () ;
extern int sys_setreuid () ;
extern int sys_setregid () ;
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 ;
ja bad_sys_call
push ds;
push es
push fs
push edx;
push ecx;
push ebx;
mov edx, 10 h;
mov ds, dx;
mov es, dx
mov edx, 17 h;
mov fs, dx;
call [_sys_call_table + eax * 4 ]
push eax;
mov eax, _current;
cmp dword ptr [state + eax], 0 ;
jne reschedule
cmp dword ptr [counter + eax], 0 ;
je reschedule
ret_from_sys_call:
可是我们用的系统调用并没有使用 int 或者 syscall 呀,那是因为 Linux 的 glibC 标准库,给我们把几乎所有的系统调用全部封装了。
把 vfork 转化成系统调用号,放到 eax 寄存器中,然后直接调用 syscall,完成系统调用。
#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 0x80 和 syscall 的作用就是让 cs 段寄存器指向操作系统的代码区,同时将权限标志位将 11 设置为 0,此时就标志着陷入内核了。
1.4 信号捕捉 sigaction #include <signal.h>
int sigaction (int signum, const struct sigaction* act, struct sigaction* oldact) ;
signum: 指定要设置或者获取处理程序的信号编码
act: 注册方法,指向一个 sigaction 的结构体指针,该结构体包含了新信号的处理信息。如果为 NULL,则表示获取当前信号处理程序而不进行修改。
oldcat: 输出型参数,如果为非 NULL,则储存之前的信号处理信息,以便在需要时恢复。
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 字段说明这些额外屏蔽的信号。当信号处理函数返回时,自动恢复为原来的信号屏蔽字。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
void handler (int signum) {
std::cout << "hello signal:" << signum << std::endl;
while (true ) {
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);
while (true ) {
std::cout << "hello world" << getpid () << std::endl;
sleep (1 );
}
return 0 ;
}
除了当前信号被自动屏蔽外,还希望屏蔽另外的信号,则用 sa_mask 字段说明这些额外屏蔽的信号。
总结:从现在开始我们知道的信号捕捉的方法有两种,一种叫做 signal 一种是 sigaction,sigaction 对普通信号来讲为我们提供了能够捕捉信号时屏蔽其他信号的能力(默认也会屏蔽自己),当信号处理函数完成时它会解除屏蔽。因此就不存在递归式的信号处理了。当解除屏蔽的时候此信号会被立即抵达。
二、可重入函数 main 函数调用 insert 函数向一个链表 head 中插入节点 Node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换至内核,再次回到用户态之前发现有信号要处理,于是切换到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2,插入操作的两步都做完之后从 sighandler 返回到内核,再次回到用户态就从 main 函数调用的 insert 函数中继续进行,先前做的第一步后被打断,现在做第二步。结果是 main 函数和 sighandler 都向链表中插入了两个节点,而最后只有一个节点插入到链表中了,Node2 节点丢失了,这就会造成内存泄漏。
像这样的 main 执行流和 sighandler 执行流 insert 方法被两个执行流重复进入了,函数被重复进入代码出了问题则称此函数为不可重入函数,函数被重复进入了代码没有出问题则称此函数为可重入函数。
如果一个函数用了全局变量,那么这个函数大概率是不可重入的,如果一个函数内部只用了自己的临时变量,那么这个函数大概率是可重入的。
三、volatile volatile 是 c 语言中的关键字,保证内存的可见性
#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 方法把 flag 有 0 -> 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
#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 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
四、SIGCHLD 信号 SIGCHLD 信号编号为 17
我们用 wait 或者 waitpid 函数来清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞的查询是否有子进程结束等待清理(也就是轮询的方式),采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式父进程在处理自己的工作的时候还有时不时的轮询一下,程序实现复杂。
其实子进程在退出的时候会给父进程发送 SIGCHLD 信号,我们为什么看不到呢?原因是父进程接收到这个信号的默认动作是忽略。父进程可以自定义 SIGCHLD 函数,这样父进程就可以专心处理自己的工作,子进程在结束时会通知父进程,父进程在信号处理函数中只需调用 wait 函数清理子进程即可。
#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 ) {
printf ("child : %d\n" , getpid());
sleep(3 );
exit (1 );
}
while (1 ) {
printf ("father proc is doing some thing!\n" );
sleep(1 );
}
return 0 ;
}