skaiuijing

前言

Sparrow RTOS是笔者之前写的一个极简性RTOS,初代版本只有400行,后面笔者又添加了消息队列、信号量、互斥锁三种IPC机制,使之成为一个较完整、堪用的内核,初代版本以简洁为主,使用数组和表作为任务挂载的抽象数据结构,对数表版本的Sparrow RTOS总结如下:

缺陷

由于数组和表的限制,该版本并不支持同优先级和时间片功能,设计互斥锁时也受到一定影响,而且最大只支持32个任务,有许多不便之处。

优点

使用数表存储任务,对任务的挂载以位操作和下标操作为主,内核简洁小巧,执行效率高,适用于任务较少、硬件资源少的情况。

链表版本内核的设计

使用链表作为任务挂载的数据结构,能够实现同优先级、时间片等功能,对任务对象的操作也更加灵活。

链表设计

1
2
3
4
5
graph LR
Ab(链表头部)--双向链表-->Ac(链表头节点)--双向链表-->Ad(链表节点)--双向链表-->Ae(链表尾节点)
Ae--双向链表-->Ac

Ab-->Ae

任务链表由头部节点和任务节点两部分组成,头部会指向头节点和尾节点,头节点到尾节点之间会形成一个环路。

就绪列表设计

链表简化设计如下:

1
2
3
4
5
graph LR
Aa(链表数组Index)
Ba(链表头部Index1)-->Bb(任务节点)
Ca(链表头部Index2)-->Cb(任务节点)
Da(链表头部Index3)-->Db(任务节点)

实际设计:

1
2
3
4
5
6
7
8
9
10
11
12
graph LR
Ab(链表头部Index1)--双向链表-->Ac(任务头节点)--双向链表-->Ad(任务节点)--双向链表-->Ae(任务尾节点)
Ae--双向链表-->Ac
Ab-->Ae

Bb(链表头部Index2)--双向链表-->Bc(任务头节点)--双向链表-->Bd(任务节点)--双向链表-->Be(任务尾节点)
Be--双向链表-->Bc
Bb-->Be

Cb(链表头部Index3)--双向链表-->Cc(任务头节点)--双向链表-->Cd(任务节点)--双向链表-->Ce(任务尾节点)
Ce--双向链表-->Cc
Cb-->Ce

任务节点通过链表进行挂载,那么怎么找到任务对象的起始地址呢?

请读者想一想,任务对象的成员都是已知的,所以我们完全可以用链表节点的地址减去前面的成员的地址,就能得到任务对象的起始地址,然后再把起始地址类型转换为任务对象指针。

基于这个思想,其实我们是可以在面向对象的语言中修改私有属性的(如果这门语言支持指针这种直接操作内存的语法的话)。

不过一个个算还是太麻烦了,我们可以直接使用宏:

1
2
3
4
//get father struct address
//how to use it:struct parent *parent_ptr = container_of(child_ptr, struct parent, child)
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))

这样就可以直接通过链表找到任务对象起始地址了。

相对于初步的Sparrow RTOS,链表版本的功能增加如下,增加了一个TimeSlice,也就是时间片功能。

1
2
3
4
5
6
7
8


void xTaskCreate( TaskFunction_t pxTaskCode,
const uint16_t usStackDepth,
void * const pvParameters,
uint32_t uxPriority,
TaskHandle_t * const self,
uint8_t TimeSlice)

任务优先级设置

使用链表数组对应每个优先级,因此我们可以通过设置链表数组的大小来更改支持的优先级范围。不过由于支持同优先级和时间片,因此挂载的任务数量其实是不受限制的(除非内存不够)。

时间片

时间片是针对同优先级的说法,当最高优先级有多个任务时,每个任务会根据自身设置的时间片轮流享有CPU运行时间。

在时钟触发型RTOS中,一个时间片就是两次systick时钟中断之间的响应间隔,在Sparrow RTOS中,默认为1ms。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xTaskCreate(    taskA,
256,
NULL,
3,
&tcbTask1,
1
);

xTaskCreate( taskB,
256,
NULL,
3,
&tcbTask2,
3
);

对于taskA和taskB,当最高优先级为3时,这两个任务会轮流执行,不过taskA只会执行1个时间片,然后就会将CPU执行权交给taskB,taskB会执行三个时间片,然后再将CPU执行权交给taskA,如此反复循环(如果最高优先级一直是3)。

互斥锁设计

在Sparrow RTOS的数表版本中,互斥锁的优先级反转功能是设置优先级为阻塞任务中最大的那个优先级+1,但是这样会导致浪费优先级,对于可能发生阻塞的任务,我们要确保这些任务的优先级必须设置合理,不然会导致灾难的发生。

但是对于链表版本,由于支持同优先级,因此我们可以设置相同的优先级避免优先级反转现象的发生,而不会占用额外的优先级。

原子操作

由于临界区屏蔽中断的较为粗暴,所以对于简单的加减操作,可以使用内核提供的原子操作,例如:

1
2
3
atomic_add(a,v),表示*v + a
atomic_inc(v),表示*v自加

考虑下面的情况:

1
2
3
4
5
6
7
8
9
10
11
void  taskA(){
a++;
任务切换发生,另一个任务令a++;
b = a;读取a,但是a的值是错误的
}

void taskB(){
a++;
c = a; a的值是错误的
}

我们使用A和B两个线程对a进行递增,但是两个线程的递增可能是无效的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
graph LR
A(线程1读取counter值等于0)-->B(线程1增加counter值)
B-->C(CPU0写入counter值等于1)
D(线程2读取counter值等于0)-->E(线程2增加counter值)
E-->F(线程1写入counter值等于1)
C-->G(最终counter值等于1)
F-->G






原子操作具有return版本,例如:

1
int a = atomic_inc_return(a,v);

其实原子操作不仅可以保证线程操作的原子性,也可以在多CPU条件下保证数据操作的原子性。

总结

以上就是对Sparrow RTOS链表版本内核的总结,整体来看,链表版本支持更多任务数量和功能,但是执行效率和简洁性不如数表版本,不过二者适用情景不同,根据实际情况选择即可。

笔者本人更喜欢数表版本,只使用了几百行程序就实现了RTOS的基本功能,简洁明了,同时也是一个良好的学习素材。笔者追求的程序风格一直都是模块化、高效、简洁明了,数表版本的内核是非常令笔者得意的,毕竟几千几万行的操作系统内核浩如烟海,几百行的可不多见。

对于学习Sparrow RTOS的读者来说,笔者推荐数表版本的内核,虽然代码量不多,但彻底搞懂并能更改代码可不容易。

结语

Sparrow RTOS将会持续维护更新,不断完善,其实笔者也是有为它添加设备树、驱动框架和网络协议栈这些功能的想法,不过这都是后话了,也许哪天会更新,也许一直没时间做这些,这都是不确定的。不过它的初衷就是一个学习用途的RTOS,而它也确实非常适合这一任务。

最后,笔者真诚希望读者都能在Sparrow RTOS的教程中收获对操作系统的思考与领悟,操作系统的学习之路道阻且长,在海滩拾贝的过程中,希望读者也能收获属于自己的快乐。

以上,与君共勉。

项目地址:skaiui2/SKRTOS_sparrow: Lightweight rtos inspired by SKRTOS