🌟 各位看官好,我是!****
🌍 Linux == Linux is not Unix !
🚀 今天来学习Linux的信号处理流程,了解操作系统是如何运行起来的及各种中断。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!
目录
[穿插 -- 操作系统如何运行](#穿插 -- 操作系统如何运行)
[软中断 -- 辅助系统调用](#软中断 -- 辅助系统调用)
信号捕捉
对信号产生和信号保存有了一定的理解后,就可以从时间维度上讲解最后一个话题:信号处理

在信号处理中,明确进程接收信号后的处理时机与方式十分重要。在不考虑信号屏蔽的情况下,进程收到信号后未必会立即处理,往往会选择在 "合适的时机" 进行 ------ 这通常是因为进程当前可能正在执行更关键的操作,不适合被信号打断。
要理解这一点,需先明确代码执行的两种基本模式:
- 内核态:是操作系统运行时的状态,主要用来执行内核代码
- 用户态:用CPU执行用户自己的代码,所处的状态
那么"合适的时机"指的是什么时候呢?具体又是如何处理的呢?
- 进程从内核态切换回用户态的时候
- OS会检测当前进程的三张表,决定要不要处理信号
既然是从内核态切回用户态,说明我进程之前从用户态进入过内核态 -->我怎么没见过啊?(实际上系统调用就会从用户态切到内核态)小编将会在下文对信号捕捉流程进行详细解释.
信号捕捉流程

如果信号的处理动作是用户⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。
os处理默认和忽略,是很容易的事情,os默认就有权限!
- 如果是ignore,修改pending表由1置0,返回代码继续执行;
- 如果是dfl,如果该信号动作是暂停,此时在os内部,把进程pcb状态由r置为s,将pcb从运行队列剥离出来,加入到等待队列里
而自定义处理方法的处理过程比较复杂,因此就拿它来进行说明:
- 用户程序注册了 SIGQUIT 信号的处理函数 sighandler
- 当前正在执⾏ main 函数,这时发⽣中断或异常切换到内核态。
- 在中断处理完毕后要返回⽤⼾态的 main 函数之前检查到有信号 SIGQUIT 递达。
- 内核决定返回用户态后不是恢复 main 函数的上下⽂继续执⾏,⽽是执⾏ sighandler 函数,sighandler 和 main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个独⽴的控制流程。
- sighandler 函数返回后⾃动执⾏特殊的系统调⽤ sigreturn 再次进⼊内核态。
- 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下⽂继续执行了。
细节1:我们说执行代码只有两种模式,那么执行自定义方法的是用户还是OS呢?需要明确的是只能是用户,不能是OS!因为是在执行自己的代码啊
细节2:执行自定义方法是如何做到从用户态再次进入内核的?将do_signal地址压入到栈里,再通过sigretum特殊系统调用放入到eip里,此时就可以回到内核态继续运行.(实际上是在执行一条能触发软中断的指令)
可是这也太复杂了吧,一个信号捕捉流程就涉及到这么多内容,别担心,这里画出一张表让你一秒钟记住它:

穿插 -- 操作系统如何运行
硬件中断
若让操作系统定期主动扫描键盘硬件状态,仔细想想,这显然不现实。毕竟操作系统作为系统资源的统筹者,绝不会做这种持续浪费 CPU 资源的事。
那有没有更高效的方式?当然有!我们可以让外部设备主动 "告知" 操作系统:当设备准备好(比如有按键输入)时,主动触发一个信号,通知操作系统来处理。
这,正是硬件中断机制诞生的初衷 ------ 用硬件层面的 "主动通知",替代软件层面低效的 "被动轮询",让系统资源利用更高效,也让硬件交互更及时 。

- 中断向量表就是操作系统的⼀部分,启动就加载到内存中了
- 通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
- 由外部设备触发的,中断系统运⾏流程,叫做硬件中断
我们之前讲过当代码中有scanf时,键盘这个设备会等待我们的响应(此时是在设备的等待队列里),而一旦我们键盘按下时,OS就得知我们的键盘是有数据的.由此可以猜想是键盘这个设备告诉OS我已经准备就绪了!
我们之前提到过,当代码中执行到scanf这类等待输入的函数时,目标进程会进入键盘设备的等待队列,直到有按键输入才会继续响应。这背后的关键,其实正是键盘设备在 "主动告知" 操作系统:"我已准备好数据了"。
- 具体来看这个过程:当程序执行到
scanf时,若我们按下某个按键,意味着外设(键盘)已处于就绪状态。 - 这时,键盘会主动发起硬件中断请求。
- 这些请求会由中断控制器(通常支持多设备级联,可统一管理各类外设的中断)收集汇总。随后,中断控制器会通知 CPU:有设备发起了硬件中断,请处理。
- 此时 CPU 可能正在执行某个进程的代码、处理其数据。一旦收到中断通知,CPU 会先将当前的执行上下文(比如寄存器中的临时数据等)保存起来,以确保后续能准确恢复。
- 而操作系统中内置了一套与中断对应的处理机制 ------CPU 会根据中断控制器提供的中断号,找到对应的中断处理程序并执行(比如键盘中断对应的处理程序,会负责读取按键扫描码、转换为对应字符等)。
- 当中断处理程序执行完毕后,CPU 会从之前保存的上下文信息中恢复现场,回到被中断前的状态,继续执行原来的进程工作。
整个过程既避免了操作系统对设备的低效轮询,又能让设备事件得到及时响应,是硬件与系统协同的典型机制。
OS核心就是中断向量表,中断向量表处理方法:
bash
//Linux内核0.11源码
void trap_init(void)
{
int i;
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);// 设置并⾏⼝的陷阱⻔。
}
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 中断信号请求。
}
时钟中断
我们不经有个疑惑?
- 进程能在操作系统的统筹下完成调度与执行,可操作系统本身也是一款软件 ------ 既然如此,它又由谁来指挥、靠什么来推动执行呢?
- 外部设备虽能触发硬件中断,但这类中断通常需要用户操作或设备自身状态变化(如按键按下、数据接收完成)来触发。那么,是否存在能定期自动触发中断的设备呢?

在外部设备有一个时钟源,它会以固定频率进行震动,向CPU触发中断.操作系统不就在硬件的推动下,自动调度了么!!!
bash
// 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
_timer_interrupt:
push ds ;// save ds,es and put kernel data space
push es ;// into them. %fs is used by _system_call
push fs
push edx ;// we save %eax,%ecx,%edx as gcc doesn't
push ecx ;// save those across function calls. %ebx
push ebx ;// is saved as we use that in ret_sys_call
push eax
mov eax,10h ;// ds,es 置为指向内核数据段
mov ds,ax
mov es,ax
mov eax,17h ;// fs 置为指向局部数据段(出错程序的数据段)
mov fs,ax
inc dword ptr _jiffies
;// 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断.
mov al,20h ;// EOI to interrupt controller ;//1
out 20h,al ;// 操作命令字OCW2 进0x20 端口.
;// 下面3 句从选择符中取出当前特权饭别(0 或3)并压入推钱,作为do_timer的参数、
mov eax,dword ptr [R_CS+esp]
and eax,3 ;// %eax is CPL
push eax
;// do_timer(CPL)执行任务切换、计时等工作,在kernel/shched.c,305 行实现
call _do_timer ;// 'do_timer(long CPL)' does everything from
// 时钟中断C 函数处理程序,在kernel/system_cal1.s 中的_timer_interrupt(176 行)被调用。
// 参数cp1 是当前特权级0 或3,0 表示内核代码在执行。
// 对于一个进程由于执行时间片用完时,则进行任务切换。并执行一个计时更新工作。
void do_timer(long cpl)
{
extern int beepcount; // 扬声器发声时间滴答数(kernel/chr_drv/console.c,697)
extern void sysbeepstop(void); //关闭扬声器(kernel/chr drv/console.c,691)
//如果发声计数次数到,则关闭发声。(向x61 口发送命令,复位位0 和1。位0 控制8253
// 计数器2 的工作,位1 控制扬声器)。
if (beepcount)
if (!--beepcount)
sbsbeepstop();
schedule();
}
// 'schedule(void)'是调度函数。这是个很好的代码!没有任何理由对它进行修改,因为它可以在所有的
// 环境下工作(比如能够对I0-边界处理很好的响应等)。只有一件事值得留意,那就是这里的信号
// 处理代码.
// 注意!!任务0 是个闲置('idle')任务,只有当没有其它任务可以运行时才调用它。它不能被杀
// 死,也不能睡眠。任务0 中的状态信息'state'是从来不用的。
void schedule(void)
{
int i, next, c;
struct task_struct **p; //任务结构指针的指针
}
// 如果检测到当前进程时间片没有用完,就会返回,cpu恢复现场,继续执行当前进程
if ((--current->counter) > 0)
return; // 如果进程运行时间还没完,则退出.
/* 检测alarm(进程的报警定时值),唤醒任何已得到信号的可中断任务 */
switch_to(next); // 切换到任务号为next 的任务,并运⾏之。
细节1: 当前CPU计算机,没有外部时钟源,集成到CPU内部了
细节2: CPU主频,CPU固定频率收到对应的中断的!!
- 那么,每触发一次时钟中断,时间就是固定的 ! jiffies++ :中断的次数
- 那么,时钟中断被触发的次数*中断触发的时间间隔 == 时间 !
bash
struct task_struct
{
long counter; //就是时间片
};
细节3:如何理解时间片到了?
bash
// 如果检测到当前进程时间片没有用完,就会返回,cpu恢复现场,继续执行当前进程
if ((--current->counter) > 0)
return; // 如果进程运行时间还没完,则退出.
细节4:每隔一段时间,触发一次时钟中断,什么时候,运行进程自己的代码呢?
结论:内存管理、文件管理都是围绕进程展开的,os把进程调起来,内存、文件管理自然也能被调起来!

细节5:
- 普通进程的执行:依赖 EIP 的 "自然指引",是连贯的 "主动执行"(只要没被打断,就一直跑);
- OS 的执行:依赖硬件中断(时钟、键盘、网卡等)"被动触发"------ 中断打断进程后,CPU 转去执行 OS 的代码,执行完再切回进程;
- 两者的并发:CPU 在 "进程代码" 和 "OS 代码" 之间快速切换,虽然物理上 CPU 同一时刻只能跑一个指令,但宏观上看起来像同时运行。
结论一:OS的运行,是在时钟源的中断下,,一直触发中断处理,执行的操作系统代码!!!
结论二:OS是一种什么软件?操作系统,可以是一种什么都不做的软件只需要是一个死循环即可!!!(OS是一个躺平在中断上的软件集合!)
Linux查看主频(时钟源产生频率HZ):cat /proc/cpuinfo
死循环
如果是这样,操作系统不就可以躺平了吗?对,操作系统⾃⼰不做任何事情,需要什么功能,就向中断向量表⾥⾯添加⽅法即可.操作系统的本质:就是⼀个死循环!
bash
void main(void) /* 这⾥确实是void,并没错。 */
{ /* 在startup 程序(head.s)中就是这样假设的。 */
...
/*
* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返
* 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任
* 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),
* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没
* 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。
*/
for (;;)
pause();
} // end main
这样,操作系统,就可以在硬件时钟的推动下,⾃动调度了.
所以为什么说主频越快,CPU越快呢?
系统时钟基于此信号生成,定期触发中断唤醒操作系统,使其能及时检查进程时间片是否用尽、完成进程切换,能支撑操作系统更高效地调度资源.
软中断 -- 辅助系统调用
无论是在外设还是cpu内部金晶震,都是通过硬件触发时钟中断的,不过就是触发高低电频,让cpu保护现场,然后通过中断号查中断向量表,有没有一种可能让cpu自身能够生成时钟中断,通过软件的方式让cpu走这套流程呢?
有.为了让操作系统⽀持进⾏系统调⽤,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内部触发中断逻辑。
系统调用号+虚拟地址空间+软中断
系统调用是基于软中断的!!!
用户层怎么把系统调⽤号给操作系统?寄存器(⽐如EAX)
操作系统怎么把返回值给用户?寄存器或者用户传⼊的缓冲区地址
系统调用号的宏定义(__NR_xxx) :负责给每个系统调用分配唯一编号(用户态视角的 "标识")。
bash
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall (__NR_SYSCALL_BASE+ 0)
#define __NR_exit (__NR_SYSCALL_BASE+ 1)
#define __NR_fork (__NR_SYSCALL_BASE+ 2)
#define __NR_read (__NR_SYSCALL_BASE+ 3)
#define __NR_write (__NR_SYSCALL_BASE+ 4)
#define __NR_open (__NR_SYSCALL_BASE+ 5)
#define __NR_close (__NR_SYSCALL_BASE+ 6)
/* 7 was sys_waitpid */
#define __NR_creat (__NR_SYSCALL_BASE+ 8)
#define __NR_link (__NR_SYSCALL_BASE+ 9)
#define __NR_unlink (__NR_SYSCALL_BASE+ 10)
Linux 内核的 32 位 x86 架构系统调用表(sys_call_table),定义了系统调用号与内核函数的对应关系。
bash
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
.long sys_unlink /* 10 */
内核实现函数(sys_xxx) :负责实际执行系统调用的功能。

-
用户态通过宏定义获取编号:调用
read时,使用宏__NR_read得到其对应的系统调用号。__NR_read = __NR_SYSCALL_BASE + 3(假设__NR_SYSCALL_BASE为 0,则编号为 3)。 -
用户态传递编号触发系统调用:用户态程序通过
syscall指令(或int 0x80软中断)触发系统调用,并将__NR_read对应的编号(3)存入指定寄存器(如 x86 的EAX),同时传递参数(文件描述符、缓冲区等)。 -
内核通过系统调用表查找函数:内核收到系统调用后,从寄存器中取出编号(3),以该编号为下标查询
sys_call_table数组,得到对应的内核函数地址:
sys_call_table[3] = &sys_read(即数组第 3 项指向sys_read函数)。 -
执行内核函数并返回结果:内核调用
sys_read函数完成实际的 "读取数据" 操作,将结果通过寄存器或用户缓冲区返回给用户态,完成整个调用流程。
- 系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法.
- 系统调⽤号的本质:数组下标!

对于进程来讲,每一个进程都有自己的用户级页表但对于每一个进程来讲,共用一个内核级页表.
结论1:无论进程怎么切换,怎么调度,每一个进程 都可以找到同一个内核!!!也随时能找到内核!!!
结论2:用户要访问OS,只有一种途径,就是系统调用!!!你的进程,是如何看到系统调用的???通过虚拟地址空间! --> 所有的函数调用,未来全部都可以理解成为在我自己的虚拟地址空间中完成!
- 虚拟地址空间分为用户态和内核态(只有在内核态才能访问内核空间) --> 而这涉及到权限管理的问题,权限如何体现,安全又如何保证呢? --> CPU内部有一个cs(code segment),当cs寄存器的CPL = 3时,只能访问用户空间的数据和代码;当CPL = 0时,可以访问整个内存空间,拥有最高权限.
- 我们用的系统调用,我们只知道系统调用的名字啊!并不知道系统调用具体在哪里啊?

如何理解内核态和用户态

- 操作系统⽆论怎么切换进程,都能找到同⼀个操作系统!换句话说操作系统系统调⽤⽅法的执⾏,
- 关于特权级别,涉及到段,段描述符,段选择⼦,DPL,CPL,RPL等概念, ⽽现在芯⽚为了保证兼容性,已经⾮常复杂了,进⽽导致OS也必须得照顾它的复杂性,这块我们不做深究了。
- 用户态就是执⾏用户[0,3]GB时所处的状态
- 内核态就是执⾏内核[3,4]GB时所处的状态
- 区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
- ⼀般执⾏ int 0x80 或者 syscall 软中断,CPL会在校验之后⾃动变更
是在进程的地址空间中执⾏的!
操作系统怎么知道硬件报错了?
硬件报错,触发了CPU的中断! --> CPU执行中断处理方法 --> 中断处理方法,就是OS的一部分!!
写时拷贝和缺页中断
如何理解写时拷贝?
数据段默认为r -->触发mmu报错 -->os能识别,虚拟到物理的转化是存在的,权限级别是r,访问区域是代码区 -->应该要进行拷贝空间了
如何理解缺页中断? 基于中断机制!
缺页中断 --> malloc了1M空间,不需要立马在物理空间上开辟,会在虚拟空间上开辟,但是并没有映射到具体的物理地址,给你返回了.此时你认为你在物理内存申请了一段空间,你用指针去访问时,一旦访问就会mmu报错,虚拟地址已经申请了啊,为什么没有物理地址的指向呢?哦忘记开辟了,此时就会开辟物理内存,触发中断,执行中断向量表的方法,检查是内存空间有没有开辟,空间有了,此时恢复继续执行.
纽扣电池

总结
什么叫做操作系统?
操作系统是一个死循环,是一个基于中断工作的软件集合!!!
操作系统就是躺在中断处理例程上的代码块!
外部硬件引起的错误 --> 硬件中断
CPU内部的软中断,⽐如int 0x80或者syscall,我们叫做 陷阱(没有出错,用户主动让自己陷入内核!!)
CPU内部的软中断,⽐如除零/野指针等,我们叫做 异常(出错了,但不是外设引起的,是用户操作导致cpu内部硬件出问题 )。(能理解"缺⻚异常"为什么这么叫了吗?)
本文深入解析了Linux信号处理机制与操作系统运行原理。主要内容包括:
- 信号捕捉流程,从产生到处理的全过程;
- 中断机制如何驱动操作系统运行,重点分析时钟中断;
- 系统调用实现原理与内核态切换;
- 写时拷贝和缺页中断机制;
- 操作系统本质是依赖硬件中断驱动的"死循环"。通过分析Linux0.11内核源码,展示了中断向量表初始化、进程调度等关键实现,揭示了Linux系统底层运行的完整逻辑框架。

