动手做一个自组网的网络 - 操作系统内核
介绍
- 注意: 这不是一个真正意义上的操作系统,属于操作系统内核、微核心。
- 起初,只是想自己做一个
MCU
上的操作系统调度内核来验证自己的一些想法,之前用过一些RTOS
。因为我的源码阅读能力是很弱的,有时需要了解关于某个小问题的解释,还得网上翻翻别人的记录,就这样,从使用一个栈倒腾两个任务开始,再到使用两个栈切换两个任务,最后一个简单且脆弱的调度器雏形就跑起来了。 - 写这篇主要是回顾之前写这个
MCU
操作系统内核的想法,是思路上的总结。 - 源码
https://gitee.com/cyes/rtos
需要实现一个什么样目标
- 简单、稳定的
- 可以将任务线程化调度
- 方便调试
一个简单的调度器
中断
- 这里只讨论和系统调度有关的中断
-
pendsv
- 可悬起系统调用,在一个任务运行时间片结束时标记该中断来延迟运行系统调度,这个中断的特性是只能在其他所有中断执行完毕后才会响应。 -
svc
- 软件中断 - 一个调度器想要循环调度所有任务理论上只需要pendsv
即可,只不过第一个任务比较特殊,因为在他之前没有任务堆栈被使用,所以需要把这部分特殊的前奏单独拿出来交给另一个中断进行,svc
正好合适做这个,于是用他进入中断来切换第一个任务堆栈。 -
systick
- 定时器中断,任何能提供周期性中断的中断源都可以用来做这件事,甚至可以拿按键配合gpio
中断来手动切换这些任务,不嫌累的话。 -
中断响应序列 - 中断发生时硬件做了什么
-
上述流程中,只有中断处理是需要软件额外介入的,其余都是硬件自动完成
-
注意: 在中断返回时,内核需要特定的
EXC_RETURN
来指导中断返回,否则一些中断返回过程会被终止。-
返回时需要将
EXC_RETURN
写入PCEXC_RETURN
解释 0xffffffff1
返回 handler
模式0xffffffff9
返回线程模式,并使用 MSP
0xffffffffd
返回线程模式,并使用 PSP
-
测试中在
cortex-m3
、cortex-m4
、cortex-m7
内核都需要这么做,否则不会正常返回。 -
cortex-m0/m0+
把EXC_RETURN
写入通用寄存器返回也可以。 -
这里顺带提一下
cortex-m0/m0+
,由于支持的指令集较少,意味着在汇编阶段的栈操作需要使用其他指令实现,具体见portable文件内,另外他的通用寄存器也仅支持低位寄存器r0 ~ r7
。
-
-
寄存器
-
cortex-m4f
的寄存器有16
个,分别是r0 ~ r15
,带有32
个浮点寄存器,分别是s0 ~ s31
。 -
平时CPU在执行代码时就是以这些寄存器为空间,不断的处理数据,所谓的保护现场就是保护这些寄存器,因为在任意时刻,所有发生的事情都是在这些寄存器里面发生的,恢复了这些寄存器也就是还原了一个时间点。
-
在发生中断时,硬件会自动将
r0-r3,r12,r14(LR),r15(PC),xPSR
这些寄存器压栈到栈地址,使用FPU
的话,会额外保存s0 ~ s15
,栈地址就是我们在运行一个任务前指定的堆栈地址,同样,中断返回时硬件也会从堆栈里面弹出这些寄存器,我们需要做的就是按照规则把堆栈的数据修改好,这样硬件弹出的就是我们需要的数据了。
栈
- 用任务比作人的话,栈就是他的背包,平时手里拿不了的东西都可以放到背包里。
- 现在我想把AB两个函数分别切换为两个死循环执行不同功能函数,因为两个函数都需要调用其他函数,所以就要各指定一个栈地址
- 当需要从A任务切换B任务时,需要做下面几件事
- 保存当前所有的寄存器到A的堆栈
- 将PC改为B任务的地址
- 将B任务栈里的数据恢复到寄存器里
链表
- 上面描述了调度器,主要是利用硬件特性对CPU进行分时复用,接下来是链表,他是任务管理的核心
- 关于链表网上有很多介绍,任何人使用任何语言都能写出他的实现,所以我就不罗嗦直接抄了
- 这里根据
linux
内核2.6
的list
实现做了些主要汇总,这些实现妙就妙在 函数全部都是static inline
struct list_head // 链表结构体的指针的数据结构
LIST_HEAD_INIT // 用于初始化这个头部编成独立结点
LIST_HEAD(name) // 初始化一个变量
INIT_LIST_HEAD(ptr) // 将这个指针初始化为孤立的结点
list_for_each(pos, head) // 从前到后遍历链表
list_for_each_safe(pos, n, head) // 删除当前结点不会造成断链
list_entry(ptr, type, member) // 获得这个指针的所再的项的开头
list_del(struct list_head *entry) // 删除这个结点, 没有切断联系
list_empty(struct list_head *head) // 判断这个链表是否为空
list_add(struct list_head *new, struct list_head *head) // 将新元素放到链表头
list_move(struct list_head *list, struct list_head *head) // 移动链表到第二个链表
- 了解一个新的东西,我一般会想这个东西是为了解决什么问题而存在的,只有了解 了他能解决什么问题,为什么别的方式解决不了这个问题时,才算学会了
- 这里,我用它来管理多个任务。
怎么实现阻塞
-
阻塞的方法是不执行,不执行的本质是将任务从链表移除
- 这里的任务运行仅有
运行态
阻塞态
就绪态
三种,为了方便,阻塞态位于各自的阻塞事件链表上
- 这里的任务运行仅有
-
当任务创建时放到就绪链表,当任务需要被阻塞时就放到其他链表。因为调度器只在就绪链表里寻找任务,所以就产生了阻塞效果。
-
任务需要执行时,则将该任务移回即可,这一切都依赖于链表的特性
内存管理
- 我实现的算法没有任何策略,且不满足实时性。
- 舍弃速度照顾碎片化,写了这么一个功能,因为能保证安全有效且简单
- 内存需要监控,可以通过LCD直观看到内存动态分配情况,这能很好的监控运行中的内存使用情况。图片ref
队列
-
队列有很多种实现方式
- 链表队列 — 默认不分配内存,写入即分配内存并写入链表,读取即移除链表并释放内存
- 定长预分配内存队列 — 预先分配内存并分割一定数量,写入即填充数据,数据只能读写固定长度
- 不定长预分配内存队列 — 参考定长预分配内存模式,数据可以跨多块读写
-
目前仅实现了定长预分配内存队列,其他两种都有各自的优点。
-
可以升级为动态链表队列,这样长度可以限定,内存也会有效利用(需要解决内存分配的时间确定性)。
信号量和互斥锁
-
信号量就是队列的特殊实现方式
-
互斥锁的本质就是在信号量的基础上增加额外功能,目前只实现了优先级天花板策略。
后续更新
- 组件接口 - 可以快速使用的一些常用硬件库
- 动态任务栈 - 动态管理栈空间,无需指定大小
- 多核调度和新平台支持
总结
- 到这里,一个可以实现调度多任务的简单操作系统内核就可以使用了。
- 当开始写的时候,总有一些卡点不知道怎么做,在工程初具雏形后思维就开始跳跃了,时刻与技术人的偏执做斗争。慢慢地越往后写越觉得这玩意就是一个无底洞,哪里有什么完美,
linux
现在不也在持续提交代码嘛,能用就行吧。