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*,如圖
 
 
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
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM