并行任务与调度:构建多线程嵌入式环境
在嵌入式系统开发中,实现并行任务和调度是构建高效多线程环境的关键。下面将详细介绍任务管理、上下文切换、任务创建以及调度器实现等方面的内容。
1. 任务管理
操作系统通过交替运行应用程序,为并行运行的进程和线程提供抽象。在单 CPU 系统中,同一时间只能有一个线程运行,其他线程则处于等待状态,直到下一次任务切换。
任务管理有两种模式:
-
协作模式
:任务切换是线程主动请求的自愿行为。
-
抢占模式
:内核会在任务执行的任意点周期性中断任务,临时保存状态并恢复下一个任务。
任务切换的核心是上下文切换,即把当前 CPU 寄存器的值存储到 RAM 中,并从内存中加载下一个任务的寄存器值。
1.1 任务块
任务在系统中以任务块结构表示,包含调度器跟踪任务状态所需的所有信息。每个任务块可能包含指向起始函数的指针、可选参数,以及为每个任务分配的私有栈区域。
#define TASK_WAITING 0
#define TASK_READY 1
#define TASK_RUNNING 2
#define TASK_NAME_MAXLEN 16;
struct task_block {
char name[TASK_NAME_MAXLEN];
int id;
int state;
void (*start)(void *arg);
void *arg;
uint32_t *sp;
};
为了管理系统中的所有任务块,定义了一个全局数组:
#define MAX_TASKS 8
static struct task_block TASKS[MAX_TASKS];
static int n_tasks = 1;
static int running_task_id = 0;
#define kernel TASKS[0]
任务创建函数
task_create
用于创建新任务:
struct task_block *task_create(char *name, void (*start)(void *arg), void *arg)
{
struct task_block *t;
int i;
if (n_tasks >= MAX_TASKS)
return NULL;
t = &TASKS[n_tasks];
t->id = n_tasks++;
for (i = 0; i < TASK_NAME_MAXLEN; i++) {
t->name[i] = name[i];
if (name[i] == 0)
break;
}
t->state = TASK_READY;
t->start = start;
t->arg = arg;
t->sp = ((&stack_space) + n_tasks * STACK_SIZE);
task_stack_init(t);
return t;
}
1.2 内存分配
运行独立栈需要预先为每个任务分配内存。在简单情况下,所有任务使用相同大小的栈,且在调度器启动前创建,不能终止。
内存区域可在链接脚本中定义:
PROVIDE(_end_stack = ORIGIN(CCRAM) + LENGTH(CCRAM));
PROVIDE(stack_space = ORIGIN(CCRAM));
PROVIDE(_start_heap = _end);
在内核源码中声明:
extern uint32_t stack_space;
#define STACK_SIZE (256)
2. 上下文切换
上下文切换过程包括在执行期间获取 CPU 寄存器的值,并将其保存到当前运行任务的栈底,然后恢复下一个任务的值以继续执行。
Cortex - M 提供了两个用于上下文切换的 CPU 异常:
-
PendSV
:抢占式内核通过设置系统控制块中特定寄存器的一位来强制立即中断,通常与下一个任务的上下文切换相关。
-
SVCall
:用户应用程序向内核请求访问资源的主要入口点,可实现阻塞系统调用的抽象。
在上下文切换期间,部分寄存器值的存储和恢复由硬件实现,而 R4 - R11 寄存器的值需要通过软件处理:
static void __attribute__((naked)) store_context(void)
{
asm volatile("mrs r0, msp");
asm volatile("stmdb r0!, {r4-r11}");
asm volatile("msr msp, r0");
asm volatile("bx lr");
}
static void __attribute__((naked)) restore_context(void)
{
asm volatile("mrs r0, msp");
asm volatile("ldmfd r0!, {r4-r11}");
asm volatile("msr msp, r0");
asm volatile("bx lr");
}
3. 任务创建
系统运行时,除运行中的任务外,其他任务都处于等待状态,完整的栈帧保存在栈底,栈指针存储在控制块中。
新创建的任务在上下文切换中间首次唤醒,需要伪造一个栈帧推到栈底,以便任务恢复时能从入口点继续执行。
struct stack_frame {
uint32_t r0, r1, r2, r3, r12, lr, pc, xpsr;
};
struct extra_frame {
uint32_t r4, r5, r6, r7, r8, r9, r10, r11;
};
static void task_stack_init(struct task_block *t)
{
struct stack_frame *tf;
t->sp -= sizeof(struct stack_frame);
tf = (struct stack_frame *)(t->sp);
tf->r0 = (uint32_t) t->arg;
tf->pc = (uint32_t) t->start;
tf->lr = (uint32_t) task_terminated;
tf->xpsr = (1 << 24);
t->sp -= sizeof(struct extra_frame);
}
4. 简单内核示例
以下是一个简单的内核主函数,用于创建任务和准备栈:
void main(void) {
clock_pll_on(0);
systick_enable();
led_setup();
kernel.name[0] = 0;
kernel.id = 0;
kernel.state = TASK_RUNNING;
task_create("test0",task_test0, NULL);
task_create("test1",task_test1, NULL);
while(1) {
schedule();
}
}
5. 调度器实现
调度器的实现方式决定了系统的架构,可采用协作调度或抢占调度。
5.1 管理调用
调度器的核心是与系统中断事件相关的异常处理程序,如 PendSV 和 SVCall。通过设置特定标志触发 PendSV 异常:
#define SCB_ICSR (*((volatile uint32_t *)0xE000ED04))
#define schedule() SCB_ICSR |= (1 << 28)
PendSV 处理程序完成上下文切换的步骤如下:
1. 将当前栈指针存储在任务块中。
2. 调用
store_context
函数将额外的栈帧推到栈中。
3. 将当前任务状态改为
TASK_READY
。
4. 选择一个新任务恢复执行。
5. 将新任务状态改为
TASK_RUNNING
。
6. 从关联的任务块中获取新的栈指针。
7. 调用
restore_context
函数从栈中弹出额外的栈帧。
8. 设置中断处理程序的特殊返回值以激活线程模式。
void __attribute__((naked)) isr_pendsv(void)
{
store_context();
asm volatile("mrs %0, msp" : "=r"(TASKS[running_task_id].sp));
TASKS[running_task_id].state = TASK_READY;
running_task_id++;
if (running_task_id >= n_tasks)
running_task_id = 0;
TASKS[running_task_id].state = TASK_RUNNING;
asm volatile("msr msp, %0"::"r"(TASKS[running_task_id].sp));
restore_context();
asm volatile("mov lr, %0" ::"r"(0xFFFFFFF9));
asm volatile("bx lr");
}
5.2 协作调度器
在协作调度中,每个任务的主函数通过调用
schedule
宏自愿暂停执行。
void task_test0(void *arg)
{
uint32_t now = jiffies;
blue_led_on();
while(1) {
if ((jiffies - now) > 1000) {
blue_led_off();
schedule();
now = jiffies;
blue_led_on();
}
}
}
void task_test1(void *arg)
{
uint32_t now = jiffies;
red_led_on();
while(1) {
if ((jiffies - now) > 1000) {
red_led_off();
schedule();
// 原文档此处代码未完整给出,推测后续逻辑与 task_test0 类似
}
}
}
总结
通过以上内容,我们了解了如何在嵌入式系统中实现任务管理、上下文切换、任务创建和调度器。这些技术是构建多线程嵌入式环境的基础,不同的调度策略可以根据具体需求进行选择和实现。
流程图:PendSV 上下文切换流程
graph TD;
A[开始] --> B[存储当前栈指针到任务块];
B --> C[调用 store_context 保存额外栈帧];
C --> D[将当前任务状态改为 TASK_READY];
D --> E[选择新任务];
E --> F[将新任务状态改为 TASK_RUNNING];
F --> G[从任务块获取新栈指针];
G --> H[调用 restore_context 恢复额外栈帧];
H --> I[设置特殊返回值激活线程模式];
I --> J[结束];
表格:任务状态定义
| 状态 | 含义 |
|---|---|
| TASK_WAITING | 任务等待中 |
| TASK_READY | 任务准备好执行 |
| TASK_RUNNING | 任务正在执行 |
并行任务与调度:构建多线程嵌入式环境(续)
6. 调度策略分析
除了前面提到的协作调度器,还有其他多种调度策略可以用于交替执行系统中的任务,下面将对一些常见的调度策略进行分析。
6.1 轮转调度(Round - Robin Scheduling)
轮转调度是一种简单且常用的调度策略。在这种策略下,每个任务被分配一个固定的时间片(时间量子),当任务的时间片用完后,调度器会将 CPU 控制权交给下一个任务。
以下是一个简单的轮转调度示例代码:
#define TIME_QUANTUM 100 // 时间片大小
void round_robin_scheduler() {
static int current_task = 0;
while (1) {
// 执行当前任务
TASKS[current_task].state = TASK_RUNNING;
// 模拟任务执行
for (int i = 0; i < TIME_QUANTUM; i++) {
// 执行任务代码
}
TASKS[current_task].state = TASK_READY;
// 切换到下一个任务
current_task = (current_task + 1) % n_tasks;
}
}
6.2 优先级调度(Priority Scheduling)
优先级调度根据任务的优先级来决定哪个任务先执行。优先级高的任务会优先获得 CPU 资源。
#define MAX_PRIORITY 5
// 假设 task_block 结构体中添加了 priority 字段
struct task_block {
char name[TASK_NAME_MAXLEN];
int id;
int state;
void (*start)(void *arg);
void *arg;
uint32_t *sp;
int priority;
};
void priority_scheduler() {
while (1) {
int highest_priority_task = -1;
int highest_priority = -1;
for (int i = 0; i < n_tasks; i++) {
if (TASKS[i].state == TASK_READY && TASKS[i].priority > highest_priority) {
highest_priority = TASKS[i].priority;
highest_priority_task = i;
}
}
if (highest_priority_task != -1) {
TASKS[highest_priority_task].state = TASK_RUNNING;
// 执行任务代码
TASKS[highest_priority_task].state = TASK_READY;
}
}
}
7. 同步机制
在多线程环境中,同步机制是必不可少的,用于避免多个任务同时访问共享资源而导致的数据不一致或其他问题。
7.1 互斥锁(Mutex)
互斥锁是一种最基本的同步机制,用于保护共享资源,确保同一时间只有一个任务可以访问该资源。
// 定义互斥锁结构体
typedef struct {
int locked;
} Mutex;
// 初始化互斥锁
void mutex_init(Mutex *mutex) {
mutex->locked = 0;
}
// 加锁操作
void mutex_lock(Mutex *mutex) {
while (mutex->locked) {
// 等待锁释放
}
mutex->locked = 1;
}
// 解锁操作
void mutex_unlock(Mutex *mutex) {
mutex->locked = 0;
}
7.2 信号量(Semaphore)
信号量是一种更通用的同步机制,可以用于控制对多个共享资源的访问。
// 定义信号量结构体
typedef struct {
int count;
} Semaphore;
// 初始化信号量
void semaphore_init(Semaphore *semaphore, int initial_count) {
semaphore->count = initial_count;
}
// P 操作(等待信号量)
void semaphore_wait(Semaphore *semaphore) {
while (semaphore->count <= 0) {
// 等待信号量可用
}
semaphore->count--;
}
// V 操作(释放信号量)
void semaphore_signal(Semaphore *semaphore) {
semaphore->count++;
}
8. 系统资源分离
为了提高系统的稳定性和安全性,需要对系统资源进行分离。例如,将内核空间和用户空间的资源分开,避免用户任务对内核资源的非法访问。
在前面的示例中,我们已经对栈空间进行了分离,将每个任务的栈空间独立分配。此外,还可以对内存、外设等资源进行分离。
// 内存分配示例,将 SRAM 分为内核和用户空间
// 内核空间使用 CCRAM 剩余部分作为栈
// 用户空间使用 SRAM 除 .data 和 .bss 部分作为堆
PROVIDE(_end_stack = ORIGIN(CCRAM) + LENGTH(CCRAM));
PROVIDE(stack_space = ORIGIN(CCRAM));
PROVIDE(_start_heap = _end);
9. 总结与展望
通过本文的介绍,我们详细了解了并行任务和调度在嵌入式系统中的实现方法,包括任务管理、上下文切换、任务创建、调度器实现、同步机制和系统资源分离等方面。
不同的调度策略和同步机制可以根据具体的应用场景进行选择和组合。例如,在对实时性要求较高的系统中,可以采用优先级调度策略;在资源竞争不激烈的系统中,可以使用协作调度器。
未来,随着嵌入式系统的发展,对并行任务和调度的要求也会越来越高。例如,支持更多的任务、更高的并发度、更复杂的调度策略等。同时,安全性和可靠性也是需要重点关注的方面,需要进一步研究和优化系统资源分离和同步机制。
流程图:优先级调度流程
graph TD;
A[开始] --> B[查找最高优先级的就绪任务];
B --> C{是否找到任务};
C -- 是 --> D[将任务状态改为 TASK_RUNNING];
D --> E[执行任务];
E --> F[将任务状态改为 TASK_READY];
F --> B;
C -- 否 --> B;
表格:同步机制对比
| 同步机制 | 特点 | 适用场景 |
|---|---|---|
| 互斥锁 | 一次只允许一个任务访问共享资源 | 保护临界区,避免数据竞争 |
| 信号量 | 可以控制对多个共享资源的访问 | 资源计数,生产者 - 消费者问题等 |
超级会员免费看
4545

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



