WillOS 核心机理深度解析(第二章):内存分配——为操作系统筑基

项目地址:WillOS/WillOS/Inc/allocator.h at master · LieWill/WillOS

返回总导读:关于我独立开发 RTOS 这一件事

在 RTOS(实时操作系统)的世界里,内存分配器(Allocator)就像是一个地产开发商。它负责将物理内存这片“荒地”规划成整齐的“地块”,让系统中的其他组件(特别是接下来的调度器)能够在这些地块上安家落户。

在 WillOS 中,WillOS/Src/allocator.c 实现了一个确定性高且具备内存合并能力的动态堆管理器。


1. 思考:为什么我们需要“动态”分配内存和栈?

在学习具体的分配算法之前,新手最容易产生的疑问是:“我直接定义全局变量数组(静态分配)不行吗?为什么要搞这么复杂的堆(Heap)管理器?”

1.1 静态 vs. 动态:从“买房”到“租房”

  • 静态分配(买房):在编译时就定死了每个任务用多少内存。如果你有 10 个功能,哪怕其中 9 个现在不运行,它们的内存也一直被占着。在内存紧缺的嵌入式芯片里,这极大地浪费了资源。
  • 动态分配(租房):只有当一个任务(Task)被创建时,系统才从堆里切出一块地给它;当任务结束(Delete)时,这块地立刻归还。这种按需分配的能力,让有限的 RAM 能跑起更复杂的应用。

1.2 为什么要给每个任务分配独立的“栈”?

每一个任务在运行时,都需要存储局部变量、函数跳转地址、以及中断发生时的现场。这些数据必须存在**栈(Stack)**里。

  • 机理:如果多个任务共用一个栈,任务 A 执行到一半被切走,任务 B 进来就会把 A 的数据覆盖掉。
  • 物理本质:在 WillOS 中,为任务分配内存的本质,就是为它划定一个私有的、受保护的“沙盒”。有了这个沙盒,任务才能在里面放心地进行函数调用,而不用担心被别人“强拆”。

2. 演进之路:从“裸奔”的 v1.0 到“工业级”的 v2.0

WilOS 的内存管理并不是一蹴而就的,它经历了一次从“逻辑原型”到“工业方案”的彻底进化。

2.1 第一版方案 (v1.0: kalloc.c) —— 逻辑原型的探索

最初的代码采用了基于单向总链表的 First-Fit 算法:

  • “裸奔”状态:完全没有任何临界区保护。在单任务下没问题,但一旦进入多任务并发,两个任务同时申请内存就会把链表指针对冲,引发 HardFault
  • 搜索低效:由于空闲块和已分配块混在一起,每次分配都需要跳过大量已占用的块,搜索复杂度随运行时间呈 O(N)O(N) 增长。
  • 脆弱的扩容realloc 只是简单地“重新找个地儿拷贝”,造成了极大的总线带宽浪费。

2.2 第二版方案 (v2.0: allocator.c) —— 工业级的重塑

现在的 v2.0 是针对 RTOS 环境量身定制的:

  • 双向分离链表:将“空闲块”和“已分配块”分属不同链表管理。分配时只搜索空闲列表,效率呈倍数提升。
  • 临界区铁甲:所有接口均用 EnterCritical 包裹,确保多任务抢占时的原子性。
  • 黑科技:Bond 指针与原位扩容:这是 v2.0 的精髓,也是实现“碎片自动吞噬”的关键。

3. 资源勘探:内存版图的边界

内存分配器的首要任务是明确:我能管哪块地?

WillOS 通过链接脚本提供的符号,自动识别内存版图:

  • 起始点 (_end):这是 BSS 段(全局变量)结束的地方,标志着堆空间的开始。
  • 终止点 (_estack - _Min_Stack_Size):为了保证主系统栈(MSP)不被挤压,分配器会预留一部分空间,在此之前的区域都是安全的“可分配区”。

这种设计避免了在代码中“硬编码”地址,实现了很好的硬件适配性。


4. 核心机理:Bond 指针与“碎片回收魔法”

RTOS 内存管理的头号敌人是碎片(Fragmentation)。WillOS 核心机理中最精妙的设计莫过于 bond 指针。

4.1 每一个块都是自描述的

每一个内存块都被一个头部保护着,其中包含一个关键字段:bond

  • 物理锚点bond 指针记录了该内存块在物理内存上的结束边界
  • 邻里识别:通过 bond,分配器可以瞬间知道自己的物理邻居是谁。

4.2 碎片自动回收:合纵连横

当用户调用 kfree 时,系统不只是简单地把块扔回链表,而是触发一次“邻里大搜救”:

  1. 右邻合并:检查 my_block->bond 是否等于某个空闲块的起始地址。如果是,说明由于刚才的释放,这两块地连起来了!立即合并。
  2. 左邻合并:同理,寻找物理终点恰好接在自己头上的空闲邻居。
    机理效果:只要系统还在运行,小尺寸的碎片就会由于这种“物理聚合”不断合成为大块内存,系统永远不会因为“碎地太多”而无法分配大任务。

4.3 原位扩容 (In-place Expansion)

当你调用 krealloc 想要增加内存时,v2.0 展现出了它的智慧:

  • 不急着搬家:它首先通过 bond 观察现在的地块“右侧”是不是刚好有一块空闲地。
  • 无感扩充:如果是,它直接吞掉右侧的部分空闲空间,修改一下 bond 边界值。
    意义:这种“原位扩容”不需要进行昂贵的内存拷贝(memcpy),对运行效率的提升是巨大的,这也是衡量一个内核分配器是否达到“工业级”的标准之一。

5. 安全流程:临界区的铠甲

在多任务并发环境下,内存链表是非常脆弱的。如果两个任务同时尝试修改链表指针,系统会瞬间崩溃。

  • EnterCritical / ExitCritical:你可以看到分配器的每一个对外接口(kmallockfree)都套了一层“铠甲”。
  • 机理:通过关闭中断(或提升优先级),确保内存分配操作是原子性的。这意味着在地产商“分地”的时候,任何人都不准进来打扰。

6. 承上启下:为调度器(Scheduler)搭建舞台

理解了内存分配器,我们就触碰到了操作系统的脉搏。内存分配器在这里扮演了“承上启下”的角色:

6.1 承上:对硬件资源的抽象

它通过简单的 kmalloc(Size),将复杂的物理 SRAM 地址抽象成了任务随时可以申请的“资源”。

6.2 启下:为“任务”提供土壤

在下一篇文章中,我们将讨论 Scheduler(调度器)。你会发现调度的第一阶段就是调用 ThreadCreate,而它内部的核心逻辑是:

// 调度器中的代码片段
TCB_Ring_t *new = (TCB_Ring_t *)kmalloc(Size + sizeof(TCB_Ring_t));

这里隐藏了一个精妙的设计:
调度器向分配器申请了一块连续的内存。这块内存的前半部分存储 TCB(任务控制块)——也就是任务的身份证;后半部分直接作为 Stack(任务栈)——也就是任务的办公室。

这种“身份证+办公室”合二为一的布局,极大地提高了缓存命中率和内存利用率。


总结

没有 Allocator,RTOS 只能是死板的静态系统。有了它,操作系统才拥有了在运行时“动态创造生命(任务)”的基本物质基础。

既然现在我们已经能为任务分配“地块”和“办公室”了,那么接下来的问题是:我们该如何管理这些排队等地的任务,并让它们有序地轮流使用 CPU 呢?

请看下一篇:WillOS 核心机理深度解析(第三章):调度算法——指挥多任务并行的“中央大脑”》。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇