第4章 RTOS的SMP扩展设计与实现
单核RTOS的SMP扩展,需要解决系统初始化、任务调度、CPU间的同步与互斥、以及中断处理等方面问题,本文将针对这些问题提出相应的解决方案,并在uC/OS-II上进行SMP扩展实现。
4.1 系统初始化
虽然SMP系统中CPU间的地位是平等的,没有主次之分,但是由于在系统初始化期间只有一个“执行上下文”,因此只能有一个CPU来引导,我们把这个CPU称为引导CPU(Bootstrap
CPU),有时也称之为主CPU;其它CPU为应用CPU(Application
CPU),有时也称之为次CPU。主CPU完成内部寄存器、MMU管理机制、以及操作系统内核的初始化后,会为每一个CPU创建一个idle任务,然后依次启动系统中的各个CPU。当所有次CPU被激活完成各自初始化后,所有次CPU开始运行idle任务,接受主CPU的调度,接着由统一的调度器在多个CPU间调度任务,至此所有CPU处于完全平等的地位,系统启动完毕。SMP启动时序如图4-2所示。
在MPC8641D平台uC/OS-II-SMP初始化流程如图4-2所示。
在系统初始化期间,主CPU在OSInit()函数中不仅创建了与自己绑定的idle任务,而且还创建与次CPU绑定的idle任务。主CPU首先通过执行Start.S中的指令初始化内部寄存器、相关硬件环境、以及自己的栈指针,然后跳转到c_main()函数中初始化MMU、串口及时钟,最后启动初始任务KernelStart。由初始任务KernelStart向次CPU发送启动信号,次CPU获得启动信号后也会跳到Start.S中执行相关的汇编指令来初始化自身寄存器,接着跳转到secondary_start()函数中进一步对次CPU的MMU寄存器进行初始化,使次CPU可以访问所有内存资源,最后次CPU运行绑定在该CPU上的idle任务,等待调度器调度。
Start.S文件根据主CPU和次CPU的ID号来让不同的CPU执行各自相关的汇编指令序列。其关键代码如下:
#if CONFIG_OS_SMP
mfspr r3, MSSCR0
isync
andi. r3, r3, 0x20
srwi r3, r3, 5
mtspr PIR, r3
//初始化CPU的ID编号,主CPU初始化为0,次CPU初始化为1
mtspr 279, r3
mfspr r3, MSSCR0
isync
rlwinm. r3, r3, 0, 26, 26
beq doCpu0Stack
lis sp,
HI(CPU1_STACK_START_ADR)
addi sp, sp,
LO(CPU1_STACK_START_ADR)
b doneCpu0Stack
#endif
doCpu0Stack:
lis sp, HI(RAM_LOW_ADRS)
addi sp, sp,
LO(RAM_LOW_ADRS)
doneCpu0Stack:
……
上面的这段汇编指令根据CPU
ID号来决定主CPU和次CPU各自执行的指令序列。例如主CPU将会执行doCpu0Stack和doneCpu0Stack之间的指令,将自身的堆栈寄存器初始化为RAM_LOW_ADRS,而次CPU的堆栈寄存器将被初始化为CPU1_STACK_START_ADR。
由于主CPU和次CPU的栈指针值不同,各自跳转的C函数也不同,主CPU跳转到c_main()中执行,次CPU跳转到secondary_start()函数中执行。其关键代码如下:
#if CONFIG_OS_SMP
mfspr r3, MSSCR0
isync
rlwinm. r3, r3, 0, 26, 26
beq bootUsrInit
b
secondary_start//从CPU跳转到secondary_start()函数中无需返回
#endif
bootUsrInit:
b c_main //跳转到c_main函数,无需返回
上面的这段汇编指令根据当前CPU
ID来确定CPU的跳转入口函数。主CPU跳转到c_main()函数中:
void c_main (void)
{
bsp_module_init();//初始化板级支持包:mmu,时钟,串口等底层硬件
kernelLockInit(); //初始化全局内核锁
OSPrintk("OS c_main Entry Poits: 0x%x\n",c_main);
OSInit();
OSTaskCreateExt(KernelStart,
//创建一个最高优先级的任务
(void *)0,
//不带有参数
(OS_STK
*)&AppStartTaskStk[APP_TASK_START_STK_SIZE-1],
//提供分配的堆栈空间
APP_KERNEL_START_PRIO,
//定义优先级为最高:0
APP_KERNEL_START_PRIO,
(OS_STK
*)&AppStartTaskStk[0],
APP_TASK_START_STK_SIZE,
(void *)0,
OS_TASK_OPT_STK_CHK |
OS_TASK_OPT_STK_CLR);
OSStart();
//启动多任务,其实就是启动KernelStart,在KernelStart任务中开启次CPU
}
次CPU跳转至secondary_start()函数中:
void secondary_start(void)
{
BSP_InitMMU();
//初始化次CPU的MMU
core1_pic_init();//初始化次CPU的中断控制器
while(!just_ack);
APStart();
//恢复与次CPU绑定的任务上下文
}
4.2 共享资源的可重入化
可重入资源指的是该资源可以被多个CPU访问,而不用担心资源的核心数据被破坏。共享资源的可重入化指的是采用必要的手段来对共享资源进行保护,使其数据不因多CPU的访问而出现不一致。
对任何一款单核RTOS进行SMP扩展,必须解决如何协调CPU间的并行活动以避免破坏内核数据结构,其本质就是实现对系统全局共享资源的可重入化。本文提出使用原子操作、内存屏障、自旋锁、以及内核锁机制实现全局共享资源的可重入化。
4.2.1 原子操作
在单CPU环境下,由若干条汇编指令组成的具有“读-修改-更新”类型的汇编序列,它们在访问内存时具有第一次读原值,第二次写新值的特性。这类汇编指令序列在单CPU的环境下,只需使用中断锁就能保证操作序列的原子性,即汇编序列的执行不会被中断。但是在SMP环境下,“读-修改-更新”类指令序列的每一条指令都是一个微操作,即使是关中断也无法保证“读-修改-更新”操作的原子性。
例如两个CPU都试图读取同一个内存单元,但是存储器仲裁器只允许其中一个CPU访问,另一个CPU延迟访问,这样当第一个读操作完成后,延迟操作的CPU和前一个CPU读到的都是同一个原值,但是当两个CPU都试图向同一个内存单元写入一个新值时,虽然两次写操作被存储器仲裁器串行化后都可以写成功,但是全局结果却是不对的。避免由于“读-修改-更新”类指令序列操作产生错误的最容易的方法就是确保这类操作序列在芯片级是原子的,即“读-修改-更新”指令序列的执行过程不会中断。在MPC8641D硬件平台,我们利用该平台提供的“lwarx”和“stwcx.”来实现原子操作,例如原子操作atomic_add(atomic_t
*v,int a)函数实现如下:
//
//原子的实现两个数的和操作
//
atomic_t atomic_add(atomic_t *v,int
a)
{
atomic_t t;
__asm__ __volatile__(
" isync \n\
1: lwarx %0,0,%2 #
atomic_add\n\
add %0,%1,%0\n"
" stwcx. %0,0,%2 \n\
bne- 1b \n\
isync"
: "=&r" (t)
: "r" (a), "r" (v)
: "cc", "memory");
return;
}
再如atomic_case(compre and set)操作的实现:
//
//atomic_ case:compare and
set,将old_value和*lock比较
//如果old_value和*lock的值相等,则将*slock的值置为new,返回
true
//如果old_value和*lock的值不相等,则返回false
//
atiomic_t atomic_case(atiomic_t *lock,
atiomic_t old_value, atiomic_t
new_value)
{
unsigned int tmp;
__asm__ __volatile__(
" isync\n\
1: lwarx
%0,0,%2\n\
cmpw 0,%0,%1\n\
bne- 2f\n\
stwcx. %3,0,%2\n\
bne- 1b\n\
isync \n\
2:"
: "=&r"
(tmp)
: "r" (old_value), "r"
(lock),"r"(new_value)
: "cr0", "memory");
return tmp==old_value;
}
利用上面的两个原子操作,我们就可以实现RTOS中全局变量的可重入化,例如在单核uC/OS-II中对全局变量OSIdleCtr的自加操作OSIdleCtr++,在SMP环境下可以用atomic_add(&OSIdleCtr,1)进行扩展。后面我们将看到,SMP环境下的自旋锁可以用原子操作atomic_case()来实现。
在uC/OS-II的SMP扩展中,我们实现的原子操作接口如下表所示:
4.2.2 内存屏障
一方面由于使用编译器的优化策略,使得编译器会重新安排汇编指令的执行顺序;另一方面在SMP硬件平台下,CPU间会并行的执行若干条操作,导致可能会重新安排指令访问内存的顺序。例如一个典型的序列问题涉及到两个CPU,在一个CPU上准备好一项工作后,它会设置一个布尔型的变量为真来通知另外一个正在等待该工作的CPU。其代码如下:
// CPU 0 - announce the availability of
work
pWork = &work_item; //
store pointer to work item to be performed
workAvailable = 1;
// CPU 1 - wait for work to be
performed
while (!workAvailable);
doWork (pWork); // error - pWork might
not be visible to this CPU yet
由于CPU
0对写操作的重排序,将导致操作“workAvailable = 1”会在“pWork =
&work_item”之前执行,这样会导致CPU
1在pWork变量更新前,观察到workAvailable的改变,这样CPU1引用的pWork指针就会指向一个不正确的值。为了解决SMP系统中出现的这个问题,内存屏障应运而生,内存屏障可以被分成三类。
(1)读内存屏障memory_barriar_r()
该屏障保证所有在该屏障之前的读操作执行完毕之后,屏障之后的读操作才可以执行。如果没有内存屏障,CPU可以自由地对等待的读操作重新排序,从单核处理器的角度看这种排序不会影响程序执行的正确性;但在SMP环境下就会出现上面的问题。
例如下面两个操作在插入读内存屏障之前执行的顺序是不确定的
a = *pAvalue; // read may occur _after_
read of *pBvalue
b = *pBvalue; // read may occur _before
read of *pAValue
在这两个操作之间插入读内存屏障之后,就可以保证读操作以确定的顺序执行。
a = *pAvalue; // will occur before read
of *pBvalue
memory_barriar_r ();
b = *pBvalue; // will occur after read
of *pAvalue
(2)写内存屏障memory_barriar_w()
写内存屏障保证该屏障之前的写操作执行完毕之后,其后的写操作才可以运行。仅仅使用读内存屏障虽然保证了读操作的顺序,但是意义不大。只有和写内存屏障相配套使用才有实际意义。上面的示例我们用写内存屏障来保证两个赋值操作正确的执行顺序如下。
pWork =
&work_item;
memory_barriar_w ();
workAvailable = 1;
(3)完全(读/写)内存屏障memory_barriar_rw ( )
读写内存屏障也叫完全内存屏障,它是读屏障和写屏障的组合,即把读屏障和写屏障两条操作组合成了单个操作。在一些系统上读写屏障可能会比前面两个内存屏障的开销大些。除非要同时保证读顺序和写顺序,否则不建议使用读写屏障。在本课题研究的SBC8641D硬件平台上,上述三类内存屏障的实现如下:
#define _barriar(x)
__asm volatile (x ::: "memory")
#define memory_barriar_r
_ barriar
("sync");
#define memory_barriar_w
_ barriar
("sync");
#define memory_barriar_rw
_ barriar
("sync");
4.2.3 核间中断
MPC8641D集成了OpenPIC可编程中断控制器来,因此我们可以通过配置OpenPIC的4组核间中断寄存器来实现CPU间的通信。在MPC8641D处理器平台上,OpenPIC中断控制器和内存是统一编址的,SBC8641D平台中OpenPIC对应的地址空间为:
- 0xF804 0000至0xF804 FFF0:全局配置寄存器;
- 0xF805 0000 至 0xF805FFF0:中断源配置寄存器;
- 0xF806 0000 至 0xF806FFF0;Per-CPU寄存器;
其中4组核间中断寄存器IPIDR(Interprocessor Interrupt Dispatch
Register)的地址空间如表4-2所示。
每个CPU的IPIDR(Interprocessor Interrupt Dispatch
Register)寄存器的低两bit位代表着MPC8641D的CPU0和CPU1,如果CPU 0给CPU
1发中断只需要把自己的IPIDR寄存器的bit 1位置1,如果CPU0给自己发核间中断只需要把IPIDR寄存器的bit
0置1(前提是核间中断不被屏蔽)。
本文配置4组核间中断的向量号为81~84,MPC8641D平台支持11个外部中断、48个内部中断、以及其它中断的向量号分配如下:
//全局中断号定义
#define OPENPIC_VEC_EXT_BASE 1
// 1 - 12
#define OPENPIC_VEC_INTERNAL_BASE 13
// 13 - 60
#define OPENPIC_VEC_MSG_BASE 61
// 61 - 64
#define OPENPIC_VEC_SMSG_BASE 65
// 65 - 72
#define OPENPIC_VEC_TIMERA_BASE
73
// 73 - 76
#define OPENPIC_VEC_TIMERB_BASE
77
// 77 - 80
#define OPENPIC_VEC_IPI_BASE 81
// 81 - 84
对OpenPIC中断控制器中配置寄存器的初始化代码如下:
void openpic_init(void)
{
//
初始化外部中断:中断优先级为12,高电平触发
for(i = OPENPIC_VEC_EXT_BASE; i
< (OPENPIC_VEC_EXT_BASE + OPENPIC_NUM_EXTERNAL);
i++)
{
openpic_init_irq(i, IRQ_PRI_12, i, 1,
1);
openpic_map_irq(i, TO_CPU0,
TO_DEFAULT);
}
//
初始化内部中断:中断优先级为5,高电平触发
for(i = OPENPIC_VEC_INTERNAL_BASE; i
< (OPENPIC_VEC_INTERNAL_BASE +
OPENPIC_NUM_INTERNAL); i++)
{
openpic_init_irq(i, IRQ_PRI_5, i, 1,
0);
openpic_map_irq(i, TO_CPU0,
TO_DEFAULT);
}
// MSG中断, SMSG中断:
中断优先级为8
for(i = OPENPIC_VEC_MSG_BASE; i
< (OPENPIC_VEC_MSG_BASE + OPENPIC_NUM_MSG);
i++)
{
openpic_init_irq(i, IRQ_PRI_8, i, 0,
0);
openpic_map_irq(i, TO_CPU0,
TO_DEFAULT);
}
for(i = OPENPIC_VEC_SMSG_BASE; i
< (OPENPIC_VEC_SMSG_BASE + OPENPIC_NUM_SMSG);
i++)
{
openpic_init_irq(i, IRQ_PRI_8, i, 0,
0);
openpic_map_irq(i, TO_NONE,
TO_DEFAULT);
}
// 初始化定时器中断组A,
组B
for(i = OPENPIC_VEC_TIMERA_BASE; i
< OPENPIC_VEC_TIMERA_BASE + OPENPIC_NUM_SMSG;
i++)
{
openpic_init_irq(i, IRQ_PRI_0, i, 0,
0);
openpic_map_irq(i, TO_NONE,
TO_DEFAULT);
}
//
初始化CPU交互中断
#if CONFIG_OS_SMP//初始化核间中断
for(i = OPENPIC_VEC_IPI_BASE; i
< (OPENPIC_VEC_IPI_BASE + OPENPIC_NUM_IPI);
i++)
{
openpic_initipi(i-OPENPIC_VEC_IPI_BASE,
IRQ_PRI_8, i);
}
#endif
//
初始化虚假中断中断号
openpic_set_spurious(OPENPIC_VEC_SPURIOUS);
// Set the lowest priority for this
process
openpic_set_priority(0);
// Set Mix
mode
openpic_setfield(OPENPIC_REG_GCR0,
OPENPIC_CONFIG_8259_PASSTHROUGH_DISABLE);
// Clear all ISR
pending
while(openpic_iack() !=
0xff)
{
openpic_eoi();
}
openpic_set_priority(1);
openpic_write((volatile u32
*)&OpenPIC->global.ext.mer,
0xf);
}
如代码所示,我们为四组和间中断IPIDR分配向量号为81~84,目前只使用了一组IPIDR寄存器,即配置的81号中断寄存器。发送核间中断的代码如下:
void IPI_send(int cpuid)
{
// OSPrintk("IPI send
start\n");
openpic_enable_ipi(81);//清除81号IPIDR的屏蔽位
openpic_cause_IPI(0,cpuid+1);//向CPU
cpuid发核间中断
// OSPrintk("IPI send
end\n");
}
其中cpuid为发送核间中断到目标CPU
的ID号。
- 当cpuid=0时,向CPU 0核间中断;
- 当cpuid=1时,向CPU 1发送核间中断;
- 当cpuid=2时,同时向CPU0和CPU1发送核间中断。
核间中断处理流程类似图3-4中断务处理流程。我们发送核间中断的目的是希望目标CPU执行中断级调度器来使得更高优先级的任务有机会运行,这在和间中断返回时调用中断级调度器OSIntExit()实现,所以核间中断处理函数不需要做任何事情,我们需要的是这一例行公事般的流程。关于IPI_send()函数的使用,我们会在4.3.5节、以及4.3.6节进一步的讨论。
4.2.4 内核锁
uC/OS-II的中断锁不仅实现了阻止中断ISR访问临界资源,还阻止了更高优先级的任务抢占CPU来访问该临界资源;但是在SMP环境下,仅通过关本CPU上的中断只能保证本CPU上ISR和任务之间的互斥,其它CPU仍可访问该临界区,SMP硬件平台多个CPU的存在根本上改变了uC/OS-II中断锁的语义。
因此我们迫切需要实现一种新的锁机制来实现SMP环境下CPU之间的互斥,这样结合中断锁,既能实现当前CPU上ISR与任务之间的互斥,也能实现与其它CPU上的ISR和任务之间的互斥,其示意如图4-3。
4.2.4.1 自旋锁的设计
为了保证CPU间的互斥,我们首先想到使用自旋锁[25]。自旋锁使用存储在共享内存上的一个标志作为开关,所有CPU对此开关的操作都是原子性的(通过atomic_case()实现),从而保证系统同一时间只有一个CPU能获得锁,如果CPU获取不到该自旋锁,它会一直在锁附近自旋等待。其执行流程如图4-4。
SBC8641D平台我们用4.2.1节设计的原子操作atomci_case()实现:
atomic_t
flag=0;
void spin_lock ()//获取自旋锁
{
while(1){
while(atomic_get(&flag);
if(atomic_case(&flag, 0,1) ==
TRUE)
return;
} // end while(1)
}
void spin_unlock ()//释放自旋锁
{
atomic_set(&flag,1);
}
但是在实际测试中我们发现这种自旋锁机制存在两个问题:
-
多个CPU同时申请自旋锁时会导致某个CPU一直获取不到自旋锁,产生“饥饿”现象,产生这一问题的根源是自旋锁的竞争缺乏公平机制;
- 自旋锁释放时会修改flag变量,导致系统中所有CPU的Cache都需要刷新,由Cache一致性引发的损失也不容小觑。
对于自旋锁的饥饿问题,我们可以通过限制CPU自旋的次数来解决,即当CPU自旋一定的次数之后,发现自己仍然无法获得自旋锁,则主动放弃申请自旋锁,去做别的事情。这类自旋锁一般称为实时自旋锁,但是这种自旋锁并没有从本质上解决CPU饥饿问题,因此需要设计一种新的锁机制。
4.2.4.2 基于ticket的内核锁设计
为了解决上面的问题,我们提出了基于ticket的锁机制。其基本思想是:每一个申请锁的CPU必须首先获取一个“ticket”,系统根据“ticket”的值依次满足每一个CPU的请求,当CPU的“ticket”与系统允许处理的“ticket”相同时,该CPU获得锁。当CPU使用完锁后,将会释放给下一个持有“ticket”等待锁的CPU。我们用代码来描述一下这种ticket锁设计思想。
基本数据结构:
struct
ticketlock_t
{
atomic_t
nextTicket;
//
下一个ticket值
atomic_t
ticketInService;
//拿到锁的ticket值
}
获取锁:
void
ticket_lock(spinlock_t *lock)
{
atomic_t
myticket;
myticket =
atomic_inc(&lock->nextTicket);
//spinning waiting for task
turn
while(myticket !=
atomic_get(&lock->ticketInService));
memory_barria_rw(); //
SMP中内存屏障必须成对使用
}
释放锁:
void ticket_unlock(spinlock_t
*lock)
{
unsigned int
ticket;
ticket =
lock->ticketInService;
memory_barria_rw();//
SMP中内存屏障必须成对使用
//release the lock to the next request
task
atomic_set(&lock->ticketInService,
ticket+1);
}
这种新的锁机制相对于自旋锁有两个优势:
- 锁释放时只会影响其直接后继CPU,减少了由于cache一致性导致的性能损失;
- 提供FCFS(First Come First Serve)公平性,杜绝饥饿发生。
因此本文在uC/OS-II的SMP扩展中,采用ticket锁的思想来设计内核锁实现CPU间的互斥。之所以把这个锁命名为内核锁,其含义是在uC/OS-II内核中使用,并不是用来锁整个uC/OS-II内核。
4.2.4.3 uC/OS-II-SMP中内核锁实现
基本数据结构:
typedef struct KernelLock
{
atomic_t
tkt_ctr;
//ticket计数器
int
cpuIndex;
//拥有内核锁的CPU ID
atomic_t
svc_ctr;
//当前正在被服务的ticket值
} KERNEL_LOCK_VAR;
定义一个全局的内核锁kernelStateLock:
KERNEL_LOCK_VAR
kernelStateLock;
//定义全局内核锁kernelStateLock
再定义一个全局Per-CPU变量,使得每个CPU都有一个ticket域来保存各自的“ticket”值:
typedef struct
_uC_Vars
{
atomicVal_t cpu_my_tkt; //用于存放获取内核锁的ticket值
} _uCOS_VARS;
typedef struct Vars
{
_uCOS_VARS vars;
}_VARS;
_VARS
KernelVars[CONFIG_NR_CPUS];
KernelVars是一个Per-CPU变量,每个CPU都有自己私有的域用来存放cpu_my_tkt“票号”,每个CPU通过CPU
ID访问自己的域值,为了便于操作我们定义了两个宏:
//用于设置当前CPU的cpu_my_tkt值的宏操作
# define _KERNEL_CPU_GLOBAL_SET(cpuid,
glob, var) \
KernelVars[cpuid].vars.cpu_##glob =
var
//用于读取当前CPU的cpu_my_tkt宏操作
# define _KERNEL_CPU_GLOBAL_GET(cpuid,
glob)
\
KernelVars[cpuid].vars.cpu_##glob
实现操作接口如下:
void
kernelLockInit
(void);//初始化全局内核锁
void
kernelLockTake(void);
//获取内核锁
void
kernelLockGive(void);
//释放内核锁
(1)获取全局内核锁kernelStateLock()
采用ticket算法,每一个申请内核锁的CPU都会先使用自己的cpu_my_tkt“票号”,当cpu_my_tkt“票号”为0时,说明已经被使用过,该CPU会重新获取一个“票号”并同时更新cpu_my_tkt,然后尝试使用获得的“票号”去获取内核锁。如果成功,则返回;若失败,则开中断,再重新尝试直到获得锁为止,执行流程如图4-5。
具体的代码如下:
void KernelLockTake(void)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
int cpuIndex;
atomic_t myTicket;
while (1)//当内核锁不可用时,当前CPU自旋等待
{
//当前CPU关中断有两个作用:一方面防止当前CPU上任务迁移,另一方面确保
//定义的两个宏操作可以原子的进行
OS_ENTER_CRITICAL();/
cpuIndex =
ProcessorID();//读取当前CPU的ID
//读取当前CPU票号
myTicket =
_KERNEL_CPU_GLOBAL_GET(cpuIndex, my_tkt);
if (myTicket ==
0)//检查票是否已经使用过,如果票已经使用过,则重新申请
{
myTicket =
atomic_add_return(&kernelStateLock.tkt_ctr,
2);
_KERNEL_CPU_GLOBAL_SET (cpuIndex, my_tkt,
myTicket);
}
if (myTicket ==
atomic_get(&kernelStateLock.svc_ctr))
break; //
success
OS_EXIT_CRITICAL();
}
memory_barrier_rw();//确保上面对全局变量的修改对其它CPU可见
kernelStateLock.cpuIndex = cpuIndex;
OS_EXIT_CRITICAL();;
return;
}
(2)释放内核锁kernelLockGive()
释放kernellock时,只需要将cpu_my_tkt设置为0,表示已经消耗了预留的“票号”,并且将kernellock中current_ticket设置为下一个等待的“票号”即可。这一过程在锁中断的条件下执行,执行流程如图4-6。
具体代码如下:
void kernelLockGive (void)
{
#if OS_CRITICAL_METHOD ==
3
OS_CPU_SR cpu_sr;
#endif
int cpuIndex;
atomic_t svcCtr;
OS_ENTER_CRITICAL();
cpuIndex = kernelStateLock.cpuIndex;
_KERNEL_CPU_GLOBAL_SET (cpuIndex, my_tkt, 0);
svcCtr = kernelStateLock.svc_ctr;
kernelStateLock.cpuIndex = -1;
memory_barrier_rw();
(void)atomic_set(&kernelStateLock.svc_ctr, svcCtr +
2);
OS_EXIT_CRITICAL();
}
这样在SMP环境下我们为了保持uC/OS-II关中断语义,只需要在OS_ENTER_CRITICAL
()后面加上kernelLockTake(),在OS_EXIT_CRITICAL
()前面加上kernelLockGive()即可。这是因为关中断阻止了本CPU上的ISR或者任务访问该临界资源,而内核锁又阻止了其它CPU上的任务或者ISR访问临界资源,从而达到了在SMP环境下对全局互斥资源进行保护目的,实现了全局互斥资源的可重入化。
待续。。。。
|