嵌入式操作系统
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

7.5 多CPU下关键区段的实现

7.5.1 多CPU环境下的实现方式

在多CPU(比如对称多处理SMP)环境下,情况要稍微复杂一些,因为不但要考虑在单CPU下可能发生的问题(线程竞争、中断竞争),还要考虑多个CPU的并发竞争问题。因此,仅仅采用关闭中断的方式来实现关键区段,已经不能满足要求。

这种情况下,一种可行的解决方案就是,为每个需要保护的全局数据结构设置一个保护标志,对这些全局数据进行修改时,首先检测该保护标志,如果该保护标志没有被设置(说明没有其他线程正在修改数据),再设置该保护标志,并进入修改数据的关键代码段,完成修改(即在关键代码段的最后)恢复该保护标志。如果试图修改全局数据的线程检测到保护标志被设置,则进入等待状态(忙等待),直到检测到该标志被释放(清除)为止。上述过程,可以采用伪代码进行如下描述:

    disable interrupt;              //Disable interrupt first.
    while(protecting flags==1);  //Waiting.
    Protecting flags=1;           //Set this flags, get lock.
    Modify global data structures;
    Protecting flags=0;           //Clear flags, release lock.
    Enable interrupt.

假设有两个线程A和B都试图修改同一个全局变量,A在修改前先禁止中断(这样就可以确保在A运行的CPU上不会发生线程切换和中断),然后检测全局变量的保护标志,由于此时没有其他线程在修改该全局变量,所以保护标志处于清除(空闲)状态,于是A设置保护标志为1(占有状态),并开始修改全局数据。

此时,线程B也试图修改全局数据,并执行跟A相同的过程:首先禁止本地中断(B所在的CPU的中断),然后检测保护标志,但这时候A正在修改全局数据,并已经设置了保护标志为1(占有状态),所以会导致线程B将一直处于检测状态(忙等待)。

当A完成对全局数据的修改后,恢复保护标志为0(释放保护标志),这时候,线程B会检测到保护标志被释放(线程B一直在检测该标志),于是线程B重新获得该标志,进入修改全局数据的过程。可以看出,采用这种方式可以很好地完成多个线程之间的同步。

细心的读者会发现,上述过程仍然是存在问题的。假设线程A在检测到保护标志空闲但还没有完成设置的时候,线程B也刚好执行到这里,发现保护标志空闲(因为A还没有完成设置),于是B也认为没有其他线程正在修改全局数据,这样就产生不一致了,A和B会同时进入修改全局数据的过程。

产生上述问题的原因,就是对保护标志的检测和设置是分开执行的,而不是作为一个整体的操作。解决上述问题单靠软件是不行的,需要靠CPU的支持,即需要CPU提供一种能够在一个原子操作内完成上述工作(检测和设置)的指令,这种指令就是有名的testing-and-setting指令。在Intel CPU中,可以采用BTS指令完成上述过程。

BTS指令的格式为:

BTS bitstr, bitoffset

其中,bitstr是一个bit串,而bitoffset则是一个偏移,指出了bitstr中的一个bit。该指令首先把bitstr中的第bitoffset个bit保存在EFLAGS寄存器的CF位中,然后设置bitstr中的第bitoffset个bit为1。所有这些操作都是原子的,即在操作的过程中不会发生中断(中断只是在指令的边界被引发)。在多CPU的环境下,可以在该指令的前面加上一个总线锁定指示符(LOCK),在该指令执行的时候锁定总线,这样在该指令执行的过程中,其他CPU也无法中断。在操作系统的实现中,采用该指令完成多个CPU之间的同步。

下面是用该指令完成保护的过程:

__TRY_AGAIN:
    LOCK BTS flags,0
    JC __TRY_AGAIN
    CRITICAL CODE

其中flags是保护全局数据的标记,BTS指令把flags中的第一个比特(比特0),读入CF标记位,然后设置flags的第一个bit为1。JC指令判断EFLAGS寄存器的CF标志是否为1,如果为1,则跳转到__TRY_AGAIN标号处执行,这样如果原来flags的第一bit为1(被占用),则上述指令会一直循环,如果一旦另外一个线程清除了flags的第一个bit(释放锁),则上述代码会检测到这个改变,然后进入CRITICAL CODE段执行。可以看出,上述过程就是一个不断检测并加锁的过程。LOCK指示符指示CPU在执行BTS指令的时候,锁住总线,这样就实现了多CPU下的原子操作。

保护标记的释放操作十分简单,只需要使用适当的指令清除保护标记的相关bit即可,在此不再详述。