從 ARM VIVT 看 cache
請訪問google 文檔.
http://docs.google.com/Doc?id=dcbsxfpf_282csrs2pfn
do_wp_page 的VIVT 考慮
在下面的函數中(write protect detected), 為什么需要 flush_cache_page,和 update_mmu_cache? 原因在於在vivt 的情況下, 如果a 進程寫入cache line 1, b進程試圖讀取自己的 cache line 2, 並且b進程是cow(一種情況), 那么在copy這個頁面的時候就需要flush 用戶a存在於cache line1 內的數據, 以保證b進程獲取正確數據.
實際上flush_cache_page(); 除了刷新用戶a的頁面外,還考慮到vipt_aliasing. 我們后面再討論.
關於這個問題的一個討論:
http://search.luky.org/linux-kernel.2000/msg03711.html
http://lists.arm.linux.org.uk/lurker/message/20040705.161647.21042a82.html
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
{
....
old_page = vm_normal_page(vma, address, orig_pte);
if (!old_page) {
if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))
goto reuse;
goto gotten;
}
/*
* Take out anonymous pages first, anonymous shared vmas are
* not dirty accountable.
*/
if (PageAnon(old_page)) {
if (trylock_page(old_page)) {
reuse = can_share_swap_page(old_page);
unlock_page(old_page);
}
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))) {
......
dirty_page = old_page;
get_page(dirty_page);
reuse = 1;
}
if (reuse) {
reuse:
flush_cache_page(vma, address, pte_pfn(orig_pte));
entry = pte_mkyoung(orig_pte);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
if (ptep_set_access_flags(vma, address, page_table, entry,1))
update_mmu_cache(vma, address, entry);
ret |= VM_FAULT_WRITE;
goto unlock;
}
Cache 的基本知識
首先推薦 wikipedia, 本文有半數來自這篇文章:
http://en.wikipedia.org/wiki/CPU_cache#Associativity
Arm4, Arm5: VIVT
Arm6, Arm7: I-cache VIPT, Dcache VIPT or PIPT.
用兩個例子和圖形分析下啥是index,啥是tag:
K8 4k 2-way cache, cache line 64B

0. 共4k, cache line 長度是64B, 共64個cache line
1. byte select : 64B, 2^6, 即 [0-5] bit 選擇一個cache line內的byte
2. 64 個cache line, 需要2^6個index去索引,6-11bit, 但是2way的含義在於一個index選擇2個cache line(數據可以存儲於這2個cache line的任一個), 即只要2^6/2, 5bit 作index 就夠了, [10-6] bit 就是k8 cache 的index
3.剩下的[31:11] bit 用於確定這2個cache line是否包含了要存取的數據
-----------------------------------
4. 上圖是2way cache 實現原理的一種示意圖. 左邊的tag ram存儲的是cache 的tag 即地址的[31:11]bit, 當再cache中查找的時候,用index [10:6]bit 同時和tag ram中的way0, way0 相比較, 如果有一個相同就代表命中了. 我們就知道命中的是那個way的cache.
5. 左邊存儲cache的具體數據, 卻並不是以cache line的形式來存儲的,而是每個way的data SRAM 各存儲了一個word, 總共有512個entry, 所以這些數據的index 是[10:2]bit.
6. 結合第4步驟選出來的way number, 就知道到底命中了那個way的數據
這種實現, cache line 的index (way 選擇), 和date的'index ' 分別具有不同的bit數目. n-way的cache意味着命中cache的時候需要同時和n個 cache index 進行比較. (肯定是同時,如果比兩次還不如用1:1的cache呢).
如果n比較大, 用類似CAM的實現方式顯然比較靠普, 就下面要說的ARM920T的I-cache.
ARM920T (SAM SUNG 2410a) I-cache :16k 64-way cache, 32B cache line


0. 共16k, cache line 長度是32B, 共512個cache line
1. byte select : 64B, 2^6, 即 [0-4] bit 選擇一個cache line內的byte
2. 512 個cache line, 需要2^9個index去索引, 64 way就是一個index選擇64個cacheline, 即只要2^9/64, 3bit 作index 就夠了, 據就是[7-5] bit是ARM920T I-cache的index (這里把cache index 叫做seg)
3.剩下的[31:8] bit 用於確定某個way內的64個cache line是否包含了要存取的數據
-----------------------------------
4. 上圖是用CAM 實現的64 way cache的一種示意圖. 0-7 這7個平面是7個CAM內存組. CAM中存的是cache TAG即地址的[31:8]bit,當再cache中查找的時候,用cache index [7-5]bit 同時和CAM 中的tag相比較, 如果命中就直接獲取到了一個完整的cache line.
5. 0-4 bit 可以選中這個cache line 中的byte, 如果是word 操作, [4:2]bit就是word index
n-way的cache意味着命中cache的時候需要同時和n個 cache index 進行比較. 這次是用CAM.
VIVT VIPT
VIVT: 意思是cache的index和tag都是虛擬地址. 這個好處就是讀取cache的時候無須TLB的介入, 速度比較快.
VIPT: 這個就是說, cache 和TLB 可以同時工作, 比不上VIVT 的cache 速度, 但是比PIPT 要好.
Cache Alias
Virtual index cache 的速度是快了, 但是會帶來其他的問題: 一個物理地址的內容可以出現在多個cache line中, 這就需要更多的cache flush操作. 反而影響了速度. 這就是cache alias.
隨着技術進步, TLB查找速度提高了, 在cache 用index查找cache的過程中TLB可以完成虛擬地址到物理地址的轉換工作, 在cache比較tag的時候物理地址已經可以使用了, 就是說采用physical tag可以和cache並行工作, virtual tag已經不怎么使用了, 特別是在容量比較大的cache中(比如l2 cache). 只有小,並且延遲很小的cache 還或許采用virtual tag.
alias就是同一個物理地址被映射到兩個或多個相同或者不同的虛擬地址的時候,cache 中存在多於一個的cache line 包含這個物理地址的數據, 具體來講, alias 可以通過如下幾種方式產生:
(強烈推薦 http://mail-index.netbsd.org/port-sh3/2006/09/07/0000.html )
一個PA 被映射到不同的虛擬地址, 如linux下, 內核和用戶訪問頁面的虛擬地址可能不同. 這些虛擬地址有個特點,因為映射到同pa , 其低幾位bit 必然相同.
PA--> VA1 |31 12 | PA's low 12 bit |
VA2 |VA2 ie, 31-11 12 | PA's low 12 bit |
這種情況下是否有alias, 取決於index采用哪幾個bit:
cache index: |n m| word|byte|
1. 如果bit [n..m]中包含了Vitual index 的幾個比特,那么因為VA1/VA2不同, 就會有兩個cache line 可以包含這個地址的數據, 這一點對於VIVT,和VIPT 都相同.
2. 如果 bit [n..m] 完全落在了PA的 低幾個bit, 對於direct map的cache, 不會有alias出現, 但是對於n-way的情況, 就要看是PT還是VT了. 如果是VIVT, 因為 VT 不同,那么alias不可避免. 如果VIPT, 則因為Pysical的高幾位地址也相同, alias就不會出現了.
-----------以下內容有點疑問
許多cpu可以通過cache miss 的時候檢查所可能的cache line, 來防止cache alias的出現. cache miss的時候, 搜索所有可能位置, 通過對比物理地址是否相同(*不確信)來消除alias. 這種消除可以簡化, 就是上面的[n...m]完全落在page index之外,比如上面的PA's low 12 bit時, 這種情況只有一個可能的位置來存儲一個PA的VA映射, 對於PT, 在這種情況下,進行hit處理的時候已經進行了這種alias的檢查, miss的時候就沒有問題了.
page color
配備有特別大容量的physical index的cache(一般是2級緩存)會引起一個問題: 應用程序無法控制自己的物理內存在cache中的位置,即無法控制cache 沖突,因為一般是os決定一個虛擬頁面映射到那個物理頁面.
比如有一個1M的的Physical Index, direct map的二級緩存,如果是4k頁面,那cache里就可以同時容納256個物理頁面, 為了區分這些頁面, 給他們編上號碼, 0-255, 稱為page的color, 不同color的page在這種二級cache里不會有沖突.
一個比較直接的辦法是在給虛擬頁面映射物理頁面的時候保證他們的color相同,這樣應用程序可以按照自己虛擬地址的color來安排cache的訪問模式, 這樣可以避免confilict miss. 應用程序可以安排在一段時間內只訪問1m的數據來避免capacity miss.
術語:
snag : 給定的時間內,擁有不同虛擬color的頁面可能擁有相同的物理color,從而引起沖突.
birthday paradox : 如果OS總是隨機的給虛擬頁面映射物理頁面,那么很可能許多頁面擁有相同的物理color,從而引起沖突. (birthday paradox: 隨機選取的一些 人中,可能會有幾對相同生日的人.)
給不同的虛擬color映射到不同的物理color的技術就是page color. 比較簡單的實現是讓他們的虛擬color和物理color相同.
如果OS保證一個物理頁面只會映射到一個color的虛擬頁面, cpu就可以使用Virtual index而無需考慮aliasing問題.早期的SPARC 和 RS/6000 就采用了這種設計.
virtual hints
CPU 如Pentium 4 (Willamette and Northwood cores), 采用了vhint 來代替vtag來做way select, 他是4way的,只用2bit(從vitual tag hash 或者其他方式獲得), 這樣就不用采用TCAM了, 並在way 內存儲physical tag.
和vhints類似有的cpu同時采用VT和PT, 比如early SPARC, VT用來作way select, 用PT做miss detection.
這里順便提下, 和cache alias相類似的問題是 Ambiguity: 不同的物理地址映射到相同的VA. 這種情況下在linux內, 只有不同進程的用戶空間的頁面才可能(fix me). 這種情況會造成同一個cache line在不同的進程中代表不同的數據, 切換進程的時候看似invalid user space 的cache必須進行. 實際上這個也要看是VIVT還是VIPT.
Process 1: |31 VA 20| phy address low bit| => PA1 | phy address low bit|
Process 2: |31 VA 20| phy address low bit| => PA2 | phy address low bit|
注:兩個進程的虛擬地址完全相同, 就是說phy address low bit 也相同.
====================
地址切換:
1. 在VIPT 的時候, 或者要像i386那樣切換整個pgd, 或者像mips, 有ASID作為區分. 這樣TLB 不會有Ambiguity, 同時VIPT 的cache 要比較phy address 來確定 cache hit. 故不用flush cache.
2. VIVT 的時候就就比較麻煩, 盡管MMU 不會給出錯誤的PA, 但是因為是vitual tag 也相同, 就會命中上一個進程的對應於不同物理地址cache了, flush cache 就必須進行了. 具體的例子, 如 arm/mm/proc-arm926.S
=====================
TLB 撤銷: exit_mm
還有就是撤銷TLB的時候, 撤銷的時候,VIVT 仍然需要把cache flush, 否則到心的mm后就會有Ambiguity錯誤了, 見 do_exit -> exit_mm->mmput->exit_mmap->flush_cache_mm.
ARM cache的一些討論
仍然有許多關於arm cache問題不清晰: 如2.6的宏cache_is_vivt: 如果編譯內核的時候沒有明確給出cache type的信息: CONFIG_CPU_CACHE_VIPT 或者 CONFIG_CPU_CACHE_VIVT, 那么cache_is_vivt 是可靠的嗎. 下面的一段注釋沒有在2.6的內核代碼中出現. 或許這是一個經驗公式,呵呵,需要列出所有arm cpu以及其cache 種類並根據ctype的類型來 hash出這個判斷方式.(fix me).
#define __cacheid_present(val) (val != read_cpuid(CPUID_ID))
#define __cacheid_type_v7(val) ((val & (7 << 29)) == (4 << 29))
#define __cacheid_vivt_prev7(val) ((val & (15 << 25)) != (14 << 25))
#define __cacheid_vipt_prev7(val) ((val & (15 << 25)) == (14 << 25))
#define __cacheid_vipt_nonaliasing_prev7(val) ((val & (15 << 25 | 1 << 23)) == (14 << 25))
#define __cacheid_vipt_aliasing_prev7(val) ((val & (15 << 25 | 1 << 23)) == (14 << 25 | 1 << 23))
#define __cacheid_vivt(val) (__cacheid_type_v7(val) ? 0 : __cacheid_vivt_prev7(val))
#define __cacheid_vipt(val) (__cacheid_type_v7(val) ? 1 : __cacheid_vipt_prev7(val))
#define __cacheid_vipt_nonaliasing(val) (__cacheid_type_v7(val) ? 1 : __cacheid_vipt_nonaliasing_prev7(val))
#define __cacheid_vipt_aliasing(val) (__cacheid_type_v7(val) ? 0 : __cacheid_vipt_aliasing_prev7(val))
#define __cacheid_vivt_asid_tagged_instr(val) (__cacheid_type_v7(val) ? ((val & (3 << 14)) == (1 << 14)) : 0)
#if defined(CONFIG_CPU_CACHE_VIVT) && !defined(CONFIG_CPU_CACHE_VIPT)
#define cache_is_vivt() 1
#define cache_is_vipt() 0
#define cache_is_vipt_nonaliasing() 0
#define cache_is_vipt_aliasing() 0
#define icache_is_vivt_asid_tagged() 0
#elif defined(CONFIG_CPU_CACHE_VIPT)
#define cache_is_vivt() 0
#define cache_is_vipt() 1
...
#else /*沒有明確配置cache type*/
/*
* VIVT or VIPT caches. Note that this is unreliable since ARM926
* and V6 CPUs satisfy the "(val & (15 << 25)) == (14 << 25)" test.
* There's no way to tell from the CacheType register what type (!)
* the cache is.
*/
#define cache_is_vivt() \
({ \
unsigned int __val = read_cpuid(CPUID_CACHETYPE); \
(!__cacheid_present(__val)) || __cacheid_vivt(__val); \
})
#endif