AbstractMachine 上运行的程序称为 “kernel” (内核)。这个名字通常表示直接运行在硬件上、对硬件有直接控制的代码。不仅是操作系统内核,像在 GPU 上运行的二进制文件也称为 “kernel”。
AbstractMachine 上的 kernel 被编译成一个目标体系结构/平台上可执行的文件,并可以直接在环境 (如计算机硬件、虚拟机等) 上执行,例如:
在《操作系统》实验中,请大家以 x86_64-qemu 为基准平台,其他实现作为参考。
Kernel 是一个 C 语言书写的程序,C 语言代码定义了程序 (kernel) 的运行时状态,包括:
以上所有数据存储在一个平坦的地址空间中 (只读代码、栈、数据、堆区互不相交且连续存储),对相应符号执行取地址操作 &
会得到指向相应区域的指针,并且可以转换为 intptr_t
类型的整数。程序启动时,kernel 被 TRM 加载,kernel 运行时可使用的栈大小不少于 4 KiB;堆内存的大小和位置在运行时确定,通过 heap
变量获取,kernel 对应的状态机开始执行。AbstractMachine 会创建一个栈帧,从 main
函数开始执行,包含参数 const char *args
,args
保存在栈帧中;args
指向的内容为只读数据。
int main(const char *args) {
...
}
的参数在 make run
前通过设置 mainargs
环境变量指定。参数字符串的长度 (包含末尾的 \0
) 不能超过 1024 字节。main 函数返回后,kernel 完成运行。假设返回值为 r
,AbstractMachine 会执行 halt(r)
终止。
我们不妨假定执行一条 C 语言语句会从状态 迁移到 ,记作 。Kernel 在运行时允许调用 AbstractMachine API,此时的状态迁移 由 AbstractMachine 定义。
Area
结构体typedef struct Area {
void *start, *end;
} Area;
表示左闭右开区间 [start, end)
的一段内存。我们假设地址空间的最后一个字节 (例如 32-bit 平台下地址 0xffffffff
) 永远不会被包含在某个 Area
中,因此 end
不会溢出。例如,32-bit 平台下的某个 Area:
(Area) {
.start = (void *)0;
.end = (void *)0xffffffff;
}
则它最后一个字节的地址是 0xfffffffe
。klib-macros.h
提供了一些区间的构造/判断的宏:
#define RANGE(st, ed) (Area) { .start = (void *)(st), .end = (void *)(ed) }
#define IN_RANGE(ptr, area) ((area).start <= (ptr) && (ptr) < (area).end)
heap
: kernel 可用的物理内存堆区extern Area heap;
标记了一段连续的、代码可以使用的物理内存 [heap.start, heap.end)
,这段内存 (堆区) kernel 可以任意读写。
heap
在 main
被调用前初始化,之后不会改变 (任意处理器都可以读取它)。修改 heap
导致 undefined behavior;Kernel 没有任何理由需要修改它。
putch
: 打印字符void putch(char ch);
向默认的调试终端打印 ASCII 码为 ch
的字符:
-serial
可以选择输出位置。halt
: 终止 AbstractMachinevoid halt(int code) __attribute__((__noreturn__));
立即终止整个 AbstractMachine 的运行,并返回数字编号 code
(0-255):
code
。halt
都会终止整个 Kernel 的执行。
进入共享内存多处理器模式。假设系统中有 个处理器,在完成 MPE 初始化后,系统中就有多个并行执行、共享内存且拥有独立堆栈的状态机。
mpe_init
: 启动多处理器int mpe_init(void (*entry)());
启动系统中多处理器的执行,每个处理器都跳转到 entry
开始执行,执行流共享代码、数据;每个执行流有独立的堆栈和寄存器,且 entry 运行时可使用的栈大小不少于 4 KiB。
entry
不能返回。
cpu_count
: 处理器个数int cpu_count();
返回系统中处理器的个数,在整个 AbstractMachine 执行期间数值不会变化。
cpu_count
的实现通常实现是一个共享内存 (或设备寄存器) 的读操作。编程时 (例如实现操作系统时) 你可以假设系统中处理器的个数不超过 8 个。
cpu_current
: 当前处理器编号int cpu_current();
返回当前执行流的 CPU 编号,从 0
开始,例如 cpu_count() == 4
,则在四个 CPU 上分别调用 cpu_current()
将分别得到 0
, 1
, 2
, 3
。
atomic_xchg
: 内存交换int atomic_xchg(volatile int *addr, int newval);
原子 (不会被其他处理器的原子操作打断) 地交换内存地址中的数值,等价于以下 C 代码:
int atomic_xchg(volatile int *addr, int newval) {
int *ret = *addr;
*addr = newval;
return ret;
}
stdatomic.h
和 GCC builtin 的函数。
为了实现操作系统提供的一些基础的 I/O 设备访问。在 AbstractMachine 中,我们对常见的设备进行了抽象,把它们简化为了一组可以读或写的控制寄存器,通过读/写这些寄存器实现设备状态的读取和控制。
ioe_init
: 初始化 I/O 扩展bool ioe_init();
完成系统中 I/O 设备的初始化。
mpe_init
之前。
ioe_read/ioe_write
: I/O 设备读写void ioe_read (int reg, void *buf);
void ioe_write(int reg, void *buf);
从编号为 reg
的寄存器读取/写入,读取/写入的数据取决于寄存器的编号。设备可能会对寄存器的使用作出额外的规定,请参考 Abstract Machine 设备文档。为了减少大家定义额外的变量,我们推荐使用 klib-macros.h 中封装后的宏访问设备,例如:
*rtc = io_read(AM_TIMER_RTC);
io_read(AM_GPU_STATUS).ready;
io_write(UART_TX, 'X');
ioe_read
和 ioe_write
的代码可以被 CTE 中断 (这是允许的)。但 Kernel 需要保证在ioe_read
或 ioe_write
操作
TRM、MPE 和 IOE 能够运行多个处理器上的顺序程序,多个共享内存的执行流 (处理器) 总是执行当前指令。CTE 允许 kernel 管理异步的执行流,允许每个处理器分别在中断/异常发生时执行代码,并保存/切换到其他执行流。
CTE 允许在程序执行中引入以下异常 (trap) 控制流,包括以下来源:
在异常 (trap) 发生后,会
cte_init
中注册的处理程序应当小心数据竞争的发生。
cte_init
: 初始化上下文扩展bool cte_init(Context *(*handler)(Event ev, Context *ctx));
注册中断/系统调用处理程序:在中断/异常/系统调用时,AbstractMachine 会立即保存当前执行流的上下文 (Context, 包括寄存器现场等) 到当前执行流的内核栈 (栈空间由 kcontext/ucontext 指定),然后调用 handler
,其中参数 ev
是事件的类型:
EVENT_ERROR
- 非法访问的异常,例如 #GPEVENT_IRQ_TIMER
- 时钟中断EVENT_IRQ_IODEV
- I/O 设备中断 (键盘、串口……)EVENT_PAGEFAULT
- 缺页异常EVENT_SYSCALL
- 系统调用EVENT_YIELD
- yield()
自陷而 ctx
是指向保存在当前执行流信息的指针。handler
可以返回任何合法的 Context
(可以由 kcontext
创建,或者是某个 handler
保存的参数)。
mpe_init
之前。可以在 IOE 之前或之后初始化。
main
函数的执行,它使用 AbstractMachine (TRM) 初始化时的内核栈;mp_entry
函数的执行,每个处理器的 mp_entry
执行都拥有独立的内核栈;kcontext
创建的上下文在 CPU 上的执行,创建时会指定一个内核栈。在中断/异常发生后,handler()
的返回) 后被弹出堆栈handler
返回到另一个上下文,那么 ctx
指针指向的上下文将一直保持有效,直到这一上下文被再次调度或内核栈被释放。
iset/ienabled
: 外部中断管理bool ienabled(void);
void iset(bool enable);
读取/写入当前处理器的中断打开/屏蔽状态。其中 ienabled/iset
的 true
/false
分别表示中断打开/关闭。注意中断关闭只能屏蔽处理器外的中断 (即 EVENT_IRQ_TIMER
和 EVENT_IRQ_IODEV
),处理器同步产生的 error, page fault, syscall 和 yield 不能屏蔽。
ienabled/iset
仅能影响当前执行指令 CPU 的中断标志位,不能关闭/查询其他处理器的中断状态。
yield
: Self-Trappingvoid yield();
陷入内核执行,将会在当前处理器上调用 handler(EVENT_YIELD, saved_context)
,其中 saved_context
是保存的当前内核上下文。
yield
的当前处理器会完成 self-trapping。
kcontext
: 创建内核态运行的上下文Context *kcontext(Area kstack, void (*entry)(void *), void *arg);
将 kstack
表示的一段内存作为内核栈中创建一个可执行的内核态上下文,返回的 Context
指针可以在中断返回时被调度到处理器上执行。首次执行返回的上下文中 PC 为 entry
,并正确为 entry
函数传递了 arg
参数。
VME 允许为执行流 (上下文 Context) 赋予一个 “虚拟” 的内存视图,每当访问任何内存 的时候,都取访问 。VME 提供了描述、修改数据结构 的机制。
mpe_init
之前。
vme_init
: 初始化虚存管理bool vme_init(void *(*pgalloc)(int), void (*pgfree)(void *));
初始化虚拟存储。pgalloc
、pgfree
函数分别用于一个物理页面的分配和回收。注意 vme_init
执行时就可能调用 pgalloc
/pgfree
,你需要确保它们在 vme_init 调用时处于可用状态。
protect
/unprotect
: 地址空间管理void protect(AddrSpace *as);
void unprotect(AddrSpace *as);
创建/销毁一个地址空间,其中 protect(as)
后,as
会被初始化:
as->pgsize
是页面的大小 (AbstractMachine 可能返回与实际机器不同的页面大小)。as->area
是被保护的 (用户态可访问) 地址空间范围。as->ptr
是 AbstractMachine 私有的指针,例如 x86-qemu 的实现是 CR3 寄存器的数值。as
。
map
: 修改地址空间映射void map(AddrSpace *as, void *vaddr, void *paddr, int prot);
对地址空间 as
建立 vaddr
paddr
的映射,当 paddr
为 NULL
时,取消该页面的映射。prot 由以下 flag 组成。注意 vaddr
和 paddr
必须对齐到页面边界 (as->pgsize
),且 vaddr
必须位于 as->area
、paddr
指向的页面位于 heap
。
#define MMAP_NONE 0x00000000 // no access
#define MMAP_READ 0x00000001 // can read
#define MMAP_WRITE 0x00000002 // can write
ucontext
: 创建被保护的用户态进程上下文Context *ucontext(AddrSpace *as, Area kstack, void *entry);
与 kcontext
类似,创建一个可被调度执行的上下文,它的执行位于低权限的用户态,地址空间由 as
指定,初始 PC 为 entry
。在用户态发生中断/异常会切换到内核态 (kernel) 运行,并切换到内核栈。中断/异常处理程序 (包括保存的上下文) 都存储在 kstack
指定的内核栈。
ucontext
仅修改 kstack
中的数值,并在其中创建上下文。