1 高端記憶體與內核映射 儘管 函數族可用於從高端記憶體域向內核映射頁幀(這些在內核空間中通常是無法直接看到的), 但這並不是這些函數的實際用途. 重要的是強調以下事實 : 內核提供了其他函數用於將 頁幀顯式映射到內核空間, 這些函數與vmalloc機制無關. 因此, 這就造成了混亂. 而在高端記憶體的頁 ...
1 高端記憶體與內核映射
儘管vmalloc
函數族可用於從高端記憶體域向內核映射頁幀(這些在內核空間中通常是無法直接看到的), 但這並不是這些函數的實際用途.
重要的是強調以下事實 : 內核提供了其他函數用於將ZONE_HIGHMEM
頁幀顯式映射到內核空間, 這些函數與vmalloc機制無關. 因此, 這就造成了混亂.
而在高端記憶體的頁不能永久地映射到內核地址空間. 因此, 通過alloc_pages()函數以__GFP_HIGHMEM標誌獲得的記憶體頁就不可能有邏輯地址.
在x86_32體繫結構總, 高於896MB的所有物理記憶體的範圍大都是高端記憶體, 它並不會永久地或自動映射到內核地址空間, 儘管X86處理器能夠定址物理RAM的範圍達到4GB(啟用PAE可以定址64GB), 一旦這些頁被分配, 就必須映射到內核的邏輯地址空間上. 在x86_32上, 高端地址的頁被映射到內核地址空間(即虛擬地址空間的3GB~4GB)
內核地址空間的最後128 MiB用於何種用途呢?
該部分有3個用途。
虛擬記憶體中連續、但物理記憶體中不連續的記憶體區,可以在vmalloc區域分配. 該機制通常用於用戶過程, 內核自身會試圖儘力避免非連續的物理地址。內核通常會成功,因為大部分大的記憶體塊都在啟動時分配給內核,那時記憶體的碎片尚不嚴重。但在已經運行了很長時間的系統上, 在內核需要物理記憶體時, 就可能出現可用空間不連續的情況. 此類情況, 主要出現在動態載入模塊時.
持久映射用於將高端記憶體域中的非持久頁映射到內核中
固定映射是與物理地址空間中的固定頁關聯的虛擬地址空間項,但具體關聯的頁幀可以自由選擇. 它與通過固定公式與物理記憶體關聯的直接映射頁相反,虛擬固定映射地址與物理記憶體位置之間的關聯可以自行定義,關聯建立後內核總是會註意到的.
在這裡有兩個預處理器符號很重要 __VMALLOC_RESERVE設置了vmalloc
區域的長度, 而MAXMEM
則表示內核可以直接定址的物理記憶體的最大可能數量.
內核中, 將記憶體劃分為各個區域是通過圖3-15所示的各個常數控制的。根據內核和系統配置, 這些常數可能有不同的值。直接映射的邊界由high_memory指定。
- 直接映射區
線性空間中從3G開始最大896M的區間, 為直接記憶體映射區,該區域的線性地址和物理地址存線上性轉換關係:線性地址=3G+物理地址。
- 動態記憶體映射區
該區域由內核函數vmalloc
來分配, 特點是 : 線性空間連續, 但是對應的物理空間不一定連續. vmalloc分配的線性地址所對應的物理頁可能處於低端記憶體, 也可能處於高端記憶體.
- 永久記憶體映射區
該區域可訪問高端記憶體. 訪問方法是使用alloc_page(_GFP_HIGHMEM)
分配高端記憶體頁或者使用kmap函數將分配到的高端記憶體映射到該區域.
- 固定映射區
該區域和4G的頂端只有4k的隔離帶,其每個地址項都服務於特定的用途,如ACPI_BASE等。
說明
註意用戶空間當然可以使用高端記憶體,而且是正常的使用,內核在分配那些不經常使用的記憶體時,都用高端記憶體空間(如果有),所謂不經常使用是相對來說的,比如內核的一些數據結構就屬於經常使用的,而用戶的一些數據就屬於不經常使用的。用戶在啟動一個應用程式時,是需要記憶體的,而每個應用程式都有3G的線性地址,給這些地址映射頁表時就可以直接使用高端記憶體。
而且還要糾正一點的是:那128M線性地址不僅僅是用在這些地方的,如果你要載入一個設備,而這個設備需要映射其記憶體到內核中,它也需要使用這段線性地址空間來完成,否則內核就不能訪問設備上的記憶體空間了.
總之,內核的高端線性地址是為了訪問內核固定映射以外的記憶體資源。進程在使用記憶體時,觸發缺頁異常,具體將哪些物理頁映射給用戶進程是內核考慮的事情. 在用戶空間中沒有高端記憶體這個概念.
即內核對於低端記憶體, 不需要特殊的映射機制, 使用直接映射即可以訪問普通記憶體區域, 而對於高端記憶體區域, 內核可以採用三種不同的機制將頁框映射到高端記憶體 : 分別叫做永久內核映射、臨時內核映射以及非連續記憶體分配
2 持久內核映射
如果需要將高端頁幀長期映射(作為持久映射)到內核地址空間中, 必須使用kmap函數. 需要映射的頁用指向page的指針指定,作為該函數的參數。該函數在有必要時創建一個映射(即,如果該頁確實是高端頁), 並返回數據的地址.
如果沒有啟用高端支持, 該函數的任務就比較簡單. 在這種情況下, 所有頁都可以直接訪問, 因此只需要返回頁的地址, 無需顯式創建一個映射.
如果確實存在高端頁, 情況會比較複雜. 類似於vmalloc, 內核首先必須建立高端頁和所映射到的地址之間的關聯. 還必須在虛擬地址空間中分配一個區域以映射頁幀, 最後, 內核必須記錄該虛擬區域的哪些部分在使用中, 哪些仍然是空閑的.
2.1 數據結構
內核在IA-32平臺上在vmalloc
區域之後分配了一個區域, 從PKMAP_BASE
到FIXADDR_START
. 該區域用於持久映射. 不同體繫結構使用的方案是類似的.
永久內核映射允許內核建立高端頁框到內核地址空間的長期映射。 他們使用著內核頁表中一個專門的頁表, 其地址存放在變數pkmap_page_table
中, 頁表中的表項數由LAST_PKMAP巨集產生. 因此,內核一次最多訪問2MB或4MB的高端記憶體.
#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE)
頁表映射的線性地址從PKMAP_BASE
開始. pkmap_count
數組包含LAST_PKMAP個計數器,pkmap_page_table頁表中的每一項都有一個。
// http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126
static int pkmap_count[LAST_PKMAP];
static __cacheline_aligned_in_smp DEFINE_SPINLOCK(kmap_lock);
pte_t * pkmap_page_table;
高端映射區邏輯頁面的分配結構用分配表(pkmap_count
)來描述,它有1024項,對應於映射區內不同的邏輯頁面。當分配項的值等於0時為自由項,等於1時為緩衝項,大於1時為映射項。映射頁面的分配基於分配表的掃描,當所有的自由項都用完時,系統將清除所有的緩衝項,如果連緩衝項都用完時,系統將進入等待狀態。
// http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126
/*
高端映射區邏輯頁面的分配結構用分配表(pkmap_count)來描述,它有1024項,
對應於映射區內不同的邏輯頁面。當分配項的值等於零時為自由項,等於1時為
緩衝項,大於1時為映射項。映射頁面的分配基於分配表的掃描,當所有的自由
項都用完時,系統將清除所有的緩衝項,如果連緩衝項都用完時,系
統將進入等待狀態。
*/
static int pkmap_count[LAST_PKMAP];
pkmap_count
(在mm/highmem.c?v=4.7, line 126定義)是一容量為LAST_PKMAP
的整數數組, 其中每個元素都對應於一個持久映射頁。它實際上是被映射頁的一個使用計數器,語義不太常見.
內核可以通過get_next_pkmap_nr獲取到pkmap_count數組中元素的個數, 該函數定義在mm/highmem.c?v=4.7, line 66
/*
* Get next index for mapping inside PKMAP region for page with given color.
*/
static inline unsigned int get_next_pkmap_nr(unsigned int color)
{
static unsigned int last_pkmap_nr;
last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
return last_pkmap_nr;
}
為了記錄高端記憶體頁框與永久內核映射包含的線性地址之間的聯繫,內核使用了page_address_htable
散列表.
該表包含一個page_address_map
數據結構,用於為高端記憶體中的每一個頁框進行當前映射。而該數據結構還包含一個指向頁描述符的指針和分配給該頁框的線性地址。
/*
* Describes one page->virtual association
*/
struct page_address_map
{
struct page *page;
void *virtual;
struct list_head list;
};
該結構用於建立page-->virtual
的映射(該結構由此得名).
欄位 | 描述 |
---|---|
page | 是一個指向全局mem_map數組中的page實例的指針 |
virtual | 指定了該頁在內核虛擬地址空間中分配的位置 |
為便於組織, 映射保存在散列表中, 結構中的鏈表元素用於建立溢出鏈表,以處理散列碰撞. 該散列表通過page_address_htable
數組實現, 定義在mm/highmem.c?v=4.7, line 392
static struct page_address_slot *page_slot(const struct page *page)
{
return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}
2.2 page_address函數
page_address
是一個前端函數, 使用上述數據結構確定給定page實例的線性地址, 該函數定義在mm/highmem.c?v=4.7, line 408)
/**
* page_address - get the mapped virtual address of a page
* @page: &struct page to get the virtual address of
*
* Returns the page's virtual address.
*/
void *page_address(const struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
/*如果頁框不在高端記憶體中*/
if (!PageHighMem(page))
/*線性地址總是存在,通過計算頁框下標
然後將其轉換成物理地址,最後根據相應的
/物理地址得到線性地址*/
return lowmem_page_address(page);
/*從page_address_htable散列表中得到pas*/
pas = page_slot(page);
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {{/*如果對應的鏈表不空,
該鏈表中存放的是page_address_map結構*/
struct page_address_map *pam;
/*對每個鏈表中的元素*/
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
/*返回線性地址*/
ret = pam->virtual;
goto done;
}
}
}
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
EXPORT_SYMBOL(page_address);
page_address
首先檢查傳遞進來的page
實例在普通記憶體還是在高端記憶體.
- 如果是前者(普通記憶體區域), 頁地址可以根據
page
在mem_map數組中的位置計算. 這個工作可以通過lowmem_page_address調用page_to_virt(page)來完成 - 對於後者, 可通過上述散列表查找虛擬地址.
2.3 kmap創建映射
2.3.1 kmap函數
為通過page
指針建立映射, 必須使用kmap
函數.
不同體繫結構的定義可能不同, 但是大多數體繫結構的定義都如下所示, 比如arm上該函數定義在arch/arm/mm/highmem.c?v=4.7, line 37, 如下所示
/*高端記憶體映射,運用數組進行操作分配情況
分配好後需要加入哈希表中;*/
void *kmap(struct page *page)
{
might_sleep();
if (!PageHighMem(page)) /*如果頁框不屬於高端記憶體*/
return page_address(page);
return kmap_high(page); /*頁框確實屬於高端記憶體*/
}
EXPORT_SYMBOL(kmap);
kmap
函數只是一個page_address
的前端,用於確認指定的頁是否確實在高端記憶體域中. 否則, 結果返回page_address
得到的地址. 如果確實在高端記憶體中, 則內核將工作委托給kmap_high
kmap_high的實現在函數mm/highmem.c?v=4.7, line 275中, 定義如下
2.3.2 kmap_high函數
/**
* kmap_high - map a highmem page into memory
* @page: &struct page to map
*
* Returns the page's virtual memory address.
*
* We cannot call this from interrupts, as it may block.
*/
void *kmap_high(struct page *page)
{
unsigned long vaddr;
/*
* For highmem pages, we can't trust "virtual" until
* after we have the lock.
*/
lock_kmap(); /*保護頁表免受多處理器系統上的併發訪問*/
/*檢查是否已經被映射*/
vaddr = (unsigned long)page_address(page);
if (!vaddr) )/* 如果沒有被映射 */
/*把頁框的物理地址插入到pkmap_page_table的
一個項中併在page_address_htable散列表中加入一個
元素*/
vaddr = map_new_virtual(page);
/*分配計數加一,此時流程都正確應該是2了*/
pkmap_count[PKMAP_NR(vaddr)]++;
BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
unlock_kmap();
return (void*) vaddr; ;/*返回地址*/
}
EXPORT_SYMBOL(kmap_high);
2.3.3 map_new_virtual函數
上文討論的page_address
函數首先檢查該頁是否已經映射. 如果它不對應到有效地址, 則必須使用map_new_virtual
映射該頁.
該函數定義在mm/highmem.c?v=4.7, line 213, 將執行下列主要的步驟.
static inline unsigned long map_new_virtual(struct page *page)
{
unsigned long vaddr;
int count;
unsigned int last_pkmap_nr;
unsigned int color = get_pkmap_color(page);
start:
count = get_pkmap_entries_count(color);
/* Find an empty entry */
for (;;) {
last_pkmap_nr = get_next_pkmap_nr(color); /*加1,防止越界*/
/* 接下來判斷什麼時候last_pkmap_nr等於0,等於0就表示1023(LAST_PKMAP(1024)-1)個頁表項已經被分配了
,這時候就需要調用flush_all_zero_pkmaps()函數,把所有pkmap_count[] 計數為1的頁表項在TLB裡面的entry給flush掉
,並重置為0,這就表示該頁表項又可以用了,可能會有疑惑為什麼不在把pkmap_count置為1的時候也
就是解除映射的同時把TLB也flush呢?
個人感覺有可能是為了效率的問題吧,畢竟等到不夠的時候再刷新,效率要好點吧。*/
if (no_more_pkmaps(last_pkmap_nr, color)) {
flush_all_zero_pkmaps();
count = get_pkmap_entries_count(color);
}
if (!pkmap_count[last_pkmap_nr])
break; /* Found a usable entry */
if (--count)
continue;
/*
* Sleep for somebody else to unmap their entries
*/
{
DECLARE_WAITQUEUE(wait, current);
wait_queue_head_t *pkmap_map_wait =
get_pkmap_wait_queue_head(color);
__set_current_state(TASK_UNINTERRUPTIBLE);
add_wait_queue(pkmap_map_wait, &wait);
unlock_kmap();
schedule();
remove_wait_queue(pkmap_map_wait, &wait);
lock_kmap();
/* Somebody else might have mapped it while we slept */
if (page_address(page))
return (unsigned long)page_address(page);
/* Re-start */
goto start;
}
}
/*返回這個頁表項對應的線性地址vaddr.*/
vaddr = PKMAP_ADDR(last_pkmap_nr);
/*設置頁表項*/
set_pte_at(&init_mm, vaddr,
&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
/*接下來把pkmap_count[last_pkmap_nr]置為1,1不是表示不可用嗎,
既然映射已經建立好了,應該賦值為2呀,其實這個操作
是在他的上層函數kmap_high裡面完成的(pkmap_count[PKMAP_NR(vaddr)]++).*/
pkmap_count[last_pkmap_nr] = 1;
/*到此為止,整個映射就完成了,再把page和對應的線性地址
加入到page_address_htable哈希鏈表裡面就可以了*/
set_page_address(page, (void *)vaddr);
return vaddr;
}
從最後使用的位置(保存在全局變數last_pkmap_nr中)開始,反向掃描pkmap_count數組, 直至找到一個空閑位置. 如果沒有空閑位置,該函數進入睡眠狀態,直至內核的另一部分執行解除映射操作騰出空位. 在到達pkmap_count的最大索引值時, 搜索從位置0開始. 在這種情況下, 還調用 flush_all_zero_pkmaps函數刷出CPU高速緩存(讀者稍後會看到這一點)。
修改內核的頁表,將該頁映射在指定位置。但尚未更新TLB.
新位置的使用計數器設置為1。如上所述,這意味著該頁已分配但無法使用,因為TLB項未更新.
set_page_address將該頁添加到持久內核映射的數據結構。 該函數返回新映射頁的虛擬地址. 在不需要高端記憶體頁的體繫結構上(或沒有設置CONFIG_HIGHMEM),則使用通用版本的kmap返回頁的地址,且不修改虛擬記憶體
2.4 kunmap解除映射
用kmap映射的頁, 如果不再需要, 必須用kunmap解除映射. 照例, 該函數首先檢查相關的頁(由page實例標識)是否確實在高端記憶體中. 倘若如此, 則實際工作委托給mm/highmem.c中的kunmap_high, 該函數的主要任務是將pkmap_count數組中對應位置在計數器減1
該機制永遠不能將計數器值降低到小於1. 這意味著相關的頁沒有釋放。因為對使用計數器進行了額外的加1操作, 正如前文的討論, 這是為確保CPU高速緩存的正確處理.
也在上文提到的flush_all_zero_pkmaps
是最終釋放映射的關鍵. 在map_new_virtual從頭開始搜索空閑位置時, 總是調用該函數.
它負責以下3個操作。
flush_cache_kmaps
在內核映射上執行刷出(在需要顯式刷出的大多數體繫結構上,將使用flush_cache_all
刷出CPU的全部的高速緩存), 因為內核的全局頁表已經修改.- 掃描整個
pkmap_count
數組. 計數器值為1的項設置為0,從頁表刪除相關的項, 最後刪除該映射。 最後, 使用flush_tlb_kernel_range函數刷出所有與PKMAP區域相關的TLB項.
2.4.1 kunmap函數
同kmap類似, 每個體繫結構都應該實現自己的kmap函數, 大多數體繫結構的定義都如下所示, 參見arch/arm/mm/highmem.c?v=4.7, line 46
void kunmap(struct page *page)
{
BUG_ON(in_interrupt());
if (!PageHighMem(page))
return;
kunmap_high(page);
}
EXPORT_SYMBOL(kunmap);
內核首先檢查待釋放記憶體區域是不是在高端記憶體區域
- 如果記憶體區域在普通記憶體區, 則內核並沒有通過kmap_high對其建立持久的內核映射, 當然也無需用kunmap_high釋放
- 如果記憶體區域在高端記憶體區, 則內核通過kunmap_high釋放該記憶體空間
2.4.2 kunmap_high函數
kunmap_high函數定義在mm/highmem.c?v=4.7, line 328
#ifdef CONFIG_HIGHMEM
/**
* kunmap_high - unmap a highmem page into memory
* @page: &struct page to unmap
*
* If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called
* only from user context.
*/
void kunmap_high(struct page *page)
{
unsigned long vaddr;
unsigned long nr;
unsigned long flags;
int need_wakeup;
unsigned int color = get_pkmap_color(page);
wait_queue_head_t *pkmap_map_wait;
lock_kmap_any(flags);
vaddr = (unsigned long)page_address(page);
BUG_ON(!vaddr);
nr = PKMAP_NR(vaddr); /*永久記憶體區域開始的第幾個頁面*/
/*
* A count must never go down to zero
* without a TLB flush!
*/
need_wakeup = 0;
switch (--pkmap_count[nr]) { /*減小這個值,因為在映射的時候對其進行了加2*/
case 0:
BUG();
case 1:
/*
* Avoid an unnecessary wake_up() function call.
* The common case is pkmap_count[] == 1, but
* no waiters.
* The tasks queued in the wait-queue are guarded
* by both the lock in the wait-queue-head and by
* the kmap_lock. As the kmap_lock is held here,
* no need for the wait-queue-head's lock. Simply
* test if the queue is empty.
*/
pkmap_map_wait = get_pkmap_wait_queue_head(color);
need_wakeup = waitqueue_active(pkmap_map_wait);
}
unlock_kmap_any(flags);
/* do wake-up, if needed, race-free outside of the spin lock */
if (need_wakeup)
wake_up(pkmap_map_wait);
}
EXPORT_SYMBOL(kunmap_high);
#endif
3 臨時內核映射
剛纔描述的kmap
函數不能用於中斷處理程式, 因為它可能進入睡眠狀態. 如果pkmap數組中沒有空閑位置, 該函數會進入睡眠狀態, 直至情形有所改善.
void *kmap_atomic(struct page *page)
{
unsigned int idx;
unsigned long vaddr;
void *kmap;
int type;
preempt_disable();
pagefault_disable();
if (!PageHighMem(page))
return page_address(page);
#ifdef CONFIG_DEBUG_HIGHMEM
/*
* There is no cache coherency issue when non VIVT, so force the
* dedicated kmap usage for better debugging purposes in that case.
*/
if (!cache_is_vivt())
kmap = NULL;
else
#endif
kmap = kmap_high_get(page);
if (kmap)
return kmap;
type = kmap_atomic_idx_push();
idx = FIX_KMAP_BEGIN + type + KM_TYPE_NR * smp_processor_id();
vaddr = __fix_to_virt(idx);
#ifdef CONFIG_DEBUG_HIGHMEM
/*
* With debugging enabled, kunmap_atomic forces that entry to 0.
* Make sure it was indeed properly unmapped.
*/
BUG_ON(!pte_none(get_fixmap_pte(vaddr)));
#endif
/*
* When debugging is off, kunmap_atomic leaves the previous mapping
* in place, so the contained TLB flush ensures the TLB is updated
* with the new mapping.
*/
set_fixmap_pte(idx, mk_pte(page, kmap_prot));
return (void *)vaddr;
}
EXPORT_SYMBOL(kmap_atomic);
這個函數不會被阻塞, 因此可以用在中斷上下文和起亞不能重新調度的地方. 它也禁止內核搶占, 這是有必要的, 因此映射對每個處理器都是唯一的(調度可能對哪個處理器執行哪個進程做變動).
3.2 kunmap_atomic函數
可以通過函數kunmap_atomic取消映射
/*
* Prevent people trying to call kunmap_atomic() as if it were kunmap()
* kunmap_atomic() should get the return value of kmap_atomic, not the page.
*/
#define kunmap_atomic(addr) \
do { \
BUILD_BUG_ON(__same_type((addr), struct page *)); \
__kunmap_atomic(addr); \
} while (0)
這個函數也不會阻塞. 在很多體繫結構中, 除非激活了內核搶占, 否則kunmap_atomic根本無事可做, 因為只有在下一個臨時映射到來前上一個臨時映射才有效. 因此, 內核完全可以”忘掉”kmap_atomic映射, kunmap_atomic也無需做什麼實際的事情. 下一個原子映射將自動覆蓋前一個映射.