-
Notifications
You must be signed in to change notification settings - Fork 14
ChibiOS 关键区与锁
本文介绍 ChibiOS RT 内核概念:系统状态(state)、同步(synchronization)、关键区(critical section)等。
前置阅读:ChibiOS 线程编写
- 中断 Interrupt 简介
- 同步(Synchronization)、关键区(Critical Section)、锁(Lock)
- ChibiOS 系统状态(System State)
- 正确使用 chSysLock/Unlock(FromISR)
- 更新历史
如果读者已经熟悉中断 Interrupt(ECE220、ECE391),可以跳过这一部分。
考虑这样一个场景:CPU 需要及时接受一个外部设备的信息(例如,电机发送了回传数据),一个 naive 的方法是写一个循环,反复检查是否接收到数据,不过显然这个方法非常低效,CPU 花费大量的时间在无用的检查上。如果给循环加上周期(例如使用线程,每 10ms 检查一次),则会有延迟的问题,信息无法被及时处理。
中断则是另一个解决思路:让外部设备主动通知 CPU。当 CPU 接收到消息时,立即切换到处理消息的代码,运行结束后返回先前运行的代码。通过在切换前保存系统状态(寄存器、栈等)被打断的代码对这一过程并无感知,和顺序执行并无差别。
中断可能随时触发,中断触发称为 Interrupt Request(IRQ)。如果中断代码和正在执行的代码完全独立,则没有任何影响,但实际开发过程往往并非如此。一个简单的例子:CPU 正在执行这样一段代码:
if (now() - last_update_time > 2000) { // 符合
// 中断触发,last_update_time 在中断代码中增大
// 继续执行
int delta = now() - last_update_time;
// 此时 delta 不一定 > 2000
}
由于正在执行的代码和中断代码有交集(last_update_time),两者相互干扰了。我们编写代码,一般都是基于顺序执行的假设,因此这种问题往往很难从代码中看出来,而且 debug 相对困难。
不仅是中断,相互干扰的问题对于线程,或多处理器,也同样存在。操作系统切换线程一般也是通过中断的方式,随时可能发生。将上述例子的中断,替换成在另一个线程中执行的代码,或者在另一个处理器上并行执行的代码,同样适用。这一类问题被称为同步(Synchronization)问题。
中断/线程切换/并行执行的”随时发生“,是在汇编层面上的,在 C/C++ 层面,可能发生在一句代码中间,例如 a = a + 1
,这一句代码可能被翻译成两条汇编指令:加法和写入结果,中断/线程切换/并行执行可能在这两条指令中间发生。
由于我们控制开发不考虑多处理器场景,并行执行就不再讨论了,感兴趣的读者可以自行了解。
解决同步问题,最常见的方法是设置关键区(critical section)。关键区是一段连续代码,这段代码必须被连续执行,中间不得触发中断/线程切换:
chSysLock();
// 中断/线程切换不可触发,关键区开始 --->
if (now() - last_update_time > 2000) {
int delta = now() - last_update_time;
}
// <--- 关键区结束
chSysLock();
// 中断/线程切换可触发
控制中断/线程切换,一般涉及系统状态(system state)、优先级(priority)。而在更广义的并行开发中,除了有关键区的概念,还有更广义的锁(lock)的概念。读写共享资源前获取锁,完成后释放锁,同样构造了一个关键区,依赖于同一个锁的关键区,任意时刻最多只能有一个在执行,而其他必须等待获取锁。锁是一种更为灵活的保护共享资源的方式,可以定义多个锁保护不同的资源,亦有互斥锁(mutex)、读写锁(read/write lock)等不同类别的锁。
我们先着重于中断/线程切换,展开介绍 ChibiOS 相关的支持。
以下内容来自 ChibiOS 官方文档:ChibiOS free embedded RTOS - Introduction to the RT Kernel,非常推荐各位阅读此篇文档,介绍了 ChibiOS 的基本架构。
The states have a strong meaning and must be understood in order to utilize the RTOS correctly.
我们着重关注以下几个状态:
- Thread。ChibiOS RT 正在执行其中一个线程。普通(Normal)API 可以在这一状态中被调用。
- ISR。ChibiOS RT 正在执行中断函数(Interrupt Serving Routine)。
- S-Locked。ChibiOS 正在线程中的关键区(critical sections in thread context)。
- I-Locked。ChibiOS 正在中断函数中的关键区(critical sections in ISR context)。
状态之间的切换显示在这张图中:
简单的例子:
void GimbalSKD::SKDThread::main() {
// Thread state
setName("GimbalSKD");
while (!shouldTerminate()) {
// Thread state
chSysLock();
// S-Locked state
chSysUnlock();
// Thread state
sleep(TIME_MS2I(SKD_THREAD_INTERVAL));
}
}
在 Thread state 中,中断可以触发(IRQ),高优先级的线程可以打断低优先级的线程(保持 Thread state)。
在 S-Locked state 中,中断不可触发,线程不可被打断(context switch operation is always performed synchronously within a critical section)。
void Referee::uart_rx_callback(UARTDriver *uartp) { // 裁判系统 UART 中断回调函数
// ISR state
chSysLockFromISR();
// I-Locked state
chSysUnockFromISR();
// ISR state
}
中断触发时,系统从 Thread state 转移到 ISR state。ISR state 中,更高优先级的中断可以触发。
I-Locked state 中,中断不可触发。
首先,chSysLock 只能在 Thread state 中调用,chSysLockFromISR 只能在 ISR state 中调用。这也就意味着,不能连续调用两次 chSysLock 或 chSysLockFromISR。Unlock 同理。重复 lock/unlock 是造成 ChibiOS halt 的常见原因之一。
其次,lock 和 unlock必须匹配。chSysLock 后必须 chSysUnlock,chSysLockFromISR 后必须 chSysUnockFromISR,否则 ChibiOS 将保持在错误的 state 中。常见的问题是在关键区中使用了 return、break、continue,或者将 lock/unlock 放在 if 中。
再者,在不同的 state 中只能调用对应类别(class)的 ChibiOS API。主要关注以下几个类别的 ChibiOS API:
- Normal functions。无特殊后缀,除非文档特别说明,只能在 Thread 中调用。
- S-Class functions。函数名带 “S” 后缀,只能在 S-Locked 中调用。
- I-Class functions。函数名带 “I” 后缀,只能在 I-Locked 中调用。
- X-Class functions。函数名带 “X” 后缀,可在 Thread、S-Locked、I-Locked 中调用。
举个例子,我们调用 uartStartReceive
来开始下一轮 UART 接收。在线程中,启动第一次接收,调用的是无后缀的 uartStartReceive
:
void Referee::init() {
// Start uart driver
uartStart(UART_DRIVER, &UART_CONFIG);
// Wait for starting byte
rx_status = WAIT_STARTING_BYTE;
robot_state.robot_id = 0;
uartStartReceive(UART_DRIVER, FRAME_SOF_SIZE, &pak);
}
而在中断回调函数中,我们需要先进入 I-Locked,然后调用 uartStartReceiveI
:
void Referee::uart_rx_callback(UARTDriver *uartp) {
chSysLockFromISR(); /// --- ENTER I-Locked state. DO NOT use LOG, printf, non I-Class functions or return ---
// ...
switch (rx_status) {
case WAIT_STARTING_BYTE:
uartStartReceiveI(uartp, FRAME_SOF_SIZE, pak_uint8);
break;
// ...
}
chSysUnlockFromISR(); /// --- EXIT I-Locked state ---
}
Shell(包括 printf 等)需要缓冲区的支持,ChibiOS 提供的版本是基于 Thread state 设计的,例如,缓冲区满时,切换到其他线程。这也就导致了 printf、LOG 等函数只能在 Thread state 中调用。
新版本 Meta-Embedded 引入了 printfI、LOG_I 等函数,这些函数可在 I-Locked 中调用。
同步是并行计算一个很重要的问题,没有容易的解决方法。关键区需要合理使用,不同步可能导致正确性问题,而过度同步可能导致性能问题(关键区内无线程调度,高优先级任务被迫推迟),因此关键区应经可能保持短小。而且,并非所有共享资源都需要保护,例如以下两个线程,同步是不需要的,因为只有 Thread 1 修改 a
,而 Thread 2 只读不写。
// Thread 1
while (true) {
a += 2;
// ...
}
// Thread 2
while (true) {
if (a > 42) {
// ...
}
}
为了减少代码错误,推荐使用以下模板:
chSysLock(); /// --- ENTER S-Locked state. DO NOT use S/I-Class functions (LOG, printf) or return ---
{
// Your code
}
chSysUnlock(); /// --- EXIT S-Locked state ---
chSysLockFromISR(); /// --- ENTER I-Locked state. DO NOT use non I-Class functions (LOG, printf) or return ---
{
// Your code
}
chSysUnlockFromISR(); /// --- EXIT I-Locked state ---
使用三个 /
,在 CLion 中会将注释高亮。使用额外的一组 {}
显示 critical section 的范围(同时会在 CLion 左侧增加一个折叠)。
- 2022.01.17 初始版本。liuzikai
- 基础知识
- 基础配置
- 进阶与参考