25、并行任务与调度:构建多线程嵌入式环境

并行任务与调度:构建多线程嵌入式环境

在嵌入式系统开发中,实现并行任务和调度是构建多线程环境的关键。下面将详细介绍任务管理、上下文切换、任务创建以及调度器实现等方面的内容。

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;

通过以上内容,我们对并行任务与调度有了更深入的理解,希望这些知识能够帮助你在嵌入式系统开发中构建高效、稳定的多线程环境。

先展示下效果 https://pan.quark.cn/s/a4b39357ea24 遗传算法 - 简书 遗传算法的理论是根据达尔文进化论而设计出来的算法: 人类是朝着好的方向(最优解)进化,进化过程中,会自动选择优良基因,淘汰劣等基因。 遗传算法(英语:genetic algorithm (GA) )是计算数学中用于解决最佳化的搜索算法,是进化算法的一种。 进化算法最初是借鉴了进化生物学中的一些现象而发展起来的,这些现象包括遗传、突变、自然选择、杂交等。 搜索算法的共同特征为: 首先组成一组候选解 依据某些适应性条件测算这些候选解的适应度 根据适应度保留某些候选解,放弃其他候选解 对保留的候选解进行某些操作,生成新的候选解 遗传算法流程 遗传算法的一般步骤 my_fitness函数 评估每条染色体所对应个体的适应度 升序排列适应度评估值,选出 前 parent_number 个 个体作为 待选 parent 种群(适应度函数的值越小越好) 从 待选 parent 种群 中随机选择 2 个个体作为父方和母方。 抽取父母双方的染色体,进行交叉,产生 2 个子代。 (交叉概率) 对子代(parent + 生成的 child)的染色体进行变异。 (变异概率) 重复3,4,5步骤,直到新种群(parentnumber + childnumber)的产生。 循环以上步骤直至找到满意的解。 名词解释 交叉概率:两个个体进行交配的概率。 例如,交配概率为0.8,则80%的“夫妻”会生育后代。 变异概率:所有的基因中发生变异的占总体的比例。 GA函数 适应度函数 适应度函数由解决的问题决定。 举一个平方和的例子。 简单的平方和问题 求函数的最小值,其中每个变量的取值区间都是 [-1, ...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值