文章目錄
- 1 linux 使用者态程序虛拟位址空間
-
- 1.1 arm32 使用者程序的虛拟位址空間
- 1.2 arm64架構使用者态虛拟位址空間.
- 2 linux使用者态程序虛拟位址空間管理
-
- 2.1 程序描述符task_struct
- 2.2 程序使用者态虛拟位址空間描述符
- 3 使用者态程序的虛拟位址和實體位址的映射管理
- 4 linux使用者态程序檔案頁的虛拟位址如何對應到磁盤中檔案的具體位置?
1 linux 使用者态程序虛拟位址空間
在linux多任務作業系統中,每個使用者态程序都有自己的虛拟位址空間,使用者态程序虛拟位址空間主要分核心虛拟位址空間和使用者虛拟位址空間:
- 核心虛拟位址空間:核心總是駐留在記憶體中,是作業系統的一部分。核心虛拟位址空間為核心保留,不允許應用程式讀寫該區域的内容或直接調用核心代碼定義的函數
- 使用者虛拟位址空間
NAME DESCRIPTION 棧(stack) 局部變量、函數參數、傳回位址等,由系統配置設定和統一回收 記憶體映射段(mmap) 核心将硬碟檔案的内容直接映射到記憶體,是一種友善高效的檔案I/O方式(mmap或用malloc配置設定大于128K的記憶體塊) 堆(heap) 堆用于存放程序運作時動态配置設定的記憶體段,可動态擴張或縮減。堆中内容是匿名的(malloc配置設定小于128k記憶體塊) BSS段 未初始化資料 data段 已初始化的資料 代碼段(text) 代碼段也稱正文段或文本段,通常用于存放程式執行代碼(即CPU執行的機器指令),通常代碼段是可共享的,是以頻繁執行的程式隻需要在記憶體中擁有一份拷貝即可
使用者态程序位址空間中的虛拟位址需要通過頁表(Page Table)的映射才能擷取到其對應的實體位址,頁表由作業系統維護并被處理器引用。其中程序核心位址空間中的虛拟位址需要通過核心态的頁表才能定位到其映射的實體位址,而程序使用者位址空間中的虛拟位址則需要通過程序自己私有的頁表才能定位到其映射的實體位址.
在Linux中,核心位址空間的頁表是持續存在的,并且所有程序切換到核心模式所處的核心位址空間都共用一套核心頁表,是以核心代碼和資料總是可尋址,随時準備進行中斷和系統調用。與此相反,程序處于使用者模式時所處的使用者位址空間的映射關系随程序切換的發生而不斷變化,是以每個程序的使用者虛拟位址空間對應的頁表是私有的。
新版本arm架構的linux系統用兩個寄存器來儲存一級頁表(pgd頁表)的虛拟位址:ttbr0和ttbr1.
- 對于arm32架構linux會将程序私有pgd頁表的虛拟位址儲存在ttbr0寄存器中,而ttbr1寄存器不會使用,因為ttb0和ttb1兩個寄存器搭配使用的情況下linux隻支援2G,1G和512M等,但是ARM32虛拟位址空間的劃分比例為1:3,使用者空間是3G,核心空間是1G,是以上述寄存器硬體限制無法滿足這種通用配置,是以ARM32未使用TTBR1寄存器。那麼當程序從使用者态切換到核心态或核心态切換到使用者态時,為了避免切換頁表帶來的性能損耗,arm32系統的使用者态程序的使用者虛拟位址空間和核心虛拟位址空間使用了相同的頁表基位址,存儲在ttbr0寄存器中。具體實作方式是: 其中核心空間的pgd頁表内容會在程序被fork時複制到程序的私有pgd頁表中,也就是說arm32架構的linux系統下使用者态程序将linux核心态頁表中的一級頁表(init_mm->swapper_pg_dir)中的内容拷貝到了該程序的私有一級頁表(task_struct->mm_struct->pgd)中,最終mmu隻需通過一個ttbr0寄存器中存儲的pgd頁表就能完成核心态和用态兩個虛拟位址空間中的虛實位址轉換操作。
- 對于arm64架構linux會将核心虛拟位址空間pgd頁表(init_mm->swapper_pg_dir)的虛拟位址放在ttbr1寄存器中,而使用者程序私有pgd頁表(task_struct->mm_struct->pgd)的虛拟位址存放在ttbr0中。當核心需要将擷取該程序的一個虛拟位址vaddr對應的實體位址時,先會對該64位虛拟位址vaddr的最高位進行判斷:
- 若vaddr對應的最高位為1,則可以确定該虛拟位址位于程序的核心位址空間,是以從ttbr1寄存器中擷取到核心态pgd頁表的位址資料,用于mmu完成對vaddr的位址映射操作.
- 若vaddr對應的最高位為0,則可以确定該虛拟位址位于程序的使用者位址空間,是以從ttbr0寄存器中擷取到該程序私有頁表的對應的位址資料,用于mmu完成對vaddr的位址映射操作.
此處擴充下,就是對于x86架構linux os隻用一個CR3寄存器來存儲程序的頁表資訊,當程序被fork時核心虛拟位址空間對應pgd頁表的内容回會被複制到使用者态程序私有的pgd頁表中去(處理方式類似arm32架構的os).
1.1 arm32 使用者程序的虛拟位址空間
在linux多任務作業系統中,每個使用者态程序都有自己的虛拟位址空間。在arm32架構的linux作業系統下使用者态程序的虛拟位址空間是一個4G的記憶體位址塊,通常使用者态程序核心态和用态所占虛拟記憶體比例是1:3,該比例可以通過配置檔案根據實際情況來進行設定.
START END SIZE USE
-----------------------------------------------------------------------------
0x00000000 0xbfffffff 3G USER(使用者虛拟位址空間)
0xC0000000 0xffffffff 1G KERNEL(核心虛拟位址空間)
ps:使用者虛拟位址空間一般從ox0804800開始,前面空白部分為未使用位址空間
1.linux程序虛拟記憶體空間(32位)
1.2 arm64架構使用者态虛拟位址空間.
對于arm64架構的linux,雖然虛拟位址已經達到64位,但是處理器的實體位址總線實際位寬并沒有達到64位,通常為39位或48位,較新的arm架構可以支援到52位.
那麼為什麼arm64架構的的linux os實體位址總線位寬不支援到64位呢?因為實體位址總線寬度過高會給晶片設計帶來較大難度,加之一個48位位址線寬,其尋址能力為256TB(2^48bytes),這對于目前的個人電腦或伺服器都是夠用的。綜上所述,對于一個arm64架構的linux os往往用64位中的低48位虛拟位址來進行尋址(也可以通過核心配置進行調整可選項為38,48或52).
對于一個48位虛拟位址,4級頁表,頁面大小為4K的arm64架構linux os來說,其使用者态程序的虛拟位址空間布局如下:
START END SIZE USE
-----------------------------------------------------------------------------
0x0000000000000000 0x0000ffffffffffff 256T USER(使用者虛拟位址空間)
0X0001000000000000 0Xfffeffffffffffff 非規範區
0xffff000000000000 0xffffffffffffffff 256T KERNEL(核心虛拟位址空間)
其中使用者程序位址空間的使用者虛拟位址空間的内部結構劃分情況和圖1中arm32架構的結構劃分基本一緻,而對于核心虛拟位址空間的結構劃分可參考以前介紹的linux核心記憶體初始化相關的介紹.
2 linux使用者态程序虛拟位址空間管理
2.1 程序描述符task_struct
task_struct是linux核心的一種資料結構,Linux核心通過一個task_struct結構體來管理程序,這個結構體包含了一個程序所需的所有資訊。本文主要介紹程序使用者空間虛拟記憶體管理,在task_struct中與程序虛拟位址空間相關的成員變量如下所示(核心态程序task_struct的mm成員為NULL):
//include/linux/sched.h
struct task_struct {
......
/*
*(1)struct mm_struct被稱為程序描述符抽象并描述了Linux視角下管理程序位址空間的所有資訊
*(2)mm指向程序所擁有的記憶體描述符,而active_mm指向程序運作時所使用的記憶體描述符。
* a.對于普通程序,這兩個指針變量相同
* b.對于核心線程,不擁有任何記憶體描述符,mm成員總是設為NULL;當核心線程運作時,它的active_mm成員被初始化 * 為前一個運作程序的active_mm值
*/
struct mm_struct *mm, *active_mm;
......
};
2.2 程序使用者态虛拟位址空間描述符
程序使用者态虛拟位址主要由如下兩個資料結構來進行描述(隻列出部分成員變量):
//include/linux/mm_types.h
struct mm_struct{
//位址空間中所有VMA的連結清單首部
struct vm_area_struct * mmap;
rb_root_t mm_rb;
//最後一次通過find_vma()找到的VMA存放處
struct vm_area_struct * mmap_cache;
//全局目錄表的起始位址
pgd_t * pgd;
//通路使用者空間部分的使用者計數值
atomic_t mm_users;
//匿名使用者計數值
atomic_t mm_count;
//正在被使用中的vma數量
int map_count;
//讀寫保護鎖,長期有效
struct semaphore mmap_sem;
//用于保護mm_struct中大部分字段
spinlock_t page_table_lock;
//所有的mm_struct結構通過它連結在一起
struct list_head mmlist;
//代碼段和資料段的起始位址和中止位址。
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;
/*
*rss:某一時刻,一般一個程序虛存空間不會完全在記憶體中,一般駐留在記憶體中的為其虛存空間的子集,rss描述有多少頁駐 *留記憶體中)駐留集的大小是該程序常駐記憶體的頁面數,不包括全局零頁面,total_vm:程序中所有vma區域的記憶體空間總和, *locked_vm:記憶體中被鎖住的常駐頁面數
*/
unsigned long rss, total_vm,locked_vm;
//VM_LOCKED用于指定在預設情況下将來所有的映射是上鎖還是未鎖。
unsigned long def_flags;
unsigned long cpu_vm_mask;
unsigned long swap_cnt;
//當換出整個程序時,頁換出程序記錄最後一次被換出的位址
unsigned long swap_address;
mm_context_t context;
}
//include/linux/mm_types.h
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
//指定VMA在程序虛拟位址空間的起始位址和結束位址
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 */
//程序中所有的VMA都連結成一個連結清單
struct vm_area_struct *vm_next, *vm_prev;
//指定的VMA作為一個節點加入到紅黑樹中
struct rb_node vm_rb;
/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here. */
//指向該VMA所屬程序的mm_struct結構體
struct mm_struct *vm_mm; /* The address space we belong to. */
//VMA的通路權限
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
//指向該VMA的一組标志
unsigned long vm_flags; /* Flags, see mm.h. */
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
//實作反向映射(anon_vma_chain,anon_vma)
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store: */
//指定檔案映射的偏移量,機關是頁面大小,對于匿名映射它的值是0或者vm_addr/PAGE_SIZE
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units */
//描述一個被映射的檔案,執行一個file執行個體
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
};
linux系統使用者态一個程序虛拟位址空間由兩個資料結構來描述mm_struct和vm_area_struct。mm_struct對程序整個使用者空間虛拟記憶體進行了描述。同時一個程序的記憶體變化是動态擴充的,比如棧的擴充,堆的擴充,新檔案的映射等,這些都需要一個粒度更小的結構體來表示記憶體的動态增加或減少,是以linux os又引入了vm_area_struct結構體來對程序的一段具有相同屬性的虛拟位址空間進行描述。其中一個程序中的每個vm_area_struct執行個體都要連接配接到該程序mm_struct執行個體的mmap連結清單和mm_rb紅黑樹中,以友善查找。
最終linux程序的整個使用者态虛拟位址空間都是通過mm_struct來進行描述,而虛拟位址空間上的每個段空間都是通過vm_area_struct來表示,如圖2所示:
2.程序使用者态虛拟記憶體管理
下面對task_struct,mm_struct和vm_area_struct這3者關系做一個總結:
linux核心為每一個程序維護一個task_struct結構體,核心用這個結構體來描述一個程序,這個結構包含了程序的一些資訊,包括程序id,可執行檔案名,指向使用者棧的指針以及上下文的task結構體的指針等等。而task_struct中mm成員變量(該成員是一個mm_struct結構體資料)主要用于程序使用者态虛拟位址空間的管理。這個mm_struct結構體中有兩個比較重要的成員,一個是pgd,他指向該程序的頁表集,便于根據虛拟位址查詢頁表獲得需要的實體位址,另一個是mmap,他指向一個vm_area_struct的鍊式結構,鍊式結構中的每個vm_area_struct成員用來描述程序虛拟位址空間中具有相同屬性的一個段,vm_area_struct結構體中包括該段的起止虛拟位址,标志通路權限的權限位,标志是否是共享區的标志位。CPU在操作一塊資料時,核心會先周遊程序mm中的vm_area_struct連結清單(為了便于搜尋,程序所有的vm_area_struct結構構成一個紅黑樹,樹根儲存在mm_struct中的mm_rb字段),若連結清單中任何一個VMA都與資料塊的虛拟位址區間不比對,就說明操作的位址還未配置設定,屬于非法行為,核心會報段錯誤,并結束這個程序。當然如果權限不夠也會報錯。
ps:linux系統可以通過cat /proc/pid/map檢視程序的虛拟位址段
3 使用者态程序的虛拟位址和實體位址的映射管理
我們知道每個使用者态程序都有一個獨立的虛拟位址空間,維護着一個獨立的頁表。是以使用者态程序虛拟位址vaddr必須與程序的唯一辨別符程序pid相結合才有意義。使用者态程序的虛拟位址和實體位址的映射如下所示 :
3.程序使用者态虛拟位址映射示意圖
- 通過程序的pid,擷取到該程序的程序描述符task_struct結構體,進而通過task_struct結構體中的mm成員獲得程序對應的虛拟位址空間描述符mm_struct結構體,最後通過mm_struct結構體成員pgd獲得該程序的頁表集。(ps:vaddr必須在mm_struct結構體的的mmap連結清單中找到對應的虛拟記憶體段vm_area_struct與其對應,否則會報段錯誤。此處預設vaddr在該程序虛拟位址空間範圍内)
- 在步驟1擷取到的程序頁表集的基礎上,将使用者空間的虛拟位址vaddr通過MMU(pgd,pmd,pte)找到對應的頁表項X(linux采用了一種與具體體系結構無關代碼 的三層頁表機制來完成記憶體管理,即使底層的體系結構并不支援這個概念。每一個程序都有一個指向其自己的PGD指針 (mm_struct->pgd),這就是一個實體頁面号,其中包含了一個pgd_t類型的數組,程序頁表的載入是通過把這個結構體複制到ttbr0寄存器完成。PGD表中每個有效的項都指向一個頁面号,此頁面号包含一個pmd_t類型的PMD項數組,每一個pmd_t又指向另外的頁面号,這些頁面号由 很多個pte_t類型的PTE構成,而pte_t最終指向包含真正使用者數組的頁面).
- 在找到頁表項X後,需要對頁表項X的資料進行分析來确定程序虛拟位址vaddr映射的實體頁面相關情況。為了便于分析假設虛拟位址32位,頁大小為4K。則頁表項X為一個4bytes,32位的資料,其中X的12-31位是page base描述,而0-11位是屬性描述。
- 若頁表項X的0位為1,則程序的虛拟位址vaddr對應的實體頁在實體記憶體中。則此時将頁表項X的0-11位資料用虛拟位址的vaddr的0-11位資料替換,得到的32位位址即為虛拟位址vaddr對應的實體位址。(X[12:31]為虛拟位址vaddr所映射的實體頁的頁框号記為pfn,則vaddr對應的實體位址phyaddr = ( pfn<<12 ) & ( vaddr & ( 1 << 13 - 1) ) )
- 若頁表項X的0位為0且X的1-31位都為0,則程序vaddr虛拟位址對應的頁不在實體記憶體位址空間中,需要調頁(缺頁異常,觸發缺頁中斷,以後專題講解)。
- 若頁表項的0位為0且1-31位至少有一位為1,則該頁被交換到swap的磁盤分區。則此時頁表項X的1-7位是表示的是磁盤交換區的區号,而X的8-31位表示磁盤交換分區的頁槽索引(page-slot)類似于實體頁的頁框号(page frame)。由上我們就可以将該虛拟位址與磁盤的swap分區中的某一頁關聯起來。 4.pte頁表項與swap磁盤分區索引圖
4 linux使用者态程序檔案頁的虛拟位址如何對應到磁盤中檔案的具體位置?
在本章第3節中,我們根據某一程序的虛拟位址vaddr定位到了其pte頁表項X,若該頁表項X的0-31位都為0,則記憶體中沒有實體頁與vaddr虛拟位址建立映射關系,此時需要調頁。假設vaddr虛拟位址映射的頁是檔案頁(在程序mm_struct結構體中的mmap連結清單中找到vaddr對應的VMA結構體,若VMA中的vm_ops成員不為空,則vaddr映射的實體頁為檔案頁,為空則vaddr映射的實體頁為匿名頁),對于檔案頁的調頁linux需要進行如下操作,下面為了簡化流程便于了解忽略page cache操作:
- linux核心配置設定一個新的實體頁A
- 将新實體頁A的實體頁框号(pfn)和頁對應的權限資訊更新到頁表項X中并重新整理程序的頁表緩存
- 最後将虛拟位址vaddr檔案頁對應的磁盤檔案内容copy到新的實體頁A中,則檔案頁的調頁過程完成(忽略page cache)。
上面這一過程其實就是缺頁中斷中linux對于檔案頁的一個處理過程。那麼在上述的操作流程中linux核心是如何通過一個檔案頁的虛拟位址,準确地找到對應磁盤檔案中的頁資料的呢?
其實在使用者程序P将檔案映射到記憶體時,剛開始程序P隻給檔案配置設定了虛拟位址空間即配置設定了一個VMA(該VMA在程序頁表集中也配置設定了對應的頁表項,但對應的PTE頁表項内容為空),而并沒有将配置設定的VMA虛拟位址空間映射到實體記憶體中;但是在程序P在給檔案配置設定VMA時,它會将磁盤檔案的位置資訊記錄在VMA的vm_file和vm_pgoff成員中。假設程序P的虛拟位址vaddr在VMA的虛拟位址空間範圍内,當對程序P的vaddr進行讀或寫操作時,會發現該vaddr虛拟位址并未映射實體位址;由此觸發缺頁異常,程序p因缺頁中斷而切換到核心态,進入核心态的程序p會先分偶爾一頁實體記憶體并與vaddr建立映射(填寫對應的pte頁表項包括實體頁的pfn和該頁的通路權限資訊)。接下來需要将vaddr對應的磁盤資料拷貝到新配置設定的實體頁記憶體中,操作步驟如下:
- 在程序P使用者态虛拟位址空間的vma連結清單中找到與vaddr對應的VMA
- 通過VMA中的vm_file,vm_pgoff成員和vaddr相對VMA起始虛拟位址的偏移offset這3個資料并利用VFS相關接口來定位到vaddr對應的檔案段在磁盤中的具體位置。
- 最後通過相關系統調用函數将vaddr對應的檔案段在磁盤中的資料拷貝到剛配置設定的實體頁中。
以上就完成了檔案頁在缺頁異常情況下的調頁流程,需要注意的是上述流程中忽略了page cache操作。最後程序P就能通過虛位址vaddr擷取到對應的檔案資料。上訴流程分析結合圖5更容易了解。
5.linux使用者态程序檔案頁虛拟位址空間-實體位址空間-磁盤檔案空間的映射關系示意圖
ps:記憶體主要通過虛拟檔案系統(VFS)來操作磁盤上的檔案資料的,VMA的vm_file是一個struct file *資料結構,VFS系統能根據此資料結構确定該VMA虛拟位址空間對應于哪個磁盤檔案。VMA的vm_pgoff記錄的是檔案的偏移量,VFS檔案系統根據此資料确認該VMA虛拟位址空間對應磁盤檔案中的哪一段資料。VFS詳細介紹可參考https://www.cnblogs.com/huxiao-tee/p/4657851.html
知識來源:
1.https://www.zhihu.com/question/24011983
2.https://www.cnblogs.com/ck1020/p/6678530.html
3.https://blog.csdn.net/a372048518/article/details/103865898