当前位置: 首页 > news >正文

【Linux系统】进程间信号(捕捉信号)

信号捕捉的流程

在这里插入图片描述

  • 用户程序注册了SIGQUIT信号的处理函数sighandler
  • 当前正在执行main函数,这时发生中断或异常切换到内核态。
  • 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT 送达。
  • 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandlermain函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
  • sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
  • 如果没有新的信号要送达,这次再返回用户态就是恢复main函数的上下文继续执行了。

sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

struct sigaction 结构体

struct sigaction {void     (*sa_handler)(int);  // 信号处理函数指针void     (*sa_sigaction)(int, siginfo_t *, void *);  // 扩展的信号处理函数指针sigset_t   sa_mask;           // 在信号处理函数执行期间要阻塞的信号集int        sa_flags;          // 改变信号处理行为的标志void     (*sa_restorer)(void); // 已弃用,不要使用
};
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。
  • signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
  • sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。

操作系统是怎么运行的

硬件中断

在这里插入图片描述

  • 中断向量表就是操作系统的一部分,启动就加载到内存中了
  • 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
  • 由外部设备触发的,中断系统运行流程,叫做硬件中断

具体流程可以简单抽象为:
保存现场->根据中断号查表(中断向量表)->调用对应的中断方式

//Linux内核0.11源码
void trap_init(void)
{int i;set_trap_gate(0,&divide_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,&parallel_interrupt); // 设置并行口的陷阱门。
}void rs_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 中断信号请求。
}

时钟中断

问题:

  • 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
  • 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?

在这里插入图片描述

  • 时钟中断,一直在推进操作系统进行调度。
  • 进而我们可以得出结论:操作系统就是基于中断向量表进行工作的
  • 这样,操作系统不就在硬件的推动下,自动调度了么!!!
// 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:...;(fndp _timer(CPL) 执行任务切换、计时等工作,在kernel/sched.c.305 行实现。...;// 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 的任务,并运行之。
}

死循环

如果是这样,操作系统不就可以躺平了吗? 对,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可.

操作系统的本质: 就是一个死循环!

void main(void) /* 这里确实是void,并没有错。 */
{/* 在startup 程序(head.s)中就是这样假设的。 */.../** 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返* 回就绪运行态,但任务0 (task0) 是唯一的意外情况 (参见'schedule()') ,因为任* 务0 在任何空闲时间里都会被激活 (当没有其它任务在运行时) ,* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运行,如果没* 有的话我们就回到这里,一直循环执行'pause()'。*/for (;;)pause();
} // end main
  • 这样,操作系统,就可以在硬件时钟的推动下,自动调度了.
  • 所以,什么是时间片?CPU为什么会有主频?为什么主频越快,CPU越快?

时间片(Time slice)是操作系统进行 CPU 调度时分配给每个进程的一个时间长度。在多任务操作系统中,多个进程需要共享 CPU 资源。为了让每个进程都能得到执行机会,操作系统会将 CPU 的执行时间划分成一个个固定长度的时间片段,即时间片。

软中断

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

所以:
在这里插入图片描述
总结:系统调用也是通过中断完成的。
问题:

  • 用户层怎么把系统调用号给操作系统? - 寄存器(比如EAX)
  • 操作系统怎么把返回值给用户? - 寄存器或者用户传入的缓冲区地址
  • 系统调用的过程,其实就是先int 0x80syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法
  • 系统调用号的本质: 数组下标!
// 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/namei.c, 721)
extern int sys_link ();   // 创建文件。(fs/namei.c, 663)
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_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/sched.c, 26)
extern int sys_alarm ();  // 设置报警。(kernel/sched.c, 338)
extern int sys_fstat ();  // 使用文件句柄取文件的状态信息。(fs/stat.c, 47)
extern int sys_pause ();  // 暂停进程运行。(kernel/sys.c, 24)
extern int sys_utime ();  // 修改文件的访问和修改时间。(fs/open.c, 144)
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_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 (); // 更改文件名。(fs/namei.c, 463)
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, 68)
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。(kernel/sched.c, 363)
extern int sys_getegid (); // 取进程有效组id。(kernel/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, 82)
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。(kernel/sched.c, 353)
extern int sys_setsid (); // 在新会话中运行程序。(kernel/sys.c, 206)
extern int sys_sigaction (); // 信号处理。(kernel/signal.c, 63)
extern int sys_siggetmask (); // 取信号屏蔽码。(kernel/signal.c, 15)
extern int sys_sigsetmask (); // 设置信号屏蔽码。(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_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_kill, sys_gtty,sys_access, 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_siggetmask, sys_sigsetmask, 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_callpush ds ;// 保存原段寄存器值。push espush fspush 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 space
    mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。mov es,dxmov edx,17h ;// fs 指向局部数据段(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 reschedulecmp dword ptr [counter+eax],0 ;// counterjne reschedule// 以下这段代码执行从系统调用C 函数返回后,对信号量进行识别处理。
ret_from_sys_call:
  • 可是为什么我们用的系统调用,从来没有见过什么 int 0x80 或者 syscall 呢? 都是直接调用上层的函数的啊?
  • 那是因为Linux的gnu C标准库,给我们把几乎所有的系统调用全部封装了。
  • glibc给我们把系统调用进行了封装,其实Linux系统内核提供的系统调用,根本就不是C函数,而是系统迪调用号+约定的传递参数。

缺页中断? 内存碎片处理? 除零野指针错误?

void trap_init(void)
{int i;set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。set_trap_gate(1,&debug);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,&parallel_interrupt); // 设置并行口的陷阱门。
}
  • 缺页中断? 内存碎片处理? 除零野指针错误? 这些问题,全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。

所以:

  • CPU内部的软中断,比如int 0x80或者syscall,我们叫做 陷阱
  • CPU内部的软中断,比如除零/野指针等,我们叫做 异常(所以,能理解 “缺页异常” 为什么这么叫了吧? )

如何理解内核态和用户态

在这里插入图片描述
结论:

  • 操作系统无论怎么切换进程,都能找到同一个操作系统!换句话说操作系统系统调用方法的执行,是在进程的地址空间中执行的!(内核页表,整个系统独一份)
  • 关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL等概念,而现在芯片为了保证兼容性,已经非常复杂了,进而导致OS也必须得照顾它的复杂性,这块我们不做深究了。
  • 用户态就是执行用户[0,3]GB时所处的状态
  • 内核态就是执行内核[3,4]GB时所处的状态
  • 区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
  • 一般执行 int 0x80 或者 syscall 软中断,CPL会在校验之后自动变更

可重入函数

在这里插入图片描述

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

  • 像上例这样,insert函数被不同的控制流程调用**,有可能在第一次调用还没返回时就再次进入该函数,这称为重入**,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

  • 如果一个函数符合以下条件之一则是不可重入的:

    • 调用了malloc或free,因为malloc也是用全局的链表来管理堆的。
    • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile

• 该关键字在C当中我们已经有所涉猎, 今天我们站在信号的角度重新理解一下

$ cat sig.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;
}$ cat Makefile
sig:sig.c
gcc -o sig sig.c #-O2
.PHONY:clean
clean:
rm -f sig$./sig
^Cchage flag 0 to 1
process quit normal

标准情况下, 键入 CTRL-C ,2号信号被捕捉, 执行自定义动作, 修改 flag=1 , while条件不满足,退出循环, 进程退出

[hb@localhost code_test]$ cat sig.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;
}$ cat Makefile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig$./sig
^Cchage flag 0 to 1
^Cchage flag 0 to 1

优化情况下, 键入 CTRL-C , 2号信号被捕捉, 执行自定义动作, 修改 flag=1 , 但是while条件依旧满足,进程 继续运行!

但是很明显flag肯定已经被修改了, 但是为何循环依旧执行?

很明显, while 循环检查的 flag, 并不是内存中最新的flag, 这就存在了数据二异性的问题。

while 检测的flag 其实已经因为优化, 被放在了CPU寄存器当中。如何解决呢? 很明显需要 volatile

$ cat sig.c
#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;
}$ cat Makefile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig$./sig
^Cchage flag 0 to 1
process quit normal
  • volatile作用: 保持内存的可见性, 告知编译器**, 被该关键字修饰的变量, 不允许被优化, 对该变量的任何操作, 都必须在真实的内存中进行操作**

SIGCHLD信号

已知用wait和waitpid函数清理僵尸进程。

父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。

采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要时不时地轮询一下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。请编写程序验证这样做不会产生僵尸进程。

测试代码

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>void handler(int sig)
{pid_t id = waitpid(-1, NULL, WNOHANG);while (id > 0) {printf("wait child success: %d\n", id);id = waitpid(-1, NULL, WNOHANG);}printf("child is quit! \n");
}int main()
{signal(SIGCHLD, handler);pid_t cid;if ((cid = fork()) == 0) {printf("child id: %d\n", getpid());sleep(3);exit(1);}while(1){printf("father proc is doing some thing!\n");sleep(1);}return 0;
}

用户态和内核态

  • CPU指令集:是CPU实现软件指挥硬件执行的媒介,具体来说每一条汇编语句都对应了一条CPU指令 ,而非常多的CPU指令在一起,可以组成一个、甚至多个集合,指令的集合叫CPU指令集 。

  • CPU指令集权限分级:CPU指令集可以直接操作硬件,若指令操作不规范,造成的错误会影响整个计算机系统。为避免开发人员因操作失误影响系统,硬件设备商对CPU指令集设置了权限。以Inter CPU为例,把CPU指令集操作的权限由高到低划为4级:

    • ring 0:权限最高,可以使用所有CPU指令集。
    • ring 1
    • ring 2
    • ring 3:权限最低,仅能使用常规CPU指令集,不能使用操作硬件资源的CPU指令集 ,如I/O读写、网卡访问、申请内存都不行 。

Linux系统仅采用ring 0和ring 3这2个权限。CPU中有一个标志字段,标志着线程的运行状态,用户态为3,内核态为0。

  • ring 0(内核态):完全在操作系统内核中运行 ,执行内核空间的代码,具有ring 0保护级别,有对硬件的所有操作权限,可以执行所有CPU指令集 ,访问任意地址的内存,在内核模式下的任何异常都可能导致整台机器停机。
  • ring 3(用户态):在应用程序中运行 ,在用户模式下,具有ring 3保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存,在这种保护模式下,即便程序发生崩溃也可以恢复,电脑上大部分程序都在用户模式下运行。

低权限的资源范围较小,高权限的资源范围更大,所以用户态与内核态的概念就是CPU指令集权限的区别。

通过指令集权限区分用户态和内核态,还限制了内存资源的使用,操作系统为用户态与内核态划分了两块内存空间,给它们对应的指令集使用。

以Linux 32位操作系统为例,它的寻址空间范围是4G(2的32次方) ,操作系统会把虚拟控制地址划分为两部分,一部分为内核空间,另一部分为用户空间,高位的1G(从虚拟地址0xC0000000到0xFFFFFFFF)由内核使用,而低位的3G(从虚拟地址0x00000000到0xBFFFFFFF)由各个进程使用。
在这里插入图片描述

  • 用户态:只能操作0 - 3G范围的低位虚拟空间地址 。
  • 内核态:0 - 4G范围的虚拟空间地址都可以操作,尤其是对3 - 4G范围的高位虚拟空间地址必须由内核态去操作 。
    • 3G - 4G部分是共享的(指所有进程的内核态逻辑地址共享同一块内存地址) ,是内核的地址空间,存放整个内核的代码和所有的内核模块,以及内核所维护的数据。
    • 在内核运行过程中,会涉及内核栈的分配,内核的进程管理代码会将内核栈创建在内核空间中,相应的页表也会被创建。

用户态与内核态的切换

什么情况会导致用户态到内核态切换

  • 系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如fork()就是一个创建新进程的系统调用。操作系统提供了中断指令int 0x80来主动进入内核,这是用户程序发起的调用访问内核代码的唯一方式。调用系统函数时会通过内联汇编代码插入int 0x80的中断指令,内核接收到int 0x80中断后,查询中断处理函数地址,随后进入系统调用。
  • 异常:当CPU在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常 。
  • 中断:当CPU在执行用户态的进程时,外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。

切换时CPU需要做什么

  • 当某个进程中要读写I/O,必然会用到ring 0级别的CPU指令集 。而此时CPU的指令集操作权限只有ring 3,为了可以操作ring 0级别的CPU指令集 ,CPU切换指令集操作权限级别为ring 0(可称之为提权),CPU再执行相应的ring 0级别的CPU指令集(内核代码) 。
  • 代码发生提权时,CPU是需要切换栈的。内核有自己的内核栈。CPU切换栈需要栈段描述符(ss寄存器)和栈顶指针(esp寄存器) ,CPU通过一个段寄存器(tr)确定TSS(任务状态段,struct TSS)的位置。在TSS结构中存在SS0和ESP0。提权的时候,CPU就从这个TSS里把SS0和ESP0取出来,放到ss和esp寄存器中。

切换流程

  1. 从用户态切换到内核态时,首先用户态可以直接读写寄存器,用户态操作CPU,将寄存器的状态保存到对应的内存中,然后调用对应的系统函数,传入对应的用户栈地址和寄存器信息,方便后续内核方法调用完毕后,恢复用户方法执行的现场。
  2. 从用户态切换到内核态需要提权,CPU切换指令集操作权限级别为ring 0。
  3. 提权后,切换内核栈。然后开始执行内核方法,相应的方法栈帧临时保存在内核栈中。
  4. 当内核方法执行完毕后,CPU切换指令集操作权限级别为ring 3,然后利用之前写入的信息来恢复用户栈的执行。

从上述流程可以看出用户态切换到内核态的时候,会牵扯到用户态现场信息的保存以及恢复,还要进行一系列的安全检查,比较耗费资源。

相关文章:

  • 6.11.各顶点间的最短路径问题-Floyd算法
  • Redis基本使用
  • Git 基本操作(二)
  • AimRT从入门到精通 - 01实现一个helloworld
  • FHQ Treap
  • AI作画提示词:Prompts工程技巧与最佳实践
  • MYSQL-设计表
  • 消息队列的作用
  • [Verilog]跨时钟域数据传输解决方案
  • 2025五一数学建模竞赛A题完整分析论文(共45页)(含模型、可运行代码、数据)
  • spring中的@PostConstruct注解详解
  • 支持向量机(SVM)详解
  • 报文三次握手对么٩(๑^o^๑)۶
  • 网络原理 - 13(HTTP/HTTPS - 4 - HTTPS)
  • 从数据到故事:用可视化工具讲好商业“话本“
  • 鼎讯信通 智能通信干扰设备:多频段多模态信号压制解决方案
  • PS插件生态深度解析与精品合集推荐
  • 姜老师MBTI人格分析课程2:ENFP
  • ppt设计美化公司_杰青_长江学者_优青_青年长江学者_万人计划青年拔尖人才答辩ppt模板
  • 深入解析C++11 auto关键字:类型推导的现代实践
  • 辽宁男篮被横扫这一晚,中国篮球的一个时代落幕了
  • 4月一二线城市新房价格环比上涨,沪杭涨幅居百城前列
  • 苹果手机为何无法在美制造?全球供应链难迁移
  • 中国空间站多项太空实验已取得成果,未来将陆续开展千余项研究
  • 五大国货美妆去年业绩分化:珀莱雅百亿营收领跑,上海家化转亏
  • 视频丨中国海警位中国黄岩岛领海及周边区域执法巡查