本篇文章分析VxWorks的初始化,VxWorks的初始化可以分成两个部分:
1.具体处理器平台相关的硬件初始化:包括CPU内部寄存器、堆栈寄存器的初始化,外设初始化;
2.VxWorks内核初始化:包括核心数据结构的初始化、初始任务的创建,启动多任务等等。
我以Pentium平台为例,来分析VxWorks的初始化过程。
6.1 处理器平台相关的初始化
这部分代码初始化CPU内部寄存器,是VxWorks在内存中的入口代码。其主要工作是关中断,初始化CPU内部寄存器,特别是栈寄存器,分配栈空间。为运行第一个C函数usrInit()建立环境。
具体代码如下:
sysInit:
_sysInit:
cli /* 关中断 */
movl $ BOOT_WARM_AUTOBOOT,%ebx /*设置启动类型 */
movl $ FUNC(sysInit),%esp /* 初始化栈寄存器 */
movl $0,%ebp /* 初始化栈幁寄存器*/
ARCH_REGS_INIT /*初始化DR[0-7] ,CR0, EFLAGS寄存器 */
#if (CPU == PENTIUM) || (CPU == PENTIUM2) || (CPU == PENTIUM3) || \
(CPU == PENTIUM4)
/* ARCH_CR4_INIT /@ initialize CR4 for P5,6,7 */
xorl %eax, %eax /* 清EAX寄存器 */
movl %eax, %cr4 /* 清CR4寄存器 */
#endif /* (CPU == PENTIUM) || (CPU == PENTIUM[234]) */
/*将全局描述符表拷贝到pSysGdt指向的内存空间处*/
movl $ FUNC(sysGdt),%esi /* set src addr (&sysGdt) */
movl FUNC(pSysGdt),%edi /* set dst addr (pSysGdt) */
movl %edi,%eax
movl $ GDT_ENTRIES,%ecx /* number of GDT entries */
movl %ecx,%edx
shll $1,%ecx /* set (nLongs of GDT) to copy */
cld
rep
movsl /* copy GDT from src to dst */
/*构造初始化gdtr寄存器的值*/
pushl %eax /* push the (GDT base addr) */
shll $3,%edx /* get (nBytes of GDT) */
decl %edx /* get (nBytes of GDT) - 1 */
shll $16,%edx /* move it to the upper 16 */
pushl %edx /* push the nBytes of GDT - 1 */
leal 2(%esp),%eax /* get the addr of (size:addr) */
pushl %eax /* push it as a parameter */
call FUNC(sysLoadGdt) /* load the brand new GDT in RAM */
/*构造一个中断返回的情景*/
pushl %ebx /* push the startType */
movl $ FUNC(usrInit),%eax
movl $ FUNC(sysInit),%edx /* push return address */
pushl %edx /* for emulation for call */
pushl $0 /* push EFLAGS, 0 */
pushl $0x0008 /* a selector 0x08 is 2nd one */
pushl %eax /* push EIP, FUNC(usrInit) */
iret /* iret */
代码分析:
1. sysInit()初始化过程比较直观,但是由于这是一段汇编语句,需要考虑到汇编语言和C语言编程的一些细节。
BOOT_WARM_AUTOBOOT是一个宏,其值为0,将一个宏的值放入一个寄存器中时,采用的语句是:
movl $ BOOT_WARM_AUTOBOOT,%ebx
sysInit()是一个函数名字,其所在的地址为sysInit()的入口地址0x30800c:
0030800c <_sysInit>:
30800c: fa cli
30800d: bb 00 00 00 00 mov $0x0,%ebx
308012: bc 0c 80 30 00 mov $0x30800c,%esp
308017: bd 00 00 00 00 mov $0x0,%ebp
30801c: 31 c0 xor %eax,%eax
30801e: 0f 23 f8 mov %eax,%db7
308021: 0f 23 f0 mov %eax,%db6
<……………….略…………………>
所以
movl $ FUNC(sysInit),%esp就是将sysInit所在的地址0x30800放入到寄存器ESP中。
由于:
#define FUNC(sym) sym
#define FUNC_LABEL(sym) sym:
movl $ FUNC(sysInit),%esp和movl $ sysInit,%esp是一致的。
由于sysInit是VxWorks的入口地址,把地址赋值给ESP,意味着将sysInit地址往下的地方作为临时栈空间。
2. ARCH_REGS_INIT宏分析
ARCH_REGS_INIT宏展开如下:
#define ARCH_REGS_INIT \
xorl %eax, %eax; /* zero EAX */ \
movl %eax, %dr7; /* initialize DR7 */ \
movl %eax, %dr6; /* initialize DR6 */ \
movl %eax, %dr3; /* initialize DR3 */ \
movl %eax, %dr2; /* initialize DR2 */ \
movl %eax, %dr1; /* initialize DR1 */ \
movl %eax, %dr0; /* initialize DR0 */ \
movl %cr0, %edx; /* get CR0 */ \
andl $0x7ffafff1, %edx; /* clear PG, AM, WP, TS, EM, MP */ \
movl %edx, %cr0; /* set CR0 */ \
\
pushl %eax; /* initialize EFLAGS */ \
popfl;
其用于初始化Pentium平台的调试寄存器,控制寄存器CRO,以及EFLAGS寄存器。
从控制寄存器CRO只保留的PE位,我们可以看出目前Pentium只启用了保护模式。
关键CR0寄存器更详细的解释参考Intel官方编程手册。
3.将全局描述符表拷贝到pSysGdt指定的位置处
全局描述符表sysGdt[]定义如下:
FUNC_LABEL(sysGdt)
/* 0(selector=0x0000): Null descriptor */
.word 0x0000
.word 0x0000
.byte 0x00
.byte 0x00
.byte 0x00
.byte 0x00
/* 1(selector=0x0008): Code descriptor, for the supervisor mode task */
.word 0xffff /* limit: xffff */
.word 0x0000 /* base : xxxx0000 */
.byte 0x00 /* base : xx00xxxx */
.byte 0x9a /* Code e/r, Present, DPL0 */
.byte 0xcf /* limit: fxxxx, Page Gra, 32bit */
.byte 0x00 /* base : 00xxxxxx */
/* 2(selector=0x0010): Data descriptor */
.word 0xffff /* limit: xffff */
.word 0x0000 /* base : xxxx0000 */
.byte 0x00 /* base : xx00xxxx */
.byte 0x92 /* Data r/w, Present, DPL0 */
.byte 0xcf /* limit: fxxxx, Page Gra, 32bit */
.byte 0x00 /* base : 00xxxxxx */
/* 3(selector=0x0018): Code descriptor, for the exception */
.word 0xffff /* limit: xffff */
.word 0x0000 /* base : xxxx0000 */
.byte 0x00 /* base : xx00xxxx */
.byte 0x9a /* Code e/r, Present, DPL0 */
.byte 0xcf /* limit: fxxxx, Page Gra, 32bit */
.byte 0x00 /* base : 00xxxxxx */
/* 4(selector=0x0020): Code descriptor, for the interrupt */
.word 0xffff /* limit: xffff */
.word 0x0000 /* base : xxxx0000 */
.byte 0x00 /* base : xx00xxxx */
.byte 0x9a /* Code e/r, Present, DPL0 */
.byte 0xcf /* limit: fxxxx, Page Gra, 32bit */
.byte 0x00 /* base : 00xxxxxx */
代码中:
movl $ FUNC(sysGdt),%esi是将sysGdt[]数组的首地址(即全局描述符表sysGdt[]所在内存块的基地址)放入到寄存器esi中,比如sysGdt[]数组所在的地址是0x30380,该条指令将0x30380放入esi寄存器中。
movl FUNC(pSysGdt),%edi将pSysGdt的值放入到寄存器edi中,这里需要注意的是pSysGdt是一个指针变量,在sysLib.c中定义如下:
GDT *pSysGdt = (GDT *)(LOCAL_MEM_LOCAL_ADRS + GDT_BASE_OFFSET);
其中
#define LOCAL_MEM_LOCAL_ADRS (0x00100000)
#define GDT_BASE_OFFSET 0x1000
所有指针变量pSysGdt的值为0x101000,加载pSysGdt所在的地址为0x339980:
00339980
339980: 00 10 add %dl,(%eax)
339982: 10 00 adc %al,(%eax)
那么movl FUNC(pSysGdt),%edi指令值得效果是将0x101000的值放入edi寄存器中,如果误写成$movl FUNC(pSysGdt),%edi,将导致将0x339980写入edi寄存器中,从而引发错误。
4.通过构造中断栈幁实现跳转
sysInit()函数的最后,通过中断返回指令iret,实现跳转到第一个C函数usrInit()中,跳转之前sysInit()已经初始化了CPU的栈寄存器ESP为sysInit的入口地址,这意味着将sysInit入口地址向下的地址空间作为usrInit()函数的临时站空间。
要想成功跳转到iret函数中,必须构造中断栈幁:
pushl %ebx /* push the startType */
movl $ FUNC(usrInit),%eax
movl $ FUNC(sysInit),%edx /* push return address */
pushl %edx /* for emulation for call */
pushl $0 /* push EFLAGS, 0 */
pushl $0x0008 /* a selector 0x08 is 2nd one */
pushl %eax /* push EIP, FUNC(usrInit) */
构造的伪中断栈幁如图6.1所示。
图6.1 临时中断栈帧
当执行完iret指令后,将跳转到usrInit()函数中运行。
6.2 第一个C函数usrInit()执行
usrInit()是VxWorks启动之后执行的第一个C函数,由于在跳转到usrInit()函数之前,sysInit()已经进行了关中断操作,因此该函数是在关中断条件下,使用sysInit建立的临时栈空间执行相关硬件的初始化。
其主要完成的工作如下:
- 清BSS段,将vxWorks内核映像中所有为初始化的全局变量初始化为0;
- 建立异常向量表;
- 调用sysHwInit()初始化硬件,这里的sysHwInit()函数是vxWorks的板级支持包BSP的主调用函数;
- 创建初始化任务taskRoot,由taskRoot任务的主函数usrRoot继续完成vxWorks核心的初始化。
usrInit()的实现跟用户的配置相关,这里我们不考虑Cache的使用,由于我们侧重分析的VxWorks内核的初始化过程,cache的配置和工作机制不是我们研究的重点。
usrInit()实现代码如下:
void usrInit (int startType)
{
sysStart (startType); /* 清BSS段,同时设置中断向量表的基地址*/
excVecInit (); /*构建异常向量表 */
sysHwInit (); /*板级支持包BSP的入口函数,vxWorks的设备驱动在这里调用*/
usrKernelInit (); /* 构造初始化任务taskRoot的上下文,启动taskRoot */
}
分析:
- sysStart (startType)主要完成的工作是清BSS段、设置启动类型,并初始化CPU的中断向量表基地址寄存器。
- excVecInit()完成初始化构架异常向量表,并用构架的异常向量表的基地址初始化CPU的异常向量基地址寄存器;
- sysHwInit ()是vxWorks板级支持包BSP的入口完成,用于完成BSP定制的外设的初始化,主要包含以下几个部分:
- 初始化中断控制器和挂接中断的例程,比如Pentium平台:
- sysIntInitPIC (); /*初始化可编程中断控制器 */
- intEoiGet = sysIntEoiGet; /* 用于中断挂接的intConnect()的调用例程 */
- 遍历PCI总线,初始化总线上的网络设备;
- pciConfigForeachFunc (0, TRUE, (PCI_FOREACH_FUNC) sysNetPciInit, NULL);
- 遍历PCI总线,寻找USB设备,并添加USB设备映射空间
- 初始化串口设备
- 初始化电源管理设备
- 初始化硬盘设备
usrKernelInit()配置内核数据结构,并调用kernelInit()构造初始化任务taskRoot的上下文,启动taskRoot任务。我们单独分析usrKernelInit()函数。
6.3 usrKernelInit()函数分析
usrKernelInit()配置内核数据结构,调用kernelInit()构造初始化任务taskRoot的上下文,启动taskRoot任务,其具体代码实现如下:
void usrKernelInit (void)
{
classLibInit (); /* initialize class (must be first) */
taskLibInit (); /* initialize task object */
/* 配置内核就绪队列、活动队列、定时队列 */
#ifdef INCLUDE_CONSTANT_RDY_Q
qInit (&readyQHead, Q_PRI_BMAP, (int)&readyQBMap, 256); /* 固定优先级队列 */
#else
qInit (&readyQHead, Q_PRI_LIST); /* 简单优先级队列 */
#endif /* !INCLUDE_CONSTANT_RDY_Q */
qInit (&activeQHead, Q_FIFO); /* 先进先出的活动队列 */
qInit (&tickQHead, Q_PRI_LIST); /* 简单优先级队列*/
workQInit (); /* 内核延时工作队列 */
/*构架初始化任务taskRoot()上下文,启动taskRoot任务,其主流程为usrRoot */
kernelInit ((FUNCPTR) usrRoot, ROOT_STACK_SIZE, MEM_POOL_START,
sysMemTop (), ISR_STACK_SIZE, INT_LOCK_LEVEL);
}
分析:
在VxWorks中:
就绪队列由全局变量readyQHead指向其头部,该队列中链接的是有资格获取CPU使用权的任务;
定时队列由全局变量readyQHead指向其头部,该队列链接的是所有需要延时的任务;
活动队列由全局变量activeQHead指向其头部,该队列链接的是内核中创建的所有任务,包括就绪队列中的任务、定时队列中需要延时的任务、以及在信号量等待队列中的任务。
内核延时队列是一个大小为64的环形队列;
这四个队列构成了vxWorks内核最核心的资源。位于wind内核的内核态中,由内核全局变量kernelState进行保护。只有在windLib库中的内核态例程wind*开头的例程才可以访问。非内核态的例程只有进入内核态,才能调用wind*例程,访问并操作这三个内核队列、以及各种信号量等待队列。
下面我们依次分析者四种队列:
6.3.1 就绪队列
在VxWorks的wind内核中就绪队列可以由两种配置方式:
1. 按照优先级的从高低排序,形成一个优先级队列:
qInit (&readyQHead, Q_PRI_LIST); /* 简单优先级队列 */
这样的队列虽然比较简单。但是当存在任务就绪时,插入队列的时间跟优先级队列的长度相关,假如优先级队列的长度为n。则插入优先级队列的时间复杂度为O(n)。
2. 另外一种方式是采用才优先级位图形式的优先级队列。这样的话,优先级队列的入队时间只有优先级数相关,而与优先级队列的长度无关,插入优先级队列的时间复杂度为O(1)。
具体的机制如下:
readyQHead类型:
typedef struct /* Q_HEAD */
{
Q_NODE *pFirstNode; /* first node in queue based on key */
UINT qPriv1; /* use is queue type dependent */
UINT qPriv2; /* use is queue type dependent */
Q_CLASS *pQClass; /* pointer to queue class */
} Q_HEAD;
Q_NODE是16个字节的类型:
typedef struct /* Q_NODE */
{
UINT qPriv1; /* use is queue type dependent */
UINT qPriv2; /* use is queue type dependent */
UINT qPriv3; /* use is queue type dependent */
UINT qPriv4; /* use is queue type dependent */
} Q_NODE;
在readyQHead.pFirstNode指向的就绪队列中,每个节点代表一个WIND_TCB控制块,所以WIND_TCB控制块必须有一个成员为Q_NODE类型,
typedef struct windTcb /* WIND_TCB - task control block */
{
Q_NODE qNode; /* 0x00: multiway q node: rdy/pend q */
Q_NODE tickNode; /* 0x10: multiway q node: tick q */
Q_NODE activeNode; /* 0x20: multiway q node: active q */
OBJ_CORE objCore; /* 0x30: object management */
…………….<略>……………
} WIND_TCB;
readQHead头节点在
usrKernelInit()->qInit (&readyQHead, &qPriBMapClass, (int)&readyQBMap, 256)中初始化
将readQHead. pQClass初始化为& qPriBMapClass.
这样就可以通过readQHead. pQClass调用rqPriBMapClass .qPriBMapInit()初始化readyQHead.
通过qPriBMapInit()申明部分:
STATUS qPriBMapInit
(
Q_PRI_BMAP_HEAD * pQPriBMapHead,
BMAP_LIST * pBMapList,
UINT nPriority /* 1 priority to 256 priorities */
)
其中:
typedef struct /* Q_PRI_BMAP_HEAD */
{
Q_PRI_NODE *highNode; /* highest priority node */
BMAP_LIST *pBMapList; /* pointer to mapped list */
UINT nPriority; /* priorities in queue (1,256) */
} Q_PRI_BMAP_HEAD;
typedef struct /* Q_PRI_NODE */
{
DL_NODE node; /* 0: priority doubly linked node */
ULONG key; /* 8: insertion key (ie. priority) */
} Q_PRI_NODE;
typedef struct dlnode /* Node of a linked list. */
{
struct dlnode *next; /* Points at the next node in the list */
struct dlnode *previous; /* Points at the previous node in the list */
} DL_NODE;
typedef struct /* BMAP_LIST */
{
UINT32 metaBMap; /* lookup table for map */
UINT8 bMap [32]; /* lookup table for listArray */
DL_LIST listArray [256]; /* doubly linked list head */
} BMAP_LIST;
typedef struct /* Header for a linked list. */
{
DL_NODE *head; /* header of list */
DL_NODE *tail; /* tail of list */
} DL_LIST;
readyQHead类型将由Q_HEAD类型强制装换为Q_PRI_BMAP_HEAD类型:
这样readyQHead. qPriv1将会初始为(int)&readyQBMap,eadQHead. qPriv2被初始化为类255.
readyQHead.pFirstNode被初始化为NULL。
初始化之后的示意图状态如图6.2所示。
图6.2 就绪队列状态示意图
备注:从图中我们可以看出readyQHead.pFirstNode成员是Q_NODE类型的指针变量(Q_NODE类型占据16个字节),而pQPriBMapHead. highNode成员是Q_PRI_NODE类型的指针变量。
这意味着什么呢?
我们可以这样理解,readyQHead.pFirstNode原来是指向16个字节内存区域的指针,经过强制类型装换后,编程了指向12个字节内存区域的指针。
typedef struct /* Q_PRI_NODE */
{
DL_NODE node; /* 0: priority doubly linked node */
ULONG key; /* 8: insertion key (ie. priority) */
} Q_PRI_NODE;
typedef struct dlnode /* Node of a linked list. */
{
struct dlnode *next; /* Points at the next node in the list */
struct dlnode *previous; /* Points at the previous node in the list */
} DL_NODE;
备注:从Q_PRI_NODE的类型我们可以看出,当处理任务的代理人WIND_TCB是将IWND_TCB中的Q_NODE类型的成员变量转换为Q_PRI_NODE,这意味着下面图6.3所示映射关系。
图6.3 Q_NODE映射关系
从图中,我们可以看出,wind内核将WIND_TCB中的qNode域转换成Q_PRI_NODE节点,放到优先级队列中进行处理。由于qNode节点是WIND_TCB的第一个成员,该变量的首地址就是相应任务的WIND_TCB地址,却优先级队列中的Q_PRI_NODE需要转化为TCB节点时,只需要做类型转换即可。比如:
taskIdCurrent = (WIND_TCB *) Q_FIRST (&readyQHead)
其中Q_FIRST宏类型如下:
#define Q_FIRST(pQHead) \
((Q_NODE *)(((Q_HEAD *)(pQHead))->pFirstNode))
这样一切就清楚了。
vxWorks使用基于BIT位图的优先级队列,使用位图(bitmap)和元位图(meta-bitmap)、每个优先级对应一个FIFO队列,这种设计方案可以快速获取的Q_GET()、Q_PUT()操作方法,即Q_GET()、Q_PUT()操作的时间复杂度为0(1)。
其具体优先级位图状态如图6.4所示。
图6.4 优先级位图状态
备注:Task A,Task B, Task C的优先级为1,以对应的元位图的Bit31,二级位图Bit254.
例如当向位图队列中放入Task C时,是放入优先级为1处的FIFO队列的尾部。调整元位图和二级位图的C代码片段如下:
此时priority=1;
priority = 255 - priority;
pBMapList->metaBMap |= (1 << (priority >> 3));
pBMapList->bMap [priority >> 3] |= (1 << (priority & 0x7));
删除位图队列中的TASK F时,调度位图的C代码片段如下:
此时priority=255;
priority = 255 - priority;
pBMapList->bMap [priority >> 3] &= ~(1 << (priority & 0x7));
if (pBMapList->bMap [priority >> 3] == 0)
pBMapList->metaBMap &= ~(1 << (priority >> 3));
此时优先级位图队列的状态如图6.5所示。
图6.5 优先级位图队列状态
备注:注意元位图中的Bit0位,二级位图的中的Bit255位已经清0,255优先级对应的Task F任务已经从优先级位图队列中清除。
注意:这里需要指出的是元位图、以及二级位图中是以MSB Bit位来索引最高优先级的,这与我们在uC/OS-II中使用的以LSB Bit位来索引最高优先级的方式刚好相反。
6.3.2 定时队列设计
定时队列基于全局变量32位的无符号整数vxTicks,来判断定时器队列中的节点(每个节点代表一个WIND_TCB控制块)的定时时间是否到达。
定时队列在usrKernelInit()函数中北初始化:
qInit (&tickQHead, &qPriListClass); /* simple priority semaphore q*/
tickQHead也是Q_HEAD类型:
typedef struct /* Q_HEAD */
{
Q_NODE *pFirstNode; /* first node in queue based on key */
UINT qPriv1; /* use is queue type dependent */
UINT qPriv2; /* use is queue type dependent */
Q_CLASS *pQClass; /* pointer to queue class */
} Q_HEAD;
qInit()将tickQHead初始化为&qPriListClass,然后利用qPriListInit()初始化tickQHead的其余三个成员变量。
STATUS qPriListInit
(
Q_PRI_HEAD *pQPriHead
)
{
dllInit (pQPriHead); /* initialize doubly linked list */
return (OK);
}
通过qPriListInit()函数的类型,我们可以看出,tickQHead将会被转化为Q_PRI_HEAD类型:
typedef DL_LIST Q_PRI_HEAD;
typedef struct /* Header for a linked list. */
{
DL_NODE *head; /* header of list */
DL_NODE *tail; /* tail of list */
} DL_LIST;
其初始化后的定时器队列,在挂入了两个延时任务后的示意如图6.6所示。
图6.6 定时器队列示意图
备注:WIND_TCB块的Q_NODE域的四个成员,目前只是用了三个,没有用的是第四个成员域,定时器队列采用根据定时到期的时刻(该时间存放在qPriv3成员域中,也即key变量的值)的长短排序,到期时刻小的节点排在前面。
tickQHead指向的定时队列中,tickQHead中有两个域pFirstNode,qPriv1分别之前定时队列的头部和尾部。
定时队列的节点QPriNode的两个域在定时队列的第一个节点和最后一个节点,具有一个节点域为NULL。
即第一个节点previous为NULL,最后一个节点next为NULL
我们来分析一下入队操作:当一个任务需要延时时,将通过taskDelay()->windDelay()执行:
Q_PUT (&tickQHead, &taskIdCurrent->tickNode, timeout + vxTicks)实现。
其中vxTicks存放的是当前滴答数,timeout表现要定时的时长,那么timeout + vxTicks表示的是闹钟闹铃的时刻(这里以时钟滴答作为刻度数),Q_PUT()是一个操作宏,即最终调用:
qPriListPut(&tickQHead, &taskIdCurrent->tickNode, timeout + vxTicks)。
由于定时器是按照定时时刻从前往后排序qPriListPut会将这个新的节点放置到第一个小于其时刻值的节点前面。
加入当前的定时队列的排序是:1,3,5,7,7,9
那么新来的6节点插入后的队列是:1,3,5,6,7,7,9
那么新来的7节点插入后的队列是:1,3,5,6,7,7,7,9
备注:如果插入的节点的定时刻和队列中已有节点的定时时刻相同,那么将其插入到相同定时时刻的节点后面。
为方便阅读,我贴出插入代码:
void qPriListPut
(
Q_PRI_HEAD *pQPriHead,
Q_PRI_NODE *pQPriNode,
ULONG key
)
{
FAST Q_PRI_NODE *pQNode = (Q_PRI_NODE *) DLL_FIRST (pQPriHead);
pQPriNode->key = key;
while (pQNode != NULL)
{
if (key < pQNode->key) /* it will be last of same priority */
{
dllInsert (pQPriHead, DLL_PREVIOUS (&pQNode->node),
&pQPriNode->node);
return;
}
pQNode = (Q_PRI_NODE *) DLL_NEXT (&pQNode->node);
}
dllInsert (pQPriHead, (DL_NODE *) DLL_LAST (pQPriHead), &pQPriNode->node);
}
备注:由此看出将一个延时的任务插入定时队列的时间复杂度(这里指的是最坏时间复杂度)是跟延时队列的长度相关的,即时间复杂度为0(n)。为了保证RTOS的确定性,该插入操作在VxWorks后续版本(比如VxWorks6.8版本)中采用多级差分队列的算法,Linux-2.4之后的内核,uC/OS-III也采用了类似的算法。
出队操作比较简单,在VxWorks的时钟中断处理函数usrClock()->tickAnnounce()->windTickAnnounce()检查是否有任务的定时时间到,如果到的话,将会从定时队列中剔除,相关代码片段如下:
while ((pNode = (Q_NODE *) Q_GET_EXPIRED (&tickQHead)) != NULL)
{
pTcb = (WIND_TCB *) ((int)pNode - OFFSET (WIND_TCB, tickNode));
。。。。。。。。。。。。。。。。。。。。。
}
Q_GET_EXPIRED (&tickQHead)即调用:qPriListGetExpired(&tickQHead)
该函数返回定义检查tickQHead队列的第一个节点是否定时时间到,如果到的话,返回第一个节点的地址,同时将第一个节点从定时队列中删除,让第二个节点成为顶一个节点。
Q_PRI_NODE *qPriListGetExpired
(
Q_PRI_HEAD *pQPriHead
)
{
FAST Q_PRI_NODE *pQPriNode = (Q_PRI_NODE *) DLL_FIRST (pQPriHead);
if ((pQPriNode != NULL) && (pQPriNode->key <= vxTicks))
return ((Q_PRI_NODE *) dllGet (pQPriHead));//删除第一个节点,让其后续成为队列头部
else
return (NULL);
}
5.3.3 活动队列
活动队列链接了vxWorks内核中所有已经创建的任务,不论其是否为就绪态,都会在链入该队列中。vxWorks内核的提高的系统调用i()、以及shell中的i命令,均是遍历该活动队列来显示系统中的所有创建的任务。
在usrKernelInit()被初始化:
qInit (&activeQHead, &qFifoClass); /* FIFO queue for active q */
activeQHead类型:
typedef struct /* Q_HEAD */
{
Q_NODE *pFirstNode; /* first node in queue based on key */
UINT qPriv1; /* use is queue type dependent */
UINT qPriv2; /* use is queue type dependent */
Q_CLASS *pQClass; /* pointer to queue class */
} Q_HEAD;
qInit ()将activeQHead. pQClass初始化为&qFifoClass,进而调用qFifoInit()初始化activeQHead的前两个域:
STATUS qFifoInit
(
Q_FIFO_HEAD *pQFifoHead
)
{
dllInit (pQFifoHead);
return (OK);
}
pQFifoHead类型:
typedef DL_LIST Q_FIFO_HEAD; /* Q_FIFO_HEAD */
typedef DL_NODE Q_FIFO_NODE; /* Q_FIFO_NODE */
typedef struct dlnode /* Node of a linked list. */
{
struct dlnode *next; /* Points at the next node in the list */
struct dlnode *previous; /* Points at the previous node in the list */
} DL_NODE;
typedef struct /* Header for a linked list. */
{
DL_NODE *head; /* header of list */
DL_NODE *tail; /* tail of list */
} DL_LIST;
其初始化后,加入了两个任务的队列如图6.7所示。
图6.7 活动队列示意图
从图中,我们可以看出活动队列比较简单。由于其是双向队列,可以将其插入到指定节点的任何位置。
例如当创建任务时:
taskSpawn()->taskCreate()->taskInit()->windSpawn()将新创建的任务掺入到活动队列的尾部,代码片段如下:
Q_PUT (&activeQHead, &pTcb->activeNode, FIFO_KEY_TAIL); /* in active q*/
Q_PUT()是一个宏,进而调用qFifoPut (&activeQHead, &pTcb->activeNode, FIFO_KEY_TAIL)
void qFifoPut
(
Q_FIFO_HEAD *pQFifoHead,
Q_FIFO_NODE *pQFifoNode,
ULONG key
)
{
if (key == FIFO_KEY_HEAD)
dllInsert (pQFifoHead, (DL_NODE *)NULL, pQFifoNode);
else
dllAdd (pQFifoHead, pQFifoNode);
}
将指定的任务从活动队列中删除:
taskDelete()->taskDestroy()->windDelete()
或者taskTerminate()->taskDestroy()->windDelete()
windDelete()中的关键代码如下:
Q_REMOVE (&activeQHead, &pTcb->activeNode); /* deactivate it */
进而调用:qFifoRemove()
STATUS qFifoRemove(&activeQHead, &pTcb->activeNode); /* deactivate it */
(
Q_FIFO_HEAD *pQFifoHead,
Q_FIFO_NODE *pQFifoNode
)
{
dllRemove (pQFifoHead, pQFifoNode);
return (OK);
}
6.3.4 内核延时队列
由于wind内核态正在被其它程序访问,当前新的请求内核态例程服务的Job将被放置到内核队列中延时处理。内核工作队列是一个单读者/多写者的环形工作队列。读者总是第一个进入内核态的任务或者中断ISR,读者负责在离开wind内核前清空内核队列(通过执行内核Job)。由于内核写者主要来自于中断ISR(,还有一部分来自于任务),因此在写操作内核队列期间,CPU必须关中断;但是在读操作期间不需要关中断。
内核队列通过一个大小为1K字节的环形缓冲队列实现,队列中的每一个元素称为Job,占16个字节大小,环形缓冲队列一共有64个Job。选择64个字节大小,是想利用刚好一个字节的数据的索引值可以遍历这个队列。这是因为每遍历一个元素,索引值都需要加4,如果用8个bit位(刚好一个字节大小)的索引值,其回卷到数值0时,刚对内核队列从头开始。不用单独考虑内核队列是否回卷,省去了条件判断的时间。
备注:有两个方面的局限,可能导致未来的wind内核版本中修改内核队列,这是因为64个大小的内核队列,每个队列16个字节是硬编码的,这很有可能不能适应未来的需求,但是就目前来说,这个规模是最有效的机制。
workQInit()完成内核队列的初始化,并将读写索引初始化为0,其代码如下:
void workQInit (void)
{
workQReadIx = workQWriteIx = 0; /* initialize the indexes */
workQIsEmpty = TRUE; /* the work queue is empty */
}
workQAdd0()添加无参数的Job到内核队列中,当内核被中断时,新的服务请求将会以Job的形式添加到内核队列中。内核队列可以被第一个进入内核的中断ISR或者任务清空,但不管是中断ISR还是任务,最终都以在调度器reschedule()的末尾清空内核队列。
由于内核队列采用单读者/多写者的模式,因此我们必须在写者在向内核队列添加Job的过程中关中断,由于读者从来不会中断写者,因此中断只在写者需要引导队列写索引的时候关闭。
其实现如下:
void workQAdd0( FUNCPTR func )
{
int level = intLock (); /* 关中断 */
FAST JOB *pJob = (JOB *) &pJobPool [workQWriteIx];
workQWriteIx += 4; /* 移到写索引 */
if (workQWriteIx == workQReadIx)
workQPanic (); /* 如果内核队列满,则在关中断的情况下退出内核 */
intUnlock (level); /* 开中断 */
workQIsEmpty = FALSE; /* 标识内核队列现在非空 */
pJob->function = func; /*构造Job*/
}
添加带一个参数的Job到内核队列中:
void workQAdd1 (FUNCPTR func, int arg1 )
{
int level = intLock (); /*关中断 */
FAST JOB *pJob = (JOB *) &pJobPool [workQWriteIx];
workQWriteIx += 4; /* 移到写索引*/
if (workQWriteIx == workQReadIx)
workQPanic (); /* leave interrupts locked */
intUnlock (level); /* 开中断 */
workQIsEmpty = FALSE; /* 标识内核队列非空 */
pJob->function = func; /*向Job中添加函数 */
pJob->arg1 = arg1; /* 向Job中添加函数参数 */
}
添加带两个参数的Job到内核队列中:
void workQAdd2(FUNCPTR func, int arg1, int arg2 )
{
int level = intLock (); /* 关中断 */
FAST JOB *pJob = (JOB *) &pJobPool [workQWriteIx];
workQWriteIx += 4; /* advance write index */
if (workQWriteIx == workQReadIx)
workQPanic (); /* leave interrupts locked */
intUnlock (level); /* 开中断 */
workQIsEmpty = FALSE; /* we put something in it */
pJob->function = func; /* 向Job中添加函数*/
pJob->arg1 = arg1; /* 向Job中添加参数*/
pJob->arg2 = arg2; /* 向Job中添加参数*/
}
清空内核队列:
void workQDoWork (void)
{
FAST JOB *pJob;
int oldErrno = errno; /* save errno */
while (workQReadIx != workQWriteIx)
{
pJob = (JOB *) &pJobPool [workQReadIx]; /* get job */
/* 在执行内核Job函数之前,增加读索引,因为Job函数有可能是时钟处理函数
* windTickAnnounce () ,它也是通过这个Job函数进行调用。
*/
workQReadIx += 4;
(FUNCPTR *)(pJob->function) (pJob->arg1, pJob->arg2);
workQIsEmpty = TRUE; /* 标识内核队列有空位置 */
}
errno = oldErrno; /* restore _errno */
}
Wind内核中的三个队列、在加上各种信号量上的等待队列构成了wind内核最核心的资源,位于wind内核的内核态中,由内核全局变量kernelState进行保护。只有在windLib库中的内核态例程wind*开头的例程才可以访问。非内核态的例程只有进入内核态,才能调用wind*例程,访问并操作这三个内核队列、以及各种信号量等待队列。
5.4 kernelInit()构造初始化任务taskRoot上下文
kernelInit()函数:
kernelInit ((FUNCPTR) usrRoot, ROOT_STACK_SIZE, MEM_POOL_START,
sysMemTop (), ISR_STACK_SIZE, INT_LOCK_LEVEL);
其中:
#define ROOT_STACK_SIZE 10000 /* size of root's stack, in bytes */
#define INT_LOCK_LEVEL 0x0 /* 80x86 interrupt disable mask */
#define ISR_STACK_SIZE 1000 /* size of ISR stack, in bytes */
MEM_POOL_START标识内核映像在内存中的结束位置,通过链接脚本的end来标识。
kernelInit()代码实现如下,我们假设目标平台为Pentium,所有这里删除与Pentium平台无关代码,所有X86平台栈均向下增长。
void kernelInit
(
FUNCPTR rootRtn, /* 用户启动例程 */
unsigned rootMemSize, /*给 TCB 和初始任务栈分配的内存 */
char * pMemPoolStart, /* 内存池的起始地址 */
char * pMemPoolEnd, /* 内存池的结束地址 */
unsigned intStackSize, /* 中断栈大小 */
int lockOutLevel /* 关中断级别 (1-7) */
)
{
union
{
double align8; /* 8-byte alignment dummy */
WIND_TCB initTcb; /* context from which to activate root */
} tcbAligned;/*共用体的使用确保初始任务TCB八字节对齐*/
WIND_TCB * pTcb; /* pTcb初始任务TCB指针*/
unsigned rootStackSize; /* 初始任务的实际栈大小 */
unsigned memPoolSize; /* 初始内存池的实际大小*/
char * pRootStackBase; /* 初始任务栈基地址 */
/* 使得输入参数按照指定的字节(一般4字节对齐) */
rootMemNBytes = STACK_ROUND_UP(rootMemSize);
pMemPoolStart = (char *) STACK_ROUND_UP(pMemPoolStart);
pMemPoolEnd = (char *) STACK_ROUND_DOWN(pMemPoolEnd);
intStackSize = STACK_ROUND_UP(intStackSize);
/*初始化vxWorks中断级别*/
intLockLevelSet (lockOutLevel);
/* 时间片轮转调度模型默认禁止*/
roundRobinOn = FALSE;
/*时钟滴答初始化为0 */
vxTicks = 0; /* good morning */
#if (_STACK_DIR == _STACK_GROWS_DOWN)
vxIntStackBase = pMemPoolStart + intStackSize;//设置中断栈基地址
vxIntStackEnd = pMemPoolStart; //设置中断栈尾地址
bfill (vxIntStackEnd, (int) intStackSize, 0xee);//用0xee填充中断栈
windIntStackSet (vxIntStackBase);//设置wind内核的中断栈基地址指针vxIntStackPtr
pMemPoolStart = vxIntStackBase;
#else /* _STACK_DIR == _STACK_GROWS_UP */
<略>
#endif /* (_STACK_DIR == _STACK_GROWS_UP) */
/* Carve the root stack and tcb from the end of the memory pool. We have
* to leave room at the very top and bottom of the root task memory for
* the memory block headers that are put at the end and beginning of a
* free memory block by memLib's memAddToPool() routine. The root stack
* is added to the memory pool with memAddToPool as the root task's
* dieing breath.
*/
rootStackSize = rootMemNBytes - WIND_TCB_SIZE - MEM_TOT_BLOCK_SIZE;
pRootMemStart = pMemPoolEnd - rootMemNBytes;
#if (_STACK_DIR == _STACK_GROWS_DOWN)
pRootStackBase = pRootMemStart + rootStackSize + MEM_BASE_BLOCK_SIZE;
pTcb = (WIND_TCB *) pRootStackBase;
#else /* _STACK_GROWS_UP */
<略>
#endif /* _STACK_GROWS_UP */
//这里把taskIdCurrent初始化为0,是因为taskInit()会进入内核态,执行windSpawn()将当前
//初始任务放入活动队列(activceQueue),然后调用windExit()退出内核态,在windExit()逻辑
//中会判断taskIdCurrent和就绪队列的头readyQHead是否相等,如果相等则说明当前任务
//是优先级最高的任务,不需要进行上下文切换,这我们的情景中taskIdCurrent为NULL,而
//此时内核队列也为空,即readyQHead也为NULL,则不需要进行上下文切换,又由于此时
//内核队列为空,所以windExit()直接放回,这正是我们想要的结果,windExit()判断逻辑如
//下图黄色部分所示。
taskIdCurrent = (WIND_TCB *) NULL; /* 初始化化taskIdCurrent为空 */
bfill ((char *) &tcbAligned.initTcb, sizeof (WIND_TCB), 0);
memPoolSize = (unsigned) ((int) pRootMemStart - (int) pMemPoolStart);
//初始化任务,并将初始化任务放入活动队列,此时任务保持挂起(SUSPEND)状态
//注意初始化任务的优先级为0
taskInit (pTcb, "tRootTask", 0, VX_UNBREAKABLE | VX_DEALLOC_STACK,
pRootStackBase, (int) rootStackSize, (FUNCPTR) rootRtn,
(int) pMemPoolStart, (int)memPoolSize, 0, 0, 0, 0, 0, 0, 0, 0);
rootTaskId = (int) pTcb; /* fill in the root task ID */
/* Now taskIdCurrent needs to point at a context so when we switch into
* the root task, we have some place for windExit () to store the old
* context. We just use a local stack variable to save memory.
*/
//现在将taskIdCurrent初始化为一个临时的的TCB控制块,taskActive()进入内核态,调用
//windResume()将初始任务taskRoot放入就绪队列,此时readyQHead指向就绪队列中唯一
//的任务taskRoot初始任务,当taskActive()条用windExit()退出内核态时,由于readyQHead
//和taskIdCurrent不等,windExit()将调用调度器恢复readyQHead指向的队首任务的上下文,
//即恢复taskRoot的上下文。由于windExit()在调用调度器恢复taskRoot任务上下文之前,
//保持当前任务taskIdCurrent的上下文当当前任务的TCB控制块中,所里这里才定义了一
//个临时的上下文空间tcbAligned.initTcb,由于这个临时空间在临时栈中分配,当taskRoot
//任务起来后,临时栈即被舍弃了,因此不需要再回收了。这个情景中windExit()的执行逻
//辑,如下图红色部分所示。
taskIdCurrent = &tcbAligned.initTcb; /* update taskIdCurrent */
taskActivate ((int) pTcb); /* activate root task */
}
分析:windExit()的执行流程如图6.8所示。
图6.8 windExit()执行流程
我们在前面的博文VxWorks内核解读-3已经分析了windExit()的执行流程,这里不再赘述。
备注:这是有一点需要注意,taskActivate()调用windExit()恢复taskRoot的上下文后,启动的任务并不是usrRoot(),而是void vxTaskEntry ()函数,由vxTaskEntry()来调用usrRoot()函数。
vxTaskEntry()代码如下:
FUNC_LABEL(vxTaskEntry)
xorl %ebp,%ebp /* make sure frame pointer is 0 */
movl FUNC(taskIdCurrent),%eax /* get current task id */
movl WIND_TCB_ENTRY(%eax),%eax /* entry point for task is in tcb */
call *%eax /* call main routine */
addl $40,%esp /* pop args to main routine */
pushl %eax /* pass result to exit */
call FUNC(exit) /* gone for good */
这样做的目的有三个:
- 任务的真正入口函数保存在任务控制块中,很容易通过taskRestart()重新启动;
- vxTaskEntry()函数的引入,使得任务的主函数体相对于vxTaskEntry()来说是一个普通的函数调用,其任务栈可以被编译器自动清理,也便于调试栈回溯工具处理主函数例程的调用。
- 从vxTaskEntry()的代码我们可以看出,任务的主函数执行完毕后,将会调用exit()函数回收该任务的资源,这样就编译对删除的任务回收期资源。
现在我们接着分析初始任务taskRoot的主函数例程usrRoot()吧,O(∩_∩)O~。
6.5 初始化任务taskRoot的执行
usrRoot()属于用户自定义的例程,主要完成VxWorks内核的初始化,比如初始化I/O系统,安装驱动,创建设备,建立协议栈等待,这是都是可以通过用户来配置,它也可以创建系统符号表。
我们现在不考虑其他外围组件,只考虑Wind内核的执行,其usrRoot的实现如下:
void usrRoot (char *pMemPoolStart, unsigned memPoolSize)
{
usrKernelCoreInit (); /* vxWorks核心的初始化 */
//vxWorks的核心初始化化包括事件模块、二值信号量模块、互斥信号量模块、计数信
//号量模块、消息队列、看门狗、以及任务创建、删除、上下文切换钩子模块的初始化
memInit (pMemPoolStart, memPoolSize); /* 初始化内存分配器 */
memPartLibInit (pMemPoolStart, memPoolSize); /* 初始化核心内存管理单元 */
// memInit()以及保护了memPartLibInit()的调用,因此再次显示调试memPartLibInit()其
//实是没有必要的,还好memPartLibInit()用了一个全局变量memPartLibInstalled,借以验
//证memPartLibInit()是否已经被调用过.
sysClkInit (); /* 挂接时钟中断,并初始化时钟*/
usrMmuInit(); /*建立一一对应的MMU映射*/
usrAppInit (); /* 调用用户自定义例程*/
}
分析:
由于我们目前仅仅分析vxWorks的wind内核的工作机制,所有vxWorks的其它组件,比如I/O模块,文件系统,shell等等暂不考虑。
至此,到VxWorks运行到usrAppInit()时,vxWorks的wind内核的多任务运行环境,已经运行起来,我们可以在usrAppInit()函数中,创建我们的应用调用vxWorks提供的服务来执行。
比如:
/*
* usrAppInit - initialize the users application
*/
#include "vxWorks.h"
#define DEMO_PRI 149
extern void windDemo(int iteration);
void usrAppInit (void)
{
#ifdef USER_APPL_INIT
USER_APPL_INIT; /* for backwards compatibility */
#endif
printk("hello vxWorks\n");
//创建一个demoTask任务来运行
taskSpawn("demoTask", DEMO_PRI, 0x0001, 4000, (FUNCPTR) windDemo, 20, 0,0,0,0,0,0,0,0,0);
/* add application specific code here */
}
至此,我们VxWorks的初始化过程就分析完了,大家有任何疑问都可以给我留言,或者email:[email protected]。