Linux進程虛擬地址空間


 

轉載請注明出處,並保留以上所有對文章內容、圖片、表格的來源的描述。

一、ASLR的問題

ASLR(Address Space Layout Randomization),可以通過/proc/sys/kernel/randomize_va_space修改。但是較新的內核版本該值默認為2(在3.2.0如此),老版本為1(在2.6.18如此)。至少可以知道為0的時候是關閉,為1和為2有什么差別還不知道。

可以在Documentation/sysctl/kernel.txt中找到如下一段話:

==============================================================

randomize_va_space:

This option can be used to select the type of process address

space randomization that is used in the system, for architectures

that support this feature.

0 - Turn the process address space randomization off.  This is the

    default for architectures that do not support this feature anyways,

    and kernels that are booted with the "norandmaps" parameter.

1 - Make the addresses of mmap base, stack and VDSO page randomized.

    This, among other things, implies that shared libraries will be

    loaded to random addresses.  Also for PIE-linked binaries, the

    location of code start is randomized.  This is the default if the

    CONFIG_COMPAT_BRK option is enabled.

2 - Additionally enable heap randomization.  This is the default if

    CONFIG_COMPAT_BRK is disabled.

    There are a few legacy applications out there (such as some ancient

    versions of libc.so.5 from 1996) that assume that brk area starts

    just after the end of the code+bss.  These applications break when

    start of the brk area is randomized.  There are however no known

    non-legacy applications that would be broken this way, so for most

    systems it is safe to choose full randomization.

    Systems with ancient and/or broken binaries should be configured

    with CONFIG_COMPAT_BRK enabled, which excludes the heap from process

    address space randomization.

==============================================================

這段話中有幾個名詞需要解釋:

  • VDSO page randomized:Virtual Dynamically linked Shared Objects。是一種在用戶態調用內核態的方法。參考:http://en.wikipedia.org/wiki/VDSO
  • PIE-linked binaries:PIE(Position-Independent-Executable),是一種介於共享庫和普通可執行程序之間的一種可執行文件。參考資料:http://www.linuxfromscratch.org/~manuel/hlfs-book/glibc-2.4/chapter02/pie.html
  • CONFIG_COMPAT_BRK:內核中brk相關的變量很多指的都是堆(heap),這個配置選項 “CONFIG_COMPAT_BRK=y means that heap randomization is turned off, so it's *always* a safe choice.  I assume the help text is trying to say that if one does not run ancient binaries, then enabling heap randomization is safe.”所以該配置=y指的是關閉堆地址空間隨機化技術來支持一些老的binary(COMPAT選項一般都是向后兼容的選項)。

所以,在/proc/sys/kernel/randomize_va_space中的值如果為0則表示關閉所有的隨機化,如果為1,表示打開mmap base、棧、VDSO頁面隨機化,如果為2則表示在1的基礎上進一步打開堆地址隨機化。在打開堆地址隨機化之前,堆的起始位置是緊接着應用程序bss段之后的。

二、匿名頁

There are two type of pages: anonymous pages and file-backed pages. A file-backed page originates from mmap()-ing a file in disk, whereas an anonymous page is the kind you get when doing malloc(). It has no relationship with any files at all. When the RAM becomes tight, the kernel swaps out anonymous pages to swap space and flushes file-backed pages to the file to give room for current requests. In other words, anonymous pages may consume swap area while file-backed pages don't. The only exception is for files mmap()-ed using the MAP_PRIVATE flag. In this case, file modification occurs in RAM only.

From: http://linuxdevcenter.com/pub/a/linux/2006/11/30/linux-out-of-memory.html

Linux進程虛擬地址空間

 

        Linux進程虛擬地址空間的是Linux內存管理的另外一個重要的部分。之前說過Linux對物理內存的管理,對於用戶進程的內存訪問,Linux提供了一套另外一套更加復雜的模式,這種模式通過頁表來訪問物理內存,而這種訪問模式目前也被大部分CPU體系結構所支持。

一、內存虛擬空間的概述
內存虛擬空間的布局

        我們知道,在IA-32體系結構中,任何一個進程都能夠訪問4GB的內存空間;在這4GB內存空間中,高1GB是內核的空間,這一部分的管理已經在之前講過了。而低3GB的內存空間(我們成為用戶虛擬內存空間)中,也需要通過一定的布局來進行管理。用戶虛擬內存空間至少需要分為三個部分:堆空間、棧空間以及MMAP空間。棧空間和堆空間大家都已經很熟了,MMAP空間主要是將文件映射到進程的虛擬內存空間時,對應虛擬內存空間的位置。注意這里指的“文件”是Linux中寬泛概念的文件,包括塊設備(硬盤等)上的文件、設備文件、虛擬文件系統的文件等等。

        在IA-32架構中,Linux傳統的的布局空間如下:

2867048

        一般來說,IA-32體系結構中進程地址空間的的代碼段(.text)從0x08048000,這與最低可用地址有128MB的間距,用戶捕獲NULL指針。其他體系結構也有類似的缺口,UltraSparc使用0x10000000作為代碼段起點,AMD64則使用0x0000000000400000。在代碼段之上是數據段和bss段。再之上是堆空間,並向高地址增長。MMAP空間用於內存映射,起始於mm_struct->mmap_base,通常設置為TASK_UNMAPPED_BASE,每個體系結構有自己不同的定義,但是幾乎所有情況下其值都是TASK_SIZE/3。棧空間是從用戶虛擬地址空間的最高點(一般0xBFFFFFFF向下)向下增長。

        這里面存在一個問題,TASK_UNMAPPED_BASE在IA-32中的值只有為0x40000000,也就是說堆空間只有1GB可以使用,為了能夠擴展堆空間的大小,在內核版本2.6.7開發期間為IA-32計算機引入一個新的虛擬地址空間:

3459696

        我們可以看到唯一的差別是MMAP區域的增長方向。新的布局導致了棧空間的固定,而堆空間和MMAP區域公用一段空間,這在很大程度上增長了堆空間的大小。

進程虛擬空間的數據結構

        在Linux內核中,每一個進程都有一個自己的數據結構(可能是內核中最大的數據結構)struct task_struct,該結構中有一個struct mm_struct數據結構,該結構則保存了進程的內存管理信息。該結構的摘要如下:

<mm_types.h>

 

struct mm_struct {
...

unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff,unsigned long flags);
...
    unsigned long mmap_base; /* base of mmap area */
    unsigned long task_size; /* size of task vm space */
...
    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;
...
}

 

        該結構中get_unmapped_area函數用於在虛擬空間中獲得未被映射的空間,mmap_base是上文中MMAP區域的基地址,task_size是進程地址空間的大小,start_code和end_code是進程代碼段的起止地址,start_data和end_data是進程數據段的起止地址,start_brk和堆空間的起始地址,start_stack是棧空間的起始地址,brk表示堆區域當前的結束地址(為什么棧空間沒有當前的結束地址呢?想想esp寄存器...),arg_start和arg_end表示進程參數列表,env_start和env_end表示環境變量,這兩個區域都位於棧中最高的區域。

地址空間布局隨機化

        Linux Kernel引入了地址空間布局隨機化的概念,該概念的提出是出於安全考慮。試想如果堆棧空間的地址都是確定的,那么惡意代碼就很容易通過內存溢出的代碼來訪問堆棧空間的內容,地址空間布局隨機化就是使得進程虛擬空間的布局(主要是各個部分的起始地址)位於隨機的位置,以此來降低被攻擊的可能性。

        地址空間布局隨機化(ASLR,Address Space Layout Randomization)可以通過/proc/sys/kernel/randomize_va_space修改。randomize_va_space的可能值有三種,可以在Documentation/sysctl/kernel.txt中找到如下一段話:

==============================================================

randomize_va_space:

This option can be used to select the type of process address

space randomization that is used in the system, for architectures

that support this feature.

0 - Turn the process address space randomization off. This is the

    default for architectures that do not support this feature anyways,

    and kernels that are booted with the "norandmaps" parameter.

1 - Make the addresses of mmap base, stack and VDSO page randomized.

    This, among other things, implies that shared libraries will be

    loaded to random addresses. Also for PIE-linked binaries, the

    location of code start is randomized. This is the default if the

    CONFIG_COMPAT_BRK option is enabled.

2 - Additionally enable heap randomization. This is the default if

    CONFIG_COMPAT_BRK is disabled.  

    There are a few legacy applications out there (such as some ancient

    versions of libc.so.5 from 1996) that assume that brk area starts

    just after the end of the code+bss. These applications break when

    start of the brk area is randomized. There are however no known  

    non-legacy applications that would be broken this way, so for most

    systems it is safe to choose full randomization.

    Systems with ancient and/or broken binaries should be configured

    with CONFIG_COMPAT_BRK enabled, which excludes the heap from process

    address space randomization.

==============================================================

        具體我就不翻譯了,只是解釋其中幾個不容易理解的名詞。

  • VDSO page randomized:Virtual Dynamically linked Shared Objects。是一種在用戶態調用內核態的方法。參考:http://en.wikipedia.org/wiki/VDSO
  • PIE-linked binaries:PIE(Position-Independent-Executable),是一種介於共享庫和普通可執行程序之間的一種可執行文件。參考資料:http://www.linuxfromscratch.org/~manuel/hlfs-book/glibc-2.4/chapter02/pie.html
  • CONFIG_COMPAT_BRK:內核中brk相關的變量很多指的都是堆(heap),這個配置選項 “CONFIG_COMPAT_BRK=y means that heap randomization is turned off, so it's *always* a safe choice. I assume the help text is trying to say that if one does not run ancient binaries, then enabling heap randomization is safe.”所以該配置=y指的是關閉堆地址空間隨機化技術來支持一些老的binary(帶有COMPAT的配置選項一般都是向后兼容的選項)。

        所以,在/proc/sys/kernel/randomize_va_space中的值如果為0則表示關閉所有的隨機化,如果為1,表示打開mmap base、棧、VDSO頁面隨機化,如果為2則表示在1的基礎上進一步打開堆地址隨機化。在打開堆地址隨機化之前,堆的起始位置是緊接着應用程序bss段之后的。

二、內存映射的原理

        內存映射的原理很簡單,本質上就是將需要的數據映射到進程的虛擬地址空間中,這里面的數據可以是硬盤上的文件,也可以是內核中的數據,甚至堆棧都是使用內存映射來實現的。如圖:

4753444

        當然圖示是很簡化的,因為文件數據在硬盤上的存儲通常不是連續的,而是分布到若干的區域。內核利用address_space數據結構,提供一組方法從后備存儲器(比如硬盤)讀取數據,因此address_space行程一個輔助層,將映射的數據表示為連續的線性區域,提供給內存管理子系統。

        在Linux內存管理中有兩個很重要的概念:按需調頁(demand paging)和按需分配(demand allocation)。對於在后備存儲器上的數據,Linux並非將所有需要的數據都在執行前載入內存中,而是按照需要來載入,這種機制成為按需調頁;對於堆棧空間對物理內存的使用,也是根據程序運行時的需求來進行分配,這是按需分配。使用的各種數據結構如圖:

5041921

        過程如下:

  • 進程試圖訪問用戶虛擬地址空間的某個地址,但是該地址沒有和物理內存關聯。
  • 處理器觸發缺頁中斷,發送到內核
  • 內核處理該中斷,找到適當的后備存儲器
  • 分配物理內存頁,並從后備存儲器讀取所需數據填充物理內存
  • 更新用戶進程頁表,建立物理地址與用戶虛擬地址的聯系,恢復進程的執行
三、數據結構
基本數據結構

        我們知道struct mm_struct很重要,該結構提供了進程在內存布局中的所有信息。另外它還包括下列成員,用於管理用戶進程在虛擬地址空間中的所有內存區域。

<mm_types.h>

 

struct mm_struct {
    struct vm_area_struct * mmap; /* list of VMAs */
    struct rb_root mm_rb;
    struct vm_area_struct * mmap_cache; /* last find_vma result */
...
}

 

        對於進程的內存區域,每個區域都通過一個vm_area_struct實例來描述,所有的vm_area_struct通過兩個數據結構來管理:mmap用一個單鏈表來管理,mm_rb通過一個紅黑樹來管理。結構如圖:

6415572

        注意這里的紅黑樹只是一個簡單的表示,真實的結構遠比此復雜。

        每個區域表示一個vm_area_struct實例,定義的簡化形式如下:

<mm_types.h>

 

struct vm_area_struct {
    struct mm_struct * vm_mm; /* The address space we belong to. */
    unsigned long vm_start; /* Our start address within vm_mm. */
    unsigned long vm_end; /* The first byte after our end address
                            within vm_mm. */
    /* linked list of VM areas per task, sorted by address */
    struct vm_area_struct *vm_next;
    pgprot_t vm_page_prot; /* Access permissions of this VMA. */
    unsigned long vm_flags; /* Flags, listed below. */
    struct rb_node vm_rb;
    /*
    * For areas with an address space and backing store,
    * linkage into the address_space->i_mmap prio tree, or
    * linkage to the list of like vmas hanging off its node, or
    * linkage of vma in the address_space->i_mmap_nonlinear list.
    */
    union {
        struct {
            struct list_head list;
            void *parent; /* aligns with prio_tree_node parent */
            struct vm_area_struct *head;
        } vm_set;
        struct raw_prio_tree_node prio_tree_node;
    } shared;
    /*
    * A file’s MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
    * list, after a COW of one of the file pages. A MAP_SHARED vma
    * can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
    * or brk vma (with NULL file) can only be in an anon_vma list.
    */
    struct list_head anon_vma_node; /* Serialized by anon_vma->lock */
    struct anon_vma *anon_vma; /* Serialized by page_table_lock */
    /* Function pointers to deal with this struct. */
    struct vm_operations_struct * vm_ops;
    /* Information about our backing store: */
    unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
                                units, *not* PAGE_CACHE_SIZE */
    struct file * vm_file; /* File we map to (can be NULL). */
    void * vm_private_data; /* was vm_pte (shared mem) */
};

 

        其中:

  • vm_mm是一個反向指針,指向該區域所屬的mm_struct實例
  • vm_start和vm_end指向了該區域在用戶空間中的起始和結束地址
  • vm_next是一個單鏈表,根據地址遞增序來組織
  • vm_rb是紅黑樹的一個節點,用來與紅黑樹的集成
  • vm_page_prot存儲區域的訪問權限
  • shared聯合體反映了“共享映射”,共享映射是指文件與進程的虛擬地址空間雙向的映射,即通過進程的虛擬地址空間能夠找到在文件對應的位置,也能夠通過給定一個文件的區間,內核能夠知道該區間映射到的所有進程。后者叫做反向映射。為了實現反向映射,使用了該聯合體來實現了一個優先搜索樹(Priority Search Tree)。優先搜索樹將在后文詳細說明。
  • anon_vma_node和anon_vma用於管理匿名映射(anonymous mapping)。指向相同的頁的映射都通過anon_vma_node組織在一個雙鏈表上,內核中有若干此類鏈表,anon_vma成員是一個指向與各個鏈表關聯的管理結構的指針,該管理結構包含一個表頭和相關的鎖。有關匿名映射的內容馬上就會涉及到。
  • vm_ops是一個指向struct vm_operation_struct的指針,該結構中的方法用於在區域上執行各種標准操作,包括open、close、fault、nopage等。創建和刪除區域時會使用open/close;fault是在處理缺頁中斷時調用;nopage是被內核拋棄的缺頁中斷處理方法,新的代碼中不應該使用。
  • vm_pgoffset指定了文件映射的偏移量。該值只用於映射了一部分文件內容的情況,如果映射了整個文件則該值為0。
  • vm_file指向file實例。后文在介紹優先查找樹時會具體介紹,此外file實例是VFS文件系統中的重要的數據結構,想要細致的了解請參考文件系統相關內容。
  • vm_private_data可用於指定存儲的私有數據,不由通用的內存管理例程操作。只有少數聲音和視頻驅動程序使用了該選項。
  • vm_flags存儲了定義區域性質的標志,可以從<mm.h>中生命的VM_xxx預處理常數定義。
匿名頁和文件頁

        There are two type of pages: anonymous pages and file-backed pages. A file-backed page originates from mmap()-ing a file in disk, whereas an anonymous page is the kind you get when doing malloc(). It has no relationship with any files at all. When the RAM becomes tight, the kernel swaps out anonymous pages to swap space and flushes file-backed pages to the file to give room for current requests. In other words, anonymous pages may consume swap area while file-backed pages don't. The only exception is for files mmap()-ed using the MAP_PRIVATE flag. In this case, file modification occurs in RAM only.

From: http://linuxdevcenter.com/pub/a/linux/2006/11/30/linux-out-of-memory.html

        我是實在懶得翻譯了,只是需要說明匿名頁在進程虛擬地址空間中的堆、棧的實現有着重要用處,而由文件映射來的內存都是通過文件頁來管理的。

四、優先搜索樹(Priority Search Tree, PST)

        我們已經知道,如果進程虛擬空間的某一個內存區域(vm_area_struct)已經和后備存儲器中的某個文件的某個區域建立了映射,我們很容易通過通過頁表的機制來獲得該虛擬內存域與物理內存域的關聯信息,進而找到對應的文件區域。對於動態鏈接庫在系統中的實現,我們需要將同一個動態鏈接庫映射到調用該庫的不同的進程地址空間中,這就需要追蹤文件的某個區域都被哪些進程的地址空間映射。這個映射過程叫做“反向映射”,Linux內核中使用的方式是優先搜索樹的數據結構。

附加的數據結構

        在這里需要簡單介紹幾個附加的數據結構:

<fs.h>

 

struct address_space {
    struct inode *host; /* owner: inode, block_device */
...
    struct prio_tree_root i_mmap; /* tree of private and shared mappings */
    struct list_head i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
...
}

 

<fs.h>

 

struct file {
...
    struct address_space *f_mapping;
...
}

<fs.h>

 

struct inode {
...
    struct address_space *i_mapping;
...
}

 

        下面簡單介紹上述幾個數據結構的用途。對於進程來說,如果打開一個文件,則需要維護一個struct file的實例,該結構包含了一個指向struct address_space對象的指針。對於每個文件和塊設備,在kernel中都會表示成一個struct inode實例,這個實例對應於ext文件系統中的inode。struct file是通過open系統調用在VFS層的文件的抽象,而inode表示文件系統自身中的對象。上述三個數據結構中,struct address_space是優先搜索樹(prio tree)的關鍵結構。

        對於這些結構的關聯,我們可以通過下圖來表示:

8312934

        可以看到不同的集成打開同一文件時會生成不同的struct file實例,該實例會通過struct address_space將file->i_mapping賦值為inode->i_mapping,這使得多個進程可以同時訪問一個文件而不互相影響。struct address_space結構體中有兩個重要的變量,i_mmap結構對應的是private and shared mapping,就是那個優先搜索樹,我們可以看到從inode可以找到其對應的address_space,從該address_space的這棵樹中能夠找到該inode所有偏移對應的struct vm_area_struct,前文我們已經知道,vm_area_struct是進程虛擬內存管理的基本結構,而其中有指向其所屬進程的mm_struct的指針。所以通過這個路徑就能夠知道一個文件中某區域在所有進程中的映射情況,實現了反向映射。

優先搜索樹

        優先搜索樹在很多情況下又稱作Radix Priority Search Tree,因為其結構很像基數樹的結構。對於該優先搜索樹的操作可以在mm/prio_tree.c中找到,在此不講述具體的代碼,只是簡單的介紹一下樹的結構。

        首先我們需要明確不同進程對同一個文件不同/相同區域映射的模型,如圖:

8963489

        首先將vm_area_struct中與優先搜索樹相關的結構再列一遍:

<mm_types.h>

 

struct vm_area_struct {

...
    struct vm_area_struct *vm_next;

...
    struct rb_node vm_rb;

...
    /*
    * For areas with an address space and backing store,
    * linkage into the address_space->i_mmap prio tree, or
    * linkage to the list of like vmas hanging off its node, or
    * linkage of vma in the address_space->i_mmap_nonlinear list.
    */
    union {
        struct {
            struct list_head list;
            void *parent; /* aligns with prio_tree_node parent */
            struct vm_area_struct *head;
        } vm_set;
        struct raw_prio_tree_node prio_tree_node;
    } shared;
...
};

 

        可以看到進程對文件的映射通過兩個值可以確定區域:映射區域的開始和結束。我們將映射區域的開始稱作radix_index,結束稱作heap_index,二者的差即映射區域的大小稱作size_index。所以一個區域可以表示成(radix_index, size_index, heap_index),其實去掉size_index也可以,只是kernel的文檔中這樣表示,為了和之后的圖對應,在這里就這樣表示了。

        優先搜索樹滿足如下性質:

  • 當前節點的heap_index大於或者等於其子節點(左右兩個子節點)的heap_index
  • 如果當前節點的heap_index和某個子節點的heap_index相同,則其radix_index小於該子節點的radix_index
  • 對於heap_index和radix_index都相同的節點,則其映射到同樣的區域中。這些節點被struct vm_area_struct中shared聯合體中的vm_set組織在一起。

        我們可以看到,優先搜索樹的根是address_space中的struct prio_tree_root i_mmap,對於每個在該搜索樹中的struct vm_area_struct,都通過該區域數據結構中shared聯合體中的struct raw_prio_tree_node prio_tree_node來集成到優先搜索樹中。對於映射區域完全重疊的vm_area_struct,則通過shared聯合體中的vm_set結構的list進行連接。另外shared聯合體中的vm_set結構體還被用來映射在address_space中的非線性映射區(address_space->i_mmap_nonlinear中表示)。對於非線性映射的概念后面會講到,這里只需要提到非線性映射的vm_area_struct不會同時出現在優先搜索樹中,所以使用同一個聯合體的結構不會產生沖突。

        下面給出一個優先搜索樹的實例,由於排版原因,完全使用了屏幕截圖的方式:

9986794

        可以看到0~4層是優先搜索樹的結構,而4~8層是根據prio_tree_root->index_bits值進行優化的結果,上半部分叫做Regular radix priority search tree,是heap-and-radix indexed的,即用heap_index值和radix_index值來進行建樹;下半部分叫做Overflow-sub-trees,是根據heap_index和size_index來進行建樹。

        上面這張圖和之前的優先搜索樹的定義均來自kernel的文檔,可以在kernel源碼的Documentation/prio_tree.txt中找到更為詳細的信息,包括Overflow-sub-trees的說明等。在此就不再說明了。

五、對區域的操作

        內核提供了各種函數來對vm_area_struct進行操作,一些典型的操作如下圖所示:

10346641

        圖的上半部分表示三種類型的增加區域的操作,對於增加的區域來說,能夠和原有的區域合並為同一個區域,而三種增加區域的操作得到的新的區域又能夠表示成同一個區域。所以在進行操作之后,系統將進行優化,只保存一個區域。圖的下半部分表示兩種刪除操作,第一種刪除之后剩下一個區域,第二種刪除操作事實上相當於新增了一個區域。

        所以內核提供了如下的函數來對區域進行操作:

  • struct vm_area_struct *find_vma(struct mm_struct * mm, unsigned long addr):查找用戶地址空間中結束地址在給定地址之后的第一個區域,即滿足addr < vm_area_struct->vm_end條件的第一個區域。該函數有助於將虛擬地址關聯到區域。
  • 函數vma_merge提供將一個新區域與周邊區域合並的功能。
  • 函數insert_vm_struct是內核用於插入新區域的標准函數。
  • 函數get_unmapped_area是內核在插入新的內存區域之前,確認虛擬地址空間中有足夠的空閑空間的函數,相當於是創建區域的函數。

        對區域的操作在此就不詳細說明了,詳細內容可以參考代碼中的函數注釋說明。

六、內存映射

        我們以上所描述的若干機制,最后都是要為進行進程地址空間內存映射服務的。我們知道,C標准庫提供了mmap函數建立映射(關於mmap的用法請google或者man mmap),在內核一端,提供了兩個系統調用mmap和mmap2,某些體系結構實現了兩個版本,例如IA-64和Sparc(64),其他的只實現了第一個(AMD64)或第二個(IA-32)。具體的函數聲明就不在這里給出,請自行查看源代碼,這里指給出執行過程。

創建映射

        創建映射就是通過mmap或者mmap2系統調用實現的,下面只討論sys_mmap2,在系統調用mmap2的處理過程中,將所有的工作委托給do_mmap2.內核在其中提供文件描述符找到file實例,以及所處理文件的所有特征數據。剩余的工作委托給do_mmap_pgoff。該函數是一個重要的函數,與體系結構無關,定義在mm/mmap.c中,下圖給出了相關代碼的流程圖:

11227906

        do_mmap_pgoff曾經是內核中最長的函數,現在被分成了兩個部分。get_unmapped_area上文中已經說明,即創建一個內存區。之后需要計算該映射的flags。之后將所有的工作交給函數mmap_region。mmap_region調用find_vma_prepare函數,來查找前一個和后一個區域的vm_area_struct實例,以及紅黑樹中節點對應的數據。如果在指定的映射位置已經存在一個映射,則通過do_munmap刪除它。之后內核檢查內存空閑是否滿足要求,之后創建新的vm_area_struct實例,並用特定於文件的函數file->f_op->mmap創建映射。如果設置了VM_LOCKED,或者通過系統調用的標志參數顯示的傳遞進來,或者通過mlockall機制隱形設置,內核都會調用make_pages_present一次掃描映射中各頁,對每一頁出發缺頁中斷以便讀入其數據。之后返回映射的起始地址。

刪除映射

        刪除映射的操作很簡單,使用munmap系統調用,它需要兩個參數:接觸映射區域的起始地址和長度。按照慣例將工作交給do_munmap處理,流程如下:

1084877

        find_vma_prev找到接觸映射區域的vm_area_struct實例,如果解除映射區域的起始地址與找到的區域的起始地址不同,則需要將找到的區域的后端分裂出來,調用split_vma函數。如果解除映射的部分區域的末端與原區域不重合,那么原區域后部仍然有一部分未接觸映射,因此需要對這部分重復上述處理過程。

        內核接下來調用detach_vmas_to_be_unmapped,列出所有需要解除映射的區域,之后調用unmap_region從頁表中刪除與映射相關的所有項,此外內核還必須確保將相關的項從TLB移除或使之失效。最后用remove_vma_list釋放vm_area_struct實例占用的空間。

非線性映射

        在上文中,我們提到了struct address_space中的i_mmap_nonlinear,該部分對應的是非線性映射。相對於非線性映射,普通的映射將文件中一個連續的部分映射到虛擬內存中一個同樣連續的部分,但如果需要將文件的不同部分以不同順序映射到虛擬內存的連續區域中,通常必須使用幾個映射。從消耗的資源來看,代價比較昂貴(特別是需要分配的vm_area_struct數量)。實現同樣效果的一個更簡單的方法是使用非線性映射。該特性在內核2.5版本中引入。

mm/fremap.c

 

long sys_remap_file_pages(unsigned long start, unsigned long size,
                            unsigned long __prot, unsigned long pgoff, unsigned long flags)

 

        該系統調用允許重拍映射中的頁,是的內存與文件中的順序不再等價。實現該特性無需移動內存中的數據,只通過操作進程的頁表就可以實現。該函數可以將現存映射(位置pgoff,長度size)移動到虛擬內存中的一個新位置。start標志了移動的目標映射,因而必須落入某個現存映射的地址范圍。它還指定了由pgoff和size標識的頁移動的目標位置。

        對於所有的非線性映射區域,維護在struct address_space的i_mmap_nonlinear為表頭的鏈表中,鏈表中的各個vm_area_struct實例采用shared.vm_set.list作為鏈表元素。原因如同上文所說,在標准的優先搜索樹中不存在非線性映射區域。

        所屬區域對應的頁表項用一些特殊的項來填充,使得這些頁表項看起來像是對應於不存在的頁,但其中包含附加信息,將其標識為非線性映射的頁表項。在訪問此類頁表項描述的頁時,會產生一個缺頁中斷,從而讀入正確的頁。

反向映射

        內核利用此前討論的結構,已經可以建立虛擬和物理地址之間的聯系(通過頁表),以及進程的一個內存區域預期虛擬內存頁地址之間的關聯。仍然確實的一個聯系是,物理內存頁和所有使用該頁的進程的對應頁表項之間的聯系。在物理頁面換出時,正好需要此關聯,以便更新所有涉及的進程的頁表項。

        為此,內存使用了一些附加的數據結構和函數,實現了一種反向映射的機制。

        反向映射使用了簡介的數據結構,即在struct page結構中包含了一個用於實現反向映射的成員:

<mm.h>

 

struct page {
....
atomic_t _mapcount; /* Count of ptes mapped in mms,
                         * to show when page is mapped
                         * & limit reverse map searches.
                         */
...
};

 

        _mapcount表明共享該頁的位置的數目。計數器初值為-1,在頁插入到你想映射數據結構時,計數器賦值為0.頁每次增加一個使用者時計數器加1。此外,在struct vm_area_struct數據結構中(參考上文),我們維護了優先搜索樹,該樹中嵌入了所有非匿名映射的區域以及指向內存中同一頁的匿名區域的鏈表。這樣,內核可以根據物理頁面找到該頁面對應的vm_area_struct,從而找到包含該頁的所有使用者。該方法又名基於對象的反向映射(object-based reverse mapping),因為沒有存儲頁和使用者之間的直接關聯,而是在兩者之間插入一個對象(該頁所在的區域struct vm_area_struct)。

        在建立反向映射時,需要對匿名頁和基於文件映射的頁分別處理,這是因為管理這兩種選項的數據結構不同。對匿名頁簡歷反向映射的函數是page_add_anon_rmap,對基於文件映射的頁的函數是page_add_file_rmap。

        反向映射在頁交換中非常有用,此外內核定義的try_to_unmap函數也依賴該技術,並且也大量涉及到了頁交換的細節。所以在此就不討論了。

堆空間的管理

        對是進程用於動態分配空間的內存區域,最重要的函數是malloc來分配任意長度的內存區。malloc和內核之間的經典接口是brk系統調用,負責擴展/收縮堆。brk系統調用只需要一個參數,用於指定堆在虛擬地址空間中新的結束位置。調用流程如下:

2791700

        brk機制是基於匿名映射實現的,對於堆的擴展與收縮實際上就是匿名映射的處理,在此就不詳細說明了。

七、缺頁中斷

        缺頁中斷也叫缺頁異常,主要差別是看發生在用戶空間還是內核空間。我們在這里不區分兩者的差別,統一都叫缺頁中斷。

        下面給出了缺頁中斷的一般處理流程:

3046418

        缺頁中斷分為內核態和用戶態兩方面,處於哪一態的中斷主要取決於發生缺頁中斷的虛擬地址落在哪一部分。內核態的缺頁中斷只需要檢查當前是否在執行內核態代碼。內核態中斷不需要其它的檢查,因為內核是相信自己的,發生內核中斷時內核一定能夠在對應的位置找到缺頁並且填充該頁面,而不會產生異常狀況。對於用戶態缺頁中斷,首先需要檢查映射是否存在,其次要檢查權限,最后才能夠處理缺頁中斷。其中任何一個環節出錯都會導致Segmentation Fault。到這里大家知道了Seg Fault的原因了吧?大部分都是因為訪問地址出錯引起的。

        缺頁中斷是灰常灰常復雜的一個機制,而且非常依賴體系結構。在此我們只討論IA-32體系結構上的方法。arch/x86/kernel/entry_32.S中的一個匯編例程是缺頁中斷的入口,但是其立刻調用了arch/x86/mm/fault_32.c中的C函數do_page_fault。代碼流程如下:

3345644

        do_page_fault只需要兩個參數:發生中斷時使用中的寄存器集合,提供錯誤原因的錯誤代碼(long error_code)。目前error_code只使用了前五個比特位,語義如下表:

比特位                            置位(1)                                                    未置位(0)

0                            缺頁                                                            保護異常(沒有足夠的訪問權限)

1                            讀訪問                                                        寫訪問

2                            核心態                                                        用戶態

3                            表示檢測到使用了保留位

4                            表示缺頁異常是在取指令時出現的

        另外需要明確的是,對於缺頁中斷的恢復地址通過read_cr2()函數獲得,即保存在cr2寄存器中。

用戶空間缺頁中斷

        在確定缺頁中斷是在允許的地址觸發之后,內核必須確定將所需數據讀入物理內存的適當方法。該任務交給handle_mm_fault,它不依賴底層體系結構。該函數確認在各級頁目錄中,通向對應於一場地址的頁表項的各個頁目錄都存在。handle_pte_fault函數分析缺頁異常的原因,pte指向相關頁表項(pte_t)的指針。

mm/memory.c

 

static inline int handle_pte_fault(struct mm_struct *mm,
                                    struct vm_area_struct *vma, unsigned long address,
                                    pte_t *pte, pmd_t *pmd, int write_access)

 

        如果頁不在物理內存中,該函數的流程如下:

  • 如果沒有對應的頁表項,則內核必須從頭開始加載該頁。對匿名映射成為按需分配(demand allocation),對基於文件的映射,則稱之為按需調頁(demand paging)。
  • 如果該頁標記為不存在,而頁表中保存了相關信息,則意味着該頁已經換出(swap out),因而需要從系統的某個交換區換入。
  • 非線性映射已經換出的部分不能像普通頁那樣換入,因為必須正確地恢復非線性關聯。pte_file函數用於檢查頁表項是否屬於非線性映射,do_nolinear_fault用戶處理該類已成。

        如果該頁存在於物理內存中,但是該區域對頁授予了寫權限,而硬件的存取機制沒有授予。這種情況下出發的異常,需要調用do_wp_page函數創建該頁的副本並插入到進程的頁表中。該機制成為寫時復制(Copy on write, COW)。fork之后會大量觸發此類異常。

按需分配/調頁

        按需分配頁的工作交給do_linear_fault函數(定義在mm/memory.c中),在轉換一些參數后,其余工作交給__do_fault。該函數是缺頁中斷處理的核心之一,代碼流程如圖:

4373020

        總的來說,該部分處理具體的方法依賴於映射到發生異常的地址空間(address_space)中的文件,因此需要調用特定於文件的方法來獲取數據。通常該方法保存在vm->vm_ops->fault。由於較早的版本約定使用nopage,則如果沒有注冊fault方法,使用舊的vm->vm_ops->nopage。

        對於給定涉及的區域vm_area_struct,內核選擇何種方法讀取頁面?

  1. 使用vm_area_struct->vm_file找到映射的file對象
  2. 在file->f_mapping中找到指向映像自身的指針。
  3. 每個地址空間都有特定的地址空間操作,從中選擇readpage方法。使用mapping->a_ops->readpage(file, page)從文件中將數據傳輸到物理內存。

        如果需要寫訪問,內核必須區分共享和私有映射。對私有映射,必須准備頁的一份副本。

        對於匿名頁使用匿名頁添加新映射的方法來處理(前文提到過),並且需要將其添加到緩存中;對於基於文件映射的頁需要采用page_add_file_rmap來處理。

        此外,在需要使用新的物理頁面時,內核傾向使用用戶高端內存中分配頁面,如下:

 

    page=alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);

 

        最后必須更新處理器的MMU緩存,因為頁表已經修改。

寫時復制

        寫時復制的處理函數是do_wp_page,流程如下:

4992858

        內核首先調用vm_normal_page,通過頁表項找到struct page實例。在page_cache_get獲取頁之后,接下來anon_vma_prepare准備好反向映射機制的數據結構,以接受一個新的匿名區域。由於缺頁中斷的來源是需要將一個充滿有用數據的頁復制到新頁,因此內核調用alloc_page_vma分配一個新頁。cow_user_page將異常頁的數據復制到新頁。然后使用page_remove_rmap刪除原來的只讀頁的你想映射,最后使用lru_cache_add_active將新分配的頁放到LRU緩存的活動列表上,並通過page_add_anon_rmap將其插入到你想映射的數據結構。

內核缺頁中斷

        在訪問內核地址空間時,缺頁中斷可能被下列條件觸發:

  • 內核設計錯誤導致訪問錯誤地址
  • 內核通過用戶空間傳遞的系統調用參數,訪問了無效地址
  • 訪問使用vmalloc分配的區域,觸發缺頁中斷

        前兩種情況是真正的錯誤,內核必須對此進行額外的檢查。vmalloc的情況是合理的,需要加以矯正。我們在Figure 4-18中看到大量的fixup_exception函數就在用來搜索異常表(exception_table_entry),異常表項struct exception_table_entry實例和fixup_exception函數如下:

<include/asm-x86/uaccess_32.h>

 

struct exception_table_entry
{
    unsigned long insn, fixup;
};

 

arch/x86/mm/extable_32.c

 

int fixup_exception(struct pt_regs *regs)
{
    const struct exception_table_entry *fixup;
    fixup = search_exception_tables(regs->eip);
    if (fixup) {
        regs->eip = fixup->fixup;
        return 1;
    }
    return 0;
}

 

        EIP寄存器在IA-32處理器上包含了出發中斷的代碼段地址。search_exception_tables掃描異常表,查找合適的匹配項。如果在異常表中找到了對應的修正例程,則執行該例程;如果沒有找到,表明出現了一個真正的內核異常,將調用do_page_fault來處理該異常,並且最終導致內核進入oops狀態,並強制使用SIGKILL結束當前進程,做最后的垂死掙扎,不過大部分情況下到這里內核就掛掉了。

八、內核和用戶空間之間的數據傳遞

        內核並不能直接使用用戶空間的數據,用戶空間也不能直接調用內核空間的數據。所以數據的傳遞在內核與用戶空間之間有一套通用的接口,該接口在系統調用過程中和對device文件操作時經常被調用。見下表:

5766608

5788510

        大多數函數都有兩個版本,沒有雙下划線的版本會調用access_user對用戶空間地址進行檢查。

        這些函數主要是使用匯編語言實現的,由於調用非常頻繁,對性能要求極高,因此還必須使用GNU C用於嵌入匯編的復雜構造和代碼中的鏈接指令將異常代碼也集成進來,才能達到較好的性能。在內核2.5開發期間,編譯過程增加了一個檢查工具。該工具分析源代碼,檢查用戶空間的指針是否能夠直接解引用,而不實用上述函數。所以源自用戶空間的指針必須用關鍵字__user標記,以便工具分辨所需檢查的指針。例如:

<fs/open.c>

 

asmlinkage long sys_chroot(const char __user * filename) {
...
}

 

        終於總結完了!!!完全暈菜了有木有!

        總的來說還是總結了《深入Linux內核架構》一書(Professional Linux Kernel Architecture),其中有一些部分來自Kernel源碼自帶的Documentations,還有一些來源於Kernel Git庫的changelog,其他的來自於谷歌。文中所有的圖片和表格都來自於《深入Linux內核架構》。

        這部分的復雜程度堪比Linux對物理內存的管理,尤其是兩個反向映射(inode映射到所有使用的進程、物理頁面映射到所有使用該頁的虛擬地址的頁表!尼瑪說起來這這么長!)加上一個優先搜索樹的結構,啃了好久看了好多資料才慢慢明白。其實可以從用戶空間總結一下整個過程(只包含內存管理部分):

  1. 用戶空間調用某個命令(創建進程)
  2. 准備進程的基本內存,此時命令的代碼段和數據段以及動態鏈接庫都沒有載入到內存中。
  3. 進程執行第一條指令,觸發缺頁中斷
  4. 缺頁中斷進入到用戶態,發生按需調頁
  5. 進程使用malloc分配大內存
  6. 堆空間不夠大,發生缺頁中斷,用戶態按需分配匿名頁面
  7. 進程從硬盤中讀文件
  8. 缺頁中斷,在MMAP區域映射后備存儲器中的文件
  9. 進程使用remap_file_pages系統調用
  10. 內核建立非線性映射
  11. 進程操作某device driver,該驅動使用vmalloc分配內存
  12. 發生內核態缺頁中斷,處理vmalloc缺頁中斷
  13. 該device driver寫的很渣渣,其中錯誤訪問了0號頁面
  14. 發生內核態缺頁中斷,調用fixup_exception,發生錯誤
  15. Device driver崩潰,內核可能也掛了

        以上基本能夠囊括整個進程虛擬地址空間的管理(不包括初始化部分),希望能給大家一個整體的印象。

xelatex@pku

 


免責聲明!

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



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