Appearance
系统调用
2026 大纲 一(三)3 系统调用。
用户程序主动求助,走的是哪扇门
上一节的中断解决了"OS 怎么把 CPU 要回来",但那都是被动的——设备完成了、时钟到点了、 程序出错了。还有一种情况完全相反:用户程序自己想让 OS 帮忙做件事。
它做不了。程序的运行空间分为内核空间和用户空间、逻辑上互相隔离, 应用程序既不能直接访问内核数据,也无法在用户空间中直接执行服务例程的代码。 想读磁盘?I/O 指令是特权指令。想创建进程?PCB 在内核空间里。
所以只剩一条路:发出请求,让内核代为执行。这就像你不能自己进金库拿钱, 只能填单子让柜员操作——柜员既核对你的身份,也不需要你懂金库的编号规则。 这条路就是系统调用,它是用户程序请求 OS 服务的唯一途径, 存在的根本原因是保护,起的作用是裁决(按权限和规则审查资源访问) 与抽象(提供一致接口)。
它复用了上一节的硬件机制:陷入内核这件事,和缺页异常用的是同一套电路。 差别只在谁发起——异常是被动撞上的,系统调用是主动敲门的。 所以"凡是进内核的都是系统调用"这句话是错的,两者只是共用了同一道门。
这一节要说清三件事:这扇门怎么开(陷入指令为什么不是特权指令)、 怎么告诉内核你要办什么(功能号而不是地址)、东西怎么递进去(参数传递的三条路)。
一、概念与三个教材术语
同一件事,不同教材用了三个名字,题干里三个都可能出现:
| 术语 | 指什么 | 注意 |
|---|---|---|
| 陷入指令(trap) | 由于系统调用而引起处理器中断的机器指令,如 int 0x80 / syscall | 它是非特权指令,在用户态下执行时会产生 CPU 模式切换 |
| 访管指令 | 陷入指令的另一种叫法,"访问管理程序"之意 | 库函数封装所做的工作,就是"将系统调用所用的参数放在合适位置,然后通过执行访管指令来陷入内核" |
| 广义指令 | 有的教材把程序级接口提供的这组调用称为广义指令 | 与"系统调用""程序请求"是同一件事的不同叫法 |
旧称"管态/目态"与"内核态/用户态"的对应是:管态即内核态,目态即用户态,"访管"就是从目态请求进入管态。
二、系统调用的分类
| 类别 | 功能 | 示例 |
|---|---|---|
| 进程管理(进程控制) | 创建和撤销进程、终止或异常终止、阻塞和唤醒、挂起和激活、监视和追踪、获取与设置进程属性 | fork exec exit wait |
| 文件管理 | 建立、删除、打开、关闭、读写、链接、控制文件,显示与设置文件属性 | open close read write |
| 设备管理 | 申请设备、释放设备、设备 I/O 与重定向、获取与设置设备属性、控制与检查设备状态 | ioctl |
| 存储管理 | 申请和释放主存 | brk mmap |
| 进程通信 | 建立与断开通信连接、发送与接收消息、连接与断开共享主存、套接字操作 | pipe shmget socket |
| 信息维护 | 获取与设置日期时间、获取与设置系统数据、生成诊断与统计数据 | getpid time |
有的教材把"存储管理"并入进程管理而写成五类。分类粒度不是重点,重点是这条判据:凡是需要动内核管理的资源(CPU、主存、设备、文件、内核数据结构)的操作,都必须走系统调用。
判断一件事是不是系统调用:先问谁发起
上面那条判据只答了一半。完整的判据要问两个问题,两个都得是"是":
- 是不是由用户进程主动发起的? OS 自己在内部完成的不算。
- 它需不需要内核权限? 用户态自己就能算完的不算。
按这两问过一遍就能把常见的混淆项分开:
- 创建新进程(
fork/CreateProcess):用户进程发起,且要分配 PCB、建立地址空间、 设置文件描述符表——是系统调用。 - 页置换:缺页时由 OS 内部自动完成,用户进程从头到尾没提过这个请求——不是。
- 进程调度:由时钟中断等事件驱动,OS 内部决定——不是。
- 生成随机整数:
rand()在用户态算就行,不需要内核——不是,它只是普通库函数。
第一问筛掉的是"内核内部活动",第二问筛掉的是"纯用户态计算"。 页置换和进程调度是这一节被反复用作错项的两个,因为它们听起来"很内核"—— 但"发生在内核"和"由用户请求"是两回事。
系统调用接口不跨操作系统统一
这一条容易想当然。Linux、Windows、macOS 的调用号、参数约定、甚至同名调用的语义都不一样。 POSIX 只是一份规范,且并非所有 OS 都遵循,Windows 完全是另一套。
跨 OS 统一的是库函数那一层(C 标准库、各语言运行时把差异吸收掉了), 不是系统调用这一层。所以"不同的操作系统为应用程序提供了统一的系统调用接口"是错的。
三、执行过程:哪一步跨过了模式位
跨越模式位的只有两步,而且是不对称的(这一条与 CPU 运行模式相互印证):执行陷入指令那一刻由硬件自动置成内核态——若由软件改,用户程序就能自己升权、隔离失效;执行中断返回指令那一刻由软件(一条特权指令)改回用户态——只有已经在内核态的代码才能执行它。
四、参数怎么传进内核
| 方式 | 怎么做 | 判据:什么时候用它 | 局限 |
|---|---|---|---|
| ① 陷入指令自带参数 | 规定陷入指令之后的若干单元存放参数(直接参数);或紧邻单元存放参数的地址(间接参数),由间接地址指出参数存放区 | 参数个数固定且极少、指令格式允许携带 | 受指令格式限制;参数写死在代码里,不便于动态变化 |
| ② 通过 CPU 通用寄存器传递 | 系统调用号和各参数分别放入约定的通用寄存器 | 参数少、要求快——最常用 | 不适用于传递大量参数,寄存器数量有限 |
| ②′ 寄存器传地址(②的改进) | 参数存在主存的某个区或表中,把该区的首地址送入寄存器 | 参数多,或参数是结构体/缓冲区 | 内核需要额外一次访存去取;且必须校验这个地址是否合法 |
| ③ 主存中开辟专用堆栈区传递 | 参数按约定压入一块专用堆栈区 | 参数个数不定、需要嵌套传递 | 需要维护栈区,实现相对复杂 |
②′ 是理解这套设计的关键:寄存器又快又安全(内核直接就能读),但装不下大量数据;于是退一步——寄存器不装数据本身,只装"数据在哪",把容量问题甩给内存,同时保留寄存器"内核一定能直接读到"的好处。代价是内核必须先校验这个用户给的地址是否落在该进程的合法空间内,否则用户就能借内核之手读写任意内存。
结果也要放在双方都能访问且约定好的地方(通常仍是寄存器或用户事先传进来的缓冲区),因为调用返回时进程已切回用户态、内核栈上的一切对它不可见。这也是 read(fd, buf, n) 为什么要由调用者提供 buf:内核不能在自己的空间里造一块内存再"交给"用户。
参数里放的是句柄,不是名字
read 的原型是 read(int fd, void *buf, size_t count)——三个参数里没有文件名。
原因是按名字找文件这件事只做一次:open 那一次已经把路径解析完、 把该文件在内核里的表项建好了,返回给用户的 fd 就是这张表的下标。此后每次 read 只要拿 fd 一索引就够了,再传一次文件名等于让内核把目录查找重做一遍。
这是内核接口的通用做法:第一次调用用名字换一个句柄,后续调用都用句柄。 文件如此,进程用 PID、共享内存用 shmid 也是如此。
顺带说清 read 的另一个性质:它是同步阻塞的。若要读的数据不在内存里, 当前进程会被挂起进入阻塞态,直到磁盘 I/O 完成才被唤醒—— 这是"运行 → 阻塞"这条状态转换最典型的触发场景,进程状态与转换会再讲一遍。
五、系统调用与库函数
| 比较 | 系统调用 | 库函数 |
|---|---|---|
| 执行环境 | 内核态 | 用户态(内部可能再调用系统调用) |
| 提供者 | 操作系统内核 | 语言运行库(如 C 标准库) |
| 调用形式 | 按功能号调用 | 按转向地址调用 |
| 开销 | 需要态切换、现场保护,开销大 | 普通过程调用,开销小 |
| 举例 | read write fork | printf malloc fopen |
库函数封装的价值是"隐藏访管指令的细节,使系统调用更像过程调用"——它把"设置功能号、按约定摆放参数、执行陷入指令、解读返回码"这一串动作包成一次普通函数调用。但库函数属于用户程序,不属于系统调用程序。判断某个操作会不会陷入内核,判据是"这件事需不需要动内核管的资源":
| 操作 | 是否触发系统调用 | 理由 |
|---|---|---|
| 读写文件、网络 socket | 是 | 涉及设备/资源管理,必须陷入内核 |
| 创建/终止进程、线程 | 是 | 涉及 PCB/TCB 等内核数据结构 |
申请大块内存(malloc 大对象) | 是(间接) | 触发 brk/mmap 系统调用 |
| 申请小块内存(命中已有堆) | 否 | 用户态分配器在已映射的堆里切块 |
| 获取系统时间、PID | 是 | gettimeofday / getpid 是系统调用 |
字符串处理(strlen/strcpy) | 否 | 纯计算,全在用户态 |
printf 调用 | 不一定 | 命中行缓冲时不刷新→不触发;遇 \n 或缓冲满时触发 write |
数学运算(sin/sqrt) | 否 | 纯用户态计算 |
| 浮点除零 / 访存越界 | 不是系统调用 | 但是异常,机制上同样陷入内核 |
六、系统调用与中断的关系
| 概念 | 层次 |
|---|---|
| 系统调用 | OS 提供的功能接口(做什么) |
| 陷入指令 / 访管指令 | 触发系统调用的机制(怎么发起) |
| 中断/异常 | 实现态切换的底层硬件机制(凭什么能切) |
三者是同一件事的三层描述,不是三样并列的东西。
把"功能号换成内核入口地址"这个设想推到底,会塌在哪(想弄清这层间接寻址换来了什么时展开)
既然最终还是要跳到那个地址,为什么不让用户程序直接把地址写在陷入指令里,省掉查表这一步?推导一遍:
- 如果用户能指定内核地址,隔离就作废了。 用户程序可以填一个内核代码段中间的地址——绕过入口处的参数校验、权限检查,直接跳到检查之后的那几条指令上执行。特权隔离的全部意义在于"用户只能从内核允许的入口进来",指定地址等于允许任意入口。
- 编号是可校验的,地址不是。 内核收到功能号只需做一次范围检查:
,越界直接拒绝。而一个 32/64 位地址,内核无法判断它"是不是一个合法的服务例程起点"——所有地址在数值上看起来都一样。 - 编号提供了稳定的二进制接口。 内核升级后服务例程的地址会变,但功能号可以保持不变,老程序不必重新编译。若接口是地址,每次内核更新都会让所有已编译的程序失效。
- 查表的代价可以忽略。 一次乘加加一次访存,与随后进入内核所做的现场保护相比微不足道。
一句话:功能号是一层"只允许从正门进"的间接寻址,用一次查表换来了入口的完全可控。这与中断用中断号查向量表、而不是让设备直接给出处理程序地址,是同一个设计思想。
考点速记
- 系统调用是请求 OS 服务的唯一途径,根本原因是保护,作用是裁决 + 抽象。它是 OS 提供给应用程序的接口——中断是硬件机制、库函数在用户那一侧、原语是内核内部零件,都不是"接口"。
- 陷入指令是非特权指令(否则用户发不出请求);跨越模式位只有两步且不对称——上行由硬件、下行由一条特权指令
iret。所以"完成时导致内核态转用户态"的正是系统调用的返回。 - 完整流程:摆放参数与功能号 → 执行陷入指令 → 硬件存断点并置内核态 → 现场保护到核心栈 → 按功能号查入口地址表 → 执行服务例程 → 恢复现场 → 返回系统调用的下一条指令。⚠️传参在陷入之前,因为陷入之后栈就换成核心栈了。
- 判断是不是系统调用问两条:由用户进程主动发起 + 需要内核权限。页置换、进程调度是内核内部活动(第一条不满足),
rand()是纯用户态计算(第二条不满足)。 - 系统调用接口不跨 OS 统一,统一的是库函数那一层。
- 参数里传的是句柄不是名字(
read用fd),因为按名查找在open时已经做完了。 - 系统调用在内核态执行、库函数本身在用户态执行;异常也陷入内核但不是系统调用,分界在主动请求还是被动发生。
这一节在真题里被考过的形式:
问法高度模式化——不是问"是什么",就是问"哪些是/哪些不是", 再就是问顺序。三种问法各有固定的判据。
- 问 OS 提供给应用程序的接口是什么(2010-23)。答系统调用。四个选项分别在四个层次上:中断是硬件机制、库函数在用户那一侧、原语是内核内部不对外的零件。⚠️ 最容易误选库函数——
printf确实是应用程序直接调的,但它调的是库,write才是接口。 - 判断系统调用的四条叙述(2019-25)。答服务程序执行时在内核态 + 避免用户直接访问外设 + 是内核为应用提供服务的接口三条;"不同 OS 提供统一的系统调用接口"是错的。⚠️ 这一条是本节唯一一个"跨系统"性质的考点。
- 问哪个操作通过系统调用完成(2021-32)。答创建新进程。⚠️ 干扰项是页置换和进程调度——它们发生在内核,但不是用户发起的,所以不是系统调用。
- 问四步操作的正确顺序(2017-24)。答传参 → 陷入 → 执行服务程序 → 返回用户态。⚠️ 唯一的坑是把传参放到陷入之后:陷入之后栈指针已经指向核心栈,用户压的参数内核看不见。
- 判断
read系统调用过程的三条叙述(2012-28)。答数据不在内存则进程睡眠 + 会导致态切换两条成立;"参数应包含文件名"不成立,参数是fd。⚠️ 这道题是唯一一道深入到具体调用签名的,判据是"按名查找只在open做一次"。 - 问哪个操作完成时导致内核态转用户态(2023-26)。答执行系统调用。阻塞进程、唤醒进程完成后仍在内核态;CPU 调度完成后是什么态取决于选中的进程,不确定。
复习优先级:必须拿满,判据固定、没有推导量。三条判据就够—— 接口在哪一层(分清系统调用/中断/库函数/原语)、是不是系统调用问两条 (谁发起 + 要不要内核权限)、流程顺序里传参在陷入之前。 参数传递的三种方法至今只考过"传的是 fd 不是文件名"这一处, 理解②′那个"寄存器只装地址"的设计即可,三种方法的细节不必死记。
易错:把库函数当成 OS 提供给应用程序的接口。库函数在用户那一侧,它内部调的系统调用才是接口。
易错:认为不同操作系统的系统调用接口是统一的。统一的是库函数层,不是系统调用层。
易错:把页置换、进程调度当成系统调用。它们发生在内核,但不是用户进程发起的。
易错:把传参排到陷入指令之后。陷入后栈指针已指向核心栈,内核看不到用户压的参数。
易错:认为
read的参数里有文件名。参数是open换来的fd;按名查找只在open时做一次。
易错:认为"凡是进内核的都是系统调用"。缺页、除零同样陷入内核,但它们是被动发生的异常。
易错:把陷入指令当成特权指令。它必须能在用户态执行;
iret才是特权指令。
教材出处
- 孙钟秀、费翔林《操作系统教程》(第 6 版)1.3.5 系统调用(p25~p26):系统调用之所以存在"其根本原因是对系统进行'保护'",及其两点作用(权限裁决、资源抽象);系统调用按功能分成六类;"由于系统调用而引起处理器中断的机器指令称为陷入指令(trap),它是非特权指令";系统调用实现的三个要点(服务例程、入口地址表、陷入处理机制);参数传递的三种方法——陷入指令自带参数(直接参数/间接参数)、通过 CPU 通用寄存器传递(大量参数时改为在主存的区或表中存放参数、首地址送寄存器)、在主存中开辟专用堆栈区传递;以及"函数按转向地址调用,系统调用按功能号调用"。
- 同书 2.1 处理器·用户栈和核心栈(p44):进程有用户栈和核心栈两个栈,但硬件栈指针仅一个,随程序执行与处理器状态转换而改变取值。
- 同书 2.2.2 中断源·自愿性中断事件(p46):系统调用的共性处理流程(执行陷入指令并指明系统调用号 → 通过系统陷入机制进入处理程序、现场保护到核心栈 → 查系统调用入口表 → 执行服务例程,结束后返回系统调用的下一条指令)。
- 同书 4.x 用户空间的 I/O 软件(p134):封装函数"将系统调用所用的参数放在合适位置,然后通过执行访管指令来陷入内核"。
- 汤小丹《计算机操作系统》9.x 系统调用与库函数(p297):"库函数的目的是隐藏访管指令的细节,使系统调用更像过程调用……但一般地说,库函数属于用户程序而非系统调用程序。"
相关知识
中断和异常的处理|CPU运行模式(内核态与用户态)|程序的链接与装入