news 2026/9/10 11:25:58

SerenityOS 内核开发模式与规范指南:从 OOM 安全到设备驱动编写的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SerenityOS 内核开发模式与规范指南:从 OOM 安全到设备驱动编写的完整实践

SerenityOS 内核开发模式与规范指南:从 OOM 安全到设备驱动编写的完整实践

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

SerenityOS(Serenity Operating System)是一个从零开始、以注重代码整洁与可读性著称的类 Unix 操作系统。本文以仓库内的 Documentation/Kernel/DevelopmentGuidelines.md 为核心骨架,系统梳理内核开发者(尤其是新手开发者)在创建、修改与移除内核代码时必须遵循的开发模式与规范。你将掌握:如何在 OOM(内存耗尽)场景下写出安全的分配代码、如何用FixedStringBuffer规避内核堆分配、如何保持"不破坏用户态"的提交纪律、如何遵守 SMP 锁顺序、如何为新型设备正确申请主设备号、如何用try_create_device构造设备对象,以及内核代码的文档化要求。文中所有结论均以当前仓库源码为据,并给出精确的文件路径与代码位置,便于你对照源码深入理解。

文档定位与阅读前提

这份 DevelopmentGuidelines.md 是一份面向"内核开发者"的行为与模式指南,而不是 API 参考手册。文档开篇即说明其意图:指导新手内核开发者在创建、修改和移除内核代码时遵循既定模式;同时明确要求提交 Pull Request 之前阅读本文件、CONTRIBUTING.md(通用贡献指南)以及面向整个代码库的 Documentation/Patterns.md。

文档本身由项目长期内核开发的经验沉淀而成,覆盖的主题相互独立,每个主题都对应一套可落地的代码模式:

  • 内存不足(OOM)时如何安全处理分配失败;
  • 用栈上定长缓冲替代堆分配的FixedStringBuffer方案;
  • 内核变更与用户态兼容性的 git 提交纪律;
  • 拒绝为无用户场景膨胀内核功能;
  • SMP 环境下正确的加锁模式与死锁预防;
  • 新增系统调用的严格审查标准;
  • 安全缓解措施的底线原则;
  • 禁止硬编码用户态路径的抽象层纪律;
  • 新设备主设备号的分配规则;
  • Device派生对象的统一构造入口;
  • 内核文档的撰写要求。

下文将逐节展开,并在每一节结合仓库源码给出佐证与深化说明。

内核中的 OOM 处理:TRY()语义与adopt_*家族

问题本质:内核分配失败不可忽略

OOM(Out of Memory)是内核代码必须正面解决的"头等大事"。文档明确指出,OOM 的成因可能是分配请求过于"贪婪"而无法满足,也可能仅仅是物理内存页已无法再分配。在任何一种情况下,内核代码都不能像用户态那样"忽略"失败继续运行——一次未检查的分配失败可能直接导致空指针解引用、内核崩溃或系统状态损坏。

标准解法:TRY()+adopt_nonnull_own_or_enomem

文档给出的标准模式是:始终使用TRY()语义,配合恰当的adopt_*函数(OwnPtrRefPtr对应版本)

#include <AK/Try.h> #include <AK/OwnPtr.h> // ... auto new_object = TRY(adopt_nonnull_own_or_enomem(new (nothrow) Object(...)));

这一模式在失败时会将ENOMEM错误码一路向上传播,最终到达系统调用入口,让用户态程序得知内存不足并做出相应处理。

结合源码,我们可以把这条链路看得更清楚:

  • TRY宏定义在 AK/Try.h。它把表达式结果暂存,若is_error()则立即return _temporary_result.release_error(),否则返回release_value()。注意TRY依赖 GNU/Clang 语句表达式扩展,且通过static_assert禁止从 fallible 表达式中返回引用。
  • adopt_nonnull_own_or_enomem定义在 AK/NonnullOwnPtr.h:若指针为空返回Error::from_errno(ENOMEM),否则以 Adopt 语义接管所有权。配套的try_make(AK/NonnullOwnPtr.h)等价于adopt_nonnull_own_or_enomem(new (nothrow) T(...))
  • RefPtr版本adopt_nonnull_ref_or_enomem位于 AK/NonnullRefPtr.h 附近,语义完全对称。
  • MUST宏(AK/Try.h)则是"绝不失败"场景的强断言变体,用VERIFY(!is_error())直接终止。

例外情况:无法向用户态传播错误时

文档同时给出了一个重要例外:当错误码根本没有途径传回用户态时(例如 IDE ATA 代码中的ATAPort异步读取硬盘数据,因为异步操作无法把errno送回用户态),内部函数仍应使用ErrorOr<>返回类型,而在主调用函数中改用内核中其他有意义的设施(如设备状态、日志)来指示操作失败。这条原则保证了"错误必须被显式表达",只是表达方式随场景而变。

KString 与 FixedStringBuffer:用"零堆分配"彻底规避 OOM

两类 OOM 防护思路

文档指出,内核代码要做到 OOM 安全,有两条互补路径:

  1. 允许错误传播(上一节的TRY+adopt_*);
  2. 尽可能消除堆分配——既然不分配,就不会因分配失败而 OOM。

FixedStringBuffer就是第二条路径的产物,它被引入 AK 库并在内核系统调用处理代码中被大量使用。

设计动机:短字符串不该走堆

思路非常朴素:如果在一次系统调用中,被检查的字符串已知最大长度且相对较短(不超出栈大小),那么就可以直接从用户态拷贝到栈上存储,而不是做一次堆分配去创建KString。文档给出的上限参考是 1024 字节(理论上也可扩大栈大小)。尤其当字符串只在整个系统调用处理作用域内被检查时,堆分配对内存资源是浪费,也会给内核内存管理器增加无谓压力。

实际应用:进程与线程名称

ProcessThread类使用FixedStringBuffer存储名称,从而彻底绕开"为名称分配堆存储导致 OOM"的历史问题。源码佐证:在 Kernel/Tasks/Process.h 中可以看到:

using Name = FixedStringBuffer<32>; RecursiveSpinlockProtected<Name, LockRank::None> const& name() const; void set_name(StringView);

进程名被固定为 32 字节的栈上缓冲,且用RecursiveSpinlockProtected包裹以保障并发安全。

安全防护机制

FixedStringBuffer(实现见 AK/FixedStringBuffer.h)内置了多项安全防护:

  • 存储时清零store_characters在写入新StringView后会将其余字节全部置零(AK/FixedStringBuffer.h)。这在处理用户态传入多段以空字符分隔的字符串时尤为重要——内核只关心第一个空字符之前的内容。
  • 超长截断:存储长度被限制在min(Size, characters.length())内,超出部分直接丢弃。
  • 用户态拷贝安全:内核专用的copy_characters_from_user(AK/FixedStringBuffer.h)先校验user_str_size > Size即返回EFAULT,再校验地址范围是否属于用户空间,随后通过safe_strnlen/safe_memcpy进行容错拷贝,任何故障都以EFAULT返回而非默默截断。
  • 辅助函数Kernel/Library/StdLib.h提供try_copy_string_from_user_into_fixed_string_buffertry_copy_name_from_user_into_fixed_string_buffer(Kernel/Library/StdLib.h)等帮助函数,对输入大小超限的情况返回错误而不是截断后继续执行;另有copy_fixed_string_buffer_including_null_char_to_user(Kernel/Library/StdLib.h)用于把带结尾空字符的缓冲区拷回用户态。
  • 格式化构造FixedStringBuffer<Size>::formatted(...)通过仅使用内联容量的StringBuilder直接格式化并存储,可用于内核符号打印等场景(见 Kernel/KSyms.cpp 中FixedStringBuffer<28>::formatted("Kernel + {:p}\n"sv, ...)的用法)。

这条"能栈上就栈上、能定长就定长"的原则,与上一节的TRY传播机制互为表里,共同构成内核 OOM 安全的完整闭环。

我们绝不破坏用户态:SerenityOS 版本的不变式

与 Linux 的差异:不关心 ABI/API 稳定,但关心行为

文档在此处专门区分了 SerenityOS 与 Linux 的立场:我们不关心用户态与内核之间的 ABI/API 破坏,但绝不允许"内核改动导致用户态出现异常行为而用户态又未被适当考虑"

换句话说,内核与用户态属于同一个 git 仓库、同一套代码库,可以同步修改接口;真正不可接受的是"偷偷改了内核、把用户态晾在一边"。

破坏发生的典型场景与 git 纪律

  • 通常不破坏用户态:新的存储设备驱动、新的硬件支持等内部变更,只要实现既有抽象接口,用户态根本无须关心具体StorageDevice的细节。
  • 可能破坏用户态:用户态与内核之间的 ABI/API 变更,主要集中在系统调用处理层。
  • 正确做法:在 git 层面,把"肇事"的内核改动与配套的用户态适配改动放进同一个 commit,从而保证每一个 commit 都能独立二分(bisectable)

文档还进一步强调:对内核的改动应当用用户态工具进行测试,以确认没有给用户态功能制造异常;并且——比上面更严格——除非能明确判定某功能无人使用,否则不得移除功能;即使确认无人使用,也应考虑如何让该功能对社区更可用、更易获取。

配套阅读

与"用户态测试内核"相关的实践,可参考仓库中的 Documentation/RunningTests.md(运行测试指南)。内核侧还有大量用用户态工具/测试佐证内核行为的用例,例如 Tests/Kernel 目录下针对内核行为的测试程序。

每个内核功能都应有用户态用例背书

这条原则是上一条的"镜像":内核不应该为没人需要的东西膨胀。文档给出两个实例:

  • 项目早期曾有一个软盘(floppy)驱动,因无人使用而被移除;
  • 当 Intel AC97 声卡驱动引入后,SB16 声卡驱动很快被移除。

结论很直白:**我们没有兴趣支持没人用的硬件,也没有兴趣维护对大多数人没有意义的内核特性。**这既是去冗余的手段,也是保持内核小而美、可维护的关键。从仓库现状看,Kernel/Devices/Storage 下保留的都是有真实用途的存储控制器驱动(IDE/ATA、AHCI、NVMe、RAM 磁盘等),与文档所述"按需保留"的理念一致。

正确的加锁:SMP 下的锁顺序与保护容器

为什么锁是内核开发的头等大事

DevelopmentGuidelines.md 指引读者参考 Documentation/Kernel/AHCILocking.md 详细了解 AHCI 驱动中的加锁模式。文档强调:SerenityOS 认真对待SMP(对称多处理),因此正确加锁是内核开发思维中的最高优先级之一。

通用规则

  • 禁止在持有Spinlock之后再获取Mutex(因为获取 Mutex 可能睡眠,而 Spinlock 临界区不允许睡眠);
  • Spinlock 之间嵌套一般允许,但必须始终保持相同的获取顺序,否则会产生死锁;
  • 优先使用MutexProtected<>SpinlockProtected<>这两个 C++ 容器,把锁"附着"到具体共享数据对象上,而不是在类里随便放一把"裸" spinlock。

源码佐证

  • Kernel/Locking/MutexProtected.h 定义template<typename T> class MutexProtected,通过 RAII 访问器(with 回调)把数据访问与 Mutex 绑定;
  • Kernel/Locking/SpinlockProtected.h 定义template<typename T, LockRank Rank> class SpinlockProtected,基于 Kernel/Locking/SpinlockProtectedBase.h 实现;
  • 实际使用例:Process::name使用RecursiveSpinlockProtected<Name, LockRank::None>(Kernel/Tasks/Process.h);coredump 目录路径使用RecursiveSpinlockProtected<OwnPtr<KString>, LockRank::None>(Kernel/Tasks/Coredump.cpp)。

实战建议(结合源码模式)

当你需要为某共享数据加锁时,按如下优先级选择:

  1. 数据属于某个受保护对象 → 用MutexProtected<T>SpinlockProtected<T, Rank>包裹;
  2. 需要可重入 → 选择Recursive前缀版本(如RecursiveSpinlockProtected);
  3. 必须在不可睡眠上下文持锁 → 使用 Spinlock 系容器而非 Mutex 系;
  4. 多个锁同时持有时 → 为它们确立并固定全局顺序,并在代码注释中写明顺序约定。

系统调用接口:干净、明确、以 POSIX 为准

现状与基调

截至文档写作时,系统调用表总体相当稳定。这种稳定源于一个事实:现有 syscall 定义良好,且有广为人知的 POSIX 接口背书。因此文档要求:

  • 对新增 syscall 的建议/补丁应被严格审查——它原则上应是我们"最后的手段",优先考虑其他已有的 Unix 接口;
  • 由于不存在放之四海而皆准的"是/否"答案,引入新 syscall 的 PR 必然要经历讨论;
  • 尽可能避免架构相关的 syscall(Linux 曾有过大量此类调用,最终大多被移除)。

为什么大多数场景不需要新 syscall

文档以存储子系统为例:新驱动只需实现既有的StorageDevice抽象,注册为普通StorageDevice后,就会自动暴露在/dev下,write/open/read/ioctl等既有 syscall 立即可用——完全不需要为特定硬件发明新系统调用

这背后的机制与 Kernel/API/MajorNumberAllocation.h 的设备号体系以及 Kernel/Devices/Device.h 的try_create_device流程直接相关,详见后文"设备对象构造"一节。

安全措施:底线是不削弱既有缓解

SerenityOS 把安全信息视为严肃课题。内核中已实现大量安全缓解措施,文档指向 Base/usr/share/man/man7/Mitigations.md(内核安全缓解措施的 man page)。

作为内核开发者,要求比系统其余部分更严格,核心底线是:

  • 绝不削弱任何已实现的安全措施(这是最低要求);
  • 若能改进某安全措施而不损害其他措施,则非常欢迎;
  • 安全与性能这两个往往相互矛盾的目标之间需要权衡,出现分歧时应进行讨论。

禁止硬编码用户态路径:抽象层纪律

为了保持内核面对未来变化的灵活性,内核代码不得硬编码路径,也不得假设文件系统条目(或文件系统挂载点)位于何处——路径信息必须始终由用户态告知内核,内核不做任何假设。

唯一例外

当某个文件显然总会位于特定路径时,在内核中硬编码它仍然被视为违反抽象层。唯一的例外是:内核用dbgln语句警告用户"动态加载器不是我们通常使用的那个二进制"。更一般化的例外表述是:"谨慎使用"的、带路径假设的调试消息可以接受,但绝不能对用户产生任何功能性影响

这条纪律保证了抽象层完整干净,也是"用户态告知、内核执行"这一分工模式的具体体现。

新设备主设备号分配:MajorNumberAllocation 规则

背景

操作系统中的主设备号(major number)按**设备类型(设备家族)**分配。SerenityOS 的所有主设备号分配集中在 Kernel/API/MajorNumberAllocation.h。该文件开头的注释也明确指引:请参阅 DevelopmentGuidelines.md 了解如何新增分配。

六条分配规则

文档给出了明确的六条规则:

  1. 分配要么是新型块设备,要么是新型字符设备,不能两者兼得(按设备类型区分);
  2. 家族名字符串未被任何块/字符设备占用
  3. 家族名字符串信息丰富且简短
  4. 插入对应枚举(CharacterDeviceFamilyBlockDeviceNumber)时使用CamelCase名称;
  5. 插入对应 to-StringView 函数(character_device_family_to_string_viewblock_device_family_to_string_view)时使用snake_case名称并给出实际分配,例如:
ALWAYS_INLINE StringView character_device_family_to_string_view(CharacterDeviceFamily family) { switch (family) { ... case CharacterDeviceFamily::Generic: return "generic"sv; ... } }

同时,还需在s_character_device_numberss_block_device_numbers数组中添加条目:

static constexpr CharacterDeviceFamily s_character_device_numbers[] = { ... CharacterDeviceFamily::Generic, ... };
  1. 必须按主设备号升序插入到对应枚举与 to-StringView 函数中。

源码实测:现状与自校验

查看 Kernel/API/MajorNumberAllocation.h 可以看到当前分配(节选):

  • 字符设备家族(CharacterDeviceFamily,L20-L34):Generic = 1DeviceControl = 2Serial = 4Console = 5Mouse = 10GPURender = 28VirtualConsole = 35Keyboard = 85Audio = 116MasterPTY = 200SlavePTY = 201GPU = 226VirtIOConsole = 229
  • 块设备家族(BlockDeviceFamily,L103-L110):Storage = 3Loop = 20KCOV = 30(仅在启用内核覆盖率收集时)、StoragePartition = 100

值得注意的是,该文件用constexpr函数 +static_assert编译期强制校验升序

constexpr bool assert_character_device_numbers_are_in_order() { unsigned major = 0; for (auto& allocation : s_character_device_numbers) { if (to_underlying(allocation) < major) return false; major = to_underlying(allocation); } return true; } static_assert(assert_character_device_numbers_are_in_order());

(见 Kernel/API/MajorNumberAllocation.h,块设备同理见 L121-L131)。这意味着"升序规则"不仅仅是文档约定,而是被编译期断言强制执行的硬约束——违反升序直接编译失败。

构造 Device 派生对象:统一走try_create_device

为什么需要一个统一入口

当前内核中大量设备在启动时插入,也有设备事后插入。为简化驱动编写,构造Device派生类对象的推荐模式是调用Device::try_create_device方法。

示例:VirtIOGPU3DDevice

文档给出的示例完整保留了构造流程的每一步:

ErrorOr<NonnullLockRefPtr<VirtIOGPU3DDevice>> VirtIOGPU3DDevice::try_create(VirtIOGraphicsAdapter& adapter) { // Set up memory transfer region auto region_result = TRY(MM.allocate_kernel_region( NUM_TRANSFER_REGION_PAGES * PAGE_SIZE, "VIRGL3D kernel upload buffer"sv, Memory::Region::Access::ReadWrite, AllocationStrategy::AllocateNow)); auto kernel_context_id = TRY(adapter.create_context()); return TRY(Device::try_create_device<VirtIOGPU3DDevice>(adapter, move(region_result), kernel_context_id)); }

源码原理:after_inserting()是关键

之所以必须使用try_create_device,是因为该方法会调用虚函数Device::after_inserting(),而后者执行关键的初始化步骤:注册设备并通过常规用户态接口将其暴露出去。

实现在 Kernel/Devices/Device.h:

template<typename DeviceType, typename... Args> static inline ErrorOr<NonnullRefPtr<DeviceType>> try_create_device(Args&&... args) { auto device = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) DeviceType(forward<Args>(args)...))); TRY(static_ptr_cast<Device>(device)->after_inserting()); return device; }

after_inserting()的实现(Kernel/Devices/Device.cpp)会创建 SysFS 设备组件、把设备加入设备标识目录、注册到设备管理表——这正是"自动出现在/dev并可被常规 syscall 使用"的底层来源。其他驱动的实际用法可对照 Kernel/Devices/Audio/Channel.cpp(AudioChannel::create)与 Kernel/Devices/GPU/3dfx/VoodooDisplayConnector.cpp(VoodooDisplayConnector::create)等。

可以看到,try_create_device本身就是上一节 OOM 模式(TRY+adopt_nonnull_ref_or_enomem)在设备子系统中的直接应用——整个设备构造链都是 OOM 安全的。

内核文档:写、写好、写到位

最后,文档强调内核开发者应当重视文档建设:

  • 欢迎两种形式:新增 man page,或在Documentation/Kernel目录下描述内核概念,供其他开发者理解;
  • 没有强制的文档模板,但至少应有开头段落说明文档主题,让读者一眼知道这篇文档讲什么。

当前Documentation/Kernel目录中已有 AHCILocking.md(AHCI 加锁模式)、Containers.md(内核容器)、GraphicsSubsystem.md(图形子系统)、IOWindow.md(I/O 窗口)、ProcFSIndexing.md(ProcFS 索引)与 RAMFS.md 等,正是这条理念的实践成果。

总结:一份可以对照源码逐条核对的开发清单

将上述内容浓缩为一份可操作的内核开发自查清单:

  1. OOM 安全:所有可能失败的分配一律TRY(...)+adopt_nonnull_own_or_enomem/adopt_nonnull_ref_or_enomem(或try_make),错误码向上传播到 syscall 入口;无法传播时用内核设施显式表达失败。
  2. 少分配、不分配:短字符串(如进程/线程名)用FixedStringBuffer放栈上;从用户态拷入时用try_copy_*_into_fixed_string_buffer等辅助函数并处理EFAULT
  3. 提交纪律:内核改动 + 配套用户态改动进同一个 commit,保证每个 commit 可二分;改动必须用用户态工具验证;不随意移除无人使用的功能。
  4. 功能克制:不为无人使用的硬件/特性写驱动;移除功能要按 git 纪律处理。
  5. 加锁:持 Spinlock 时不取 Mutex;多把 Spinlock 严格按固定顺序获取;共享数据用MutexProtected/SpinlockProtected容器包裹。
  6. syscall:先找既有 Unix 接口,新增 syscall 是最后手段;不做架构相关 syscall;新硬件优先走既有设备抽象。
  7. 安全:绝不削弱既有缓解措施;改进需不损害其他措施;安全与性能权衡要讨论。
  8. 路径:内核不硬编码用户态路径;调试消息中的路径假设不得有功能影响。
  9. 设备号:按 Kernel/API/MajorNumberAllocation.h 六条规则分配主设备号,保持升序(编译期断言强制),CamelCase 枚举 + snake_case 字符串一一对应,并同步更新s_*_device_numbers数组。
  10. 设备构造:一律经Device::try_create_device<T>(...)after_inserting()完成注册与/dev暴露。
  11. 文档:内核改动尽量配套 man page 或Documentation/Kernel下的概念文档,至少写清主题开头段。

这套清单覆盖了从内存管理、并发、系统调用到设备子系统与工程协作的完整开发链路。对于任何想要为 SerenityOS 内核贡献代码的开发者,DevelopmentGuidelines.md 都是出发前必读的第一份材料,而本文则提供了与仓库源码一一对应的深度解读,方便你在实际编码时快速定位依据。

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 11:25:56

GE图引擎SetInput API

SetInput 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的…

作者头像 李华
网站建设 2026/9/10 11:24:25

AT89C51+MAX487 RS485自动收发时序设计与Proteus仿真

简介&#xff1a;本资源是一套面向嵌入式初学者与电子工程实践者的RS485自动收发通信完整仿真方案&#xff0c;聚焦AT89C51单片机与MAX487收发器协同实现半双工总线通信的核心技能训练&#xff0c;适用于工业通信、多节点传感网络等典型应用场景。压缩包共27个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/10 11:22:48

SEO综合查询工具与网站分析工具的区别:从排名到转化

1. 同一个网站&#xff0c;两张报表对不上&#xff1a;先把这个最扎心的场景说清楚做SEO的人&#xff0c;大概率都遇到过这么一幕&#xff1a;周五下午准备周报&#xff0c;你打开某个SEO综合查询工具&#xff0c;看着自己盯了两周的三个核心词从第二页爬到了首页前五&#xff…

作者头像 李华