前言:
最近一直都在看nucleus plus,之前看过一些linux内核的一些东西,但都是停留在文字上,代码看的很少,这个nucleus plus内核的代码量不大,看过source code确实对很多OS的知识有了更深入的认识,收获还是挺多的,把学到的东西记录下来。
内容:
一、nucleus plus特点:
1.内核采用微内核的设计,方便移植,资料写着更reliable,但是我不这么认为,与linux相比,以ARM平台为例,NU只用到了SVC mode,内核与用户任务都运行在同一个状态下,也就是说所有的task都拥有访问任何资源的权限,这样很reliable么?
2.real-time OS,NU是一个软实时操作系统(VxWorks是硬实时),thread control component支持多任务以及任务的抢占,对于中断的处理定义了两种服务方式,LISR和HISR,这个与linux中的上、下半部机制类似,linux中的下半部是通过软中断来实现的,NU的HISR只是作为一种优先级总是高于task的任务出现。
3.NU是以library的方式应用的,通过写自己的app task与裁剪后的NU内核及组件链接起来,NU并没有CLI
二、组件
1.IN component
初始化组件由三个部分组成,硬件在reset后首先进入INT_initialize(),进行板级的相关初始化,首先设置SVC mode,关中断,然后将内核从rom中拷贝至ram中,建立bss段,依次建立sys stack, irq stack和fiq stack,最后初始化timer,建立timer HISR的栈空间,看了一下2410平台的代码,一个tick大概是15.8ms,完成板级的初始化后就进入了INC_initialize,初始化各个组件,其中包括Application initialize,create task和HISR,最后将控制权交给schedule,主要看了一下RAM中地址空间的安排
|timer HISR stack = 1024|
|FIQ stack = 512|
|IRQ stack = 1024|
|SVC stack = 1024|
|.bss|
|.data|
|.text|
其中SVC stack的大小与中断源的个数相关,nested irq发生时,irq_context保存在SVC stack中,IRQ的stack只是做了临时栈的作用。
2.thread control component
TC组件是NU内核的最重要组成部分,主要涵盖了调度、中断、任务的相关操作、锁、时钟几个方面,下面分别介绍。
调度(schedule)
NU中的线程类型(在同一个地址空间内)有两种,HISR和task,HISR可以理解为一种优先级较高的task,但又不是task,HISR优先级高于task的实现方式就是schdule时,先去查看当前是否有active的HISR,再去查看task。task有suspend、ready、finished和terminated四种状态,而HISR只有executing和no-active这两种状态。
每一个task都有一个线程控制的数据结构(TCB thread control block),其中包括了task的优先级、状态、时间片、task栈、protect信息、signal操作的标志位和signal_handler等,task在创建时初始化这些信息,将task挂到一个create_list上,初始设定task为pure_suspend,如果设定auto start,调用resume_task()唤醒task,这里有个细节,如果在application initialize中create_task(),则task不会自动运行,因为初始化还未完成,控制权还没有交给schedule,无法调度task。task被唤醒后状态改变为ready,并挂在一个TCD_Priority_List[256]上,数组的每个元素是一个指向TCB环形双向链表的指针,根据task的tc_priority找到对应优先级的TCB head pointer。
每一个HISR都有一个HISR控制的数据结构(HCB HISR control block),其中只有优先级,HISR栈和HISR entry信息,因此HISR是不可以suspend,同时也没有time slice以及signal的相关操作,一般情况下当发生了中断后,HISR被activate,schedule就会调度HISR运行,期间如果不发生中断,HISR的执行是不会被打断的,HISR的优先级只有0、1、2,timer的HISR优先级为2,也就是说由外部中断激活的HISR很难被抢占的,只有更高优先级的中断HISR才可以。与task不同,被激活的HISR使用head_list和tail_list将HCB挂在一个单项的链表上,因为相同优先级的HISR不会抢占对方,因此不需要双向链表,使用两个指针目的是加快HISR执行的速度。
一个实时操作系统的核心就是对于任务的调度,NU的调度策略是time slice和round robin的算法,
调度的部分主要有三个函数control_to_system()用于保存上下文,建立solicited stack,关中断,关system time slice,并重置task的time slice为预设值,将sp更新为system_stack_pointer,调用schedule(),调度的过程是非常简单的查询,就是查看两个全局的变量,TCD_Execute_HISR和TCD_Execute_Task,schedule部分的关键是打开了中断,不然如果当前没有ready的task或是被激活的HISR,则shedule死循环下去,查询到下一个应该执行的线程后跳转至control_to_thread(),在这里重新开启system time slice,然后将线程的tc_stack_ptr加入到sp中,切换至线程的栈中,依次pop出来,即完成了任务调度。
任务的切换主要是上下文的切换,也就是task栈的切换,函数的调用会保存部分regs和返回地址,这些动作都是编译器来完成的,而OS中的任务切换是运行时(runtime)的一种状态变化,因此编译器也无能为力,所以对于上下文的保存需要代码来实现。
任务的抢占是异步的因此必须要通过中断来实现,一般每次timer的中断决定当前的task的slice time是否expired,然后设置TCT_Set_Execute_Task为相同优先级的其他task或更高优先级的task;高优先级的task抢占低优先级的task,一般是外部中断触发,在HISR中resume_task()唤醒高优先级的task,然后schedule到高优先级的task中,因为timer的HISR是在系统初始化就已经注册的,只是执行timeout和time slice超时后的操作,并没有执行resume_task的动作。
NU中的stack有两种solicited stack和interrupt stack,solicited stack是一种minmum stack,而interrupt stack是对当前所有寄存器全部保存,TCB中的minimum stack size = 申请得到stack size - solicited stack(在arm mode下占44字节,thumb mode下占48字节),thumb标志用来记录上下文保存时的ARM的工作模式,c代码编译为thumb模式,这样可以减小code size,提高代码密度,assembly代码编译为arm模式提升代码的效率,NU中内核的代码不多,主要是assembly代码。stack的类型与其中PC指向的shell无关,interrupt stack保存的是task或是HISR在执行的过程中被中断时的现场,solicited stack建立的地方包括 control_to_system()、schedule_protect()和send_signals()发送给占有protect资源的task的情况,HISR_Shell()执行完后会建立solicited stack,再跳转至schedule。
- (Lower Address) Stack Top -> 1 (Interrupt stack type)
- CPSR Saved CPSR
- r0 Saved r0
- r1 Saved r1
- r2 Saved r2
- r3 Saved r3
- r4 Saved r4
- r5 Saved r5
- r6 Saved r6
- r7 Saved r7
- r8 Saved r8
- r9 Saved r9
- r10 Saved r10
- r11 Saved r11
- r12 Saved r12
- sp Saved sp
- lr Saved lr
- (Higher Address) Stack Bottom-> pc Saved pc
- (Lower Address) Stack Top -> 0 (Solicited stack type)
- !!FOR THUMB ONLY!! 0/0x20 Saved state mask
- r4 Saved r4
- r5 Saved r5
- r6 Saved r6
- r7 Saved r7
- r8 Saved r8
- r9 Saved r9
- r10 Saved r10
- r11 Saved r11
- r12 Saved r12
- (Higher Address) Stack Bottom-> pc Saved pc
一个简单的例子说明stack的情况,首先是一个task在ready(executing)的状态下,而且time slice超时了,timer中断发生后,保存task上下文interrupt_contex_save(),在task的tc_stack_ptr指向的地方建立中断栈
taskA |interrupt stack|___tc_stack_ptr 栈顶端是pc=lr-4
ARM对于中断的判定发生在当前指令完成execute时,同时pipeline的原因pc=pc+8,入栈时就把lr-4首先放在stack的最高端(high)。
timer的LISR完成后激活了HISR,执行TCC_Time_slice()将当前task移到相同优先级的尾端,并且设置下一个要执行的task,HISR在栈顶端保存的是这个HISR_shell的入口地址,因为task的执行完就finished,HISR是可重入的
HISR |solicited stack| 栈顶端是HISR_shell_entry
中断(interrupt)
前面已经提及了中断的基本操作,这里就写一些代码路径的细节,中断的执行主要是两个部分LISR和HISR,分成两个部分的目的就是将关中断的时间最小化,并且在LISR中开中断允许中断的嵌套,以及建立中断优先级,都可以减少中断的延迟,保证OS的实时性。
NU的中断模式是可重入的中断处理方式,也就是基于中断优先级和嵌套的模式,中断的嵌套在处理的过程中应对lr_irq_mode寄存器进行保存,因为高优先级的中断发生时会覆盖掉低优先级中断的r14和spsr,因此要利用系统的栈来保存中断栈。
NU对于中断上下文的保存具体操作如下:
(1)在中断发生后执行的入口函数INT_IRQ()中,将r0-r4保存至irq的栈中
(2)查找到对应的interrupt_shell(),clear中断源,更新全局的中断计数器,然后进行interrupt_contex_save
(3)首先利用r1,r2,r3保存irq模式下的sp,lr,spsr,这里sp是用来切换至系统栈后拷贝lr和spsr的,这里保存lr和spsr是目的是task被抢占后,当再次schedule时可以返回task之前的状态。
(4)切换至SVC模式,如果是非嵌套的中断则保存上下文至task stack中,将irq模式下的lr作为顶端PC的返回值入栈,将SVC模式下的r6-r14入栈,将irq模式下的sp保存至r4中入栈,最后将保存在irq_stack中的r0-r4入栈
(5)如果是嵌套中断,中断的嵌套发生在LISR中,在执行LISR时已经切换至system stack,因此嵌套中断要将中断的上下文保存至system stack中,与task stack中interrupt stack相比只是少了栈顶用来标记嵌套的标志(1 not nested)
(6)有一个分支判断,就是如果当前线程是空,即TCD_Current_Thread == NULL,表明当前是schedule中,因为初始化线程是关中断的,这样就不为schedule线程建立栈帧,因为schedule不需要保存上下文,在restore中断上下文时直接跳转至schedule。
中断上下文的恢复
全局的中断计数器INT_Count是否为0来判定当前出栈的信息,如果是嵌套则返回LISR中,否则切换至system stack执行schedule
timer
timer与中断紧密相关,其实timer也是中断的一种,只是发生中断的频率较高,且作用重大,一个实时操作系统,时间是非常重要的一部分,NU中的timer主要有四个作用:
(1)维护系统时钟 TMD_system_clock
(2)task的time slice
(3)task的suspend timeout timer
(4)application timer
其中(3)(4)共用一种机制,一个全局的时间轴TMD_timer,timeout timer和app timer都建立在一个TM_TCB的数据结构上,通过tm_remaining_time来表征当前timer的剩余时间,例如当前有timer_list上有三个TM_TCB,依次是Ta = 5,Tb = 7, Tc = 20,那么建立的链表上剩余时间依次是5,2,8,如果现在要加入一个新的timer根据timer值插入至合适的位置,如果插入的timer为13,则安排在Tb后面,剩余时间为1,后面的8改为7,当发生了timer expired,则触发timer_HISR,如果是app timer则执行timer callback,如果是task timeout timer,则执行TCC_Task_Timeout唤醒task。
(2)的实现也是依赖于全局的time slice时间轴,每一个task在执行时都会将自己的时间片信息更新至全局的时间轴上,当一个task的time slice执行完在timer HISR中调用TCC_task_Timeout将当前的task放在相同优先级list的最尾端,并设置下一个最高优先级的任务。task在执行的过程中只有被中断后time slice会保存下来,其他让出处理器的情况都会将time slice更新为预设值。
protect
protect与linux的锁机制类似,互斥访问,利用开关中断来实现,并且拥有protect的task是不可以suspend的,必须要将protect释放后才可以挂起,当一个优先级较低的task占有protect资源,如果被抢占,一个高优先级的task或HISR在请求protect资源时会执行TCC_schedule_protect()让出处理器给低优先级的task执行,直到低优先级的task执行unprotect()为止,此时task或HISR建立的是solicited stack,同时在control_to_thread前开关中断一次,这样可以减少一次上下文的切换。NU中常用到的是system_protect,它就是一把大锁,保护内核中所有全局数据结构的顺序访问,粒度很大。
LISR中不可以请求protect资源,因为LISR是中断task后执行,如果task占有protect资源,这时LISR又去请求protect资源,会发生死锁,因为LISR让出处理器后,schedule没办法再次调度到LISR执行,则发生死锁错误,因此在LISR中除了activate_HISR()以外不可以使用system call,例如resume_task等等,这写系统调用都会请求protect资源。
对于protect的请求按照一定的顺序可以防止死锁,NU的源码中一般将system_protect资源的请求放在后面,其他如DM_protect先请求。