资讯动态

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

发布时间:2026/9/10 11:26:04 来源:尧图企业网站定制
SerenityOS 内核开发模式与规范指南从 OOM 安全到设备驱动编写的完整实践【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenitySerenityOSSerenity 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_*家族问题本质内核分配失败不可忽略OOMOut of Memory是内核代码必须正面解决的头等大事。文档明确指出OOM 的成因可能是分配请求过于贪婪而无法满足也可能仅仅是物理内存页已无法再分配。在任何一种情况下内核代码都不能像用户态那样忽略失败继续运行——一次未检查的分配失败可能直接导致空指针解引用、内核崩溃或系统状态损坏。标准解法TRY()adopt_nonnull_own_or_enomem文档给出的标准模式是始终使用TRY()语义配合恰当的adopt_*函数OwnPtr或RefPtr对应版本#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_makeAK/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 安全有两条互补路径允许错误传播上一节的TRYadopt_*尽可能消除堆分配——既然不分配就不会因分配失败而 OOM。FixedStringBuffer就是第二条路径的产物它被引入 AK 库并在内核系统调用处理代码中被大量使用。设计动机短字符串不该走堆思路非常朴素如果在一次系统调用中被检查的字符串已知最大长度且相对较短不超出栈大小那么就可以直接从用户态拷贝到栈上存储而不是做一次堆分配去创建KString。文档给出的上限参考是 1024 字节理论上也可扩大栈大小。尤其当字符串只在整个系统调用处理作用域内被检查时堆分配对内存资源是浪费也会给内核内存管理器增加无谓压力。实际应用进程与线程名称Process和Thread类使用FixedStringBuffer存储名称从而彻底绕开为名称分配堆存储导致 OOM的历史问题。源码佐证在 Kernel/Tasks/Process.h 中可以看到using Name FixedStringBuffer32; RecursiveSpinlockProtectedName, 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_userAK/FixedStringBuffer.h先校验user_str_size Size即返回EFAULT再校验地址范围是否属于用户空间随后通过safe_strnlen/safe_memcpy进行容错拷贝任何故障都以EFAULT返回而非默默截断。辅助函数Kernel/Library/StdLib.h提供try_copy_string_from_user_into_fixed_string_buffer、try_copy_name_from_user_into_fixed_string_bufferKernel/Library/StdLib.h等帮助函数对输入大小超限的情况返回错误而不是截断后继续执行另有copy_fixed_string_buffer_including_null_char_to_userKernel/Library/StdLib.h用于把带结尾空字符的缓冲区拷回用户态。格式化构造FixedStringBufferSize::formatted(...)通过仅使用内联容量的StringBuilder直接格式化并存储可用于内核符号打印等场景见 Kernel/KSyms.cpp 中FixedStringBuffer28::formatted(Kernel {:p}\nsv, ...)的用法。这条能栈上就栈上、能定长就定长的原则与上一节的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 定义templatetypename T class MutexProtected通过 RAII 访问器with 回调把数据访问与 Mutex 绑定Kernel/Locking/SpinlockProtected.h 定义templatetypename T, LockRank Rank class SpinlockProtected基于 Kernel/Locking/SpinlockProtectedBase.h 实现实际使用例Process::name使用RecursiveSpinlockProtectedName, LockRank::NoneKernel/Tasks/Process.hcoredump 目录路径使用RecursiveSpinlockProtectedOwnPtrKString, LockRank::NoneKernel/Tasks/Coredump.cpp。实战建议结合源码模式当你需要为某共享数据加锁时按如下优先级选择数据属于某个受保护对象 → 用MutexProtectedT或SpinlockProtectedT, Rank包裹需要可重入 → 选择Recursive前缀版本如RecursiveSpinlockProtected必须在不可睡眠上下文持锁 → 使用 Spinlock 系容器而非 Mutex 系多个锁同时持有时 → 为它们确立并固定全局顺序并在代码注释中写明顺序约定。系统调用接口干净、明确、以 POSIX 为准现状与基调截至文档写作时系统调用表总体相当稳定。这种稳定源于一个事实现有 syscall 定义良好且有广为人知的 POSIX 接口背书。因此文档要求对新增 syscall 的建议/补丁应被严格审查——它原则上应是我们最后的手段优先考虑其他已有的 Unix 接口由于不存在放之四海而皆准的是/否答案引入新 syscall 的 PR 必然要经历讨论尽可能避免架构相关的 syscallLinux 曾有过大量此类调用最终大多被移除。为什么大多数场景不需要新 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 了解如何新增分配。六条分配规则文档给出了明确的六条规则分配要么是新型块设备要么是新型字符设备不能两者兼得按设备类型区分家族名字符串未被任何块/字符设备占用家族名字符串信息丰富且简短插入对应枚举CharacterDeviceFamily或BlockDeviceNumber时使用CamelCase名称插入对应 to-StringView 函数character_device_family_to_string_view或block_device_family_to_string_view时使用snake_case名称并给出实际分配例如ALWAYS_INLINE StringView character_device_family_to_string_view(CharacterDeviceFamily family) { switch (family) { ... case CharacterDeviceFamily::Generic: return genericsv; ... } }同时还需在s_character_device_numbers或s_block_device_numbers数组中添加条目static constexpr CharacterDeviceFamily s_character_device_numbers[] { ... CharacterDeviceFamily::Generic, ... };必须按主设备号升序插入到对应枚举与 to-StringView 函数中。源码实测现状与自校验查看 Kernel/API/MajorNumberAllocation.h 可以看到当前分配节选字符设备家族CharacterDeviceFamilyL20-L34Generic 1、DeviceControl 2、Serial 4、Console 5、Mouse 10、GPURender 28、VirtualConsole 35、Keyboard 85、Audio 116、MasterPTY 200、SlavePTY 201、GPU 226、VirtIOConsole 229块设备家族BlockDeviceFamilyL103-L110Storage 3、Loop 20、KCOV 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文档给出的示例完整保留了构造流程的每一步ErrorOrNonnullLockRefPtrVirtIOGPU3DDevice 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 buffersv, Memory::Region::Access::ReadWrite, AllocationStrategy::AllocateNow)); auto kernel_context_id TRY(adapter.create_context()); return TRY(Device::try_create_deviceVirtIOGPU3DDevice(adapter, move(region_result), kernel_context_id)); }源码原理after_inserting()是关键之所以必须使用try_create_device是因为该方法会调用虚函数Device::after_inserting()而后者执行关键的初始化步骤注册设备并通过常规用户态接口将其暴露出去。实现在 Kernel/Devices/Device.htemplatetypename DeviceType, typename... Args static inline ErrorOrNonnullRefPtrDeviceType try_create_device(Args... args) { auto device TRY(adopt_nonnull_ref_or_enomem(new (nothrow) DeviceType(forwardArgs(args)...))); TRY(static_ptr_castDevice(device)-after_inserting()); return device; }after_inserting()的实现Kernel/Devices/Device.cpp会创建 SysFS 设备组件、把设备加入设备标识目录、注册到设备管理表——这正是自动出现在/dev并可被常规 syscall 使用的底层来源。其他驱动的实际用法可对照 Kernel/Devices/Audio/Channel.cppAudioChannel::create与 Kernel/Devices/GPU/3dfx/VoodooDisplayConnector.cppVoodooDisplayConnector::create等。可以看到try_create_device本身就是上一节 OOM 模式TRYadopt_nonnull_ref_or_enomem在设备子系统中的直接应用——整个设备构造链都是 OOM 安全的。内核文档写、写好、写到位最后文档强调内核开发者应当重视文档建设欢迎两种形式新增 man page或在Documentation/Kernel目录下描述内核概念供其他开发者理解没有强制的文档模板但至少应有开头段落说明文档主题让读者一眼知道这篇文档讲什么。当前Documentation/Kernel目录中已有 AHCILocking.mdAHCI 加锁模式、Containers.md内核容器、GraphicsSubsystem.md图形子系统、IOWindow.mdI/O 窗口、ProcFSIndexing.mdProcFS 索引与 RAMFS.md 等正是这条理念的实践成果。总结一份可以对照源码逐条核对的开发清单将上述内容浓缩为一份可操作的内核开发自查清单OOM 安全所有可能失败的分配一律TRY(...)adopt_nonnull_own_or_enomem/adopt_nonnull_ref_or_enomem或try_make错误码向上传播到 syscall 入口无法传播时用内核设施显式表达失败。少分配、不分配短字符串如进程/线程名用FixedStringBuffer放栈上从用户态拷入时用try_copy_*_into_fixed_string_buffer等辅助函数并处理EFAULT。提交纪律内核改动 配套用户态改动进同一个 commit保证每个 commit 可二分改动必须用用户态工具验证不随意移除无人使用的功能。功能克制不为无人使用的硬件/特性写驱动移除功能要按 git 纪律处理。加锁持 Spinlock 时不取 Mutex多把 Spinlock 严格按固定顺序获取共享数据用MutexProtected/SpinlockProtected容器包裹。syscall先找既有 Unix 接口新增 syscall 是最后手段不做架构相关 syscall新硬件优先走既有设备抽象。安全绝不削弱既有缓解措施改进需不损害其他措施安全与性能权衡要讨论。路径内核不硬编码用户态路径调试消息中的路径假设不得有功能影响。设备号按 Kernel/API/MajorNumberAllocation.h 六条规则分配主设备号保持升序编译期断言强制CamelCase 枚举 snake_case 字符串一一对应并同步更新s_*_device_numbers数组。设备构造一律经Device::try_create_deviceT(...)走after_inserting()完成注册与/dev暴露。文档内核改动尽量配套 man page 或Documentation/Kernel下的概念文档至少写清主题开头段。这套清单覆盖了从内存管理、并发、系统调用到设备子系统与工程协作的完整开发链路。对于任何想要为 SerenityOS 内核贡献代码的开发者DevelopmentGuidelines.md 都是出发前必读的第一份材料而本文则提供了与仓库源码一一对应的深度解读方便你在实际编码时快速定位依据。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价