近些年来,随着嵌入式系统飞速的发展,嵌入式实时操作系统广泛地应用在制造工业、过程控制、通讯、仪器仪表、汽车、船舶、航空航天、军事、装备、消费类产品等方面。今天嵌入式系统带来的工业年产值超过了1万亿美元。当前嵌入式操作系统有数百种,比较流行的有Nucleus Plus,QNX,Lynx OS,Psos,VRTX,VXWorks,Windows CE等。它们各有特色,用户可以根据自身的软硬件环境的要求,选择合适的操作系统。既然Nucleus已经成为嵌入式应用的潮流和方向,剖析Nucleus 中线程控制的实现方法就具有重要的意义。
1 Nucleus内核
Nucleus Plus是一个为实时嵌入式应用而设计的抢占式多任务的操作系统内核。Nucleus Plus大约有95%的代码是用ANSI C编写的。正因如此,Nucleus Plus特别便于移植,并且在大多数的多处理器系统中都可使用。从实现角度来看,Nucleus Plus是一组C,将应用程序与核心函数库连接在一起,生成一个目标代码,下载到目标板的RAM中或直接烧录到目标板的ROM中执行。如果所有的服务都需要,在典型的目标环境中,Nucleus Plus的核心代码大约有20KB。
Nucleus Plus采用了软件组件(Component)的方法。每个组件具有单一明确的目的,通常由几个C或汇编模块构成,提供清晰的外部接口,对组件的引用就是通过这些接口完成的。除了少数一些特殊情况外,不允许从外部对组件内的变量进行访问。由于采用了软件组件的方法,Nucleus Plus各个组件非常易于替换和复用。Nucleus Plus的组件包括任务控制、中断管理、定时器管理及I/O驱动管理等。
Nucleus Plus支持大部分流行的CPU,如X86,68xxx,68HCxx,NEC V25,ColdFire,SPARClite,PowerPC等。针对不同的CPU类型,Nucleus还提供编译器、动态连接器、多任务调试器等相应的工具来配置用户的开发环境,方便软件的开发和调试。
Nucleus Plus内核(Kerne1)的主要目的是管理实时任务的竞争运行(共享CPU),为应用提供各种便利,快速响应外部事件,实现实时性。Nucleus Plus的系统结构如图1所示。
从Nucleus的系统结构框图可以看出线程控制是整个内核的核心,通过邮箱、队列、管道来实现任务之间的通信,通过信号量、事件组和信号实现任务间的同步。Nucleus Plus 提供动态和分区内存(Dynamic/Partition Memory)两种存储器管理机制。Nucleus Plus还提供定时器(Timer)来处理周期性事件和任务的睡眠和挂起超时。Nucleus Plus 将这些机制称之为软件组件(Software Component)。Nucleus Plus为每一个软件组件提供了一系列的系统调用,任务与Nucleus Plus的交互是在系统调用的界面上进行的。
2 线程控制
线程控制部件用来管理实时任务和高级中断服务的执行,它是Nucleus 嵌入式实时操作系统最核心的部分。为了控制执行过程,任务通常被分配一个优先级。任务优先级的范围从0到255,优先级0的优先权最高。除非抢占位被置为无效,否则低优先级的任务将被高优先级就绪的任务抢占。为保证任务执行的实时性,Nucleus设计了高级中断服务,它的优先级范围从0到2,其中优先级0的级别最高。
2.1 任务的状态表示
一个Nucleus任务是为完成特定目标而编写的半独立的程序段。许多应用有多个任务。任务状态有五个:执行、就绪、挂起、终止、完成,如表1所示。
表1 Nucleus任务的五种状态
状态
意义
Executing
任务当前正在执行。
Ready
任务就绪,但是另一个任务当前正在运行。
Suspended
任务因为等待服务需求而体眠。当需求满足时,任务变为就绪状态。
Terminated
任务被终止。当任务处于这种状态时,它将不再执行直到它被复位。
Finished
任务完成了它的处理.从进入函数中退出来。任务处于这种状态时,它将不再执行直到受到复位。
2.2 任务调度算法
Nucleus调度程序用来决定是否要进行任务切换,如果需要切换的话,切换到哪个进程等。首先说明Nucleus的调度时机:
(1)进程状态转换的时刻,即进程终止、进程睡眠;
(2)可运行队列中新增加一个进程时;
(3)当前进程的时间片用完;
(4)进程从系统调用返回到用户态;
(5)内核处理完中断后,进程返回到用户态。
Nucleus实时操作系统具有可抢占性,进程调度的依据就是按照优先级从高到低顺序进行调度。当低优先级任务在运行时,有高优先级任务就绪准备执行,则将发生高优先级任务抢占低优先级任务执行。
Nucleus实时操作系统的任务调度算法非常简单,它包括时间片轮转算法和轮询算法。时间片轮转算法是将相同优先级的任务都分配相同的时间片,当时间片用完后再转到下一个任务,轮流执行直到这个优先级的所有任务全部执行完毕,然后再转到下一个优先级。轮询算法是在本优先级内的所有任务按照就绪时间的先后顺序执行,当这个优先级的全部任务都执行完毕后,再执行下一个优先级的任务。
2.3 任务的优先级
任务的优先级从0到255,0的优先级别最高,255的优先级别最低。256个优先级分成32组,每组对应8个级别。例如第0组对应0~7,第一组对应8~15。由32位整型变量Tcd_priority_groups表示就绪任务属于哪个优先级组。用子优先级数组Tcd_sub_priority_groups表示组内具体的优先级。例如Tcd_sub_priority_groups[0]表示第0组的子优先级,即它的第0位为1,表明优先级为0的任务就绪;若它的第7位为1,表明优先级为7的任务就绪。Nucleus中定义:
公式1 若任务的优先级为Tc_priority,则该任务的子优先级掩码为:
tc_sub_priority=1<<(tc_priority&7)
公式2 若任务的优先级为Tc_priority,则该任务的优先级组掩码为:
tc_priority_group=1<<((tc_priority)>>3)
TC_TCB *TCD_Priority_List是任务控制块指针数组。数组中的每个元素都指向那个优先级就绪任务列表头。如果指针为空,那个优先级没有就绪任务。这个数组被优先级索引。对于每个优先级,其中的任务控制块是以双向列表的形式链接于这个变量的,如图2所示。
Nucleus在进行任务切换时需要计算最高优先级。为此系统引入了一个大小为256的常数数组Unsigned char tcd_lowest_set_bit[n],其表示下标n的二进制数的不为0的最低位的位号。所以,系统中最高优先级的组号=8×不为零的最低字节的字号+Tcd_lowest_set_bit[不为零的最低字节],系统中的最高优先级=(系统中最高优先级的组号<<3)+Tcd_lowest_set_bit[与这组号对应的子优先级]。计算最高优先级的程序如下:
if(tcd_priority_groups!=0)|if(tcd_priority_groups&0x000000f)
index=0;/*第0字节的基本组号*/
if(tcd_priority_groups&0x0000f00)
index=8;/* 第1字节的基本组号*/
if(tcd_priority_groups&0x00f0000)
index=16;/*第2字节的基本组号*/
else
index=24;/* 第3字节的基本组号*/
/*计算最高优先级的组号*/
index=index + tcd_lowest_set_bit[(int)((tcd_priority_group>>index)&0x000000ff)];
temp = tcd_sub_priority_groups[index];
/*计算最高优先级*/
tcd_highest_priority=(index << 3) + tcd_lowest_set_bit[temp];
}
此时,Tcd_priority_list[tcd_highest_priority]所指向的任务就是系统中最高优先级任务。
2.4 中断
中断是对外部和内部事件提供立即响应服务的机制,也是操作系统实时性的重要体现。Nucleus将中断服务分为低级中断服务(LISR)和高级中断服务(HISR)。在LISR中执行少量的必要的工作,以便尽快地响应同级或低级的中断。大部分工作在HISR中完成。HISR类似于任务,有自己的堆栈,但它的优先级比一切任务的优先级高(即系统总是首先调度HISR运行)。HISR的优先级分为0到2,其中0的优先级别最高,2的优先级别最低。HISR的状态只有两种:激活状态和静止状态。LISR按需要激活相应的HISR,然后HISR进入对应的HISR优先级就绪队列,等待CPU调度。相同优先级的HISR链接成单链表,用数组*tc_hcb tcd_active_hisr_heads[tc_hisr_priorities]存放这些链表的头地址。这样在寻找最高优先级的HISR时,只需简单查找数组中第一个非零元素所指向的链表的第一个结点HISR。
LISR在执行时需保存现场,以矢量号为变元调用Tcc_dispatch_lisr()执行对应的低层中断例程,再跳转到现场恢复例程。LISR无控制块且没有自己的堆栈,运行时占用被中断的任务、HISR的堆栈或系统堆栈。LISR可用汇编语言或c语言编程,若采用c语言则调用Tcc_register_lisr()安装到某号中断矢量上。
2.5 互斥保护量
应用程序和操作系统的c语言例程用互斥保护量来保护临界区。在Nucleus中保护量定义为一个结构:
typedef struct tc_protect_struct
{
tc_tcb tc_tcb_pointer; /*占有这个保护量的线程*/
unsigned tc_thread_waiting; /*等待占有保护量的线程*/
}tc_protect;
应用程序和系统组件用例程Tct_protect()来请求保护量。该例程首先判断保护量的Tc_tcb_pointer是否为空。如果为空,则表示该保护量可被线程占有,把调用者的任务控制块(Tcb)地址存入Tc_Tcb_pointer,同时将零送入Tc_thread_waiting以示无其它线程在等待占有这个保护量。最后将保护量的地址存入调用者的Tc_current_protect中,表示该任务可占用这个保护量。若tc_tcb_pointer非空,则表示该保护量已被占有,将1存入Tc_thread_waiting中,以表示有线程在等待。为了防止优先级逆转,Nucleus调用例程Tct_schedule_protected()直接调度占有该保护量的线程运行。
当任务占有临界区完毕后,系统调用Tct_unprotect()归还调用者的当前保护量。例程首先判断保护量的Tc_thread_waiting是否为0。如果为0,则表明无任务在等待这个保护量,Nucleus将0存入Tc_tcb_pointer和Tc_current_protect。如果保护量不为0,则有较高的优先级等待这个保护量。系统调用Tct_control_to_system()将控制权归还给系统,选择系统中最高优先级的就绪任务运行,同时清变量Tc_tcb_pointer和Tc_current_protect。
3 结束语
Nucleus是一种实时 多任务的嵌入式操作系统。它可以管理各种系统资源,调度任务的运行。Nucleus使嵌入式多任务开发省时、省力,并且提高效率。它已成为嵌入式应用的潮流和方向。本文深入剖析了Nucleus 的线程控制模块的实现机理,对于嵌入式实时操作系统的开发具有重要的意义。