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即可,在此不再详述。