Mmap內(nèi)存映射的原理以及實(shí)現(xiàn)
面試和工作中可能會(huì)用到mmap內(nèi)存映射,今天就來聊一聊
1、mmap基礎(chǔ)概念
- mmap 即 memory map,也就是內(nèi)存映射;
- mmap 是一種內(nèi)存映射文件的方法,即將一個(gè)文件或者其它對(duì)象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對(duì)映關(guān)系;
- 實(shí)現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會(huì)自動(dòng)回寫臟頁面到對(duì)應(yīng)的文件磁盤上;
- 即完成了對(duì)文件的操作而不必再調(diào)用 read、write 等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對(duì)這段區(qū)域的修改也直接反映用戶空間,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享;
mmap 具有如下的特點(diǎn):
- mmap 向應(yīng)用程序提供的內(nèi)存訪問接口是內(nèi)存地址連續(xù)的,但是對(duì)應(yīng)的磁盤文件的 block 可以不是地址連續(xù)的;
- mmap 提供的內(nèi)存空間是虛擬空間(虛擬內(nèi)存),而不是物理空間(物理內(nèi)存),因此完全可以分配遠(yuǎn)遠(yuǎn)大于物理內(nèi)存大小的虛擬空間(例如 16G 內(nèi)存主機(jī)分配 1000G 的 mmap 內(nèi)存空間);
- mmap 負(fù)責(zé)映射文件邏輯上一段連續(xù)的數(shù)據(jù)(物理上可以不連續(xù)存儲(chǔ))映射為連續(xù)內(nèi)存,而這里的文件可以是磁盤文件、驅(qū)動(dòng)假造出的文件(例如 DMA 技術(shù))以及設(shè)備;
- mmap 由操作系統(tǒng)負(fù)責(zé)管理,對(duì)同一個(gè)文件地址的映射將被所有線程共享,操作系統(tǒng)確保線程安全以及線程可見性;
- mmap 的設(shè)計(jì)很有啟發(fā)性?;诖疟P的讀寫單位是 block(一般大小為 4KB),而基于內(nèi)存的讀寫單位是地址(雖然內(nèi)存的管理與分配單位是 4KB)。換言之,CPU 進(jìn)行一次磁盤讀寫操作涉及的數(shù)據(jù)量至少是 4KB,但是進(jìn)行一次內(nèi)存操作涉及的數(shù)據(jù)量是基于地址的,也就是通常的 64bit(64 位操作系統(tǒng))。mmap 下進(jìn)程可以采用指針的方式進(jìn)行讀寫操作,這是值得注意的;
2、mmap內(nèi)存映射原理
mmap內(nèi)存映射的實(shí)現(xiàn)過程,總的來說可以分為三個(gè)階段:
2.1進(jìn)程啟動(dòng)映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域;
- 進(jìn)程在用戶空間調(diào)用庫函數(shù)mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 在當(dāng)前進(jìn)程的虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址
- 為此虛擬區(qū)分配一個(gè)vm_area_struct結(jié)構(gòu),接著對(duì)這個(gè)結(jié)構(gòu)的各個(gè)域進(jìn)行了初始化
- 將新建的虛擬區(qū)結(jié)構(gòu)(vm_area_struct)插入進(jìn)程的虛擬地址區(qū)域鏈表或樹中
2.2調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系
- 為映射分配了新的虛擬地址區(qū)域后,通過待映射的文件指針,在文件描述符表中找到對(duì)應(yīng)的文件描述符,通過文件描述符,鏈接到內(nèi)核“已打開文件集”中該文件的文件結(jié)構(gòu)體(struct file),每個(gè)文件結(jié)構(gòu)體維護(hù)著和這個(gè)已打開文件相關(guān)各項(xiàng)信息;
- 通過該文件的文件結(jié)構(gòu)體,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數(shù);
- 內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址;
- 通過remap_pfn_range函數(shù)建立頁表,即實(shí)現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系。此時(shí),這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中;
2.3進(jìn)程發(fā)起對(duì)這片映射空間的訪問,引發(fā)缺頁異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝
- 前兩個(gè)階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當(dāng)進(jìn)程發(fā)起讀或?qū)懖僮鲿r(shí);
- 進(jìn)程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址,通過查詢頁表,發(fā)現(xiàn)這一段地址并不在物理頁面上。因?yàn)槟壳爸唤⒘说刂酚成?,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁異常;
- 缺頁異常進(jìn)行一系列判斷,確定無非法操作后,內(nèi)核發(fā)起請(qǐng)求調(diào)頁過程。
- 調(diào)頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁,如果沒有則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中;
- 1之后進(jìn)程即可對(duì)這片主存進(jìn)行讀或者寫的操作,如果寫操作改變了其內(nèi)容,一定時(shí)間后系統(tǒng)會(huì)自動(dòng)回寫臟頁面到對(duì)應(yīng)磁盤地址,也即完成了寫入到文件的過程;
- 修改過的臟頁面并不會(huì)立即更新回文件中,而是有一段時(shí)間的延遲,可以調(diào)用msync()來強(qiáng)制同步, 這樣所寫的內(nèi)容就能立即保存到文件里了;
3、mmap函數(shù)實(shí)例分析
3.1mmap函數(shù)的原型
參數(shù)addr:指定映射的起始地址,通常設(shè)為NULL,由內(nèi)核來分配
參數(shù)length:代表將文件中映射到內(nèi)存的部分的長度。
參數(shù)prot:映射區(qū)域的保護(hù)方式??梢詾橐韵聨追N方式的組合:
- PROT_EXEC 映射區(qū)域可被執(zhí)行
- PROT_READ 映射區(qū)域可被讀取
- PROT_WRITE 映射區(qū)域可被寫入
- PROT_NONE 映射區(qū)域不能存取
參數(shù)flags:映射區(qū)的特性標(biāo)志位,常用的兩個(gè)選項(xiàng)是:
- MAP_SHARD:寫入映射區(qū)的數(shù)據(jù)會(huì)復(fù)制回文件,且運(yùn)行其他映射文件的進(jìn)程共享
- MAP_PRIVATE:對(duì)映射區(qū)的寫入操作會(huì)產(chǎn)生一個(gè)映射區(qū)的復(fù)制,對(duì)此區(qū)域的修改不會(huì)寫會(huì)原文件
參數(shù)fd:要映射到內(nèi)存中的文件描述符,有open函數(shù)打開文件時(shí)返回的值。
參數(shù)offset:文件映射的偏移量,通常設(shè)置為0,代表從文件最前方開始對(duì)應(yīng),offset必須是分頁大小的整數(shù)倍。
函數(shù)返回值:實(shí)際分配的內(nèi)存的起始地址
3.2munmap函數(shù)
與mmap函數(shù)成對(duì)使用的是munmap函數(shù),它是用來解除映射的函數(shù);
- 參數(shù)start:映射的起始地址
- 參數(shù)length:文件中映射到內(nèi)存的部分的長度
- 返回值:解除成功返回0,失敗返回-1
3.3實(shí)例
下面是一個(gè)mmap使用的實(shí)例代碼
這段代碼實(shí)現(xiàn)了將測(cè)試文件testdata打開,并用mmap函數(shù)將文件映射到虛擬內(nèi)存中,通過指針start對(duì)文件進(jìn)行讀寫。在終端中可看到由文件讀取的數(shù)據(jù)。程序結(jié)束后,可以查看testdata文件,來查看寫入的數(shù)據(jù)