基本调度机制
用户在基于RTOS开发应用前,首先要创建线程。
aCoral中,用户创建一个线程时须指定用户希望采用的调度策略,例如,用户想创建一个周期性执行的线程并希望通过周期来触发多线程的调度。
//创建一个周期性的线程
acoral_period_policy_data_t data;
data.CPU = 0;
data.prio = 25;
data.timer = 1000;
acoral_create_thread_ext(test,ACORAL_PRINT_STACK_SIZE,0,NULL,NULL,ACORAL_SCHED_POLICY_PERIOD,&data);//调用线程创建接口
aCoral将线程创建分为两大类:
- 普通线程创建
- 特殊线程创建
普通线程
普通线程是指用户需要用通用调度策略进行调度的线程,例如,用户希望自己创建的线程采用FIFS的方式进行调度。
如果要创建通用调度策略的线程,就用普通线程创建函数acoral_create_thread(),它是一个宏,指向create_comm_thread()
#define acoral_create_thread(route,stack_szie,args,name,prio,CPU) create_comm_thread(route,stack_szie,args,name,prio,CPU);
#define acoral_create_thread_ext(route,stack_size,args,name,stack,policy,policy,data) create_thread_ext(route,stack_size,args,name,stack,policy,policy,data);
创建普通线程时,需要的参数分别为:执行线程的函数名,线程的堆栈空间,传入线程的参数,创建线程的名字,创建线程的优先级,绑定线程到指定CPU运行。
创建线程需要做的第一项工作:为线程分配内存空间,线程通过acoral_thread_t描述的,为该线程分配空间就是为TCB分配空间,其返回值是刚分配的TCB的指针。
分配过程通过函数acoral_alloc_thread()实现。
为TCB分配空间后,便是对TCB的各成员和调度策略控制块赋值,部分值是用户传入的参数,另一些值是在create_comm_thread()内部确定的。
acoral_id create_comm_thread(void (*route)(void *args), acoral_u32 stack_size,void *args, acoral_char *name, acoral_u8 prio, acoral_8 CPU){
acoral_comm_policy_data_t policy_ctrl;
acoral_thread_t *thread;
// 分配tcb数据块
thread = acoral_alloc_thread(); // 返回刚分配的线程指针
if(NULL == thread){
acoral_printerr("Alloc thread:%s fail\n",name);
acoral_prints("No Mem Space or Beyond the max thread\n");
return -1;
}
/*为tcb成员赋值*/
thread->name = name;
stack_size = stack_size&(~3);
thread->stack_size = stack_size;
thread->stack_buttom = NULL;
policy_ctrl.CPU = CPU;
/*设置线程的优先级*/
policy_ctrl.prio = prio;
policy_ctrl.prio_type=ACORAL_BASE_PRIO;
thread->policy = ACORAL_SCHED_POLICY_COMM;
thread->CPU_mask = ~1;
return comm_policy_thread_init(thread,route,args,&policy_ctrl);
}
线程分配空间函数acoral_alloc_thread()是通过acoral_get_res()为资源控制块acoral_pool_ctrl_t分配空间。
typedef struct{
acoral_u32 type; // 资源类型:线程控制块资源、事件块资源、时间数据块资源、驱动块资源等。
acoral_u32 size; // 资源大小,一般就是结构体大小,如线程控制块的大小,用sizeof(acoral_thread_t)形式赋值。
acoral_u32 num_per_pool;// 每个资源池对象的个数
acoral_u32 num;// 已分配的资源池的个数
acoral_u32 max_pools;// 最多可以分配多少个资源池
acoral_list_t *free_pools,*pools,list[2]; //空闲资源池链表
acoral_res_api_t *api;// 资源操作接口
#ifdef CFG_CMP
acoral_spin_lock_t lock;
#denif
acoral_u8 *name;//该类资源名称。
}acoral_pool_ctrl_t;
资源池管理的资源内存是从第一级内存系统(伙伴系统)分配的,为了最大使用内存,减少内存碎片,对象的个数、最大值、可分配内存等都是通过计算后由用户指定的。
例如,伙伴算法设定基本内存块的大小为1KB,资源的大小为1KB,用户一个资源池包含20个资源,这样计算下来分配20KB的内存空间,但是伙伴系统只能分配2i个基本内存块的大小,故会分配32KB,可包含32个资源对象,大于20个,所以每个资源池对象的个数更改为32。
所以,资源池真正可分配的对象的个数等于用户指定的资源对象的个数
线程初始化
创建线程的最后一步是对创建的普通线程进行初始化comm_policy_thread_init(),不同的调度策略,需要不同的初始化函数。
各调度策略需要什么线程初始化函数是在系统初始化时绑定的。
comm_policy_thread_init()主要是将通用策略控制块中的成员赋值给刚创建的线程的TCB的成员,然后调用acoral_thread_init()进行线程初始化。
acoral_id comm_policy_thread_init(acoral_thread_t *thread, void (*route)(void *args), void *args, void *data){
acoral_sr CPU_sr;
acoral_u32 prio;
acoral_comm_policy_data_t *policy_data;
policy_data = (acoral_comm_policy_data_t *)data;
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 = BASE_PRIO_MAX - 1;
}
thread->prio = prio;
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;
}
堆栈初始化
acoral_thread_init()的主要工作是做堆栈相关的初始化,包括堆栈空间、堆栈内容等。
acoral_err acoral_thread_init(acoral_thread_t *thread, void (*route)(void *args), void (*exit)(void), void *args){
acoral_sr = cpu_sr;
acoral_u32 stack_size = thread->stack_size;
if(thread->stack_buttom==NULL){ //如果堆栈指针为NULL,则说明需要动态分配
if(stack_size < ACORAL_MIN_STACK_SIZE)
stack_size = ACORAL_MIN_STACK_SIZE;
thread->stack_buttom = (acoral_u32 *)acoral_malloc(stack_size);// 分配堆栈,既然是动态分配,就有分配失败的可能
if(thread->stack_buttom == NULL){
return ACORAL_ERR_THREAD_NO_STACK;
}
thread->stack_size = stack_size;
}
thread->stack = (acoral_u32 *)((acoral_8 *)thread->stack_buttom+stack_size-4);// 模拟线程创建时的堆栈环境
HAL_STACK_INIT(&thread->stack, route, exit, args);
// 为线程创建CPU
if(thread->CPU_mask == -1)
thread->CPU_mask = 0xefffffff;
if(thread->CPU < 0){
thread->CPU = acoral_get_idle_maskCPU(thread->CPU_mask);
}
if(thread->CPU >= HAL_MAX_CPU){
thread->CPU = HAL_MAX_CPU - 1;
}
thread->data = NULL;
thread->state = ACORAL_THREAD_STATE_SUSPEND;
/*继承父线程的console_id*/
thread->console_id = acoral_cur_thread->consoled_id;
// 初始化线程的其它成员,如等待队列、就绪队列、延迟队列以及这些队列相关的自旋锁
acoral_init_list(thread->waiting);
acoral_init_list(thread->ready);
acoral_init_list(thread->timeout);
acoral_init_list(thread->global_list);
acoral_spin_init(thread->waiting.lock);
acoral_spin_init(thread->ready.lock);
acoral_spin_init(thread->timeout.lock);
acoral_spin_init(thread->global_list.lock);
acoral_spin_init(move_lock);
HAL_ENTER_CRITICAL();
acoral_list_add2_tail(&thread->global_list,&acoral_thread_queue.head);// 将刚创建的线程挂到全局队列尾部
HAL_EXIT_CRITICAL();
return 0;
}
HAL_STACK_INIT()包括四个参数:堆栈指针变量地址、线程执行函数、线程退出函数、线程参数,无返回值。
从名字可以看出,HAL_STACK_INIT()是与硬件相关的函数,不同的处理器有不同的寄存器[寄存器个数、寄存器功能分配(程序指针、程序当前状态寄存器、连接寄存器、通用寄存器等)],这些寄存器体现了当然线程的运行环境,如果当前线程被其它中断或线程抢占,将发生线程上下文切换。
需要通过堆栈来保存被抢占线程的运行环境,先保存哪个寄存器,再保存哪个寄存器,需要根据处理器的结构来确定,HAL_STACK_INIT()就是用来规定寄存器保存顺序的。
ARM9 S3C2440的线程环境是通过R0~R15以及CPSR来保存的,即当发生上下文切换时,需要保持这16个寄存器的值(除R13(SP)外)。
故在堆栈初始化时,就得压入这么多寄存器来模拟线程的环境。
#define HAL_STACK_INIT(stack,route,exit,args) hal_stack_init(stack,route,exit,args)
// 用一个数据结构表示环境
typedef struct{
acoral_u32 cpsr;
acoral_u32 r0;
acoral_u32 r1;
acoral_u32 r2;
acoral_u32 r3;
acoral_u32 r4;
acoral_u32 r5;
acoral_u32 r6;
acoral_u32 r7;
acoral_u32 r8;
acoral_u32 r9;
acoral_u32 r10;
acoral_u32 r11;
acoral_u32 r12;
acoral_u32 lr;
acoral_u32 pc;
}hal_ctx_t;
由于是用C语言来模拟线程创建时的堆栈环境,所以用宏转换定义hal_stack_init()来实现HAL_STACK_INIT()。
R0~R7为通用寄存器,R8-R12为影子寄存器,R14(LR)为链接寄存器,R15(PC)是程序指针。
hal_stack_init()是线程相关的接口,包括四个参数,堆栈指针变量地址、线程执行函数、线程退出函数、线程参数,无返回值。
void hal_stack_init(acoral_u32 **stk, void (*route)(), void (*exit)(), void *args){
hal_ctx_t *ctx = (hal_ctx_t *)*stk;
ctx--;//获得堆栈模拟环境的基地址,堆栈是向下生长的,hal_ctx_t结构是向上的
ctx = (hal_ctx_t *)((acoral_u32 *)ctx+1);//调整了4个字节,传进来的堆栈指针的内存本身就可以容纳一个数据。
ctx->cpsr = 0x000001fL; //压入处理器状态寄存器
ctx->r0 = (acoral_u32)args; //压入刚创建线程时需要传递的参数
ctx->r1=1;
ctx->r2=2;
ctx->r3=3;
ctx->r4=4;
ctx->r5=5;
ctx->r6=6;
ctx->r7=7;
ctx->r8=8;
ctx->r9=9;
ctx->r10=10;
ctx->r11=11;
ctx->r12=12;
ctx->lr = (acoral_u32)exit;
ctx->pc = (acoral_u32)route; //压入线程函数的入口地址
*stk = ctx;
}
挂载线程到就绪队列
comm_policy_thread_init()是对通用调度策略下的线程初始化,最后一步是恢复线程,即是将新创建的线程挂载到一个就绪队列上。
恢复线程有两个接口:acoral_resume_thread()和acoral_rdy_thread(),前者比后者多一个acoral_sched()调度函数和一个判断;此外,acoral_resume_thread可由用户调度的,而acoral_rdy_thread只能由内核调用;acoral_resume_thread可能立即导致当前线程挂起。
void acoral_resume_thread(acoral_thread_t *thread){
acoral_sr CPU_sr;
acoral_8 CPU;
if(!(thread->state&ACORAL_THREAD_STATE_SUSPEND))// 如果当前线程不处于suspend状态,则不需要唤醒
return;
acoral_enter_critical();
acoral_rdyqueue_add(thread);
acoral_exit_critical();
acoral_sched();// 执行调度函数,对任务重新调度。
}
/*将线程挂到就绪队列上*/
void acoral_rdyqueue_add(acoral_thread_t *thread){
acoral_rdy_queue_t *rdy_queue;
rdy_queue = &acoral_ready_queues;
acoral_prio_queue_add(rdy_queue,thread->prio,&thread->ready);// 将线程挂到优先队列
thread->state &= ~ACORAL_THREAD_STATE_SUSPEND;
thread->state |= ACORAL_THREAD_STATE_READY;
acoral_set_need_sched(true); //线程所在的就绪队列发生了变化,有可能导致任务切换,所以要置为调度标志,让调度函数起作用。
}
void acoral_prio_queue_add(acoral_rdy_queue_t *array, acoral_u8 prio, acoral_list_t *list)
{
acoral_queue_t *queue;
acoral_list_t *head;
array->num++;
queue = array->queue + prio;// 根据线程优先级找到线程所在的优先级链表
head = &queue->head;
acoral_list_add2_tail(list, head);// 将该线程挂到该优先级链表上
acoral_set_bit(prio, array->bitmap); // 置位该优先级就绪标志位
}
调度
调用acoral_sched(),一个普通线程创建的最后一步是调用内核调度函数acoral_sched(),从就绪队列中取出符合调度算法的线程依次执行。
真正意义上的多任务操作系统,都要通过一个调度程序(Scheduler)来实现调度功能,该调度程序以函数形式存在,用来实现操作系统的调度策略,可在内核的各个部分进行调用。
调用调度程序的具体位置又被称为是一个调度点,由于调度程序通常是由外部事件的中断来触发的,或者由周期性的时钟信号触发,因此调度点通常位于以下位置:
- 中断服务程序结束的位置。当用户通过按键向系统提出新的请求,系统首先以中断服务程序ISR响应用户请求,然后在中断服务程序结束时创建新的任务,并将新任务挂载到就绪队列末尾。
接下来,RTOS就会进入一个调度点,调用调度程序,执行相应的调度策略。
当I/O中断发生的时候,如果I/O事件是一个或多个任务正在等待的事件,则在I/O中断结束时刻,也将会进入一个调度点,调用调度程序,调度程序将根据调度策略确定是否继续执行当前处于运行状态的任务,或是让高优先级就绪任务抢占该任务。 - 运行任务因缺乏资源而被阻塞的时刻。例如,使用串口UART传输数据,如果UART正在被其它任务使用,这将导致当前任务从就绪状态转换成等待状态,不能继续执行,此时RTOS会进入一个调度点,调用调度程序。
- 任务周期开始或者结束的时刻。一些嵌入式实时系统往往将任务设计成周期性运行的,如空调控制器、雷达探测系统等,这样,在每个任务的周期开始或者结束时刻,都将进入调度点。
- 高优先级任务就绪的时刻。当高优先级任务处于就绪状态时,如果采用基于优先级的抢占式调度策略,将导致当前任务暂停运行,使更高优先级任务处于运行状态,此时也会进入调度点。
普通线程创建流程
首先为线程分配空间,然后根据创建线程的调度策略对线程TCB进行相关初始化,然后对线程的堆栈进行初始化,最后将创建的线程挂载到就绪队列上,供内核进行调度。