由于项目关系,有幸接触到了 VxWorks 的源代码,于是带着膜拜的心态开始阅读并分析其源代码,并将分析的结果记录在这里。我分析的版本是 VxWorks 6.7。

国内关于 VxWorks 的资料比较少,而且网上找到的资料不少是针对 VxWorks 5.x 的,和旧版比起来,VxWorks 6.x 增加了对多核硬件(SMP)的支持,并且加入了实时进程(RTP),可以开发用户态的应用程序,代码上的区别还是很大的。

文件组织结构

假设 VxWorks 开发套件安装在C:\WindRiver,那么 VxWorks 6.7 的源代码位于C:\WindRiver\vxworks-6.7\target。

其中有如下子目录:

  • config:这个目录中包括所有的 BSP
    1. comps/VxWorks:包含系统默认的组件定义文件(CDF)
    2. comps/src:这个目录下包含 configlettes,也就是一些短小的配置代码
  • h:VxWorks 内核头文件
    1. private:私有头文件,只有 src 中的代码能够访问
    2. make:Makefile 片段文件,这些文件会被各个工程的 Makefile 中引用
    3. tool:包含与工具链相关的 Makefile 片段,以及链接脚本
  • src:VxWorks 内核的源文件
  • lib:这个目录保存编译生成的内核库(单核版本)
  • lib_smp:这个目录保存编译生成的内核库(多核版本)
  • usr:所有用户态相关的文件都放在这里(RTP)
    1. h:用户态代码使用的头文件
    2. src:用户层库的源文件
    3. lib:保存编译生成的用户库

VxWorks内核的源代码就放在 src 目录下,这个目录下面又有许多子目录,比较重要的两个是 wind 和 os。wind 目录下包含任务管理相关代码,os 下则包含对象系统、内存管理等内容。

VxWorks 是模块化设计的,代码也是按照模块化组织的。VxWorks 把模块划分得非常精细,一个典型的模块就包括一个 C 文件和一个头文件。例如内存管理模块 memPartLib,相关代码包括 src/os/mm/memPartLib.c、h/memPartLib.h、h/private/memPartLibP.h,其中 h/private 中的头文件是私有头文件,风河不保证私有头文件中的函数接口发生改变,因此开发者为了确保与后续版本的 VxWorks 兼容,在开发中应该使用 VxWorks 的公开 API。

VxWorks 将功能相关的模块放在同一个目录下,每个目录下还有一个 Makefile 文件,这个文件用来指导 VxWorks 内核的编译过程。需要说明的是,编译 VxWorks 内核生成的不是完整 OS,而是内核库,要创建一个完整的 OS,需要得到内核库之后创建 VIP 项目,下一节会有详细说明。

VxWorks 的常规开发流程

平时我们讲 Linux 开发,通常意思是在 Linux 系统之上进行开发应用程序,同时开发出来的应用程序也运行在 Linux 系统上。然而 VxWorks 是一个嵌入式系统,我们不方便在 VxWorks 中进行开发,一般而言,VxWorks 开发工作是在一个运行着 Linux 或 Windows 的开发机上进行的,在开发机上编译之后,再把生成的东西放在开发板上运行。而且编译出来的也不是应用程序,而是一个系统镜像(system image),这个系统镜像可以直接加载到开发版上,或者烧录到设备 ROM 中。

这种系统镜像类型的项目叫做 VIP(VxWorks Image Project),它产生的输出就是一个完整的 OS。而且 VxWorks 的可定制成都很高,开发者可以选择这个 OS 中需要哪些功能,不需要那些功能,还有相关配置参数的取值。开发工具会根据用户的配置,生成一个定制的系统镜像。

VIP 项目的构建过程其实非常简单,就是把相关的静态库链接在一起。由于静态库实际上就是若干目标文件的打包,因此静态库链接就相当于把一对目标文件链接在一起。根据 VIP 项目中的用户配置,开发工具可以选择链接哪些目标文件,不链接哪些目标文件,这就实现了系统功能的定制。(当然,VIP 构建过程肯定不是只有链接,BSP代码、开发者编写的代码、根据 VIP 配置自动生成的代码,这些都需要进行编译,并于内核库共同链接。)

因此,构建 VIP 项目之前,需要保证所需静态库存在,这些静态库就是内核库,通过编译 VxWorks 内核源代码生成。编译内核库有两种方式:

  • 通过命令行,在 src 目录下执行 make CPU=PENTIUM TOOL=gnu VXBUILD=SMP 命令来编译(当然,需要根据具体的目标平台、编译器和多核选项来调整构建参数)。这种方式生成的静态库会保存在 lib 或 lib_smp 目录下(取决于单核还是多核)。
  • 通过集成开发环境 Workbench,创建 VSB(VxWorks Source Build)项目,构建 VSB 项目来编译内核库。这样生成的内核库会位于 VSB 项目目录中。

风河之所以将编译 VxWorks 的过程分为两步,一个原因是这可以加快 VIP 项目的构建速度,另一个原因则是用户可以在没有源代码的情况下使用 VxWorks 开发。用户可以创建 BSP 项目开发自己的版级支持包,也可以创建 VIP 项目,编写自己的内核应用程序,但是系统核心部分的代码是不变的。因此首先把系统核心部分编译成静态链接库,这样在构建 BSP 项目时就可以直接链接已经生成的库。

接下来对 VxWorks 代码进行分析。

对象系统

尽管 VxWorks 主要使用 C 语言开发,但是仍然大量应用了面向对象的思想。

任务(Task)

任务可谓是 VxWorks 的核心概念,类似于 Linux 系统中的内核线程,是系统中最基本的独立执行单元。任务运行在特权模式(Supervisor Mode),相互之间没有地址空间隔离,整个系统共用同一套虚拟地址空间(暂时不考虑 RTP 的情况)。由于任务直接运行在特权模式,可以直接用函数调用的方法使用 VxWorks API,不需要系统调用。VxWorks 支持多任务,而且是抢占式多任务,因此一个任务不需要主动让出 CPU,系统会在必要的时刻强制停止当前任务的执行,而切换到一个新的任务。

任务切换、陷阱

系统支持多任务,那就需要实现任务切换。任务切换就是从一个任务切换到另一个任务的过程,这个过程往往是借助 CPU 的陷阱机制实现的。

陷阱(Trap)在不同体系架构下可能有不同的名字,例如 x86 下的中断和异常都可以称作陷阱。所谓陷阱,是一种异步的事件,当陷阱发生的时候,CPU 可以暂停目前正在运行的代码,保存状态,跳转到陷阱处理程序执行,完成之后恢复状态,继续执行之前被打断的程序。具体来说就是:

  • 陷阱发生,CPU 将指令指针(PC)、栈指针(SP)的值保存在栈上(保存状态,这一步由 CPU 自动进行)
  • 根据陷阱的向量号,跳转到陷阱处理程序的入口点开始执行(执行 ISR,仍然 CPU 自动进行)
  • ISR 执行完毕之后,软件执行陷阱返回指令,CPU 从栈上恢复出 PC 和 SP 的值(恢复状态)
  • 继续执行之前被打断的程序

VxWorks 区分了两种不同的陷阱,即中断和异常。二者的主要区别在于来源不同,中断的来源往往外部设备,例如时钟、键盘等;异常则是 CPU 内部产生的,例如除零、违反特权级、分页异常等。中断和异常的处理程序都有自己的栈,当陷阱发生时,首先保存状态到当前栈,然后切换到相应的中断栈或异常栈,执行处理函数,最后切换回执行栈并恢复状态。

由于陷阱前和陷阱后,PC 与 SP 的取值完全相同,因此完全感觉不到任何中断。但是有一点,保存 PC/SP 和恢复 PC/SP 都是通过栈进行的,如果我们在处理陷阱的过程中改变了栈,就会导致陷阱返回的时候,恢复的不是陷阱之前被中断的程序,这就是任务切换的基本思路。

执行栈、异常栈、中断栈

如果了解一些操作系统的概念,应该会知道,每个线程都拥有自己的栈。但是在 VxWorks 中,存在着三种栈:

  • 执行栈(Execution Stack),表示用于执行正常代码,进行函数调用、保存局部变量的栈。每个任务都拥有自己专属的执行栈。
  • 异常栈(Exception Stack),当异常发生的时候,ISR 会切换到这个栈,并执行相应的执行。理论上,异常处理程序完全可以在执行栈上运行,但是 VxWorks 还是给异常单独分配了一个栈。异常栈也是每个任务一份,因此当一个任务正在执行异常处理代码的时候,也是可以进行任务切换的。
  • 中断栈(Interrupt Stack),当中断发生的时候,ISR 会切换到这个栈,并执行相应的中断处理函数。中断栈并不是每个任务一份,而是每个 CPU 一份,因此中断上下文不属于任何任务。每次从中断返回的时候,有可能进行调度和任务切换。

每个任务都拥有自己的执行栈和异常栈,当一个任务处于运行状态时,它有可能正在运行栈上执行业务代码,也有可能在异常栈上处理异常,这两种状态都属于运行状态。运行状态下,如果产生中断,任务的当前上下文(PC、SP 以及其他寄存器)会被保存到当前栈(运行栈或者异常栈)上,切换到中断栈,执行中断处理代码。

在中断即将返回的时候,VxWorks 会检查目前是否需要执行调度和任务切换(RoundRobin 情况)。例如中断之前正在运行任务 A,中断之后需要切换到任务 B,那么我们在完成中断返回前,并不会从中断栈切换回任务 A 的运行/异常栈,而是切换到任务 B 的运行/异常栈。

调度(Scheduling)

调度算法