Linux內存管理第九章 -- High Memory Management(高端內存)

Linux內存管理第九章 – High Memory Management

kernel僅僅能夠直接訪問那些已經建立了頁表項的物理內存。但是在大多數情況下,在32位機器上,用戶空間和內核空間被切割爲3GB/1GB,這就意味着內核最多能夠直接訪問的內存爲896MB。但是在64位機器上,這不是一個問題,因爲64位機器上有足夠的虛擬地址空間。
在很多32位機器的都超過了1GB的物理內存,內核如何訪問超過896MB的物理內存是個不可忽視的問題。Linux使用的解決方案是將high memory zone中的物理內存臨時映射到lower page table,至於如何映射,後面回來詳細討論。
high memory和IO有一個相互關聯的問題:它們必須互相訪問,因爲不是所有設備都能訪問high memory,不是所有memory都是對CPU可用的。舉個例子:如果CPU的PAE擴展功能被打開了,硬件設別能夠訪問的內存大小是2GB或者使用64位系統。如果設備寫入內存失敗時最好的情況,最壞的時刻是干擾了內核的內存。解決該問題的辦法是使用bounce buffer,後續會詳細討論。
本章從簡潔地描述PKMap地址空間如何被管理開始,然後再來討論page如何映射被取消映射到虛擬高端內存區域。然後再將一下映射的原子操作性進而深入到bounce buffer。最後再來討論下在memory很緊張的情況下如何使用emergency pools。

Managing the PKMap Address Space

PKMap地址空間是從PKMAP_BASE到FIXADDR_START,該區間位於內核頁表的top位置。該預留的空間非常有限。在x86上,PKMAP_BASE是在0xFE000000,而FIXADDR_START的值依賴於在編譯階段的配置選項,但該區域通常是線性地址空間尾部的一些pages大小。這也就是說只有少於32MB的頁表空間用戶將high memory映射到可用的虛擬地址空間。PKMap地址空間具體位置參考下圖:
temp

Mapping High Memory Pages(臨時映射)

用於映射high memory的API見下表。映射一個page的主要函數是kmap().對於不想要阻塞的用戶來說,可以使用kmap_noblock(),對於不想在映射過程中被中斷的用戶可以使用kmap_atomic()。由於kmap pool非常小因此調用kmap()的使用者應儘可能快速再調用kumap()因爲相比於low memory,high memory的size越大這個映射小窗口的壓力就越大。

void * kmap(struct page *page)
將一個high memory的struct page映射到low memory,返回映射後的虛擬地址
void * kmap_nonblock(struct page *page)
功能和kmap()一樣,當沒有可用的slot的時候,該函數不會則塞
void * kmap_atomic(struct page *page, enum km_type type)
kmap空間中有些slot是以原子使用的方式給中斷使用。該函數不會被鼓勵使用因爲調用者不會進入睡眠和調度狀態。該函數用於將一個high memory page進行原子操作映射而達到特殊的目的

函數kmap()自身非常簡單。它首先通過函數might_sleep()來檢查調用它的調用者不是中斷處理函數因爲該函數可能再次被中斷。然後再檢查輸入參數struct *page是否小於highmem_start_page因爲低於這個值得struct *page已經是可見的了,不需要再次被映射。
如果檢查發現struct *page已經處於low memory中,那麼久簡單地返回它所對應的虛擬地址。通過這樣的方式就不要調用者自己來判斷每個struct *page是否已經處於low memory中,無論怎麼樣,該函數都是安全的。
如果檢查發現是一個high page需要被映射,那麼就調用kmap_high()來開始做真正的工作。kmap_high()函數首先檢查page->virtual是否爲NULL,不爲NULL,則表明已經被映射過了。如果爲NULL,再調用map_new_virtual()來映射該page。

void *kmap(struct page *page) {
	might_sleep();
	if (page < highmem_start_page)
		return page_address(page);
	return kmap_high(page);
}
void fastcall *kmap_high(struct page *page)
{
	unsigned long vaddr;
	/*
	 * For highmem pages, we can't trust "virtual" until
	 * after we have the lock.
	 *
	 * We cannot call this from interrupts, as it may block
	 */
	spin_lock(&kmap_lock);
	vaddr = (unsigned long)page_address(page);
	if (!vaddr)
		vaddr = map_new_virtual(page);
	pkmap_count[PKMAP_NR(vaddr)]++;
	if (pkmap_count[PKMAP_NR(vaddr)] < 2)
		BUG();
	spin_unlock(&kmap_lock);
	return (void*) vaddr;
}

使用map_new_virtual()函數來創建一個新的虛擬映射是一個線性掃描pkmap_count的簡單case。每次掃描的起始偏移爲last_pkmap_nr而不是0是爲防止前後兩次kmap()中重複掃描相同的區域。當last_pkmap_nr接近到最大值時,將會調用flush_all_zero_pkmaps()先將pkmap_count數組中值爲1的項(即沒有人使用該page),將其對應的case頁表項清除。
如果掃描一輪之後仍然沒有空閒的entry,當前調用進程會被添加到pkmap_map_wait等待隊列直到下一個kunmap()調用釋放一個空閒entry。
也就是pkmap_count[ ]數組全部被用完了,先解除映射,然後再等待kunmap()到來。
一旦一個新的映射被創建,pkmap_count數組使用數也會增加然後返回對應的low memory虛擬地址。

#define PKMAP_NR(virt)  ((virt-PKMAP_BASE) >> PAGE_SHIFT)
//PKMAP_BASE - FIXADDR_START之間的地址整個page,一個page,一個page往後映射,
//將每個page的起始虛擬地址記錄到struct page->virtual中
#define PKMAP_ADDR(nr)  (PKMAP_BASE + ((nr) << PAGE_SHIFT))

Unmapping Pages

解除一個high memory page映射的函數如下表。kumap()函數主要做兩個檢查:第一,判斷該調用是否處於中斷上下文,第二,檢查page是否低於highmem_start_page,如果是,該函數已經處於low memory不需要做進一步處理。一旦需要將一個page解除映射,就調用kumap()來執行解除操作。

void kunmap(struct page *page)
將一個struct page從low memory中解除映射並釋放它所映射的頁表entry
void kunmap_atomic(void *kvaddr, enum km_type type)
將一個page原子性解除映射,即解除過程不會被中斷

而kumap_high()的原理非常簡單。將 pkmap_count對應的元素值減一。如果pkmap_count某個元素的值變爲1,即該映射地址沒有人再使用了,則pkmap_map_wait等待隊列的進程將會被喚醒來使用這個空閒的slot。該page此時並不立即從頁表中解除映射因爲這需要刷新TLB,這個動作被推遲到flush_all_zero_pkmaps()時執行。

void fastcall kunmap_high(struct page *page)
{
	unsigned long vaddr;
	unsigned long nr;
	int need_wakeup;

	spin_lock(&kmap_lock);
	vaddr = (unsigned long)page_address(page);
	if (!vaddr)
		BUG();
	nr = PKMAP_NR(vaddr);

	/*
	 * A count must never go down to zero
	 * without a TLB flush!
	 */
	need_wakeup = 0;
	switch (--pkmap_count[nr]) {
	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.
		 */
		need_wakeup = waitqueue_active(&pkmap_map_wait);
	}
	spin_unlock(&kmap_lock);

	/* do wake-up, if needed, race-free outside of the spin lock */
	if (need_wakeup)
		wake_up(&pkmap_map_wait);
}

這也就是說,不用每次釋放一個page就是刪除一個頁表項,而是先把這些釋放的slot攢着,到快沒有slot可用的時候再集中清空頁表,刷新TLB。因爲只要頁表由變化就需要刷新TLB,而刷新TLB代價很大,不能頻繁刷新。

Mapping High Memory Pages Atomically(固定映射)

kmap_atomic()不被鼓勵使用但是當有必要的時候仍然爲每個CPU預留一些slots,比如當bounce buffer在中斷中被驅動設別使用。一種硬件架構需要很多不同的使用原子映射high memory的需求,這些需求用km_type所列舉。全部kmap_atimic()需求數量爲KM_TYPE_NR。在x86上,總共有6種不同的使用atomic kmaps的情況。
下圖中標紅色的位置就是固定映射區:
gyfd
系統在啓動階段在FIX_KMAP_BEGIN ~ FIX_KMAP_END中爲每一個處理器預留KM_TYPE_NR個atomic mapping entry。因此很明顯可以看出一個調用atomic kmap的用戶不會進入睡眠或者退出直到自己調用kumap_atomic(),因爲該處理器中的下一個進程可能也要用相同的entry但是會fail。
下面再看看具體的示意圖:
kmap
下面來看下kmap_atomic()源碼:

void *kmap_atomic(struct page *page, enum km_type type)
{
	enum fixed_addresses idx;
	unsigned long vaddr;

	/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
	inc_preempt_count();
	if (page < highmem_start_page)
		return page_address(page);

	idx = type + KM_TYPE_NR*smp_processor_id();//計算每個CPU的映射類型爲type的偏移
	vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);//從FIX_TOP 減掉(FIX_KMAP_BEGIN + idx)*PAGE_SHIFT 即爲該固定映射的虛擬地址
#ifdef CONFIG_DEBUG_HIGHMEM
	if (!pte_none(*(kmap_pte-idx)))
		BUG();
#endif
	set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));
	__flush_tlb_one(vaddr);

	return (void*) vaddr;
}

void kunmap_atomic(void *kvaddr, enum km_type type)
{
#ifdef CONFIG_DEBUG_HIGHMEM
	unsigned long vaddr = (unsigned long) kvaddr & PAGE_MASK;
	enum fixed_addresses idx = type + KM_TYPE_NR*smp_processor_id();
	if (vaddr < FIXADDR_START) { // FIXME
		dec_preempt_count();
		preempt_check_resched();
		return;
	}
	if (vaddr != __fix_to_virt(FIX_KMAP_BEGIN+idx))
		BUG();
	/*
	 * force other mappings to Oops if they'll try to access
	 * this pte without first remap it
	 */
	pte_clear(kmap_pte-idx);//清除頁表項
	__flush_tlb_one(vaddr);//每次釋放都刷新TLB中的特定頁表項
#endif
	dec_preempt_count();
	preempt_check_resched();
}

Bounce Buffers

有一些設備不能夠訪問CPU可見的全部範圍的內存空間,Bounce Buffers對於這些設備來說就非常重要了。一個非常明顯的例子就是當一些硬件設備地址總線的位數比CPU的地址總線位數少的時候,例如:32位的設備跑在64位架構中,或者因特爾處理器開啓PAE.

Bounce Buffers的基本概念非常簡單。駐留在low memory中的Bounce Buffers只要地址夠低就能夠滿足這些特殊設備從Bounce Buffers中讀數據或者向Bounce Buffers中寫數據,然後再將這些數據copy到high memory中。額外的以此copy是不期望看到的,但是確實無法避免的。在low memory中分配的pages作爲page buffer供DMA向這些特殊設備來回傳遞數據。當IO完成之後,這些數據將被kernel copy到high memory的buffer page中,因此Bounce Buffers扮演了一種橋樑的作用。Bounce Buffers有很大的開銷因爲至少會引起整個page的拷貝,但是相比較於將low memory換出到磁盤的消耗來說,這也算不上很大的消耗。

Disk Buffering

通過slab allocator分配的struct buffer_head來管理pages中的大小約爲1KB放入blocks。buffer_head的使用者擁有註冊回調函數的選項。這個回調函數被存儲在buffer_head->b_end_io()然後當IO完成的時候調用該回調函數。這種機制是bounce buffers自己將數據從bounce buffers拷貝出來,該回調函數爲bounce_end_io_write()。
關於buffer heads其他特性或者buffer heads如何被block layer所使用超越了當前文章的範圍。其餘IO layer更加相關。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章