天天看點

linux核心研究筆記(一) - page介紹

============ “不負責任”聲明 begin ============   咳,首先我是一個平時工作在linux應用層的伺服器程式員,對于核心的了解也是皮毛,僅是業餘時間中的業餘研究的一些筆記,文中的一些觀點也許隻是我對核心的粗淺認識,大家可千萬不要輕易信以為真啊 PS:文中的核心代碼預設都是2.6.27.62版本,且環境都按x86 32   ============ “不負責任”聲明 end ============     核心中最初勾引我好奇心的還是記憶體管理方面,我們平時編寫應用程式時,一個程序所能擁有的記憶體大小幾乎可以趨近于實體記憶體最大值或是超越這個值,雖然知道核心做記憶體方面的映射然後向我們的使用者空間呈現出所謂的虛拟記憶體,但還是對其中實作疑惑甚多,而且一些關于記憶體的名詞也是有許多,什麼虛拟位址,核心線性位址,核心邏輯位址,balablabla...   屁話不講了,我們直接來看核心最底層是如何來管理實體記憶體的。  

1 struct page {
 2     atomic_t _count;        /* Usage count, see below. */
 3     atomic_t _mapcount; /* Count of ptes mapped in mms,
 4                                     * to show when page is mapped
 5                                     * & limit reverse map searches.
 6                                     */
 7     union {
 8         struct {
 9         unsigned long private;      /* Mapping-private opaque data:
10                          * usually used for buffer_heads
11                          * if PagePrivate set; used for
12                          * swp_entry_t if PageSwapCache;
13                          * indicates order in the buddy
14                          * system if PG_buddy is set.
15                          */
16         struct address_space *mapping;  /* If low bit clear, points to
17                          * inode address_space, or NULL.
18                          * If page mapped as anonymous
19                          * memory, low bit is set, and
20                          * it points to anon_vma object:
21                          * see PAGE_MAPPING_ANON below.
22                          */
23         };
24         struct kmem_cache *slab;    /* SLUB: Pointer to slab */
25         struct page *first_page;    /* Compound tail pages */
26     };
27     struct list_head lru;       /* Pageout list, eg. active_list
28                      * protected by zone->lru_lock !
29                      */
30 };      

  核心将實體記憶體劃分為一個個 4K or 8K 大小的小塊(實體頁),而這一個個小塊就對應着這個page結構,它是核心管理記憶體的最小單元 上面的結構體隻貼出了部分資料域,其注釋核心也寫得很清楚了 需要說得是,這個page結構描述的是某片實體頁,而不是它包含的資料 不管是核心還是我們使用者空間,配置設定記憶體時,底層都逃不掉這一個個的page,是以這個page可以作為:        1. 頁緩存使用(mapping域指向address_space對象)                這個東西主要是用來對磁盤資料進行緩存,我們平時監控伺服器時,經常會用top/free看到cached參數,這個參數其實就是頁緩存(page cache),一般如果這個值很大,就說明核心緩沖了許多檔案,讀IO就會較小      2. 作為私有資料(由private域指向)                 可以是作為塊沖區中所用,也可以用作swap,當是空閑的page時,那麼會被夥伴系統使用。      3. 作為程序頁表中的映射                映射到程序頁表後,我們使用者空間的malloc才能獲得這塊記憶體   先來看一下核心中和page相關的一些常量:   include/asm-x86/page.h ---------------------------------------------------

1 #define PAGE_SHIFT  12
2 #define PAGE_SIZE   (_AC(1,UL) << PAGE_SHIFT)
3 #define PAGE_MASK   (~(PAGE_SIZE-1))      

---------------------------------------------------   可以看出一個page所對應的實體塊的大小(PAGE_SIZE)是4096   arch/x86/kernel/e820.c ---------------------------------------------------

1 #ifdef CONFIG_X86_32
2 # ifdef CONFIG_X86_PAE
3 #  define MAX_ARCH_PFN      (1ULL<<(36-PAGE_SHIFT))
4 # else
5 #  define MAX_ARCH_PFN      (1ULL<<(32-PAGE_SHIFT))
6 # endif
7 #else /* CONFIG_X86_32 */
8 # define MAX_ARCH_PFN MAXMEM>>PAGE_SHIFT
9 #endif      

---------------------------------------------------   核心會将所有struct page* 放到一個全局數組(mem_map)中,而核心中我們常會看到pfn,說得就是頁幀号,也就是數組的index,這裡的MAX_ARCH_PFN就是系統的最大頁幀号,但這個隻是理論上的最大值,在start_kernel()時,setup_arch()函數會通過e820_end_of_ram_pfn()函數來獲得實際實體記憶體并傳回最終的max_pfn,可以看下e820_end_of_ram_pfn的實作(其内部直接調用e820_end_pfn函數)  

1 /*
 2 * Find the highest page frame number we have available
 3 */
 4 static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type)
 5 {
 6     int i;
 7     unsigned long last_pfn = 0;
 8     unsigned long max_arch_pfn = MAX_ARCH_PFN;
 9 
10     for (i = 0; i < e820.nr_map; i++) {
11         struct e820entry *ei = &e820.map[i];
12         unsigned long start_pfn;
13         unsigned long end_pfn;
14 
15         if (ei->type != type)
16             continue;
17 
18         start_pfn = ei->addr >> PAGE_SHIFT;
19         end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;
20 
21         if (start_pfn >= limit_pfn)
22             continue;
23         if (end_pfn > limit_pfn) {
24             last_pfn = limit_pfn;
25             break;
26         }
27         if (end_pfn > last_pfn)
28             last_pfn = end_pfn;
29     }
30 
31     if (last_pfn > max_arch_pfn)
32         last_pfn = max_arch_pfn;
33 
34     printk(KERN_INFO "last_pfn = %#lx max_arch_pfn = %#lx\n",
35              last_pfn, max_arch_pfn);
36     return last_pfn;
37 }      

從上面的宏定義還可以看到 在x86_32時,核心會看是否啟用PAE,PAE會比沒有PAE所擁有的page更多(也即是說能通路更多的實體記憶體),PAE是一種實體位址擴充技術,讓你在32位的系統中能通路超越4G的空間,其技術實作還是通過局部位址的映射,這裡不展開說   接着來看下page結構的相關宏/函數:   pfn_to_page/page_to_pfn - 這兩個底層使用 __pfn_to_page/__page_to_pfn宏,它們的作用是struct page* 和 前面提到的pfn頁幀号之間的轉換,看下實作

1 __pfn_to_page:(mem_map + ((pfn) - ARCH_PFN_OFFSET))
2 __page_to_pfn:((unsigned long)((page) - mem_map) + ARCH_PFN_OFFSET)      

就是簡單地和mem_map進行加減操作(最後那個OFFSET可以無視,預設0),由于mem_map也是struct page*類型,是以相加減就能得到對應的pfn(數組index)和對應的struct page*,如圖

linux核心研究筆記(一) - page介紹
1 #define phys_to_page(phys) (pfn_to_page(phys >> PAGE_SHIFT))
2 #define page_to_phys(page) (page_to_pfn(page) << PAGE_SHIFT)      

這兩個宏的功能分别是将struct page*和實體位址之間進行轉換 例如page_to_phys, 通過page_to_pfn宏取得相應的pfn後,還記得PAGE_SHIFT嗎,假設pfn是1,左移12位,就是4096,也就是第二個對應的實體頁的位置,這樣就取得了實體位址(雖然核心在虛拟位址中是在高位址的,但是在實體位址中是從0開始的,是以這裡也是從0開始)  

1 #define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)
2 #define page_to_virt(page)  __va(page_to_pfn(page) << PAGE_SHIFT)      

這兩個宏的作用是在struct page*和核心邏輯/線性位址 之間做轉換   這裡要補幾個概念性的問題 - 核心邏輯/線性位址:其實對于linux核心來說,這個位址等同于實體位址,隻是它們之間有一個固定的偏移量,linux核心中常提到的邏輯位址和線性位址其實是同一個東西 核心虛拟位址:與上面的核心邏輯位址的差別在于,核心虛拟位址不一定是在硬體實體上是連續的,有可能是通過分頁映射的不連續的實體位址 這裡的virt指得就是邏輯/線性位址,而不是真正的virtual位址   繼續看__pa和__va宏

1 #define __pa(x)         ((unsigned long) (x) - PAGE_OFFSET)
2 #define __va(x)         ((void *)((unsigned long) (x) + PAGE_OFFSET))      

可以看到它們隻是做了一個偏移量(PAGE_OFFSET),在x86_32中,這個PAGE_OFFSET是0xC0000000,為什麼是這個值呢,因為32位系統中,核心的虛拟位址隻有1G,這個之後具體講記憶體布局的時候再讨論   還有一個常用的宏/函數是page_address,它特殊的地方在于,以上的那些宏針對的或是傳回的都是核心邏輯位址,也就是說是做簡單的偏移加減,但是在32位系統中有個high_mem的概念 - 高端記憶體,它的作用讓核心如何通路超出32位範圍的記憶體,方法就是利用某一小塊固定的記憶體做映射(這裡的HighMem我個人認為就是前面提到的PAE技術的一種實作,以後讨論) 是以一個page對應的虛拟位址,有可能是直接做實體偏移的位址(也就是以上幾個宏可以直接應用的),還有就是被高端記憶體映射的 針對後者,以上的幾個宏是無法得到page的虛拟位址的,隻有應用到page_address函數 我們看下page_address的實作:

1 void *page_address(struct page *page)                                                                                                                                                               
 2 {
 3     unsigned long flags;
 4     void *ret;
 5     struct page_address_slot *pas;
 6 
 7     if (!PageHighMem(page))
 8         return lowmem_page_address(page);
 9 
10     pas = page_slot(page);
11     ret = NULL;
12     spin_lock_irqsave(&pas->lock, flags);
13     if (!list_empty(&pas->lh)) {
14         struct page_address_map *pam;
15 
16         list_for_each_entry(pam, &pas->lh, list) {
17             if (pam->page == page) {
18                 ret = pam->virtual;
19                 goto done;
20             }  
21         }  
22     }  
23 done:
24     spin_unlock_irqrestore(&pas->lock, flags);
25     return ret;
26 }      

标紅的地方會判斷page是否是HighMem,如果不是,直接調用lowmem_page_address,這個函數内部實作就是page_to_virt,是以就是簡單地做偏移了,關于HighMem的映射之後再讨論了     以上就是核心常用的幾個page轉換的宏/函數,最後咱們簡單看下page的配置設定接口(釋放的我懶得一一比對寫了)   傳回page結構的: struct page * alloc_pages(gfp_mask, order)          // 配置設定 1<<order 個連續的實體頁 struct page * alloc_page(gfp_mask)                     // 配置設定一個實體頁   傳回page對應的邏輯位址的: __get_free_pages(gfp_mask, order)                    // 和alloc_pages一樣,隻不過傳回的是第一個頁的核心邏輯位址 __get_free_page(gfp_mask)                              // 傳回一個頁的邏輯位址     配置設定和釋放都牽涉到底層的夥伴算法,那麼也放到之後再講吧~   另外歡迎大家到我的個人部落格一起讨論:www.cppthinker.com  

轉載于:https://www.cnblogs.com/emperor_zark/archive/2013/03/15/linux_page_1.html

繼續閱讀