VxWorks 代码分析
由于项目关系,有幸接触到了 VxWorks 6.7 的源代码,于是带着膜拜的心态开始阅读并分析其源代码,并将分析的结果记录在这里。
国内关于 VxWorks 的资料比较少,而且网上找到的资料不少是针对 VxWorks 5.x 的,和旧版比起来,VxWorks 6.x 增加了对多核硬件(SMP)的支持,并且加入了实时进程(RTP),可以开发用户态的应用程序,代码上的区别还是很大的。
VxWorks 代码的特点
VxWorks 主要使用 C 语言,BSP 部分包含一些汇编。使用 C89 标准语法,没有使用太多编译器扩展,代码风格很奇怪。在每个源文件开头的注释中,可以看到该文件的修改历史。可以看出,风河似乎很抗拒重构,特别愿意继承原有的代码,许多模块都是尽可能修改,哪怕重构之后反而会更简单。
作为一个实时操作系统,VxWorks 会放弃一定性能和通用性以追求更高的稳定性和可定制性。VxWorks 很多内核函数都要获取全局内核锁,多核环境下也如此。VxWorks 不会去检测硬件配置,例如探测内存大小、CPU 个数,而是要求开发者将这些配置硬编码,编译到 OS image 内部。这是因为 VxWorks 特殊的应用场合,基本都是针对某个硬件平台定制,这也说明很多 VxWorks 可以采用的方案在其他操作系统中完全不适用。
有些广告宣传说 VxWorks 是微内核,这是错的,VxWorks 是标准的宏内核。大部分系统组件和用户开发的 Task 都运行在特权模式,没有地址空间隔离。即使硬件平台支持 MMU,所有任务也是共享同一套页表,MMU 的作用仅仅是将碎片的物理页映射为连续的虚拟页。
VxWorks 模块化做得确实很好,从文件名来看,基本都是 xxxLib.c
、xxxLib.h
。每个模块相对独立,有明确定义的接口,头文件里也有详细的注释(开发文档就是用这些注释自动生成的)。
文件组织结构
VxWorks 并不是一个开源的操作系统,其源代码通常是随着开发套件一起安装的。VxWorks 的开发 IDE 是 Workbench,基于 Eclipse 定制,与源代码和其他辅助工具一起构成风河开发套件。假设风河开发套件安装在 C:\WindRiver
,那么 VxWorks 6.7 的源代码位于 C:\WindRiver\vxworks-6.7\target
。其中有如下子目录:
config
:这个目录中包括所有的 BSPcomps/vxWorks
:包含系统默认的组件定义文件(CDF)comps/src
:这个目录下包含 configlettes,也就是一些短小的配置代码
h
:VxWorks 内核头文件private
:私有头文件,只有src
中的代码能够访问make
:Makefile 片段文件,这些文件会被各个工程的 Makefile 中引用tool
:包含与工具链相关的 Makefile 片段,以及链接脚本
src
:VxWorks 内核的源文件lib
:这个目录保存编译生成的内核库(单核版本)lib_smp
:这个目录保存编译生成的内核库(多核版本)usr
:所有用户态相关的文件都放在这里(RTP)h
:用户态代码使用的头文件src
:用户层库的源文件lib
:保存编译生成的用户库
下面对代码结构中涉及到的一些概念进行解释:
- BSP(board support package)。VxWorks 是一个跨平台的操作系统,可以运行在 x86、PowerPC、ARM 等多种架构的硬件上。系统的核心代码是通用的,然而操作具体硬件的代码无法通用。因此将硬件相关的代码从系统中剥离出来,组成一个单独的模块,称作 BSP,每种硬件设备对应一个 BSP。针对某个设备构建系统,只需要将这个设备的 BSP 与通用代码一起编译即可;同时移植系统到新设备也更容易,只需要为这个新设备开发一套 BSP 即可。BSP 代码位于
target/config
目录下,每个子目录都表示一个 BSP。 - 组件和组件定义文件。VxWorks 是一个可定制化程度很高的系统,定制的单位就是组件(component)。每个组件都实现了某种功能,开发者可以根据项目的具体需求,决定添加哪些组件到系统中来。将更多的组建添加到系统中,可以让系统具有更多功能;将不需要的组件移除系统,能让生成的系统镜像更小,运行速度更快。Workbench 提供了一个图形化配置工具,通过它可以方便地配置项目中使用的组件,Workbench 会自动管理组件之间的依赖关系。组件的名称、相互依赖关系,以及配置项和 configlette 等信息被保存在组建定义文件(component definition file,简称 CDF)中,Workbench 正是通过 CDF 文件分析出系统中每个组件的信息,并给用户提供一套管理界面。CDF 文件位于
target/config/comps/vxWorks
目录下,后缀名为*.cdf
。 - configlette。从字面上看,configlette 的意思就是“小配置文件”,本质上就是一个代码文件。之所以要把这些代码特殊对待,是因为代码中使用了一些配置项,这些配置项只有在用户完成系统定制之后才能确定最终取值,在编译内核库的时候尚不能确定(关于内核库与构建流程将在后面介绍)。configlette 文件保存在
target/config/comps/src
目录下。
VxWorks内核的源代码就放在 src
目录下,这个目录下面又有许多子目录,按不同的功能模块组织分类。由于 VxWorks 系统经历过无数次的升级改进,src
下的内容显得有些凌乱,以下是几个核心子目录:
arch
,体系架构相关代码,例如arm
、ppc
、x86
等os
,包含系统核心代码,包括类和对象、内存管理、输入输出,以及 RTPwind
,包含任务管理和调度、信号量、消息队列。许多重要的内核 API 都定义在这个目录下drv
,传统驱动程序代码hwif
,这个目录下包含 VxBus 框架的驱动程序代码util
,包含一些通用数据结构和工具库的定义,例如AVL平衡树、链表、优先队列等,这个目录下定义的许多数据结构被其他模块使用
比较重要的两个是 wind
和 os
。wind
目录下包含任务管理相关代码,os
下则包含对象系统、内存管理等内容。这两个目录的功能看起来有些重合,区别在于,wind
模块提供的是公开的内核 API,函数接口是稳定的,而 os
模块提供的则是私有函数,供其他的 VxWorks 内核代码使用,风河不保证函数接口在不同系统版本之间保持一致。
从代码文件的命名方式上,就能大概看出属于哪个组件。例如内存管理组件 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 系统镜像的构建需要已经编译完成的内核库。而内核库正是 target/src
目录下的代码,俗称的 VxWorks 系统源代码,实际上就是内核库的源代码。编译内核库是通过 make 工具完成的,首先运行的是 target/src/Makefile
文件,这个文件又会递归调用 make 执行子目录下的 Makefile 文件,最终遍历到所有子目录。如果分析一下这些 Makefile 文件,会发现它们基本上都有这样的内容:
include $(TGT_DIR)/h/make/defs.library
include $(TGT_DIR)/h/make/rules.library
这两行引用了 target/h/make/defs.library
和 target/h/make/rules.library
两个文件,分别包含编译过程需要的变量定义和规则定义,相当于把相同的逻辑提取了出来,每个子目录只需要引用这些文件,并定义自己的变量即可。
概括起来,每个源码目录的编译过程非常简单,变量 OBJS
指定了需要编译的目标文件列表,变量 SUBDIRS
指定了需要递归执行 make 命令的子目录,如果 SUBDIRS
没有定义,那就遍历所有的子目录。也就是说,OBJS
决定了当前目录下的哪些文件需要编译,SUBDIRS
则决定哪些子目录需要递归。
每个子目录为一个单位,其中的所有目标文件会在编译完成后打包成一个静态库文件。静态库本质上就是一些目标文件组成的压缩包,因此编译内核库的过程就是将 target/src
目录下的每一个源文件编译成目标文件,存放在 target/lib_smp
中。
组件配置系统
VxWorks 系统中的各个组件都是可以选择添加的,用户通过 Workbench 可以对系统镜像项目中使用的组件进行配置。组件的配置情况并不会决定哪些文件参与编译,毕竟在构建系统镜像之前,内核库就已经编译完成了。组件的配置情况决定的是系统的启动过程,如果启用了某一个组件,那么就会在系统启动阶段调用这个组件的初始化函数。
组件配置情况通过四个文件体现出来:prjComps.h
、prjParams.h
、prjConfig.c
和 linkSyms.c
。
前两个头文件 prjComps.h
、prjParams.h
通过宏定义了当前配置下启用的每一个组件,并通过宏定义了用户配置的组件参数。这些宏会被 prjConfig.c
和 configlette 文件使用,正是因为使用到了构建镜像时才有的宏,configlette 被强行从内核源代码中分离出来,不和内核库一同编译,而是作为 VIP 项目代码的一部分。
prjConfig.c
则包含系统启动过程中执行的一系列函数,包括多任务环境启动之前执行的 usrInit
,以及多任务启动之后,以第一个任务身份运行的函数 usrRoot
。大量的组件都需要多任务环境,因此主要在 usrRoot
函数中初始化,而在多任务环境尚未建立的时候,只有一些基础核心组件可以初始化,如 cacheLib、objLib、objOwnerLib、objInfo、classLib、classListLib 等。
linkSyms.c
的内容则比较简单,定义了一个巨大的函数指针数组。由于在链接的时候,链接器会检查每个目标文件的引用数量。如果一个目标文件中没有函数会变量被其他文件引用,那么就说明这个目标文件是多余的,链接器便会把这个目标文件从最终的镜像文件中删除,以减小镜像文件的大小。然而有的时候,我们确实需要一些函数留在镜像中。链接的时候没有引用,并不表示运行时这个函数也不需要,更何况 VxWorks 允许用户通过命令行直接调用函数,还支持内核模块动态加载。为了避免一些关键模块被链接器优化掉,我们定义一个函数指针数组,将那些链接时没有引用但是又非常重要的函数放在数组中。这样强行添加一个引用,模块的引用数量就不再是零了,也就不会被链接器删除。
VxWorks 系统启动过程
在介绍 VxWorks 启动过程之前,首先补充一下系统镜像工程(VxWorks Image Project,VIP)的几种 Build Spec:
default
:默认选项,生成的镜像不含 bootloader 代码,需要专门的程序加载到 RAM 中才能运行。default_rom
:生成的镜像包含 bootloader 代码,可以加载到 ROM 中,内部的 bootloader 会自动复制内核到 RAM 并运行。default_romCompress
:类似default_rom
,但是对镜像进行了压缩,体积更小,复制到 RAM 之前需要解压。default_romResident
:类似default_rom
,但是系统镜像不需要复制到 RAM,可以直接在 ROM 中运行。
包含 bootloader 的镜像可以直接烧录到设备 ROM 中,适合于生产环境。不包含 bootloader 的镜像需要专门的引导器程序启动,但不需要烧录 ROM,可以网络引导,适合于开发环境。bootloader 代码位于 BSP 中的 romInit.s
文件内,负责将保存在 ROM 中的系统镜像复制到 RAM 中,如果启用了压缩,还需要对系统镜像解压。然后跳转到内核的入口点开始执行。bootloader 程序的入口点是 romInit
,内核入口点是 sysInit
。不管是否使用 bootloader,对内核而言,启动的过程是完全一致的。
sysInit
这个函数是 VxWorks 内核镜像最先开始运行的函数,代码位于 BSP 目录下的 sysALib.s
汇编文件中。源文件中,有着关于本函数功能的一段注释:
This is the system start-up entry point for VxWorks in RAM, the first code executed after booting. It disables interrupts, sets up the stack, and jumps to the C routine usrInit() in usrConfig.c.
可以看出,本函数的主要工作就是禁用中断、设置函数栈,然后跳转到 usrInit
函数,开始执行 C 语言代码。
如果分析以下 VIP 项目生成的系统镜像文件,就会发现 sysInit
函数位于代码段的开始位置,也就是 VxWorks 内核被加载到的位置。这个位置由宏 RAM_LOW_ADRS
控制,表示内核被加载到的位置。由于函数 sysInit
就位于内核镜像的开头位置,bootloader 与开发工具都能够非常方便地找到这个函数的地址,并控制 CPU 跳转过去。
当函数 usrInit
开始执行时,按顺序执行下列操作(usrInit
的执行过程与具体的处理器架构关系非常紧密,下面给出的是 SPARC 架构的情况):
- 禁用中断,通过写
PSR
寄存器,屏蔽 ET(Enable Trap)位实现。 - 加载 trap table,将 trap table 的地址写入
TBR
寄存器。 - 初始化寄存器窗,写
WIM
寄存器。 - 初始化所有的通用寄存器,包括每个窗之内的 local 与 input 寄存器。
- 启用 cache snooping,使用硬件机制保证 SMP 环境下的缓存一致。
- 将 trap tabl 复制到 8KB 之前的位置,并重新加载
TBR
。若之前TBR
位于0x40003000
,则复制之后位于0x40001000
。 - 调用函数
_leon23_checkcpu
,初始化_nwindows
和_nwindows_min1
变量。 - 最后跳转到
usrInit
函数中开始执行。
usrInit
这个函数是系统中最早开始运行的 C 语言代码,位于 VIP 项目下的 prjConfig.c
文件中,函数的代码由 Workbench 自动生成。
这个函数运行的时候,多任务环境尚未建立,因此在这一阶段,只能对一些不依赖多任务的模块进行初始化。例如 cacheLib、objLib、objOwnerLib、objInfo、classLib、classListLib 这些模块,都不依赖多任务,因此可以在 usrInit
中进行初始化。基本类和对象相关的模块都在这一阶段初始化,因为类和对象是 VxWorks 其他模块的基础,而且并不需要多任务环境。
除了初始化类和对象相关模块,在 usrInit
函数中还执行了如下操作:
- 调用
sysStart
函数,该函数将 bss 段清零,使之符合 C 语言的规范。 - 调用
usrCacheEnable
,启用指令缓存和数据缓存。 - 调用
usrKernelInit
,这个函数负责初始化任务管理,配置多任务环境,并且启动系统的第一个任务。
usrKernelInit
这个函数位于 target/config/comps/src/usrKernel.c
,由 usrInit
调用,执行过程为:
- 调用
kernelLockInit
,初始化内核锁 - 调用
taskLibInit
,初始化任务管理模块 - 配置调度策略
- 初始化 work queue,这是一个用于记录异步操作的队列
- 填充
_KERNEL_INIT_PARAMS
结构体,并调用kernelInit
启动多任务 - 因此主要的启动工作是在
kernelInit
函数中进行的。在_KERNEL_INIT_PARAMS
结构体中,指定根任务的入口点为usrRoot
函数
kernelInit
这个函数在 usrKernelInit
中调用,实现位于 src/wind/kernelLib.c
文件中。这个函数设置了内存分区,创建根任务的 TCB 对象,并且开始运行第一个任务。具体执行过程为:
- 根据 SMP/UP 以及不同硬件栈的生长方向,划分系统的内存区域
- 如果是 SMP,为每个处理器准备一个
tIdleTask
任务 - 为根任务和每个处理器的 Idle 任务准备执行栈和异常栈,这些栈所使用的内存空间直接从系统的可用内存中划分。这里不能使用内存管理模块进行分配,因为此时内存管理功能尚不可用
- 调用
taskInitExcStk
初始化根任务和每个处理器的 Idle 任务 - 调用
taskActivate
,启动根任务
usrRoot
usrRoot
函数是根任务的入口点,因此 usrRoot
执行时,VxWorks 的多任务执行环境已经建立,此时所有模块都可以初始化。
usrRoot
的实现位于 VIP 项目自动生成的 prjConfig.c
文件中,调用了大部分库的初始化函数。由于不同的 VIP 配置会导致不同的初始化步骤,因此 usrRoot
函数的内容在不同的 VIP 项目里存在一定差异。此外由于 projConfig.c
文件是 Workbench 自动生成的,因此不建议开发者手动编辑该文件。
在 usrRoot
函数的最后,通常会调用 usrAppInit
,执行用户编写的入口函数。usrAppInit
就相当于实时系统的 main 函数,用户编写的代码从这里执行。当函数 usrAppInit
退出之后,根任务便结束运行,但用户如果在 usrAppInit
中创建了新的任务,那么新创建的任务便会在根任务生命周期结束之后开始运行。到此 VxWorks 系统的初始化过程结束。
任务(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。理论上,异常处理程序完全可以在执行栈上运行,但有栈溢出的风险,因为初始栈指针位置不确定。异常栈也是每个任务一份,因此当一个任务正在执行异常处理代码的时候,也是可以进行任务切换的。
- 中断栈(Interrupt Stack),当中断发生的时候,ISR 会切换到这个栈,并执行相应的中断处理函数。中断栈并不是每个任务一份,而是每个 CPU 一份,因此中断上下文不属于任何任务。每次从中断返回的时候,有可能进行调度和任务切换。
如无特殊说明,提到“栈”,指的就是执行栈。
每个任务都拥有自己的执行栈和异常栈,当一个任务处于运行状态时,它有可能正在运行栈上执行业务代码,也有可能在异常栈上处理异常,这两种状态都属于运行状态。运行状态下,如果产生中断,任务的当前上下文(PC、SP 以及其他寄存器)会被保存到当前栈(运行栈或者异常栈)上,切换到中断栈,执行中断处理代码。
在中断即将返回的时候,VxWorks 会检查目前是否需要执行调度和任务切换(RoundRobin 情况)。例如中断之前正在运行任务 A,中断之后需要切换到任务 B,那么我们在完成中断返回前,并不会从中断栈切换回任务 A 的运行/异常栈,而是切换到任务 B 的运行/异常栈。
多核支持
Per-CPU Variables
VxWorks 不能运行时动态获取 CPU 数量,而是编译时指定。这样分配 per-CPU var 非常简单,只要把相关变量放在结构体中,有多少个 CPU,就声明多少个结构体变量。
perCPU 变量数组 vxKernelVars
,定义在 src/wind/kernelData.c
文件中。结构体类型为 WIND_VARS,定义在 h/private/windLibP.h。
h/private/windLibP.h
提供了许多宏,用来访问 vxKernelVars 成员,如:
- _WRS_KERNEL_CPU_GLOBAL_OR
- _WRS_KERNEL_CPU_GLOBAL_SET
自旋锁
VxWorks 使用自旋锁很大胆,根本不担心影响性能。而且 VxWorks 自旋锁是严格先到先得,以便获得确定性。
VxWorks 有一个全局锁 kernelStateLock
,位于文件 src/os/lock/kernelLockLib.c
。这个锁的结构如下(经过了简化):
struct vxKernelLock {
atomic_t ticket_counter;
atomic_t service_counter;
};
获取自旋锁的过程: - atomic fetch ticket_counter
and add one, original value is ticket
- spin while service_counter != ticket
释放自旋锁的过程: - atomic add one to service_counter
这种锁可以类比银行柜台取号: - 用户在取号机取号,取到的号记作 ticket
,同时取号机(下一个号)的数字加一。取号机便对应 ticket_counter
。 - 只有一个服务窗口,窗口上方显示正在处理哪个编号的客户,对应 service_counter
。 - 每个用户不断关注窗口上方的数字,一旦与自己手上的 ticket
一致,就坐到窗口前,表示得到了自旋锁。否则一直等待,即自旋。 - 拥有锁的用户完成了工作,准备释放锁,只需让窗口上方的数字加一。