将陆续上传新书《自己动手写CPU》,今天是第46篇。
在MIPS32指令集中有两条特殊的存储加载指令:链接加载指令LL、条件存储指令SC,本次将介绍这两条指令,在后续将实现这两条指令。
9.6 链接加载指令ll、条件存储指令sc说明
在本章前面的部分,笔者花费很多笔墨介绍了OpenMIPS中除ll、sc之外的加载、存储指令的实现过程,本节至9.9节将专门介绍链接加载指令ll、条件存储指令sc的实现过程。ll、sc指令是MIPS32指令集架构中比较特殊的加载存储指令,用来实现信号量机制。
在多线程系统中,需要RMW(Read-Modify-Write)操作序列保证对某个资源的独占性,RMW操作序列的含义是,读取内存某个地址的数据,读取的数据经过修改,然后再保存回内存原地址,这个过程不能有任何打扰,因此需要建立一个临界区域(Critical Region),临界区域中完成的操作通常称为原子操作,原子操作不被打扰。操作系统建立临界区域的方式通常是信号量机制,如下。
wait(semaphore);
原子操作;
signal(semaphore);
semaphore是一个信号量,为1表示信号量使用中,为0表示信号量空闲。进行原子操作前,使用wait函数查询semaphore的值,如果为1,则等待,否则,将其置为1,开始执行原子操作,操作结束后,signal函数将semaphore置为0,这样其它线程就可以执行原子操作了。
需要注意的是,wait函数的执行也是一个原子操作,是一种“先检测后设置”操作(test-and-set operation),这种操作一般不希望被外部设备中断,也不希望被其它线程打断,很多处理器都有专门的指令用来实现“先检测后设置”操作,比如:680x0 CPU、x86 CPU等。这也是一种信号量机制。
MIPS32架构采用特殊的方式实现信号量机制,对于原子操作,MIPS32架构并不保证它一定是原子性的,也就是允许检测和设置在没有原子性保证的情况下运行,但只在它确实原子的运行了的时候才让“设置”生效。MIPS32架构采用链接加载指令ll、条件存储指令sc来实现这种信号量机制。
ll指令同一般的加载指令一样,从内存中加载一个字,但是,有一点不同,ll指令还会将处理器内部的一个链接状态位LLbit置为1,表明发生了一个链接加载操作,并将链接加载的地址保存到一个特殊寄存器LLAddr中(这个寄存器在多处理器中有作用,OpenMIPS是单处理器,所以在OpenMIPS实现过程中并没有实现LLAddr寄存器)。
ll指令执行完毕后,会进行一定的操作(如:修改加载得到的数据),然后执行sc指令,这可以认为是一个RMW序列。有如下两种情况干扰这个RMW序列,受到干扰后,处理器会设置链接状态位LLbit为0。
- 在ll、sc指令之间产生异常,从而进入异常处理例程,或者发生线程切换,导致RMW序列受到干扰。
- 多处理器的系统中,另一个CPU改写了RMW序列要操作的内存空间。
对于OpenMIPS而言,只有第1种情况。
执行sc指令时,会对从ll指令开始的RMW序列进行检查,判断是否受到干扰,实际就是判断LLbit是否为1,如果没有受到任何干扰,LLbit保持为1,那么操作是原子的,sc指令会对ll指令加载数据的地址进行写回操作,并设置一个通用寄存器的值为1,表示成功,反之不进行写回操作,并设置一个通用寄存器的值为0,表示失败。
ll、sc指令的格式如图9-28所示。从图中可知,可以依据指令码对这2条指令进行区分。
- 当指令中的指令码为6‘b110000时,是ll指令,链接加载指令
指令用法为:ll rt, offset(base)
指令作用为:从内存中指定的加载地址处,读取一个字节,然后符号扩展至32位,保存到地址为rt的通用寄存器中。其中加载地址的计算方法如下。
加载地址 = signed_extended(offset) + GPR[base]
此外,还要设置链接状态位LLbit为1。
- 当指令中的指令码为6‘b111000时,是sc指令,条件存储指令
指令用法为:sc rt, offset(base)
指令作用为:如果RMW序列没有受到干扰,也就是LLbit为1,那么将地址为rt的通用寄存器的值保存到内存中指定的存储地址处,同时设置地址为rt的通用寄存器的值为1,设置LLbit为0。如果RMW序列受到了干扰,也就是LLbit为0,那么不修改内存,同时设置地址为rt的通用寄存器的值为0。其中存储地址的计算方法如下。
存储地址 = signed_extended(offset) + GPR[base]
下面通过一个例子体会ll、sc指令的作用,这个例子实现了上面介绍的wait函数,不过此处是使用ll、sc指令实现的。
wait: ori $1, $0, sem // sem是信号量的地址,将这个地址赋给寄存器$1 TryAgain: ll $2, 0($1) // 获取信号量的值,保存到寄存器$2 bne $2, $0, WaitForSem // 如果信号量被占用(其值为1),那么转移到地址WaitForSem // 继续等待;如果信号量空闲(其值为0),那么执行下面的指令 nop ori $2, $0, 1 sc $2, 0($1) // 如果没有被干扰,那么设置信号量被占用(将1保存到信号 // 量中),同时,设置寄存器$2为1,反之,不修改信号量, // 设置寄存器$2为0 beq $2, $0, TryAgain // 如果寄存器$2为0,表示ll、sc指令没有成功,未获取到 // 信号量,回到TryAgain继续尝试 nop jr $31 // 反之, 表示ll、sc指令成功,获取到信号量,可以进入 // “临界区域”了。调用wait函数时,会将返回地址放在 // 寄存器$31,所以此处jr $31指令就是回到调用过程, // 进入临界区域
下一次将修改OpenMIPS以实现LL、SC指令