將陸續上傳新書《自己動手寫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指令