天天看點

kmap_atomic的細節以及改進

kmap_atomic用于高端記憶體映射,用于緊急的,短時間的映射,它沒有使用任何鎖,完全靠一個數學公式來避免混亂,它空間有限且虛拟位址固定,這意味着它映射的記憶體不能長期被占用而不被unmap,kmap_atomic在效率上要比kmap提升不少,然而它和kmap卻不是用于同一場合的。不管怎麼說,它的設計是很完美的。

     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

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271144

繼續閱讀