Linux進程地址空間的使用及說明
進程地址空間
進程地址空間在內(nèi)核中使用 vm_area_struct 數(shù)據(jù)結(jié)構(gòu)來描述,簡稱 VMA,表示進程地址空間或進程線性區(qū)。
由于這些地址空間屬于各個用戶進程,因此在用戶進程的 mm_struct 數(shù)據(jù)結(jié)構(gòu)中有相應(yīng)的成員,用于對這些 VMA 進行管理。
內(nèi)存區(qū)域
進程地址空間(process address space)是指進程可尋址的虛擬地址空間,進程可以通過內(nèi)核的內(nèi)存管理機制動態(tài)地添加和刪除內(nèi)存區(qū)域,這些內(nèi)存區(qū)域在 Linux 內(nèi)核采用 VMA 數(shù)據(jù)結(jié)構(gòu)來抽象描述。
每個內(nèi)存區(qū)域具有相關(guān)的權(quán)限,如可讀、可寫或者可執(zhí)行權(quán)限。若一個進程訪問了不在有效范圍的內(nèi)存區(qū)域,或者非法訪問了內(nèi)存區(qū)域,或者以不正確的方式訪問了內(nèi)存區(qū)域,那么處理器會報告缺頁異常。
在 Linux 內(nèi)核的缺頁異常處理中會處理這些情況,嚴(yán)重的會報告“SegmentFault'”并終止該進程。
內(nèi)存區(qū)域主要包含內(nèi)容如下:
- 代碼段映射:可執(zhí)行文件中包含只讀并可執(zhí)行的程序頭,如代碼段和 init 段等
- 數(shù)據(jù)段映射:可執(zhí)行文件中包含可讀/可寫的程序頭,如數(shù)據(jù)段和未初始化數(shù)據(jù)段等
- 用戶進程棧:通常位于用戶空間的最高地址,從上往下延伸。它包含棧幀,里面包含了局部變量和函數(shù)調(diào)用參數(shù)等
- mmap 映射區(qū)域:位于用戶進程棧下面,主要用于 mmap 系統(tǒng)調(diào)用
- 堆映射區(qū)域:malloc() 函數(shù)分配的進程虛擬地址就是這段區(qū)域
每個進程都有一套頁表,這樣每個進程地址空間就是相互隔離的。即使兩個進程地址空間的虛擬地址是相同的,但是經(jīng)過兩套不同頁表的轉(zhuǎn)換之后,它們也會對應(yīng)不同的物理地址。
mm_struct 數(shù)據(jù)結(jié)構(gòu)
Linux 內(nèi)核需要管理每個進程所有的內(nèi)存區(qū)域以及它們對應(yīng)的頁表映射,所以必須抽象出一個數(shù)據(jù)結(jié)構(gòu),這就是 mm_struct 數(shù)據(jù)結(jié)構(gòu)。
進程控制塊(Process Control Block,PCB)數(shù)據(jù)結(jié)構(gòu) task_struct 中有一個指針 mm,該指針指向這個 mm_struct 數(shù)據(jù)結(jié)構(gòu)。mm_struct 數(shù)據(jù)結(jié)構(gòu)定義在 include/linux/mm_types.h 文件中,下面是它的主要成員。


mm_struct 數(shù)據(jù)結(jié)構(gòu)中主要成員的含義如下:
mmap:進程里所有的 VMA 形成一個單鏈表,這是該鏈表的頭mmrb:VMA 紅黑樹的根節(jié)點get_unmapped_area:用于判斷虛擬內(nèi)存空間是否有足夠的空間,返回一段沒有映射過的空間的起始地址,這個函數(shù)會使用具體的處理器架構(gòu)的實現(xiàn)mmap_base:指向 mmap 空間的起始地址。在 32 位處理器中,mmap 空間的起始地是 0x4000 0000pgd:指向進程的 PGD(一級頁表)mm_users:記錄正在使用該進程地址空間的進程數(shù)目,如果兩個線程共享該地址空間那么 mm_users 的值等于 2mm_count:mm_struct 結(jié)構(gòu)體的主引用計數(shù)mmap_sem:保護 VMA 的一個讀寫信號量mmlist:所有的 mm_struct 數(shù)據(jù)結(jié)構(gòu)都連接到一個雙向鏈表中,該鏈表的頭是 init_mm 內(nèi)存描述符,它是 init 進程的地址空間start_code,end_code:代碼段的起始地址和結(jié)束地址start_data,end_data:數(shù)據(jù)段的起始地址和結(jié)束地址start_brk:堆空間的起始地址brk:表示當(dāng)前堆中的 VMA 的結(jié)束地址total_vm:已經(jīng)使用的進程地址空間總和
從進程的角度來觀察內(nèi)存管理,可以沿著 mm_struct 數(shù)據(jù)結(jié)構(gòu)進行延伸和思考,如下圖所示。

VMA 數(shù)據(jù)結(jié)構(gòu)
VMA(vm_area_struct)數(shù)據(jù)結(jié)構(gòu)定義在 mm_types.h 文件中,其主要成員如下。

VMA 數(shù)據(jù)結(jié)構(gòu)中各個成員的含義如下:
vm_start和vm_end:指定 VMA 在進程地址空間的起始地址和結(jié)束地址vm_next和vm_prev:進程的 VMA 都連接成一個鏈表vmrb:VMA 作為一個節(jié)點加入紅黑樹,每個進程的 mm_struct 數(shù)據(jù)結(jié)構(gòu)中都有一棵紅黑樹 mm->mm_rbvm_mm:指向該 VMA 所屬進程的 mm_struct 數(shù)據(jù)結(jié)構(gòu)vm_page_prot:VMA 的訪問權(quán)限vm_flags:描述該 VMA 的一組標(biāo)志位anon_vma_chain和anon_vma:用于管理反向映射(Reverse Mapping,RMAP)vm_ops:指向許多方法的集合,這些方法用于在 VMA 中執(zhí)行各種操作,通常用于文件映射vm_pgoff:指定文件映射的偏移量,這個變量的單位不是字節(jié),而是頁面的大小件映射。(PAGE SIZE)。對于匿名頁面來說,它的值可以是 0 或者 vm_addr/PAGE_SIZEvm_file:指向 file 的實例,描述一個被映射的文件
mm_struct 數(shù)據(jù)結(jié)構(gòu)是描述進程內(nèi)存管理的核心數(shù)據(jù)結(jié)構(gòu),該數(shù)據(jù)結(jié)構(gòu)提供了管理 VMA 所需要的信息,每個 VMA 都要連接到 mm_struct 中的鏈表和紅黑樹,以方便查找。
VMA 按照起始地址以遞增的方式插入 mm_struct->mmp 鏈表中。
當(dāng)進程擁有大量的 VMA 時,掃描鏈表和查找特定的 VMA 是非常低效的操作,如在云計算的機器中,所以內(nèi)核中通常需要紅黑樹來協(xié)助,以便提高查找速度。
站在進程的角度來看,我們可以從進程控制塊。task_struct 數(shù)據(jù)結(jié)構(gòu)里順藤摸瓜找到該進程所有的 VMA,如圖所示。

- task_struct 結(jié)結(jié)構(gòu)中有一個 mm 成員指向進程的內(nèi)存管理描述符 mm_struct 數(shù)據(jù)結(jié)構(gòu)
- 可以通過 mm_struct 數(shù)據(jù)結(jié)構(gòu)中的 mmap 成員來遍歷所有的 VMA
- 也可以通過 mm_struct 數(shù)據(jù)結(jié)構(gòu)中的 mm_rb 成員來遍歷和查找 VMA
- mm_struct 數(shù)據(jù)結(jié)構(gòu)的 pgd 成員指向進程的頁表,每個進程都有一份獨立的頁表
- 當(dāng) CPU 第一次訪問虛擬地址空間時會觸發(fā)缺頁異常。在缺頁異常處理中,分配物理頁面,利用分配的物理頁面來創(chuàng)建頁表項并且填充頁表,完成虛擬地址到物理地址的映射關(guān)系的建立
VMA 的屬性
作為一個進程地址空間的區(qū)間,VMA 是有屬性的,如可讀/可寫、共享等屬性。
vm_flags 成員描述這些屬性,描述了該 VMA 的全部頁面信息,包括如何映射頁面、訪問每個頁面的權(quán)限等信息,VMA 屬性的標(biāo)志位如下所示。
VM_READ: 可讀屬性VM_WRITE: 可寫屬性VM_EXEC: 可執(zhí)行VM_SHARED: 允許被多個進程共享VM_MAYREAD: 允許設(shè)置 VM_READ 屬性VM_MAYWRITE: 允許設(shè)置 VM WRITE 屬性VM_MAYEXEC: 允許設(shè)置 VM EXEC 屬性VM_MAYSHARE: 允許設(shè)置 VM SHARED 屬性VM_GROWSDOWN: 該 VMA 允許向低地址增長VM_UFFD_MISSING: 表示該 VMA 適用于用戶態(tài)的缺頁異常處理VM_PFNMAP: 表示使用純正的 PFN,不需要使用內(nèi)核的 page 數(shù)據(jù)結(jié)構(gòu)來管理物理頁面VM_DENYWRITE: 表示不允許寫入VM_UFFD_WP: 用于頁面的寫保護跟蹤VM_LOCKED: 表示該 VMA 的內(nèi)存會立刻分配物理內(nèi)存,并且頁面被鎖定,不會被交換到交換分區(qū)VM_IO: 表示 I/0 內(nèi)存映射VM_SEQ_READ: 表示應(yīng)用程序會順序讀該 VMA 的內(nèi)容VM_RAND_READ: 表示應(yīng)用程序會隨機讀該 VMA 的內(nèi)容VM_DONTCOPY: 表示在創(chuàng)建分支時不要復(fù)制該 VMAVM_DONTEXPAND: 通過 mremapo 系統(tǒng)調(diào)用禁止 VMA 擴展VM_ACCOUNT: 在創(chuàng)建 IPC 以共享 VMA 時,檢測是否有足夠的空閑內(nèi)存用于映射VM_HUGETLB: 用于巨頁的映射VM_SYNC: 表示同步的缺頁異常VM_ARCH_1: 與架構(gòu)相關(guān)的標(biāo)志位VM_WIPEONFORK: 表示不會從父進程相應(yīng)的 VMA 中復(fù)制頁表到子進程的 VMA 中VM_DONTDUMP: 表示該 VMA 不包含到核心轉(zhuǎn)儲文件中VM_SOFTDIRTY: 軟件模擬實現(xiàn)的臟位。用于一些特殊的架構(gòu),需要打開 CONFIG_MEM_SOFT_DIRTYVM_MIXEDMAP: 表示混合使用了純 PFN 以及 page 數(shù)據(jù)結(jié)構(gòu)的頁面,如使用 vm_insert_page() 函數(shù)插入 VMAVM_HUGEPAGE: 表示在 madvise 系統(tǒng)調(diào)用中使用 MADV_HUGEPAGE 標(biāo)志位來標(biāo)記該 VMAVM_NOHUGEPAGE: 表示在 madvise 系統(tǒng)調(diào)用中使用 MADV_NOHUGEPAGE 標(biāo)志位來標(biāo)記該 VMAVM_MERGEABLE: 表示該 VMA 是可以合并的,用于 KSM 機制VM_SPECIAL: 表示該 VMA 是不可以合并的
VMA 屬性的標(biāo)志位可以任意組合,但是最終要落實到硬件機制上,即頁表項的屬性中。VMA 屬性到頁表屬性的轉(zhuǎn)換如下圖所示。vm_area_struct 數(shù)據(jù)結(jié)構(gòu)中有兩個成員和屬性相關(guān):一個是 vm_flags 成員,用于描述 VMA 的屬性;另外一個是 vm_page_prot 成員,用于將 VMA 屬性標(biāo)志位轉(zhuǎn)換成與處理器相關(guān)的頁表項的屬性,它和具體架構(gòu)相關(guān)。
在創(chuàng)建一個新的 VMA 時使用 vm_get_page_prot() 函數(shù)可以把 vm_flags 標(biāo)志位轉(zhuǎn)化成具頁表項的硬件標(biāo)志位。

這個轉(zhuǎn)化過程得益于內(nèi)核預(yù)先定義了一個內(nèi)存屬性數(shù)組 protection_map[], 我們只需要根據(jù) vm_flag 標(biāo)志位來查詢這個數(shù)組即可,在這個場景下,通過查詢 protection_map[] 數(shù)組可以獲得頁表屬性。
protection_map[] 數(shù)組的每個成員代表一個屬性的組合,如__P000 表示無效的 PTE 屬性,__P001 表示只讀屬性,__P1O0 表示可執(zhí)行屬性(PAGE_EXECONLY)等。

下面以只讀屬性(PAGE_READONLY)來看,它究竟包含哪些頁表項的標(biāo)志位。

把上述的宏全部展開,我們可以得到如下頁表項的標(biāo)志位。
PTE_TYPE_PAGE:表示這是一個基于頁面的頁表項,即設(shè)置頁表項的 Bit[1:O]PTE_AF:設(shè)置訪問位PTE_SHARED:設(shè)置內(nèi)存共享屬性MT_NORMAL:設(shè)置內(nèi)存屬性為 normalPTE_USER:設(shè)置 AP 訪問位,允許通過用戶權(quán)限訪問該內(nèi)存PTE_NG:設(shè)置該內(nèi)存對應(yīng)的 TLB 只屬于該進程PTE_PXN:表示該內(nèi)存不能在特權(quán)模式下執(zhí)行PTE_UXN:表示該內(nèi)存不能在用戶模式下執(zhí)行PTE_RDONLY:表示只讀屬性
內(nèi)核如何管理內(nèi)存

Linux 進程在內(nèi)核中作為進程描述符 task_struct 的實例實現(xiàn)。task_struct 中的 mm 字段指向內(nèi)存描述符 mm_struct ,它是程序內(nèi)存的執(zhí)行內(nèi)容。它存儲了如上所示的內(nèi)存段的開始和結(jié)束、進程使用的物理內(nèi)存頁數(shù)(rss 代表 Resident Set Size)、使用的虛擬地址空間 以及其他信息。
每個虛擬內(nèi)存區(qū)域(VMA)是一個連續(xù)的虛擬地址范圍;這些區(qū)域永遠(yuǎn)不會重疊。vm_area_struct 的實例完整地描述了一個內(nèi)存區(qū)域,包括其起始和結(jié)束地址、用于確定訪問權(quán)限和行為的標(biāo)志,以及用于指定該區(qū)域映射的文件(如果有)的 vm_file 字段。不映射文件的 VMA 是匿名的。除了內(nèi)存映射段之外,上面的每個內(nèi)存段(例如,堆、堆棧)對應(yīng)于單個 VMA。這不是必需的,盡管這在 x86 機器中很常見。VMA 不關(guān)心它們位于哪個段。 程序的 VMA 都以鏈表形式存儲在其內(nèi)存描述符中 mmap 字段,按起始虛擬地址排序,并作為以 mm_rb 字段為根的紅黑樹 。紅黑樹允許內(nèi)核快速搜索覆蓋給定虛擬地址的內(nèi)存區(qū)域。當(dāng)您讀取文件/proc/pid_of_process/maps 時,內(nèi)核只是遍歷進程的 VMA 鏈接列表并打印每一個 VMA。
VMA 的大小必須是頁面大小的倍數(shù)。處理器查閱頁表以將虛擬地址轉(zhuǎn)換為物理內(nèi)存地址。每個進程都有自己的一組頁表;每當(dāng)發(fā)生進程切換時,用戶空間的頁表也會切換。Linux 在內(nèi)存描述符的 pgd 字段中存儲指向進程頁表的指針。頁表中的每個虛擬頁都對應(yīng)一個頁表項 (PTE),在常規(guī) x86 分頁中,它是一個簡單的 4 字節(jié)記錄,如下所示:

malloc函數(shù)
malloc() 函數(shù)是 C 標(biāo)準(zhǔn)庫封裝的一個核心函數(shù),C 標(biāo)準(zhǔn)庫做一些處理后會調(diào)用 Linux 的系線調(diào)用接口 brk 向系統(tǒng)申請內(nèi)存。
brk 系統(tǒng)調(diào)用
brk 系統(tǒng)調(diào)用主要實現(xiàn)在 mm/mmap.c 文件中。

詳細(xì)流程這里不一一列出來了,下面用一張圖概括 brk 的流程,如下:
malloc流程
假設(shè)不考慮 libc 的因素,malloc() 分配 100 字節(jié),那么內(nèi)核會分配多少字節(jié)呢?處理器的 MMU 的最小處理單元是頁面,所以內(nèi)核分配內(nèi)存、建立虛擬地址和物理地址映射關(guān)系都以頁面為單位,PAGE_ALIGN(addr)宏讓地址按頁面大小對齊。
下圖所示為 malloc() 函數(shù)的實現(xiàn)流程。

mmap函數(shù)
mmap/munmap 函數(shù)是用戶空間中常用的系統(tǒng)調(diào)用函數(shù),無論是在用戶程序中分配內(nèi)存、讀寫大文件、鏈接動態(tài)庫文件,還是多進程間共享內(nèi)存,都可以看到 mmp/munmap() 函數(shù)的身影。mmp/munmap 函數(shù)的聲明如下。



mmap/munmap 函數(shù)的參數(shù)如下。
addr:用于指定映射到進程地址空間的起始地址,為了提高應(yīng)用程序的可移植性,一般設(shè)置為 NULL,讓內(nèi)核來分配一個合適的地址length:表示映射到進程地址空間的大小prot:用于設(shè)置內(nèi)存映射區(qū)域的讀寫屬性等flags:用于設(shè)置內(nèi)存映射的屬性,如共享映射、私有映射等fd:表示這是一個文件映射,fd 是打開的文件的句柄offset:在文件映射時,表示文件的偏移量。prot 參數(shù)通常表示映射頁面的讀寫權(quán)限,有如下參數(shù)組合PROT_EXEC:表示映射的頁面是可以執(zhí)行的PROT_READ:表示映射的頁面是可以讀取的PROT_WRITE:表示映射的頁面是可以寫入的PROT_NONE:表示映射的頁面是不可訪問的
flags 參數(shù)是一個很重要的參數(shù),可以設(shè)置為以下值。
MAP_SHARED:創(chuàng)建一個共享映射的區(qū)域。多個進程可以通過共享映射方式來映射一個文件,這樣其他進程也可以看到映射內(nèi)容的改變,修改后的內(nèi)容會同步到磁盤文件中MAP_PRIVATE:創(chuàng)建一個私有的寫時復(fù)制的映射。多個進程可以通過私有映射的方式來映射一個文件,這樣其他進程不會看到映射內(nèi)容的改變,修改后的內(nèi)容也不會同步到磁盤文件中MAP_ANONYMOUS:創(chuàng)建一個匿名映射,即沒有關(guān)聯(lián)到文件的映射MAP_FIXED:使用參數(shù) addr 創(chuàng)建映射,如果在內(nèi)核中無法映射指定的地址,那么 mmap 會返回失敗,參數(shù) addr 要求按頁對齊。如果 addr 和 length 指定的進程地址空間和已有的 VMA 重疊,那么內(nèi)核會調(diào)用 do_munmapO 函數(shù)把這段重疊區(qū)域銷毀,然后重新映射新的內(nèi)容MAP_POPULATE:對于文件映射來說,會提前預(yù)讀文件內(nèi)容到映射區(qū)域,該特性只支持私用映射
通過參數(shù) fd 可以看出 mmap 映射是否和文件相關(guān)聯(lián),因此在 Linux 內(nèi)核中,映射可以分成匿名映射和文件映射。
- 匿名映射:沒有映射對應(yīng)的相關(guān)文件,匿名映射的內(nèi)存區(qū)域的內(nèi)容會初始化為 0
- 文件映射:映射和實際文件相關(guān)聯(lián),通常把文件內(nèi)容映射到進程地址空間,這樣應(yīng)用程序就可以像操作進程地址空間一樣讀寫文件
私有匿名映射
當(dāng)使用參數(shù) fd=-1 且 flags = MAP_ANONYMOUS|MAP_PRIVATE 時,創(chuàng)建的 mmap 映射是私有匿名映射。
私有匿名映射常見的用途是在 glbc 分配大內(nèi)存塊時,如果需要分配的內(nèi)存大 MMAP_THREASHOLD(128KB),glibc 會默認(rèn)使用 mmap 代替 brk 來分配內(nèi)存。
共享匿名映射
當(dāng)使用參數(shù) fd=-1 且 flags = MAP_ANONYMOUS | MAP_SHARED 時,創(chuàng)建的 mmap 映射是共享匿名映射。
共享匿名映射讓相關(guān)進程共享一塊內(nèi)存區(qū)域,通常用于父、子進程之間的通信創(chuàng)建共享匿名映射有如下兩種方式。
- 使 fd=-1 且 flags = MAP_ANONYMOUS | MAP_SHARED。在這種情況下,do_mmap_pgoffO->mmap_region() 函數(shù)最終會調(diào)用 shmem_zero_setup():來打開一個特殊的“/dev/zero”設(shè)備文件
- 直接打開“/dev/zero”設(shè)備文件,然后使用這個文件句柄來創(chuàng)建 mmap
上述兩種方式最終都調(diào)用 shmem 模塊來創(chuàng)建共享匿名映射。
私有文件映射
創(chuàng)建文件射時如果 flags 設(shè)置為 MAPP_PRIVATE,就會創(chuàng)建私有文件映射。
私有文件映射常用的場景是加載動態(tài)共享庫。
共享文件映射
創(chuàng)建文件映射時,如果 flags 設(shè)置為 MAP_SHARED,就會創(chuàng)建共享文件映射。
如果 prot 參數(shù)指定了 PROT_WRITE,那么打開文件時需要指定 O_RDWR 標(biāo)志位。共享文件映射通常有 mmap 如下兩個常用的場景。
- 讀寫文件。把文件內(nèi)容映射到進程地址空間,同時對映射的內(nèi)容做了修改,內(nèi)核的回寫(writeback)機制最終會把修改的內(nèi)容同步到磁盤中
- 進程間通信。進程之間的進程地址空間相互隔離,一個進程不能訪問另外一個進程的地址空間。如果多個進程同時映射到一個文件,就實現(xiàn)了多進程間的共享內(nèi)存通信。如果一個進程對映射內(nèi)容做了修改,那么另外的進程是可以看到的
總結(jié)
mmap 機制在 Linux 內(nèi)核中實現(xiàn)的代碼框架和 brk 機制非常類似,其中有很多關(guān)于 VMA 的操作。
mmap 機制和缺頁中斷機制結(jié)合在一起會變得復(fù)雜很多。
mmap 機制在 Linux 內(nèi)核中的實現(xiàn)流程如圖所示。

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
linux中高并發(fā)socket最大連接數(shù)的優(yōu)化詳解
這篇文章主要給大家介紹了關(guān)于linux中高并發(fā)socket最大連接數(shù)優(yōu)化的相關(guān)資料,文中介紹的很詳細(xì),相信對大家具有一定的參考價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧。2017-02-02
詳解如何備份及恢復(fù) Linux 文件權(quán)限
這篇文章主要介紹了詳解如何備份及恢復(fù) Linux 文件權(quán)限,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03

