5.07 (野火开源图书读书笔记)
首先要思考的一个问题:
线程的基本结构 :
1 void thread_entry (void *parg) 2 { 3 /* 线程主体,无限循环且不能返回*/ 4 for (;;) 5 { 6 /* 线程主体代码*/ 7 } 8 }
线程是一个无限循环且不返回的函数----------------------------------划重点。
线程是实现目的的任务主体,而操作系统又是为了实现管理任务而生,因而对于线程的理解很重要。
所以在学习rt_thread时需要了解其线程控制块的构成。
基本的线程控制块成员:
struct rt_thread { void *sp; //线程栈指针 void *entry; //线程入口地址 void *parameter; //线程形参 void *stack_addr; //线程起始地址 rt_uint32_t stack_size; //线程栈大小,单位为字节 rt_list_t tlist; //线程链表节点 }; typedef rt_thread *rt_thread_t;
只有线程的栈+线程控制块+线程实体联系在一起,才能完成一次实际的任务调度。所需函数为:rt_thread_init
rt_err_t rt_thread_init(struct rt_thread *thread, void (*entry)(void *parameter), void *parameter, void *stack_start, rt_uint32_t stack_size) { rt_list_init(&(thread->tlist));//链表节点初始化 thread->entry = (void *)entry; thread->parameter = parameter; thread->stack_addr = stack_start; thread->stack_size = stack_size; /* 初始化线程栈,并返回线程栈指针*/ thread->sp = (void *)rt_hw_stack_init( thread->entry, thread->parameter, (void *)((char *)thread->stack_addr+thread->stack_size-4;)); return RT_EOK;// }
在系统中不可能只存在一个线程,所有线程都通过双向数据链表结构串联在一起。
双向链表结构如下:
struct rt_list_node { struct rt_list_node *next; struct rt_list_node *prev; }; typedef struct rt_list_node rt_list_t;

链表在使用之前需要初始化:自己指向自己
rt_inline void rt_list_init(rt_list_t *l) { l->next = l->prev = l; }
相比于裸机系统而言,操作系统细化了程序内部的等级秩序和切换规则,为便于管理运行过程中的变量以及环境参数等信息,每一个线程都需要有自己的栈空间。
线程栈的定义:
1 rt_uint8_t *rt_hw_stack_init(void *tentry, 2 void *parameter, 3 rt_uint8_t *stack_addr) 4 { 5 struct stack_frame *stack_frame; //truct stack_frame 类结构体 7 rt_uint8_t *stk; 8 unsigned long i; 9 /*获取栈顶指针*/ 10 stk = stack_addr + sizeof(rt_uint32_t); 11 /* 让stk 指针向下8 字节对齐*/ 12 stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);//满足浮点运算要求 13 /* stk 指针继续向下移动sizeof(struct stack_frame) 个偏移*/ 14 stk -= sizeof(struct stack_frame); 15 /* 将stk 指针强制转化为stack_frame 类型后存到stack_frame */ 16 stack_frame = (struct stack_frame *)stk; 17 /* 以stack_frame 为起始地址,将栈空间里面的sizeof(struct stack_frame)25 个内存初始化为0xdeadbeef */ 18 for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++) 19 { 20 ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef; 21 } 22 /* 初始化异常发生时自动保存的寄存器*/ 23 stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; 24 stack_frame->exception_stack_frame.r1 = 0; 25 stack_frame->exception_stack_frame.r2 = 0; 26 stack_frame->exception_stack_frame.r3 = 0; 27 stack_frame->exception_stack_frame.r12 = 0; 28 stack_frame->exception_stack_frame.lr = 0; 29 stack_frame->exception_stack_frame.pc = (unsigned long)tentry; 30 stack_frame->exception_stack_frame.psr = 0x01000000L; 31 return stk; 32 }
线程的栈有了,线程控制块有了,线程实体也具备了,一个可以运行的任务模块完成了,用来实现挂载任务模块的数据链表该出现了。
每一个线程按照自己不同的优先级,挂载到不同的数据链表上,如下:

通过上述的就绪链表,将需要实现的线程(任务)相互联系起来,并按照优先级进行了分组,接下来就是如何调度的问题。
调度器的实现:
初始化函数
/* 初始化系统调度器*/ void rt_system_scheduler_init(void) { register rt_base_t offset;// /* 线程就绪列表初始化*/ for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++) { rt_list_init(&rt_thread_priority_table[offset]); } /* 初始化当前线程控制块指针*/ rt_current_thread = RT_NULL;// }
启动调度器:
1 /* 启动系统调度器*/ 2 void rt_system_scheduler_start(void) 3 { 4 register struct rt_thread *to_thread; 5 /* 手动指定第一个运行的线程*// 6 to_thread = rt_list_entry(rt_thread_priority_table[0].next, 7 struct rt_thread, 8 tlist); 9 rt_current_thread = to_thread; 10 /* 切换到第一个线程,该函数在context_rvds.S 中实现,在rthw.h 声明,用于实现第一次任务切换。当一个汇编函数在C 文件中调用的时候, 11 如果有形参,则执行的时候会将形参传人到CPU 寄存器r0。*/ 12 rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp); 13 14 }
调度器在启动的时候会从就绪列表中取出优先级最高的线
程的线程控制块,然后切换到该线程。但是目前我们的线程还不支持优先级,那么就手动
指定第一个运行的线程为就绪列表下标为0 这条链表里面挂着的线程。
rt_list_entry() 是一个已知一个结构体里面的成员的地址,反推出该结构体的首地址的宏。
/* 已知一个结构体里面的成员的地址,反推出该结构体的首地址*/ #define rt_container_of(ptr, type, member) \ ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member))) #define rt_list_entry(node, type, member) \ rt_container_of(node, type, member)
node 表示一个节点的地址,type 表示该节点所在的结构体,的类型,member 表示该节点在该结构体中的成员名称。
将0 地址强制类型类型转换为type,即(type*)0,然后通过指针访问结构体成员的方式获取到偏移的大小,用已知的节点地址减去偏移地址,就是该结构体首地址。
实现线程切换的函数是采用汇编语言编写:
1 全局变量 2 IMPORT rt_thread_switch_interrupt_flag 3 IMPORT rt_interrupt_from_thread 4 IMPORT rt_interrupt_to_thread 5 6 常量 7 有关内核外设寄存器定义可参考官方文档:STM32F10xxx Cortex-M3 programming manual 8 系统控制块外设SCB 地址范围:0xE000ED00-0xE000ED3F 9 10 11 SCB_VTOR EQU 0xE000ED08 ; 向量表偏移寄存器 12 NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制状态寄存器 13 NVIC_SYSPRI2 EQU 0xE000ED20 ; 系统优先级寄存器(2) 14 NVIC_PENDSV_PRI EQU 0x00FF0000 ; PendSV 优先级值(lowest) 15 NVIC_PENDSVSET EQU 0x10000000 ; 触发PendSV exception 的值
//代码产生指令 AREA |.text|, CODE, READONLY, ALIGN=2 THUMB REQUIRE8 PRESERVE8 函数原型:void rt_hw_context_switch_to(rt_uint32 to); r0 --> to 该函数用于开启第一次线程切换 rt_hw_context_switch_to PROC ; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C 文件调用 EXPORT rt_hw_context_switch_to ;设置rt_interrupt_to_thread 的值 ;将rt_interrupt_to_thread 的地址加载到r1 LDR r1, =rt_interrupt_to_thread ; 将r0 的值存储到rt_interrupt_to_thread(ro的值来自函数值传参) STR r0, [r1] ; 设置rt_interrupt_from_thread 的值为0,表示启动第一次线程切换 ; 将rt_interrupt_from_thread 的地址加载到r1 LDR r1, =rt_interrupt_from_thread ; 配置r0 等于0 MOV r0, #0x0 ; 将r0 的值存储到rt_interrupt_from_thread STR r0, [r1] ; 设置中断标志位rt_thread_switch_interrupt_flag 的值为1 ; 将rt_thread_switch_interrupt_flag 的地址加载到r1 LDR r1, =rt_thread_switch_interrupt_flag
; 配置r0 等于1 MOV r0, #1 ; 将r0 的值存储到rt_thread_switch_interrupt_flag STR r0, [r1] ; 设置 PendSV 异常的优先级 LDR r0, =NVIC_SYSPRI2 ;r0中加载中断优先级寄存器2的地址 LDR r1, =NVIC_PENDSV_PRI ;r1中加载设置PendSV中断为低优先级的数据地址 LDR.W r2, [r0,#0x00] ; 读 ;R2中存入中断优先级地址 ORR r1,r1,r2 ; 改 ;r2与r1相与的结果存入r1中 STR r1, [r0] ; 写 ;设置PendSV中断优先级为低优先级 ; 触发 PendSV 异常 (产生上下文切换) LDR r0, =NVIC_INT_CTRL LDR r1, =NVIC_PENDSVSET STR r1, [r0] ;触发中断 ; 开中断 CPSIE F ;开异常 CPSIE I ;开中断 ; 永远不会到达这里 ENDP ALIGN 4 END
以上操作只是触发了PendSV的异常中断:
真正的PendSV的异常中断:在下面
//PendSV_Handler() 函数 //PendSV_Handler() 函数是真正实现线程上下文切换的地方 ; * void PendSV_Handler(void); ; * r0 --> switch from thread stack ; * r1 --> switch to thread stack ; * psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack PendSV_Handler PROC EXPORT PendSV_Handler ; 失能中断,为了保护上下文切换不被中断 MRS r2, PRIMASK CPSID I ; 获取中断标志位,看看是否为0 ; 加载rt_thread_switch_interrupt_flag 的地址到r0 LDR r0, =rt_thread_switch_interrupt_flag ; 加载rt_thread_switch_interrupt_flag 的值到r1 LDR r1, [r0] ; 判断r1 是否为0,为0 则跳转到pendsv_exit CBZ r1, pendsv_exit ; r1 不为0 则清0 MOV r1, #0x00 ; 将r1 的值存储到rt_thread_switch_interrupt_flag,即清0 STR r1, [r0] ; 判断rt_interrupt_from_thread 的值是否为0 ; 加载rt_interrupt_from_thread 的地址到r0 LDR r0, =rt_interrupt_from_thread ; 加载rt_interrupt_from_thread 的值到r1 LDR r1, [r0] ; 判断r1 是否为0,为0 则跳转到switch_to_thread ; 第一次线程切换时rt_interrupt_from_thread 肯定为0,则跳转到switch_to_thread CBZ r1, switch_to_thread ;;;上文保存!!!!!!! ; 当进入PendSVC Handler 时,上一个线程运行的环境即: ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参) ; 这些CPU 寄存器的值会自动保存到线程的栈中,剩下的r4~r11 需要手动保存 ; 获取线程栈指针到r1 MRS r1, psp ; 将CPU 寄存器r4~r11 的值存储到r1 指向的地址(每操作一次地址将递减一次) STMFD r1!, {r4 - r11} ; 加载r0 指向值到r0,即r0=rt_interrupt_from_thread LDR r0, [r0] ; 将r1 的值存储到r0,即更新线程栈sp STR r1, [r0] ;;下文切换␣ !!!!! switch_to_thread ; 加载rt_interrupt_to_thread 的地址到r1 ; rt_interrupt_to_thread 是一个全局变量,里面存的是线程栈指针SP 的指针 LDR r1, =rt_interrupt_to_thread ; 加载rt_interrupt_to_thread 的值到r1,即sp 指针的指针 LDR r1, [r1] ; 加载rt_interrupt_to_thread 的值到r1,即sp LDR r1, [r1] ; 将线程栈指针r1(操作之前先递减) 指向的内容加载到CPU 寄存器r4~r11 LDMFD r1!, {r4 - r11} ; 将线程栈指针更新到PSP MSR psp, r1 pendsv_exit ; 恢复中断 MSR PRIMASK, r2 ; 确保异常返回使用的栈指针是PSP,即LR 寄存器的位2 要为1 ORR lr, lr, #0x04 ; 异常返回,这个时候栈中的剩下内容将会自动加载到CPU 寄存器: ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参) ; 同时PSP 的值也将更新,即指向线程栈的栈顶 BX lr ; PendSV_Handler 子程序结束 ENDP
调度的本质是:在就绪列表中寻找优先级别最高的线程,然后执行该线程,
简单起见先实现两个任务的轮转。
rt_schedule() 函数:
1 void rt_schedule(void) 2 { 3 struct rt_thread *to_thread; 4 struct rt_thread *from_thread; 5 /* 两个线程轮流切换*/// 6 if( rt_current_thread ==rt_list_entry(rt_thread_priority_table[0].next,struct rt_thread,tlist)) 7 { 8 from_thread = rt_current_thread; 9 to_thread = rt_list_entry( rt_thread_priority_table[1].next, 10 struct rt_thread, 11 tlist); 12 rt_current_thread = to_thread; 13 14 } 15 else// 16 { 17 from_thread = rt_current_thread; 18 to_thread = rt_list_entry( rt_thread_priority_table[0].next, 19 struct rt_thread, 20 tlist); 21 rt_current_thread = to_thread; 22 } 23 /* 产生上下文切换*/ 24 rt_hw_context_switch((rt_uint32_t)&from_thread->sp,(rt_uint32_t)&to_ 25 ,→thread->sp); 26 }
rt_hw_contex_switch() 函数
/* ; *----------------------------------------------------------------------3; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); ; * r0 --> from ; * r1 --> to ; *---------------------------------------------------------------------- ; */ rt_hw_context_switch PROC EXPORT rt_hw_context_switch ; 设置中断标志位rt_thread_switch_interrupt_flag 为1 ; 加载rt_thread_switch_interrupt_flag 的地址到r2 LDR r2, =rt_thread_switch_interrupt_flag ; 加载rt_thread_switch_interrupt_flag 的值到r3 LDR r3, [r2] ;r3 与1 比较,相等则执行BEQ 指令,否则不执行 CMP r3, #1 BEQ _reswitch ; 设置r3 的值为1 ; 将r3 的值存储到rt_thread_switch_interrupt_flag,即置1 STR r3, [r2] ; 设置rt_interrupt_from_thread 的值 ; 加载rt_interrupt_from_thread 的地址到r2 LDR r2, =rt_interrupt_from_thread ; 存储r0 的值到rt_interrupt_from_thread,即上一个线程栈指针sp 的指针 STR r0, [r2] _reswitch ; 设置rt_interrupt_to_thread 的 32 ; 加载rt_interrupt_from_thread 的地址到r2 LDR r2, =rt_interrupt_to_thread ; 存储r1 的值到rt_interrupt_from_thread,即下一个线程栈指针sp 的指针 STR r1, [r2] ; 触发PendSV 异常,实现上下文切 LDR r0, =NVIC_INT_CTRL LDR r1, =NVIC_PENDSVSET STR r1, [r0] ; 子程序返回 BX LR ; 子程序结束 ENDP

浙公网安备 33010602011771号