kmap的實現分析與實驗

kmap的實現分析
kmap/unkmap系統調用是用來映射高端物理內存頁到內核地址空間的api函數,他們分配的內核虛擬地址範圍屬於[PKMAP_BASE,PAGE_OFFSET]即[0xbfe00000,0xc0000000]範圍,大小是2M的虛擬空間,爲了映射該塊虛擬地址,所使用的二級頁表的大小剛好是一個物理page的總計是兩個pte table(4KB)
kmap的調用流程分析:
arch/arm/mm/highmem.c
void *kmap(struct page *page)
{
	might_sleep();
	if (!PageHighMem(page)){//如果是低端內存,則直接返內存頁對應的直接映射虛擬地址
		//printk("low mem page\n");
		return page_address(page);//所有的低端內存,在內核初始化時就已經映射好了,並且是不變得,且物理到虛擬相差0xc0000000
	}else{
		//printk("high mem page\n");
	}
	return kmap_high(page);//高端內存頁
}

進入/trunk/mm/highmem.c的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)//如果該頁的映射還未建立
		vaddr = map_new_virtual(page);//開始建立新的映射
	pkmap_count[PKMAP_NR(vaddr)]++;//該數組的值爲1,說明映射已經建立,爲2表明該應聲存在着引用
	BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
	unlock_kmap();
	return (void*) vaddr;
}

進入到map_new_virtual函數:
static inline unsigned long map_new_virtual(struct page *page)
{
	unsigned long vaddr;
	int count;

start:
	count = LAST_PKMAP; // 2MB/4096KB=512 entries = LAST_PKMAP

	/* Find an empty entry */
	for (;;) {
		last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
		if (!last_pkmap_nr) {
			flush_all_zero_pkmaps();
			count = LAST_PKMAP;
		}
		if (!pkmap_count[last_pkmap_nr])//爲0,說明該虛擬地址不存在映射,沒人使用
			break;	/* Found a usable entry */
		if (--count)//如果遍歷了整個kmap虛擬空間,都不能找到空閒的虛擬地址,則休眠等待unkmap釋放虛擬地址
			continue;

		/*
		 * Sleep for somebody else to unmap their entries
		 */
		{
			DECLARE_WAITQUEUE(wait, current);

			__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);//#define PKMAP_ADDR(nr)		(PKMAP_BASE + ((nr) << PAGE_SHIFT))
	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;
}

上面代碼中的pkmap_page_table是kmap所對應的虛擬地址[PKMAP_BASE,PAGE_OFFSET]所對應的二級映射表,即pte table,該映射表剛好是4KB用來映射2MB的虛擬到物理地址
pkmap_page_table使在trunk/arch/arm/mm/mmu.c文件中設置的:

static void __init kmap_init(void)
{
#ifdef CONFIG_HIGHMEM
        //獲取kmap所對應的虛擬地址[PKMAP_BASE,PAGE_OFFSET]所對應的二級映射表的開始地址。該二級映射表剛好就是一個物理頁的大小
	pkmap_page_table = early_pte_alloc_and_install(pmd_off_k(PKMAP_BASE),
		PKMAP_BASE, _PAGE_KERNEL_TABLE);
	printk("************************************************\n");
	printk("pkmap_page_table:%x, phy of pkmap_page_table:%x\n",pkmap_page_table,virt_to_phys(pkmap_page_table));
	printk("************************************************\n");
#endif
}
上述函數中的pmd_off_k(PKMAP_BASE)是獲取PKMAP_BASE虛擬地址對應的一級映射表中所對應的頁表項地址,

static pte_t * __init early_pte_alloc_and_install(pmd_t *pmd,
	unsigned long addr, unsigned long prot)
{
	if (pmd_none(*pmd)) {//如果一級頁表項無效,即還未分配該表項所指向二級頁表,即pte table
		pte_t *pte = early_pte_alloc(pmd);//分配二級頁表,即pte tabble
		early_pte_install(pmd, pte, prot);//將pte table的hw/pte page0,hw/pte page1分別填充到一級頁表項的低4byte和高4byte

	}
	BUG_ON(pmd_bad(*pmd));
	return pte_offset_kernel(pmd, addr);//返回二級頁表中對應的頁表項地址。
}

以上過程,具體見下圖的映射關係圖1




kmap的實驗

kmap試驗目的:

a:kmap映射高端內存頁返回的地址是否屬於0xbfe00000 - 0xc0000000範圍。
b:kmap的二級映射表的虛擬地址:pkmap_page_table,在函數kmap_init中打印該虛擬地址和對應的物理地址,然後根據二級映射表的結構,找到kmap返回的虛擬地址對應的物理地址。
再根據該物理地址,使用mu內存查看工具,查看該物理頁的內容是否是我們之前在驅動中通過kmap返回的虛擬地址設置的特殊數值。

詳細的測試代碼如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/delay.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <linux/list.h>
#include <linux/pci.h>
#include <linux/gpio.h>
#include <linux/gfp.h>
#include <asm/highmem.h>
下面的函數,是通過kmap分配高端內存頁,並且將分配得到的內存頁,都特別的設置成特殊的數據,依次爲:0x5a,0x5b,0x5c
struct page * map_high_mem(int order)
{
	int i=0;
	static int poison = 0x5a;
	unsigned char *buf = NULL;
	struct page *high_page = alloc_pages(__GFP_HIGHMEM,order);	//指定可以從高端內存分配物理空閒頁
	//struct page *high_page = alloc_pages(GFP_HIGHUSER,order);
	if(high_page){
		printk("high_page alloc success\n");
	}else{
		printk("high_page alloc failed\n");
	}
	buf = kmap(high_page);//爲該高端內存頁,建立臨時映射,該函數可能休眠
	if(buf){
		printk("kmap success,buf addr:%x\n",buf);//如果映射成功,返回影射後的虛擬地址
		for(i=0;i<4096;i++)
			buf[i] = poison;
		poison++;
	}else{
		printk("kmap failed\n");
	}
	return high_page;
}

void free_high_mem(struct page *page,int order)
{
	kunmap(page);//拆除映射
	__free_pages(page,order);//釋放對應物理頁
}


struct page *page_array[5];
#define NUM_ORDER 0
static int __init dev_init(void)
{
	int ret;
	int i;
	/*************************************************************/	
	i = 0;
	page_array[i++] = map_high_mem(NUM_ORDER);//連續分配,映射三個物理頁
	page_array[i++] = map_high_mem(NUM_ORDER);
	page_array[i++] = map_high_mem(NUM_ORDER);
	
	printk("module address,page_array:0x%x\n",page_array);
	return ret;
}


static void __exit dev_exit(void)
{

	int i = 0;
	free_high_mem(page_array[i++],NUM_ORDER);
	free_high_mem(page_array[i++],NUM_ORDER);
	free_high_mem(page_array[i++],NUM_ORDER);
}
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LKN@SCUT");

以上是我們的測試代碼,代碼編譯,加載執行。

以下log是得到pkmap_page_table的物理地址,即kmap的二級映射表的開始物理地址,該log是內核啓動階段在kmap_init函數打印出來的。
[    0.000000] ************************************************
[    0.000000] pkmap_page_table:ef7fc000, phy of pkmap_page_table:2f7fc000(759MB)
[    0.000000] ************************************************
由於kmap是2MB的虛擬空間,剛好一個page大小的二級映射表就可以完全覆蓋到。如上的log所顯示,這個二級映射表的開始物理地址是:2f7fc000。

模塊加載時的log:
/storage/sdcard1 # insmod mymap.ko 
[   83.289132] kernel buffer virtial address:ee155000
[   83.293936] kernel buffer physical address:2e155000
[   83.298801] high_page alloc success
[   83.302264] kmap success,buf addr:bfeee000------------->(a)  //第一次kmap映射返回的虛擬地址,將該頁都初始化爲0x5a
[   83.306393] high_page alloc success
[   83.309809] kmap success,buf addr:bfeef000------------->(b)  //第二次kmap映射返回的虛擬地址,將該頁都初始化爲0x5b
[   83.313990] high_page alloc success
[   83.317414] kmap success,buf addr:bfef0000------------->(c)   //第三次kmap映射返回的虛擬地址,將該頁都初始化爲0x5c


可見三次kmap返回的虛擬地址都是屬於0xbfe00000 - 0xc0000000範圍。

根據他們返回的虛擬地址,我們再結合之前得出的二級映射表的物理地址,我們推算出該三個虛擬地址所對應的物理內存頁,分別如下:

由程序的虛擬地址得到對應的物理地址的公式爲:virt_addr----->phy_addr
二級映射表的開始物理地址+2KB的硬件頁表頁內偏移+virt_addr[bit20,bit12] * 4 上存儲的內容即爲虛擬頁對應的物理頁

case a: bfeee000---------->374ce000
2f7fc000 + 1024*2(800) + 238*4(3b8) = 2F7FCBB8(二級映射表項的物理地址)
0x37ce000即是虛擬頁bfeee000對應的物理頁,我們可以看到,該頁上的內容剛好就是我們之前設置的0x5a。




case b: bfeef000---------->37b0c000
2f7fc000 + 1024*2(800) + 238*4(3bc) = 2F7FCBBC(二級映射表項的物理地址)

37b0c000即是虛擬頁bfeef000對應的物理頁,我們可以看到,該頁上的內容剛好就是我們之前設置的0x5b。



case c: bfef0000---------->37b0b000
2f7fc000 + 1024*2(800) + 240*4(3C0) = 2F7FCBC0(二級映射表項的物理地址)



37b0b000即是虛擬頁bfef0000對應的物理頁,我們可以看到,該頁上的內容剛好就是我們之前設置的0x5c。

通過以上的實驗,我們驗證了自己對linux arm二級映射表結構的理解,同時也明白了kmap映射的原理。



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