Back

[中文] Operating Systems Notes: 02 - 中断异常机制[中文] Operating Systems Notes: 02 - 中断异常机制

一些问题#

  1. 应用程序是如何与操作系统交互的?

    • 应用程序通过系统调用与操作系统交互。系统调用提供了应用程序访问操作系统服务的接口,例如文件操作、进程管理和内存管理等。
  2. 怎样理解“操作系统是由中断/异常/事件驱动的“这句话?

    • 这句话的意思是操作系统的运行依赖于中断、异常和事件的触发。中断和异常是硬件或软件产生的信号,通知操作系统需要处理的事件。操作系统通过响应这些信号来管理系统资源和执行任务。
  3. 中断/异常的来源有什么不同?处理方式是一样的吗?

    • 中断通常由外部设备(如键盘、鼠标、网络接口等)产生,而异常通常由CPU在执行指令时检测到的错误(如除零错误、非法指令等)产生。处理方式有所不同,中断处理程序通常较为简单,主要负责响应外部设备的请求,而异常处理程序则需要更复杂的错误处理机制。
  4. 回顾一下:ICS对异常的描述及分类

    • ICS(计算机系统结构)将异常分为四类:陷入(Trap)、故障(Fault)、终止(Abort)和中断(Interrupt)。陷入是由用户程序主动发起的系统调用,故障是可恢复的错误,终止是不可恢复的错误,中断是由外部设备发起的请求。
  5. 中断/异常处理流程中,哪些工作是硬件(体系结构)负责的?哪些工作是软件(操作系统)负责的?

    • 硬件负责检测中断/异常、保存当前的处理器状态、查找中断向量表并跳转到相应的中断/异常处理程序。软件负责具体的中断/异常处理逻辑,包括错误处理、资源管理和恢复系统状态等。
  6. 从中断响应(硬件)到中断处理程序(软件)执行结束,计算机系统经过了哪些流程?

    • 计算机系统首先由硬件检测到中断信号,保存当前处理器状态,查找中断向量表并跳转到中断处理程序。中断处理程序执行相应的处理逻辑,处理完成后恢复处理器状态,返回到中断前的执行点继续执行。
  7. 操作系统初始化与中断/异常有哪些关联?

    • 操作系统初始化时会设置中断向量表、初始化中断控制器、注册中断/异常处理程序等。中断/异常机制是操作系统正常运行的重要保障。
  8. 什么是软件异常?它是如何工作的?

    • 软件异常是由软件引发的异常情况,例如非法内存访问、除零错误等。软件异常通过硬件检测并触发相应的异常处理程序,操作系统负责处理这些异常并采取相应的措施,如终止进程、生成错误报告等。
  9. X86有哪些控制和状态寄存器?所起的作用是什么?

    • X86处理器有多个控制和状态寄存器,包括CR0-CR4(控制寄存器)、EFLAGS(状态寄存器)、GDTR/IDTR(全局/中断描述符表寄存器)等。控制寄存器用于控制处理器的操作模式,状态寄存器保存处理器的状态标志,描述符表寄存器用于存储全局和中断描述符表的地址。
  10. X86在PentiumII 300之后提供了sysenter/sysexit指令,为什么?与int 0x80/iret有什么不同?X86-64提供的系统调用指令是什么?

    • sysenter/sysexit指令提供了更高效的系统调用机制,减少了系统调用的开销。与int 0x80/iret相比,sysenter/sysexit指令不需要保存和恢复中断标志,减少了上下文切换的开销。X86-64提供的系统调用指令是syscall/sysret。
  11. 关于基于x86体系结构的Linux的系统调用实现:

  12. 系统调用入口程序system_call()与中断描述符表是什么关系?与系统调用表是什么关系?

    • system_call()是系统调用的入口程序,通过中断描述符表(IDT)中的中断向量指向。系统调用表(sys_call_table)存储了所有系统调用的地址,system_call()根据系统调用号查找并调用相应的系统调用处理程序。
  13. 系统调用处理结束后,处理器转去执行哪个模块?

    • 系统调用处理结束后,处理器会返回到用户态,继续执行被中断的用户程序。
  14. 系统调用与函数/过程调用的区别是什么?系统调用与C函数调用的区别?系统调用与API的关系?

    • 系统调用是操作系统提供的接口,用于应用程序请求操作系统服务。函数/过程调用是程序内部的调用机制。系统调用与C函数调用的区别在于系统调用需要从用户态切换到内核态,而C函数调用在用户态内执行。API(应用程序编程接口)是应用程序与操作系统或库函数之间的接口,系统调用是API的一部分,提供底层操作系统服务。

关键核心:ECF——异常控制流#

理解ECF(异常控制流)是深入理解计算机系统和操作系统交互的关键。

  1. 理解应用程序是如何与操作系统交互的

    • ECF描述了应用程序在运行过程中如何通过系统调用、中断和异常与操作系统进行交互。通过理解ECF,可以更好地理解操作系统如何管理硬件资源和提供服务。
  2. 编写有趣的新应用程序

    • 通过掌握ECF的原理,开发者可以编写更高效、更可靠的应用程序。理解系统调用和异常处理机制,可以帮助开发者优化程序性能,并处理各种异常情况。
  3. 理解并发

    • ECF在并发编程中起着重要作用。通过理解中断和异常的处理流程,可以更好地设计和实现多线程、多进程的并发程序,确保程序的正确性和高效性。
  4. 理解软件异常如何工作

    • 软件异常是ECF的重要组成部分。通过理解软件异常的触发和处理机制,可以更好地调试和维护程序,提升程序的稳定性和安全性。

一、中央处理器(CPU)#

关于寄存器#

处理器由运算器、控制器、寄存器及高速缓存构成:

用户可见寄存器

  • 机器语言可以直接访问
  • 数据寄存器(通用寄存器)
  • 地址寄存器
  • 条件码寄存器:保存CPU操作结果的标记位

控制和状态寄存器

  • 用于控制处理器操作,在特权级别下可访问
  • 程序计数器(PC, Program Counter)
  • 指令寄存器(IR, Instruction Register)
  • 程序状态字(PSW, Program Status Word)

操作系统的需求之一 —— 保护#

从操作系统的特征考虑#

操作系统需要处理并发和共享资源的问题,这就提出了对系统进行保护与控制的要求。为了实现这一点,操作系统依赖于硬件机制来隔离操作系统和用户程序。

硬件机制的支持#

为了实现保护,硬件需要提供基本的运行机制:

  1. 处理器的不同运行模式

    • 处理器具有不同的运行模式,每种模式下运行的指令集合不同,这些模式被称为特权级别。
    • 通过特权级别,处理器可以区分内核态和用户态,从而控制哪些指令可以在特定模式下执行。
  2. 特权级别

    • 特权级别决定了处理器可以执行哪些指令以及访问哪些资源。
    • 在高特权级别(如内核态),处理器可以执行所有指令并访问所有资源。
    • 在低特权级别(如用户态),处理器只能执行非特权指令,访问受限的资源。

通过这些硬件机制,操作系统能够有效地保护自身不被用户程序破坏,同时也能控制用户程序的行为,确保系统的稳定和安全。

处理器的状态(模式)#

现代处理器通常将CPU状态划分为两种、三种或四种,在程序状态字寄存器PSW中设置位,根据运行程序对资源和指令的使用权限设置不同的CPU状态。

例如X86架构中的EFLAGS寄存器,RISC-V的三种特权模式:

  • 机器模式(M模式)
  • 用户模式(U模式)
  • 监管模式(S模式)

特权指令和非特权指令#

操作系统需要两种CPU状态:

  • 内核态(Kernel Mode):运行操作系统程序
  • 用户态(User Mode):运行用户程序

特权指令:只能由操作系统使用、用户程序不能使用的指令 非特权指令:用户程序可以使用的指令

X86支持4个处理器特权级别(特权环 Ring):R0、R1、R2和R3

  • R0相当于内核态,特权能力最高
  • R3相当于用户态,特权能力最低
  • 目前大多数基于x86处理器的操作系统只用了R0和R3两个特权级别

CPU状态之间的转换#

  • 用户态 → 内核态:唯一途径是通过中断/异常/陷入机制
  • 内核态 → 用户态:通过设置程序状态字PSW

陷入指令(访管指令, supervisor call):提供给用户程序的接口,用于调用操作系统功能 例如:int, trap, syscall, sysenter/sysexit, ecall

二、中断机制#

中断对于操作系统的重要性就如同汽车发动机、飞机引擎的作用,操作系统是由”中断驱动”或”事件驱动”的。

主要作用:

  • 及时处理设备发来的中断请求
  • 捕获用户程序提出的服务请求
  • 防止用户程序执行过程中的破坏性活动

中断/异常的概念#

CPU对系统发生的某个事件作出的一种反应:

  • CPU暂停正在执行的程序
  • 保留现场后自动转去执行相应事件的处理程序
  • 处理完成后返回断点,继续执行被打断的程序

特点:

  • 是随机发生的
  • 是自动处理的
  • 是可恢复的

中断/异常的来源及处理方式#

中断的来源

  • 外部设备:如键盘、鼠标、网络接口卡等外设发出的中断信号
  • 定时器:系统定时器发出的中断信号
  • 其他硬件部件:如硬盘、打印机等

异常的来源

  • 程序错误:如除零错误、非法指令、页面错误等
  • 系统调用:用户程序通过系统调用引发的陷入
  • 其他内部事件:如调试事件、断点等

处理方式

  • 中断处理:中断处理程序通常是操作系统的一部分,负责响应外部设备的请求。处理过程包括保存当前CPU状态、执行中断处理程序、恢复CPU状态并返回被中断的程序。
  • 异常处理:异常处理程序也由操作系统提供,负责处理程序运行过程中出现的错误或特殊事件。处理过程包括识别异常类型、执行相应的处理程序、根据异常类型决定是否返回被中断的程序或终止程序。

术语演化的历史背景#

  • 中断的引入:为了支持CPU和设备之间的并行操作

    • 当CPU启动设备进行输入/输出后,设备便可以独立工作,CPU转去处理与此次输入/输出不相关的事情;当设备完成输入/输出后,通过向CPU发中断报告此次输入/输出的结果,让CPU决定如何处理以后的事情
  • 异常的引入:表示CPU执行指令时本身出现的问题

    • 如算术溢出、除零、取数时的奇偶错,访存地址时越界或执行了“陷入指令”等,这时硬件改变了CPU当前的执行流程,转到相应的错误处理程序或异常处理程序或执行系统调用

事件#

事件可分为中断(外中断)和异常(内中断, 即下面三个表项):

类别原因异步/同步返回行为
中断(Interrupt)来自I/O设备、其他硬件部件异步总是返回到下一条指令
陷入(Trap)有意识安排的同步返回到下一条指令
故障(Fault)可恢复的错误同步返回到当前指令
终止(Abort)不可恢复的错误同步不会返回

中断/异常机制工作原理#

中断/异常机制是现代计算机系统的核心机制之一,通过硬件和软件相互配合,使计算机系统得以充分发挥能力:

硬件工作:中断/异常响应

  • 捕获中断源发出的中断/异常请求
  • 以一定方式响应
  • 将处理器控制权交给特定的处理程序

软件工作:中断/异常处理程序

  • 识别中断/异常类型
  • 完成相应的处理

在每条指令执行周期的最后时刻扫描中断寄存器,查看是否有中断信号。 若无中断信号,继续执行下一条指令。 若有中断,中断硬件将该中断触发器内容按规定编码送入PSW的相应位,称为中断码,通过交换中断向量引出中断处理程序。

硬件——中断响应过程示意

  1. 在每条指令执行周期的最后时刻,扫描中断寄存器。
  2. 检查是否有中断信号。
    • 若无中断信号,继续执行下一条指令。
    • 若有中断信号,中断硬件将中断触发器内容按规定编码送入PSW的相应位,称为中断码。
  3. 通过交换中断向量,引出中断处理程序。
  4. 硬件将处理器控制权交给特定的中断处理程序。
  5. 中断处理程序执行相应的中断处理任务。
  6. 中断处理完成后,恢复CPU状态并返回被中断的程序。

软硬协同——中断向量表#

中断向量:一个内存单元,存放中断处理程序入口地址和程序运行所需的处理机状态字

硬件执行流程按中断号/异常类型的不同,通过中断向量表转移控制权给中断处理程序

Linux中的中断向量(X86)

  • 0~19:不可屏蔽中断和异常
  • 20~31:Intel保留
  • 32~127:外部中断(IRQ)
  • 128(0x80):用于系统调用的可编程异常
  • 129~238:外部中断
  • 239:本地APIC时钟中断
  • 240:本地APIC高温中断
  • 241~250:Linux保留
  • 251~253:处理器间中断
  • 254:本地APIC错误中断
  • 255:本地APIC伪中断

中断响应流程#

  1. 设备发中断信号
  2. 硬件保存现场
  3. 根据中断码查表
  4. 把中断处理程序入口地址等推送到相应的寄存器
  5. 执行中断处理程序

上半部和下半部处理#

在 Linux 系统中,中断处理程序应该尽量短且快,以减少对正常进程调度的影响。然而,中断处理程序可能会暂时关闭中断,如果执行时间过长,可能会丢失其他设备的中断请求。为了解决这个问题,Linux 将中断过程分为上半部和下半部。

上半部用于快速处理中断,通常会暂时关闭中断请求,主要负责处理与硬件紧密相关或时间敏感的任务。下半部用于延迟处理上半部未完成的工作,一般以内核线程的方式运行。

上半部(Top Half)

  • 上半部是中断处理程序的第一部分,直接由硬件中断触发。
  • 其主要任务是快速响应中断,处理与硬件紧密相关或时间敏感的操作。
  • 上半部运行在中断上下文中,通常会暂时关闭中断,不能被阻塞,也不能进行复杂的操作。
  • 典型的上半部操作包括:读取硬件寄存器、清除中断源、调度下半部等。

下半部(Bottom Half)

  • 下半部是中断处理程序的第二部分,通常由上半部调度执行。
  • 其主要任务是延迟处理上半部未完成的工作,完成较为复杂和耗时的处理。
  • 下半部运行在进程上下文中,可以被阻塞,也可以进行复杂的操作。
  • 典型的下半部操作包括:数据处理、更新数据结构、唤醒等待的进程等。

例如,当网卡收到网络包后,通过 DMA 将数据写入内存,并通过硬件中断通知内核有新数据到达。内核调用中断处理程序,分为上半部和下半部。上半部会先禁止网卡中断,避免频繁硬中断降低内核效率,然后触发软中断,将耗时且复杂的任务交给软中断处理程序(下半部)处理,如解析网络数据并将其传递给应用程序。

为什么引入上半部和下半部处理?

  1. 提高响应速度:上半部只执行最紧急的操作,尽量缩短中断处理时间,使系统能够快速响应其他中断。
  2. 减少中断禁用时间:上半部运行在中断上下文中,系统在处理上半部时会禁用中断。通过将复杂操作移到下半部,可以减少中断禁用时间,提高系统的并发性。
  3. 分离紧急和非紧急任务:将紧急任务放在上半部,非紧急任务放在下半部,有助于合理分配系统资源,提高系统的整体性能和稳定性。

所以,中断处理程序的上半部和下半部可以理解为:

  • 上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
  • 下半部是由内核触发,也就是软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行。

还有一个区别,硬中断(上半部)是会打断 CPU 正在执行的任务,然后立即执行中断处理程序,而软中断(下半部)是以内核线程的方式执行,并且每一个 CPU 都对应一个软中断内核线程,名字通常为「ksoftirqd/CPU 编号」,比如 0 号 CPU 对应的软中断内核线程的名字是 ksoftirqd/0。

Ref: 软中断

软件——中断处理程序#

设计操作系统时,为每一类中断/异常事件编好相应的处理程序,并设置好中断向量表。系统运行时若响应中断,中断硬件部件将CPU控制权转给中断处理程序:

  1. 保存相关寄存器信息
  2. 分析中断/异常的具体原因
  3. 执行对应的处理功能
  4. 恢复现场,返回被事件打断的程序

中断/异常机制小结#

以设备输入输出中断为例:

  • 打印机给CPU发中断信号
  • CPU处理完当前指令后检测到中断,判断出中断来源并向相关设备发确认信号
  • CPU开始为软件处理中断做准备:
    • 处理器状态被切换到内核态
    • 在系统栈中保存被中断程序的重要上下文环境,主要是程序计数器PC、程序状态字PSW
  • CPU根据中断码查中断向量表,获得与该中断相关的处理程序的入口地址,并将PC设置成该地址,新的指令周期开始时,CPU控制转移到中断处理程序
  • 中断处理程序开始工作
    • 在系统栈中保存现场信息
    • 检查I/O设备的状态信息,操纵I/O设备或者在设备和内存之间传送数据等等
  • 中断处理结束时,CPU检测到中断返回指令,从系统栈中恢复被中断程序的上下文环境,CPU状态恢复成原来的状态,PSW和PC恢复成中断前的值,CPU开始一个新的指令周期

三、IA32 体系结构对中断的支持#

基本概念——X86处理器#

中断:由硬件信号引发的,分为可屏蔽和不可屏蔽中断

异常:由指令执行引发的,比如除零异常

  • 80x86处理器发布了大约20种不同的异常
  • 对于某些异常,CPU会在执行异常处理程序之前产生硬件出错码,并压入内核态堆栈

系统调用:异常的一种,用户态到系统态的唯一入口

IA32体系结构对中断的支持#

中断控制器(PIC或APIC)

  • 负责将硬件的中断信号转换为中断向量,引发CPU中断

实模式:中断向量表(Interrupt Vector)

  • 存放中断服务程序的入口地址
  • 不支持CPU运行状态切换
  • 中断处理与一般的过程调用相似

保护模式:中断描述符表(Interrupt Descriptor table)

  • 采用门(gate)描述符数据结构描述中断向量
  • 表项包含四种类型门描述符:
    • 任务门(Task Gate)
    • 中断门(Interrupt Gate)
    • 陷阱门(Trap Gate)
    • 调用门(Call Gate)

中断向量表/中断描述符表

  • 表项包含四种类型门描述符:
    • 任务门(Task Gate)
      • 中断发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中(Linux没有使用任务门)
    • 中断门(Interrupt Gate)
      • 给出段选择符 (Segment Selector)、中断/异常程序的段内偏移量 (Offset)
      • 通过中断门后系统会自动禁止中断
    • 陷阱门(Trap Gate)
      • 与中断门类似,但通过陷阱门后系统不会自动关中断
    • 调用门(Call Gate)

中断/异常的硬件处理过程

  1. 确定与中断或异常关联的向量i
  2. 通过IDTR寄存器找到IDT表,获得中断描述符 (表中的第i个表项)
  3. 从GDTR寄存器获得GDT的地址,结合中断描述符中的段选择符,在GDT表获取对应的段描述符
  4. 特权级检查
  5. 检查是否发生了特权级的变化,如需要则进行堆栈切换
  6. 硬件压栈,保存上下文环境
  7. 如果是中断,清IF位
  8. 通过中断描述符中的段内偏移量和段描述符中的基地址,找到中断/异常处理程序的入口地址,执行其第一条指令

四、系统调用(System call)#

系统调用是用户在编程时可以调用的操作系统功能:

  • 系统调用是操作系统提供给编程人员的唯一接口
  • 使CPU状态从用户态陷入内核态

每个操作系统都提供几百种系统调用,包括进程控制、进程通信、文件使用、目录操作、设备管理、信息维护等。

经典问题:系统调用与C函数调用的区别?#

  1. 系统调用

    • 定义:系统调用是操作系统提供给用户程序的接口,用于执行特权操作,如文件操作、进程控制、内存管理等。
    • 执行环境:系统调用会导致CPU从用户态切换到内核态,执行内核中的代码。
    • 实现方式:通过特定的陷入指令(如int 0x80)触发中断或异常,进入内核态执行相应的系统调用服务例程。
    • 开销:由于涉及用户态到内核态的切换,系统调用的开销较大。
  2. C函数调用

    • 定义:C函数调用是程序内部的函数调用,用于实现特定的功能或算法。
    • 执行环境:C函数调用在用户态执行,不涉及特权操作。
    • 实现方式:通过函数调用指令(如call)在程序内部跳转到函数的入口地址执行。
    • 开销:C函数调用的开销较小,因为不涉及用户态和内核态的切换。

例如,printf函数是一个C库函数,它最终会调用系统调用write来将数据输出到终端。printf函数本身在用户态执行,而write系统调用会切换到内核态执行实际的输出操作。

静态:系统调用机制的设计#

机制与策略分离原则指导下的系统调用设计:

  1. 中断/异常机制:支持系统调用服务的实现
  2. 陷入指令:引发异常,完成用户态到内核态的切换
  3. 系统调用号和参数:每个系统调用都事先给定一个编号(功能号)
  4. 系统调用表:存放系统调用服务例程的入口地址

静态:参数传递过程问题#

怎样实现用户程序的参数传递给内核?常用的3种实现方法:

  • 由陷入指令自带参数:陷入指令的长度有限,只能自带有限的参数
  • 通过通用寄存器传递参数:寄存器的个数会限制传递参数的数量
  • 在内存中开辟专用堆栈区来传递参数

数据段部分

section .data
output:
    ascii "Hello!\n"
output_end:
equ len, output_end - output
asm
  1. section .data:定义数据段,用于存放程序中的数据
  2. output::定义一个标签,表示数据的起始位置
  3. ascii "Hello!\n":定义一个ASCII字符串”Hello!”,后跟换行符
  4. output_end::定义另一个标签,表示数据的结束位置
  5. equ len, output_end - output:定义一个常量len,其值为output_endoutput之间的字节数,即字符串的长度

代码段部分

section .text
globl _start
_start:
    movl $4, %eax     #eax存放系统调用号
    movl $1, %ebx
    movl $output, %ecx
    movl $len, %edx
    int $0x80         #引发一次系统调用
end:
    movl $1, %eax     #1这个系统调用的作用?
    movl $0, %ebx
    int $0x80
asm
  1. section .text:定义代码段,用于存放程序的指令
  2. globl _start:声明_start标签为全局的,使链接器能够找到程序的入口点
  3. _start::程序的入口点
  4. movl $4, %eax:将4放入eax寄存器,4是Linux系统调用表中write函数的调用号
  5. movl $1, %ebx:将1放入ebx寄存器,1代表标准输出文件描述符
  6. movl $output, %ecx:将output字符串的地址放入ecx寄存器,作为要输出的数据
  7. movl $len, %edx:将len(字符串长度)放入edx寄存器
  8. int $0x80:触发中断0x80,执行系统调用,这里执行的是write(1, “Hello!\n”, 7)
  9. movl $1, %eax:将1放入eax寄存器,1是Linux系统调用表中exit函数的调用号
  10. movl $0, %ebx:将0放入ebx寄存器,作为exit()的参数,表示程序正常退出(返回值为0)
  11. int $0x80:再次触发中断0x80,执行系统调用exit(0),终止程序

动态:系统调用的执行过程#

当CPU执行到特殊的陷入指令时:

  1. 中断/异常机制:硬件保护现场;通过查中断向量表把控制权转给系统调用总入口程序
  2. 系统调用总入口程序:保存现场;将参数保存在内核堆栈里;通过查系统调用表把控制权转给相应的系统调用处理例程或内核函数
  3. 执行系统调用例程
  4. 恢复现场,返回用户程序

五、Linux系统调用实现#

基于x86体系结构的Linux系统调用实现:

  • 陷入指令选择int $0x80
  • 门描述符:系统初始化时对IDT表中的第128号门初始化
  • 门类型:15,陷阱门:陷阱门不会自动屏蔽中断,允许在处理系统调用时继续响应其他中断,提高系统的并发性和响应速度。
  • DPL:3,与用户级别相同,允许用户进程使用该门描述符

系统调用号示例#

# define __NR_exit 1
# define __NR_fork 2
# define __NR_read 3
# define __NR_write 4
# define __NR_open 5
# define __NR_close 6
# define __NR_waitpid 7
# define __NR_creat 8
# define __NR_link 9
# define __NR_unlink 10
# define __NR_execve 11
# define __NR_chdir 12
# define __NR_time 13
...
c

系统执行 int $0x80 指令#

  1. 特权级的改变:由于从用户态切换到内核态,CPU需要切换栈。

    • 用户栈切换到内核栈:CPU从任务状态段(TSS)中装入新的栈指针(SS:ESP),指向内核栈。
  2. 保存用户态信息:用户栈的信息(SS:ESP)、EFLAGS、用户态CS、EIP寄存器的内容会被压栈,以便返回时使用。

    • 将EFLAGS压栈后,复位TF(陷阱标志),IF(中断标志)位保持不变。
  3. 查找IDT:使用128在中断描述符表(IDT)中找到对应的门描述符,从中找出段选择符装入代码段寄存器CS。

    • 代码段描述符中的基地址加上陷阱门描述符中的偏移量,定位到system_call的入口地址。
  4. 特权级检查:代码只能访问相同或较低特权级的数据。

    • 确保系统调用在内核态执行,防止用户态代码直接访问内核数据。
  5. 系统调用号和参数传递

    • 系统调用号:通过EAX寄存器传递。
    • 系统调用参数:通过EBX、ECX、EDX、ESI、EDI寄存器传递。
  6. 执行系统调用:根据系统调用号,查找系统调用表,找到对应的系统调用处理例程并执行。

    • 处理完成后,将结果放入EAX寄存器,并通过ret_from_sys_call例程返回用户态程序。

Linux系统调用执行流程#

应用程序 封装例程 陷入处理 内核函数
bash
  1. 用户态下调用C库的库函数,比如write()
  2. 封装后的write()先做好参数传递工作,然后使用int 0x80指令产生一次异常
  3. CPU通过0x80号在IDT中找到对应的服务例程system_call(),并调用之
  4. system_call()将参数保存在内核栈;根据系统调用号索引系统调用表,找到系统调用程序入口,比如sys_write()
  5. sys_write()执行完后,经过ret_from_sys_call()例程返回用户程序

示例:系统调用的参数传递#

系统调用使用寄存器传递参数,要传递的参数包括:

  • 系统调用号
  • 系统调用所需的参数

用于传递参数的寄存器有:

  • eax用于保存系统调用号和系统调用返回值
  • 系统调用参数保存在ebx, ecx, edx, esi和edi中,参数个数不超过6个

进入内核态后,system_call再将这些参数保存在内核堆栈中。

假如C库中封装的系统调用号3的函数原型如下:

movl 0x8(%esp), %ecx  # 将用户态堆栈中的para2放入ecx
movl 0x4(%esp), %ebx  # 将用户态堆栈中的para1放入ebx
movl $0x3, %eax       # 系统调用号保存在eax中
int $0x80             # 引发系统调用
movl %eax, errno      # 将结果存入全局变量errno中
movl $-1, %eax        # eax置为-1,表示出错
asm

则调用时,参数传递如下:

  • eax = 3
  • ebx = para1
  • ecx = para2

系统调用小结#

  • 系统调用:用户在程序中调用操作系统提供的一些子功能
  • 一种特殊的过程调用,由特殊的机器指令实现(每种机器的指令集都支持—访管指令)
  • 系统调用是操作系统提供给编程人员的唯一接口
  • CPU状态从目态转入管态
  • 利用系统调用,可以动态请求和释放系统资源,完成与硬件相关的工作以及控制程序的执行等
  • 每个操作系统都提供几百种系统调用(POSIX标准)
  • 系统调用与C函数调用的区别?
  • 完成系统调用机制的运行需要什么条件(准备工作)?
    • 静态 和 动态
    • 封装内核函数 - 库函数(API);访管指令与陷入机制;编译器;操作系统(初始化、系统调用编号及参数;系统调用表)
    • 陷入内核,总入口程序,保存现场(压栈),查表分派,执行返回
[中文] Operating Systems Notes: 02 - 中断异常机制
https://www.lyt0112.com//blog/operating_systems_note_2-zh
Author Yutong Liang
Published at March 13, 2025
Copyright CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨