先做个实验,给「操作系统是干嘛的」这个问题一个肉眼可见的答案。写一个最小 C 程序,只打印一行 hello,然后用 strace 看它到底对系统提了多少请求:
// hello.c
#include <stdio.h>
int main(void) { printf("hello\n"); return 0; }
$ gcc hello.c -o hello
$ strace ./hello
execve("./hello", ["./hello"], ...) = 0 ← 把程序装进内存
mmap(NULL, 8192, ..., PROT_READ, ...) = ... ← 建立若干内存映射
openat(AT_FDCWD, "/lib/x86_64...libc.so.6"...) ← 找到 C 库加载
...
write(1, "hello\n", 6) = 6 ← 真正打印的那一下
exit_group(0) ← 退出
一个「只打印一行字」的程序,运行期间对内核发起了几十次系统调用:加载动态库、建立内存映射、设置栈、写终端、退出。没有一次是你写的代码直接要求的,全是操作系统和 C 运行时替你操办的。这基本就是操作系统的画像:一个住在所有程序和硬件之间的管家,程序每走一步,背后都有它在搬东西、记账、把风。本章先建立这门课的全景:OS 的两大职责、用户态和内核态的边界、以及内核的两种流派。
所有教材都把 OS 的功能列成一张四行表(进程管理、内存管理、文件管理、设备管理),这张表没问题,但我想先给你两个更本质的视角,整门课都在这两条线上来回。
视角一:向上提供抽象。裸机上写程序,你得自己跟每个型号的硬盘说行话、自己防着别的程序改你的内存。OS 把这些苦役打包成干净的接口:文件(把「磁盘块」抽象成带名字的字节序列)、进程(把「CPU 加一段内存」抽象成独立运行的程序)、虚拟内存(把「物理页框」抽象成每程序独享的地址空间,计组课第 8 章讲过硬件侧)、socket(把「网卡和协议栈」抽象成读写字节)。抽象做得好,程序就不用关心底层是 NVMe 还是机械盘、是 Intel 还是 ARM。
视角二:向下管理资源。CPU 就几个核,进程成百上千;内存几十 GB,申请者胃口无限;磁盘人人都想写。OS 是那个决定「谁在什么时候用哪块资源」的裁判:调度器分 CPU,页分配器分内存,文件系统分磁盘。裁判做久了你就会发现,这门课大半的篇幅(调度算法、页面置换、文件布局)都在回答同一类问题:资源不够分的时候,怎么分得又快又公平。
两个视角合起来,还能解释 OS 和计组课的分工:计组第 8 章讲过 MMU 和页表的硬件机制,OS 负责的是软件侧——页表里填什么、缺页了怎么置换。硬件提供机制,操作系统提供策略,这个口诀贯穿全课。
CPU 把指令分成两等:普通指令(加加减减,随便执行)和特权指令(改页表、关中断、直接访问设备,碰硬件命脉)。对应的执行状态叫用户态和内核态。操作系统本体住在内核态,应用程序全在用户态,用户态程序想用内核的服务,只有几扇门,而且门都在硬件层面:
write、open、fork),同步。三扇门后是同一套流程(计组第 9 章讲过):保存现场、切到内核栈、按向量表跳进内核处理、恢复现场返回。区别只在按门铃的是谁。
系统调用值得多看一眼成本。以 write 为例:参数装寄存器,syscall 指令触发特权级切换,内核保存现场、查系统调用表、执行真正的写、再恢复现场切回来。一轮下来开销在百纳秒到微秒量级,比普通函数调用贵几百倍。所以有句工程铁律:syscall 要攒着批做。读文件一次读 64KB 和一次读 64 字节循环 1024 次,前者快几个数量级,差价全在系统调用次数上。printf 慢、日志框架讲「缓冲写入」,根源都是省这扇门的过路费。
顺带纠正一个常见误读:用户态和内核态是权限等级,不是「两种程序」的同义词。内核代码也可能在内核态之外执行(内核线程跑在内核态但不是中断上下文),而一个普通程序一发系统调用就临时穿上内核态的衣服。这层区分到进程章节会更清楚。
把哪些功能放进内核态,是个真正的设计分歧,形成了两大流派:
宏内核(monolithic):进程、内存、文件、驱动,全套塞进内核态,彼此直接函数调用。Linux 是代表。快(调用就是一次 jump),但一荣俱荣一损俱损:一个烂驱动空指针,能把整个内核带崩,蓝屏和 Kernel panic 就是这么来的。
微内核(microkernel):内核态只留最小核心(IPC、基础调度、基本内存),文件系统、驱动统统推到用户态当服务进程,互相发消息。代表是 Minix、QNX(如今在你车机和基站里跑得多)。可靠性极好:文件服务崩了重启一个就行,内核岿然不动;代价是消息传递开销,一次跨服务调用要走几轮 IPC,比函数调用贵一个量级。
这个分歧在 1992 年引发过操作系统史上最著名的骂战,Tanenbaum(Minix 作者、AST)公开批评 Linux 是「过时的宏内核设计」,Torvalds 回信硬刚,「如果 GNU 内核去年春天就绪,我绝对不会浪费时间重造轮子,但它没有,而且到目前为止 Linux 大部分时候没给我惹麻烦」(大意)。三十年后回看,双方都对了一半:Linux 靠宏内核的性能和务实统治了服务器与安卓;而微内核的隔离思想以另一种方式获胜——hypervisor(第 5 章虚拟化)、安全微内核(手机 TEE)、以及把 Linux 驱动逐步往用户态搬的趋势(FUSE、DPDK、eBPF),全是在宏内核里给「用户态服务化」开后门。工程史很少给「哪个对」的答案,更多是「在不同约束下各有各的活法」。
printf 是库函数,内部攒缓冲后真正调的是 write 系统调用;open 则几乎直接就是系统调用。分不清的话用 strace 一看便知,这是排查「程序为什么有奇怪的文件操作」的第一工具。copy_from_user 这类专用通道。strace -c ./hello 看统计视图,按调用次数排序,前三名是谁?查一下各自的作用,想想为什么它们是最高频的。strace 一个 cat /etc/hostname 和一个 Python 脚本(只 print 一行),对比系统调用数量,差价在哪?