7.micro-Kernel
On -Kernel Construction
传统观点是, 微内核是 inefficient 和 not sufficiently flexible 的, 但是本文说明相反观点, 并展示微内核可以达到 macro kernel 的 efficiency
以及为什么当前已有的 measurement 很难说明这一点
设计目标
- 微内核支持 modular
- 外部系统可以使用 micro kernel 提供的 mechanism
- 微内核系统更加 flexible 和 tailorable
明确 target
- 决定整个 -kernel 的能力的不应该是 performance, 而应该是 functionality
- 也就是说尽量不要把指令放进内核, 除非纯外部执行无法完成指定任务
- 系统应该能支持 protection
- independence principle: 不同 sub system 之间不应该有任何互相影响和 corruption
- 这里的子系统指的是进程, 也就是用户态的资源分割单位
- integrity principle: 不同子系统之间应该有 address 联系, 也就是进程能通过地址来互相通信
Address Space
从硬件来看地址空间就是 virtual addr 到 physical addr 的映射, 这是通过 TLB 和 page table 来实现的
微内核不在 addr space 的使用中再次引用 hardware 概念, 而是直接提供抽象概念, 从而实现 虚拟地址无法访问到 hardware (phy addr), 从而实现了更高层的 protection. 但是从 efficiency 角度考虑, 这层抽象需要类似于物理层的样子, 从而实现更加高效的访问
核心思路就是把过去驻留在内核并可直接碰物理内存/页表的大块儿东西——比如大多数设备驱动, 文件系统, 网络协议栈——尽量搬到用户态
宏内核里, 进程的地址空间是独立的 “用户区 + 只在内核态可访问的内核区” 同存于一张页表;进程平时用的是自己的用户虚拟地址; 内核只是被全局映射进来以便快速进入内核态
本系统的addr space 抽象设计思路是采用 recursive construction:
所谓 recursive construction 的意思是:
- 所有物理内存最初都在 a₀ 里(系统初始地址空间);
- 新的地址空间(a₁, a₂ …)只能通过 map/grant 从已有空间"派生"出映射;
- 这样, 所有页最终都能追溯到 a₀(对应物理内存);
但是: 运行时 CPU 并不会真的"逐层追溯"; 递归只是逻辑模型; - 内核在 map/grant 时, 实际就改好了页表, 保证新地址空间能直接 VA→PA, 不需要一层层走;
- 递归的意义是 权限传播链, 不是运行时查表链;
Recursive Construction
首先内存地址空间有三个部分:
Grant赠送: 也就是将一块地址空间直接赠送给另一个进程Map映射: 将一个地址共享给另一个进程, 原进程对这块地址仍然有权限Flush刷新: 将一块地址空间从进程中删除
这样设计的最大特点是__将内存管理和paging放到内核以外的地址空间中__, 只有 Grant/Map/Flush 这三个操作驻留在内核中, 这样就实现了内存管理的 modular, p-kernel自己几乎没有"可被应用直接使用的高层资源"(比如文件/套接字/块设备都不在内核里)
grant 只在很少的情况下使用: 只有在使用的时候能减少一个中间层的时候
(例如两个硬盘上面加入了一个统一文件资源访问接口的中间层, 那么每次访问物理地址都需要经过额外一次映射, grant可以支持将空间直接给到对应的硬盘从而减少一次映射)
Thread and IPC
线程是指在 process 内执行任务的工作流, 其资源包括:
- a set of registers
- instruction ptr
- stack ptr
- state info (including the addr space the thread is running)
进程之间的通信IPC 是通过 thread 为单位进行执行的, 也就是 thread 是进程链接的节点
IPC 也是进程之间独立的基础 (share memory by communication)
IPC 一定会通过内核转发, 这样保证了通信 受控, 不会破坏地址空间的隔离
线程是 用户态执行流, 运行在它所属的地址空间里;
内核只负责:
- 保存/恢复线程的上下文(TCB);
- 调度线程;
- 提供陷入点(系统调用, 中断)来完成受控操作;
IPC 监控
传统系统中会 supervise ipc, 例如 Clans 框架, 这里其并不会给 micro kernel
造成很大负担, 每次监视只会增加 2 cycle
Interrupt
硬件中断是通过 IPC 进行实现的, 硬件中断会被内核"重铸"为一种通知/消息, 发送给绑定该中断的用户态驱动线程
比如使用键盘输入io到一个程序中, 真正的"中断"只发生在两处:
- 设备向 CPU触发硬件中断;
- CPU陷入内核, 执行极短的中断入口/分发代码;后续驱动收到的消息, 驱动与应用之间的传递不再是硬件中断, 而是普通的
IPC/通知
虽然内核转发中断信息但是内核并不知道 msg semantics, 从而保护了内核安全
通常没有"常驻的内核进程", 只有极短的陷入路径与调度器; 系统服务都在用户态服务器里
Unique ID (uid)
不同进程之间通信需要一个唯一标识符用来表示要发送给谁, 或者知道是谁发来的, 以及内核要知道转发给谁
Flexibility
这一部分主要说明在微内核架构之上构建宏观内核的其他功能也是可行且不低效的
MemManager
微内核唯一的责任是保证所有地址空间操作(map/grant/flush)合法, 真正的分配/策略决策全由用户态服务器完成
上文提及的 初始地址空间存在于内核内部, 但是其管理者 就已经存在于内核外部了
Pager
我们首先区分一下 mmu 和 pager 的职责划分:
- Pager 负责的是从用户态的 page 的使用 (LRU/Reference bit/Dirty bit) 或者简单的说就是专门处理 page fault 的逻辑
- Pager 对接 client: 用户访问缺页地址的时候会让 pager 来进行处理, 往往设计内核调用一个 ipc 来让 pager 从 disk 读取 page 到内存然后返回给用户
- Pager 对接 Driver (往往是外接存储盘): 当文件页 pager 需要从磁盘取一页, 通过 IPC 向驱动发 I/O 请求; 驱动用 DMA/PIO 拉到内存页(这页通常由 Memory Manager 提供或预先准备的缓冲页)→ 返回给 pager
- MMU 负责的是 page table 的地址维护 (不包括上面适用page fault bits, 但是有新的权限维护例如 不能访问 的 page table)
- MMU 不是在 内核中的, 但是其只能被内核进行控制, 只有 MMU 找不到 page 才会触发 page fault, 让内核陷入之后再让 pager 进行处理
Device Driver
DD 在这里实际上就是一个进程, 首先将硬件 IO port 映射到其地址空间, 并将硬件中断信息发送给 kernel 再进行转发到目标进程, 也就是通过 IPC 进行通信;
有一些专门需要外设的软件驱动 例如 Screen 会有 Device specific memory;
Remote Communication/Unix Server
对于远程通信, 微内核只负责将消息发送到网络驱动, communication server 可以看作 client 的 “pager” 用来负责发送数据的时候缺页的处理者 (发生缺页的时候内核转发给 communication server 来进行解决)
同样的对于 UNIX server 也可以看作是 client 的 pager 因为其也是接受 IPC 进行控制
Efficiency Measurement
Kernel-User Switches
标准的零功耗 kernel call getpid 基本上除了调用内核其他没有任何操作, 所以可以用来测量从用户态转变到内核态然后返回用户态内核调用的开销, 此处基于 Mach 微内核系统调用的 kernel call 开销依然很大, 这里使用的硬件处理器是 486(50MHz) 和 x86 处理器, 因为在这两个处理器上 kernel-user mode switches 是非常困难且开销很大;
从具体步骤开销上看, 基础的更换开销是 107 cycle, 而这里在 486 处理器上面的开销是
50 * 18 us = 900 cycle 也就是有 800 个 cycle 是纯 kernel-overhead;
但是已经有数学证明了这里的 L3 微内核的最优秀 overhead 是 15 cycles
但是仍然要排除注入其他指令执行, cache/TLB miss 等其他因素
