Skip to content

CestlavieBitOS/bitos

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RISC-V 混合内核操作系统设计方案(C 实现)

  1. 总体设计理念与目标

本操作系统的设计追求面向未来、可持续演进的接口和架构,可用于从零开始构建一个RISC-V 架构、C 语言实现、混合内核模型的系统。其核心理念是在不背负传统 POSIX 包袱的前提下,提供一个能不断迭代的新型 OS 接口,同时与函数式编程 (FP) 思想契合,使系统行为可组合、可推理、可测试和可回放。主要目标包括:

长期演进:接口设计如协议般可扩展、可协商,保持向后兼容,在演进过程中只增不改。通过版本协商和特性发现机制,支持在不破坏旧接口的情况下引入新功能。

安全默认:以最小权限原则为默认,所有资源访问需凭能力句柄 (Capability/Handle),实现天然的沙箱隔离,方便多租户、容器和插件生态。没有获得句柄就无法操作资源,从机制上避免传统全局 UID 或路径字符串带来的权限混乱。

高并发友好:采用异步优先 (async-first) 的接口模型,所有I/O和同步原语走异步提交/完成队列,统一等待、超时与取消语义,最大程度减少系统调用往返开销。同步调用仅作为用户态封装,以便充分利用批处理和避免阻塞内核线程。

可观测、可复现:将追踪 (tracing) 和事件流作为一等公民,内建系统级 tracing 接口;支持记录/回放 (record/replay) 机制,方便调试复杂并发问题。

FP 友好:让系统交互可以表示为纯函数式的效应 (effect),消除隐式全局状态,依赖通过句柄显式传递。通过类型系统约束资源生命周期,实现更强的可组合性和可验证性。

非目标方面,本系统并不以直接运行未修改的现有 Unix 程序为设计中心(可提供兼容层,但不内置于内核设计)。另外,内核不将所有高层策略硬编码其中——内核只提供机制,调度策略、文件系统语义、网络协议等尽可能在用户态实现,以便灵活演进。

综上,本系统旨在 “将 OS 接口变成可版本化演进的对象协议”,以能力句柄显式管理权限,以异步优先 + 可取消作为统一交互模型,并让高级语义在用户态快速迭代。

  1. 架构分层与职责(内核 / 用户态 / 服务)

为实现上述理念,系统采取分层设计,将内核与用户态职责明确划分,保持内核 ABI 小而稳定,用户态 API 可灵活演进。

内核层 (Kernel ABI):提供极小且稳定的底层机制,包括能力句柄、进程内的对象表、进程间通信 (IPC) 通道、异步提交/完成队列 (AsyncQ)、虚拟内存对象 (VMO) 及映射 (VMAR)、计时器/单调时钟、基本诊断等。内核接口尽量少且固定,通过这些机制支撑上层服务。

用户态系统服务层:实现可进化的高层语义,如文件系统服务(路径解析、权限检查、缓存策略)、网络协议栈服务(socket语义、TLS、DNS 等)、驱动框架(尽可能在用户态运行驱动)、应用沙箱/权限管理、GUI 多媒体等。这些通过内核提供的IPC机制以对象协议形式对外提供服务接口,可以根据需求升级版本。

演进约束:接口演进遵循严格的 Additive-only 规则,即只新增不修改已有语义,不重用旧编号,不移除旧方法(旧接口可弃用但需保留)。当需要更改语义时,通过引入新方法或新协议版本来实现。所有对象需支持基本的get_info()查询自身协议版本和能力位等,以便可发现、可协商。这种机制确保随着时间推移,系统可以不断添加新特性,同时保持对旧软件的兼容。

RISC-V 平台架构:在RISC-V架构上,推荐采用OpenSBI作为引导层:M态运行OpenSBI固件,S态运行本系统内核,U态运行用户服务和应用。内核采用Sv39分页模式(后续可扩展Sv48)实现虚拟内存,使用高半区映射方案将内核映射到高地址空间、用户态在低地址空间,明确区分权限。关键的硬件资源采用分层管理:比如外部中断通过PLIC处理,定时器中断通过CLINT或SBI提供;系统调用通过在S态触发ecall来陷入内核。这种架构确保内核与硬件有效协作:OpenSBI屏蔽了复杂的M态细节,内核专注于S态调度和内存管理,用户态运行受保护,形成清晰的三层特权结构。

  1. 内核接口设计(ABI,句柄,系统调用)

最小化系统调用ABI是本设计的关键。内核只提供有限且通用的系统调用,大部分功能通过**“对象 + 方法 + 消息”**在用户态实现。也就是说,内核定义少量syscall来操作通用对象和机制,而不是为每种高层功能提供专用调用。这不仅减小了ABI表面,还使新功能的添加无需修改内核接口。

在RISC-V上,系统调用通过S态的ecall指令触发。约定使用寄存器a7传递调用号,a0..a5传递参数,调用返回时a0返回状态码(0表示成功,负值表示错误码),额外的返回值通过传入的用户指针或次级寄存器提供。这种调用约定遵循RISC-V SBI/ABI规范,简洁统一。

**系统调用集合(第一版)**划分为几类:

句柄通用操作:如sys_close(h)关闭句柄,sys_dup(h, new_rights, out_h*)复制并缩减权限派生新句柄,sys_getinfo(h, out_info*)查询对象信息。这些提供基本的资源管理(关闭、复制、查询),适用于所有句柄类型。

能力租约 (Lease):如sys_lease_create(out_lease*)创建租约对象,sys_lease_revoke(lease)撤销租约从而批量失效其派生的能力,(可选 sys_lease_attach(h, lease)将某个句柄归属到特定租约)。Lease相关调用实现能力收回机制,有助于权限撤销和会话清理。

进程间通信 (IPC):如sys_channel_create(out_end1*, out_end2*)创建一对通信通道端点,sys_channel_send(ch, buf*, len, handles*, nhandles)发送消息(含数据缓冲和可选句柄数组),sys_channel_recv(ch, buf*, cap, out_handles*, cap_handles, out_len*, out_nhandles*)接收消息。这些调用提供基础的消息传递能力,并支持句柄在消息中传递(能力传递),实现进程/服务间的安全通信。

异步队列 (AsyncQ):如sys_asyncq_create(out_q*)创建一个异步队列对象,sys_submit(q, ops*, nops, out_submit_id*)提交一批异步操作,sys_poll(q, completions*, cap, timeout_ns, out_n*)轮询/等待操作完成,sys_cancel(q, token)取消一个尚未完成的操作。这是异步优先模型的核心接口,统一处理各种等待和I/O的提交与完成。

内存管理:如sys_vmo_create(size, out_vmo*)创建指定大小的虚拟内存对象,sys_vmo_read(vmo, offset, buf, len) / sys_vmo_write(...)从VMO读取/写入数据(调试便利接口),sys_vmar_map(vmar, vmo, off, size, perms, out_addr*)将VMO映射到当前进程地址空间,sys_vmar_unmap(vmar, addr, size)解除映射。通过这些调用,用户态可以分配共享内存、执行内存映射以及管理虚拟地址空间,形成用户态内存管理的基础。

进程与线程:提供基本的进程/线程创建与启动能力,如sys_proc_create(out_proc*)创建进程控制块,sys_thread_create(proc, entry, stack, out_thr*)创建线程并指定入口,sys_proc_start(proc)启动一个进程(将其中线程运行)。后续可用更高级的sys_spawn(image, args_msg, handle_table_msg, out_proc*)来一站式地创建并启动进程,将镜像和初始句柄表打包传入。在系统初期,可简化为“单进程多线程”(所有服务在一个进程内)以降低复杂性,待基础稳定后再逐步过渡到真正的多进程架构。

上述系统调用组成了内核提供的稳定 ABI。通过这些通用调用,操作系统可以在用户态实现各种具体功能。例如,没有专门的open()或read()系统调用,而是通过通用的IPC和VMO机制,由文件系统服务在用户态提供文件打开和读写语义。这样内核ABI保持不变,文件系统的行为可以在不修改内核的情况下演化升级。

  1. 核心内核对象模型

内核将资源抽象为统一的对象 (Object),通过**句柄 (Handle)**来引用和操作。每个进程维护自己的句柄表,句柄本质上是进程私有的索引值(如32位整数),对应内核中的对象指针及相关元数据。下面列出系统核心对象类型及其作用:

句柄 / 能力 (Handle/Capability):表示对内核对象的不可伪造引用。每个句柄关联一组权限位 (Rights),例如可读、可写、可执行、可传递、可复制、可观察等。默认情况下进程没有任何句柄(无权访问任何资源),只有通过安全方式获取句柄才能操作对应资源,从而实现最小权限原则。句柄的权限位可用于细粒度控制访问 —— 在每次使用句柄执行操作前,内核会检查句柄是否存在、是否拥有所需权限、所属租约是否已撤销等,以确保安全。通过sys_dup可以从已有句柄派生出权限更弱的新句柄(如只读子句柄),但要求新权限是旧权限的子集。这种能力模型使安全策略显式化:权限不来自进程的全局身份或工作目录等隐式状态,而来自进程持有的句柄集合。

租约 (Lease):一种特殊的内核对象,用于实现能力撤销 (Revocation)。当创建租约(lease)后,可以将若干句柄归属该租约;若干子能力(派生句柄)挂在同一租约上。当需要收回权限时,只需撤销对应的租约,内核即可将该租约标记为失效,使挂在其上的所有句柄立即或尽快失去效力。实现上,租约对象可简单地包含一个原子布尔标志alive表示是否有效,以及挂接在其上的句柄引用列表。sys_lease_revoke 会将alive置false,后续所有通过该租约派生的句柄在使用时都会得到错误(区分被撤销与正常关闭的错误码)。这种机制类似于“租约过期”:还可扩展TTL(有效期)或续租功能,用于实现临时授权、会话管理等。总之,Lease使权限收回成为一等能力,解决传统能力系统中难以收回权限的问题。

通信通道 (Channel):表示双端点的IPC通道,允许进程/线程间发送和接收结构化消息。每个Channel有两个句柄,分别属于两侧通信者。通过sys_channel_send发送消息时,可以附带任意字节数据以及一个或多个句柄(能力)。附带句柄相当于将某些资源的访问权传递给对方(需要发送端对这些句柄拥有TRANSFER权限)。接收方通过sys_channel_recv获取消息数据以及转递来的句柄,其获得的句柄具有原始句柄相同的权限位(内核可在传递过程中根据策略进一步收缩权限)。Channel提供可靠的消息队列(FIFO)语义,常用于实现服务调用协议:用户态服务监听一个Channel,客户端通过发送不同的方法号和参数的消息来请求服务,实现类似调用远程对象的方法。

异步队列 (AsyncQ):AsyncQ对象是实现异步提交/完成模型的关键机制。它由内核和用户协作管理,用于提交各种异步操作并取回完成结果。每个AsyncQ包含:一个提交队列(由用户通过sys_submit提交操作,内核读取执行)、一个完成队列(内核将操作结果写入,用户通过sys_poll提取)、以及管理等待和取消的数据结构。提交的每个操作包含一个操作码(如发送、接收、定时器等待、VMO读写等)和一个令牌 (token)。令牌用于标识操作和关联完成结果,用户也可用它来取消操作。Cancel机制需要严格的语义保证:根据操作状态(未执行、执行中、已完成)决定取消是否成功。AsyncQ允许批量提交和批量收割完成,以减少用户态/内核态切换次数。通过AsyncQ,系统实现单一的等待机制——无论是I/O、IPC消息还是定时器事件,都通过提交操作并异步等待完成,使得阻塞等待变成可组合的异步任务,大大提高并发友好性。

虚拟内存对象 (VMO):抽象表示一段可共享的物理内存页集合,可用于实现共享内存、文件映射、零拷贝数据传输等。每个VMO有固定大小和权限(读/写/执行),可以通过sys_vmo_create创建。VMO的数据可通过sys_vmo_read/write读取修改(方便调试,小数据传输),但更高效的用法是将VMO 映射 (map) 进进程地址空间:通过sys_vmar_map将VMO的一段映射到某虚拟地址,从而直接在用户空间读写。跨进程共享通过传递VMO的句柄来实现:一个进程创建VMO并写入数据,然后将VMO句柄通过Channel发送给另一个进程,共享的数据即可在对方进程映射后零拷贝访问。VMO还可支持写时复制 (COW) 等特性,方便实现高效的进程复制或内存快照。

虚拟内存地址域 (VMAR):表示一片分配的虚拟地址空间区域,控制可映射地址范围和权限边界。通常每个进程有一个根 VMAR 覆盖整个用户地址空间,内核通过VMAR管理用户地址空间的布局和保护。通过VMAR句柄执行map/unmap操作时,内核会检查目标区域是否有权限重映射、是否遵守W^X策略(Writable and eXecutable不可同时赋予同一内存页,以增强安全)。VMAR使地址空间操作受控于能力权限,杜绝用户对自身地址空间进行未授权的篡改。

上述核心对象构成了系统内核提供的能力机制基座。内核的调度线程、定时器等也可抽象为对象(例如Timer对象),但最基本的是上述几类,它们相互配合支撑起更高层的功能。例如,文件句柄、网络socket等在实现上都可以是这些基础对象的组合或特例,并通过对象协议暴露给用户态。

每个进程的句柄表在实现上可以看作一个数组或哈希表结构,条目包含指向实际内核对象的指针、权限位、所属租约等信息。在执行系统调用操作时,内核会根据提供的句柄值在句柄表中查找对应对象,并进行权限校验和状态检查,以确保操作合法安全。只有通过这种封装,才能保证不同进程间完全隔离,任何对象只能被持有其句柄且具备相应权限的进程访问。

  1. 用户态运行时设计(libsys, libmsg, librt)

在用户态,需要提供相应的运行时库支撑上述内核接口,以方便开发者使用C语言进行系统编程。本方案建议提供三个关键库模块:libsys、libmsg 和 librt,分别封装系统调用、消息编解码和运行时抽象,加速用户态开发。

libsys:系统调用封装库。提供与内核ABI一一对应的薄封装函数,如sys_close()、sys_channel_send()等,将用户态调用转换为实际的RISC-V ecall 调用。。libsys不引入额外语义,尽量保持简单稳定,仅做基本的参数封装和错误码转换,使上层不直接接触内联汇编或寄存器操作。通过libsys,C 开发者可像调用普通函数一样使用系统调用,提升开发效率。

libmsg:结构化消息编解码库。提供构造和解析TLV(Tag-Length-Value) 格式消息的工具。包括:

Builder接口:如msg_put_u32(msg, tag, value)、msg_put_bytes(msg, tag, buf, len)等,用于构造消息体。特别地,句柄(能力引用)通常不直接序列化进字节流,而是在消息结构中单独附加,因为句柄传递由内核在IPC层面支持。

Parser接口:提供迭代读取TLV字段的方法,如msg_parse_next(&parser, &tag, &len, &value_ptr),库内部会自动跳过无法识别的tag。这样确保旧版本的服务可以安全地忽略新版本消息中它不识别的字段,保证协议的扩展性。

libmsg使服务协议的实现更加容易——开发者无需手动处理字节序和兼容性,可专注于协议逻辑。TLV格式具备天然的向后兼容优势:字段都有tag标识,接受方跳过未知tag即可,新增字段不会影响旧实现。

librt:运行时库,提供事件循环和更高级的异步抽象。其目标是让C语言编写的上层代码也能像函数式风格那样编写可组合的异步任务。librt 的核心组件包括:

事件循环 (rt_loop):内部使用sys_poll等待AsyncQ上的事件完成,并将完成事件分派给相应的回调函数处理。开发者向librt注册异步操作(带有回调或携带promise/future),librt在后台反复调用sys_poll获取完成队列中的事件并处理。

令牌-回调映射:librt维护一个映射表,将每次提交sys_submit时使用的token关联到用户提供的回调或后续操作。当sys_poll取回完成事件(包含token和状态)时,librt自动找到对应的回调并执行,实现异步结果的分发。

组合子 (Combinators):为方便管理并发,librt提供常用的异步操作组合工具,例如rt_timeout(task, ns)用于给任务设定超时时间、rt_all(tasks[])并行等待多个任务全部完成、rt_race(a, b)竞争执行两个任务取最快的结果、rt_cancel(token)取消任务等。这些接口可以视作对AsyncQ原语的用户态扩充,帮助开发者以更直观的方式编排异步逻辑,类似于高级语言的Promise.all()、async/await等模式。

通过上述运行时库,用户态开发可以基于能力安全和异步并发的原语构建复杂应用,而无需重复处理底层细节。例如,通过librt,开发者可以方便地实现一个同时等待文件I/O和网络I/O的操作,并在任一完成或超时后立即响应,而底层由AsyncQ保证所有等待的统一取消和同步。

此外,这些库(尤其是libsys和libmsg)稳定后也将构成用户空间的系统API,未来即使内核ABI保持不变,libmsg可以扩充支持新的协议字段,librt可以改进调度策略等,从而实现用户态API可进化而内核无需变动的目标。

  1. 服务协议与结构化消息(TLV,协议演进)

正如前述,操作系统的大量高层功能将在用户态以服务进程形式提供。为让内核与用户态服务协作无缝、服务之间接口可演进,需要设计良好的服务协议和消息格式。

对象协议是系统接口的基础思想:“一切皆对象,每个对象通过句柄发送结构化消息进行操作”。具体而言,每项服务对外暴露一个或多个对象句柄,客户端通过IPC Channel向这些对象发送请求消息,消息中包含方法标识和参数。服务处理后,通过回复消息返回结果或错误码。这种面向对象的协议比传统函数调用更灵活,方法的语义可以随协议版本升级进行扩展,同时通过句柄自然隔离不同资源。

为实现协议的自描述和易扩展,消息采用TLV格式非常合适。建议的结构如下:

消息头:固定大小,包含关键信息,例如:

protocol_id (协议标识,如文件服务FS=1、网络服务NET=2等)

proto_ver (协议版本号)

method_id (方法/操作编号)

flags (标志位,用于控制选项或标识消息类型)

消息体:紧随其后的TLV字段列表。每个字段包含tag (16位字段类型标签)、length (16位长度)、以及具体的value。这样解析方如果遇到未知的tag(超出当前版本定义),可以根据length直接跳过该字段的value,从而保持向后兼容。

例如,对文件系统服务协议一次OPEN调用的请求消息,可以构造如下:头部指明proto_id=FS (文件服务), proto_ver=3, method_id=OPEN,然后消息体包含{tag=PATH, value="..."}{tag=MODE, value=...}{tag=FLAGS,...}等字段。将来如果需要为OPEN增加新参数,比如OPTS选项字段,也可以在更高版本协议里定义新tag,旧版本服务看到不认识的tag会跳过,保持兼容。

为支持特性发现和协商,所有对象应实现通用方法如get_info()和negotiate()。其中,get_info()返回该对象的类型、所实现的协议ID及版本、支持的方法位图、能力权限等信息。客户端在拿到一个服务句柄后,首先可调用get_info()了解对端能力。negotiate(min_ver, max_ver, features)则用于客户端和服务协商协议版本和特性:双方各自有一套支持的版本范围和可选特性位,negotiate调用可以帮助选定一个双方都支持的最高版本及特性组合。通过协商,新旧版本软件可以在运行时弹性适配,而不会因为版本不匹配而完全无法通信。

服务接口对象化的好处是消除全局命名带来的隐患和僵化。例如,文件系统服务不直接向应用暴露整个内核文件系统的全局路径空间,而是提供如下对象模型:

一个文件系统服务入口对象,例如 FsSvcCap(文件服务能力),用于代表文件系统服务本身。

目录对象 (DirCap):表示一个目录,可在其上执行查找、枚举等操作。

文件对象 (FileCap):表示一个已打开的文件,可在其上读写。

典型的文件访问流程是:

应用通过名字服务 (NameSvc) 将某个服务名解析为文件系统服务的句柄 FsSvcCap。

调用文件系统服务的FS_GETROOT方法,从服务获取根目录的 DirCap。

在根目录DirCap上调用LOOKUP(name)查找子项,可能得到一个新的DirCap(子目录)或FileCap(文件)。拿到FileCap后,应用即可对其执行FILE_READ/WRITE等操作。

通过这样的对象分解,权限控制和资源管理更清晰:应用只有通过NameSvc拿到服务入口能力,才能进一步获取目录或文件能力。每一步都有明确的能力传递,而且权限可以逐级收缩(例如DirCap可能是只读的,则用它打开的FileCap也只能读)。相较于传统全局文件路径,这种方法更安全(路径解析和权限检查由服务控制)且方便协议扩展(例如可以为DirCap定义新的方法,不影响其他服务)。

网络服务亦类似,可设计 SocketCap、ListenerCap、ConnectionCap 等对象,由网络服务提供。这些服务化接口的共同点是:不依赖全局共享状态,一切经由能力传递。这种模式非常契合前述能力安全模型,也让服务的实现和升级变得灵活,因为对象协议可以版本化演进。

  1. 混合内核与服务拆分建议

操作系统内核一般在设计上存在微内核与宏内核两种极端。微内核将尽可能多的组件放到用户态,以提高模块化和安全性,但在性能和实现复杂度上有挑战;宏内核则倾向于将更多功能放进内核获取速度,但可能牺牲灵活性。本方案选择混合内核 (Hybrid Kernel) 路线,结合两者优点:机制内核化,语义用户化,同时在必要时允许内核承担部分服务职能以解決早期性能或硬件支持问题。

具体来说,按照分层原则划分如下:

必须在内核态实现的核心:首先,一些基本机制和硬件交互必须由内核承担,包括线程调度与同步原语、地址空间管理(页表维护、内存分配)、VMO/VMAR 基本内存对象管理、IPC通道和AsyncQ原语,以及句柄表和权限检查等。这些构成系统的最小可用内核。在首个里程碑版本,这些机制实现后就能跑起一个简单内核。

可选择内核化的模块(混合内核的**“混合点”):对于某些性能敏感或硬实时要求高的组件,可以在初期将它们置于内核态实现。例如关键设备驱动**(如virtio块设备、virtio网络)可以内核态实现以简化设备管理和中断处理,。又如部分文件系统缓存或网络协议的fast-path,为提升性能可以暂时内嵌内核。这些组件一旦在用户态成熟可靠后,可以再迁移出去,从而达到既保证早期可用性又不牺牲长远灵活性的目的。

用户态系统服务:大多数高层子系统则在用户态实现,包括命名服务 (NameSvc)、文件系统服务 (FsSvc)、网络服务 (NetSvc)、驱动服务 (DriverSvc) 等。NameSvc提供统一的服务发现和命名解析机制;FsSvc提供文件和目录语义;NetSvc提供socket接口及协议栈;DriverSvc管理可热插拔或非关键设备的驱动(如USB、音频等)。用户态服务的优点是崩溃不影响内核稳定,而且可以独立重启、更新,实现快速演进。例如文件系统服务可以从简单的内存文件系统逐步升级为支持块设备、缓存、复杂文件系统的模块,而这些升级对内核ABI没有任何影响。

混合内核的优势在于:开发者可以“择优”决定某组件在当前阶段放在哪一侧。例如,在早期Bring-up过程中,如果将virtio驱动移到用户态阻碍进度,可以先内核实现virtio以尽快跑通基本I/O,再在后期把网卡/磁盘驱动迁出内核进入DriverSvc。同时,由于接口始终通过能力和消息,与在用户态实现时一致,迁移过程对上层软件透明(不需要改应用逻辑,只是服务从内核切换到用户进程)。这种弹性使我们能够一开始通过内核集中实现困难部分(减少IPC损耗和调试复杂度),但又不丧失未来模块化的好处。

需要注意,在设计服务拆分时,应避免让内核承担策略。即便某服务暂时在内核实现,它也应该通过同样的对象协议与其他部分交互,像用户态服务一样暴露可版本化接口。这样一来,将其迁移出内核时无需更改接口契约。例如,把文件系统缓存放进内核,也仍通过FsSvc接口(只是在内核中实现);将来移出到用户态时,应用层面几乎无感知变化。

简而言之,本方案倡导内核提供机制,用户态定义策略:利用混合内核的过渡手段确保早期系统可用,同时在架构上保证服务化接口的完整,实现宏内核的性能和微内核的灵活兼得。

  1. 无锁与低锁数据结构设计策略

高并发环境下,锁的使用会带来性能瓶颈和复杂的死锁问题。因此设计中应尽可能减少锁的使用,实现无锁或低锁的并发结构。但完全消除锁在操作系统中并不现实——需要综合策略达到“尽量无锁”的目标。下面总结本系统追求无锁化的策略:

服务化解耦,减少共享状态:将可变的全局状态尽量移出内核,通过消息传递而非共享内存来交互。例如文件系统、网络栈等复杂子系统采用单线程事件循环的服务进程模型,则服务内部天然串行,无需锁(因为只有一个线程处理)。内核侧只保留队列、句柄、内存映射这些相对容易无锁化的机制。相较于在一个巨内核中为每个子系统设计复杂的锁策略,不如通过模块拆分,将需要锁保护的状态限定在服务内部,这样内核的锁争用面大幅降低,整体更加可靠。

分区数据结构 (Per-CPU 原则):尽量避免跨CPU共享,用每CPU/每核独享的数据结构彻底消除竞争点。例如:

每个CPU维护自己的就绪队列 (runqueue) 来调度线程,这样调度器在本地核选取下一个运行线程时无需锁。如果需要跨核迁移任务,可通过消息提示目标CPU自行拉取,而不采用全局共享队列。

AsyncQ可以设计为每线程或每核各自持有提交/完成队列。一个线程的AsyncQ主要由该线程提交和消费,或由所在CPU上的内核worker消费,如此提交和完成皆为单生产者单消费者场景,避免锁。

内存分配采用每CPU缓存。如内核对象的slab分配器可为每个CPU保留一个本地缓存(magazine或per-CPU slab),大部分对象分配/释放在本地完成无锁,只有本地缓存耗尽或过剩时才会触及全局池。

通过上述手段,将绝大多数热路径操作局限在单个CPU上下文内完成,避免频繁的跨核同步。

队列的无锁化设计:对于各种队列/环形缓冲区,尽量将复杂度从多生产者多消费者 (MPMC) 模式降维。MPMC无锁队列是最复杂也最易出错的,可以通过架构设计转化为更简单的情况:

提交队列 (SubmitQueue):如果队列由多个生产者提交操作但由一个固定的消费者处理(例如单个内核worker或特定CPU),那么可采用MPSC(多生产者单消费者)无锁环形队列。这在内核中实现AsyncQ时很适用:多个线程可以并发向某个AsyncQ提交操作,但内核统一在一个执行上下文中依序处理。

完成队列 (CompletionQueue):通常由一个生产者(内核线程或中断处理)写入完成事件,一个消费者(用户线程)读取完成事件,那么这是标准的SPSC(单生产者单消费者)队列。SPSC 队列实现最简单、性能最佳,很多硬件环形缓冲区模型也是这个模式。

通过明确各队列的访问模式,就能针对性地选用优化的数据结构。例如,可以用无锁环形缓冲(如Cache-Aligned Ring Buffer)实现MPSC,或使用写入端无锁、读取端批处理的机制等,确保队列操作既安全又高效。

读多写少场景采用 RCU/版本化:对于某些读频繁、写较少的数据结构,避免在读路径上加锁。可参考RCU (Read-Copy-Update) 或 Sequence Lock 的思想:

读侧不加锁,仅在访问前后做适当的内存屏障保证可见性,这样并发读之间无需同步,开销极低。

写侧则通过复制-更新:每次修改时先拷贝一份数据做更新,再用原子操作切换版本指针或计数,让随后读看到的新版本。旧版本延迟回收,可结合epoch计数或RCU机制,确保没有读者在用旧数据时释放之。

例如内核的对象表、路由表、名字服务表等,多为读多于写,就很适合这种方案。这样读操作完全无锁,写操作虽然需要原子更新,但频率低且能容忍一点加锁或延迟。

利用硬件特性实现极低开销同步:有些操作本身就可以设计为近乎无锁。例如租约撤销 (Lease revoke),它的核心只是设置一个原子布尔标志,和各处使用该租约检查标志而已。撤销时执行alive=false(这是一个原子写),而平时每次用句柄前只需原子读检查该标志有效。这种模式下,没有传统的锁等待,撤销操作即便并发发生也不会阻塞其他线程,属于“天然无锁”的场景。类似的设计应尽量发掘并利用。

尽管以上策略能极大减少锁,但某些地方仍难以完全无锁,只能做到尽量降低锁竞争:

页表与地址空间管理:更新页表涉及修改全局内存映射,对同一进程地址空间一般需要序列化或上锁以避免竞争更新和TLB不同步。建议对单个进程的地址空间操作使用细粒度锁或原子序号,确保同一进程的map/unmap串行执行。同时通过减少修改频率(如大页映射、批量映射)降低锁使用频度。跨不同进程的内存管理则彼此独立,无需全局锁。

内存分配器:完全无锁的通用分配算法非常复杂,而且难以处理内存耗尽等情形。更实际的做法是采用前述每CPU局部缓存技术,使绝大部分分配在本地完成无锁,仅当本地缓存不足/过剩时才使用锁访问全局池。这样“快路径无锁,慢路径上锁”的设计,通常能达到近似无锁的性能效果。

调度器的全局负载均衡:单个CPU的就绪队列可以无锁,但涉及跨CPU迁移任务(工作窃取、负载均衡)时,不可避免出现同步。策略是将负载均衡设计为低频事件,例如由专门的调度线程定期检查,或者各CPU通过消息协议交换负载,而非每次调度都全局竞争锁。这样跨CPU的同步点大幅减少,不影响常态下的调度性能。

实现无锁需要硬件支持。在RISC-V上,应确保启用了A扩展(原子指令,如LR/SC或AMO指令)并正确使用内存屏障指令(fence)来配合原子操作。没有硬件原子的情况下,就只能退而求其次用禁用中断、大锁或单核化等手段,这显然无法满足并发需求。

最后,务实的渐进策略也很重要:初期不必苛求系统彻底无锁,而应设定一个可落地的目标,例如:“热路径不上睡眠锁,不因锁导致阻塞;频繁操作不共享可写状态,尽量做到每CPU独立;仅在慢路径或初始化/回收等少数场景使用锁。”。先保证实现简单正确,再逐步将热点锁替换为无锁结构或分区方案。这种循序优化的方法,可以避免一开始为追求极限性能而牺牲系统稳定性和开发进度。

总而言之,通过合理的架构和数据结构设计,本系统争取在绝大部分场景做到无锁或低锁,并将不可避免的同步点影响降至最低,从而充分发挥多核并行能力。

  1. 最小可用系统路线图(阶段性建议)

为了循序渐进地实现上述设计,下面提出一个最小可用系统 (Minimum Viable System) 的开发路线图,分阶段逐步完善功能。每阶段在保证系统可跑的前提下引入新特性:

阶段 0:基础工具链与内核骨架 – (约1周)搭建交叉编译环境(如riscv64-elf工具链),实现内核启动框架。通过QEMU virt 虚拟板子加载OpenSBI和内核镜像启动,在串行控制台上打印调试信息。建立基本的中断和异常处理框架,确保ecall、页fault等陷入可被捕获并打印。此阶段目标:看到内核启动消息,能够响应基本异常陷入。

阶段 1:内存管理与用户态跳转 – (1-2周)实现分页机制(Sv39页表)和内核/用户地址空间布局。映射内核自身并启用分页,设置用户空间映射并跳转执行一个用户态的简单hello world程序,验证从S态进入U态执行再返回内核的trap流程。此阶段完成后,说明基本的虚存、特权级转换和异常返回机制均正常工作。

阶段 2:句柄表与IPC通道 – (1-2周)实现内核中的对象句柄表以及基本的句柄管理系统调用(close, dup, get_info)。紧接着实现Channel IPC:支持创建通道、发送和接收简单消息。可设计一个内建的测试服务,在内核或用户态等待消息,然后 echo 返回,以验证消息通路和句柄传递机制。此阶段完成标志:不同线程/进程之间可以通过channel收发消息(不一定带句柄),句柄的创建/关闭/查询运转正常。

阶段 3:异步队列与定时器 – (1-2周)引入AsyncQ机制:实现AsyncQ对象,sys_asyncq_create以及submit/poll/cancel接口。同时实现Timer对象以及对应的异步操作(如OP_TIMER_ARM)以测试异步超时和取消。在用户态实现基本的librt事件循环,提交一个定时器超时操作并通过回调打印输出,从而检验AsyncQ、取消、超时、librt协同工作。此阶段后,系统已经具备统一的异步事件处理能力。

阶段 4:核心服务 – NameSvc 与文件服务 – (约2周)实现基本的命名服务和文件系统服务。NameSvc可以开始时内置于内核,提供简单的resolve(name) -> handle和bind(name, handle)注册接口。文件服务可先做一个内存文件系统(ramfs)原型,支持基本的目录和文件对象、以及OPEN/READ/WRITE等方法。可以暂时在内核中实现ramfs(方便直接访问内存结构),通过对象协议提供服务;待验证可行后,再考虑迁移到用户态服务进程。此阶段验收:用户态可以通过NameSvc拿到FsSvc句柄,并使用文件服务的协议创建、读取、写入文件。例如,从ramfs读取一个文件内容并通过串口打印,验证文件I/O链路。

阶段 5:设备驱动支持 – (2-4周)逐步引入对实际硬件设备的支持,至少包括块设备和网络设备。实现内核态的virtio-blk驱动(用于块存储)和virtio-net驱动(用于网络接口),以及PLIC中断管理,确保能收发中断。将文件系统服务从ramfs扩展为可调用块设备读写(例如实现一个简单的FAT文件系统或基于块设备的文件系统),网络服务则建立收发包的基本骨架。此阶段完成标志:能够从虚拟磁盘加载文件,或者通过virtio-net收发简单的UDP包(哪怕是固定协议);系统由此具备与外界环境交互的基础。

阶段 6:能力撤销与安全沙箱 – (持续并行推进)实现Lease租约机制以支持能力撤销,以及进程spawn等安全模型相关功能。例如,引入sys_lease_create/revoke并将服务句柄挂到租约上,一旦某应用结束或权限需要收回,只需revoke租约即可撤销其能力。实现sys_spawn,允许启动新进程时显式传递初始句柄和命名空间,而不再默认继承整个父进程环境。构建用户态的权限管理服务,负责策略决策(如某应用请求网络访问时由策略服务决定是否授予相应SocketCap)。这一阶段并无明确“完成”标志,而是逐步增强系统的安全体系。在此过程中应始终验证:权限传递和撤销如预期运作,拒绝策略能正确阻止未授权操作。

上述阶段完成后,一个基本的但功能完整的操作系统就诞生了:它拥有微内核风格的核心,服务化的文件系统和网络栈,设备驱动可用,具备能力安全和异步并发特性。后续还可以进一步完善:

POSIX兼容层(可选并行):为了利用现有生态,可在用户态实现一个POSIX API的转换层。如提供标准的libc调用,将open/read/write/fork/exec等转译为对应的对象协议请求。兼容层需要维护进程自己的文件描述符(fd)表,将fd映射为内部handle,并在必要时模拟一些语义(如select/poll可以依赖AsyncQ实现或在兼容层做阻塞等待)。重要的是,兼容层绝不能反过来修改内核ABI设计——它只是适配层,内核仍保持新接口理念。这保证我们可以逐步移植常用Unix软件(shell、工具链等)到新系统上来,帮助生态过渡。

丰富驱动与应用支持:在基本OS可用之后,还需要添加更多驱动(例如USB、人机交互设备GPU/显示等)以及更高级的系统服务(图形窗口服务、脚本运行时等等),这些可根据项目定位(服务器/嵌入式/桌面)按需添加。

总之,路线图强调分阶段可运行:每阶段都提供一个可启动、可测试的系统快照,在此基础上迭代扩展功能。这样既能快速验证设计理念(能力机制、异步模型等)的有效性,又能及时发现问题并调整,实现稳健的发展。

  1. 开发与调试建议(跟踪 Trace、记录/回放、模拟模式)

为了提高操作系统的可调试性和开发效率,建议在架构层面从第一天起就内建一些调试辅助功能。以下是本方案在开发/调试方面的建议:

内核跟踪 (Tracing):实现一个内核级环形缓冲追踪机制,用于记录关键事件。例如,每次系统调用提交/完成、IPC消息发送/接收、线程上下文切换、页fault异常等,都以结构化记录写入内核trace缓冲区。记录内容应包括时间戳、事件类型、重要参数(如调用号、句柄值、地址等)以及当前线程/进程标识等。为了安全和性能考虑,trace缓冲可设置为固定大小的内存环,并仅允许具有特定INSPECT权限的进程通过只读方式映射读取。这样既保证普通应用无法窥探内核敏感信息,又允许调试工具或诊断服务获取trace数据。通过trace,开发者可以在运行时观察系统行为时序,有助于分析性能瓶颈、竞争情况以及逻辑错误。

记录/回放 (Record/Replay):强烈建议实现对异步事件的记录和回放功能。具体做法是在用户态的librt中,对所有提交的异步操作、分配的token、收到的完成事件按照时间顺序记录日志。日志可以记录在内存或定期刷写磁盘。在出现难以复现的并发Bug时,可以启用模拟模式 (sim mode) 来重放这些记录。模拟模式下,构造一个特殊的调试运行时(Handler)替代实际内核,将记录的操作按顺序喂入并模拟其效果,再将之前记录的完成事件按原先次序反馈给应用。这使得复杂的竞态条件可以100%重现,方便调试。此外,在回放时还可以加入特殊策略,如对事件执行插入随机延迟、改变完成顺序、注入伪造超时或错误等,模拟更苛刻的并发情形,达到类Fuzz测试的效果。通过record/replay,开发者既能捕获现场、诊断问题,又能验证并提高系统在非常规调度下的健壮性。

模拟执行模式:结合上述record/replay思想,也可以设计一个纯用户态的内核模拟器。利用函数式编程思路,把系统调用和操作抽象为中间表示(如Op数据结构),让真实内核和一个模拟解释器都能执行它。真实运行时记录这些Ops,模拟器则可在离线或不同环境中重新执行Ops序列,检查是否得到一致结果。这种方法可扩展用于指令级模拟(在指令集模拟器中跑操作序列)或者状态验证(比较两次执行结果差异)。虽然实现复杂,但有助于长期维护内核的确定性和可测试性。

断言和验证:在开发时于内核和关键服务中大量使用断言(assert)检查不变量,如引用计数不为负、锁状态正确持有、队列索引不溢出等。一旦触发断言,进入调试shell或dump出trace缓冲供分析。虽然assert在生产版本需移除或转为日志,但在开发阶段能极大加速问题定位。

可选的内核调试功能:如支持单步执行或执行轨迹记录,可以集成RISC-V的调试支持(通过OpenOCD或QEMU gdbstub连接gdb)来单步跟踪内核代码。同时,由于我们具备能力机制,可以考虑实现一个受控的“调试能力 (DebugCap)”:授予调试服务,可以检查/修改其他进程内存、设置断点、注入信号等,用以搭建一个类似内核态调试器的服务。调试能力需慎重设计权限,确保仅可信主体使用且不会滥用。

总而言之,在新操作系统的开发过程中,观察和调试机制必须先行。良好的追踪和回放支持能极大减少解决复杂并发bug所需的时间成本;而模拟和断言机制确保我们可以在引入新特性时快速验证假设、重放问题。通过以上工具的加持,开发团队可以更自信地演进系统架构,同时保持对系统行为的可控可测,从而实现我们的最终目标:一个可持续演进、高可靠性的操作系统。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published