趣谈Linux操作系统学习笔记:用户态内存映射:如何找到正确的会议室?(第25讲)...
一、mmap原理
在虛擬內存空間那一節,我們知道,每一個進程都有一個列表vm_area_struct,指向虛擬地址空間的不同內存塊,這個變量名字叫mmap
struct mm_struct {struct vm_area_struct *mmap; /* list of VMAs */ ...... }struct vm_area_struct {/** 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;/** 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_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: */unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZEunits */struct file * vm_file; /* File we map to (can be NULL). */void * vm_private_data; /* was vm_pte (shared mem) */其實內存映射不僅僅是物理內存和虛擬內存之間的映射,還包括將文件中的內容映射到虛擬內存空間,這個時候訪問內存空間就能夠訪問到文件里面的數據。
而僅有物理內存和虛擬內存的映射是一種特殊情況
1、mmap系統調用
1、如何分配一大塊內存
如果申請一大塊內存就用mmap,mmap是映射內存空間到物理內存
另外,如果一個進程想映射一個文件到自己的虛擬內存空間,也要通過mmap系統調用這個時候mmap是映射內存空間到物理內存再到文件。可見mmap這個系統調用時核心,
2、我們現在來看mmap這個系統調用
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, off) { ......error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT); ...... }SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, pgoff) {struct file *file = NULL; ......file = fget(fd); ......retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);return retval; }如果映射到文件,fd會傳進來一個文件描述符,并且mmap_pgoff里面通過fget函數,根據文件描述符獲得struct file、struct file表示打開一個文件
接下來的調用鏈是:
這里主要干了兩件事情
1、調用 get_unmapped_area 找到一個沒有映射的區域
2、調用 mmap_region 映射這個區域。
3、我們先來看 get_unmapped_area 函數。
unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags) {unsigned long (*get_area)(struct file *, unsigned long,unsigned long, unsigned long, unsigned long); ......get_area = current->mm->get_unmapped_area;if (file) {if (file->f_op->get_unmapped_area)get_area = file->f_op->get_unmapped_area;} ...... } const struct file_operations ext4_file_operations = { .......mmap = ext4_file_mmap.get_unmapped_area = thp_get_unmapped_area, };unsigned long __thp_get_unmapped_area(struct file *filp, unsigned long len,loff_t off, unsigned long flags, unsigned long size) {unsigned long addr;loff_t off_end = off + len;loff_t off_align = round_up(off, size);unsigned long len_pad;len_pad = len + size; ......addr = current->mm->get_unmapped_area(filp, 0, len_pad,off >> PAGE_SHIFT, flags);addr += (off - addr) & (size - 1);return addr; }?4、我們再來看 mmap_region 函數,看它如何映射這個虛擬內存區域
unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,struct list_head *uf) {struct mm_struct *mm = current->mm;struct vm_area_struct *vma, *prev;struct rb_node **rb_link, *rb_parent;/** Can we just expand an old mapping?*/vma = vma_merge(mm, prev, addr, addr + len, vm_flags,NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);if (vma)goto out;/** Determine the object being mapped and call the appropriate* specific mapper. the address has already been validated, but* not unmapped, but the maps are removed from the list.*/vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);if (!vma) {error = -ENOMEM;goto unacct_error;}vma->vm_mm = mm;vma->vm_start = addr;vma->vm_end = addr + len;vma->vm_flags = vm_flags;vma->vm_page_prot = vm_get_page_prot(vm_flags);vma->vm_pgoff = pgoff;INIT_LIST_HEAD(&vma->anon_vma_chain);if (file) {vma->vm_file = get_file(file);error = call_mmap(file, vma);addr = vma->vm_start;vm_flags = vma->vm_flags;} ......vma_link(mm, vma, prev, rb_link, rb_parent);return addr; .....1、還記得咱們剛找到了虛擬內存區域的前一個 vm_area_struct,我們首先要看,是否能夠基于它進行擴展,也即調用 vma_merge,和前一個 vm_area_struct 合并到一起。
2、如果不能,就需要調用 kmem_cache_zalloc,在 Slub 里面創建一個新的 vm_area_struct對象,設置起始和結束位置,將它加入隊列。如果是映射到文件,則設置 vm_file 為目標文件,
調用 call_mmap。其實就是調用 file_operations 的 mmap 函數
3、對于 ext4 文件系統,調用的是 ext4_file_mmap。從這個函數的參數可以看出,這一刻文件和內存開始發生關系了。這里我們將vm_area_struct 的內存操作設置為文件系統操作,也就是說,
讀寫內存其實就是讀寫文件系統。
static inline int call_mmap(struct file *file, struct vm_area_struct *vma) {return file->f_op->mmap(file, vma); }static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma) { ......vma->vm_ops = &ext4_file_vm_ops; ...... }5、我們再回到 mmap_region 函數。
最終,vma_link 函數將新創建的 vm_area_struct 掛在了 mm_struct 里面的紅黑樹上。
這個時候,從內存到文件的映射關系,至少要在邏輯層面建立起來。那從文件到內存的映射關系呢?vma_link 還做了另外一件事情,就是 __vma_link_file。這個東西要用于建立這層映射關系。
對于打開的文件,會有一個結構 struct file 來表示。它有個成員指向 struct address_space 結構,這里面有棵變量名為 i_mmap 的紅黑樹,vm_area_struct 就掛在這棵樹上。
struct address_space {struct inode *host; /* owner: inode, block_device */ ......struct rb_root i_mmap; /* tree of private and shared mappings */ ......const struct address_space_operations *a_ops; /* methods */ ...... }static void __vma_link_file(struct vm_area_struct *vma) {struct file *file;file = vma->vm_file;if (file) {struct address_space *mapping = file->f_mapping;vma_interval_tree_insert(vma, &mapping->i_mmap);}到這里,內存映射的內容要告一段落,你可能會困惑,好像還沒有和物理內存法神過任何關系、還是在虛擬內存里面折騰呀?對的,因為到目前為止,我們還沒有開始真正訪問內存呀!
這個時候,內存管理并不直接分配物理內存,因為物理內存相對于虛擬地址空間太寶貴了,只要等你真正用的那一刻才會開始分配
二、用戶態缺頁異常
一旦開始訪問虛擬內存的某個地址,如果我們發現,并沒有對應的物理頁,那就出發缺頁中斷,調用do_page_fault
dotraplinkage void notrace do_page_fault(struct pt_regs *regs, unsigned long error_code) {unsigned long address = read_cr2(); /* Get the faulting address */ ......__do_page_fault(regs, error_code, address); ...... }/** This routine handles page faults. It determines the address,* and the problem, and then passes it off to one of the appropriate* routines.*/ static noinline void __do_page_fault(struct pt_regs *regs, unsigned long error_code,unsigned long address) {struct vm_area_struct *vma;struct task_struct *tsk;struct mm_struct *mm;tsk = current;mm = tsk->mm;if (unlikely(fault_in_kernel_space(address))) {if (vmalloc_fault(address) >= 0)return;} ......vma = find_vma(mm, address); ......fault = handle_mm_fault(vma, address, flags); ......1、在do_page_fault里面,先要判斷缺頁中斷是否發生在內核,如果發生在內核則調用vmalloc_fault,這就是和咱們前面學過的虛擬內存的布局對應上了
2、在內核里面,vmalloc區域需要內核頁表映射到物理頁,咱們這里把內核的這部分放放,接著看用戶空間的部分
3、接下來在用戶空間里面,找到你訪問的那個地址所在的區域 vm_area_struct,然后調用 handle_mm_fault 來映射這個區域。
static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,unsigned int flags) {struct vm_fault vmf = {.vma = vma,.address = address & PAGE_MASK,.flags = flags,.pgoff = linear_page_index(vma, address),.gfp_mask = __get_fault_gfp_mask(vma),};struct mm_struct *mm = vma->vm_mm;pgd_t *pgd;p4d_t *p4d;int ret;pgd = pgd_offset(mm, address);p4d = p4d_alloc(mm, pgd, address); ......vmf.pud = pud_alloc(mm, p4d, address); ......vmf.pmd = pmd_alloc(mm, vmf.pud, address); ......return handle_pte_fault(&vmf); }到這里,終于看到了我們熟悉的 PGD、P4G、PUD、PMD、PTE,這就是前面講頁表的時候,講述的四級頁表的概念,因為暫且不考慮五級頁表,我們暫時忽略P4G
1、pgd_t 用于全局頁目錄項,pud_t 用于上層頁目錄項,pmd_t 用于中間頁目錄項,pte_t 用于直接頁表項。
2、每個進程都有獨立的地址空間,為了這個進程獨立完成映射,每個進程都有獨立的進程頁表,這個頁表的最頂級的 pgd 存放在 task_struct 中的 mm_struct 的 pgd變量里面
3、在一個進程新創建的時候,會調用 fork,對于內存的部分會調用 copy_mm,里面調用 dup_mm
/** Allocate a new mm structure and copy contents from the* mm structure of the passed in task structure.*/ static struct mm_struct *dup_mm(struct task_struct *tsk) {struct mm_struct *mm, *oldmm = current->mm;mm = allocate_mm();memcpy(mm, oldmm, sizeof(*mm));if (!mm_init(mm, tsk, mm->user_ns))goto fail_nomem;err = dup_mmap(mm, oldmm);return mm; }在這里,除了創建一個新的 mm_struct,并且通過 memcpy 將它和父進程的弄成一模一樣之外,我們還需要調用 mm_init 進行初始化。接下來,
mm_init 調用 mm_alloc_pgd,分配全局、頁目錄項,賦值給 mm_struct 的 pdg 成員變量。
static inline int mm_alloc_pgd(struct mm_struct *mm) {mm->pgd = pgd_alloc(mm);return 0; }pgd_alloc 里面除了分配 PDG 之外,還做了很重要的一個事情,就是調用 pgd_ctor
static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd) {/* If the pgd points to a shared pagetable level (either theptes in non-PAE, or shared PMD in PAE), then just copy thereferences from swapper_pg_dir. */if (CONFIG_PGTABLE_LEVELS == 2 ||(CONFIG_PGTABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||CONFIG_PGTABLE_LEVELS >= 4) {clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,swapper_pg_dir + KERNEL_PGD_BOUNDARY,KERNEL_PGD_PTRS);} ...... }1、pgd_ctor干了什么
2、load_new_mm_cr3 為什么要將虛擬地址轉換為物理地址?
因為cr3存放的是物理地址,只有將虛擬地址轉換為物理地址才能加載到 cr3 里面去
3、load_new_mm_cr3將虛擬地址轉換為虛擬地址的調用鏈
4、地址轉換的過程無需進入內核態
5、觸發缺頁異常調用鏈
只有訪問內存的時候發現沒有映射多物理內存,頁表也沒有創建過,才觸發缺頁異常
繞了一大圈,終于將頁表整個機制的各個部分串了起來。但是咱們的故事還沒講完,物理的內存、還沒找到。我們還得接著分析 handle_pte_fault 的實現。
static int handle_pte_fault(struct vm_fault *vmf) {pte_t entry; ......vmf->pte = pte_offset_map(vmf->pmd, vmf->address);vmf->orig_pte = *vmf->pte; ......if (!vmf->pte) {if (vma_is_anonymous(vmf->vma))return do_anonymous_page(vmf);elsereturn do_fault(vmf);}if (!pte_present(vmf->orig_pte))return do_swap_page(vmf); ...... }匿名頁調用
這個函數你還記得嗎?就是咱們伙伴系統的核心函數,專門用來分配物理頁面的。do_anonymous_page 接下來要調用 mk_pte,將頁表項指向新分配的物理頁,set_pte_at 會將頁表項塞到頁表里面。
static int do_anonymous_page(struct vm_fault *vmf) {struct vm_area_struct *vma = vmf->vma;struct mem_cgroup *memcg;struct page *page;int ret = 0;pte_t entry; ......if (pte_alloc(vma->vm_mm, vmf->pmd, vmf->address))return VM_FAULT_OOM; ......page = alloc_zeroed_user_highpage_movable(vma, vmf->address); ......entry = mk_pte(page, vma->vm_page_prot);if (vma->vm_flags & VM_WRITE)entry = pte_mkwrite(pte_mkdirty(entry));vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,&vmf->ptl); ......set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); ...... }第二種情況映射到文件 do_fault,最終我們會調用 __do_fault
int swap_readpage(struct page *page, bool do_poll) {struct bio *bio;int ret = 0;struct swap_info_struct *sis = page_swap_info(page);blk_qc_t qc;struct block_device *bdev; ......if (sis->flags & SWP_FILE) {struct file *swap_file = sis->swap_file;struct address_space *mapping = swap_file->f_mapping;ret = mapping->a_ops->readpage(swap_file, page);return ret;} ...... }這里調用了struct vm_operations_struct vm_ops的fault函數,還記得咱們上面用mmap映射文件的時候,對于ext4文件系統,vm_ops指向了ext4_file_vm_ops也就是調用了函數ext4_filemap_fault
static const struct vm_operations_struct ext4_file_vm_ops = {.fault = ext4_filemap_fault,.map_pages = filemap_map_pages,.page_mkwrite = ext4_page_mkwrite, };int ext4_filemap_fault(struct vm_fault *vmf) {struct inode *inode = file_inode(vmf->vma->vm_file); ......err = filemap_fault(vmf); ......return err; }ext4_filemap_fault里面的邏輯我們很容易就能讀懂,vm_file就是咱們當時mmap的時候映射的那個文件,然后我們需要調用filemap_fault
對于文件映射來說,一般這個文件會在物理內存里面有頁面作為它的緩存,find_get_page就是找那個頁,如果找到了,就調用,預讀一些數據到內存里面;如果沒有,就跳到no_cached_page
int filemap_fault(struct vm_fault *vmf) {int error;struct file *file = vmf->vma->vm_file;struct address_space *mapping = file->f_mapping;struct inode *inode = mapping->host;pgoff_t offset = vmf->pgoff;struct page *page;int ret = 0; ......page = find_get_page(mapping, offset);if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {do_async_mmap_readahead(vmf->vma, ra, file, page, offset);} else if (!page) {goto no_cached_page;} ......vmf->page = page;return ret | VM_FAULT_LOCKED; no_cached_page:error = page_cache_read(file, offset, vmf->gfp_mask); ...... }1、如果沒有物理內存中的緩存頁
2、那我們就調用 page_cach—_read
3、在這里顯示分配一個緩存頁
4、將這一頁加到 lru 表里面
5、然后在 address_space 中調用 aaddress_space_operations 的readpage 函數,將文件內容讀到內存中。address_space 的作用咱們上面也介紹過了。
static int page_cache_read(struct file *file, pgoff_t offset, gfp_t gfp_mask) {struct address_space *mapping = file->f_mapping;struct page *page; ......page = __page_cache_alloc(gfp_mask|__GFP_COLD); ......ret = add_to_page_cache_lru(page, mapping, offset, gfp_mask & GFP_KERNEL); ......ret = mapping->a_ops->readpage(file, page); ...... }struct address_space_operations對于 ext4 文件系統的定義如下所示。這么說來,
上面的 readpage 調用的其實是 ext4_readage。因為我們還沒講到文件系統,這里我們不詳細介紹
ext4_readpage 具體干了什么。你只要知道,最后會調用 ext4_read_inline_page,這里面有部分邏輯和內存映射有關就行了。
static const struct address_space_operations ext4_aops = {.readpage = ext4_readpage,.readpages = ext4_readpages, ...... };static int ext4_read_inline_page(struct inode *inode, struct page *page) {void *kaddr; ......kaddr = kmap_atomic(page);ret = ext4_read_inline_data(inode, kaddr, len, &iloc);flush_dcache_page(page);kunmap_atomic(kaddr); ...... }1、為什么要在內核里面映射一把?
1、在 ext4_read_inline_page 函數里,我們需要先調用 kmap_atomic,將物理內存映射到內核的虛擬地址空間,得到內核中的地址kaddr
2、kaddr它是用來做臨時內核映射的。本來把物理內存映射到用戶虛擬地址空間,不需要在內核里面映射一把。
但是,現在因為要從文件里面讀取數據并寫入這個物理頁面,又不能使用物理地址,
我們只能使用虛擬地址,這就需要在內核里面臨時映射一把。臨時映射后,ext4_read_inline_data 讀取文件到這個虛擬地址。讀取完畢后,我們取消這個臨時映射 kunmap_atomic 就行了。
我們再來看第三種情況,do_swap_page。之前我們講過物理內存管理,你這里可以回憶一下。如果長時間不用,就要換出到硬盤,
也就是 swap,現在這部分數據又要訪問了,我們還得想辦法再讀到內存中來。
int do_swap_page(struct vm_fault *vmf) {struct vm_area_struct *vma = vmf->vma;struct page *page, *swapcache;struct mem_cgroup *memcg;swp_entry_t entry;pte_t pte; ......entry = pte_to_swp_entry(vmf->orig_pte); ......page = lookup_swap_cache(entry);if (!page) {page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vma,vmf->address); ......} ......swapcache = page; ......pte = mk_pte(page, vma->vm_page_prot); ......set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);vmf->orig_pte = pte; ......swap_free(entry); ...... }1、do_swap_page函數會先查找 swap 文件有沒有緩存頁。
2、如果沒有,就調用swapin_readahead,將 swap 文件讀到內存中來,形成內存頁,并通過 mk_pte 生成頁表項。
3、set_pte_at 將頁表項插入頁表,將 swap 文件清理。因為重新加載回內存了,不再需要 swap 文件了。
4、swapin_readahead 會最終調用 swap_readpage,在這里,我們看到了熟悉的readpage 函數,也就是說讀取普通文件和讀取 swap 文件,
過程是一樣的,同樣需要用 kmap_atomickmap_atomic 做臨時映射。
int swap_readpage(struct page *page, bool do_poll) {struct bio *bio;int ret = 0;struct swap_info_struct *sis = page_swap_info(page);blk_qc_t qc;struct block_device *bdev; ......if (sis->flags & SWP_FILE) {struct file *swap_file = sis->swap_file;struct address_space *mapping = swap_file->f_mapping;ret = mapping->a_ops->readpage(swap_file, page);return ret;} ...... }通過上面復雜的過程,用戶缺頁異常處理完畢了,物理內存中有了頁面,頁表也建立好了映射,接下來用戶程序在虛擬內存空間里面,可以通過虛擬地址順利經過頁表映射的訪問物理頁面上的數據了
為了加快映射速度,我們不需要每次從虛擬地址到物理地址都轉換走一遍頁表
1、頁表一般都很大,只能存放在內存中,操作系統每次訪問內存要折騰兩步
1、先通過查詢頁表得到物理地址
2、然后訪問該物理地址讀取指令、數據
2、TLB 頁表的 Cache
為了提高映射速度,我們引入了TLB(Translation Lookaside Buffer)我們經常稱為快表,專門用來做地址映射的硬件設備。
它不在內存中、可存儲的數據比較少,但是比內存要快。所以,我們可以想象,TLB 就是頁表的 Cache,其中存儲了當前最可能被訪問到的頁表項,其內容是部分頁表項的一個副本。
3、有了 TLB 之后,地址映射的過程就像圖中畫的
1、我們先查塊表,塊表中有映射關系,然后直接轉換為物理地址。
2、如果在 TLB 查不到映射關系時,才會到內存中查詢頁表。
總結時刻
用戶態的內存映射機制,我們解析的差不多了,我們來總結一下,用戶態的內存映射機制包含以下幾個部分
用戶態內存映射函數 mmap,包括用它來做匿名映射和文件映射...
用戶態的頁表結構,存儲位置在 mm_struct 中。
在用戶態訪問沒有映射的內存會引發缺頁異常,分配物理頁表,補齊頁表。如果是匿名映射則
分配物理內存;如果是 swap,則將 swap 文件讀入;如果是文件映射,則將文件讀入
?
轉載于:https://www.cnblogs.com/luoahong/p/10916458.html
總結
以上是生活随笔為你收集整理的趣谈Linux操作系统学习笔记:用户态内存映射:如何找到正确的会议室?(第25讲)...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 本地 php nginx压测试
- 下一篇: 将GB28181国标流转成RTSP流