物理地址空間佈局
CPU所訪問的都是虛擬內存地址。通常32位Linux內核地址空間劃分0~3G爲用戶空間,3~4G爲內核空間。
Linux系統在初始化時,會根據實際的物理內存的大小,爲每個物理頁面創建一個page對象,所有的page對象構成一個mem_map數組。
進一步,針對不同的用途,Linux內核將所有的物理頁面劃分到3類內存管理區中,如圖,分別爲ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA的範圍是0~16M,該區域的物理頁面專門供I/O設備的DMA使用。之所以需要單獨管理DMA的物理頁面,是因爲DMA使用物理地址訪問內存,不經過MMU,並且需要連續的緩衝區,所以爲了能夠提供物理上連續的緩衝區,必須從物理地址空間專門劃分一段區域用於DMA。
ZONE_NORMAL的範圍是16M~896M,該區域的物理頁面是內核能夠直接使用的。
ZONE_HIGHMEM的範圍是896M~結束,該區域即爲高端內存,內核不能直接使用。
爲什麼有高端內存的概念?
當內核模塊代碼或線程訪問內存時,代碼中的內存地址都爲邏輯地址,而對應到真正的物理內存地址,需要地址一對一的映射,假設簡單的地址映射關係,那麼內核邏輯地址空間訪問爲0xc0000000 ~ 0xffffffff,那麼只能訪問1G物理內存。
若機器中安裝8G物理內存,那麼內核就只能訪問前1G物理內存,後面7G物理內存將會無法訪問。顯然不能將內核地址空間0xc0000000 ~ 0xfffffff全部用來簡單的地址映射。因此32bit OS將內核地址空間劃分三部分:ZONE_DMA、ZONE_NORMAL和 ZONE_HIGHMEM。ZONE_HIGHMEM即爲高端內存.
只有1G的線性地址空間,如何使用大於1G的物理內存?這就是高端內存的由來。
內核不能把1G的線性地址全部用來直接內存地址映射,而是需要保留128M的地址空間用來映射896M以上的內存地址。
舉個簡單的例子:
從公司下班回家需要乘坐328公交,公交車有30個固定的位置以及可以容納20人站立的過道,公交途徑很多站,假設公交一開始三十個位置已經固定有人,且中途不下車,在各個站臺,過道的乘客上上下下。
328公交只能載客50人嗎?顯然不是,到達終點站時,售票員發現售出了150張票。
如何將50人載客量的公交實際載客150人?那就是通過在不同的站臺上上下下,但任一時刻,公交最多容納50人。
與之類比,內核空間中896M對應公交的30個固定位置,用作直接映射。高端內存的128M對應讓乘客站着的空間,用作臨時映射,這樣可以通過128M的線性地址空間,訪問超過1G的物理內存。
linux 高端內存的劃分
內核將高端內存劃分爲3部分:
VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。
對 於高端內存,可以通過 alloc_page() 或者其它函數獲得對應的 page,但是要想訪問實際物理內存,還得把 page 轉爲線性地址才行(爲什麼?想想 MMU 是如何訪問物理內存的),也就是說,我們需要爲高端內存對應的 page 找一個線性空間,這個過程稱爲高端內存映射。
對應高端內存的3部分,高端內存映射有三種方式:
映射到”內核動態映射空間”(noncontiguous memory allocation)
這種方式很簡單,因爲通過 vmalloc() ,在”內核動態映射空間”申請內存的時候,就可能從高端內存獲得頁面(參看 vmalloc 的實現),因此說高端內存有可能映射到”內核動態映射空間”中。
持久內核映射(permanent kernel mapping)
如果是通過 alloc_page() 獲得了高端內存對應的 page,如何給它找個線性空間?
內核專門爲此留出一塊線性空間,從 PKMAP_BASE 到 FIXADDR_START ,用於映射高端內存。在 2.6內核上,這個地址範圍是 4G-8M 到 4G-4M 之間。這個空間起叫”內核永久映射空間”或者”永久內核映射空間”。這個空間和其它空間使用同樣的頁目錄表,對於內核來說,就是 swapper_pg_dir,對普通進程來說,通過 CR3 寄存器指向。通常情況下,這個空間是 4M 大小,因此僅僅需要一個頁表即可,內核通過來 pkmap_page_table 尋找這個頁表。通過 kmap(),可以把一個 page 映射到這個空間來。由於這個空間是 4M 大小,最多能同時映射 1024 個 page。因此,對於不使用的的 page,及應該時從這個空間釋放掉(也就是解除映射關係),通過 kunmap() ,可以把一個 page 對應的線性地址從這個空間釋放出來。
臨時映射(temporary kernel mapping)
內核在 FIXADDR_START 到 FIXADDR_TOP 之間保留了一些線性空間用於特殊需求。這個空間稱爲”固定映射空間”在這個空間中,有一部分用於高端內存的臨時映射。
非連續內存的管理
非連續內存的線性地址空間是從VMALLOC_START~VMALLOC_END,共128MB大小。當內核需要用vmalloc類的函數進行非連續內存分配時,就會申請一個vm_struct結構來描述對應的vmalloc區,若分配多個vmalloc的內存區,那麼相鄰兩個vmalloc區之間的間隔大小至少爲4KB,即至少是一個頁框大小。
vmalloc是內核中使用到的內存分配函數,一般用來分配大塊內存,
這個函數得到的是連續的虛擬地址,物理上地址不連續。
連續物理內存的分配並不是必要的。對於大部分DMA操作,我們的確需要連續的物理內存;但是對於某些分配內存情況:比如,模塊加載,設備和聲音驅動程序中,可以在內核源碼中關鍵字vmalloc查找,對vmalloc的使用有個感性認識。
內核中描述非連續區的數據結構是struct vm_struct:
struct vm_struct {
struct vm_struct *next; //指向下一個vm_struct區,所有非連續區組成一個單鏈表
void *addr; //代表每個內存區的起始地址,即指向申請的內存區的第一個內存單元(線性地址)
unsigned long size; //當前所申請的內存區大小加4KB(安全區)
unsigned long flags; //標識內存區類型
struct page **pages; //指向nr_pages頁描述符指針數組的指針
unsigned int nr_pages; //所申請的內存區大小對應的頁框數
phys_addr_t phys_addr; //該字段一般爲0,除非內存已經被申請用作映射一個硬件設備的I/O共享內存
const void *caller; //當前調用vmalloc類的函數的返回地址
};
1分配非連續的內存區
分配函數主要是vmalloc(),vmap(),vmalloc()會去調用__vmalloc_node_range()函數:
void *__vmalloc_node_range(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, unsigned long vm_flags, int node,
const void *caller)
{
struct vm_struct *area;
void *addr;
unsigned long real_size = size;
//size要對其爲4K的整數倍,因爲非連續內存區域是將各個物理頁進行映射
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > totalram_pages)
goto fail;
//找到一塊空閒的線性地址區域,用來映射該非連續內存
area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
vm_flags, start, end, node, gfp_mask, caller);
if (!area)
goto fail;
addr = __vmalloc_area_node(area, gfp_mask, prot, node);
if (!addr)
return NULL;
/*
* In this function, newly allocated vm_struct has VM_UNINITIALIZED
* flag. It means that vm_struct is not fully initialized.
* Now, it is fully initialized, so remove this flag here.
*/
clear_vm_uninitialized_flag(area);
/*
* A ref_count = 2 is needed because vm_struct allocated in
* __get_vm_area_node() contains a reference to the virtual address of
* the vmalloc'ed block.
/
/*一個內存塊分配的通知*/
kmemleak_alloc(addr, real_size, 2, gfp_mask);
return addr;
fail:
warn_alloc(gfp_mask,
"vmalloc: allocation failure: %lu bytes", real_size);
return NULL;
}
分配物理頁面 並映射
__vmalloc_area_node:
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, int node)
{
struct page **pages;
unsigned int nr_pages, array_size, i;
const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;
// 計算想要分配的物理頁數
nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
array_size = (nr_pages * sizeof(struct page *));
area->nr_pages = nr_pages;
/* Please note that the recursion is strictly bounded. */
//array_size大於PAGE_SIZE的話,就需要遞歸調用__vmalloc_node申請內存,
//否則直接調用kmalloc_node申請內存
if (array_size > PAGE_SIZE) {
pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
PAGE_KERNEL, node, area->caller);
} else {
pages = kmalloc_node(array_size, nested_gfp, node);
}
area->pages = pages;
if (!area->pages) {
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
//爲非連續內存進行頁面的分配,每次分配一個頁面,將其頁框指針記錄在pages數組中,可以看到物理內存地址不是連續的
for (i = 0; i < area->nr_pages; i++) {
struct page *page;
if (node == NUMA_NO_NODE)
page = alloc_page(alloc_mask);
else
page = alloc_pages_node(node, alloc_mask, 0);
if (unlikely(!page)) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
area->pages[i] = page;
if (gfpflags_allow_blocking(gfp_mask))
cond_resched();
}
//將虛擬地址跟每個物理頁面建立映射
if (map_vm_area(area, prot, pages))
goto fail;
return area->addr;
fail:
warn_alloc(gfp_mask,
"vmalloc: allocation failure, allocated %ld of %ld bytes",
(area->nr_pages*PAGE_SIZE), area->size);
vfree(area->addr);
return NULL;
}
持久內核映射
從 PKMAP_BASE 到 FIXADDR_START,爲內核的持久映射空間。
通過kmap函數實現,將高端內存長期映射到內核地址空間中。
kmap的內核實現:
void *kmap(struct page *page)
{
BUG_ON(in_interrupt());
if (!PageHighMem(page))
return page_address(page);
return kmap_high(page);
}
如果給定的page不是高端物理頁面,直接通過page_address返回該頁面的虛擬地址
否則調用kmap_high建立高端映射.
kmap_high:
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)
vaddr = map_new_virtual(page);
pkmap_count[PKMAP_NR(vaddr)]++;
BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
unlock_kmap();
return (void*) vaddr;
}
在kmap_high函數中先調用page_address檢查該page是否已經被映射。如果沒有映射調用map_new_virtual(page)處理。
pkmap_count是一個計數數組,標示該頁被引用的次數。 每一個持久地址映射項都對應一個計數.
對於這個計數,我們主要區分三種情況:
1. count= 0, 相應的頁表項還沒有映射到高端物理內存頁框,可以使用它
2. count = 1,相應的頁表項沒有映射到高端物理內存頁框,但是現在不能使用它,因爲自從上次使用過後,相應的TLB項還沒有刷新。
3. count > 1,相應的頁表項已經映射到高端物理內存頁框,使用者的數目是n - 1
步驟:通過kmap將傳入的page*映射到虛擬內存時分以下幾步:1.在內核地址空間(持久映射區)分配一個頁;2.建立傳入的物理內存頁和虛擬地址之間的映射;3.統計內核地址空間中哪些頁被引用。
map_new_virtual:
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);
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 = 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;
set_page_address(page, (void *)vaddr);
return vaddr;
}
1.從上一次使用的位置last_pkmap_nr開始反向掃描pkmap_count數組,直到找到一個空閒位置,如果沒有空閒位置,該函數進入睡眠狀態,直到內核的另一部分執行解除映射操作。
2.調用set_pte_at()修改內核頁表,將該頁映射到指定位置,但尚未更新TLB。
3.pkmap_count[last_pkmap_nr] = 1;新位置的使用計數器設置爲1,表示該分頁已分配,但是無法使用。因爲TLB項未更新。
4.調用set_page_address()將該頁添加到內核映射的數據結構。
set_page_address:
/**
* set_page_address - set a page's virtual address
* @page: &struct page to set
* @virtual: virtual address to use
*/
void set_page_address(struct page *page, void *virtual)
{
unsigned long flags;
struct page_address_slot *pas;
struct page_address_map *pam;
BUG_ON(!PageHighMem(page));
pas = page_slot(page);
if (virtual) { /* Add */
pam = &page_address_maps[PKMAP_NR((unsigned long)virtual)];
pam->page = page;
pam->virtual = virtual;
spin_lock_irqsave(&pas->lock, flags);
list_add_tail(&pam->list, &pas->lh);
spin_unlock_irqrestore(&pas->lock, flags);
} else { /* Remove */
spin_lock_irqsave(&pas->lock, flags);
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
list_del(&pam->list);
spin_unlock_irqrestore(&pas->lock, flags);
goto done;
}
}
spin_unlock_irqrestore(&pas->lock, flags);
}
done:
return;
}
該函數爲page指針和虛擬地址建立一個page_address_map結構來映射,並將結構添加到page_address_maps全局hash鏈表中。
總結步驟:
1.當調用kmap函數,傳入一個物理頁幀page的指針時,會返回該page關聯的虛擬地址。kmap函數中先判斷該page是低端內存還是高端內存,如果是低端內存,則直接調用page_address函數,通過直接映射關係,計算出該page關聯的虛擬地址;如果傳入kmap函數的page對應的是高端內存的物理頁幀,則調用kmap_high函數處理。
2.進入在kmap_high函數中,說明入參page對應高端內存的物理頁幀,仍然要先調用page_address判斷該物理頁是否已經關聯了虛擬地址。如果已經關聯則將對應的虛擬地址在pkmap_count中的項+1,返回該page對應的虛擬地址。如果沒有關聯,則調用map_new_virtual函數,給該page建立關聯的物理地址。
3.在map_new_virtual函數中,先通過掃描pkmap_count中的項爲該page找到未用的虛擬地址,然後修改頁表,便於通過虛擬地址找到物理地址。然後調用set_page_address函數爲page和虛擬地址建立映射結構page_address_map,並添加到上圖所示的結構中。
臨時映射
臨時內核映射實現簡單,可以用在中斷處理程序和可延遲函數的內部(這些函數不能被阻塞),因爲臨時內核映射從來不阻塞當前進程,因爲它被設計成是原子的。對比永久內核映射,發現如果頁框暫時沒有空閒的虛擬地址可以映射,那麼永久內核映射將要被阻塞。
建立臨時內核映射禁用內核搶佔,這是必須的,因爲映射對於每個處理器都是獨特的,如果沒有禁用搶佔,那麼哪個任務在哪個CPU上運行是不確定的。
臨時映射通過kmap_atomic函數實現:
void *kmap_atomic(struct page *page)
{
int idx, cpu_idx;
unsigned long vaddr;
/**
*原子映射是基於每個cpu的,因此在當前cpu上應用搶佔,直到unmap的時候才
*開啓,這樣不會導致原子映射的重入了,
*/
preempt_disable();
pagefault_disable();
if (!PageHighMem(page))
return page_address(page);
//遞增一個每cpu變量,返回遞增後的結果
cpu_idx = kmap_atomic_idx_push();
/*
*kernel可以在多個cpu上同時運行不同的task,他們共同使用一個內存地址空間
*也就是說,內存空間對於多個cpu看到的是同一個,該函數使用的是地址空間中頂部的
*一小段地址空間,也就是臨時映射區,內核邏輯將這一小段地址空間分成若干各節
*每一節的大小是一個頁面的大小,可以映射一個頁面,根據公用地址空間的原理
*所有的cpu共同使用這些節,因此如何能保證N個cpu調用此函數不會將page映射一個地址呢,
*這就是這個數學公式所起到的作用
*/
idx = cpu_idx + KM_TYPE_NR * smp_processor_id();
//固定映射的線性地址轉化成虛擬地址
vaddr = FIXMAP_ADDR(idx);
//設置頁表
set_pte_at(&init_mm, vaddr, fixmap_page_table + idx,
mk_pte(page, kmap_prot));
return (void *)vaddr;
}
EXPORT_SYMBOL(kmap_atomic);
調用kmap_atomic的時候,有一個參數是的枚舉類型km_type,不同的cpu得到的vaddr不一樣,保證N個cpu調用此函數不會將page映射一個地址。