嵌入式系统同步与资源分离技术解析
1. 任务调度与同步机制概述
在多线程环境中,共享内存、外设和系统访问时,系统需要同步机制来协调任务对系统资源的访问。同步机制对于解决并发问题至关重要,其中信号量和互斥锁是最常用的两种机制。
1.1 任务调度策略
不同的任务调度策略会影响任务的响应时间。在某些情况下,任务回到调度轮询中与同优先级的其他任务竞争,可能导致任务反应延迟。在最坏情况下,延迟可以估计为 N * TIMESLICE,其中 N 是中断发生时准备运行的进程数量。而基于优先级的调度方法能在一定程度上保证实时任务在中断发生后优先调度,使中断到恢复任务所需的时间可测量,通常在几微秒级别。
不过,严格的基于优先级的调度策略在控制良好的场景下能改善延迟并保证实时响应,但在通用嵌入式系统中灵活性较差,因为其他约束可能比任务延迟更重要,这时基于时间的抢占式调度方法可能会提供更好的结果。
1.2 同步机制
在多线程环境中,系统需要提供同步机制,让任务能够协作使用系统范围内的共享资源。信号量和互斥锁是解决大多数并发问题的常用同步机制。可能阻塞调用任务的函数必须能与调度器交互,当资源不可用时将任务置于等待状态,直到锁被释放或信号量增加。
2. 信号量机制
信号量是最常见的同步原语,它提供一个具有独占访问权限的计数器,供两个或多个线程协作使用特定的共享资源。信号量的 API 必须保证对象可用于实现具有独占访问权限的计数器,这通常依赖于 CPU 的辅助功能,因此同步策略的内部实现依赖于目标处理器中实现的微代码。
2.1 Cortex - M3/M4 上的实现
在 Cortex - M3/M4 上,锁定机制的实现依赖于 CPU 提供的用于执行独占操作的指令,主要包括:
-
Load Register Exclusive (LDREX)
:将内存地址中的值加载到 CPU 寄存器中。
-
Store Register Exclusive (STREX)
:尝试将寄存器中的新值存储到与最后一次 LDREX 指令对应的内存地址中。如果 STREX 成功,CPU 保证内存写入操作是独占的,并且自最后一次 LDREX 调用以来该值未被修改。两个并发的 LDREX/STREX 部分中,只有一个会成功写入寄存器,第二个 STREX 指令将失败并返回零。
这些指令的特性保证了对计数器的独占访问,用于实现信号量和互斥锁的基本函数。
2.2 信号量操作函数
-
sem_trywait 函数
:尝试减少信号量的值。除非信号量的值为 0,否则操作总是允许的。函数成功时返回 0,信号量值为 0 时返回 -1。其操作步骤如下:
1. 从函数参数指向的内存中读取信号量变量的值到寄存器 R1。
2. 如果 R1 的值为 0,信号量无法获取,函数返回 -1。
3. 将 R1 的值减 1。
4. 将 R1 的值存储到函数参数指向的内存中,并将 STREX 操作的结果放入 R2。
5. 如果操作成功,R2 包含 0,信号量被获取并成功减少,函数返回成功状态。
6. 如果存储操作失败(尝试并发访问),立即重复该过程进行第二次尝试。
以下是实现上述步骤的汇编代码:
sem_trywait:
LDREX r1, [r0]
CMP r1, #0
BEQ sem_trywait_fail
SUBS r1, #1
STREX r2, r1, [r0]
CMP r2, #0
BNE sem_trywait
DMB
MOVS r0, #0
BX lr
sem_trywait_fail:
DMB
MOV r0, #-1
BX lr
- sem_dopost 函数 :用于增加信号量的值。该函数与等待例程类似,但增加计数信号量的值。操作最终会成功,即使多个任务同时尝试访问信号量。函数成功时返回 0,如果计数器之前的值为 0,则返回 1,提醒调用者通知处于等待状态的监听器信号量值已增加,相关资源现在可用。
.global sem_dopost
sem_dopost:
LDREX r1, [r0]
ADDS r1, #1
STREX r2, r1, [r0]
CMP r2, #0
BNE sem_dopost
CMP r0, #1
DMB
BGE sem_signal_up
MOVS r0, #0
BX lr
sem_signal_up:
MOVS r0, #1
BX lr
为了将 sem_wait 函数的阻塞状态集成到调度器中,操作系统向任务公开的信号量接口将非阻塞的 sem_trywait 调用封装为阻塞版本,当信号量的值为 0 时阻塞任务。
为实现信号量接口的阻塞版本,信号量对象可以跟踪访问资源并等待发布事件的任务。任务标识符存储在名为 listeners 的数组中。
#define MAX_LISTENERS 4
struct semaphore {
uint32_t value;
uint8_t listeners[MAX_LISTENERS];
};
typedef struct semaphore semaphore;
int sem_wait(semaphore *s)
{
int i;
if (s == NULL)
return -1;
if (sem_trywait(s) == 0)
return 0;
for (i = 0; i < MAX_LISTENERS; i++) {
if (!s->listeners[i])
s->listeners[i] = t_cur->id;
if (s->listeners[i] == t_cur->id)
break;
}
task_waiting(t_cur);
schedule();
return sem_wait(s);
}
3. 互斥锁机制
互斥锁(Mutex)是互斥的缩写,与信号量密切相关,实际上可以使用相同的汇编例程实现。互斥锁本质上是一个初始值为 1 的二进制信号量,允许第一次锁定操作。
由于信号量的特性,当计数器值达到 0 后,任何尝试减少计数器的操作都会失败。因此,我们可以将信号量的基本操作 sem_wait 和 sem_post 分别重命名为 mutex_lock 和 mutex_unlock。
两个任务可以同时尝试减少未锁定的互斥锁,但只有一个会成功,另一个会失败。在示例调度器的阻塞版本互斥锁中,基于信号量函数构建的互斥锁 API 包装如下:
typedef semaphore mutex;
#define mutex_init(m) sem_init(m, 1)
#define mutex_trylock(m) sem_trywait(m)
#define mutex_lock(x) sem_wait(x)
#define mutex_unlock(x) sem_post(x)
4. 优先级反转问题
在开发使用集成同步机制的抢占式、基于优先级的调度器的操作系统时,经常会遇到优先级反转现象。这种情况会影响与低优先级任务共享资源的实时任务的响应时间,在某些情况下,可能导致高优先级任务长时间饥饿。
优先级反转通常发生在高优先级任务等待低优先级任务释放资源时,而低优先级任务可能会被系统中的其他无关任务抢占。具体事件序列如下:
1. T1、T2 和 T3 是三个运行的任务,优先级分别为 1、2 和 3。
2. T1 使用互斥锁锁定资源 X。
3. T1 被优先级更高的 T3 抢占。
4. T3 尝试访问共享资源 X,并在互斥锁上阻塞。
5. T1 恢复在临界区的执行。
6. T1 被优先级更高的 T2 抢占。
7. 任意数量优先级大于 1 的任务可以在 T1 释放锁并唤醒 T3 之前中断 T1 的执行。
为避免这种情况,可以实现优先级继承机制。该机制通过临时提高共享资源任务的优先级到访问该资源的所有任务中的最高优先级,确保低优先级任务不会导致高优先级任务的调度延迟,从而满足实时要求。
以下是优先级反转和优先级继承的 mermaid 流程图:
graph TD;
A[T1 锁定资源 X] --> B[T3 抢占 T1];
B --> C[T3 尝试访问资源 X 并阻塞];
C --> D[T1 恢复执行];
D --> E[T2 抢占 T1];
E --> F[可能多个任务中断 T1];
G[优先级继承] --> H[T1 临时提级];
H --> I[T1 快速释放资源];
I --> J[T3 继续执行];
综上所述,信号量、互斥锁等同步机制在多线程环境中起着关键作用,能有效协调任务对共享资源的访问。同时,优先级反转问题需要通过优先级继承等机制来解决,以保证系统的实时性和稳定性。
5. 系统资源分离
为了提高系统的安全性和稳定性,需要对系统资源进行分离,包括特权级别分离和内存分段等操作。
5.1 特权级别
Cortex - M CPU 设计为支持两种不同的特权级别执行代码,这对于运行不可信应用代码的系统尤为重要,可让内核始终控制执行过程,防止因用户线程行为不当导致系统故障。
- 默认执行级别 :启动时的默认执行级别是特权级别,以便内核能够启动。应用程序可以配置为在用户级别执行,并在上下文切换操作期间使用不同的堆栈指针寄存器。
- 改变特权级别 :只能在异常处理程序中更改特权级别,通过存储在 LR 中的特殊异常返回值实现。控制特权级别的标志是 CONTROL 寄存器的最低位,可在从异常处理程序返回之前的上下文切换期间更改,将应用程序线程降级到用户特权级别运行。
- 堆栈指针寄存器 :大多数 Cortex - M CPU 提供两个独立的堆栈指针 CPU 寄存器:
- 主堆栈指针(MSP):由中断处理程序和内核使用。
- 进程堆栈指针(PSP):操作系统必须使用 PSP 执行用户线程。
堆栈选择取决于异常处理程序结束时的特殊返回值。目前实现的调度器将该值硬编码为 0xFFFFFFF9,用于在中断后返回线程模式并保持在特权级别执行。从中断处理程序返回 0xFFFFFFFD 值告诉 CPU 在返回线程模式时选择 PSP 作为堆栈指针寄存器。
为了正确实现特权分离,用于切换任务的 PendSV 处理程序需要修改,以使用正确的堆栈指针保存和恢复上下文。之前使用的 store_context 和 restore_context 函数分别重命名为 store_kernel_context 和 restore_kernel_context,因为内核仍使用主堆栈指针。同时添加两个新函数用于从新的上下文切换例程中存储和恢复线程上下文,该例程使用 PSP 寄存器:
static void __attribute__((naked)) store_user_context(void)
{
asm volatile("mrs r0, psp");
asm volatile("stmdb r0!, {r4 - r11}");
asm volatile("msr psp, r0");
asm volatile("bx lr");
}
static void __attribute__((naked)) restore_user_context(void)
{
asm volatile("mrs r0, psp");
asm volatile("ldmfd r0!, {r4 - r11}");
asm volatile("msr psp, r0");
asm volatile("bx lr");
}
修改后的 PendSV 处理程序如下:
void __attribute__((naked)) isr_pendsv(void)
{
if (t_cur->id == 0) {
store_kernel_context();
asm volatile("mrs %0, msp" : "=r"(t_cur->sp));
} else {
store_user_context();
asm volatile("mrs %0, psp" : "=r"(t_cur->sp));
}
if (t_cur->state == TASK_RUNNING) {
t_cur->state = TASK_READY;
}
t_cur = tasklist_next_ready(t_cur);
t_cur->state = TASK_RUNNING;
if (t_cur->id == 0) {
asm volatile("msr msp, %0" ::"r"(t_cur->sp));
restore_kernel_context();
asm volatile("mov lr, %0" ::"r"(0xFFFFFFF9));
asm volatile("msr CONTROL, %0" ::"r"(0x00));
} else {
asm volatile("msr psp, %0" ::"r"(t_cur->sp));
restore_user_context();
asm volatile("mov lr, %0" ::"r"(0xFFFFFFFD));
asm volatile("msr CONTROL, %0" ::"r"(0x01));
}
asm volatile("bx lr");
}
设置了特权模式位的任务对系统资源的访问受到限制,特别是不能访问 SCB 区域的寄存器,这意味着一些基本操作(如通过 NVIC 启用和禁用中断)仅内核可以执行。与 MPU 结合使用时,特权分离通过在访问级别实施内存分离进一步提高系统安全性,可检测并中断行为不当的应用程序代码。
5.2 内存分段
动态内存分段策略可以集成到调度器中,确保单个任务不访问与系统关键组件关联的内存区域,并允许从用户空间访问需要内核监督的资源。
在之前的学习中,我们了解到 MPU 可用于划分连续的内存段,并禁止系统上运行的任何代码访问特定区域。MPU 控制器提供权限掩码,可更精细地更改单个内存区域的属性。例如,可以只允许 CPU 在特权级别运行时访问某些区域,这是防止用户应用程序在没有内核监督的情况下访问系统某些区域的有效方法。
以下是 MPU 区域属性寄存器中特定权限的定义:
#define RASR_KERNEL_RW (1 << 24)
#define RASR_KERNEL_RO (5 << 24)
#define RASR_RDONLY (6 << 24)
#define RASR_NOACCESS (0 << 24)
#define RASR_USER_RW (3 << 24)
#define RASR_USER_RO (2 << 24)
内核可以在启动时强制执行 MPU 配置,以下是一个示例:
int mpu_enable(void)
{
volatile uint32_t type;
volatile uint32_t start;
volatile uint32_t attr;
type = MPU_TYPE;
if (type == 0)
return -1;
MPU_CTRL = 0;
start = 0;
attr = RASR_ENABLED | MPUSIZE_256K | RASR_SCB |
RASR_RDONLY;
mpu_set_region(0, start, attr);
start = 0x20000000;
attr = RASR_ENABLED | MPUSIZE_128K | RASR_SCB |
RASR_USER_RW | RASR_NOEXEC;
mpu_set_region(1, start, attr);
start = 0x10000000;
attr = RASR_ENABLED | MPUSIZE_64K | RASR_SCB |
RASR_KERNEL_RW | RASR_NOEXEC;
mpu_set_region(2, start, attr);
start = 0x40000000;
attr = RASR_ENABLED | MPUSIZE_1G | RASR_SB |
RASR_KERNEL_RW | RASR_NOEXEC;
mpu_set_region(4, start, attr);
start = 0xE0000000;
attr = RASR_ENABLED | MPUSIZE_256M | RASR_SB |
RASR_KERNEL_RW | RASR_NOEXEC;
mpu_set_region(5, start, attr);
SHCSR |= MEMFAULT_ENABLE;
MPU_CTRL = 1;
return 0;
}
在上下文切换期间,调度器可以在从 isr_pendsv 服务例程返回之前调用自定义 MPU 模块导出的函数,以临时允许非特权模式下即将运行的任务访问其堆栈区域:
void mpu_task_stack_permit(void *start)
{
uint32_t attr = RASR_ENABLED | MPUSIZE_1K |
RASR_SCB | RASR_USER_RW;
MPU_CTRL = 0;
// 后续代码可能未完整给出
}
总结
本文详细介绍了嵌入式系统中的同步机制和资源分离技术。同步机制方面,信号量和互斥锁是解决多线程环境中共享资源访问问题的关键,同时要注意优先级反转问题及其解决方法。资源分离技术通过特权级别分离和内存分段,提高了系统的安全性和稳定性,确保内核能够更好地控制资源访问,防止用户线程的不当行为影响系统运行。
| 技术点 | 作用 | 关键实现 |
|---|---|---|
| 信号量 | 协调多线程对共享资源的使用 | LDREX/STREX 指令,sem_trywait、sem_dopost 函数 |
| 互斥锁 | 实现资源的互斥访问 | 基于信号量的 mutex_lock、mutex_unlock 函数 |
| 优先级继承 | 解决优先级反转问题 | 临时提高共享资源任务的优先级 |
| 特权级别分离 | 控制任务对系统资源的访问权限 | 利用 MSP 和 PSP 寄存器,修改异常返回值 |
| 内存分段 | 防止任务访问关键内存区域 | MPU 配置,权限掩码设置 |
以下是系统资源分离的 mermaid 流程图:
graph TD;
A[启动系统] --> B[设置 MPU 配置];
B --> C[任务调度];
C --> D[上下文切换];
D --> E{任务特权级别};
E -- 特权级别 --> F[使用 MSP 寄存器];
E -- 用户级别 --> G[使用 PSP 寄存器];
D --> H[检查内存访问权限];
H -- 允许 --> I[任务继续执行];
H -- 不允许 --> J[中断任务];
通过合理运用这些技术,开发者可以构建出更加健壮、安全和高效的嵌入式系统。
超级会员免费看
858

被折叠的 条评论
为什么被折叠?



