mmap詳解
- mmap基礎概念
- mmap內存映射原理
- mmap相關函數調用
- mmap的使用細節
- mmap和常規文件操作的區別
mmap基礎概念
mmap
是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內存,而系統會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read
,write
等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。mmap
還可以用于實現共享內存,允許不同進程間共享數據,如下圖所示:
我們知道,在進程虛擬地址空間中,內存映射部分是處于堆棧之間的,linux內核使用vm_area_struct
結構來表示一個獨立的虛擬內存區域,由于每個不同質的虛擬內存區域功能和內部機制都不同,因此一個進程使用多個vm_area_struct
結構來分別表示不同類型的虛擬內存區域。各個vm_area_struct
結構使用鏈表或者樹形結構鏈接,方便進程快速訪問,如下圖所示:
mm_struct
就是進程用戶空間的抽象,一個進程只有一個mm_struct
結構,當一個mm_struct
結構卻可以為多個進程所共享,例如當一個進程創建一個子進程時(vfork
或clone
),子進程與父進程共享一個mm_struct
,mm_struct
的代碼就如下:
struct mm_struct {struct vm_area_struct *mmap; /* list of VMAs */ //指向VMA對象的鏈表頭struct rb_root mm_rb; //指向VMA對象的紅黑樹的根u64 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMUunsigned long (*get_unmapped_area) (struct file *filp,unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags); // 在進程地址空間中搜索有效線性地址區間的方法
#endifunsigned long mmap_base; /* base of mmap area */unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES/* Base adresses for compatible mmap() */unsigned long mmap_compat_base;unsigned long mmap_compat_legacy_base;
#endifunsigned long task_size; /* size of task vm space */unsigned long highest_vm_end; /* highest vma end address */pgd_t * pgd; //指向頁全局目錄/*** @mm_users: The number of users including userspace.** Use mmget()/mmget_not_zero()/mmput() to modify. When this drops* to 0 (i.e. when the task exits and there are no other temporary* reference holders), we also release a reference on @mm_count* (which may then free the &struct mm_struct if @mm_count also* drops to 0).*/atomic_t mm_users; //使用計數器/*** @mm_count: The number of references to &struct mm_struct* (@mm_users count as 1).** Use mmgrab()/mmdrop() to modify. When this drops to 0, the* &struct mm_struct is freed.*/atomic_t mm_count; //使用計數器atomic_long_t nr_ptes; /* PTE page table pages */ //進程頁表數
#if CONFIG_PGTABLE_LEVELS > 2atomic_long_t nr_pmds; /* PMD page table pages */
#endifint map_count; /* number of VMAs */ //VMA的個數spinlock_t page_table_lock; /* Protects page tables and some counters */struct rw_semaphore mmap_sem;struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung* together off init_mm.mmlist, and are protected* by mmlist_lock*/unsigned long hiwater_rss; /* High-watermark of RSS usage */unsigned long hiwater_vm; /* High-water virtual memory usage */unsigned long total_vm; /* Total pages mapped */ //進程地址空間的頁數unsigned long locked_vm; /* Pages that have PG_mlocked set */ //鎖住的頁數,不能換出unsigned long pinned_vm; /* Refcount permanently increased */unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */ //數據段內存的頁數unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */ //可執行內存映射的頁數unsigned long stack_vm; /* VM_STACK */ //用戶態堆棧的頁數unsigned long def_flags;unsigned long start_code, end_code, start_data, end_data; //代碼段,數據段等的地址unsigned long start_brk, brk, start_stack; //堆棧段的地址,start_stack表示用戶態堆棧的起始地址,brk為堆的當前最后地址unsigned long arg_start, arg_end, env_start, env_end; //命令行參數的地址,環境變量的地址unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv *//** Special counters, in some configurations protected by the* page_table_lock, in other configurations by being atomic.*/struct mm_rss_stat rss_stat;struct linux_binfmt *binfmt;cpumask_var_t cpu_vm_mask_var;/* Architecture-specific MM context */mm_context_t context;unsigned long flags; /* Must use atomic bitops to access the bits */struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_MEMBARRIERatomic_t membarrier_state;
#endif
#ifdef CONFIG_AIOspinlock_t ioctx_lock;struct kioctx_table __rcu *ioctx_table;
#endif
#ifdef CONFIG_MEMCG/** "owner" points to a task that is regarded as the canonical* user/owner of this mm. All of the following must be true in* order for it to be changed:** current == mm->owner* current->mm != mm* new_owner->mm == mm* new_owner->alloc_lock is held*/struct task_struct __rcu *owner;
#endifstruct user_namespace *user_ns;/* store ref to file /proc/<pid>/exe symlink points to */struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIERstruct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKSpgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_CPUMASK_OFFSTACKstruct cpumask cpumask_allocation;
#endif
#ifdef CONFIG_NUMA_BALANCING/** numa_next_scan is the next time that the PTEs will be marked* pte_numa. NUMA hinting faults will gather statistics and migrate* pages to new nodes if necessary.*/unsigned long numa_next_scan;/* Restart point for scanning and setting pte_numa */unsigned long numa_scan_offset;/* numa_scan_seq prevents two threads setting pte_numa */int numa_scan_seq;
#endif/** An operation with batched TLB flushing is going on. Anything that* can move process memory needs to flush the TLB when moving a* PROT_NONE or PROT_NUMA mapped page.*/atomic_t tlb_flush_pending;
#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH/* See flush_tlb_batched_pending() */bool tlb_flush_batched;
#endifstruct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGEatomic_long_t hugetlb_usage;
#endifstruct work_struct async_put_work;#if IS_ENABLED(CONFIG_HMM)/* HMM needs to track a few things per mm */struct hmm *hmm;
#endif
} __randomize_layout;
struct vm_area_struct
用于描述進程地址空間中的一段虛擬區域,每一個VMA都對應一個struct vm_area_struct
。
/** 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 {/* The first cache line has the info for VMA tree walking. */unsigned long vm_start; /* Our start address within vm_mm. */ //起始地址unsigned long vm_end; /* The first byte after our end addresswithin vm_mm. */ //結束地址,區間中不包含結束地址/* linked list of VM areas per task, sorted by address */ //按起始地址排序的鏈表struct vm_area_struct *vm_next, *vm_prev;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. */struct mm_struct *vm_mm; /* The address space we belong to. */pgprot_t vm_page_prot; /* Access permissions of this 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;/** 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) */atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMUstruct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMAstruct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endifstruct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;
我們的mmap
函數在使用的過程中就是要創建一個新的vm_area_struct
,并將其與文件的物理磁盤地址相連,關系圖如下圖:
mmap內存映射原理
mmap內存映射的實現過程,總的來說可以分為三個階段:
- 進程啟動映射過程,并在虛擬地址空間中為映射創建虛擬映射區域
-
1. 進程在用戶空間調用庫函數
mmap
; -
2. 在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續的虛擬地址;
-
3. 為此虛擬區分配一個
vm_area_struct
結構,接著對這個結構的各個域進行了初始化; -
4. 將新建的虛擬區結構(
vm_area_struct
)插入進程的虛擬地址區域鏈表或樹中。
- 調用內核空間的系統調用函數
mmap
(不同于用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關系
- 5. 為映射分配了新的虛擬地址區域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核“已打開文件集”中該文件的文件結構體(
struct file
),每個文件結構體維護著和這個已打開文件相關各項信息。 - 6. 通過該文件的文件結構體,鏈接到
file_operations
模塊,調用內核函數mmap
,其原型為:intmmap(struct file *filp, struct vm_area_struct *vma)
,不同于用戶空間庫函數。 - 7. 內核
mmap
函數通過虛擬文件系統inode
模塊定位到文件磁盤物理地址。 - 8. 通過
remap_pfn_range
函數建立頁表,即實現了文件地址和虛擬地址區域的映射關系。此時,這片虛擬地址并沒有任何數據關聯到主存中。
注意:前兩個階段僅在于創建虛擬區間并完成地址映射,但是并沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時,也就是接下來這個階段。
- 進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝
- 9. 進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常;
- 10. 缺頁異常進行一系列判斷,確定無非法操作后,內核發起請求調頁過程;
- 11. 調頁過程先在交換緩存空間(
swap cache
)中尋找需要訪問的內存頁,如果沒有則調用nopage
函數把所缺的頁從磁盤裝入到主存中。 - 12. 之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間后系統會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程。
注意:修改過的臟頁面并不會立即更新回文件中,而是有一段時間的延遲,可以調用msync()來強制同步, 這樣所寫的內容就能立即保存到文件里了。
mmap相關函數調用
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
返回值說明:
- 成功執行時,
mmap
返回被映射區的指針。失敗時,mmap
返回MAP_FAILED
[其值為(void *)-1
],error
被設為以下的某個值:
1 EACCES:訪問出錯2 EAGAIN:文件已被鎖定,或者太多的內存已被鎖定3 EBADF:fd不是有效的文件描述符4 EINVAL:一個或者多個參數無效5 ENFILE:已達到系統對打開文件的限制6 ENODEV:指定文件所在的文件系統不支持內存映射7 ENOMEM:內存不足,或者進程已超出最大內存映射數量8 EPERM:權能不足,操作不允許9 ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標志
10 SIGSEGV:試著向只讀區寫入
11 SIGBUS:試著訪問不屬于進程的內存區
參數說明:
void *addr
:一個提示地址,表示希望映射區域開始的地址。然?,這個地址可能會被內核忽略,特別是當我們沒有足夠的權限來請求特定的地址時。如果addr
是NULL
,則系統會?動選擇?個合適的地址;size_t length
: 要映射到進程地址空間中的字節數。這個長度必須是系統頁面大小的整數倍(通常是 4KB ,但可能因系統而異)。如果指定的length
不是頁面大小的整數倍,系統可能會向上舍入到最近的頁面大小邊界(系統內存頁大小為4KB(即4096字節),而請求的內存大小為3500字節,則按照向上舍入的原則,應分配4096字節的內存);int prot
: 指定了映射區域的內存保護屬性。可以是以下值的組合(使用按位或運算符|
):
—PROT_READ
:映射區域可讀。
—PROT_WRITE
:映射區域可寫。
—PROT_EXEC
:映射區域可執行。
—PROT_NONE
:頁不可訪問int flags
: 指定了映射的類型和其他選項,可以是一下位的組合值;
1 MAP_FIXED //使用指定的映射起始地址,如果由start和len參數指定的內存區重疊于現存的映射空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。并且起始地址必須落在頁的邊界上。2 MAP_SHARED //與其它所有映射這個對象的進程共享映射空間。對共享區的寫入,相當于輸出到文件。直到msync()或者munmap()被調用,文件實際上不會被更新。3 MAP_PRIVATE //建立一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件。這個標志和以上標志是互斥的,只能使用其中一個。4 MAP_DENYWRITE //這個標志被忽略。5 MAP_EXECUTABLE //同上6 MAP_NORESERVE //不要為這個映射保留交換空間。當交換空間被保留,對映射區修改的可能會得到保證。當交換空間不被保留,同時內存不足,對映射區的修改會引起段違例信號。7 MAP_LOCKED //鎖定映射區的頁面,從而防止頁面被交換出內存。8 MAP_GROWSDOWN //用于堆棧,告訴內核VM系統,映射區可以向下擴展。9 MAP_ANONYMOUS //匿名映射,映射區不與任何文件關聯。
10 MAP_ANON //MAP_ANONYMOUS的別稱,不再被使用。
11 MAP_FILE //兼容標志,被忽略。
12 MAP_32BIT //將映射區放在進程地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標志只在x86-64平臺上得到支持。
13 MAP_POPULATE //為文件映射通過預讀的方式準備好頁表。隨后對映射區的訪問不會被頁違例阻塞。
14 MAP_NONBLOCK //僅和MAP_POPULATE一起使用時才有意義。不執行預讀,只為已存在于內存中的頁面建立頁表入口。
int fd
: ?個有效的文件描述符,指向要映射的文件或設備。對于匿名映射,這個參數可以是-1
(在某些系統上,也可以使用MAP_ANONYMOUS
或MAP_ANON
標志來指定匿名映射,此時fd
參數會被忽略);off_t offset
: ?件中的起始偏移量,即映射區域的開始位置。offset
和length
一起定義了映射區域在文件中的位置和大小。
相關函數:int munmap( void * addr, size_t len )
- 成功執行時,
munmap
返回0
。失敗時,munmap
返回-1
,error
返回標志和mmap
一致; - 該調用在進程地址空間中解除一個映射關系,
addr
是調用mmap
時返回的地址,len
是映射區的大小; - 當映射關系解除后,對原來映射地址的訪問將導致段錯誤發生。
接下來我們來daemon一段代碼驗證一下:
寫入映射
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>#define SIZE 4096int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "filename" << std::endl;return 1;}std::string filename = argv[1];// 首先需要打開一個文件,要成功寫入文件映射,這里打開文件的模式必須是:O_RWDRint fd = ::open(filename.c_str(), O_CREAT | O_RDWR, 0666);if (fd < 0){std::cerr << "open failed!!!" << std::endl;return 2;}// 默認文件大小是0,無法與mmap形成文件映射,這里需要手動設置文件大小::ftruncate(fd, SIZE);char *mmap_addr = (char *)::mmap(nullptr, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (mmap_addr == MAP_FAILED){perror("mmap error");return 3;}// 對文件進行操作for (int i = 0; i < SIZE; i++){mmap_addr[i] = 'a' + i % 26;}// 取消文件映射::munmap(mmap_addr, SIZE);// 關閉文件::close(fd);return 0;
}
讀取映射
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>#define SIZE 4096int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "filename" << std::endl;return 1;}std::string filename = argv[1];// 首先需要打開一個文件,要成功寫入文件映射,這里打開文件的模式必須是:O_RWDRint fd = ::open(filename.c_str(), O_RDONLY);if (fd < 0){std::cerr << "open failed!!!" << std::endl;return 2;}// 獲取真實文件大小struct stat st;::fstat(fd, &st);char *mmap_addr = (char *)::mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd, 0);if (mmap_addr == MAP_FAILED){perror("mmap error");return 3;}std::cout << mmap_addr << std::endl;// 取消文件映射::munmap(mmap_addr, st.st_size);// 關閉文件::close(fd);return 0;
}
mmap的使用細節
- 使用
mmap
需要注意的一個關鍵點是,mmap
映射區域大小必須是物理頁大小(page_size
)的整倍數(32位系統中通常是4k字節)。原因是,內存的最小粒度是頁,而進程虛擬地址空間和內存的映射也是以頁為單位。為了匹配內存的操作,mmap
從磁盤到虛擬地址空間的映射也必須是頁; - 內核可以跟蹤被內存映射的底層對象(文件)的大小,進程可以合法的訪問在當前文件大小以內又在內存映射區以內的那些字節。也就是說,如果文件的大小一直在擴張,只要在映射區域范圍內的數據,進程都可以合法得到,這和映射建立時文件的大小無關;
- 映射建立之后,即使文件關閉,映射依然存在。因為映射的是磁盤的地址,不是文件本身,和文件句柄無關。同時可用于進程間通信的有效地址空間不完全受限于被映射文件的大小,因為是按頁映射。
場景一:一個文件的大小是 5000 字節,mmap函數從一個文件的起始位置開始,映射 5000 字節到虛擬內存中。
在32位系統下,一個物理頁面所占的大小是 4KB,也就是 4096 字節,如果 mmap 需要將這5000字節的數據映射到虛擬內存當中,就需要映射 8KB 大小,也就是 8192 字節大小,也就是說,在 mmap 函數執行以后,實際上映射到虛擬內存當中大小為 8192 字節大小,對于第5000 ~ 8191字節的數據是以0來進行填充的。
此時:
- 讀 / 寫前 5000 個字節,也就是0 ~ 4999會返回操作文件的內容;
- 讀 5000 ~ 8191 的數據,返回的是0,寫 5000 ~ 8191 的數據,程序不會有任何報錯,但是不會將數據寫入到原文件當中;
- 讀 / 寫 8191 以外的部分,就會返回一個 SIGSEGV 信號。
場景二:一個文件的大小是 5000 字節,mmap函數從一個文件的起始位置開始,映射 15000 字節到虛擬內存中,即映射大小超過了原始文件的大小。
由于原文件大小為 5000 字節,在 0 ~ 8191 之間跟場景一一樣,但是系統要求 mmap 映射 15000 字節大小,而文件大小只占2個物理頁,所以在 8191 ~ 15000 之間的字節不能讀寫,會返回信號異常的錯誤。
此時:
- 對于 0 ~ 8191 字節之間數據操作跟場景一相同;
- 因為原文件只占兩個物理頁,所以對 8191 ~ 15000 字節之間的不能進行讀寫,否則就會返回SIGBUS信號,同樣,對于 15000 字節以外的進行讀寫,會返回一個 SIGSEGV 信號。
場景三:一個文件初始大小為0,使用mmap操作映射了1000*4K的大小,即1000個物理頁大約4M字節空間,mmap返回指針ptr
- 如果在文件建立映射之初,就直接對文件進行讀寫操作,因為此時文件大小為0,沒有映射對應合理的物理頁,就會如圖場景二一樣返回一個SIGBUS信號;
- 但是當映射建立完成以后,已經返回一個 ptr 指針,此時每次操作 ptr 讀寫之前,先增加文件的大小,那么 ptr 在文件內部操作就是合法的,比如文件擴充 4096 個字節,那么此時 ptr 就能操作
([ptr ~ (char*)ptr + 4095])
之間的數據,只要訪問操作最終實在 1000 個映射空間大小范圍內的。
mmap和常規文件操作的區別
- 常規的文件操作(
read / write
)這些操作,是使用了頁緩存機制的,也就是說,我們在調取read
函數時,首先是會將磁盤的數據給寫到頁緩存當中的(內核識別到缺頁異常才會有),此時就完成了一次拷貝,但是頁緩存是處于內核當中的,用戶又不能直接進行訪問,所以又需要將這部分數據拷貝到用戶空間當中,這就進行了兩次拷貝;同樣,write
函數也是一樣的道理,首先會將數據寫入到buffer當中,但是待寫入的buffer并不能直接訪問,所以就會將數據先寫入到對應的主存當中,這就會造成一次拷貝,然后內核在選擇恰當的時機將數據寫入到磁盤當中,進行兩次拷貝。 - 對于
mmap
來說,我們在調用mmap
函數以后,創建新的虛擬內存區域和創建虛擬內存區域與磁盤文件之間的映射關系這兩步并沒有進行任何的拷貝操作,而是當訪問對應的的內存區域發現沒有可以訪問的數據時,此時會觸發缺頁異常,就會將對應的數據從磁盤拷貝到內存當中,然后根據對應的映射關系去進行訪問即可,這期間其實也就進行了一次數據的拷貝工作,提高了對應的效率。