系统调用
系统调用是用户程序请求内核提供受保护服务的接口,例如访问文件、建立网络连接或创建进程。程序不能像调用普通函数一样任意执行内核代码,而要通过规定入口、参数和权限检查。进入内核改变的是执行权限,不必然换成另一个线程;这一区别是分析 I/O 和性能成本的基础。
1. 面试核心
先记住这几条,再读正文- 用户态与内核态是受硬件约束的执行权限状态,系统调用通过受控入口请求内核服务,不能绕过内核对地址、资源和权限的检查。卡 · 权限边界与入口
- 库函数是用户态的软件接口,一次库函数调用可能没有、一次或多次系统调用;不能用函数名直接计数内核进入次数。
- 系统调用通常仍在原线程的执行链路中完成,只有发生调度才会切换当前运行任务。卡 · 特权切换与调度
- 系统调用是主动请求,异常通常与当前指令同步,外部中断通常来自异步事件,三者都可能进入内核,但触发原因不同。卡 · 异常、中断与输出链路
printf先格式化并处理用户态缓冲,之后才可能进入写入链路;一次 printf 不等于立即刷新屏幕。卡 · 库函数与系统调用
2. 权限边界与入口
用户态运行普通应用,内核态允许执行操作系统所需的特权操作。这样,一个应用不能随意修改页表、访问其他进程的内存或直接控制所有设备。具体硬件可以有多级权限,操作系统如何使用这些级别由架构与系统决定;不要把某一平台的级别编号当成通用定义。
程序需要读取文件时,提交的是一个请求。典型链路是:
- 用户代码调用库封装,按照当前架构的系统调用 ABI 准备调用编号与参数。
- 执行规定的入口指令,硬件和内核入口代码建立相应的受保护执行状态,保存必要的返回信息。
- 内核识别请求,检查描述符、用户地址、长度和访问权限,再执行操作。
- 返回结果或错误信息,恢复用户态执行;返回前也可能处理信号等待办工作。
ABI 是二进制接口约定,这里规定调用号、参数如何传递、返回值在哪里等。x86-64、AArch64 等架构的指令与寄存器约定不同,甚至同一系统的不同 ABI 也可能不同。业务程序通常使用 libc 等库封装,不应随意硬编码调用号。
进入内核不意味着用户地址立即可信。用户传来的指针可能越界、不可访问,数据也可能在检查与使用之间被其他线程改变。内核需要使用合适的访问和复制机制,并在整个操作中维护安全边界。
3. 库函数与系统调用
库函数是一个软件接口,可以完全在用户态实现,也可以封装内核服务。例如计算字符串长度不需要为了每个字符进入内核;文件 I/O 接口则通常最终需要内核参与。
printf 不是 Linux 系统调用。它先解析格式、生成字节并处理 FILE 流的缓冲,必要时再通过底层写入接口提交数据。缓冲方式与流的目标有关,重定向到文件和连接终端时不能假定行为完全相同。
以 POSIX 的 write 接口为例:
#include <unistd.h>
int main(void) {
const char message[] = "ok\n";
ssize_t written = write(STDOUT_FILENO, message, sizeof(message) - 1);
return written == (ssize_t)(sizeof(message) - 1) ? 0 : 1;
}
它尝试把 3 个字节交给标准输出描述符关联的对象,并检查实际返回数量。返回的字节数可能小于请求长度,也可能发生错误;这个最小例子以非零退出码报告未完整写入。短写与就绪等待的关系,见I/O 模型。
在有 strace 的 Linux 环境,可以观察实际请求:
cc -std=c11 output.c -o output-demo
strace -e trace=write ./output-demo
这个程序的输出写入通常能看到类似 write(1, "ok\n", 3) = 3 的记录。跟踪工具看到的是相应内核接口活动,并不是源码里的所有函数调用。系统还可能通过 vDSO 等方式让部分服务在用户态完成,所以“调用了某个 libc 函数”也不能直接推出发生了特权切换。
4. 特权切换与调度
特权切换改变当前代码的执行权限,任务上下文切换则保存一个任务的执行状态并恢复另一个任务。它们可能相邻发生,但不是同一件事。
如果内核能立即完成请求,当前线程可以进入内核、执行操作,再直接返回用户态。若请求需要等待数据或资源,线程可能进入等待状态,调度器让另一个就绪任务运行。数据就绪后,等待线程被唤醒,也要再次获得运行机会才能继续。
| 情况 | 必然进入内核吗 | 必然调度到另一个线程吗 |
|---|---|---|
| 普通用户态函数调用 | 否 | 否 |
| 经内核入口执行系统调用 | 是 | 否 |
| 阻塞系统调用等待资源 | 是 | 可能交出执行机会,取决于具体执行过程 |
| 调度器选择其他任务 | 已在可进行调度的内核执行路径 | 这时才发生任务切换 |
不能用固定倍数概括每次系统调用的成本。入口出口、参数检查、复制、锁竞争、缓存状态和真正的 I/O 工作都会影响耗时。批量处理可以减少调用次数,但批量过大也可能增加等待与内存成本,应结合实际测量。进程与线程共享哪些执行资源,见进程与线程。
5. 异常、中断与输出链路
异常通常由正在执行的指令触发,例如缺页、非法指令或调试断点;外部中断通常来自设备、定时器等相对于当前指令流的异步事件。系统调用则是程序主动请求的服务入口。硬件对这些事件的术语和具体分类可能不同,解释时应先说明讨论的平台。
| 事件 | 典型触发 | 处理后可能结果 |
|---|---|---|
| 系统调用 | 应用请求读写文件、创建任务等 | 返回结果、错误或进入等待 |
| 缺页异常 | 当前内存访问缺少所需映射或权限 | 建立合法映射后重试,或报告非法访问 |
| 定时器/设备中断 | 时钟或设备状态变化 | 更新内核状态、唤醒任务、按条件调度 |
| 调试异常 | 执行断点或满足调试条件 | 交由调试处理,可能暂停被调试任务 |
缺页并不都代表程序错误,例如按需分配和写时复制都可能依靠缺页处理,详见虚拟内存。外部中断也不只会在用户态发生,CPU 正在执行内核代码时同样可能收到中断。
将这些机制串到“printf 如何输出到屏幕”时,需要先确认标准输出实际连到哪里:
- 格式化与流缓冲属于库层;缓冲刷新后,底层写入把字节交给描述符关联的内核对象。
- 如果输出重定向到文件,链路走文件系统和相应存储;不会因为函数叫
printf就自动显示在屏幕上。 - 如果连接终端或伪终端,还可能经过终端驱动、终端模拟器进程、图形系统与显示设备;内核接受字节不等于用户已经看到画面。
因此,用户态缓冲、内核接受、设备完成和可见显示是不同边界。系统调用返回成功,也不能一概推断数据已持久化到存储介质,或另一端已经完成业务处理。
6 · 面试题卡片
本页题目与参考回答
6.1 为什么需要用户态与内核态,系统调用如何跨越边界?
考察重点:硬件权限、受控入口和参数验证。
相关内容:权限边界与入口。
参考回答
权限分离限制普通应用直接操作受保护资源,系统调用提供受控的服务请求入口。 用户代码按 ABI 准备调用号与参数,通过规定入口交给内核;内核检查地址、描述符和权限,执行后返回结果或错误。进入内核不使用户输入自动可信,也不允许应用任意跳转到内核内部地址执行。
6.2 一次系统调用是否一定发生线程上下文切换?
考察重点:特权切换与调度切换。
相关内容:特权切换与调度。
参考回答
不一定,系统调用可以由原线程进入内核、完成操作后返回。 只有阻塞、抢占等条件导致调度器选择其他任务时,才切换任务上下文。入口出口仍有状态保存和检查成本,但不能因此把每次系统调用都算作线程切换。
6.3 操作系统中的中断是什么,有哪些触发来源?
考察重点:主动请求、同步异常与异步事件。
相关内容:异常、中断与输出链路。
参考回答
外部中断通常由设备或定时器等异步事件触发,使处理器进入相应处理路径,保存必要状态并执行处理程序。 分类时还要区分由当前指令同步触发的异常,以及应用主动发起的系统调用;不同架构对广义中断、异常和陷入的术语并不完全相同。中断也能发生在内核执行期间,不必然造成进程切换,不能把一次调试断点代表所有中断。
6.4 printf 到屏幕输出之间有哪些环节?
考察重点:库缓冲、描述符目标、内核与显示边界。
相关内容:库函数与系统调用、异常、中断与输出链路。
参考回答
先由库完成格式化和流缓冲,刷新时再通过底层写入接口进入内核,之后取决于描述符指向的目标。 重定向文件会走文件系统;终端输出可能继续经过终端或伪终端、终端模拟器与图形显示链路。一次 printf 不保证一次 write,也不保证立即可见;内核接受数据与最终显示、持久化是不同完成条件。
7. 参考资料
- Linux man-pages:syscall(2)
- Linux man-pages:write(2)
- Linux man-pages:stdio(3)
- Linux Kernel:Entry/exit handling
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明