kmap_atomic用於高端記憶體映射,用於緊急的,短時間的映射,它沒有使用任何鎖,完全靠一個數學公式來避免混亂,它空間有限且虛擬位址固定,這意味著它映射的記憶體不能長期被佔用而不被unmap,kmap_atomic在效率上要比kmap提升不少,然而它和kmap卻不是用於同一場合的,kmap用於休眠的情況而kmap_atomic則用在不能休眠的情況下(如中斷處理常式等不會被重新調度的時候)。不管怎麼說,它的設計是很完美的。
kernel可以在多個cpu上同時運行不同的task,然而它們共同使用一個記憶體位址空間,也就是說,地址空間對於多個cpu看到的是同一個,kmap_atomic使用的是地址空間頂部的一小段地址空間,核心邏輯將這一小段地址空間分成了若干個節,每一節的大小是一個page的大小,可以用來映射一個page,根據公用地址空間的原理,所有的cpu共同使用這些節,因此如何能保證N個cpu調用kmap_atomic不會將page映射到一個地址呢?這就是這個數學公式所起的作用:
idx = type + KM_TYPE_NR*smp_processor_id();
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
其中KM_TYPE_NR代表type的最大值加1:
enum km_type {
KM_BOUNCE_READ,
KM_SKB_SUNRPC_DATA,
KM_SKB_DATA_SOFTIRQ,
KM_USER0,
KM_USER1,
...
KM_TYPE_NR
};
調用kmap_atomic的時候,有一個參數就是上述的枚舉類型km_type,這樣不同的cpu得到的vaddr就不可能一樣了,原因是這樣的:type + KM_TYPE_NR*smp_processor_id()可以寫成z=x+N*y(x<N),只要y不同,z就一定不會相同,因為z=x+N*y<N+N*y=(N+1)*y,設現有y1<y2(二者最少相差1),則若z1=z2,由於y1*N-y2*N>N,必有x1-x2>N,而x1和x2在0-N的範圍內,因此這是不可能的,所以只要cpu不同,它們是不可能映射到同一虛擬位址的,也就是不會導致衝突的發生,最終通過__fix_to_virt(FIX_KMAP_BEGIN + idx)得到映射後的虛擬位址,該__fix_to_virt宏基本是一個常量轉換,根據idx找到虛擬位址空間最高處的那個屬於本次映射的小段。
再看kmap_atomic這個api的原形:void *kmap_atomic(struct page *page, enum km_type type),有個參數是type,也就是說具體映射到哪一段是由調用者來決定的,可是這真的有必要嗎?調用者無非需要的是一個虛擬位址而已,它不管一個page具體映射到哪個虛擬位址,就像kmap做的那樣,原則上說,kmap_atomic提供的僅僅是底層的一個實現機制,一個介面,它完全可以用不同的方式實現,調用者實在沒有必要牽扯進這個底層的細節問題,因此km_type是沒有必要的,故而2.6.37核心中果斷地去除了這個km_type,現如今2.6.37核心的kmap_atomic的實現如下:
void *kmap_atomic_prot(struct page *page, pgprot_t prot)
{
unsigned long vaddr;
int idx, type;
pagefault_disable(); //原子映射是基於每cpu的,因此在當前cpu上禁用搶佔,直到unmap的時候才開啟,這樣就不會導致原子映射的重入了,畢竟如果禁用搶佔的話,調用者進程在開啟搶佔之前別的進程是不可能在核心空間運行,除非該進程在unmap之前睡眠,如果真的那樣,別的進程就很可能在同一cpu重入kmap_atomic_prot了,然後就可能映射到同一虛擬位址(在當前2.6.37版本內部分解決了這個問題,見下面的push/pop),因此有人說原子映射期間進程不允許睡眠
if (!PageHighMem(page)) //非高端頁面直接返回一一映射地址
return page_address(page);
type = kmap_atomic_idx_push(); //遞增一個每cpu變數,返回遞增後的結果
idx = type + KM_TYPE_NR*smp_processor_id();
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
set_pte(kmap_pte-idx, mk_pte(page, prot)); //設定頁表
return (void *)vaddr;
}
static inline int kmap_atomic_idx_push(void)
{
int idx = __get_cpu_var(__kmap_atomic_idx)++;
#ifdef CONFIG_DEBUG_HIGHMEM
WARN_ON_ONCE(in_irq() && !irqs_disabled());
BUG_ON(idx > KM_TYPE_NR); //這個BUG_ON提醒__kmap_atomic_idx不能超過KM_TYPE_NR,原因同老版本的一樣
#endif
return idx;
}
可見新版本的原子映射中沒有了km_type參數,只有一個page參數,完全靠一個stack實現了類似km_type的機制,在unmap的時候會調用__get_cpu_var(__kmap_atomic_idx)--將這個變數遞減掉。看了這個新版本的實現之後,我們會發現,既然調用kmap_atomic_prot的時候禁用了搶佔,如果進程不主動睡眠的話,在單一的cpu上__kmap_atomic_idx一般是不會大於1的,那麼push中的BUG_ON當然就是杞人憂天了(除非使用一個原子映射期間又進行了另一個原子映射),然而如果原子映射的調用者睡眠了的話,誰也再來一個映射也不會和前面的重合,因為__kmap_atomic_idx此時遞增了,然後這個進程也睡眠了,喚醒了原來的那個原子映射的調用者,該進程unmap了它的那個原子映射,很不巧,它釋放的是第二個進程映射的頁面...因此還是不要在使用原子映射之間睡眠。
那麼新版本的原子映射有沒有什麼缺點呢?kmap_atomic_idx_push和kmap_atomic_idx_pop都是函數,增加了幾次函數調用並沒有什麼(它們都是inline的),最重要的是增加了幾個變數的訪問和操作--__kmap_atomic_idx
本文來自CSDN部落格,轉載請標明出處:http://blog.csdn.net/dog250/archive/2011/01/11/6129826.aspx