《內核源碼情景分析》(浙大)筆記


內核源碼情景分析筆記

背景

1.1 x86尋址

8086, 8088, 80186, 80286, 80386, 80486,

  • 實地址模式

8086

段寄存器 CS, DS, SS 和 ES 為16bit
實際地址(20bit) = 內部地址(16bit) + 段寄存器值(16bit) << 4 

  • 保護模式 (段式內存管理)

80386

設計思路: 在保護模式下改變段寄存器的功能, 使其從單純的基地址變成指向這樣的一個數據結構的指針.
涉及的新增寄存器:

  • GDTR (Global Description Table Register)
  • LDTR (Local Description Table Register)

段寄存器的定義也有所變化, 其中高13位為描述表的index,低2位表示特權級別, 第2位(從0開始)表示選用GDTR還是LDTR.
所以具體 段描述地址 = index + GDTR/LDTR指向的基地址

定位段描述地址可獲得段描述項(8 byte), 其中定義了基地址(32 bit),段長度(20 bit)以及權限信息.
段單元大小 0 代表1B , 1 代表 4KB.
最大段 \(4GB = 4KB * 2^{20}\), 所有段寄存器都指向同一個描述項目, 且該項的基地址為零, 使用最長段長,
這時候物理地址和邏輯地址是相同的. 這被成為平坦模式.Flat Mode.

  • 頁式管理

在段式內存管理的基礎上分頁管理, 在段式管理處理后可得線性地址, 80386在把線性地址空間划分為4KB的頁面,
每個頁面映射到物理空間任意一塊4KB區間.
線性地址結構:頁目錄 10bit + 頁面表 10bit + 頁內偏移12bit

其中CR3寄存器指向當前頁面目錄.
線性地址物理地址:

  1. CR3獲取頁面目錄的基地址
  2. 以線性地址的目錄錄位段為下標,在目錄中取得相應頁面表的基地址.
  3. 以線性地址的頁面表位段為下標,在所得的頁面表取得相應的頁面描述項.
  4. 將頁面描述項的頁面基地址與頁內偏移相加得到物理地址.

目錄項含有指向頁面表的指針4B, 頁面表項中則含有一個指向頁面起始地址的指針4B.

一個頁面可以存儲1024個表項 \(4KB=1024*4B\), 這就是4KB的由來.

由於頁面表和頁面的起始地址都需要4K邊界對齊, 所以這些指針的低12位永為0, 那么余下的12位可以用來放控制信息

PSE(page size extension)機制: 頁目錄項中的ps位為0代表4KB默認大小, 為1則頁面大小為4M那么,
取消頁面表, 線性地址的低22位全部用來4M頁面內的偏移.

CR0寄存器的最高位PG是頁式映射機制的總開關.

PAE(Physical Address Extension)機制, 在CR4寄存器中將PAE位置為1則地址總線的寬度為36位.

1.2 C in Linux

Linux由GNU的C語言編寫而成, 所以這里介紹內核中 用到的一些ANSI C技巧.

  • inline and const 從C++借鑒, 其中inline函數相對於宏而言更便於調試,
    在未開啟編譯優化時inline函數與普通函數一致, 在調試成功后開啟優化提高運行效率.

  • 屬性描述符 attributealignedpacked等.

    為防止與變量沖突引入等價關鍵字 __aligin__ , __packed__, __asm__, __volatile__.

  • 一些奇怪的Macro定義:

    • 空定義
      /* include/asm-i386/system.h:14 */
      #define prepare_to_switch() do { } while(0)
      

      為了不同架構有不同實現

    • 執行一次定義
      /* fs/proc/kcore.c:163 */
      #define DUMP_WRITE(addr, nr) do {memcpy(bufp, addr, nr); bufp += nr;} while(0)
      

      為了在沒有花括號的if-else語句內正確執行

  • 抽象數據結構類型

    /* include/linux/list.h:16 */
    struct lsit_head {
        struct list_head *next, *prev;
     }
    

實現雙向鏈表

  • 反解析宿主地址
```c
/* mm/page_alloc.c 188*/
page = memlist_entry(cur, struct page, list);
```
```c
/* include/linux/mm.h */
typedef struct page {
	struct list_head list;
	...
}

從cur一個list__head指針換算出page結構的地址

實際實現:

 page = ((struct page*))(cahr*)(curr) - (unsigned long)(&(struct page*)0)->list)));

curr是page結構內部成分list的地址, 思路是通過curr去減去list在page內的偏移量.

1.3 Assembly in Linux

為什么要匯編

  1. 硬件專用指令如inb, outb無法在C中找到對應語句
  2. CPU中的特殊指令如開閉中斷,還有后代CPU的擴展指令
  3. 內核中要求效率的操作
  4. 空間有限, 如對於引導程序而言.

GNU 386匯編

Unix 采用的是AT&T格式的指令, 而非Intel定義的x86指令.

Unix早期實在PDP-11機器上開發完成, 后一直到VAX和68000系列.

那么GNU自然繼承了AT&T的386匯編語言格式.

異同點:

  1. 一般Intel大寫, AT&T小寫
  2. AT&T的寄存器需要%前綴
  3. AT&T 的源操作數在前,目標操作在后, 而Intel相反.
  4. AT&T格式中,訪問指令的操作數寬度由操作碼后綴決定:
    如 b(8), w(16), l(32)
    Intel格式中需要在內存單元的操作數前加上BYTE PTR
    WORD PTRDWORD PTR.
  5. AT&T 直接操作書需要加上$作為前綴, 而Intel不需要
  6. AT&T格式中jmp/call指令的操作數需要加上*作為前綴
  7. AT&T的長跳轉和調用為ljmplcall,Intel為JMP FAR
    CALL FAR.如下
; Intel
CALL FAR SECTION:OFFSET
JMP FAR SECTION:OFFSET

;AT&T

lcall $section, $offset
ljmp $section, $offset

  1. 間接尋址
SECTION:[BASE+INDEX*SCALE+DISP] ;Intel
section:disp(base,index,scale) ;AT&T

C代碼插入匯編是以匯編片段的形式,如

/* include/asm-i386/atomic.h */
 static __inline__ void atomic_add(int i, atomic_t *v)
 {
 __asm__ __volatile__(
 LOCK "addl %1,%0"
 :"=m" (v->counter)
 :"ir" (i), "m" (v->counter));
 }

LOCK表示在執行addl時候將總線鎖住, 保證操作原子性.

匯編片段形式如下:

指令部分: 輸出部分 : 輸入部分 : 損壞部分

  • 指令部分: 這部分和純粹的匯編大致相同但是由於涉及到和C變量結合,那么對於數字加上%前綴表示
    需要使用寄存器的樣板操作數. 指令部分中用到多少操作數代表有多少變量需要和寄存器結合, 由gcc和
    gas在編譯和匯編時根據后面提供的約束條件給出. 為了避免和樣板操作數混淆,具體指定寄存器需要使用%%前綴.

    在指令部分對操作數的引用總是將其當成32bits 的長字, 也可以顯式的聲明對於那個字節的操作, 如在%與序號間插入
    b 表示最低字節, 插入一個h表示次低字節.

  • 輸出部分: 這部分是對於目標操作數如何結合的約束條件, 每個條件稱為一個約束(constraint), 每個輸出約束以=開頭
    ,然后是一個字母表示對操作數類型的說明, 之后是關於變量結合的約束.

    :"=m" (v->counter) 就表示目標操作數(指令部分的%0)是一個內存單元 v->counter

    凡是與輸出部中說明的操作數相結合的寄存器或操作數本身,在執行嵌入的匯編代碼以后均不保留執行前的內容

  • 輸入部分: 與輸出約束類似但是不帶=號.

    :"ir" (i), "m" (v->counter));中有兩個輸入約束, "ir"(i)表示指令中的%可以是一個在寄存器中的直接操作數
    ,並且該操作數來自C代碼的變量i.

    第二個約束為"m"(v->counter) 意思為為操作數(%0)分配一個內存單元.

  • 損壞部分: 對於明示使用的寄存器gcc會對其作pushl和popl操作來保護原有內容.但是對於中間操作使用的寄存器沒法
    保證, 所以要在損壞部分對操作的副作用進行說明.

操作數的編號從輸出部得第一個約束開始, 順序數下來,每個約束計數一次.

約束條件:

條件 說明
m v o Memory
r Any Register
q Anyone in eax, ebx, ecx, edx
i h Direct operation
E F Float
g Any
a b c d eax, ebx, ecx, edx
S D esi edi
I constant(0-31)

__memcpy() 的實現:

/* include/asm-i386/string.h 199 */

static inine void * __memcpy(void * to, const void * from, size_t n){
	int d0, d1, d2;
	__asm__ __volatile__(
		"rep ; movsl \n\t"
		"testb $2, %b4\n\t"
		"je 1f\n\t"
		"1:\ttestb $1,%b4\n\t"
		"je 2f\n\t"
		"movsb\n"
		"2:"
		: "=&c" (d0), "=&D" (d1), "=&S" (d2)
		: "0" (n/4), "q" (n), "1" ((long) to), "2" ((long) from)
		: "memory"
			);
	return (to);                    
}

解析:

  • 對於輸出部: 有三條約束,分別定義了變量d0對應%0操作數必須放在ecx中,
    變量d1對應操作數%1必須存在edi中, 變量d2對應操作數%2必須放在esi中.

  • 對於輸入部: 有四條約束, 分別規定了%3%0使用相同的寄存器即ecx, 並且要求gcc自動插入必要指令
    事先將其設置c為n/4(實際上是由字節數n換算成長字(DWORD)個數n/4),對於n自身由gcc自動分配寄存器;

    %5%6即參數tofrom, 分別與%1,%2使用相同的寄存器,所以為ediesi

  • 對於指令部: 第一條為rep, 表示下一條指令將重復執行, 每重復一次ecx自減,直至為零.

    movsl 默認將esi所指的內容復制一個長字到edi所指位置,然后將esiedi分別加4.

    拷貝完長字部分還需判斷有無剩余, 這里測試n的最低字節中的bit2, 若為1表示至少還有兩個字節
    然后使用movsw來拷貝.

    再通過測試n的最低字節中的bit1, 若為1則表示還有一個字節未拷貝, 通過movsb解決.

** strncmp() **實現:

/* include/asm-i386/string.h 127 */
static inline int strncmp(const char * cs,const char * ct,size_t count)
{
register int __res;
int d0, d1, d2;
__asm__ __volatile__(
	"1:\tdecl %3\n\t"
	"js 2f\n\t"
	"lodsb\n\t"
	"scasb\n\t"
	"jne 3f\n\t"
	"testb %%al,%%al\n\t"
	"jne 1b\n"
	"2:\txorl %%eax,%%eax\n\t"
	"jmp 4f\n"
	"3:\tsbbl %%eax,%%eax\n\t"
	"orb $1,%%al\n"
	"4:"
		     :"=a" (__res), "=&S" (d0), "=&D" (d1), "=&c" (d2)
		     :"1" (cs),"2" (ct),"3" (count));
return __res;
}

解析:

  1. 輸出部: 定義了 %0__res 必須與eax綁定, %1d0esi綁定,
    %2d1edi綁定, %3d2ecx綁定.

  2. 輸入部: 定義了 %4cs%1使用相同的寄存器, %5ct%2使用相同寄存器. %6count%3使用相同寄存器.

  3. 指令部
    指令參考

    OP Explanation
    decl 自減
    js 前面運算完符號位SF為1時候跳轉
    lodsb 將DS:ESI指向的源串元素裝入AL中
    scasb 將AL的內容減去DS:EDI指向元素
    sbbl 帶借位的減操作 並寫入
    orb 字節邏輯或操作 並寫入
    testb 字節與測試

    先對ecxcount自減 如果結果為負直接跳出並返回0, 若為正數則開始比較, 首先通過lodsb指令
    裝入AL寄存器, 再通過串掃描比較EDI執行與AL的內容, 如不等直接返回1; 相等之后判斷當前是否到達串尾(al=0)
    未到達則返回開始處循環比較.

存儲管理

2.1 內存管理基本框架

Linux考慮到64bit系統, 將內核的映射機制設計成三層,在頁目錄和頁面表的中間增設"中間目錄"概念.

  • 頁面目錄 PGD(Page Global Directory)
  • 中間目錄 PMD(Page Middle Directory)
  • 頁面表 PT (Page Table)
  • 表項 PTE (Page Table Entry)

線性地址到物理地址的映射過程: PGD+PMD+PT+OFFSET

內核將內存划分為1G內核空間和3G用戶空間
其中0xC0000000-0xFFFFFFFF為內核空間, 線性映射所有內核進程共享該空間:

/* include/asm-i386/page.h 68 */
/*
 * This handles the memory map.. We could make this a config
 * option, but too many people screw it up, and too few need
 * it.
 *
 * A __PAGE_OFFSET of 0xC0000000 means that the kernel has
 * a virtual address space of one gigabyte, which limits the
 * amount of physical memory you can use to about 950MB. 
 *
 * If you want more physical memory than this then see the CONFIG_HIGHMEM4G
 * and CONFIG_HIGHMEM64G options in the kernel configuration.
 */

#define __PAGE_OFFSET		(0xC0000000)


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

注意:這里物理內存是從0開始映射的,也就是物理內存0-1G范圍內被映射到內核
實際的映射由MMU完成這里是方便內核取得物理地址, 如在進程切換的時候CR3要做相應切換,
其指向的PGD應為物理地址, 而該目錄的起始地址在內核中式以虛地址形式存在的,所以用到了
這里的__pa()函數轉換. 代碼如下:

/* include/asm-i386/mmu_context.h 43 */

/* Re-laod page tables */
 
asm volatile("movl %0, %%cr3": : "r" (__pa(next->pgd)));

TIPs: 如何計算系統最大進程數
由於全局段描述表GDT中要保存每個進程的局部描述表LDT同時要保存TSS(Task State Struct), 除去保留項
GDT(13bit寬度)可用8180,所以理論上最大進程數為4090.

2.2 地址映射全過程

Linux 采用先分段再分頁是對Intel CPU的妥協, 其實在 M68K, PowerPC上是不存在段式映射的.
比如在可執行程序ELF通過objdump解析后得出如下:

0804119d <greeting>:
 804119d:	55                   	push   %ebp
 804119e:	89 e5                	mov    %esp,%ebp
 80411a0:	53                   	push   %ebx
 80411a1:	83 ec 04             	sub    $0x4,%esp
 80411a4:	e8 3b 00 00 00       	call   11e4 <__x86.get_pc_thunk.ax>
 80411a9:	05 57 2e 00 00       	add    $0x2e57,%eax
 80411ae:	83 ec 0c             	sub    $0xc,%esp
 80411b1:	8d 90 08 e0 ff ff    	lea    -0x1ff8(%eax),%edx
 80411b7:	52                   	push   %edx
 80411b8:	89 c3                	mov    %eax,%ebx
 80411ba:	e8 81 fe ff ff       	call   1040 <puts@plt>
 80411bf:	83 c4 10             	add    $0x10,%esp
 80411c2:	90                   	nop
 80411c3:	8b 5d fc             	mov    -0x4(%ebp),%ebx
 80411c6:	c9                   	leave  
 80411c7:	c3                   	ret    

從上述結果可以看出ld為greeting()分配了地址0x08048386.
那么如何解析需要從

  1. 段式映射階段: 由於當前執行過程中是由CPU中的EIP指定,在代碼段中, 所以段選擇index存於代碼段寄存器CS
    中, CS內容由內核建立進程時設置, regs->xcs = __USER_CS. 其中 __USER_CS 被定義為 0x23.

    /* include/asm-i386/processor.h 408 */
    #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)
    
    /* include/asm-i386/segment.h 4 */
    
    #define __KERNEL_CS 0x10
    #define __KERNEL_DS 0x18
    #define __USER_CS   0x23
    #define __USER_DS   0x2B
    

段寄存器值對照表如下

Name Value index TI RPL
__KERNEL_CS 0x10 0000 0000 0001 0 0 00
__KERNEL_DS 0x18 0000 0000 0001 1 0 00
__USER_CS 0x23 0000 0000 0010 0 0 11
__USER_DS 0x2B 0000 0000 0010 1 0 11

可以看出所有的TI都是0 代表只使用GDT(LDT只有在VM86模式中運行wine以及模擬Windows環境才會用到)

RPL只用到了 0 內核 和 3 用戶.

下面在GDT中找到index = 4 的描述項(從0開始的)

/* arch/i386/kernel/head.S 444 */
/*
 * This contains typically 140 quadwords, depending on NR_CPUS.
 *
 * NOTE! Make sure the gdt descriptor in head.S matches this if you
 * change anything.
 */
ENTRY(gdt_table)
	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x0000000000000000	/* not used */
	.quad 0x00cf9a000000ffff	/* 0x10 kernel 4GB code at 0x00000000 */
	.quad 0x00cf92000000ffff	/* 0x18 kernel 4GB data at 0x00000000 */
	.quad 0x00cffa000000ffff	/* 0x23 user   4GB code at 0x00000000 */
	.quad 0x00cff2000000ffff	/* 0x2b user   4GB data at 0x00000000 */
	.quad 0x0000000000000000	/* not used */
	.quad 0x0000000000000000	/* not used */
	/*
	 * The APM segments have byte granularity and their bases
	 * and limits are set at run time.
	 */
	.quad 0x0040920000000000	/* 0x40 APM set up for bad BIOS's */
	.quad 0x00409a0000000000	/* 0x48 APM CS    code */
	.quad 0x00009a0000000000	/* 0x50 APM CS 16 code (16 bit) */
	.quad 0x0040920000000000	/* 0x58 APM DS    data */
	.fill NR_CPUS*4,8,0		/* space for TSS's and LDT's */

GDT第一項和第二項不使用, 可以解析得到如下結論:
除了DPL權限和type字段表示代碼和數據段不同外, 其余都完全相同.

每個段都是從0地址開始的整個4GB的虛擬空間, 虛地址到線性地址的映射保持原值不變.(段映射機制形同虛設, 虛擬地址到線性地址是完全相同的)

  1. 頁映射階段
    與段式映射過程中所有進程共用GDT不同, 每個進程的PGD是不通的, 由每個進程的mm_struct數據結構保存.
    進程切換需要將CR3設置為新的PGD物理地址, MMU硬件直接從CR3中取得指向當前頁面的物理地址.

    將0x0804119d二進制展開:$$0000, 1000, 0000, 0100, 0001, 0001, 1001, 1010$$
    解析如下

    name value
    頁面目錄項 0000100000
    頁面表項 1000010001
    offset 000110011010

總結: 頁面映射需要訪存三次, 第一次是頁面目錄, 第二次是頁面表, 第三次是訪問的真正目標.

2.3 常見數據結構


/* include/asm-i386/page.h 36 */
/*
 * These are used to make use of C type-checking..
 */
#if CONFIG_X86_PAE
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
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

pgd_t, pmd_t and pte_t are all long integer, when it comes with PAE enable, they are
defined to long long integer.

值得注意的是: pte_t只有前20bit是基地址(頁面大小是4KB), 所以余下的12bit作為頁面的狀態信息和訪問權限.

內核代碼中頁面項的生成方式如下:

頁面序號左移12位,與頁面控制/狀態位段相或,得到表項值.

/* include/asm-i386/pgtable-2level.h 61 */

#define __mk_pte(page_nr, pgprot) __pte(((page_nr) << PAGE_SHIFT) |  pgprot_val(pgprot))

/* include/asm-i386/page.h */
#define pgprot_val(x) ((x).pgprot)
#define __pte(x) ((pte_t) {(x)})
/* include/asm-i386/pgtable.h 162 */

#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 */

內核中有全局變量mem_map, 是一個指向page的數組, 每個page代表一個物理頁面.

/* include/linux/mm.h 126 */

/*
 * Try to keep the most commonly accessed fields in single cache lines
 * here (16 bytes or greater).  This ordering should be particularly
 * beneficial on 32-bit processors.
 *
 * The first line is data used in page cache lookup, the second line
 * is used for linear searches (eg. clock algorithm scans). 
 */
typedef struct page {
	struct list_head list;
	struct address_space *mapping;
	unsigned long 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;

當頁面內容來自文件,index代表頁面在文件中的序號;

當頁面內容被swap, 但是內容還作為緩沖時,怎index指明了頁面的去向.

系統中的每個物理頁面都有一個page結構,系統在初始化時根據物理內存的大小建立其一個page結構數組
mem_map.每個物理頁面的page結構在這個數組里的下標就是該物理頁面的序號.
物理頁面被分為ZONE_DMAZONE_NORMAL(以及物理內存超過1GB時的ZONE_HIGHMEM)

ZONE_DMA的理由:

  1. 預留空間給磁盤I/O
  2. DMA由於不經過MMU提供地址映射,所以需要直接訪問物理地址.
  3. 使用DMA緩沖很大所以需要的物理頁要連續, 不能被分頁機制破環地址的連續性.
/* include/linux/mmzone.h 11 */

/*
 * 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;
	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

其中offset代表該分區在mem_map中的起始頁面號, 管理區的建立每個物理頁面便永久屬於該管理區.

理想環境下, 物理空間均勻一致, CPU訪問內存中的任意地址時間相同,稱之為均質存儲結構(Uniform Memory Architecture)

但實際情況下都是NUMA結構, 所以上述的物理頁面管理機制要修改, 管理區不再是最高層機構, 而時在每個存儲節點都需要至少兩個管理區. 而且page結構數組頁不在時全局性的, 而數叢書與具體的節點. 在zone_struct之上又有了一層代表着存儲節點的pglist_data數據結構.

/* include/linux/mmzone.h */

typedef struct pglist_data {
	zone_t node_zones[MAX_NR_ZONES];
	zonelist_t node_zonelists[NR_GFPINDEX];
	struct page *node_mem_map;
	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;

pglist_data可以通過node_next形成一個單鏈隊列.
每個結構中的指針node_mem_map指向具體節點的page結構數組.
node_zone[] 代表該節點的最多三個頁面管理區, 同時zone_struct結構中也有指向pglist_data的指針.

node_zonelists鏈是用來進行頁面分配指定頁面管理區的分配順序的.

同時相對於物理內存的管理, 虛擬內存同樣也有虛存空間的概念.
對其的抽象就是vm_area_struct數據結構. 虛存空間由[vm_start, vm_end) 左閉右開.
權限由vm_page_protvm_flags 控制.
通過vm_next 進行遍歷, 同時為了加快查找速度建立了AVL(Adelson-Velskii and Landis)樹結構.


/* include/linux/mm.h */

/*
 * 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;
	struct vm_area_struct * vm_avl_left;
	struct vm_area_struct * vm_avl_right;

	/* 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) */
};

在兩種情況下虛存頁面會與磁盤文件發生交互:

  1. 盤區交換swap
  2. mmap系統調用, 將已打開文件映射到用戶空間,通訪存的方式來訪問文件。

虛擬區間操作函數:

/* include/linux/mm.h:115 */
/*
 * 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);
};

openclose 負責虛存空間的打開和關閉, nopage 為出現page fault時調用函數。

最后vm_area_struct中還有一個指針vm_mm, 該指針指向一個mm_struct數據結構。
事實上這個是更高層次的數據結構, 每個進程只有一個mm_struct , 可以看作整個用戶空間的抽象。
mm_structtask_structmm指針指向。當進程傳見子進程時會共享該結構。

在某進程空間內找到給定虛擬地址的所屬的區間的vm_area_struct, 有find_vma()函數實現:

/* mm/mmap.c 404 */

/* 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. */
		/* (Cache hit rate is typically around 35%.) */
		vma = mm->mmap_cache;
		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 {
				/* 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;
		}
	}
	return vma;
}

可以看出先對緩存mmap_cache進行判斷,然后通過AVL/線性查找方式去定位vma.

2.4 越界訪問

  1. 訪問失敗
  • 對應頁面目錄,頁面表項為空。
  • 物理頁不在內存中
  • 訪問權限不匹配
  1. 頁錯誤處理
    上述將會引發Page Fault異常, do_page_fault
/* arch/i386/mm/fault.c 106 */

/*
 * This routine handles page faults.  It determines the address,
 * and the problem, and then passes it off to one of the appropriate
 * routines.
 *
 * error_code:
 *	bit 0 == 0 means no page found, 1 means protection fault
 *	bit 1 == 0 means read, 1 means write
 *	bit 2 == 0 means kernel, 1 means user-mode
 */
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
	struct task_struct *tsk;
	struct mm_struct *mm;
	struct vm_area_struct * vma;
	unsigned long address;
	unsigned long page;
	unsigned long fixup;
	int write;
	siginfo_t info;

	/* get the address */
	__asm__("movl %%cr2,%0":"=r" (address));

	tsk = current;

	/*
	 * We fault-in kernel-space virtual memory on-demand. The
	 * 'reference' page table is init_mm.pgd.
	 *
	 * NOTE! We MUST NOT take any locks for this case. We may
	 * be in an interrupt or a critical region, and should
	 * only copy the information from the master page table,
	 * nothing more.
	 */
	if (address >= TASK_SIZE)
		goto vmalloc_fault;

	mm = tsk->mm;
	info.si_code = SEGV_MAPERR;

	/*
	 * If we're in an interrupt or have no user
	 * context, we must not take the fault..
	 */
	if (in_interrupt() || !mm)
		goto no_context;

	down(&mm->mmap_sem);

	vma = find_vma(mm, address);
	if (!vma)
		goto bad_area;
	if (vma->vm_start <= address)
		goto good_area;
	if (!(vma->vm_flags & VM_GROWSDOWN))
		goto bad_area;
	if (error_code & 4) {
		/*
		 * accessing the stack below %esp is always a bug.
		 * The "+ 32" is there due to some instructions (like
		 * pusha) doing post-decrement on the stack and that
		 * doesn't show up until later..
		 */
		if (address + 32 < regs->esp)
			goto bad_area;
	}
	if (expand_stack(vma, address))
		goto bad_area;
/*
 * Ok, we have a good vm_area for this memory access, so
 * we can handle it..
 */
good_area:
	info.si_code = SEGV_ACCERR;
	write = 0;
	switch (error_code & 3) {
		default:	/* 3: write, present */
#ifdef TEST_VERIFY_AREA
			if (regs->cs == KERNEL_CS)
				printk("WP fault at %08lx\n", regs->eip);
#endif
			/* fall through */
		case 2:		/* write, not present */
			if (!(vma->vm_flags & VM_WRITE))
				goto bad_area;
			write++;
			break;
		case 1:		/* read, present */
			goto bad_area;
		case 0:		/* read, not present */
			if (!(vma->vm_flags & (VM_READ | VM_EXEC)))
				goto bad_area;
	}

	/*
	 * If for any reason at all we couldn't handle the fault,
	 * make sure we exit gracefully rather than endlessly redo
	 * the fault.
	 */
	switch (handle_mm_fault(mm, vma, address, write)) {
	case 1:
		tsk->min_flt++;
		break;
	case 2:
		tsk->maj_flt++;
		break;
	case 0:
		goto do_sigbus;
	default:
		goto out_of_memory;
	}

	/*
	 * Did it hit the DOS screen memory VA from vm86 mode?
	 */
	if (regs->eflags & VM_MASK) {
		unsigned long bit = (address - 0xA0000) >> PAGE_SHIFT;
		if (bit < 32)
			tsk->thread.screen_bitmap |= 1 << bit;
	}
	up(&mm->mmap_sem);
	return;

/*
 * Something tried to access memory that isn't in our memory map..
 * Fix it, but check if it's kernel or user first..
 */
bad_area:
	up(&mm->mmap_sem);

bad_area_nosemaphore:
	/* User mode accesses just cause a SIGSEGV */
	if (error_code & 4) {
		tsk->thread.cr2 = address;
		tsk->thread.error_code = error_code;
		tsk->thread.trap_no = 14;
		info.si_signo = SIGSEGV;
		info.si_errno = 0;
		/* info.si_code has been set above */
		info.si_addr = (void *)address;
		force_sig_info(SIGSEGV, &info, tsk);
		return;
	}

	/*
	 * Pentium F0 0F C7 C8 bug workaround.
	 */
	if (boot_cpu_data.f00f_bug) {
		unsigned long nr;
		
		nr = (address - idt) >> 3;

		if (nr == 6) {
			do_invalid_op(regs, 0);
			return;
		}
	}

no_context:
	/* Are we prepared to handle this kernel fault?  */
	if ((fixup = search_exception_table(regs->eip)) != 0) {
		regs->eip = fixup;
		return;
	}

/*
 * Oops. The kernel tried to access some bad page. We'll have to
 * terminate things with extreme prejudice.
 */

	bust_spinlocks();

	if (address < PAGE_SIZE)
		printk(KERN_ALERT "Unable to handle kernel NULL pointer dereference");
	else
		printk(KERN_ALERT "Unable to handle kernel paging request");
	printk(" at virtual address %08lx\n",address);
	printk(" printing eip:\n");
	printk("%08lx\n", regs->eip);
	asm("movl %%cr3,%0":"=r" (page));
	page = ((unsigned long *) __va(page))[address >> 22];
	printk(KERN_ALERT "*pde = %08lx\n", page);
	if (page & 1) {
		page &= PAGE_MASK;
		address &= 0x003ff000;
		page = ((unsigned long *) __va(page))[address >> PAGE_SHIFT];
		printk(KERN_ALERT "*pte = %08lx\n", page);
	}
	die("Oops", regs, error_code);
	do_exit(SIGKILL);

/*
 * We ran out of memory, or some other thing happened to us that made
 * us unable to handle the page fault gracefully.
 */
out_of_memory:
	up(&mm->mmap_sem);
	printk("VM: killing process %s\n", tsk->comm);
	if (error_code & 4)
		do_exit(SIGKILL);
	goto no_context;

do_sigbus:
	up(&mm->mmap_sem);

	/*
	 * Send a sigbus, regardless of whether we were in kernel
	 * or user mode.
	 */
	tsk->thread.cr2 = address;
	tsk->thread.error_code = error_code;
	tsk->thread.trap_no = 14;
	info.si_code = SIGBUS;
	info.si_errno = 0;
	info.si_code = BUS_ADRERR;
	info.si_addr = (void *)address;
	force_sig_info(SIGBUS, &info, tsk);

	/* Kernel mode? Handle exceptions or die */
	if (!(error_code & 4))
		goto no_context;
	return;

vmalloc_fault:
	{
		/*
		 * Synchronize this task's top level page-table
		 * with the 'reference' page table.
		 */
		int offset = __pgd_offset(address);
		pgd_t *pgd, *pgd_k;
		pmd_t *pmd, *pmd_k;

		pgd = tsk->active_mm->pgd + offset;
		pgd_k = init_mm.pgd + offset;

		if (!pgd_present(*pgd)) {
			if (!pgd_present(*pgd_k))
				goto bad_area_nosemaphore;
			set_pgd(pgd, *pgd_k);
			return;
		}

		pmd = pmd_offset(pgd, address);
		pmd_k = pmd_offset(pgd_k, address);

		if (pmd_present(*pmd) || !pmd_present(*pmd_k))
			goto bad_area_nosemaphore;
		set_pmd(pmd, *pmd_k);
		return;
	}
}

解析:

  1. 首先定義了三個error_code
error_code bit 0 1
bit0 no page found protection fault
bit1 read write
bit2 kernel user-mode
  1. 由於page fault發生時, CPU會將失敗地址放入CR2寄存器中, 所以通過匯編將其放入address.
    同時異常還發送了當時的reg以及error_code, 用與確定當時的現場以及失敗的原因.
  2. 之后是對當前進程task_struct的獲取(通過current宏實現)
  3. 接下來針對page_fault的發生是是在中斷還是異常,即進程有關和無關問題. 特別地, 中斷表示
    內存映射失敗發生在某個中斷服務程序中,而與進程無關, 還有一種情形是映射mm指針為空, 說明進程的映射尚未建立, 也與當前進程無關.這種情況交由no_cotext處理.
  4. 接下來需要對操作進行互斥訪問, 對mm_struct設置了信號量mmap_sem.由down/up操作進行P/V.
  5. 確定完失敗地址和進程以后需要對地址映射的區間定位, 這里用到了find_vma(), 如果找不到說明訪問越界,繼而轉向bad_area. 如果找到了則說明映射已經建立了, 轉向good_area.
  6. 如果給定地址落在空洞區(被撤銷或者未建立), 未建立的空洞只有一種情形, 那就是堆棧區以下的空洞.
    如何確定? 通過vm_area_structvm_flags中的標志位VM_GROWSDOWN, 該標志位為0時代表空洞上方非堆棧區,那么就是撤銷的種情形, 反之就是堆棧下的空洞.
  7. 確定地址位於被撤銷的空洞則轉向bad_area, 由於這里不需要對mm_struct進行操作, 先通過up()釋放mmap_sem信號, 之后判斷error_code的bit2為1時, 表示失敗是由CPU處於用戶模式那么進行一些task設置后發出SIGSEGV信號退出.

2.5 用戶堆棧的擴展

   ┏━━━━━━━━━━━━┓-----------------
   ┃ / / / / / /┃ 
   ┃/ / / / / / ┃ ←---系統空間
   ┣━━━━━━━━━━━━┫ 0xC0000000 ----
   ┃            ┃
   ┃  堆 棧 區  ┃
   ┃            ┃
   ┣━━━━━━━━━━━━┫
   ┃  空  洞    ┃
   ┃            ┃
   ┣━━━━━━━━━━━━┫ ←---用戶空間
   ┃ 數據       ┃
   ┃ 代碼區     ┃
   ┗━━━━━━━━━━━━┛-----------------

對於堆棧區下方的空洞, 需要檢查其異常地址是否緊挨着堆棧指針所指空間, 如果地址在%esp-4.
i386 CPU的pusha 將會使%esp-32, 超出該范圍一定是錯的, 轉向bad_area.

如果是正常的堆棧擴展要求, 那么應該從空洞頂部開始分配若干頁面建立映射.
調用expand_stack()

/* inlcude/linux/mm.h  487 */

/* vma is the first one with  address < vma->vm_end,
 * and even  address < vma->vm_start. Have to extend vma. */
static inline int expand_stack(struct vm_area_struct * vma, unsigned long address)
{
	unsigned long grow;

	address &= PAGE_MASK;
	grow = (vma->vm_start - address) >> PAGE_SHIFT;
	if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur ||
	    ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur)
		return -ENOMEM;
	vma->vm_start = address;
	vma->vm_pgoff -= grow;
	vma->vm_mm->total_vm += grow;
	if (vma->vm_flags & VM_LOCKED)
		vma->vm_mm->locked_vm += grow;
	return 0;
}

解析:
vma 代表用戶空間堆棧所在區間, 確定頁號偏移量, 分配空間(注意棧大小不可大於RLIMIT_STACK).
此外此函數只改變了堆棧區vma的結構, 並未建立物理內存的映射.

接下來由good_area完成, 首先對error_code判斷並分配對策, 對於上述情形, 可以執行handle_mm_fault().

/* mm/memory.c 1189 */

/*
 * By the time we get here, we already hold the mm semaphore
 */
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
	unsigned long address, int write_access)
{
	int ret = -1;
	pgd_t *pgd;
	pmd_t *pmd;

	pgd = pgd_offset(mm, address);
	pmd = pmd_alloc(pgd, address);

	if (pmd) {
		pte_t * pte = pte_alloc(pmd, address);
		if (pte)
			ret = handle_pte_fault(mm, vma, address, write_access, pte);
	}
	return ret;
}
  1. pdg_offset()宏操作計算指向該地址所屬頁面目錄項的指針.
#define pdg_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#deinfe pdg_offset(mm, address) ((mm)->pdg + pgd_index(address))
  1. 對於pmg_alloc分配中間頁目錄項, 由於i386只用了兩層映射, 所以被定義為return (pmd_t *)pgd;.
  2. 接下來分配頁面表的頁面pte_alloc(), 先將給定地址轉換成所屬頁面表的下表, 加入目錄項為空, 那么需要get_new()來生成頁面表. (當釋放頁面表時, 內核將釋放的頁面表先保存在緩沖池中, 而不先將物理內存頁面釋放, 只有當池滿的情況才會將頁面表所占的物理內存頁面釋放.)
  3. 在緩沖池內的頁面表可以通過get_pte_fast獲取, 若不存在則使用get_pte_slow獲取.
  4. 有了頁目錄表, 中間目錄表,頁面表以后接下來需要對物理內存頁面進行映射, 由handle_pte_fault()完成.
/* mm/memory.c 1135 */

/*
 * These routines also need to handle stuff like marking pages dirty
 * and/or accessed for architectures that don't do it in hardware (most
 * RISC architectures).  The early dirtying is also good on the i386.
 *
 * There is also a hook called "update_mmu_cache()" that architectures
 * with external mmu caches can use to update those (ie the Sparc or
 * PowerPC hashed page tables that act as extended TLBs).
 *
 * Note the "page_table_lock". It is to protect against kswapd removing
 * pages from under us. Note that kswapd only ever _removes_ pages, never
 * adds them. As such, once we have noticed that the page is not present,
 * we can drop the lock early.
 *
 * The adding of pages is protected by the MM semaphore (which we hold),
 * so we don't need to worry about a page being suddenly been added into
 * our VM.
 */
static inline int handle_pte_fault(struct mm_struct *mm,
	struct vm_area_struct * vma, unsigned long address,
	int write_access, pte_t * pte)
{
	pte_t entry;

	/*
	 * We need the page table lock to synchronize with kswapd
	 * and the SMP-safe atomic PTE updates.
	 */
	spin_lock(&mm->page_table_lock);
	entry = *pte;
	if (!pte_present(entry)) {
		/*
		 * If it truly wasn't present, we know that kswapd
		 * and the PTE updates will not touch it later. So
		 * drop the lock.
		 */
		spin_unlock(&mm->page_table_lock);
		if (pte_none(entry))
			return do_no_page(mm, vma, address, write_access, pte);
		return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access);
	}

	if (write_access) {
		if (!pte_write(entry))
			return do_wp_page(mm, vma, address, pte, entry);

		entry = pte_mkdirty(entry);
	}
	entry = pte_mkyoung(entry);
	establish_pte(vma, address, pte, entry);
	spin_unlock(&mm->page_table_lock);
	return 1;
}

pte為空那么進入do_no_page() 否則do_swap_page().

COW(Copy On Write):

讀共享,寫時復制.

do_no_pagedo_anonymous_page實現:

  1. ptre_wrprotect() 修復讀操作異常, 將_PAGE_RW設為0, 表示頁面只讀.
  2. pte_mkwrite() 修復寫操作異常, 將_PAGE_RW設為1, 但是所映射的頁面都是ZERO_PAGE. 就是說凡是
    寫保護的頁面, 開始一律映射到物理內存頁面empty_zero_page, 而不管其虛擬地址是什么. 實際上, 這個頁面的內容為全0, 所以映射之初若從該頁面讀出就讀得0.
    只有科協頁面才通過alloc_page()為其分配獨立的物理內存.
  3. 這里頁面位於堆棧區, 首先通過alloc_page()分配物理內存頁面, 然后設置狀態和標志位,並通過set_pte設置進指針page_table所指的頁面表項. 注意i386的MMU實現是在CPU內部,所以這里的update_mmu_cache是個空函數.

最后, 經歷層層返回控制流到do_page_fault,然后交由用戶空間, 重新執行失敗的指令.(這是與普通中斷所不同的)

2.6 物理頁的使用和周轉

在系統初始化階段, 系統檢測全部物理內存, 並為每個物理頁分配page結構, 形成全局數組, 由mem_map指向.
同時又將這些頁面合成物理地址連續的許多內存頁面塊, 在根據塊的大小建立其若干zone, 在每個zone內設置一個空閑塊隊列組. 類似的, 交換設備的每個物理頁面也要在內存有相應的數據結構,swap_info_struct.
這里定義了計數器表示頁面是否被分配使用, 以及多少用戶在共享頁面. swap_map指向一個數組, 數組內的元書是盤上的物理頁面, 下標表示盤上位置.數組大小取決於pages, 表示頁面交換設備大小.

內核還允許多個頁面交換設備建立一個swap_info_struct的數組swap_info.

同時還設立了一個隊列swap_list定義優先級.

就像通過pte_t結構建立其物理內存頁面和虛存頁面聯系一樣, 盤上頁面也有swp_entry_t數據結構.
其為32位無符號整數, sw怕_entry_t = offset(24bbit) + type(7bit) + 0(1bit)
offset是文件的邏輯頁面號, type是設備號.
其實從

#define pte_to_swp_entry(pte)  ((swp_entry_t) {(pte).pte_low})
#define swp_entry_to_pte(x) ((pte_t) {(x).val })

可以看出兩者是可以互相轉換的, 其實不同點在與type處的P值, 若該值為1表示其位於內存中,那么就可以當其為pte, 否則變成一個swp_entry_t.
注意: 盤上頁面的釋放不需要擦除數據, 只需要在內存中消除相應記錄即可.

所謂內存頁面的周轉分為

  1. 頁面的分配, 使用和回收
  2. 盤區交換, 交換的終極目的也是頁面的回收. 也並非所有頁面都可以被交換: 只有映射到用戶內存空間的頁面才會被換出 (實際上內核區有一部分是物理內存的完全映射ps:4G以內)

內存分類:

  1. 內核代碼和內核全局變量所占內存頁面是靜態的, 不會被分配以及換出.

  2. 除上內核使用的頁面還是需要動態分配, 但是不會被交換.

    • "閱后即焚" 空閑-> 分配 -> 使用 -> 釋放 -> 空閑
    • 具有緩存價值: 放入LRU
  3. 進程的代碼段和全局變量都在用戶空間, 所占的內存頁面都是動態的.

頁面交換:

為了防止抖動發生, 需要做些調整:

  1. 空閑 頁面的page數據結構通過隊列頭結構list鏈入某個頁面管理區zone的空閑區隊列free_area/ 頁面的技術count=0.

  2. 分配 通過函數__alloc_pages()_get_free_page() 從某個空閑隊列中分配內存頁面, 並將分配的使用計數count=1, 其page數據結構的隊列頭list結構變為空閑.

  3. 活躍狀態 頁面的page數據結構通過其隊列頭結構lru鏈入活躍頁面隊列active_list, 並且至少有一個進程空間頁面表項指向該頁面. 每當為頁面建立或恢復映射時, 都是頁面的使用計數count加一.

  4. 不活躍狀態 臟頁 頁面的page結構通過其隊列頭結構lru鏈入不活躍臟頁隊列inactive_dirty_list, 但是原則上不再有任何進程的頁面表項指向該頁面. 每當斷開頁面映射時都使頁面的使用計數count減1.
    將不活躍的臟頁寫入將換設備, 並將頁面的page數據結構從不活躍臟頁面隊列inactive_dirty_list轉移到某個不活躍干凈頁面隊列中.

  5. 不活躍狀態 干凈 頁面的page數據結構通過其隊列頭結構lru鏈入某個不活躍頁面隊列 inactive_clean_list.

  6. 如果在轉入不活躍狀態后頁面被訪問, 則轉入活躍狀態並恢復映射.

  7. 如有需要, 就從干凈頁面隊列回收頁面, 或退回空閑隊列, 或直接另行分配.

注意: 內核設置了全局性的active_list和inactive_dirty_list兩個LRU隊列, 還在每個頁面管理區
zone設置了一個inactive_clean_list.
為了回收頁面提供參考, 同時通過一個全局address_space數據結構swapper_space, 把所有的可交換的內存頁面管理起來, 每個可交換內存頁面的page數據結構都是通過其隊列頭結構list鏈入其中的一個隊列.此外, 為了加快在暫存隊列中的搜索, 又設置了一個page_hash_table

內核分配空閑內存頁面以后, 通過add_to_swap_cache()將其page結構鏈入相應的隊列.

2.7 物理頁面分配

當一個進程需要分配若干連續的物理頁面時, 可以通過alloc_pages()實現.

Linux 2.4.0有兩種實現分別位於mm/numa.cmm/page_alloc.c中.

/* mm/numa.c  91 */

/*
 * This can be refined. Currently, tries to do round robin, instead
 * should do concentratic circle search, starting from current node.
 */
struct page * alloc_pages(int gfp_mask, unsigned long order)
{
	struct page *ret = 0;
	pg_data_t *start, *temp;
#ifndef CONFIG_NUMA
	unsigned long flags;
	static pg_data_t *next = 0;
#endif

	if (order >= MAX_ORDER)
		return NULL;
#ifdef CONFIG_NUMA
	temp = NODE_DATA(numa_node_id());
#else
	spin_lock_irqsave(&node_lock, flags);
	if (!next) next = pgdat_list;
	temp = next;
	next = next->node_next;
	spin_unlock_irqrestore(&node_lock, flags);
#endif
	start = temp;
	while (temp) {
		if ((ret = alloc_pages_pgdat(temp, gfp_mask, order)))
			return(ret);
		temp = temp->node_next;
	}
	temp = pgdat_list;
	while (temp != start) {
		if ((ret = alloc_pages_pgdat(temp, gfp_mask, order)))
			return(ret);
		temp = temp->node_next;
	}
	return(0);
}

解析:

  1. 首先通過NODE_DATA宏和numa_node_id()取到CPU所在節點的pg_data_t隊列到temp.
  2. 兩個while循環, 第一次從temp到隊尾查找分配符合節點, 第二次從頭節點到temp節點查找合適節點.

TODO: page 86

2.8 頁面的定期換出

kswapd是一個由內核調度的線程, 專門負責頁面換出.

kswapd 通過在系統初始化進行kswapd_init()

  1. swap_setup根據物理內存大小設定一個全局量page_cluster 該參數是用於讀磁盤預讀數目指示.
  2. 創建現場kswapdkreclaimd
    初始化之后, 程序進入無線循環, 每次循環的末尾調用interruptible_sleep_on_timeout() 進入睡眠, 讓內核自由調度. 一定時間后又會被喚醒, 繼續執行. 本版本定義了經歷HZ次時鍾中斷后重新喚醒, 而HZ代表每秒鍾始終中斷次數, 即1s鍾重新調度.

kswapd 在例程中會有兩部分:

  1. 物理頁短缺的情況下, 找到可交換頁面, 斷開映射, 並設置其狀態為不活躍.
  2. 不活躍臟頁寫回交換設備, 使其成為不活躍干凈頁面繼續緩沖, 或者回收.

2.9 頁面的換入

當尋址過程中發現地址映射存在, 但是對應的P位為0, 即不在內存中.
需要由交換區換入內存.

2.10 內核緩沖區的管理

早在Solaris 2.4(Unix變種)就提出了slab的緩沖區分配和管理辦法.
在slab管理方法中, 每種重要數據都有自己專用的緩沖區隊列, 每種數據都有相應的constructordestructor函數. 同時將結構稱為對象, 每個對象的緩沖區隊列並非由各個對象直接構成, 而是由一大連串的slab構成. 對象分為大對象和小對象.小對象是指小於頁面大小.

  • slab可能由\(1,2,4,8,16,32\)個連續的物理頁面構成.

  • 每個slab的前端是slab描述結構slab_t, 用同一種對象的多個slab通過描述結構的隊列頭形成一條雙向隊列. 每個slab的雙向隊列在邏輯上分為三段:

    • 各個已經分配使用的對象
    • 部分已經分配使用的對象
    • 空閑狀態的對象
  • 每個slab都有一個對象區, 這個是對象數據結構的數組, 以對象的序號為下標就可以得到具體的對象的起始地址.

  • 每個slab上還有個對象鏈接數組, 用來實現一個空閑對象鏈.

  • 每個slab上都有一個字段指向了slab上的第一個空閑對象.

  • 在slab的描述結構中還有一個已經分配使用的對象的計數器, 當一個空閑的對象分配使用時, 就將slab的控制結構中的計數器加1.

  • 當時釋放一個對象時, 只需要調整鏈接數組中的相應元素以及slab描述結構中的計數器,並且更具該slab的使用情況而調整其在slab隊列中的位置.

  • 每個slab的頭部都一部分區域不使用, 稱之為着色區.
    着色區的大小使slab中的每個對象的起始地址都按高速緩存中的緩沖行大小對齊.(80386的一級高速緩存中緩存行大小為16B)

  • 每個slab上最后一個對象之后也有一個小小的廢料區是不用的, 這是對着色區大小的補償, 其大小取決與着色區的大小以及slab與其對象相對的大小.

  • 每個對象的大小基本是所需數據結構的大小. 只有當數據結構的大小不與告訴緩存中的緩存行對齊才增加若干字節使其對齊. 所以, 一個slab上的所有對象的起始地址一定是對其的.

    /* mm/slab.c 138 */
    /*
     * slab_t
     *
     * Manages the objs in a slab. Placed either at the beginning of mem allocated
     * for a slab, or allocated from an general cache.
     * Slabs are chained into one ordered list: fully used, partial, then fully
     * free slabs.
     */
    typedef struct slab_s {
    	struct list_head	list;
    	unsigned long		colouroff;
    	void			    *s_mem;		/* including colour offset */
    	unsigned int		inuse;		/* num of objs active in slab */
    	kmem_bufctl_t		free;
    } slab_t;
    

解析:

  1. list 用來將一塊slab鏈入一個專用緩沖區隊列
  2. colouroff 為本slab上着色區大小
  3. s_mem指向對象區的起點
  4. inuse是已分配對象的計數器
  5. free指向空閑對象鏈中的第一個對象.

Cache形成層次式樹形結構:

  • 總根cache_cache是一個kmem_cache_t 結構, 用來維持第一層slab隊列, slab對象都是kmem_cache_t.
  • 每個第一層上的kmem_cache_t都是隊列頭, 維護第二次隊列.
  • 第二層為某種對象, 數據結構專用, slab上都維護一個空閑隊列.

分配一個數據的緩存時, 只需指明隊列不需要說明大小.

void *kmem_cache_alloc(kmem_cache_t *cachep, int flags);
void *kmem_cache_free(kmem_cache_t *cachep, void *objp);

對於非專用的緩沖區隊列, 由通用的緩沖區分配機制. 類似物理頁面中分配按大小分區, 又采用slab方式管理的通用緩沖池,
稱為slab cache. 其頂層時一個結構數組(靜態). slab對象大小從32,64到128K.分配釋放函數為:

void *kmalloc(size_t size, int flags);
void free(const void *objp);

分配策略: 對於專用的數據結構使用kmem_cache_alloc 分配, 對於非專用使用kmalloc, 對於數據結構很大接近一個頁面的使用alloc_pages分配.

內存還有一組用於分配的函數:
vmalloc()vfree()

void *vmalloc(unsigned long size);
void vfree(void *addr);

vmalloc() 從內核的虛存空間分配虛存以及相應的物理地址, 類似於系統調用brk(), 而brk()時用戶空間使用的, vmalloc()則在系統空間. vmalloc 不會被kswapd換出, 因為kswapd只能掃描用戶進程空間.

2.10.1 專用緩沖區的建立

/* net/core/skbuff.c 473 */
void __init skb_init(void)
{
	int i;

	skbuff_head_cache = kmem_cache_create("skbuff_head_cache",
					      sizeof(struct sk_buff),
					      0,
					      SLAB_HWCACHE_ALIGN,
					      skb_headerinit, NULL);
	if (!skbuff_head_cache)
		panic("cannot create skbuff cache");

	for (i=0; i<NR_CPUS; i++)
		skb_queue_head_init(&skb_head_pool[i].list);
}

可以看到skb_init建立了一個sk_buff數據結構的專用緩沖區隊列, 名為skbuff_head_cache, 每個緩沖區大小為sizeof(struct sk_buff), slab中位移為offset=0, flags為SLAB_HWCACHE_ALIGN 表示要與高速緩存中的緩沖行邊界(16B或32B)對齊. 對象構造函數為skb_headerinit(), destructor為NULL.

2.10.2 緩沖區的分配和釋放

在建立了一種緩沖區的專用隊列后, 可以用kmem_cache_alloc()進行分配:

  1. 無空閑slab對象 需要重新分配 kmem_chache_grow()
  2. 有空閑slab對象 kmem_alloc_one_tail()
  3. kmem_cache_reap()被定時調用來釋放完全空閑的slab.

專用緩沖區的釋放是由kmem_cache_free 完成的.

  1. 操作的主體是__kmem_cache_free(), 需要關中斷.
  2. 根據待釋放對象的地址可以算出其所在頁面的, 進一步, 頁面的page結構中鏈頭list內,原本用於隊列鏈接的指針prev指向了找到對象所在的
    slab, 所以通過GET_PAGE_SLAB可以得到slab的指針.

注意: 緩沖區的釋放不等同slab的釋放, slab的釋放是由kswapd等內核線程調用kmem_cache_reap()完成的.

通用緩沖區隊列的分配的關鍵是通用緩沖區的cache_size, 里面根據緩沖區的大小而分成若干隊列.
kmalloc():

/* mm/slab.c  1511*/
/**
 * kmalloc - allocate memory
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * kmalloc is the normal method of allocating memory
 * in the kernel.  The @flags argument may be one of:
 *
 * %GFP_BUFFER - XXX
 *
 * %GFP_ATOMIC - allocation will not sleep.  Use inside interrupt handlers.
 *
 * %GFP_USER - allocate memory on behalf of user.  May sleep.
 *
 * %GFP_KERNEL - allocate normal kernel ram.  May sleep.
 *
 * %GFP_NFS - has a slightly lower probability of sleeping than %GFP_KERNEL.
 * Don't use unless you're in the NFS code.
 *
 * %GFP_KSWAPD - Don't use unless you're modifying kswapd.
 */
void * kmalloc (size_t size, int flags)
{
	cache_sizes_t *csizep = cache_sizes;

	for (; csizep->cs_size; csizep++) {
		if (size > csizep->cs_size)
			continue;
		return __kmem_cache_alloc(flags & GFP_DMA ?
			 csizep->cs_dmacachep : csizep->cs_cachep, flags);
	}
	BUG(); // too big size
	return NULL;
}

釋放是由kmem_cache_reap()的調用, 其掃描cache_sizes,從clock_searchp開始, 每次保持回收80%.調用kmem_slab_destroy.

2.11 外部設備存儲空間的地址映射

對外部設備的訪問有兩種形式:

  • 內存映射式

    設備的存儲單元映射到內核, 直接訪問內存單元.

  • I/O映射式

    內核通過IN/OUT這種專門的外設I/O指令.(外部設備的存儲單元和內存分屬兩個不同的體系)

2.11.1 內存映射

Linux 的虛存映射是通過ioremap()建立的, 通常內存映射是由缺頁異常引起的被動建立, 但是ioremap()則是
先准備一個物理存儲區間, 地址為外設在總線的存儲器地址.(注意這里並非一定是存儲單元在外設卡上的物理地址,這里隱含了一層地址映射).

┌─────────────────────────────────┐
│                                 │ 
│                                 │ 
│       0x0000f00000000000        │  ---> PCI: 0
├─────────────────────────────────┤
│                                 │
│                                 │
│                                 │
│                                 │
│                                 │
│                                 │
│                                 │
│                                 │
└─────────────────────────────────┘

比如這里PCI總線上的圖形卡上的存儲器是從地址0開始, 裝載到總線上的物理地址可能是從0x0000f00000000000開始.
但是這里僅僅是總線地址, 而非虛擬地址, 所以需要建立映射, 這個函數原名是vremap()后稱為irremap()(這里是從物理地址到虛擬內存的反向映射)

__ioremap()實現:

/* arch/i386/mm/ioremap.c 92*/

/*
 * Remap an arbitrary physical address space into the kernel virtual
 * address space. Needed when the kernel wants to access high addresses
 * directly.
 *
 * NOTE! We need to allow non-page-aligned mappings too: we will obviously
 * have to convert them into an offset in a page-aligned mapping, but the
 * caller shouldn't need to know that small detail.
 */
void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
{
	void * addr;
	struct vm_struct * area;
	unsigned long offset, last_addr;

	/* Don't allow wraparound or zero size */
	last_addr = phys_addr + size - 1;
	if (!size || last_addr < phys_addr)
		return NULL;

	/*
	 * Don't remap the low PCI/ISA area, it's always mapped..
	 */
	if (phys_addr >= 0xA0000 && last_addr < 0x100000)
		return phys_to_virt(phys_addr);

	/*
	 * Don't allow anybody to remap normal RAM that we're using..
	 */
	if (phys_addr < virt_to_phys(high_memory)) {
		char *t_addr, *t_end;
		struct page *page;

		t_addr = __va(phys_addr);
		t_end = t_addr + (size - 1);
	   
		for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++)
			if(!PageReserved(page))
				return NULL;
	}

	/*
	 * Mappings have to be page-aligned
	 */
	offset = phys_addr & ~PAGE_MASK;
	phys_addr &= PAGE_MASK;
	size = PAGE_ALIGN(last_addr) - phys_addr;

	/*
	 * Ok, go for it..
	 */
	area = get_vm_area(size, VM_IOREMAP);
	if (!area)
		return NULL;
	addr = area->addr;
	if (remap_area_pages(VMALLOC_VMADDR(addr), phys_addr, size, flags)) {
		vfree(addr);
		return NULL;
	}
	return (void *) (offset + (char *)addr);
}

解析:

  1. 首先進行sanity check, 判斷地址不越出, 大小不為0, 沒有覆蓋VGA,BIOS的映射區, 不低於物理內存直接映射的地址上限(high_memory是全局變量.) 還需要保證物理地址是按頁面4K對其.
  2. 通過get_vm_area來找到一片虛存地址空間來存放映射, 這段空間屬於內核不屬於任何特定進程.
    • 通過內核的虛存隊列vmlist獲取.
    • 內核委會一個內核專用的mm_structinit_mm.

2.12 系統調用brk()

用戶通過brk()向內核申請空間. (實際上用戶使用庫函數malloc()來調用系統調用brk())

┌───────────────┐
│               │ 
│    Kernel     │ 
│               │             
├───────────────┤
│    User       │
│    Heap       │
│    Stack      │
├───────────────┤
│               │
│  Free Space   │
├───────────────┤
│     .bss      │
├───────────────┤
│     .data     │
├───────────────┤
│     .text     │
└───────────────┘

從data段往上開始分配, 並記錄邊界, 內核記錄在進程的mm_struct->brk里, 進程由庫函數管理.
brk()sys_brk()實現:

/* mm/mmap.c 113 */

/*
 *  sys_brk() for the most part doesn't need the global kernel
 *  lock, except when an application is doing something nasty
 *  like trying to un-brk an area that has already been mapped
 *  to a regular file.  in this case, the unmapping will need
 *  to invoke file system routines that need the global lock.
 */
asmlinkage unsigned long sys_brk(unsigned long brk)
{
	unsigned long rlim, retval;
	unsigned long newbrk, oldbrk;
	struct mm_struct *mm = current->mm;

	down(&mm->mmap_sem);

	if (brk < mm->end_code)
		goto out;
	newbrk = PAGE_ALIGN(brk);
	oldbrk = PAGE_ALIGN(mm->brk);
	if (oldbrk == newbrk)
		goto set_brk;

	/* Always allow shrinking brk. */
	if (brk <= mm->brk) {
		if (!do_munmap(mm, newbrk, oldbrk-newbrk))
			goto set_brk;
		goto out;
	}

	/* Check against rlimit.. */
	rlim = current->rlim[RLIMIT_DATA].rlim_cur;
	if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim)
		goto out;

	/* Check against existing mmap mappings. */
	if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
		goto out;

	/* Check if we have enough memory.. */
	if (!vm_enough_memory((newbrk-oldbrk) >> PAGE_SHIFT))
		goto out;

	/* Ok, looks good - let it rip. */
	if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
		goto out;
set_brk:
	mm->brk = brk;
out:
	retval = mm->brk;
	up(&mm->mmap_sem);
	return retval;
}

解析:

  1. 通過brk參數判斷新邊界和舊邊界之間的關系, 如果新邊界要高於舊邊界則說明是在申請分配空間, 反之釋放.
  2. 釋放使用do_mumap()函數解除物理映射關系.
  3. find_vma_prev()找到結束地址高於addr的第一個區間, 如找到返回vm_area_struct結構指針. 不同的是它還通過參數返回其前一區間結構的指針.
    TODO: 166

2.13 系統調用mmap()

一個進程可以通過mmap()將一個已經打開文件的內容映射到它的用戶空間

mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

參數fd 代表打開的文件, offset為文件的起點, 而start為映射到用戶空間中的起始地址, length是長度, prot為訪問模式(讀寫執行), flags為控制目的.

在2.4.0版本的內核中實現這個調用的函數為sys_mmap2(), 但是在老一些版本中使用的是old_mmap(), 他們的系統調用號不同, 所以還共存.

/* arch/i386/kernel/sys_i386.c 68 */
asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
   unsigned long prot, unsigned long flags,
   unsigned long fd, unsigned long pgoff)
{
   return do_mmap2(addr, len, prot, flags, fd, pgoff);
}

/* arch/i386/kernel/sys_i386.c 91 */

asmlinkage int old_mmap(struct mmap_arg_struct *arg)
{
   struct mmap_arg_struct a;
   int err = -EFAULT;

   if (copy_from_user(&a, arg, sizeof(a)))
   	goto out;

   err = -EINVAL;
   if (a.offset & ~PAGE_MASK)
   	goto out;

   err = do_mmap2(a.addr, a.len, a.prot, a.flags, a.fd, a.offset >> PAGE_SHIFT);
out:
   return err;
}

二者的區別在與傳參, 主體實現都是do_mmap2(),


/* arch/i386/kernel/sys_i386.c 42 */
/* common code for old and new mmaps */
static inline long do_mmap2(
  unsigned long addr, unsigned long len,
  unsigned long prot, unsigned long flags,
  unsigned long fd, unsigned long pgoff)
{
  int error = -EBADF;
  struct file * file = NULL;

  flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
  if (!(flags & MAP_ANONYMOUS)) {
  	file = fget(fd);
  	if (!file)
  		goto out;
  }

  down(&current->mm->mmap_sem);
  error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);
  up(&current->mm->mmap_sem);

  if (file)
  	fput(file);
out:
  return error;
}

sys_execve(), 在load_auto_binary中可以看出通過do_mmap()將可執行程序映射到當前進程的用戶空間.
此外do_mmap()還會創建進程間通信的共享內存區.

TODO: 185-192

中斷, 異常和系統調用

TODO: 194-266

進程與進程調度


免責聲明!

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



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