原文:http://www.prtos.org/vxworks-wind-reader-faq/。
本文概述:本网站自2016年2月份建站以来,陆续发布了一系列关于VxWorks Wind内核运行机制的博文,并陆续收到一些读者提出的问题,我从中选出了几个问题系统性的分析和解答下。
先回忆一下我学习VxWorks系统经历。我接触VxWorks系统始于2011年8月份来上海读研。当时我从网上下载了Tornado 2.2.1的Pentium开发环境(VxWorks5.5版本)搭建了VxWorks基于VMware仿真运行环境,并结合VxWorks的文档(Tornado2.2/docs/books.html)详细学习了VxWorks系统的开发和使用。
当时为了能每个月多挣点生活费,我报名兼职了VxWorks培训,为培训中心的学员培训VxWorks系统,培训内容包括Tornado开发环境的使用、Host Shell的模块加卸载功能、任务级和系统级调试、Wind View工具使用、以及VxWorks BSP开发等等。在培训的过程中很多学员问的更多的问题是VxWorks内核是如何实现的、和Linux内核或者开源的实时内核(比如uC/OS)相比差别在哪里。他们跟我说VxWorks BSP开发和Tornado使用网上已经有各种资料可以参考,他们很关心VxWorks内核的运行机制,了解了VxWorks内核将会有助于他们更好的进行VxWorks BSP开发和系统配置。
也许是有需求就有动力!从那时开始我系统性的钻研VxWorks源码,将网上下载的VxWorks源码重新整理编译后整合到Tornado开发环境中替换原有的VxWorks库文件,然后通过VxWorks和Qemu的仿真调试、以及添加额外的Log输出信息来更加可控地分析VxWorks运行机制,同时为了便于对比分析,我搭建了uC/OS的Qemu仿真运行环境,通过两个开发环境的现场调试运行、在培训的过程中可以让学员更直观地感受到系统的运行状况,培训获得了非常好的效果,当然了我也受益匪浅O(∩_∩)O~
言归正传,现在转到Wind内核的FQA(Freqently Question Answer)部分,针对大家提出的问题给出一个系统的解答。由于有些读者提的问题一针见血,并且在留言板一两句话解释不清楚,因此写了这篇博文。个人觉得学习一个新的系统,需要不断地质疑解惑,能提出问题是学习的一个很重要的途径。
第1部分 情景分析
假设允许被抢占的任务taskA正在运行,在T1时刻,时钟中断产生,Wind内核响应时钟中断,时钟中断ISR唤醒定时截止时间到,并且比taskA优先级更高的任务TaskB,在时钟中断返回时,Wind内核将保存taskA的上下文,并恢复taskB的上下文,从而taskB开始运行。这个情景也体现是RTOS的最基本的特征—抢占式内核。
我将抽丝剥茧般分析整个过程,让大家看到Wind内核保存taskA上下文和恢复taskB上下文的时机,以及上下文的保存过程中,Wind内核究竟做了哪些事情。
这类分析必须借助底层的平台,我选择Pentium平台。关于X86平台的保护模式以及段寄存器的作用,由于不是本文的重点就不重点描述了。
Wind定义了五个全局描述段:
FUNC_LABEL(sysGdt)
/* 0(selector=0x0000): 空段*/
.word 0x0000
.word 0x0000
.byte 0x00
.byte 0x00
.byte 0x00
.byte 0x00
/* 1(selector=0x0008): 特权模式任务代码段 */
.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): 数据段*/
.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): 异常代码段 */
.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): 中断数据段 */
.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 */
由于Wind内核运行在CPU的特权态,所以上述任务代码段和数据段、以及异常代码段和中断代码段的特权级都是0,Wind内核的数据段是可读/写,任务代码段、中断代码段和异常代码段只具有读/执行权限。从上面的设计我们可以看出,Wind内核任务态上下文和中断/异常状态上下文是不同的,即正常的任务执行是CS寄存器指向任务代码段(CS寄存器的值为0x0008);当发生外部中断时(比如时钟中断),CS寄存器执行中断代码段(CS的值为0x0020);当CPU发生异常(比如除零异常时),CS指向异常代码段(CS的值为0x0018)。
这样当Wind内核处理中断或者异常时,就可以根据CS寄存器的值来判断当前是在中断上下文中、还是异常上下文。需要指出的是这两种运行模式下数据段是公用的。
这也是Wind内核定义并初始化下面三个全局变量的意义之所在:
FUNC_LABEL(sysCsSuper)
.long 0x00000008 /* 指向特权任务代码段*/
FUNC_LABEL(sysCsExc)
.long 0x00000018 /* 指向异常代码段*/
FUNC_LABEL(sysCsInt)
.long 0x00000020 /* 指向中断代码段*/
那么问题来了,CPU怎么知道当发生中断时CS寄存器为0x00000020,而发生异常时CS寄存器的值为0x00000018呢?
这是通过中断描述符表IDT(Interrupt Description Table)完成的。我们知道Pentium处理器的IDT表有256项,其中CPU保留其中的0~31项(Pentium处理器只用到其中的0~19项),外部中断使用IDT表的32~255项。由于Pentium使用两片级联的I8259A中断控制器,所以外部中断只使用了其中的32~47项,而其中的时钟中断使用的就是IDT的第32项。
对于Pentium处理器来说,Wind内核只需要把IDT的0~19项的CS字段设置为0x00000018,其余表项的CS字段设置为0x00000020即可实现中断上下文和异常上下文的区分(Wind内核通过excVecInit()函数完成IDT表的初始化)。比如当CPU检测到有时钟中断发生时,通过中断向量号32索引到IDT表的第32项中断描述符,然后将该中断描述符的CS域存放的值0x00000020载入CS寄存器、将该描述符的Offset域存放的函数地址加载都EIP寄存器,即可进入中断上下文中执行。
备注:在Pentium平台,excVecInit()初始化IDT表,将IDT的0~19项的跳转指针Offset初始化为excStub()函数,20~255项初始化为excIntStub()函数。
当配置具体的外部中断时,Wind内核会通过intConnect()->intVecSet()重新设置中断IDT表对应表项的跳转指针Offset值。以外部中断时钟中断为例:
由usrRoot()->sysClkConnect()->sysHwInit2()->intConnect()->intHandlerCreateI86()构造一段执行代码存放在内存中,并将这块内存的入口地址放入IDT表的第32表项的跳转指针Offset域中。
Wind内核通过拷贝intConnectCode指针指向的区域获取这块执行代码(我在VxWorks内核解读-4有详细描述过),并提供了修改这段代码中预留调用函数的入口(一般称之为Hook函数)。针对时钟中断,这部分执行指令序列转译成汇编代码如下所示,为了方便描述我把这段代码命名为clockShellCode。
call intEnt //调用中断入口函数
pushl %eax //保存eax,edx,ecx寄存器
pushl %edx
pushl %ecx
call sysClkInt //调用时钟中断处理函数
pushl $0 // i8259IntEoiMaster的参数0
call i8259IntEoiMaster //发EOI(end of interrupt)信号给I8259A主片
addl $4, %esp //弹出压入栈i8259IntEoiMaster的参数0
popl %ecx //恢复eax,edx,ecx寄存器
popl %edx
popl %eax
jmp intExit //中断返回
描述清楚上面的基础设置后,我们接着分析设定的运行场景:
当任务taskA运行到T1时刻,时钟中断产生,Pentium CPU要做的第一件事情是将Pentium处理器的EFLAGS寄存器的值、以及CS:EIP寄存器的值压入taskA的任务栈中,CS:EIP寄存器中保存的是taskA将要执行的下一条指令的地址(CS寄存器存放任务代码段描述符选择子0x0008,EIP存放段内偏移量),接着CPU会根据时钟中断所使用的向量号32定位到IDT的第32表项,并从这个中断描述符表项中获取要加载到CS寄存器中的值0x00000020,以及加载到EIP寄存器中的clockShellCode的首地址。
注意:taskA被中断的时刻T1,Pentium CPU硬件只将EFAGS,CS:EIP三个寄存器的保存到taskA的任务栈,然后Pentium CPU接着执行clockShellcode代码。
执行到这里,有读者邮件问我,中断发生的时刻WIND内核并没有把taskA的上下文(比如eax,ecx,edx,ebx,esp,ebp,esi,edi寄存器的值)全部保存,而是在最后调用intExit()函数时,才在intExit()中调用saveIntContext()函数保存taskA上下文。这段时间Wind内核还调用了其它函数,最起码有一个时钟中断ISR,这不就破坏了taskA上下文(即eax,ecx,edx,ebx,esp,ebp,esi,edi寄存器的值被改变)?换句话说intExit()保存的上下文还是taskA的上下文吗?
答案是肯定的,保存的确实是taskA的上下文,这是因为虽然从中断发生的时刻到IntExit()真正执行保存taskA上下文期间,wind内核执行了诸如clockShellcode,并调用了IntEnt()、sysClkInt()函数,但是执行的原则是不能破坏taskA的上下文,即所有使用寄存器的值都事先在栈中备份,使用完毕后再从栈中恢复。比如clockShellcode中的eax,edx,ecx就是如此。至于ebx则由编译器进行保存和恢复,EBP和ESP配套形成一个栈帧,由C编译器进行函数调用时自动管理。
需要指出的是:在处理中断时Wind内核需要保存的taskA的寄存器只有EAX,EDX, ECX, EBX, ESP,之后便直接处理中断ISR。由此,我们可以看出中断上下文的保存比任务上下文的量级要轻很多。如果taskA在中断打断后没有更高优先级的taskB就绪,taskA恢复执行的开销是很小的。
执行到这里也许有读者会问,说为什么不在T1时刻把taskA的上下文全部保存,而非要在intExit()中保存呢?
这是因为虽然taskA被外部事件所中断,但是taskA在中断ISR执行完毕后仍然是系统中优先级最高的任务,那么任务上下文就没有必要保存了。如果现在保存的话,等到中断ISR执行完毕后,还要进行taskA上下文的恢复工作,这无形中增加了系统开销。所以只能等到中断ISR执行完毕后,再判断是否有更高优先级的任务就绪,从而决定是否需要保存。而在我们的情景中,时钟中断执行完毕后更高优先级的任务taskB就绪,因此需要保存taskA上下文,并恢复taskB上下文。
我们接着分析clockShellcode代码,其执行序列如下:
1)执行intEnt()函数:将taskA的栈切换到中断栈,
2)时钟中断处理函数sysClkInt()(该函数会进一步调用usrClock()->tickAnnounce()),
3)执行EOI函数i8259IntEoiMaster(),
4)执行intExit()函数:将中断栈切换到taskA的栈,….,执行saveIntContext,进而执行reschedule(),切换到新任务taskB。
这一部分的执行流程我在VxWorks内核解读-3和VxWorks内核解读-4分析过,这里不再赘述。
为了完整性,我再分析一下在此情景中IntExit()的执行流程,如下图所示:
在我们的情景中时钟中断执行完毕后,taskB的优先级更高, 这时intExit()会执行红色分支,调用saveIntContext()函数保存taskA的上下文,进而执行调度器reschedule()恢复taskB上下文。
第2部分 读者问题解答
问题1:针对上面的情景,有读者问(为了方便描述,我修改了部分语句):
“在saveIntContext中实际上是把被中断的taskA的上下文保存到taskA的TCB中,其中,PC,EFLAGS是由taskA栈中的EIP和EFLAGS弹栈(由硬件自动在T1时刻压入)得到的,EDX,ECX,EBP,ESP等是读取当时CPU的寄存器压入到taskA的TCB中。我的问题是:此时EDX等非硬件自动保存的寄存器的状态还是T1时刻的状态吗?如果不是的话,则保存的上下文状态就不准确了。在clockShellcode执行期间,虽然工作的堆栈从taskA 的私有栈切换到中断栈了,但是上述EDX等些寄存器是公用的,应该会受到影响的,也就是说saveIntContext所保存的EDX等寄存器的状态似乎不对,我无法理解”。
回答:这个问题在我们分析完上面假设场景后自然得到解答O(∩_∩)O~,不在复述。
问题2:代码中的supervisor stack是指什么?它就是当时被中断的taskA的栈吗?
回答:supervisor stack其实就是被中断的任务栈,由于VxWorks的所有任务运行在特权态(即wind内核运行在特权态),因为这个原因才会有这个名称,Wind源码中也把任务称为特权任务supervisor task。
问题3:EIP和PC寄存器是什么关系呢?一般是怎么使用的?
回答:PC是一个通用的说法,在X86平台,就是指CS:EIP。其中CS是当前代码段寄存器,EIP是段内偏移量,两者结合起来CS:EIP就是一个PC指针,具体可参考X86编程指南。
问题4:新选择的任务taskB何时开始执行的?
回答:Wind内核调度器reschedule()执行流程,如下图所示:
从上图我们可以看出,taskB上下文在windLoadContext()中恢复,windLoadContext代码如下:
FUNC_LABEL(windLoadContext)
movl FUNC(taskIdCurrent),%eax /* current tid */
movl WIND_TCB_ERRNO(%eax),%ecx /* save errno */
movl %ecx,FUNC(errno)
movl WIND_TCB_ESP(%eax),%esp /* push dummy except. */
pushl WIND_TCB_EFLAGS(%eax) /* push eflags */
pushl FUNC(sysCsSuper) /* push CS */
pushl WIND_TCB_PC(%eax) /* push pc */
movl WIND_TCB_EDX(%eax),%edx /* restore registers */
movl WIND_TCB_ECX(%eax),%ecx
movl WIND_TCB_EBX(%eax),%ebx
movl WIND_TCB_ESI(%eax),%esi
movl WIND_TCB_EDI(%eax),%edi
movl WIND_TCB_EBP(%eax),%ebp
movl WIND_TCB_EAX(%eax),%eax
iret /* enter task's context. */
只有执行最后一条汇编指令iret后,taskB代码的执行。
iret是X86的中断返回指令,执行iret的结果是从esp指向的栈中弹出EFLAGS、CS:EIP寄存器的值到Pentium处理器的EFLAGS、CS、EIP寄存器中,CPU立即执行taskB上次被中断时将要执行的指令。
问题5:WIND_TCB_PC,WIND_TCB_EFLAGS,WIND_TCB_EBP,WIND_TCB_ESP以及ICC_INT_ENT(见intHandlerCreateI86()函数)是在什么文件中定义的?我在代码(从网上下载的vxworks的完整源码文件)中没有找到。
回答:在h\arch\i86\regsI86.h文件中。
问题6:reschedule函数中的第00453行中的CS为何在saveIntContext中不保存,而在此处要压栈呢?它一般是怎么用的?
回答:Wind内核采用平板内存,所有任务共享全局代码空间,所以任务切换中,CS不用保存和恢复。但是wind内核异常/中断处理和正常的任务运行使用不同的代码段,前者是sysCsExc和sysCsInt,后者sysCsSuper,所以要进行CS寄存器值的切换。
评论