注:本文是我見(jiàn)到的所有 關(guān)于 高端內(nèi)存 解釋 的最詳細(xì)、最清晰的 解釋 ,其他帖子寥寥數(shù)語(yǔ)寫(xiě)的都是垃圾,保存下來(lái)只為方便后來(lái)人和我自己,感謝原文作者! 原文地址:http://bbs.chinaunix.net/thread-1938084-1-1.html 注:本文提及的物理地址空間可以理解
注:本文是我見(jiàn)到的所有關(guān)于高端內(nèi)存解釋的最詳細(xì)、最清晰的解釋?zhuān)渌恿攘葦?shù)語(yǔ)寫(xiě)的都是垃圾,保存下來(lái)只為方便后來(lái)人和我自己,感謝原文作者!
原文地址:http://bbs.chinaunix.net/thread-1938084-1-1.html
注:本文提及的物理地址空間可以理解為就是物理內(nèi)存,但是在某些情況下,把他們理解為物理內(nèi)存是不對(duì)的。
本文討論的環(huán)境是NON-PAE的i386平臺(tái),內(nèi)核版本2.6.31-14
一.什么是高端內(nèi)存
linux中內(nèi)核使用3G-4G的線性地址空間,也就是說(shuō)總共只有1G的地址空間可以用來(lái)映射物理地址空間。但是,如果內(nèi)存大于1G的情況下呢?是不是超過(guò)1G的內(nèi)存就無(wú)法使用了呢?為此內(nèi)核引入了一個(gè)高端內(nèi)存的概念,把1G的線性地址空間劃分為兩部分:小于896M物理地址空間的稱(chēng)之為低端內(nèi)存,這部分內(nèi)存的物理地址和3G開(kāi)始的線性地址是一一對(duì)應(yīng)映射的,也就是說(shuō)內(nèi)核使用的線性地址空間3G--(3G+896M)和物理地址空間0-896M一一對(duì)應(yīng);剩下的128M的線性空間用來(lái)映射剩下的大于896M的物理地址空間,這也就是我們通常說(shuō)的高端內(nèi)存區(qū)。
所謂的建立高端內(nèi)存的映射就是能用一個(gè)線性地址來(lái)訪問(wèn)高端內(nèi)存的頁(yè)。如何理解這句話(huà)呢?在開(kāi)啟分頁(yè)后,我們要訪問(wèn)一個(gè)物理內(nèi)存地址,需要經(jīng)過(guò)MMU的轉(zhuǎn)換,也就是一個(gè)32位地址vaddr的高10位用來(lái)查找該vaddr所在頁(yè)目錄項(xiàng),用12-21位來(lái)查找頁(yè)表項(xiàng),再用0-11位偏移和頁(yè)的起始物理地址相加得到paddr,再把該paddr放到前端總線上,那么我們就可以訪問(wèn)該vaddr對(duì)應(yīng)的物理內(nèi)存了。在低端內(nèi)存中,每一個(gè)物理內(nèi)存頁(yè)在系統(tǒng)初始化的時(shí)候都已經(jīng)存在這樣一個(gè)映射了。而高端內(nèi)存還不存在這樣一個(gè)映射(頁(yè)目錄項(xiàng),頁(yè)表都是空的),所以我們必須要在系統(tǒng)初始化完后,提供一系列的函數(shù)來(lái)實(shí)現(xiàn)這個(gè)功能,這就是所謂的高端內(nèi)存的映射。那么我們?yōu)槭裁床辉傧到y(tǒng)初始化的時(shí)候把所有的內(nèi)存映射都建立好呢?主要原因是,內(nèi)核線性地址空間不足以容納所有的物理地址空間(1G的內(nèi)核線性地址空間和最多可達(dá)4G的物理地址空間),所以才需要預(yù)留一部分(128M)的線性地址空間來(lái)動(dòng)態(tài)的映射所有的物理地址空間,于是就產(chǎn)生了所謂的高端內(nèi)存映射。
二.內(nèi)核如何管理高端內(nèi)存
上面的圖展示了內(nèi)核如何使用3G-4G的線性地址空間,首先解釋下什么是high_memory
在arch/x86/mm/init_32.c里面由如下代碼:
#ifdef CONFIG_HIGHMEM highstart_pfn = highend_pfn = max_pfn; if (max_pfn > max_low_pfn) highstart_pfn = max_low_pfn; e820_register_active_regions(0, 0, highend_pfn); sparse_memory_present_with_active_regions(0); printk(KERN_NOTICE "%ldMB HIGHMEM available.\n", pages_to_mb(highend_pfn - highstart_pfn)); num_physpages = highend_pfn; high_memory = (void *) __va(highstart_pfn * PAGE_SIZE-1)+1; #else e820_register_active_regions(0, 0, max_low_pfn); sparse_memory_present_with_active_regions(0); num_physpages = max_low_pfn; high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1)+1; #endif |
high_memory是“具體物理內(nèi)存的上限對(duì)應(yīng)的虛擬地址”,可以這么理解:當(dāng)內(nèi)存內(nèi)存小于896M時(shí),那么high_memory = (void *)__va(max_low_pfn * PAGE_SIZE),max_low_pfn就是在內(nèi)存中最后的一個(gè)頁(yè)幀號(hào),所以high_memory=0xc0000000+物理內(nèi)存大小;當(dāng)內(nèi)存大于896M時(shí),那么highstart_pfn= max_low_pfn,此時(shí)max_low_pfn就不是物理內(nèi)存的最后一個(gè)頁(yè)幀號(hào)了,而是內(nèi)存為896M時(shí)的最后一個(gè)頁(yè)幀號(hào),那么high_memory=0xc0000000+896M.總之high_memory是不能超過(guò)0xc0000000+896M.
由于我們討論的是物理內(nèi)存大于896M的情況,所以high_memory實(shí)際上就是0xc0000000+896M,從high_memory開(kāi)始的128M(4G-high_memory)就是用作用來(lái)映射剩下的大于896M的內(nèi)存的,當(dāng)然這128M還可以用來(lái)映射設(shè)備的內(nèi)存(MMIO)。
從上圖我們看到有VMALLOC_START,VMALLOC_END,PKMAP_BASE,FIX_ADDRESS_START等宏術(shù)語(yǔ),其實(shí)這些術(shù)語(yǔ)劃分了這128M的線性空間,一共分為三個(gè)區(qū)域:VMALLOC區(qū)域(本文不涉及這部分內(nèi)容,關(guān)注本博客的其他文章),永久映射區(qū)(permanetkernelmappings), 臨時(shí)映射區(qū)(temporary kernelmappings).這三個(gè)區(qū)域都可以用來(lái)映射高端內(nèi)存,本文重點(diǎn)闡述下后兩個(gè)區(qū)域是如何映射高端內(nèi)存的。
三.永久映射區(qū)(permanet kernel mappings)
1.介紹幾個(gè)定義:
PKMAP_BASE:永久映射區(qū)的起始線性地址。
pkmap_page_table:永久映射區(qū)對(duì)應(yīng)的頁(yè)表。
LAST_PKMAP:pkmap_page_table里面包含的entry的數(shù)量=1024
pkmap_count[LAST_PKMAP]數(shù)組:每一個(gè)元素的值對(duì)應(yīng)一個(gè)entry的引用計(jì)數(shù)。關(guān)于引用計(jì)數(shù)的值,有以下幾種情況:
0:說(shuō)明這個(gè)entry可用。
1:entry不可用,雖然這個(gè)entry沒(méi)有被用來(lái)映射任何內(nèi)存,但是他仍然存在TLBentry沒(méi)有被flush,
所以還是不可用。
N:有N-1個(gè)對(duì)象正在使用這個(gè)頁(yè)面
首先,要知道這個(gè)區(qū)域的大小是4M,也就是說(shuō)128M的線性地址空間里面,只有4M的線性地址空間是用來(lái)作永久映射區(qū)的。至于到底是哪4M,是由PKMAP_BASE決定的,這個(gè)變量表示用來(lái)作永久內(nèi)存映射的4M區(qū)間的起始線性地址。
在NON-PAE的i386上,頁(yè)目錄里面的每一項(xiàng)都指向一個(gè)4M的空間,所以永久映射區(qū)只需要一個(gè)頁(yè)目錄項(xiàng)就可以了。而一個(gè)頁(yè)目錄項(xiàng)指向一張頁(yè)表,那么永久映射區(qū)正好就可以用一張頁(yè)表來(lái)表示了,于是我們就用pkmap_page_table來(lái)指向這張頁(yè)表。
pgd = swapper_pg_dir + pgd_index(vaddr); pud = pud_offset(pgd, vaddr);//pud==pgd pmd = pmd_offset(pud, vaddr);//pmd==pud==pgd pte = pte_offset_kernel(pmd, vaddr); pkmap_page_table = pte; |
2.具體代碼分析(2.6.31)
void *kmap(struct page *page) { might_sleep(); if (!PageHighMem(page)) return page_address(page); return kmap_high(page); } |
kmap()函數(shù)就是用來(lái)建立永久映射的函數(shù):由于調(diào)用kmap函數(shù)有可能會(huì)導(dǎo)致進(jìn)程阻塞,所以它不能在中斷處理函數(shù)等不可被阻塞的上下文下被調(diào)用,might_sleep()的作用就是當(dāng)該函數(shù)在不可阻塞的上下文下被調(diào)用是,打印棧信息。接下來(lái)判斷該需要建立永久映射的頁(yè)是否確實(shí)屬于高端內(nèi)存,因?yàn)槲覀冎赖投藘?nèi)存的每個(gè)頁(yè)都已經(jīng)存在和線性地址的映射了,所以,就不需要再建立了,page_address()函數(shù)返回該page對(duì)應(yīng)的線性地址。(關(guān)于page_address()函數(shù),參考本博客的專(zhuān)門(mén)文章有解釋?zhuān)W詈笳{(diào)用kmap_high(page),可見(jiàn)kmap_high()才真正執(zhí)行建立永久映射的操作。
/** * 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)]++; BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); unlock_kmap(); return (void*) vaddr; } |
kmap_high函數(shù)分析:首先獲得對(duì)pkmap_page_table操作的鎖,然后再調(diào)用page_address()來(lái)返回該page是否已經(jīng)被映射,我們看到前面在kmap()里面已經(jīng)判斷過(guò)了,為什么這里還要再次判斷呢?因?yàn)樵佾@的鎖的時(shí)候,有可能鎖被其他CPU拿走了,而恰巧其他CPU拿了這個(gè)鎖之后,也是執(zhí)行這段code,而且映射的也是同一個(gè)page,那么當(dāng)它把鎖釋放掉的時(shí)候,其實(shí)就表示該page的映射已經(jīng)被建立了,我們這里就沒(méi)有必要再去執(zhí)行這段code了,所以就有必要在獲得鎖后再判斷下。
如果發(fā)現(xiàn)vaddr不為空,那么就是剛才說(shuō)的,已經(jīng)被其他cpu上執(zhí)行的任務(wù)給建立了,這里只需要把表示該頁(yè)引用計(jì)數(shù)的pkmap_count[]再加一就可以了。同時(shí)調(diào)用BUG_ON來(lái)確保該引用計(jì)數(shù)確實(shí)是不小于2的,否則就是有問(wèn)題的了。然后返回vaddr,整個(gè)建立就完成了。
如果發(fā)現(xiàn)vaddr為空呢?調(diào)用map_new_virtual()函數(shù),到此我們看到,其實(shí)真正進(jìn)行建立映射的代碼在這個(gè)函數(shù)里面
static inline unsigned long map_new_virtual(struct page *page) { unsigned long vaddr; int count; start: count = LAST_PKMAP;//LAST_PKMAP=1024 /* 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]) break; /* Found a usable entry */ if (--count) 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); 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; } |
last_pkmap_nr:記錄上次被分配的頁(yè)表項(xiàng)在pkmap_page_table里的位置,初始值為0,所以第一次分配的時(shí)候last_pkmap_nr等于1。
接下來(lái)判斷什么時(shí)候last_pkmap_nr等于0,等于0就表示1023(LAST_PKMAP(1024)-1)個(gè)頁(yè)表項(xiàng)已經(jīng)被分配了,這時(shí)候就需要調(diào)用flush_all_zero_pkmaps()函數(shù),把所有pkmap_count[]計(jì)數(shù)為1的頁(yè)表項(xiàng)在TLB里面的entry給flush掉,并重置為0,這就表示該頁(yè)表項(xiàng)又可以用了,可能會(huì)有疑惑為什么不在把pkmap_count置為1的時(shí)候也就是解除映射的同時(shí)把TLB也flush呢?個(gè)人感覺(jué)有可能是為了效率的問(wèn)題吧,畢竟等到不夠的時(shí)候再刷新,效率要好點(diǎn)吧。
再判斷pkmap_count[last_pkmap_nr]是否為0,0的話(huà)就表示這個(gè)頁(yè)表項(xiàng)是可用的,那么就跳出循環(huán)了到下面了。
PKMAP_ADDR(last_pkmap_nr)返回這個(gè)頁(yè)表項(xiàng)對(duì)應(yīng)的線性地址vaddr.
#definePKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
set_pte_at(mm,addr, ptep, pte)函數(shù)在NON-PAE i386上的實(shí)現(xiàn)其實(shí)很簡(jiǎn)單,其實(shí)就等同于下面的代碼:
staticinline void native_set_pte(pte_t *ptep , pte_t pte)
{
*ptep = pte;
}
我們已經(jīng)知道頁(yè)表的線性起始地址存放在pkmap_page_table里面,那么相應(yīng)的可用的頁(yè)表項(xiàng)的地址就是&pkmap_page_table[last_pkmap_nr],得到了頁(yè)表項(xiàng)的地址,只要把相應(yīng)的pte填寫(xiě)進(jìn)去,那么整個(gè)映射不就完成了嗎?
pte由兩部分組成:高20位表示物理地址,低12位表示頁(yè)的描述信息。
怎么通過(guò)page查找對(duì)應(yīng)的物理地址呢(參考page_address()一文)?其實(shí)很簡(jiǎn)單,用(page- mem_map) 再移PAGE_SHIFT位就可以了。
低12位的頁(yè)描述信息是固定的:kmap_prot=(_PAGE_PRESENT| _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_GLOBAL).
下面的代碼就是做了這些事情:
mk_pte(page,kmap_prot));
#definemk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
#definepage_to_pfn __page_to_pfn
#define__page_to_pfn(page) ((unsigned long)((page) - mem_map) + \
ARCH_PFN_OFFSET)
staticinline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
return __pte(((phys_addr_t)page_nr < massage_pgprot(pgprot)); } 接下來(lái)把pkmap_count[last_pkmap_nr]置為1,1不是表示不可用嗎,既然映射已經(jīng)建立好了,應(yīng)該賦值為2呀,其實(shí)這個(gè)操作是在他的上層函數(shù)kmap_high里面完成的(pkmap_count[PKMAP_NR(vaddr)]++). 到此為止,整個(gè)映射就完成了,再把page和對(duì)應(yīng)的線性地址加入到page_address_htable哈希鏈表里面就可以了(參考page_address一文)。 我們繼續(xù)看所有的頁(yè)表項(xiàng)都已經(jīng)用了的情況下,也就是1024個(gè)頁(yè)表項(xiàng)全已經(jīng)映射了內(nèi)存了,如何處理。此時(shí)count==0,于是就進(jìn)入了下面的代碼: /* * Sleepfor 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 (unsignedlong)page_address(page); /* Re-start */ goto start; } 這段代碼其實(shí)很簡(jiǎn)單,就是把當(dāng)前任務(wù)加入到等待隊(duì)列pkmap_map_wait,當(dāng)有其他任務(wù)喚醒這個(gè)隊(duì)列時(shí),再繼續(xù)gotostart,重新整個(gè)過(guò)程。這里就是上面說(shuō)的調(diào)用kmap函數(shù)有可能阻塞的原因。 那么什么時(shí)候會(huì)喚醒pkmap_map_wait隊(duì)列呢?當(dāng)調(diào)用kunmap_high函數(shù),來(lái)釋放掉一個(gè)映射的時(shí)候。 kunmap_high函數(shù)其實(shí)頁(yè)很簡(jiǎn)單,就是把要釋放的頁(yè)表項(xiàng)的計(jì)數(shù)減1,如果等于1的時(shí)候,表示有可用的頁(yè)表項(xiàng)了,再喚醒pkmap_map_wait隊(duì)列 /** *kunmap_high - map a highmem page into memory * @page:&struct page to unmap * * IfARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called * onlyfrom user context. */ voidkunmap_high(struct page *page) { unsigned long vaddr; unsigned long nr; unsigned long flags; int need_wakeup; 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]) {//減一 case 0: BUG(); case 1: /* * Avoidan unnecessary wake_up() function call. * Thecommon case is pkmap_count[] == 1, but * nowaiters. * Thetasks queued in the wait-queue are guarded * by boththe lock in the wait-queue-head and by * thekmap_lock. As the kmap_lock is held here, * no needfor the wait-queue-head's lock. Simply * test ifthe queue is empty. */ need_wakeup =waitqueue_active(&pkmap_map_wait); } unlock_kmap_any(flags); /* do wake-up, if needed, race-free outside ofthe spin lock */ if (need_wakeup) wake_up(&pkmap_map_wait); }
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com