[linux仓库]信号处理[进程信号·伍]

🌟 各位看官好,我是!****

🌍 Linux == Linux is not Unix !

🚀 今天来学习Linux的信号处理流程,了解操作系统是如何运行起来的及各种中断。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!

目录

信号捕捉

信号捕捉流程

[穿插 -- 操作系统如何运行](#穿插 -- 操作系统如何运行)

硬件中断

时钟中断

死循环

[软中断 -- 辅助系统调用](#软中断 -- 辅助系统调用)

系统调用号+虚拟地址空间+软中断

如何理解内核态和用户态

写时拷贝和缺页中断

纽扣电池

总结


信号捕捉

对信号产生和信号保存有了一定的理解后,就可以从时间维度上讲解最后一个话题:信号处理

在信号处理中,明确进程接收信号后的处理时机与方式十分重要。在不考虑信号屏蔽的情况下,进程收到信号后未必会立即处理,往往会选择在 "合适的时机" 进行 ------ 这通常是因为进程当前可能正在执行更关键的操作,不适合被信号打断。

要理解这一点,需先明确代码执行的两种基本模式:

  • 内核态:是操作系统运行时的状态,主要用来执行内核代码
  • 用户态:用CPU执行用户自己的代码,所处的状态

那么"合适的时机"指的是什么时候呢?具体又是如何处理的呢?

  1. 进程从内核态切换回用户态的时候
  2. OS会检测当前进程的三张表,决定要不要处理信号

既然是从内核态切回用户态,说明我进程之前从用户态进入过内核态 -->我怎么没见过啊?(实际上系统调用就会从用户态切到内核态)小编将会在下文对信号捕捉流程进行详细解释.

信号捕捉流程

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

os处理默认和忽略,是很容易的事情,os默认就有权限!

  • 如果是ignore,修改pending表由1置0,返回代码继续执行;
  • 如果是dfl,如果该信号动作是暂停,此时在os内部,把进程pcb状态由r置为s,将pcb从运行队列剥离出来,加入到等待队列里

而自定义处理方法的处理过程比较复杂,因此就拿它来进行说明:

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

细节1:我们说执行代码只有两种模式,那么执行自定义方法的是用户还是OS呢?需要明确的是只能是用户,不能是OS!因为是在执行自己的代码啊

细节2:执行自定义方法是如何做到从用户态再次进入内核的?将do_signal地址压入到栈里,再通过sigretum特殊系统调用放入到eip里,此时就可以回到内核态继续运行.(实际上是在执行一条能触发软中断的指令)

可是这也太复杂了吧,一个信号捕捉流程就涉及到这么多内容,别担心,这里画出一张表让你一秒钟记住它:

穿插 -- 操作系统如何运行

硬件中断

若让操作系统定期主动扫描键盘硬件状态,仔细想想,这显然不现实。毕竟操作系统作为系统资源的统筹者,绝不会做这种持续浪费 CPU 资源的事。

那有没有更高效的方式?当然有!我们可以让外部设备主动 "告知" 操作系统:当设备准备好(比如有按键输入)时,主动触发一个信号,通知操作系统来处理。

这,正是硬件中断机制诞生的初衷 ------ 用硬件层面的 "主动通知",替代软件层面低效的 "被动轮询",让系统资源利用更高效,也让硬件交互更及时 。

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

我们之前讲过当代码中有scanf时,键盘这个设备会等待我们的响应(此时是在设备的等待队列里),而一旦我们键盘按下时,OS就得知我们的键盘是有数据的.由此可以猜想是键盘这个设备告诉OS我已经准备就绪了!

我们之前提到过,当代码中执行到scanf这类等待输入的函数时,目标进程会进入键盘设备的等待队列,直到有按键输入才会继续响应。这背后的关键,其实正是键盘设备在 "主动告知" 操作系统:"我已准备好数据了"。

  1. 具体来看这个过程:当程序执行到scanf时,若我们按下某个按键,意味着外设(键盘)已处于就绪状态。
  2. 这时,键盘会主动发起硬件中断请求
  3. 这些请求会由中断控制器(通常支持多设备级联,可统一管理各类外设的中断)收集汇总。随后,中断控制器会通知 CPU:有设备发起了硬件中断,请处理。
  4. 此时 CPU 可能正在执行某个进程的代码、处理其数据。一旦收到中断通知,CPU 会先将当前的执行上下文(比如寄存器中的临时数据等)保存起来,以确保后续能准确恢复。
  5. 而操作系统中内置了一套与中断对应的处理机制 ------CPU 会根据中断控制器提供的中断号,找到对应的中断处理程序并执行(比如键盘中断对应的处理程序,会负责读取按键扫描码、转换为对应字符等)。
  6. 当中断处理程序执行完毕后,CPU 会从之前保存的上下文信息中恢复现场,回到被中断前的状态,继续执行原来的进程工作。

整个过程既避免了操作系统对设备的低效轮询,又能让设备事件得到及时响应,是硬件与系统协同的典型机制。

OS核心就是中断向量表,中断向量表处理方法:

bash 复制代码
//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 中断信号请求。
}

时钟中断

我们不经有个疑惑?

  1. 进程能在操作系统的统筹下完成调度与执行,可操作系统本身也是一款软件 ------ 既然如此,它又由谁来指挥、靠什么来推动执行呢?
  2. 外部设备虽能触发硬件中断,但这类中断通常需要用户操作或设备自身状态变化(如按键按下、数据接收完成)来触发。那么,是否存在能定期自动触发中断的设备呢?

在外部设备有一个时钟源,它会以固定频率进行震动,向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 :负责实际执行系统调用的功能

  1. 用户态通过宏定义获取编号:调用 read 时,使用宏 __NR_read 得到其对应的系统调用号。__NR_read = __NR_SYSCALL_BASE + 3(假设 __NR_SYSCALL_BASE 为 0,则编号为 3)。

  2. 用户态传递编号触发系统调用:用户态程序通过 syscall 指令(或 int 0x80 软中断)触发系统调用,并将 __NR_read 对应的编号(3)存入指定寄存器(如 x86 的 EAX),同时传递参数(文件描述符、缓冲区等)。

  3. 内核通过系统调用表查找函数:内核收到系统调用后,从寄存器中取出编号(3),以该编号为下标查询 sys_call_table 数组,得到对应的内核函数地址:
    sys_call_table[3] = &sys_read(即数组第 3 项指向 sys_read 函数)。

  4. 执行内核函数并返回结果:内核调用 sys_read 函数完成实际的 "读取数据" 操作,将结果通过寄存器或用户缓冲区返回给用户态,完成整个调用流程。

  • 系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法.
  • 系统调⽤号的本质:数组下标!

对于进程来讲,每一个进程都有自己的用户级页表但对于每一个进程来讲,共用一个内核级页表.

结论1:无论进程怎么切换,怎么调度,每一个进程 都可以找到同一个内核!!!也随时能找到内核!!!

结论2:用户要访问OS,只有一种途径,就是系统调用!!!你的进程,是如何看到系统调用的???通过虚拟地址空间! --> 所有的函数调用,未来全部都可以理解成为在我自己的虚拟地址空间中完成!

  1. 虚拟地址空间分为用户态和内核态(只有在内核态才能访问内核空间) --> 而这涉及到权限管理的问题,权限如何体现,安全又如何保证呢? --> CPU内部有一个cs(code segment),当cs寄存器的CPL = 3时,只能访问用户空间的数据和代码;当CPL = 0时,可以访问整个内存空间,拥有最高权限.
  2. 我们用的系统调用,我们只知道系统调用的名字啊!并不知道系统调用具体在哪里啊?

如何理解内核态和用户态

  • 操作系统⽆论怎么切换进程,都能找到同⼀个操作系统!换句话说操作系统系统调⽤⽅法的执⾏,
  • 关于特权级别,涉及到段,段描述符,段选择⼦,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信号处理机制与操作系统运行原理。主要内容包括:

  1. 信号捕捉流程,从产生到处理的全过程;
  2. 中断机制如何驱动操作系统运行,重点分析时钟中断;
  3. 系统调用实现原理与内核态切换;
  4. 写时拷贝和缺页中断机制;
  5. 操作系统本质是依赖硬件中断驱动的&quot;死循环&quot;。通过分析Linux0.11内核源码,展示了中断向量表初始化、进程调度等关键实现,揭示了Linux系统底层运行的完整逻辑框架。
相关推荐
HIT_Weston5 小时前
15、【Ubuntu】【VSCode】VSCode 断联问题分析:UID 补充
linux·vscode·ubuntu
Ro Jace5 小时前
SCI论文实验设计方案(以信号处理领域为例)
人工智能·信号处理
碰大点5 小时前
Ubuntu 16.04交叉编译arm-linux-gnueabihf的QT5.6.2
linux·arm开发·qt·ubuntu·arm-linux
小-黯6 小时前
Linux硬盘挂载脚本
linux·运维·服务器
PeaceKeeper76 小时前
简易的arm-linux库文件移植
linux·运维·arm开发
运维帮手大橙子6 小时前
最近面试题总结
linux·服务器·网络
Madison-No712 小时前
【Linux】gcc/g++编辑器 && 初识动静态库 && 程序翻译过程
linux·服务器
字节逆旅14 小时前
一个从从容容,一个连滚带爬:scp 与 rsync 的不同人生
linux
洛克大航海14 小时前
Linux 中新建用户
linux·运维·服务器