目录
- 4. 信号捕捉
- 4.1 信号捕捉的流程(CPU在用户态与内核态之间切换)
- 4.2 操作系统是怎么运行的
- 4.2.1 硬件中断
- 4.2.2 时钟中断
- 4.2.3 死循环
- 4.2.4 软中断
- 4.2.5 缺页中断?内存碎片处理?除零野指针错误?
- 4.3 用户态和内核态
- 4.4 sigaction-更改进程在接收到特定信号时所采取的操作
- 5. 可重入函数(重复进入)
- 6. volatile保持内存可见性-防止优化
- 7. SIGCHLD信号 - 了解
4. 信号捕捉
当前阶段
信号的处理,不是立即处理,而是可以等一会再处理,即合适的时候,进行信号的处理。
4.1 信号捕捉的流程(CPU在用户态与内核态之间切换)
合适的时候是指什么时候?
答:进程从内核态返回到用户态时,进行信号检查(对应上图的2)
补1,内核态:用户调用系统调用后,OS执行系统调用,OS执行系统调用所处的模式就是内核态。
补2,用户态:OS执行用户所写的代码时,此时OS的模式就是用户态。
(上图上半部分执行时是用户态,下半部分执行时是内核态)
示例1:如果信号的处理动作是忽略,则在第3步之后直接返回main函数。
示例2:如果信号的处理动作是默认,则在第3步之后执行默认动作。(暂停就使进程停在某一步,终止就直接杀死进程)
示例3:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:(信号捕捉过程,用户态&内核态切换时机)
用户程序注册了SIGQUIT 信号的处理函数sighandler 。
当前正在执行 main 函数,这时发生中断或异常切换到内核态。
在中断处理完毕后要返回用户态的main 函数之前检查到有信号SIGQUIT 递达。
内核决定返回用户态后不是恢复main 函数的上下文继续执行,而是执行 sighandler 函数, sighandler 和main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。
如果没有新的信号要递达,这次再返回用户态就是恢复main 函数的上下文继续执行了。
问:while(true) {},这种代码在我使用Ctrl+c时也会进入内核态吗?
答:会的!因为CPU的进程调度方式,每次执行到时间片为0时会切换进程继续运行。
4.2 操作系统是怎么运行的
4.2.1 硬件中断
1.OS怎么知道键盘上面有数据了?答:硬件中断,硬件输入设备和CPU有硬件连接,目的是为了中断触发。
中断向量表就是操作系统的一部分,启动就加载到内存中了
通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
由外部设备触发的,中断系统运行流程,叫做硬件中断
Linux操作系统内核源代码:注册中断向量表
// Linux内核0.11源码voidtrap_init(void){inti;set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3);/* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。for(i=17;i<48;i++)set_trap_gate(i,&reserved);set_trap_gate(45,&irq13);// 设置协处理器的陷阱门。outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯片的IRQ2 中断请求。outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯片的IRQ13 中断请求。set_trap_gate(39,¶llel_interrupt);// 设置并行口的陷阱门。}voidrs_init(void){set_intr_gate(0x24,rs1_interrupt);// 设置串行口1 的中断门向量(硬件IRQ4信号)。set_intr_gate(0x23,rs2_interrupt);// 设置串行口2 的中断门向量(硬件IRQ3信号)。init(tty_table[1].read_q.data);// 初始化串行口1(.data 是端口号)。init(tty_table[2].read_q.data);// 初始化串行口2。outb(inb_p(0x21)&0xE7,0x21);// 允许主8259A 芯片的IRQ3,IRQ4 中断信号请求。}📌中断 VS 信号
发中断 — 发信号
保存终端号 — 记录信号
中断号 — 信号编号
处理终端 — 处理信号,自定义捕捉
区别:
- 信号:纯软件,本质是用软件,来模拟硬件中断的!
4.2.2 时钟中断
2.当没有中断到来的时候,OS在做什么?什么都没做,OS是暂停的!
// 内核源代码的main函数中for(;;)pause();问题:
- 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
- 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?
前提:进程调度这个方法也在OS的中断向量表中。
操作系统运行流程:时钟源会以固定的、特定的频率,向CPU发送特定的中断。操作系统OS每隔这个固定的时间就会执行进程调度的方法。(操作系统就在硬件时钟中断的驱动下进行调度了。)(时钟源原来在CPU的外部,为了提高效率现今已经集成到了CPU内部:即CPU自己每隔固定的时间,自己触发中断执行进程调度的函数)
CPU的主频就是CPU中断触发的固定频率!!
时间片:在进程控制块struct tast_struct 中,有一个计数器,用来进行时间片计数,当计数器减到0时(即为时间片耗尽),执行进程调度服务,schedule()。时钟源每+1,时间片计数器就对应的-1。
- 时间片的本质就是计数器。
current->count--;if(current->count==0){schedule();}离线计时:这个频率还可以给计算机进行离线计时,即电脑关机之后,再次开机,仍然是准确的时间。(有一个变量将时间抓换成时间戳,再将时间戳转换成历史总频率,再由CPU的固定频率始终在计算当前时刻)
总结:操作系统就是基于中断进行工作的软件。
回看:2.5.3小节使用示例2!!!模拟操作系统运行方式。
Linux操作系统内核代码如下:操作系统执行进程调度流程
// Linux 内核0.11// main.c有以下函数sched_init();// 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c)// 1.调度程序的初始化子程序。voidsched_init(void){...set_intr_gate(0x20,&timer_interrupt);// 修改中断控制器屏蔽码,允许时钟中断。outb(inb_p(0x21)&~0x01,0x21);// 设置系统调用中断门。set_system_gate(0x80,&system_call);...}// 2.system_call.s_timer_interrupt:...;// do_timer(CPL)执行任务切换、计时等工作,在kernel/shched.c,305 行实现。call _do_timer;// 'do_timer(long CPL)' does everything from// 3.调度入口voiddo_timer(longcpl){...schedule();}// 4.进程切换voidschedule(void){...switch_to(next);// 切换到任务号为next 的任务,并运行之。}4.2.3 死循环
如果是这样,操作系统不就可以躺平了吗?对,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可。操作系统的本质:就是一个死循环!
voidmain(void)/* 这里确实是void,并没错。 */{/* 在startup 程序(head.s)中就是这样假设的。 */.../* * 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返 * 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任 * 务0 在任何空闲时间里都会被激活(当没有其它任务在运行时), * 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运行,如果没 * 有的话我们就回到这里,一直循环执行'pause()'。 */for(;;)pause();}// end main- 这样,操作系统,就可以在硬件时钟的推动下,自动调度了.
- 所以,什么是时间片?CPU为什么会有主频?为什么主频越快,CPU越快?主频可以作为OS调度执行速度的参考之一
4.2.4 软中断
上述外部硬件中断,需要硬件设备触发。
有没有可能,因为软件原因,也触发上面的逻辑?有!
为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内部触发中断逻辑。
前提:
当我们进行系统调用的时候,具体是怎么进入操作系统,完成系统调用过程的,毕竟CPU只有一个?- 系统调用表
fn_ptr sys_call_table[]每个系统调用都有一个唯一的下标,这个下标叫做系统调用号(内核中的)用户层怎么把系统调用号给操作系统? - 寄存器(比如EAX)
操作系统怎么把返回值给用户?- 寄存器或者用户传入的缓冲区地址
总结:系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法。
补,问题:我们本不就是直接使用的系统调用吗,为什么还要转换成系统调用号来让操作系统执行指定的系统调用呢?
答:OS不提供任何系统调用接口,OS只提供系统调用号:而我们使用的open()、fork()等所谓的系统调用都是经过glibc封装过的!
系统调用号的本质:数组下标!
系统调用流程举例:
- 用户调用系统调用
intopen(constchar*pathname,intflags);pid_tfork(void);
- OS将通过寄存器eax系统调用转汇编
move eax2// 2就是open系统调用的系统调用号int0x80
int 0x80触发软中断执行以下函数// int 0x80、syscall触发软中断就跳入以下函数voidCallSystem(){// 1.获取系统调用号n /* int n = 0; move n eax */// 2.调用系统调用方法(当然,肯定要做安全检测)sys_call_table[n]();}
sys_call_table(n)根据系统调用号查表执行对应的系统调用函数。
// sys.h// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。externintsys_setup();// 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)externintsys_exit();// 程序退出。 (kernel/exit.c, 137)externintsys_fork();// 创建进程。 (kernel/system_call.s, 208)externintsys_read();// 读文件。 (fs/read_write.c, 55)externintsys_write();// 写文件。 (fs/read_write.c, 83)externintsys_open();// 打开文件。 (fs/open.c, 138)externintsys_close();// 关闭文件。 (fs/open.c, 192)externintsys_waitpid();// 等待进程终止。 (kernel/exit.c, 142)externintsys_creat();// 创建文件。 (fs/open.c, 187)externintsys_link();// 创建一个文件的硬连接。 (fs/namei.c, 721)externintsys_unlink();// 删除一个文件名(或删除文件)。 (fs/namei.c, 663)externintsys_execve();// 执行程序。 (kernel/system_call.s, 200)externintsys_chdir();// 更改当前目录。 (fs/open.c, 75)externintsys_time();// 取当前时间。 (kernel/sys.c, 102)externintsys_mknod();// 建立块/字符特殊文件。 (fs/namei.c, 412)externintsys_chmod();// 修改文件属性。 (fs/open.c, 105)externintsys_chown();// 修改文件宿主和所属组。 (fs/open.c, 121)externintsys_break();// (-kernel/sys.c, 21)externintsys_stat();// 使用路径名取文件的状态信息。 (fs/stat.c, 36)externintsys_lseek();// 重新定位读/写文件偏移。 (fs/read_write.c, 25)externintsys_getpid();// 取进程id。 (kernel/sched.c, 348)externintsys_mount();// 安装文件系统。 (fs/super.c, 200)externintsys_umount();// 卸载文件系统。 (fs/super.c, 167)externintsys_setuid();// 设置进程用户id。 (kernel/sys.c, 143)externintsys_getuid();// 取进程用户id。 (kernel/sched.c, 358)externintsys_stime();// 设置系统时间日期。 (-kernel/sys.c, 148)externintsys_ptrace();// 程序调试。 (-kernel/sys.c, 26)externintsys_alarm();// 设置报警。 (kernel/sched.c, 338)externintsys_fstat();// 使用文件句柄取文件的状态信息。(fs/stat.c, 47)externintsys_pause();// 暂停进程运行。 (kernel/sched.c, 144)externintsys_utime();// 改变文件的访问和修改时间。 (fs/open.c, 24)externintsys_stty();// 修改终端行设置。 (-kernel/sys.c, 31)externintsys_gtty();// 取终端行设置信息。 (-kernel/sys.c, 36)externintsys_access();// 检查用户对一个文件的访问权限。(fs/open.c, 47)externintsys_nice();// 设置进程执行优先权。 (kernel/sched.c, 378)externintsys_ftime();// 取日期和时间。 (-kernel/sys.c,16)externintsys_sync();// 同步高速缓冲与设备中数据。 (fs/buffer.c, 44)externintsys_kill();// 终止一个进程。 (kernel/exit.c, 60)externintsys_rename();// 更改文件名。 (-kernel/sys.c, 41)externintsys_mkdir();// 创建目录。 (fs/namei.c, 463)externintsys_rmdir();// 删除目录。 (fs/namei.c, 587)externintsys_dup();// 复制文件句柄。 (fs/fcntl.c, 42)externintsys_pipe();// 创建管道。 (fs/pipe.c, 71)externintsys_times();// 取运行时间。 (kernel/sys.c, 156)externintsys_prof();// 程序执行时间区域。 (-kernel/sys.c, 46)externintsys_brk();// 修改数据段长度。 (kernel/sys.c, 168)externintsys_setgid();// 设置进程组id。 (kernel/sys.c, 72)externintsys_getgid();// 取进程组id。 (kernel/sched.c, 368)externintsys_signal();// 信号处理。 (kernel/signal.c, 48)externintsys_geteuid();// 取进程有效用户id。 (kenrl/sched.c, 363)externintsys_getegid();// 取进程有效组id。 (kenrl/sched.c, 373)externintsys_acct();// 进程记帐。 (-kernel/sys.c, 77)externintsys_phys();// (-kernel/sys.c, 82)externintsys_lock();// (-kernel/sys.c, 87)externintsys_ioctl();// 设备控制。 (fs/ioctl.c, 30)externintsys_fcntl();// 文件句柄操作。 (fs/fcntl.c, 47)externintsys_mpx();// (-kernel/sys.c, 92)externintsys_setpgid();// 设置进程组id。 (kernel/sys.c, 181)externintsys_ulimit();// (-kernel/sys.c, 97)externintsys_uname();// 显示系统信息。 (kernel/sys.c, 216)externintsys_umask();// 取默认文件创建属性码。 (kernel/sys.c, 230)externintsys_chroot();// 改变根系统。 (fs/open.c, 90)externintsys_ustat();// 取文件系统信息。 (fs/open.c, 19)externintsys_dup2();// 复制文件句柄。 (fs/fcntl.c, 36)externintsys_getppid();// 取父进程id。 (kernel/sched.c, 353)externintsys_getpgrp();// 取进程组id,等于getpgid(0)。(kernel/sys.c, 201)externintsys_setsid();// 在新会话中运行程序。 (kernel/sys.c, 206)externintsys_sigaction();// 改变信号处理过程。 (kernel/signal.c, 63)externintsys_sgetmask();// 取信号屏蔽码。 (kernel/signal.c, 15)externintsys_ssetmask();// 设置信号屏蔽码。 (kernel/signal.c, 20)externintsys_setreuid();// 设置真实与/或有效用户id。 (kernel/sys.c,118)externintsys_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};// 调度程序的初始化子程序。voidsched_init(void){...// 设置系统调用中断门。set_system_gate(0x80,&system_call);}// 内核中,汇编代码执行系统调用,117行,call跳转:由_sys_call_table+4*eac (起始地址+4字节*eac偏移量)_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 parameterspush ebx;// to the system callmov edx,10h;// set up ds,es to kernel spacemov ds,dx;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。mov es,dx mov edx,17h;// fs points to local data spacemov 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;// statejne reschedule cmp dword ptr[counter+eax],0;// counterje reschedule;// 以下这段代码执行从系统调用C 函数返回后,对信号量进行识别处理。ret_from_sys_call:4.2.5 缺页中断?内存碎片处理?除零野指针错误?
voidtrap_init(void){inti;set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3);/* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。for(i=17;i<48;i++)set_trap_gate(i,&reserved);set_trap_gate(45,&irq13);// 设置协处理器的陷阱门。outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯片的IRQ2 中断请求。outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯片的IRQ13 中断请求。set_trap_gate(39,¶llel_interrupt);// 设置并行口的陷阱门。}- 缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。
📌 所以:
操作系统就是躺在中断处理例程上的代码块!
CPU内部的软中断,比如int 0x80或者syscall,我们叫做 陷阱
CPU内部的软中断,比如除零/野指针等,我们叫做 异常。(所以,能理解“缺页异常”为什么这么叫了吗?)
4.3 用户态和内核态
结论:
操作系统无论怎么切换进程,都能找到同一个操作系统!换句话说操作系统的系统调用方法的执行,是在进程的地址空间中执行的!
用户和内核,都在同一个[0, 4]GB的虚拟地址空间上。
- 用户态就是执行用户[0,3]GB时所处的状态,用户态以用户身份,智能访问自己的[0, 3]GB。
- 内核态就是执行内核[3,4]GB时所处的状态,内核态以内核的身份,通过系统调用的方式,访问OS[3,4]GB
区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
一般执行 int 0x80 或者syscall 软中断,CPL会在校验之后自动变更
关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL等概念, 而现在芯片为了保证兼容性,已经非常复杂了,进而导致OS也必须得照顾它的复杂性,这块我们不做深究了。
4.4 sigaction-更改进程在接收到特定信号时所采取的操作
#include<signal.h>intsigaction(intsigno,conststructsigaction*act,structsigaction*oact);返回值:sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。
参数:signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
const struct sigaction *act结构体参数:将其中的sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段(同样为*act结构体中的成员变量)说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。
5. 可重入函数(重复进入)
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
可重入条件:函数只有自己的临时变量等情况。
6. volatile保持内存可见性-防止优化
- 该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下
#include<iostream>#include<unistd.h>#include<signal.h>#include<cstdlib>intflag=0;// volatile int flag = 0; // 保持内存空间可见性voidhandler(intsignu){std::cout<<"更改全局变量,"<<flag<<"->1"<<std::endl;flag=1;}intmain(){signal(2,handler);while(!flag);std::cout<<"process quit normal!"<<std::endl;return0;}$ g++ test.cc $ a.out#Ctrl+c#标准情况下,键入Ctrl+c,2号信号被捕捉,执行自定义动作,修改flag=1,while条件不满足,退出循环,进程退出.$ g++ test.cc -O1# 后缀 -OX,(此处为英文字母大写O和数字,表示编译器几级优化)#优化情况下,键入Ctrl+,2号信号被捕捉,执行自定义动作,修改 flag=1,但是while条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显,while循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。while检测的flag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要volatile# 使用volatile修饰flag# 再次使用优化编译、执行、Ctrl+c,可见while条件不满足,退出循环,进程退出。- volatile作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
7. SIGCHLD信号 - 了解
进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。(见代码1)
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。请编写程序验证这样做不会产生僵尸进程。(见代码2)
- 作用:可以通过信号捕捉的方案回收子进程(代码1)
#include<iostream>#include<unistd.h>#include<signal.h>#include<cstdlib>#include<signal.h>#include<sys/types.h>#include<sys/wait.h>// 作用:可以通过信号捕捉的方案回收子进程voidWaitAll(intnum){while(true){pid_t n=waitpid(-1,nullptr,WNOHANG);// 非阻塞等待if(n==0){break;}elseif(n<0){std::cout<<"waitpid error"<<std::endl;break;}}std::cout<<"father get a signal: "<<num<<std::endl;}intmain(){// 父进程signal(SIGCHLD,WaitAll);// 父进程for(inti=0;i<10;i++){pid_t id=fork();if(id==0){std::cout<<" I am child, exit"<<std::endl;sleep(3);exit(3);}}while(true){std::cout<<"I am father, exit"<<std::endl;sleep(1);}return0;}- 代码2
#include<iostream>#include<unistd.h>#include<signal.h>#include<cstdlib>#include<signal.h>#include<sys/types.h>#include<sys/wait.h>// 作用:可以将子进程退出给父进程发送的SIGCHLD信号重定义为SIG_IGN,自动回收子进程intmain(){// 父进程signal(SIGCHLD,SIG_IGN);// 父进程for(inti=0;i<10;i++){pid_t id=fork();if(id==0){sleep(3);std::cout<<" I am child, exit"<<std::endl;exit(3);}}while(true){std::cout<<"I am father, exit"<<std::endl;sleep(1);}return0;}此处设置的SIG_IGN和默认的信号处理动作不同,默认信号处理动作是SIG_DEF。