并行任务与调度:构建多线程嵌入式环境
在嵌入式系统开发中,实现并行任务和调度是构建多线程环境的关键。下面将详细介绍任务管理、上下文切换、任务创建以及调度器实现等方面的内容。
1. 任务管理
操作系统通过交替运行应用程序,提供了并行运行进程和线程的抽象。在单 CPU 系统中,同一时间只能有一个线程运行,其他线程则处于等待状态,直到下一次任务切换。
任务管理有两种模式:
-
协作模式
:任务切换是线程主动请求的操作。
-
抢占模式
:内核会周期性地在任务执行的任意点中断任务,临时保存状态并恢复下一个任务。
任务切换的核心是上下文切换,即把当前 CPU 寄存器的值存储到 RAM 中,并从内存中加载下一个要运行任务的寄存器值。
任务块结构
任务在系统中以任务块结构表示,包含了调度器跟踪任务状态所需的所有信息。每个任务块可能包含以下内容:
- 指向起始函数的指针,定义任务启动时执行的代码起始位置。
- 一组可选参数。
- 为每个任务分配的私有栈区域,用于存储寄存器值,确保线程和进程的执行上下文相互分离。
以下是一个简单的任务块结构声明:
#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;
}
2. 上下文切换
上下文切换过程包括在执行期间获取 CPU 寄存器的值,并将其保存到当前运行任务的栈底,然后恢复下一个任务的寄存器值以继续执行。
在 Cortex - M 平台上,有两个 CPU 异常用于支持上下文切换:
-
PendSV
:用于强制在不久的将来触发中断,通常与下一个任务的上下文切换相关。
-
SVCall
:用户应用程序向内核提交访问资源正式请求的主要入口。
在上下文切换时,Cortex - M CPU 会自动将部分寄存器的值复制到栈中,形成栈帧。但栈指针不包含 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);
}
起始任务的寄存器初始化如下:
-
程序计数器 (PC)
:包含起始函数的地址。
-
R0 - R3
:可能包含传递给起始函数的可选参数。
-
执行程序状态寄存器 (xPSR)
:仅设置第 24 位的拇指标志。
-
链接寄存器 (LR)
:包含起始函数返回时调用的过程指针。
4. 内存分配与任务栈
运行独立栈需要预先为每个任务分配内存。在简单情况下,所有任务使用相同大小的栈,并且在调度器启动前创建,不能终止。
参考平台有一个独立的核心耦合内存,栈空间的起始地址映射在 CCRAM 的开头,剩余的 CCRAM 空间用作内核栈,SRAM 除了
.data
和
.bss
段外用于堆分配。
以下是相关的链接脚本指令和内核源文件声明:
// 链接脚本指令
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)
每次创建新任务时,栈空间中的下一个千字节将被分配为其执行栈,初始栈指针设置在该区域的最高地址。
5. 简单内核示例
以下是一个简单的内核主函数示例,用于创建任务和准备栈:
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();
}
}
6. 调度器实现
调度器的核心组件是与系统中断事件相关的异常处理程序,如 PendSV 和 SVCall。
6.1 超级调用
在 Cortex - M 上,可以通过设置中断控制和状态寄存器的 PENDSET 标志(对应第 28 位)来触发 PendSV 异常。定义了一个简单的宏来启动上下文切换:
#define SCB_ICSR (*((volatile uint32_t *)0xE000ED04))
#define schedule() SCB_ICSR |= (1 << 28)
6.2 PendSV 处理程序
PendSV 处理程序需要完成以下步骤来实现上下文切换:
1. 将当前栈指针从 SP 寄存器存储到任务块中。
2. 调用
store_context
函数将额外的栈帧压入栈中。
3. 将当前任务的状态更改为
TASK_READY
。
4. 选择一个新任务来恢复执行。
5. 将新任务的状态更改为
TASK_RUNNING
。
6. 从关联的任务块中获取新的栈指针。
7. 调用
restore_context
函数从栈中弹出额外的栈帧。
8. 设置一个特殊的返回值,以便在 PendSV 服务例程结束时激活线程模式。
以下是
isr_pendsv
函数的实现:
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");
}
6.3 协作调度器
在协作调度器中,每个任务的主函数通过调用
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();
}
}
}
总结
通过上述内容,我们了解了如何在嵌入式系统中实现任务管理、上下文切换、任务创建和调度器。这些技术是构建多线程嵌入式环境的基础,能够提高系统的并发处理能力和资源利用率。不同的调度策略可以根据具体需求进行选择和实现,以满足系统的性能和功能要求。
流程总结
以下是上下文切换的流程:
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 | 任务正在执行 |
并行任务与调度:构建多线程嵌入式环境
7. 调度策略分析
除了前面提到的协作调度器,还有其他多种调度策略可以用于交替执行系统中的任务,下面对几种常见的调度策略进行分析。
7.1 轮转调度(Round - Robin Scheduling)
轮转调度是一种简单且公平的调度策略。在这种策略下,每个任务被分配一个固定的时间片(时间量子),当任务的时间片用完后,调度器会强制进行上下文切换,将 CPU 控制权交给下一个任务。这种调度策略确保了每个任务都有机会在一定时间内执行,避免了某个任务长时间占用 CPU。
实现轮转调度时,需要在调度器中维护一个任务队列,按照任务的顺序依次分配时间片。当一个任务的时间片用完后,将其移到队列的末尾,等待下一次调度。
以下是一个简单的轮转调度示例代码框架:
// 假设已经有任务块数组 TASKS 和任务数量 n_tasks
int current_task_index = 0;
int time_quantum = 10; // 时间片大小
void round_robin_schedule() {
// 执行当前任务
// 模拟时间片计数
for (int i = 0; i < time_quantum; i++) {
// 执行任务的代码逻辑
}
// 时间片用完,切换任务
current_task_index = (current_task_index + 1) % n_tasks;
// 触发上下文切换
schedule();
}
7.2 优先级调度(Priority Scheduling)
优先级调度根据任务的优先级来决定哪个任务先执行。每个任务被分配一个优先级,调度器总是选择优先级最高的任务执行。当有更高优先级的任务就绪时,调度器会立即中断当前正在执行的任务,将 CPU 控制权交给高优先级任务。
实现优先级调度时,需要在任务块结构中添加一个优先级字段,并在调度器中根据优先级对任务进行排序。
以下是修改后的任务块结构和优先级调度示例代码框架:
// 修改后的任务块结构
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_schedule() {
int highest_priority_task_index = 0;
for (int i = 1; i < n_tasks; i++) {
if (TASKS[i].priority > TASKS[highest_priority_task_index].priority) {
highest_priority_task_index = i;
}
}
// 如果最高优先级任务不是当前任务,进行上下文切换
if (highest_priority_task_index != running_task_id) {
running_task_id = highest_priority_task_index;
schedule();
}
}
8. 同步机制
在多线程环境中,多个任务可能会同时访问共享资源,这可能会导致数据竞争和不一致的问题。因此,需要使用同步机制来确保任务之间的正确协作。
8.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;
}
使用互斥锁的示例代码:
Mutex shared_resource_mutex;
int shared_resource = 0;
void task_access_shared_resource(void *arg) {
mutex_lock(&shared_resource_mutex);
// 访问共享资源
shared_resource++;
mutex_unlock(&shared_resource_mutex);
}
8.2 信号量(Semaphore)
信号量是一种更通用的同步机制,它可以用于控制对多个共享资源的访问。信号量维护一个计数器,当计数器大于 0 时,任务可以获取信号量并访问资源;当计数器为 0 时,任务将被阻塞。
以下是一个简单的信号量实现示例:
// 信号量结构体
typedef struct {
int count;
} Semaphore;
// 初始化信号量
void semaphore_init(Semaphore *semaphore, int initial_count) {
semaphore->count = initial_count;
}
// 获取信号量
void semaphore_wait(Semaphore *semaphore) {
while (semaphore->count <= 0) {
// 忙等待
}
semaphore->count--;
}
// 释放信号量
void semaphore_signal(Semaphore *semaphore) {
semaphore->count++;
}
使用信号量的示例代码:
Semaphore resource_semaphore;
int resource_count = 2; // 假设有 2 个资源
void task_access_resource(void *arg) {
semaphore_wait(&resource_semaphore);
// 访问资源
semaphore_signal(&resource_semaphore);
}
9. 系统资源分离
为了确保系统的稳定性和安全性,需要对系统资源进行分离。不同的任务应该有独立的内存空间、寄存器状态等,避免相互干扰。
在前面的任务管理中,已经通过为每个任务分配独立的栈空间来实现了部分资源分离。此外,还可以通过内存保护单元(MPU)来进一步保护任务的内存空间,防止任务越界访问其他任务的内存。
以下是一个简单的使用 MPU 进行内存保护的示例代码框架:
// 假设已经有 MPU 相关的寄存器定义
void setup_mpu_for_task(struct task_block *task) {
// 设置 MPU 寄存器,为任务分配独立的内存区域
// 例如,设置内存区域的起始地址、大小、访问权限等
// MPU->RBAR = task->memory_start_address;
// MPU->RASR = task->memory_attributes;
}
10. 总结与展望
通过本文的介绍,我们详细了解了并行任务与调度的相关知识,包括任务管理、上下文切换、任务创建、调度器实现、调度策略、同步机制和系统资源分离等方面。这些技术是构建多线程嵌入式环境的基础,能够提高系统的并发处理能力和资源利用率。
在实际应用中,可以根据系统的需求选择合适的调度策略和同步机制,以确保系统的性能和稳定性。未来,可以进一步研究和实现更复杂的调度算法和同步机制,如实时调度算法、读写锁等,以满足不同场景下的需求。
调度策略对比表
| 调度策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 协作调度 | 实现简单,任务自主控制切换 | 可能导致某个任务长时间占用 CPU | 对实时性要求不高的系统 |
| 轮转调度 | 公平性好,每个任务都有机会执行 | 时间片大小难以确定 | 对公平性要求较高的系统 |
| 优先级调度 | 可以优先处理重要任务 | 可能导致低优先级任务饥饿 | 对实时性要求较高的系统 |
同步机制流程图
graph TD;
A[任务请求资源] --> B{资源是否可用};
B -- 是 --> C[获取资源,执行任务];
C --> D[释放资源];
B -- 否 --> E[任务阻塞等待];
E --> F{资源可用};
F -- 是 --> C;
通过以上内容,我们对并行任务与调度有了更深入的理解,希望这些知识能够帮助你在嵌入式系统开发中构建高效、稳定的多线程环境。
超级会员免费看
4587

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



