目录

四:捕捉信号

4-1 信号捕捉的流程

4-3 穿插话题 - 操作系统是怎么运⾏的

4-3-1 硬件中断

4-3-2 时钟中断

4-3-3 死循环

4-3-4 软中断

4-3-5 缺⻚中断?内存碎⽚处理?除零野指针错误?

4-4 如何理解内核态和⽤⼾态

四:捕捉信号

4-1 信号捕捉的流程

如果信号的处理动作是⽤⼾⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。

由于信号处理函数的代码是在⽤⼾空间的,处理过程⽐较复杂:

  1. ⽤⼾程序注册了 SIGQUIT 信号的处理函数 sighandler
  2. 当前正在执⾏ main 函数,这时发⽣中断或异常切换到内核态
  3. 在中断处理完毕后要返回⽤⼾态的 main 函数之前检查到有信号 SIGQUIT 递达
  4. 内核决定返回⽤⼾态后不是恢复 main 函数的上下⽂继续执⾏,⽽是执⾏ sighandler 数, sighandler main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个 独⽴的控制流程
  5. sighandler 函数返回后⾃动执⾏特殊的系统调⽤ sigreturn 再次进⼊内核态
  6. 如果没有新的信号要递达,这次再返回⽤⼾态就是恢复 main 函数的上下⽂继续执⾏了

4-3 穿插话题 - 操作系统是怎么运⾏的

4-3-1 硬件中断


  • 中断向量表就是操作系统的⼀部分,启动就加载到内存中了
  • 通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
  • 由外部设备触发的,中断系统运⾏流程,叫做硬件中断
//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 中断信号请
求。
}

4-3-2 时钟中断

// 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 的任务,并运⾏之。
}

4-3-3 死循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统⾃⼰不做任何事情,需要什么功能,就向中断向量表⾥⾯添加⽅法即可. 操作系统的本质:就是⼀个死循环
void main(void) /* 这⾥确实是void,并没错。 */
{ /* 在startup 程序(head.s)中就是这样假设的。 */
...
/*
* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返
* 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任
* 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),
* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没
* 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。
*/
for (;;)
pause();
} // end main

4-3-4 软中断

  • 上述外部硬件中断,需要硬件设备触发。
  • 为了让操作系统⽀持进⾏系统调⽤,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内部触发中断逻辑。

⽤⼾层怎么把系统调⽤号给操作系统? - 寄存器(⽐如EAX)
操作系统怎么把返回值给⽤⼾?- 寄存器或者⽤⼾传⼊的缓冲区地址
系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,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/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
Linux的gnu C标准库,给我们把⼏乎所有的系统调⽤全部封装了。

4-3-5 缺⻚中断?内存碎⽚处理?除零野指针错误?

缺⻚中断?内存碎⽚处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,
然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来
处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。

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

4-4 如何理解内核态和⽤⼾态

结论:
  • 操作系统⽆论怎么切换进程,都能找到同⼀个操作系统!换句话说操作系统系统调⽤⽅法的执⾏,是在进程的地址空间中执⾏的!
  • ⽤⼾态就是执⾏⽤⼾[0,3]GB时所处的状态
  • 内核态就是执⾏内核[3,4]GB时所处的状态
  • 区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
  • ⼀般执⾏ int 0x80 或者 syscall 软中断,CPL会在校验之后⾃动变更。

感谢观看!!!祝大家心想事成、学业进步!!!😊

Logo

昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链

更多推荐