特殊线程
特殊线程是指用户需要用特殊调度策略进行调度的线程,如用户希望自己创建的线程采用RM、EDF等方式进行调度。
如果要用特殊调度策略的线程,就得使用扩展策略线程创建函数acoral_create_thread_ext(),它是一个宏,指向create_thread_ext()。
用户调用create_thread_ext()创建线程时,需要的参数分别为线程函数指针、堆栈大小、线程函数的参数、线程名称、堆栈指针、调度策略、调度策略数据。
acoral_id create_thread_ext(void (*route)(void *args), acoral_u32 stack_size, void *args, acoral_char *name, void *stack, acoral_u32 sched_policy, void *data){
acoral_thread_t *thread;
thread = acoral_alloc_thread(); //返回刚分配的TCB指针
if(NULL == thread){
acoral_printerr("Alloc thread:%s fail\n",name);
acoral_printk("No Mem Space or Beyond the max thread\n");
return -1;
}
thread->name = name;
stack_size = stack_size&(~3);
thread->stack_size = stack_size;
if(stack != NULL){
thread->stack_buttom = (acoral_u32 *)stack;
}else{
thread->stack_buttom = NULL;
}
thread->policy = sched_policy;
return acoral_policy_thread_init(sched_policy,thread,route,args,data);
}
创建特殊线程的过程与创建普通线程的过程大致相同,主要区别在于线程初始化,根据用户指定的调度策略,将线程初始化为某一特定类型的线程,acoral_policy_thread_init()将从调度策略链表上取出相应的策略控制块,根据其元素,对线程进行初始化。
需要的参数有调度策略、TCB、线程函数、线程函数的参数、线程策略数据。
acoral_id acoral_policy_thread_init(acoral_u32 policy, acoral_thread_t *thread, void (*route)(void *args), void *args, void *data){
acoral_sr CPU_sr;
acoral_sched_policy_t policy_ctrl;
policy_ctrl = acoral_get_policy_ctrl(policy);// 根据策略类型,找到调度策略控制块
if(policy_ctrl == NULL || policy_ctrl->plicy_thread_init == NULL){
HAL_ENTER_CRITICAL();
acoral_release_res((acoral_res_t *) thread);
HAL_EXIT_CRITICAL();
acoral_printerr("No thread policy support:%d\n",thread->policy);
return -1;
}
return policy_ctrl->policy_thred_init(thread,route,args,data);// 根据调度策略控制块初始化各成员,调用对应的线程初始化函数(采用什么线程初始化函数在系统初始化时绑定)
}
如果用户希望采用时间片轮转调度策略,将用slice_policy_thread_init(),根据CPU、优先级、优先级类型、时间片等信息对所创建的线程TCB成员进行初始化。而普通线程初始化时是不需要时间片信息的。
acoral_id slice_policy_init(acoral_thread_t *thread,void (*route)(void *args),void *args, void *data){
acoral_sr CPU_sr;
acoral_u32 prio;
acoral_slice_policy_data_t *policy_data;
slice_policy_data_t *private_data;
if(thread->policy == ACORAL_SCHED_POLICY_SLICE){
policy_data = (acoral_slice_policy_data_t *)data;// 将传入的调度策略数据data转换为acoral_slice_policy_data_t类型
thread->CPU = policy_data->CPU;
prio = policy_data->prio;
if(policy_data->prio_type == ACORAL_BASE_PRIO){
prio += ACORAL_BASE_PRIO_MIN;
if(prio >= ACORAL_BASE_PRIO_MAX){
prio = ACORAL_BASE_PRIO_MAX-1;
}
}
thread->prio = prio;
private_data = (slice_policy_data_t *)acoral_malloc2(sizeof(slice_policy_data_t)); //为TCB的private_data成员分配空间
if(private_data==NULL){
acoral_printerr("No level2 mem space for private_data:%s\n",thread->name);
HAL_ENTER_CRITICAL();
acoral_release_res((acoral_res_t *)thread);
HAL_EXIT_CRITICAL();
return -1;
}
private_data->slice_ld = TIME_TO_TICKS(policy_data->slice);//将时间片大小转换为Ticks,并赋值给private_data的slice_ld成员
thread->slice = private_data->slice_ld;// 将时间片赋给TCB的slice成员,供调度使用。
thread->private_data = private_data;
thread->CPU_mask= -1;
}
if(acoral_thread_init(thread,route,acoral_thread_exit,args) != 0){
acoral_printerr("No thread stack:%s\n",thread->name);
HAL_ENTER_CRITICAL();
acoral_release_res((acoral_res_t *)thread);
HAL_EXIT_CRITICAL();
return -1;
}
//将线程就绪,并重新调度
acoral_resume_thread(thread);
return thread->res.id;
}
每当系统Tick+1,slice就将-1。
编写线程函数
线程是一段代码执行的载体,这段代码就是线程函数。
创建线程所需要的第一个参数就是线程函数:void (*route)(void *args)。
当用户需要为自己创建的线程编写执行函数,编写的规范是void thread(void *args)。
线程函数返回为空,有一个参数,这个参数可以是任何数据结构,用户知道是什么数据结构,可以进行转换。
void最好换成ACORAL_COMM_THREAD或者ACORAL_RM_THREAD这些前缀,这些前缀可以看出线程所采用的调度策略。
ACORAL_COMM_THREAD test1()
{
acoral_print("in test1,this thread's period is 1s/n");
while(1){
acoral_delay_self(1000);
acoral_print("in test1\n");
}
}
void test_delay_init()
{
acoral_create_thread(test1,ACORAL_PRINT_STACK_SIZE,NULL,"test1",22,0);//执行线程的函数名、线程的堆栈空间、传进线程的参数、创建线程的名字、优先级,线程在0号CPU上运行
}
调度线程
创建线程的最后步骤就是将其挂载到就绪队列(此时,系统已经具备多个线程并发执行的环境),然后调用调度函数acoral_sched(),由它来安排线程的具体执行,这就是线程调度。
线程调度分为主动调度、被动调度。
- 主动调度:任务主动调用调度函数,根据调度算法选择需要执行的任务。
- 被动调度:往往是事件触发,如Ticks时钟中断来了,任务执行时间+1,导致任务的执行时间到了,又或者高优先级的任务的等待时间到了,就需要调用调度函数来切换任务。
对于嵌入式实时操作系统而言,调度策略通常基于优先级的抢占式调度,而调度的本质就是从就绪队列中找到最高优先级的线程来执行。
当就绪队列中有4个线程,其优先级分别为4、6、3、9,则调度方式如下。
调度前准备
void acoral_sched(){
if(!acoral_need_sched) // 判断是否需要调度
return;
if(acoral_intr_nesting) //如果中断被屏蔽
return;
if(acoral_sched_is_lock)//调度被禁止
return;
/*如果没有开始调度,则返回*/
if(!acoral_start_sched)
return;
/*进行简单处理后会直接或间接调用acoral_real_sched(),或者acoral_real_intr_sched()*/
HAL_SCHED_BRIDGE();
return;
}
- 调度开始标志acoral_start_sched,这个用来标志调度系统初始化完毕,系统可以进行调度了。
- 是否需要调度标志acoral_need_sched,为了减少不必要的调度。将线程挂到就绪队列或从就绪队列取下线程的过程与调用调度函数acoral_sched()有个时间差,而这个时间差中有可能发生中断,在中断退出时就会执行调度函数,该情况下返回的时候就没有必要再次执行调度函数了,所以用acoral_need_sched标志,表示可以调度了。当执行一次调度后,标志失败,除非再来一个操作(如挂起、恢复、创建线程等操作),才能重新置位此标志。
- 是否处于中断函数执行过程的标志。在中断函数执行过程中是不能进行线程切换的。
- 调度锁,禁止调度的标识,它是用来实现暂时禁止抢占功能的。
HAL_SCHED_BRIDGE()是一个可配置的硬件抽象层的宏。
#define HAL_SCHED_BRIDGE() hal_sched_bridge_comm()
void hal_sched_bridge_comm(){
acoral_sr cpu_sr;
HAL_ENTER_CRITICAL();
acoral_real_sched();
HAL_EXIT_CRITICAL();
}
acoral_real_sched()必须是原子的,调用时必须关闭中断,而stm32的硬件特性决定,HAL_CTX_SWITCH和HAL_SWITCH_TO等函数在pendsv中执行最合适,容易实现线程切换,即acoral_real_sched中的HAL_CTX_SWITCH和HAL_SWITCH_TO之前必须开启中断,这样就发生矛盾了。
将aCoral移植到stm32时,HAL_SCHED_BRIDGE会直接跳到汇编代码的HAL_SCHED_BRIDGE,再执行PENDSV中断。
相当于触发PENDSV中断,然后在中断服务程序中调用acoral_real_intr_sched。
acoral_real_intr_sched比acoral_real_sched多一个intr_nesting(中断前套数递减及判断操作)。
找到最高优先级线程
acoral_real_sched()的核心工作是从就绪队列中找到最高优先级线程在CPU上执行。
void acoral_real_sched(){
acoral_thread_t *prev;
acoral_thread_t *next;
acoral_set_need_sched(false);
prev = acoral_cur_thread;
/*选择最高优先级进程*/
acoral_select_thread();
next = acoral_ready_thread();
if(prev != next){
acoral_set_running_thread(next);
if(prev->state == ACORAL_THREAD_STATE_EXIT){
prev->state = ACORAL_THREAD_STATE_RELEASE;
HAL_SWITCH_TO(&next->stack);
return;
}
}
/*线程切换*/
HAL_CONTEXT_SWITCH(&prev->stack,&next->stack);
}
acoral_select_thread()从就绪队列中找到最高优先级线程后,如果*prev指向的线程(被抢占或者被中断的前一个线程)已执行完毕,且置为ACORAL_THREAD_STATE_EXIT状态,则直接从 *prev线程环境下切入到 next,只有一个参数:要切换的线程的堆栈指针,其接口形式为HAL_SWITCH_TO(&next->stack)。
如果prev指向的线程并未执行完毕,只是被暂时中断,则进行两个线程的上下文切换,其接口形式为HAL_CONTEXT_TO(&prev->stack, &next->stack)。
typedef struct{
acoral_list_t head;
void *data;
}acoral_queue_t;
typedef struct{
acoral_num; //就绪的线程数
acoral_u32 bitmap[PRIO_BITMAX_SIZE]; //优先级位图,每一位对应一个优先级,为1表示这个优先级有就绪线程。
acoral_queue_t queue[ACORAL_MAX_PRIO_NUM]; //每个优先级都有独立的队列
}acoral_rdy_queue_t;
/*选择优先级最高的线程*/
static acoral_rdy_queue_t acoral_ready_queues;
/*改成static acoral_rdy_queue_t *acoral_ready_queues后,由于static存在,会把这个指针变量初始化为0,后面初始化就绪队列的操作就会修改0地址的异常向量表,导致时钟中断被打开后,无法正常进入中断服务。*/
void acoral_select_thread() {
acoral_u32 index;
acoral_rdy_queue_t *rdy_queue;
acoral_list_t *head;
acoral_thread_t *thread;
acoral_queue_t *queue;
rdy_queue = &acoral_ready_queues;
index = acoral_get_highprio(rdy_queue);
queue = rdy_queue->queue + index;
head = &queue->head;
thread = list_entry(head->next,acoral_thread_t,ready);
acoral_set_ready_thread(thread);
}
aCoral线程优先级是通过acoral_prio_array来描述的,并且由优先级位图数组bitmap[PRIO_BITMAP_SIZE]标识某一优先级是否任务就绪。
acoral_u32 acoral_get_highprio(acoral_rdy_queue_t *array){
return acoral_find_first_bit(array->bitmap,PRIO_BITMAP_SIZE);
}
acoral_u32 acoral_ffs(acoral_u32 word){
acoral_u32 k;
k = 31;
// 如果低十六位有为1的,则为1的那一位肯定少于16,所以减掉16,并去掉高16
if(word & 0x0000ffff){
k-=16;
word <<= 16;
}
if(word & 0x00ff0000){
k-=8;
word <<= 8;
}
if(word & 0x0f000000){
k-=4;
k <<= 4;
}
if(word & 0x30000000){
k-=2;
k <<= 2;
}
if(word & 0x40000000){
k-=1;
}
return k;
}
acoral_u32 acoral_find_first_bit(const acoral_u32 *b,acoral_u32 length){
acoral_u32 v;
acoral_u32 off;
for(off = 0;v=b[0ff],off<length;off++){
if(v){ //选择bitmap中第一个数值不为0的32位
break;
}
}
return acoral_ffs(v) + off*32; //进一步确定最低一位是哪一位
}
线程切换
如果先前的线程处于退出状态“ACORAL_THREAD_STATE_EXIT”,则调用HAL_SWITCH_TO切换到最高优先级的线程;否则调用HAL_CONTEXT_SWITCH切换到最高优先级的线程,这就是线程抢占过程中的上下文切换(Context Switching)。
HAL_CONTEXT_SWITCH:
stmfd sp!,{lr} @保存PC
stmfd sp!,{r0-r12,lr} @保存寄存器LR,及r0-r12
mrs r4,CPSR
stmfd sp!,{r4} @保存cpsr
str sp,[r0] @保存旧上下文栈指针到旧的线程prev->stack
ldr sp,[r1] @获得新上下文指针
ldmfd sp!,{r0}
msr cpsr,r0 @恢复新cpsr(不能用spsr,因为sys,user模式没有SPSR)
ldmfd sp!,{r0-r12,lr,pc} @恢复寄存器
线程现场信息需要16个寄存器保存,并且hal_stack_init()函数模拟了任务第一次执行时的运行环境信息。这些信息的内容及保存顺序:PC、LR、R12、R11,…,R0、CPSR。实际任务切换顺序也是:PC、LR、R12、R11,…,R0、CPSR。
在执行HAL_CONTEXT_SWITCH(&prev->stack,&next->stack)时需要两个参数:要切换的两个线程堆栈指针变量地址,主要步骤为:
- 依次保存CPU的PC、LR、R12、R11…R0、CPSR到被抢占线程的堆栈(R12(SP)指向的地址)。
- 将R13(SP)保存到Prev-stack中,此时SP->CPSR。
- 将抢占线程堆栈地址(Next->stack)传给R13(SP)。
- 将抢占线程堆栈(R13(SP)指向的地址)内容依次恢复到CPU的CPSR、R0、…R12、LR、PC寄存器中。
stmfd sp!,{lr}保存的LR是调用HAL_CONTEXT_SWITCH之前的程序指针PC,stmfd sp!,{r0-r12,lr}保存的LR是任务环境里的链接寄存器。
造成任务切换的原因:
- 任务主动发起线程切换。如果任务主动发起线程切换,例如,某一任务执行过程中需要主动挂起自己(acoral_suspend_self()),这将触发acoral_sched()重调度,此时,两个LR的值是相等的,都是调用HAL_CONTEXT_SWITCH之前的程序指针,因为切换前后,ARM处理器一直处于系统模式/用户模式(System/User)。
- 中断触发线程切换。如果是中断引发线程切换,例如,一个低优先级的线程正在运行,用户通过一个中断创建一个新线程,而新线程的优先级高于以前线程的优先级,则低优先级任务将被高优先级任务抢占,此时第一个LR保存的是中断模式下的程序指针PC,第二个LR是用户模式下旧线程的LR,两个LR不相等。
ARM有七种工作模式:用户(User)、快速中断(FIQ)、外部中断(IRQ)、管理(Supervisor)、数据访问中止(Abort)、系统(System)、未定义(Undefined)。
中断发生器前,处理器工作中系统模式/用户模式(System/User),中断发生后,处理器自动切换到中断模式(IRQ),在不同模式下,寄存器分配及使用是不一样的。
在该情况下,内核将通过acoral_real_intr_sched()触发重调度,此时的任务切换工作是由HAL_INTR_CTX_SWITCH完成的。
HAL_INTR_CTX_SWITCH:
stmfd sp!,{r2-r12,lr} @保存正在服务的中断上下文
@以下几行把旧的线程prev的上下文从正在服务的中断栈顶转移到虚拟机栈中
ldr r2,=IRQ_stack @取irq栈基址,这里存放着被中断线程的上下文
ldmea r2!,{r3-r10} @按递增式空栈方式弹栈,结果:
@[r2-1]=LR_irq->r10,被中断线程的PC+4
@[r2-2]=r12->r9,被中断线程的r12
@[r2-3]=r11->r8,被中断线程的PC
@.......
@[r2-8]=r6->r3,被中断线程的r6
sub r10,r10,#4 @中断栈中的LR_irq-4=PC
@以下三句就是取出旧的线程prev的SP_sys,只能通过stmfd指令间接取
mov r11,sp @下一句不能用SP,故先拷贝到r11
stmfd r11!,{sp}^ @被中断线程的SP_sys压入正在服务的中断栈中
ldmfd r11!,{r12} @从正在服务的中断栈中读取 SP_sys->R12
stmfd r12!,{r10} @保存 PC_sys
stmfd r12!,{lr}^ @保存 lr_sys
stmfd r12!,{r3-r9} @保存被中断线程的r12-r6到它的栈中
ldmea r2!,{r3-r9} @读被中断线程的r5-r0->r9-r4,SPSR_irq->r3,递增式空栈
stmfd r12!,{r3-r9} @保存被中断线程的r5-r0,CPSR_sys到它的栈中
str r12,[r0] @换出的上下文的栈指针-->old_sp
@以下几行把新的线程next的上下文copy到IRQ栈顶
@与递减式满栈对应,此时IRQ栈用递增式空栈的方式访问
ldr r12,[r1] @读取需换入的栈指针
ldmfd r12!,{r3-r11} @读取换入线程的CPSR_sys->r3
@读取换入线程的r0-r7->r4-r11
stmea r2!,{r3-r11} @保存换入线程的CPSR_sys->SPSR_irq, r0-r7到IRQ栈
ldmfd r12!,{r3-r7} @读取换入线程的r8-r12->r3-r7
stmea r2!,{r3-r7} @保存换入线程的r8-r12到IRQ栈
ldmfd r12!,{lr}^ @恢复换入线程的LR_sys到寄存器中
ldmfd r12!,{r3} @读取换入线程的PC->r3
add r3,r3,#4 @模拟IRQ保存被中断上下文PC的方式:PC+4->LR_irq
stmea r2!,{r3} @保存换入线程的LR_irq到IRQ栈
@就是将r12赋值给sp^,因为无法通过mov,所以要
stmfd r12!,{r12} @读取SP_sys到r12
ldmfd r12!,{sp}^ @恢复SP_sys
mov r0,r0 @无论是否操作当前状态的SP,操作sp后,不能立即执行函数
@返回指令,否则返回指令的结果不可预知。
ldmfd sp!,{r2-r12,pc}
中断环境下,中断硬件系统可能已经保存了部分旧线程的环境,因此线程切换时需要做特殊处理。
中断环境下,线程切换函数调用后不能立即切换到新的线程,中断服务函数必须执行完才能执行新的线程,否则,进入新的线程,相当于中断被终止了,无法复原中断。
就比如中断模式下的堆栈,因为中断没有完全执行完,中断的堆栈没有回收,这种情况肯定是不能容忍的。
中断环境下,旧线程的运行环境无法获取,运行到中断切换任务函数时,处理器寄存器的值已经不是旧的线程的寄存器了,被破坏掉了,因此在中断入口就得保存旧的线程的环境。
这里有两种方式来处理中断发生时保存旧线程的运行环境:
- 刚进入中断时,就将旧的线程保存到旧的线程的堆栈,这种方式在退出中断时需要切换线程的情况下效果很好,但如果不是这样,则需要弹出旧的线程的堆栈,这样就比较麻烦,且绝大部分中断不会触发切换。
- 刚进入中断时,将旧的线程的上下文环境保存到中断的堆栈中,而在线程切换时从中断模式栈顶复制环境到旧的线程的堆栈,这样虽然复杂些,但是在中断发生后不需要切换线程,中断退出的处理简单。