嵌入式实时操作系统的设计与开发(四)

特殊线程

特殊线程是指用户需要用特殊调度策略进行调度的线程,如用户希望自己创建的线程采用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;
}
  1. 调度开始标志acoral_start_sched,这个用来标志调度系统初始化完毕,系统可以进行调度了。
  2. 是否需要调度标志acoral_need_sched,为了减少不必要的调度。将线程挂到就绪队列或从就绪队列取下线程的过程与调用调度函数acoral_sched()有个时间差,而这个时间差中有可能发生中断,在中断退出时就会执行调度函数,该情况下返回的时候就没有必要再次执行调度函数了,所以用acoral_need_sched标志,表示可以调度了。当执行一次调度后,标志失败,除非再来一个操作(如挂起、恢复、创建线程等操作),才能重新置位此标志。
  3. 是否处于中断函数执行过程的标志。在中断函数执行过程中是不能进行线程切换的。
  4. 调度锁,禁止调度的标识,它是用来实现暂时禁止抢占功能的。

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)时需要两个参数:要切换的两个线程堆栈指针变量地址,主要步骤为:

  1. 依次保存CPU的PC、LR、R12、R11…R0、CPSR到被抢占线程的堆栈(R12(SP)指向的地址)。
  2. 将R13(SP)保存到Prev-stack中,此时SP->CPSR。
  3. 将抢占线程堆栈地址(Next->stack)传给R13(SP)。
  4. 将抢占线程堆栈(R13(SP)指向的地址)内容依次恢复到CPU的CPSR、R0、…R12、LR、PC寄存器中。

stmfd sp!,{lr}保存的LR是调用HAL_CONTEXT_SWITCH之前的程序指针PC,stmfd sp!,{r0-r12,lr}保存的LR是任务环境里的链接寄存器。

造成任务切换的原因:

  1. 任务主动发起线程切换。如果任务主动发起线程切换,例如,某一任务执行过程中需要主动挂起自己(acoral_suspend_self()),这将触发acoral_sched()重调度,此时,两个LR的值是相等的,都是调用HAL_CONTEXT_SWITCH之前的程序指针,因为切换前后,ARM处理器一直处于系统模式/用户模式(System/User)。
  2. 中断触发线程切换。如果是中断引发线程切换,例如,一个低优先级的线程正在运行,用户通过一个中断创建一个新线程,而新线程的优先级高于以前线程的优先级,则低优先级任务将被高优先级任务抢占,此时第一个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}

中断环境下,中断硬件系统可能已经保存了部分旧线程的环境,因此线程切换时需要做特殊处理。
中断环境下,线程切换函数调用后不能立即切换到新的线程,中断服务函数必须执行完才能执行新的线程,否则,进入新的线程,相当于中断被终止了,无法复原中断。
就比如中断模式下的堆栈,因为中断没有完全执行完,中断的堆栈没有回收,这种情况肯定是不能容忍的。
中断环境下,旧线程的运行环境无法获取,运行到中断切换任务函数时,处理器寄存器的值已经不是旧的线程的寄存器了,被破坏掉了,因此在中断入口就得保存旧的线程的环境。
这里有两种方式来处理中断发生时保存旧线程的运行环境:

  1. 刚进入中断时,就将旧的线程保存到旧的线程的堆栈,这种方式在退出中断时需要切换线程的情况下效果很好,但如果不是这样,则需要弹出旧的线程的堆栈,这样就比较麻烦,且绝大部分中断不会触发切换。
  2. 刚进入中断时,将旧的线程的上下文环境保存到中断的堆栈中,而在线程切换时从中断模式栈顶复制环境到旧的线程的堆栈,这样虽然复杂些,但是在中断发生后不需要切换线程,中断退出的处理简单。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饼干饼干圆又圆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值