OS 四大目的
多路复用、隔离、共享、抽象——操作系统通过管理硬件资源,为应用程序提供简洁、安全、可移植的接口。
MIT 6.1810 · 第 1 课
操作系统是什么?为什么需要它?本课程从 UNIX 系统调用入手,带你走进操作系统的世界,以 xv6 为案例探索内核的核心机制。
一台计算机由 硬件(CPU、RAM、磁盘、网卡等)和 用户应用程序(shell、编译器、数据库等)组成。操作系统(O/S)位于应用程序与硬件之间,其核心是 内核(kernel),提供文件系统、进程管理、内存管理、网络等服务。
| 层次 | 例子 |
|---|---|
| 用户应用程序 | sh, cc, DB, grep, echo |
| 内核服务 | FS, processes, memory, network |
| 系统调用接口 | open, read, write, fork, exec, pipe |
| 硬件 | CPU, RAM, disk, NIC |
将有限的硬件资源在众多应用程序之间共享。多个进程共用 CPU、内存和磁盘,内核负责调度与分配。
一个程序的 bug 或恶意行为不能破坏其他程序或内核。这是安全与稳定性的基石。
协作的应用程序需要安全地交换数据——管道、文件、共享内存都是内核提供的共享机制。
把硬件的复杂性隐藏起来,提供简洁、可移植的接口,让程序员不必关心磁盘块、网卡寄存器等细节。
实验是 6.1810 的核心,占期末总评的 50%。实验分三类:
| 类型 | 内容 | 示例 |
|---|---|---|
| 系统编程 | 使用系统调用编写应用 | UNIX shell |
| O/S 原语 | 实现内核基本功能 | 线程切换 |
| 内核扩展 | 在 xv6 上添加新机制 | 网络协议栈 |
重要提示:大部分实验一周完成,请尽早开始!
| 项目 | 比例 |
|---|---|
| 实验(Labs) | 50% |
| 期中 + 期末考试 | 30% |
| 实验检查面谈 | 15% |
| 课前作业 | 5% |
应用程序通过 系统调用(system call) 与操作系统交互。它们看起来像函数调用,但实际上会切换到内核态执行,同时保持用户/内核隔离。
// ex1.c: 从标准输入读,写到标准输出
char buf[64];
int n;
while((n = read(0, buf, sizeof(buf))) > 0) {
write(1, buf, n);
}
read(fd, buf, n) 与 write(fd, buf, n) 是系统调用。第一个参数 fd(file descriptor)是"文件描述符"——UNIX 中一切皆文件的抽象:
read() 返回实际读取的字节数,出错返回 -1。UNIX I/O 是 8 位字节流,具体解释由应用程序决定。
// ex2.c: 创建文件并写入
int fd = open("out", O_WRONLY | O_CREATE);
write(fd, "x", 1);
close(fd);
open() 返回一个新的 fd,是一个小整数,索引到内核维护的每个进程的文件描述符表。不同进程的 fd 命名空间独立,所以 fd 3 在不同进程中通常指代不同的文件。
// ex3.c: fork 创建新进程
int pid = fork();
if (pid == 0) {
// 子进程执行这里
printf("child\n");
} else {
// 父进程执行这里
printf("parent, child pid=%d\n", pid);
}
fork() 复制当前进程——包括指令、数据、寄存器、fd 表、当前工作目录。父子进程初始状态几乎完全相同,唯一的区别是 fork() 的返回值:父进程得到子进程的 pid,子进程得到 0。
// ex4.c: exec 替换当前程序
char *argv[] = { "echo", "a", "b", "c", 0 };
exec("echo", argv);
printf("exec failed\n"); // 如果 exec 成功,永远不会到这里
exec() 丢弃当前进程的指令和数据内存,从文件加载新的程序,但保留文件描述符。如果成功,exec() 不会返回。
// ex5.c: fork + exec + wait
int pid = fork();
if (pid == 0) {
exec("echo", argv); // 子进程执行新程序
exit(1); // exec 失败时退出
}
wait(0); // 父进程等待子进程结束
Shell 对每个命令都用 fork() 创建子进程,子进程调用 exec() 运行程序,父进程调用 wait() 等待。exit(status) 通过 wait(&status) 向父进程传递 32 位状态:0 表示成功,1 表示出错。
fork() 复制父进程全部内存,exec() 又立刻丢弃——看起来浪费。在"写时复制(copy-on-write)"实验中,你将消除这种浪费。
// ex6.c: 实现 echo hello > out
close(1); // 释放 fd 1
open("out", O_CREATE | O_WRONLY); // 新文件获得 fd 1
exec("echo", argv); // echo 的 stdout 现在指向 out
open() 总是选择最小的未使用 fd。先 close(1),再 open() 就保证新文件获得 fd 1。fd 提供了间接层——程序只使用 0 和 1,不用关心输入输出的真正来源与去向。
// ex7.c: 实现 ls | grep x
int p[2];
pipe(p); // p[0]=读端, p[1]=写端
if (fork() == 0) {
close(1);
dup(p[1]); // 子进程 stdout 指向管道写端
close(p[0]);
close(p[1]);
exec("ls", argv);
}
wait(0);
if (fork() == 0) {
close(0);
dup(p[0]); // 子进程 stdin 从管道读端读取
close(p[0]);
close(p[1]);
exec("grep", argv);
}
pipe() 创建一对 fd,写端写入的数据由内核缓冲,读端等到有数据后读取。管道是独立的抽象,但与 fork() > 组合后能优雅地实现进程间通信与流水线。
例 8 展示父/子进程通过管道通信。例 9 展示目录的本质:目录也可以像文件一样 open() 再 read(),返回的是文件名列表。
UNIX 的设计值得反复追问:
write()?fork() 和 exec() 分开?UNIX 的设计非常成功,但并非唯一好方案。理解设计的取舍,是学习操作系统的核心。
Q1:以下哪项不是操作系统提供的核心服务?
Q2:fork() 的返回值,以下哪项是正确的?
Q3:Shell 如何实现 echo hello > out?
Q4:管道 pipe(p) 创建的两个 fd,用途是什么?
Q5:UNIX 中文件描述符(fd)提供的核心抽象是什么?
多路复用、隔离、共享、抽象——操作系统通过管理硬件资源,为应用程序提供简洁、安全、可移植的接口。
应用程序通过系统调用进入内核。这条边界同时保证隔离(用户不能直接访问硬件)和可控的内核入口。
xv6 是一个小型的类 UNIX 操作系统,代码量足以读完,但包含进程、文件、内存、管道等核心机制。
一切皆文件、fd 间接、fork/exec 分离、管道组合——这些设计相互作用,构成了优雅的 UNIX 工具生态。