sparrow系列十五
skaiuijing
前言
在前面一篇文章中,笔者带领大家使用PendSV中断实现了多线程,也终于度过了关于arm架构最难的部分。现在,笔者可以带领大家稍微迈入一点大家所熟悉的操作系统的世界,也就是多线程的并发问题。
操作系统中的并发
试想,RTOS中如果两个线程同时对一个全局资源进行更改,那么会发生什么情况?例如,A线程要向一个地址写入数据,恰好B线程也要向一个地址写入数据,这种情况下会发生什么呢?
显然,最终的结果是第二个完成写入的线程胜利。
竞态通常作为对资源的共享访问而产生,当两个线程访问相同的的资源时,混合的可能性就永远存在。所以,笔者给出两点建议:
1.只要可能,就应该避免资源的共享
这种思想最直接的方式就是减少全局变量的使用。如果我们使用全局变量,必须要考虑是否会造成多个线程并发访问的场景。然而,在实际开发中,这其实是一种奢望,因为硬件资源本身就是共享的,而且有时候为了内存不得不使用全局变量。
2.必须显式地管理对共享资源的访问
我们必须确保一次只有一个线程访问共享资源,那么,我们该如何做到呢?
临界区与原子操作
共享资源往往被称之为临界资源,为了使得操作共享资源时只有一个线程,我们引入了临界区和原子操作的概念。它们的本质是一样的,都是在执行过程中不能被打断的一段代码。
临界区
在RTOS中通常是通过开关中断来实现。当然,早期的unix和linux等操作系统不支持对称多处理时,并发执行的唯一原因也是硬件中断服务,开关中断也是一种常用的实现对资源的唯一访问的手段。
原子操作
不一定通过开关中断实现,也可以通过汇编(ldrex和strex等指令)或者锁等方式来实现对资源的唯一访问。
如何防止临界区被打断?
在Sparrow RTOS中,我们使用PendSV中断来完成上下文切换,从而实现多线程,这可能会造成竞态的出现。还有一种可能,就是外部中断的发生,导致当前任务对共享资源的访问被打断。我们发现,Sparrow中的并发都来自中断,那么,只要在对临界资源的访问的程序中加上关闭中断的操作,这段程序就只有当前线程执行了,当然,执行完后我们肯定要开启中断。
先分析RTOS的运行架构,以cm3为例,它的中断屏蔽寄存器如下:
详细信息:
现在我们将要使用BASEPRI寄存器,其实用其他两个都是可以的,但是笔者感觉还是灵活一点比较好。
进入临界区
configShieldInterPriority 是宏,其值为191,由于BASEPRI寄存器是一个八位的寄存器,它是高四位有效,所以它的值是11,默认屏蔽11及以上的中断。顺便一提,之所以使用uint32_t而不是uint8_t,是考虑
(可能有读者疑惑为什么要使用191而不是11,答案是:大小端序问题。笔者曾经被这玩意坑过不少次。PendSV中断的默认优先级是15,当移植RTOS发现PendSV中断都不响应时,此时就可以怀疑CPU到底是大端序还是小端序了。当然,也有可能是编译器干的)
下面的代码就是往basepri寄存器中写入191,return xReturn是考虑代码进入临界区时,可能又发生中断,中断里面也有临界区,当退出中断时,此时basepri的值就会变成默认状态的0,这段代码之后的临界区就是无效的:
1 |
|
退出临界区
将之前保存的值写入basepri寄存器即可:
1 | __attribute__((always_inline)) void xEixtCritical( uint32_t xReturn ) |
实验
现在我们进行一个小实验来验证我们的临界区是否成功实现,我们需要验证临界区是否能够屏蔽中断,那我们使用什么中断进行验证呢?
不要忘了上下文切换函数PendSV本身就是一个中断。
先加上我们自己写的延时函数(hal库的hal_delay容易出bug导致卡死,与它的计数默认使用中断进行有关),修改led_bright函数如下:
1 | void fone(uint32_t time) |
编译然后下载到开发板上进行调试,先介绍一下实验思路;
我们在count++上面添加了一个延时1s的函数。如果临界区没有屏蔽中断,那么延时时肯定会触发PendSV中断,那我们运行到中断里面时,count的值仍然是0。如果临界区是正确的,那么count的值会是1,这证明是在延时1s后才触发的中断。
先打上断点;
gdb下,按c运行到断点处,现在count为0。
在PendSV中断这里也打上断点:
按r运行到PendSV中断里面。
打印:
结果是1,说明我们的临界区起作用了。
总结
讲解了操作系统中的一些并发问题,然后通过配置中断寄存器的方式实现了临界区,最后通过实验来验证临界区是否成功执行。
本次实验的文件夹:skaiui2/SKRTOS_sparrow at experiment