ARM64的启动过程之(三):为打开MMU而进行的CPU初始化
原文地址:http://www.wowotech.net/linux_kenrel/__cpu_setup.html
一、前言
上一節主要描述了為了打開MMU而進行的Translation table的建立,本文延續之前的話題,主要是進行CPU的初始化(注:該初始化僅僅為是為了turn on MMU)。
本文主要分析ARM64初始化過程中的__cpu_setup函數,代碼位于arch/arm64/mm/proc.S中。主要的內容包括:
1、cache和TLB的處理
2、Memory attributes lookup table的構建
3、SCTLR_EL1、TCR_EL1的設定
二、cache和TLB的處理
1、oerview
根據ARM64 boot protocol,我們知道,會bootloader將內核解壓并copy到RAM中,同時將CPU core(BSP)的狀態設定為:關閉MMU,disable D-cache,I-cache狀態可以是enable,也可以是disable的。其實在bootloader將控制權交給Kernel之前,bootloader已經走過千山萬水,為了性能,很可能是打開了MMU以及各種cache,只是在進入kernel的時候,受限于ARM64 boot protocol而將CPU以及cache、MMU等硬件狀態設定為指定的狀態。因此,實際上這時候,instruction cache以及TLB中很可能有殘留的數據,因此需要將其清除。
2、如何清除instruction cache的數據?
聽起來這個問題似乎有點愚蠢,實際上不是。隨著人類不斷向更快的計算機系統進發,memory hierarchy也變得異常復雜起來,cache也形成了cache hierarchy(ARMv8最大支持7個level,L1~L7),不同級別的cache中都包含了部分下一級cache(或者main memory)的內容。這時候,維護數據一致性變得復雜了,例如:當要操作(例如clean或者invalidate)某個地址對應的cacheline的時候,是僅僅操作L1還是覆蓋L1和L2,異或將L1~L3中對應的cacheline都設置為無效呢?PoU(Point of Unification)和PoC(Point of Coherency)這兩個術語就是用來定義cache操作范圍的,它們其實都是用來描述計算機系統中memory hierarchy的一個具體的“點”,操作范圍是從PE到該點的所有的memory level。
我們先看PoU,PoU是以一個特定的PE(該PE執行了cache相關的指令)為視角。PE需要透過各級cache(涉及instruction cache、data cache和translation table walk)來訪問main memory,這些操作在memory hierarchy的某個點上(或者說某個level上)會訪問同一個copy,那么這個點就是該PE的Point of Unification。假設一個4核cpu,每個core都有自己的L1 instruction cache和L1 Data cache,所有的core共享L2 cache。在這樣的一個系統中,PoU就是L2 cache,只有在該點上,特定PE的instruction cache、data cache和translation table walk硬件單元訪問memory的時候看到的是同一個copy。
PoC可以認為是Point of System,它和PoU的概念類似,只不過PoC是以系統中所有的agent(bus master,又叫做observer,包括CPU、DMA engine等)為視角,這些agents在進行memory access的時候看到的是同一個copy的那個“點”。例如上一段文章中的4核cpu例子,如果系統中還有一個DMA controller和main memory(DRAM)通過bus連接起來,在這樣的一個系統中,PoC就是main memory這個level,因為DMA controller不通過cache訪問memory,因此看到同一個copy的位置只能是main memory了。
之所以區分PoC和PoU,根本原因是為了更好的利用cache中的數據,提高性能。OK,我們回到本節開始的問題:如何清除instruction cache的數據?我們還是用一個具體的例子來描述好了:對于一個PoU是L2 cache的系統,清除操作應該到哪一個level?根據ARM64 boot protocol規定,kernel image對應的VA會被cleaned to PoC,這時候,各級的data cache的數據都是一致性的,按理說,BSP只需要清除本cpu core上的instruction cache就OK了。不過代碼使用了PoU,也就是說操作到了L2,而實際上,L2是unified cache,其數據是有效的,清除了會影響性能,這里我也想的不是很清楚,先存疑吧。
3、代碼解析
ENTRY(__cpu_setup)?
??? ic??? iallu --------------------------------(1)?
??? tlbi??? vmalle1is------------------------------(2)?
??? dsb??? ish --------------------------------(3)
??? mov??? x0, #3 << 20 ----------------------------(4)?
??? msr??? cpacr_el1, x0??????????? // Enable FP/ASIMD?
??? msr??? mdscr_el1, xzr??????????? // Reset mdscr_el1?
(1)ic iallu指令設置instruction cache中的所有的cache line是無效的,直到PoU。同時設置為無效狀態的還包括BTB(Branch Target Buffer) cache。在處理器設計中,分支指令對性能的影響非常巨大(打破了pipeline,影響了并行處理),因此在處理器中會設定一個Branch target predictor單元用來對分支指令進行預測。Branch target predictor憑什么進行預測呢?所謂預測當然是根據過去推測現在,因此,硬件會記錄分支指令指令的跳轉信息,以便Branch target predictor對分支指令進行預測,這個硬件單元叫做Branch Target Buffer。程序中的分支指令辣么多,Branch Target Buffer不可能保存所有,只能cache近期使用到的分支跳轉信息。
(2)tlbi這條指令通過猜測也知道是對TLB進行invalidation的操作,但是vmalle1is是什么鬼?它其實是vm-all-e1-is,vmall表示要invalidate all TLB entry,e1表示該操作適用于EL1,is表示inner sharebility。根據ARM ARM描述,這條指令的作用范圍是inner shareable的所有PEs。這里有一個疑問:其實啟動過程有些是只在BSP上進行,例如前面文章中的save boot parameter、校驗blob、建立頁表都是全局性的,只做一次就OK了。而這里的__cpu_setup函數是會在每一個cpu core上執行,因此應該盡量少的影響系統。如果這里是invalidation所有的inner shareable的PE的TLB,那么在secondary cpu core啟動的時候會再執行一次,對系統影響很大,合理的操作應該是操作自己的TLB就OK了。
(3)step 1和step 2的操作和打開MMU操作有嚴格的時序要求,dsb這個memory barrier操作可以保證在執行打開MMU的時候,step 1和step 2都已經執行完畢。同樣的,ish表示inner shareable。
(4)CPACR_EL1(Architectural Feature Access Control Register)是用來控制Trace,浮點運算單元以及SIMD單元的。FPEN,bits [21:20]是用來控制EL0和EL1狀態的時候訪問浮點單元和SIMD單元是否會產生exception從進入上一個exception level。這里的設定運行用戶空間(EL0)和內核空間(EL1)訪問浮點單元和SIMD單元。MDSCR_EL1(Monitor Debug System Control Register)主要用來控制debug系統的。
三、Memory attributes lookup table的構建
1、overview
MMU的作用有三個:地址映射,控制memory的訪問權限,控制memory attribute。ARM64的啟動過程之(二):創建啟動階段的頁表對前面兩個功能有了簡單的描述,關于memory attribute將在本節描述。在Translation table中描述符中除了地址信息還有一些attribute的信息,例如attribute index域,既然叫做index則說明該域并沒有保存實際的memory attribute,實際的attribute保存在MAIR_ELx中。在這個64 bit的寄存器中,每8個bit一組,形成一種類型的memory attribute。
2、memory type
我們知道,ARMv8采用了weakly-order內存模型,也就是說,通俗的講就是處理器實際對內存訪問(load and store)的執行序列和program order不一定保持嚴格的一致,處理器可以對內存訪問進行reorder。例如:對于寫操作,processor可能會合并兩個寫的請求。處理器這么任性當然是從性能考慮,不過這大大加大了軟件的復雜度(軟件工程師需要理解各種memory barrier操作,例如ISB/DSB/DMB,以便控制自己程序的內存訪問的order)。
地址空間那么大,是否都任由processor胡作非為呢?當然不是,例如對于外設的IO地址,處理必須要保持其order。因此memory被分成兩個基本的類型:normal memory和devicememory。除了基本的memory type,還有memory attribute(例如:cacheability,shareability)來進一步進行描述,我們在下一節描述。
標識為normal memory type的memory就是我們常說的內存而已,對其訪問沒有副作用(side effect),也就是說第n次和第n+1次訪問沒有什么差別。device memory就不會這樣,對一些狀態寄存器有可能會read clear,因此n和n+1的內存訪問結果是不一樣的。正因為如此,processor可以對這些內存操作進行reorder、repeat或者merge。我們可以把程序代碼和數據所在的memory設定為normal memory type,這樣可以獲取更高的性能。例如,在代碼執行過程中,processor可能進行分支預測,從而提前加載某些代碼進入pipeline(而實際上,program不一定會fetch那些指令),如果設定了不正確的memory type,那么會阻止processor進行reorder的動作,從而阻止了分支預測,進而影響性能。
對于那些外設使用的IO memory,對其的訪問是有side effect的,很簡單的例子就是設備的FIFO,其地址是固定不變的,但是每次訪問,內部的移位寄存器就會將下一個數據移出來,因此每次訪問同一個地址實際上返回的數據是不一樣的。device不存在cache的設定,總是no cache的,處理器訪問device memory的時候,限制會比普通memory多,例如不能進行Speculative data accesses(所謂不能進行Speculative data accesses就是說cpu對memory的訪問必須由順序執行的執行產生,不能由于自己想加快性能而投機的,提前進行某些數據訪問)。
3、 memory attribute
上一節將memory分成兩個大類:normal memory和device,但是這么分似乎有些粗糙,我們可以進一步通過memory attribute將memory分成更多的區域。一個memory range對應的memory attribute是定義在頁表的描述符中(由upper attribues和lower attributes組成),最重要的attributes定義在lower attributes中的AttrIndx[2:0],該域只是一個index而已,指向MAIR_ELx中具體的memory attribute。8-bit的memory attribute的具體解釋可以參考ARM ARM。
對于device type,其總是non cacheable的,而且是outer shareable,因此它的attribute不多,主要有下面幾種附加的特性:
(1)Gathering 或者non Gathering (G or nG)。這個特性表示對多個memory的訪問是否可以合并,如果是nG,表示處理器必須嚴格按照代碼中內存訪問來進行,不能把兩次訪問合并成一次。例如:代碼中有2次對同樣的一個地址的讀訪問,那么處理器必須嚴格進行兩次read transaction。
(2)Re-ordering (R or nR)。這個特性用來表示是否允許處理器對內存訪問指令進行重排。nR表示必須嚴格執行program order。
(3)Early Write Acknowledgement (E or nE)。PE訪問memory是有問有答的(更專業的術語叫做transaction),對于write而言,PE需要write ack操作以便確定完成一個write transaction。為了加快寫的速度,系統的中間環節可能會設定一些write buffer。nE表示寫操作的ack必須來自最終的目的地而不是中間的write buffer。
對于normal memory,可以是non-cacheable的,也可以是cacheable的,這樣就需要進一步了解Cacheable和shareable atrribute,具體如下:
(1)是否cacheable
(2)write through or write back
(3)Read allocate or write allocate
(4)transient or non-transient cache
最后一點要說明的是由于cache hierararchy的存在,memory的屬性可以針對inner和outer cache分別設定,具體如何區分inner和outer cache是和具體實現相關,但通俗的講,build in在processor內的cache是inner的,而outer cache是processor通過bus訪問的。
4、代碼分析
ldr??? x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \???????????? MAIR(0x04, MT_DEVICE_nGnRE) | \
???????????? MAIR(0x0c, MT_DEVICE_GRE) | \
???????????? MAIR(0x44, MT_NORMAL_NC) | \
???????????? MAIR(0xff, MT_NORMAL)
??? msr??? mair_el1, x5
頁表中的memory attribute的信息并非直接體現在descriptor中的bit中,而是通過了一個間接的手段。描述符中的AttrIndx[2:0]是一個index,可以定位到8個條目,而這些條目就是保存在MAIR_EL1(Memory Attribute Indirection Register (EL1))中。對于ARM64處理器,linux kernel定義了下面的index:
#define MT_DEVICE_nGnRnE??? 0?
#define MT_DEVICE_nGnRE??????? 1?
#define MT_DEVICE_GRE??????? 2?
#define MT_NORMAL_NC??????? 3?
#define MT_NORMAL??????? 4
NC是no cache,也就是說MT_NORMAL_NC的memory是normal memory,但是對于這種類型的memory的訪問不需要通過cache系統。這些index用于頁表中的描述符中關于memory attribute的設定,對于初始化階段的頁表都是被設定成MT_NORMAL。
四、SCTLR_EL1、TCR_EL1的設定
1、寄存器介紹
SCTLR_EL1是一個對整個系統(包括memory system)進行控制的寄存器,我們這里描述幾個重要的域。這些域有兩種類型,一種是控制EL0狀態時候能訪問的資源。例如:UCI bit[26]控制是否允許EL0執行cache maintemance的指令(DC或者IC指令),如果不允許,那么會陷入EL1。nTWE bit[18]控制是否允許EL0執行WFE指令,如果不允許,那么會陷入EL1。bit 16類似bit 18,但是是for WFI指令的。UCT bit[15]控制是否允許EL0訪問CTR_EL0(該寄存器保存了cache信息),如果不允許,那么會陷入EL1。UMA,bit [9]控制是否可以訪問cpu狀態寄存器的PSTATE.{D,A, I, F}比特。還有一種是實際控制memory system的域,例如:C bit[2]是用來enable或者disable EL0 & EL1 的data cache。具體包括通過stage 1 translation table訪問的memory以及對stage 1 translation table自身memory的訪問。I bit[12]是用來enable或者disable EL0 & EL1 的instruction cache。M bit[0]是用來enable或者disable EL0 & EL1 的MMU。
我們知道,kernel space和user space使用不同的頁表,因此有兩個Translation Table Base Registers,形成兩套地址翻譯系統,TCR_EL1寄存器主要用來控制這兩套地址翻譯系統。TBI1,bit[38]和TBI0,bit[37]用來控制是否忽略地址的高8位(TBI就是Top Byte ignored的意思),如果允許忽略地址的高8位,那么MMU的硬件在進行地址比對,匹配的時候忽略高八位,這樣軟件可以自由的使用這個byte,例如對于一個指向動態分配內存的對象指針,可以通過高8位來表示reference counter,從而可以跟蹤其使用情況,reference count等于0的時候,可以釋放內存。AS bit[36]用來定義ASID(address space ID)的size,A1, bit [22]用來控制是kernel space還是user space使用ASID。ASID是和TLB操作相關,一般而言,地址翻譯的時候并不是直接查找頁表,而是先看TLB是否命中,具體判斷的標準是虛擬地址+ASID,ASID是每一個進程分配一個,標識自己的進程地址空間。這樣在切換進程的時候不需要flush TLB,從而有助于performance。TG1,bits [31:30]和TG0,bits [15:14]是用來控制page size的,可以是4K,16K或者64K。當MMU進行地址翻譯的時候需要訪問頁表,SH1, bits [29:28]和SH0, bits [13:12]是用來控制頁表所在memory的Shareability attribute。ORGN1, bits [27:26]和ORGN0, bits [11:10]用來控制頁表所在memory的outercachebility attribute的。IRGN1, bits [25:24]和IRGN0, bits [9:8]用來控制頁表所在memory的inner cachebility attribute的。T1SZ, bits [21:16]和T0SZ, bits [5:0]定義了虛擬地址的寬度。
2、代碼分析
代碼位于arch/arm64/mm/proc.S中,該函數主要為打開MMU做準備,具體代碼如下:
??? adr??? x5, crval ------------------------------(1)?
??? ldp??? w5, w6, [x5]?
??? mrs??? x0, sctlr_el1?
??? bic??? x0, x0, x5??????????? // clear bits?
??? orr??? x0, x0, x6??????????? // set bits??
??? ldr??? x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \?
??????????? TCR_TG_FLAGS | TCR_ASID16 | TCR_TBI0?
??? tcr_set_idmap_t0sz??? x10, x9 -----------------------(2)
??? mrs??? x9, ID_AA64MMFR0_EL1?
??? bfi??? x10, x9, #32, #3?
??? msr??? tcr_el1, x10 ----------------------------(3)?
??? ret??????????????????? // return to head.S ---------------------(4)?
ENDPROC(__cpu_setup)
(1)在調用__enable_mmu之前要準備好SCTLR_EL1的值,該值在這段代碼中設定并保存在x0寄存器中,隨后做為參數傳遞給__enable_mmu函數。具體怎么設定SCTLR_EL1的值呢?這是通過crval變量設定的,如下:
??? .type??? crval, #object?
crval:?
??? .word??? 0xfcffffff----------------SCTLR_EL1寄存器中需要清0的bit?
??? .word??? 0x34d5d91d -------------SCTLR_EL1寄存器中需要設置成1的bit
由代碼可知,EE和E0E這兩個bit沒有清零,因此實際上這些bit保持不變(在el2_setup中已經設定)。這里面具體各個bit的含義清參考ARM ARM文檔,我們不一一說明了。
(2)這里的代碼是準備TCR寄存器的值。TCR_TxSZ(VA_BITS)是根據CONFIG_ARM64_VA_BITS配置來設定內核和用戶空間的size,其他的是進行page size的設定或者是page table對應的memory的attribute的設定,具體可以對照ARM ARM文檔進行分析。
(3)到這里x10已經準備好了TCR寄存器的值,還缺省IPS(Intermediate Physical Address Size)的設定(IPS和2 stage地址映射相關,它和虛擬化有關,這里就不展開描述了,內容太多)。ID_AA64MMFR0_EL1, AArch64 Memory Model Feature Register 0,該寄存器保存了memory model和memory management的支持情況,該寄存器的PARange保存了物理地址的寬度信息,bfi??? x10, x9, #32, #3 指令就是將x9寄存器的內容左移32bit,copy 3個bit到x10寄存器中(IPS占據bits [34:32])。
(4)stext的代碼如下:
ENTRY(stext)
……?
??? ldr??? x27, =__mmap_switched?
??? adr_l??? lr, __enable_mmu?
??? b??? __cpu_setup?
ENDPROC(stext)
在調用__cpu_setup之前設定了lr的內容是__enable_mmu,而調用__cpu_setup使用的是b而不是bl指令,因此lr寄存器沒有修改,因此,這里的ret返回到__enable_mmu函數。
五、參考文獻
1、ARM Architecture Reference Manual
原創文章,轉發請注明出處。蝸窩科技
標簽: ARM64啟動過程 MMU
?ARM64的啟動過程之(四):打開MMU|ARM64的啟動過程之(二):創建啟動階段的頁表?評論:
tigger?2015-10-26 16:44 我看了一下代碼
ioremap與ioremap_nocache的實現是一樣的
而且他們的參數都是PROT_DEVICE_nGnRE
這樣讓我思考nE 與E 實際差異會帶來什么問題?arm 默認實現都是E嗎? 回復 linuxer?
2015-10-27 09:13 @tigger:使用ioremap和ioremap_nocache都是創建PROT_DEVICE_nGnRE類型的映射,也就是enable了write buffer,寫入這段memory的時候數據可以不到最終的地點(外設)而是寫入write buffer,然后由中間節點(例如interconnection器件)返回write response給bus master,說明本次transaction結束了。在總線不是那么繁忙的時候,可以最終將write buffer中的數據寫入終點(外設)
啟用了write buffer會帶來性能的提升,在保證硬件操作邏輯正確的情況下,啟用write buffer是正確的行為。
以上是我的理解,也很膚淺,不過慢慢積累吧! 回復 tigger?
2015-10-26 16:32 所謂Speculative data accesses就是說cpu對memory的訪問必須由順序執行的執行產生,不能由于自己想加快性能而投機的,提前進行某些數據訪問
這里應該是說 不能Speculative data accesses 吧? 回復 linuxer?
2015-10-26 18:19 @tigger:是的,筆誤了,多謝指正 回復 tigger?
2015-10-26 15:48 hello
所以說PoC&PoU 在一個SMP系統中是同時存在的,只是具體的memory 點,具體看待了?
smp 各個cpu直接是PoU,smp 各個cpu 與dma 直接是Poc? 回復 linuxer?
2015-10-26 18:16 @tigger:所以說PoC&PoU 在一個SMP系統中是同時存在的,只是具體的memory 點,具體看待了?
---------------------------------------
對于ARMv8而言,其最大能支持7個level的的cache,當然實際上系統未必會實現那么多level。對于一個系統,同時并且真實存在的就是各個level的cache,PoC和PoU僅僅說明cache操作是哪一個level。具體PoC和PoU是哪一個level的cache是和系統設計相關的,具體系統具體分析。
smp 各個cpu直接是PoU,smp 各個cpu 與dma 直接是Poc?
----------------------------------
我似乎不是很明白你的問題,你能不能更明確的說明你的問題? 回復 tigger?
2015-10-27 09:51 @linuxer:我的意思是,比如一個8核的soc,在處理smp各個核直接的數據的時候,需要考慮PoU,需要考慮各個核與比如DMA之間的數據的時候,需要考慮的就是POC? 回復
總結
以上是生活随笔為你收集整理的ARM64的启动过程之(三):为打开MMU而进行的CPU初始化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ARM64的启动过程之(二):创建启动阶
- 下一篇: ARM64的启动过程之(四):打开MMU