儲存管理(一)--學習《Linux核心原始碼情景分析》第二章(方便理解,內容在註釋中)
2.1 Linux記憶體管理基本框架
32位cpu上的頁式記憶體管理是採用兩層對映方式,但在64位cpu上採用兩層對映方式就不太合理了,所以在Linux中頁式管理採用的是三層對映方式:頁面目錄(PGD)、中間目錄(PMD)、頁面表(PT),頁面表中的各項稱為PTE。頁式管理採用三層對映的過程如下(幾乎就和兩層的一樣):
前邊說了為了64位的cpu著想,Linux將其頁式對映模式設定為了三層,那麼Linux遇到只要兩層對映的32位cpu(i386)時怎麼處理呢?如下:
/*
* traditional i386 two-level paging structure:
*/
#define PGDIR_SHIFT 22 //PGD 的起始位
#define PTRS_PER_PGD 1024 //每個PGD有多少個成員
/*
* the i386 is two-level, so we don't really have any
* PMD directory physically.
*/
#define PMD_SHIFT 22 // PMD 的起始位
#define PTRS_PER_PMD 1 //每個PMD有多少個成員
#define PTRS_PER_PTE 1024
這是《Linux核心原始碼情景分析》中獲得的程式碼,但是我在2.6.22版本中原始碼中看到的PGDIR_SHIFT 和 PMD_SHIFT為21,想必核心有所變動了。先就依據書上來看,從上註釋很容易發現中間目錄 PMD 是無用的(因為起始位和PGD起始位重複),三層就變成了兩層。
32位地址意味著有4G記憶體空間,Linux核心將4G記憶體空間分為1G系統空間(0xC0000000-0xFFFFFFFF)、3G使用者空間(0x00000000-0xBFFFFFFF),其中描述的地址範圍是指的虛地址。程序都有自己的3G使用者空間,但是當程序通過系統呼叫進入核心就使用系統空間了,所以每個程序都擁有4G虛擬記憶體空間。系統空間從虛地址看是佔了0xC0000000-0xFFFFFFFF的1G空間,但是在實際實體地址中是佔了0x00000000-0x3FFFFFFF的1G空間。那麼0xC0000000就成了Linux用來轉化實體地址和虛擬地址的一箇中間值了,轉換方式如下程式碼:
#define __PAGE_OFFSET (0xC0000000)
==================== include/asm-i386/page.h 113 116 ====================
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) //將系統空間的虛擬地址轉換為實體地址
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) //將系統空間的實體地址轉換為虛擬地址
2.2地址對映全過程
這裡以一段簡單程式為例項將前邊講的頁式管理和段式管理串起來理解:
#include <stdio.h>
greeting()
{
printf("Hello, world!\n");
}
main()
{
greeting();
}
objdump 是一個反彙編工具,很實用,但是我不會...。從書上給出的上述程式的反彙編中可以看出:在連結(ld)過程給函式greeting( )分配的函式地址是0x08048380,且在執行main函式時會call 8048380。那麼要訪問到 0x08048380 這個虛擬地址,整個過程是如何的呢:先段式後頁式,如下。
先進行段式記憶體管理,這個過程只適用於i386這類cpu,因為其他cpu是沒有段式記憶體管理對映過程的。段式對映過程(其實也只是為了跳過這個對映過程而已):依據相應性質的段暫存器為下標在全域性暫存器中找到相應的描述表項。要訪問 0x08048380 這個地址要是使用的是cs暫存器,溫習一下段暫存器的格式:
然後我們再看看Linux核心給各暫存器賦值的:
==================== include/asm-i386/processor.h 408 417 ====================
#define start_thread(regs, new_eip, new_esp) do { \
__asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \
set_fs(USER_DS); \
regs->xds = __USER_DS; \
regs->xes = __USER_DS; \
regs->xss = __USER_DS; \
regs->xcs = __USER_CS; \
regs->eip = new_eip; \
regs->esp = new_esp; \
} while (0)
上述是給各暫存器賦值,這些硬體操作應該是在寫驅動時涉獵過的。上述程式碼中不難發現:SS堆疊段暫存器被設定為__USER_DS,從中可以說明intel想將程序分為:程式碼段、資料段、堆疊段,但是Linux卻並沒有那麼做,Linux中只有程式碼段和資料段。這裡的巨集如下定義:
#define __KERNEL_CS 0x10
#define __KERNEL_DS 0x18
#define __USER_CS 0x23
#define __USER_DS 0x2B
各巨集展開為2進位制的結果如下:
從上圖可知:TI 都是0,表示他們都使用GDT。事實上Linux只在模擬windows及windows上軟體時才用到LDT。現在使用程序空間而非系統空間,所以想要訪問 0x08048380虛擬地址,將在段式記憶體管理中將使用__USER_DS,將在GDT表中找到下標為4的全域性段描述項。GDT表如下:
444 /*
445 * This contains typically 140 quadwords, depending on NR_CPUS.
446 *
447 * NOTE! Make sure the gdt descriptor in head.S matches this if you
448 * change anything.
449 */
450 ENTRY(gdt_table)
451 .quad 0x0000000000000000 /* NULL descriptor */
452 .quad 0x0000000000000000 /* not used */
453 .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
454 .quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
455 .quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
456 .quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
457 .quad 0x0000000000000000 /* not used */
458 .quad 0x0000000000000000 /* not used */
我們將上述4個暫存器對應的這4個段描述項展開為2進位制如圖:
對照第一章講的全域性段描述項格式,會發現4個描述項的共同點:1. 段基地址全為0。 2. 段長度單位為4k 3.段長上限為0xffffffff,乘上段長單位剛好4G。 4. 4個段訪問都是32位。 5. 描述項都在記憶體中。這樣看來,每個段都是從0 ~ 4G空間完整對映的,相當於虛擬地址和線性地址兩著一致。
當然4個段描述項也有不同點在bit40 ~ bit46,對應於type欄位、S標誌位、DPL標誌位。也就是核心、使用者許可權不同和段的型別不同(程式碼段、資料段)。
上邊說了那麼多也就是說明:Linux只是在忽悠cpu,其實段式對映根本就沒用。那麼程式訪問 0x08048380 這個虛擬地址經過段式對映,也還是還是訪問 0x08048380 本身。
下面終於說到頁式記憶體管理了:
每個程序都有自己的頁面目錄,這個目錄儲存在每個程序的mm_struct結構體中,每當排程一個程序進行執行,核心通過mm_struct結構體為即將執行的程序設定好 CR3暫存器,MMU總是從 CR3中取得頁面目錄指標。下面這段彙編就是將頁面目錄實體地址設定到CR3暫存器中。
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk,
unsigned cpu)
{
......
==================== include/asm-i386/mmu_context.h 44 44 ====================
asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
......
==================== include/asm-i386/mmu_context.h 59 59 ====================
}
下面來介紹怎麼通過頁式管理找到 0x08048380這個虛擬地址的實體地址的,我們知道現在 CR3 暫存器已經設定好了。現看看 0x08048380 這個地址的二進位制碼,畢竟是依據此來尋找到對應的頁面目錄、頁面表、頁面。(每個程序的CR3暫存器設定的值是不同的,所以同一虛擬地址在不同程序對應不同實體地址)
0x08048380(十六進位制):0000 1000 00(目錄中找到頁表地址)00 0100 1000(頁表中找到頁面地址) 0011 0110 1000(頁面中找到實體地址)
依據高10位(紅字)為下標在CR3指向的頁面目錄(結構體陣列)中找“結構體x”(因為有該結構體使得虛擬地址和實體地址不一一對應),這個結構體“結構體x”中含有某些資訊,如:指向頁面表的地址 等。例如:本例中高10位是 0000 1000 00,表示十進位制為32,那麼就以32位為下標在頁面目錄中去找,找到對應”結構體x“,在”結構體x“中得到頁面表地址的資訊,在其高20位後的的低位補上12個全0就得到指向某頁面表的指標。這裡就有兩個疑問了,為什麼只要”結構體x“的高20位就能得到指向某頁面表的指標呢?”結構體x“剩下還有12位有什麼用呢?答案是:因為每個頁表組有1024個成員,每個成員4位元組,一個頁表組就是4k位元組,剛好就是12位,所以在”結構體x“高20位後補上12位0就是對應頁表組的指標。而剩下的12位則另作它用,比如最低位是P位,為1表示頁表在記憶體中。
我們已經通過高十位找到了對應的頁面表,現在我們應該繼續在頁表中找到對應的頁面了,其過程與上述幾乎一模一樣,在頁面表中以 00 0100 1000 的值為下表找到”結構體y“,然後在”結構體y“高20位後的的低位補上12個全0就是指向某個頁面的指標(為什麼只用高20位就不再累述),這裡的頁面代表記憶體的某塊實體地址。
頁面找到了,最後操作就是以頁面為基地址加上最後12位偏移量找到具體實體地址了,以 0011 0110 1000 的值為偏移量在頁面中找到對應實體地址。
上述整個過程經過3次對映,看似比較複雜,實際上訪問過程是依賴快取記憶體來實現的,雖然第一次訪問目錄組和頁表組需要通過記憶體訪問,但是一旦裝入快取記憶體以後,就可以在快取記憶體中找到而不用去記憶體中讀取,再加上整個過程式硬體完成的,所以是非常快的。
2.3幾個重要的結構體和函式
上述內容講的是記憶體對映的流程,但是具體細節並未細說,並且其也僅只是記憶體管理的一部分。而記憶體管理中有很多資料結構是很重要的,下面將詳細介紹這些資料結構。
頁面目錄、中間目錄、頁表分別是由pgd_t、pmd_t、pte_t 結構體構成的結構體陣列。其中結構體定義如下:
/*
* These are used to make use of C type-checking..
*/
#if CONFIG_X86_PAE //64位機
typedef struct { unsigned long pte_low, pte_high; } pte_t;
typedef struct { unsigned long long pmd; } pmd_t;
typedef struct { unsigned long long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
#else //32位機
typedef struct { unsigned long pte_low; } pte_t; //頁表
typedef struct { unsigned long pmd; } pmd_t; //中間目錄
typedef struct { unsigned long pgd; } pgd_t; //頁面目錄
#define pte_val(x) ((x).pte_low)
#endif
#define PTE_MASK PAGE_MASK
前邊說過pte_t(頁表)中的最後12位用來存放訪問許可權和狀態資訊,具體的有關位段是如何定義的呢?核心另定義了一個頁面保護結構體pgprot_t來表示頁面後12位:
typedef struct { unsigned long pgprot; } pgprot_t;
上述低12位中有9位是標誌位,在核心中定義如下:
#define _PAGE_PRESENT 0x001
#define _PAGE_RW 0x002
#define _PAGE_USER 0x004
#define _PAGE_PWT 0x008
#define _PAGE_PCD 0x010
#define _PAGE_ACCESSED 0x020
#define _PAGE_DIRTY 0x040
#define _PAGE_PSE 0x080 /* 4 MB (or 2MB) page, Pentium+, if present.. */
#define _PAGE_GLOBAL 0x100 /* Global TLB entry PPro+ */
#define _PAGE_PROTNONE 0x080 /* If not present ,這一位在Intel手冊上規定為保留不用,所以實際沒啥用*/
頁表項由高20位和低12位組合,找到某頁表程式碼如下:
#define pgprot_val(x) ((x).pgprot)
#define __pte(x) ((pte_t) { (x) } )
#define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot)) //將頁表序號左移12位然後或上pgprot,並將這個值賦值給pte_t結構體。
核心程式碼中設定頁面的方式:
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
找到某頁表程式碼如下,核心中mem_map是一個page陣列的指標,可以通過該指標找到代表目標實體地址的資料結構:
#define pte_page(x) (mem_map + ((unsigned long)(( (x).pte_low >> PAGE_SHIFT ))))
核心中通過檢測頁表低12位值來判斷頁面的訪問許可權和狀態資訊,是通過和上述巨集定義的標誌位進行按位與來實現的。特別應注意第0位p標誌位,為0表示虛擬對映以建立但是物理頁面不在記憶體中(不常用到被置換了出去)。
每個物理頁面都有與之對應的page資料結構,mem_map是一個page陣列的指標,是系統在初始化是根據實體記憶體的大小建立的:
typedef struct page {
struct list_head list; //和free_area_struct有關聯的雙向連結串列
struct address_space *mapping; //下文2.6節會提到
unsigned long index; /* 當頁面內容來自一個檔案時,index代表頁面在該檔案的序號 */
struct page *next_hash;
atomic_t count;
unsigned long flags; /* atomic flags, some possibly updated asynchronously */
struct list_head lru;
unsigned long age;
wait_queue_head_t wait;
struct page **pprev_hash;
struct buffer_head * buffers;
void *virtual; /* non-NULL if kmapped */
struct zone_struct *zone;
} mem_map_t;
系統在初始化時根據實體記憶體的大小建立一個page陣列mem_map。
這個頁面陣列被分為兩個管理區:ZONE_DMA、ZONE_NORMAL(還可能有第三個管理區ZONE_HIGHMEM)。管理區 ZONE_DMA 供DMA使用,因為:(1)必須留這個管理區的記憶體進行頁面和盤區的交換。(2)在i386cpu中頁式對映是cpu內部實現,沒有單獨的mmu,這樣外設就要直接訪問實體記憶體,而有的外設能訪問的地址不能太高,所以設定這麼個管理區。 (3)DMA需要的緩衝區超過一個物理頁面時,就需要兩個頁面連續,依靠cpu內部的mmu不能辦到,只能單獨設個管理區。
每個管理區都有一個zone_struct資料結構,這個結構體中有兩個佇列 free_area_struct,一個用於保持離散的物理頁面(單位為一),另一個用於保持連續的物理頁面(單位為2的偶數倍)。
/* Free memory management - zoned buddy allocator.*/
#define MAX_ORDER 10
typedef struct free_area_struct
{
struct list_head free_list;
unsigned int *map;
} free_area_t;
struct pglist_data;
typedef struct zone_struct
{
/*
* Commonly accessed fields:
*/
spinlock_t lock;
unsigned long offset; /*管理區在mem_map[]中起始頁面號*/
unsigned long free_pages;
unsigned long inactive_clean_pages;
unsigned long inactive_dirty_pages;
unsigned long pages_min, pages_low, pages_high;
/*
* free areas of different sizes
*/
struct list_head inactive_clean_list;
free_area_t free_area[MAX_ORDER];
/*
* rarely used fields:
*/
char *name;
unsigned long size;
/*
* Discontig memory support fields.
*/
struct pglist_data *zone_pgdat; //指向所屬的節點
unsigned long zone_start_paddr;
unsigned long zone_start_mapnr;
struct page *zone_mem_map;
} zone_t;
#define ZONE_DMA 0
#define ZONE_NORMAL 1
#define ZONE_HIGHMEM 2
#define MAX_NR_ZONES 3
記憶體管理中還有一重點。就是當cpu想通過PCI匯流排訪問其他cpu的實體記憶體時,或者訪問PCI匯流排連線的公用儲存模組(rom、ram...),所用時間是比直接訪問本地記憶體慢的,這樣的系統稱之為“非均質儲存結構”(NUMA)。所以在管理區上有更高一層的“節點”,頁面陣列也不是全域性陣列了,而是屬於某個節點。節點資料結構:
typedef struct pglist_data
{
zone_t node_zones[MAX_NR_ZONES]; //代表本節點的最多管理區
zonelist_t node_zonelists[NR_GFPINDEX]; // 陣列最大值有限制。#define NR_GFPINDEX 0x100,也就是最多100種分配策略
struct page *node_mem_map;50
unsigned long *valid_addr_bitmap;
struct bootmem_data *bdata;
unsigned long node_start_paddr;
unsigned long node_start_mapnr;
unsigned long node_size;
int node_id;
struct pglist_data *node_next; //若干節點形成的單鏈
} pg_data_t;
typedef struct zonelist_struct
{
zone_t * zones [MAX_NR_ZONES+1]; // NULL delimited,每一項代表具體的某個節點管理區,包含有其他節點的管理區,陣列的第一個管理區不符合要求則去第二個..以此類推
int gfp_mask;
} zonelist_t; //表示一種分配策略
以上是物理空間管理的資料結構,下邊是虛擬空間管理結構。因為程序使用虛存空間的各部分未必是連續的,所以一個各部分需要用“虛存區間”來管理(一個程序有多個虛存區間):
/*
* This struct defines a memory VMM memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start; //虛存區間的開始,包含
unsigned long vm_end; //虛存區間的結束,不包含
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next; //虛存地址高低次序連線
pgprot_t vm_page_prot; //訪問許可權
unsigned long vm_flags; //其他屬性
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height; //AVL樹
struct vm_area_struct * vm_avl_left; //AVL樹
struct vm_area_struct * vm_avl_right; //AVL樹
/* For areas with an address space and backing store,
* one of the address_space->i_mmap{,shared} lists,
* for shm areas, the list of attaches, otherwise unused.
*/
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data; /* was vm_pte (shared mem) */
};
虛存區間的劃分,並不僅取決於實體地址的連續性,還有區間的屬性:訪問許可權等。
mmap(); 函式是將一塊虛擬區間和已開啟的檔案或裝置建立對映,就可以想直接訪問記憶體中的字元陣列一樣來訪問檔案或裝置內容。
兩種情況虛存區間會和磁碟檔案發生關係:(1)將久未使用的頁面交換到磁碟上去。 (2)將磁碟檔案對映到使用者空間(mmap)。
關於虛存區間還有很重要的一個數據結構:
/*
* These are the virtual MM functions - opening of an area, closing and
* unmapping it (needed to keep files on disk up-to-date etc), pointer
* to the functions called when a no-page or a wp-page exception occurs.
*/
struct vm_operations_struct
{
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int write_access); //頁面不在記憶體中引起頁面出錯時會呼叫
};
最後vm_area_struct中還有個vm_mm指標,其指向mm_struct:
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct vm_area_struct * mmap_avl; /* tree of VMAs */ //當虛存區間的過多時線性佇列中搜索效率太低,就需要樹
struct vm_area_struct * mmap_cache; /* last find_vma result,指向最後一次用過的虛存區間 */
pgd_t * pgd;53
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
int map_count; /* number of VMAs */
struct semaphore mmap_sem;
spinlock_t page_table_lock;
struct list_head mmlist; /* List of all active mm's */
unsigned long start_code, end_code, start_data, end_data; //程式碼段、資料段,注意並不是指的段式管理裡的段。
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
unsigned long cpu_vm_mask;
unsigned long swap_cnt; /* number of pages to swap on next pass */
unsigned long swap_address;
/* Architecture-specific MM context */
mm_context_t context;
};
mm_struct比vm_area_struct高一個層次,是整個使用者空間的抽象,且一個程序只有一個mm_struct,但是可能多個程序共享一個mm_struct,如父子程序。VMA表示虛存區間。
虛存區間的頁面和物理頁面是不同的東西。
上述的結構體之間的關係圖如下:
下面這個函式十分常用並會在本章後文中用到, find_vma();。函式功能:尋找第一個結束地址高於給定虛擬地址的vm_area_struct結構體,函式的註釋將解釋函式的執行過程:
/* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm)
{
/* Check the cache first. */55
/* (Cache hit rate is typically around 35%.) */
vma = mm->mmap_cache; //檢測地址是否在最近一次訪問的區間之中,找到的概率約35%
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) //尋找所屬區間
{
if (!mm->mmap_avl) //在連結串列中查詢所屬區間
{
/* Go through the linear list. */
vma = mm->mmap;
while (vma && vma->vm_end <= addr)
vma = vma->vm_next;
}
else // //在AVL樹中中查詢所屬區間
{
/* Then go through the AVL tree quickly. */
struct vm_area_struct * tree = mm->mmap_avl;
vma = NULL;
for (;;)
{
if (tree == vm_avl_empty)
break;
if (tree->vm_end > addr)
{
vma = tree;
if (tree->vm_start <= addr)
break;
tree = tree->vm_avl_left;
}
else
tree = tree->vm_avl_right;
}
}
if (vma)
mm->mmap_cache = vma; //找到後將其賦值給mmap_cache,以便下一次可以快速查詢
}
}
return vma;
}
如果上述 find_vma();函式返回值為NULL,表示所屬的區間未建立,則需要建立新的虛存區間結構並通過insert_vm_struct();插入mm_struct的佇列或AVL樹中。
void insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)
{
lock_vma_mappings(vmp);
spin_lock(¤t->mm->page_table_lock);
__insert_vm_struct(mm, vmp); //實際上是此函式完成,上述不過是加了兩把鎖以保證整個過程不被中斷
spin_unlock(¤t->mm->page_table_lock);
unlock_vma_mappings(vmp);
}
__insert_vm_struct(mm, vmp):
/* Insert vm structure into process list sorted by address
* and into the inode's i_mmap ring. If vm_file is non-NULL
* then the i_shared_lock must be held here.
*/
void __insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)
{
struct vm_area_struct **pprev;
struct file * file;
if (!mm->mmap_avl) {
pprev = &mm->mmap;
while (*pprev && (*pprev)->vm_start <= vmp->vm_start)
pprev = &(*pprev)->vm_next;
} else {
struct vm_area_struct *prev, *next;
avl_insert_neighbours(vmp, &mm->mmap_avl, &prev, &next);
pprev = (prev ? &prev->vm_next : &mm->mmap);