Skip to content

Lecture 1 Agentic AI 时代的操作系统课:绪论

绪论,内容没那么多,随手记点有启发的东西

  • AI 能力在解决一个 well-formed 问题上极强能力远远超过人类,甚至是面对 ill-formed 问题抽象出 well-formed 问题然后优秀地解决。但是面对复杂系统的抽象实现和灰度设计能力差。人类现阶段做抽象设计能力远远高于 AI。
  • OS 就是这层抽象的一个范式。
  • "Content as code?"
  • OS 定义(from OSTEP): A body of software, in fact, that is responsible for making it easy to run programs (even allowing you to seemingly run many at the same time), allowing programs to share memory, enabling programs to interact with devices, and other fun stuff like that.
  • 计算机:硬件,软件,OS。OS 负责管理其他两者
  • Everything is a state machine. (想起了 jyy 的一次演讲)
  • “操作系统不是某个天才凭空设计出来的——它是硬件能力的自然投射。当内存足够大到能同时容纳多个程序时,'如何调度' 这个问题就自然出现了;当 I/O 设备变得多样时,'如何统一管理' 就成了刚需。理解这一点,比记忆任何操作系统定义都重要。”
  • 类 UNIX Legacy

Letcure 2 应用视角的操作系统

应用视角下,操作系统就是一组 API。

SimpleC

任何复杂的语句或者控制流表达式,(这里以 C 语言为例),都可以把它改成一行只做一件事情的形式(声明,赋值,函数调用 etc)。根据这一点实现一个 C Intermediate Language Transpiler(我们可以假如命名他为 SimpleC),这个解释器运行时大概只做三件事情的循环:取指令,解码,执行。

while (1) {
    stmt = fetch_statement();
    decode_and_execute(stmt);
}

——和 CPU 干的事情一模一样,无非抽象层的高低。具体的,每个状态可以由”变量 + 栈帧 + PC“构成。

仍然使用状态机思维,上面一行一行的汇编就是实现了一个状态的转移指令,每次实现后,程序的运行栈上的各种状态发生变化,继续执行下一条,如此循环。

程序状态机的形式化定义

  • 状态 S:{StackFrame, StackFrame, ...} + 全局变量 + 代码 + “堆” + PC
  • 初始状态 S_0:仅有一个 StackFrame = (main, argc, argv, PC=0),加上所有全局变量的初始值
  • 状态迁移 S → S ′:执行 frames[-1].PC 处的语句
  • 普通语句:push/pushv/PC++
  • 函数调用 call:压入新 StackFrame
  • 函数返回 return:弹出当前 StackFrame

递归转换为迭代的通用技巧

任何递归程序都可以通过显式管理调用栈转为迭代,相当于你去代替操作系统去实现显式管理所有进程的栈。进程调度的本质就是多个状态机之间的切换。

下面给出一个显示管理调用栈的例子:

#define ret(val) ({ top--; retval = (val); })

while (1) {
    Frame *f = top;
    if (top == stk) break; // Stack empty -> done
    int next_pc = f->pc++;
    // Single step execution...
}

编译与优化

广义的编译器包含汇编器和链接器,即把 .c 翻译成 .out(pe 文件)。

大致的编译过程如下:

.c => 汇编指令 => 二进制文件 .out

编译优化 O0-O2:

编译优化原理:CSE + TCP + GOT

CSE: Common Subexpression Elimination,公共子表达式消除。它会把重复计算的表达式复用起来。

x = a + b;
y = a + b; // 复用 x

TCP:Tree Copy Propagation 或泛指 Copy Propagation,即复制传播。

b = a;
c = b; // 优化为 c = a

callee-saved 寄存器:被调用者负责保存他的旧值,返回前恢复。这样调用者在调用函数前后看到的寄存器值是不变的。

与之对应的是 caller-saverd 寄存器,调用者想保存值,需要先自己在调用前保存。

每次函数调用前都要重新加载全局变量,编译器用 callee-saved 寄存器保存,跨调用复用。

GOT(Global Offset Table): GOT 是全局位置无关代码的地址表。位置无关代码不能把全局代码和外部函数的绝对地址硬编码在指令里,运行时会把真实地址放在 GOT,代码通过 GOT 间接取地址。

比如我取 foo 的运行时地址:

mov rax, [rip + foo@GOT]
mov eax, [rax]

内核不一定像普通用户态动态库那样依赖 GOT/PLT 动态链接,它经常是静态链接或使用自己的重定位方式。GOT/PIC 这类底层代码生成机制会影响指令数和内存访问,而操作系统热路径对这种成本极其敏感,在编译优化会通过各种方式来优化 GOT 的访问。

最小的 hello world

一个最简单的 hello world 使用 gcc 标准编译,编译结果也有 70kb。使用 gcc -c 只编译不链接,再手动 ld 链接会 fail。这是因为真正的函数入口是 _start 而非 main。而且 puts 之类标准库函数也需要链接 libc。

课程这里实现了一个 minimal.S,直接接管 os 的 sys_call,不依赖任何库来输出一个 hellworld (x86_64 version):

#include <sys/syscall.h >
.text
.global _start

_start:
mov $SYS_write , %rax // syscall number = 1
mov $1 , %rdi // fd = 1 (stdout)
lea addr(%rip), %rsi // buffer address
mov $14 , %rdx // length
syscall // invoke kernel!

mov $SYS_exit , %rax // syscall number = 60
mov $0 , %rdi // exit code = 0
syscall

这意味着一个完整程序可以不依赖任何库,只通过系统调用来实现运行。虽然现实意义比较小,但确实证明了程序和 os 的唯一接口是硬件指令:syscall(x86_64), ecall(risc_v), svc(AArch64)。

系统调用 system call

当一个程序使用去调用 system call 时,程序的主导权接管给操作系统,操作系统去实现对应的功能,完成后程序从暂停处回复执行,然后继续下一步。

事实上,所有的程序在运行上都和前面的 minimal.S 一样,各种实际的功能都去通过 syscall 来移交给操作系统来实现。

所谓”软件视角下,操作系统就是一组 api“,指的就是 system call。

strace(System call trace)

拦截并记录程序发出的每一个系统调用。课程这里用 strace 去观测 gcc 的编译过程,gcc 本身是一个调度器,真正干活的是 cc1(C -> 汇编),as(汇编 -> .o),collect2 + ld(链接)。

Lecture 03 硬件视角的操作系统

相对于软件视角下,操作系统就是一组 system call 的 api,硬件视角下,硬件根本看不到操作系统(这不是废话)。计算机系统的抽象让硬件及其只需要作为状态机来无情执行指令,下层不需要知道上层的设计,这是抽象设计以隔离复杂性。

I/O 设备

如果把计算机硬件视作一个状态机,那么 I/O 设备对于一个状态机来说,承担着输入和输出的功能,