Linux mmap()系統(tǒng)調(diào)用

Linux mmap()系統(tǒng)調(diào)用

mmap()系統(tǒng)調(diào)用的作用與使用

我們可以通過man mmap來查看一下mmap()的說明:

man mmap.png

名字

mmap, munmap -- 映射或者取消映射文件或設(shè)備到內(nèi)存

概要

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
int munmap(void *addr, size_t length);

描述

mmap()在調(diào)用進(jìn)程的虛擬地址空間中創(chuàng)建一個新映射。 新映射的起始地址在 addr 中指定。 length 參數(shù)指定映射的長度。

如果 addrNULL,則內(nèi)核選擇創(chuàng)建映射的地址; 這是創(chuàng)建新映射的最便攜方法。 如果 addr 不為 NULL,則內(nèi)核將其作為關(guān)于放置映射位置的提示; 在 Linux 上,映射將在附近的頁面邊界處創(chuàng)建。 新映射的地址作為調(diào)用結(jié)果返回。

文件映射的內(nèi)容(與匿名映射相反;請參閱下面的 MAP_ANONYMOUS)使用從文件描述符 fd 引用的文件(或其他對象)中的偏移偏移量開始的長度字節(jié)進(jìn)行初始化。 offset 必須是 sysconf(_SC_PAGE_SIZE)返回的頁面大小的倍數(shù)。

prot 參數(shù)描述了映射所需的內(nèi)存保護(hù)(并且不得與文件的打開模式?jīng)_突)。 它是 PROT_NONE 或以下一個或多個標(biāo)志的按位或:

  • PROT_EXEC:頁面帶執(zhí)行屬性。
  • PROT_READ:頁面帶可讀屬性。
  • PROT_WRITE:頁面帶可寫屬性。
  • PROT_NONE:頁面可能不能訪問。

flags 參數(shù)確定映射的更新是否對映射同一區(qū)域的其他進(jìn)程可見,以及更新是否傳遞到底層文件。 此行為是通過在標(biāo)志中準(zhǔn)確包含以下值之一來確定的:

  • MAP_SHARED:共享此映射。 映射的更新對映射此文件的其他進(jìn)程可見,并傳遞到底層文件。 (要精確控制何時將更新傳遞到底層文件需要使用 msync(2)。)
  • MAP_PRIVATE:創(chuàng)建私有的寫時復(fù)制映射。 映射的更新對映射同一文件的其他進(jìn)程不可見,也不會傳遞到底層文件。 未指定在調(diào)用 mmap() 之后對文件所做的更改在映射區(qū)域中是否可見。

此外,可以在標(biāo)志中對以下零個或多個值進(jìn)行 OR 運(yùn)算:

  • MAP_32BIT:將映射放入進(jìn)程地址空間的前 2 GB。 對于 64 位程序,此標(biāo)志僅在 x86-64 上受支持。 添加它是為了允許在前 2GB 內(nèi)存中的某處分配線程堆棧,以提高某些早期 64 位處理器上的上下文切換性能。 現(xiàn)代 x86-64 處理器不再有這個性能問題,所以在這些系統(tǒng)上不需要使用這個標(biāo)志。 設(shè)置 MAP_FIXED 時,將忽略 MAP_32BIT 標(biāo)志。

  • MAP_ANON:MAP_ANONYMOUS 的同義詞。 已棄用。

  • MAP_ANONYMOUS:映射不受任何文件的支持; 它的內(nèi)容被初始化為零。 fdoffset 參數(shù)被忽略; 但是,如果指定了 MAP_ANONYMOUS(或 MAP_ANON),某些實現(xiàn)要求 fd-1,并且便攜式應(yīng)用程序應(yīng)確保這一點。 MAP_ANONYMOUSMAP_SHARED 結(jié)合使用僅在 Linux 內(nèi)核 2.4 后才支持。

  • MAP_EXECUTABLE:該標(biāo)志被忽略。

  • MAP_FILE:兼容性標(biāo)志。 忽略。

  • MAP_FIXED:不要將 addr 解釋為提示:將映射準(zhǔn)確放置在該地址處。 addr 必須是頁面大小的倍數(shù)。 如果 addr 和 len 指定的內(nèi)存區(qū)域與任何現(xiàn)有映射的頁面重疊,則現(xiàn)有映射的重疊部分將被丟棄。 如果無法使用指定的地址, mmap() 將失敗。 因為需要一個固定地址的映射不太容易移植,所以不鼓勵使用這個選項。

  • MAP_GROWSDOWN:用于堆棧。 向內(nèi)核虛擬內(nèi)存系統(tǒng)指示映射應(yīng)在內(nèi)存中向下擴(kuò)展。

  • MAP_HUGETLB:(since Linux 2.6.32)使用“大頁面”分配映射。 有關(guān)更多信息,請參閱 Linux 內(nèi)核源文件 Documentation/vm/hugetlbpage.txt,以及下面的 NOTES。

  • MAP_HUGE_2MB, MAP_HUGE_1GB:與 MAP_HUGETLB 結(jié)合使用以在支持多個 Hugetlb 頁面大小的系統(tǒng)上選擇替代的 Hugetlb 頁面大?。ǚ謩e為 2 MB1 GB)。
    更一般地,可以通過在偏移 MAP_HUGE_SHIFT 處的六位中編碼所需頁面大小的以 2 為底的對數(shù)來配置所需的大頁面大小。 (該位域中的零值提供了默認(rèn)的大頁面大?。豢梢酝ㄟ^ /proc/meminfo 公開的 Hugepagesize 字段發(fā)現(xiàn)默認(rèn)大頁面大小。)因此,上述兩個常量定義為:

    #define MAP_HUGE_2MB    (21 << MAP_HUGE_SHIFT)
    #define MAP_HUGE_1GB    (30 << MAP_HUGE_SHIFT)
    

    可以通過列出 /sys/kernel/mm/hugepages 中的子目錄來發(fā)現(xiàn)系統(tǒng)支持的大頁面大小范圍。

  • MAP_LOCKED:(since Linux 2.5.37)以與 mlock() 相同的方式標(biāo)記要鎖定的 mmaped區(qū)域。 此實現(xiàn)將嘗試填充(預(yù)錯)整個范圍,但如果失敗,則 mmap 調(diào)用不會因 ENOMEM 而失敗。 因此,稍后可能會發(fā)生重大故障。 所以語義不如mlock()強(qiáng)。 當(dāng)映射初始化后無法接受主要故障時,應(yīng)使用 mmap()mlock()。 MAP_LOCKED 標(biāo)志在較舊的內(nèi)核中被忽略。

  • MAP_NONBLOCK:僅與 MAP_POPULATE 結(jié)合使用才有意義。 不要執(zhí)行預(yù)讀:只為 RAM 中已經(jīng)存在的頁創(chuàng)建頁表條目。 從 Linux 2.6.23 開始,這個標(biāo)志會導(dǎo)致 MAP_POPULATE 什么也不做。 有一天,可能會重新實現(xiàn) MAP_POPULATEMAP_NONBLOCK 的組合。

  • MAP_NORESERVE:不要為此映射保留交換空間。 保留交換空間時,可以保證可以修改映射。 當(dāng)沒有保留交換空間時,如果沒有可用的物理內(nèi)存,則可能會在寫入時獲得 SIGSEGV。 另請參閱 proc(5) 中對文件 /proc/sys/vm/overcommit_memory的討論。 在 2.6 之前的內(nèi)核中,此標(biāo)志僅對私有可寫映射有效。

  • MAP_POPULATE:(since Linux 2.5.46)為映射填充頁表(prefault,預(yù)先觸發(fā)page fault)。 對于文件映射,這會導(dǎo)致對文件進(jìn)行預(yù)讀。 這將有助于減少以后的頁面錯誤阻塞。 MAP_POPULATE 僅自 Linux 2.6.23 起支持私有映射。

  • MAP_STACK:在適合進(jìn)程或線程堆棧的地址分配映射。 這個標(biāo)志目前是一個空操作,但在 glibc 線程實現(xiàn)中使用,因此如果某些架構(gòu)需要對堆棧分配進(jìn)行特殊處理,稍后可以透明地為 glibc 實現(xiàn)支持。

  • MAP_UNINITIALIZED:不要清除匿名頁面。 此標(biāo)志旨在提高嵌入式設(shè)備的性能。 僅當(dāng)內(nèi)核配置了 CONFIG_MMAP_ALLOW_UNINITIALIZED 選項時,才會使用此標(biāo)志。 由于安全隱患,該選項通常僅在嵌入式設(shè)備(即可以完全控制用戶內(nèi)存內(nèi)容的設(shè)備)上啟用。

上述標(biāo)志中,只有 MAP_FIXED 在 POSIX.1-2001 和 POSIX.1-2008 中指定。 但是,大多數(shù)系統(tǒng)也支持 MAP_ANONYMOUS(或其同義詞 MAP_ANON)。

一些系統(tǒng)記錄了附加標(biāo)志 MAP_AUTOGROWMAP_AUTORESRV、MAP_COPYMAP_LOCAL。

mmap()映射的內(nèi)存跨 fork() 保留,具有相同的屬性。

文件以頁面大小的倍數(shù)進(jìn)行映射。 對于不是頁面大小倍數(shù)的文件,剩余內(nèi)存在映射時清零,并且對該區(qū)域的寫入不會寫出到文件中。 未指定在對應(yīng)于文件的添加或刪除區(qū)域的頁面上更改映射的基礎(chǔ)文件大小的影響。

munmap()系統(tǒng)調(diào)用刪除指定地址范圍的映射,并導(dǎo)致對該范圍內(nèi)地址的進(jìn)一步引用以生成無效的內(nèi)存引用。 當(dāng)進(jìn)程終止時,該區(qū)域也會自動取消映射。 另一方面,關(guān)閉文件描述符不會取消區(qū)域映射。
地址 addr 必須是頁面大小的倍數(shù)(但長度不必是)。 包含指定范圍一部分的所有頁面都未映射,對這些頁面的后續(xù)引用將生成 SIGSEGV。 如果指示的范圍不包含任何映射頁面,這不是錯誤。

返回值與錯誤碼就不看了, mmap()系統(tǒng)調(diào)用的主要作用總結(jié)下來有這么幾個:

  • 創(chuàng)建文件映射,可以使文件的內(nèi)容讀到進(jìn)程的虛擬內(nèi)存中,可以省略傳統(tǒng)的malloc()之后再read()的過程,并且可以直接修改內(nèi)存上的數(shù)據(jù),不需要調(diào)用write()系統(tǒng)調(diào)用回寫。減少了系統(tǒng)調(diào)用的次數(shù),可以提高讀寫速度。
  • 同上一條,創(chuàng)建文件映射時可以設(shè)置為共享映射, 那么修改的內(nèi)容在其他進(jìn)程可見。
  • 映射設(shè)備的地址到進(jìn)程內(nèi)存,那么內(nèi)核與進(jìn)程的數(shù)據(jù)可以不需要常規(guī)的copy_to/from_user()接口去拷貝,實現(xiàn)內(nèi)核與進(jìn)程內(nèi)存共享的功能,減少拷貝,對一些比如USB驅(qū)動等比較有用。
  • 創(chuàng)建匿名映射,作用暫時不了解。

內(nèi)核mmap()系統(tǒng)調(diào)用的實現(xiàn)

整體mmap()流程

這里的代碼基于linux 4.0arm代碼。

我們可以在arch/arm/kernel/entry-common.S找到這個sys_mmap2的定義,還有一個sys_mmap系統(tǒng)調(diào)用,但這里看起來是沒有實現(xiàn)的,只實現(xiàn)了sys_mmap2。sys_mmapsys_mmap2的差別是off參數(shù)一個單位是字節(jié),一個單位是page

可以看到這里的宏實際是調(diào)用的sys_mmap_pgoff()

sys_mmap2.png

man 手冊中我們可以看到, 如果是匿名映射,fd=-1就行了; 否則,不是匿名映射的,需要找到 fd 對應(yīng)的 file 結(jié)構(gòu)體。sys_mmap_pgoff()一開始的地方是根據(jù)是否是匿名映射去找file結(jié)構(gòu)體,關(guān)于HUGEPAGEHUGETLB的暫時就不看了。

mmap_pgoff-1.png

判斷完后,將后續(xù)的處理交給vm_mmap_pgoff():

mmap_pgoff-2.png

vm_mmap_pgoff()獲取當(dāng)前進(jìn)程的內(nèi)存描述符,用信號量保護(hù)內(nèi)存映射的過程,映射過程交給do_mmap_pgoff()實現(xiàn):

vm_mmap_pgoff.png

do_mmap_pgoff()這里一開始主要是參數(shù)檢查。mmap()時輸入的PROT_READ參數(shù)在MNT_NOEXEC標(biāo)記的文件系統(tǒng)下會默認(rèn)增加PROT_EXEC可執(zhí)行標(biāo)記;如果帶有MAP_FIXED標(biāo)記,那么傳入的addr是不允許調(diào)整,否則可以根據(jù)情況來調(diào)整一下addr,比如調(diào)整為mmap_min_addr;對len長度進(jìn)行校驗以及向上對齊;檢查pgoff+len是否會有溢出;檢查當(dāng)前進(jìn)程mmap的數(shù)量是否超出了sysctl 的限制。

do_mmap_pgoff-1.png

通常我們mmap()傳入的地址是非0值,然后這里round_hint_to_min()就給addr調(diào)整一下,調(diào)整為mmap_min_addr,0值不調(diào)整,繼續(xù)往后面的函數(shù)get_unmapped_area()傳遞:

round_hint_to_min.png

這里通過get_unmapped_area()獲取要映射的地址;然后將prot屬性和flags標(biāo)記都轉(zhuǎn)換為虛擬內(nèi)存的標(biāo)記vm_flags;并檢測MAP_LOCKED的權(quán)限以及能否mlock。

do_mmap_pgoff-2.png

這里是文件映射的標(biāo)記檢查過程,文件映射包括共享映射和私有映射。檢查對應(yīng)的文件屬性以及prot屬性是否相匹配。

do_mmap_pgoff-3.png

非文件映射,即匿名映射,也分為共享映射和私有映射。

do_mmap_pgoff-4.png

這里將是映射內(nèi)存以及是否需要觸發(fā)預(yù)讀判定。

do_mmap_pgoff-5.png

獲取可以映射的地址

通過get_unmapped_area()獲取可以映射的內(nèi)存區(qū)域,默認(rèn)是當(dāng)前進(jìn)程的內(nèi)存管理結(jié)構(gòu)體current->mm->get_unmapped_area,如果是文件映射且對應(yīng)的操作集存在,則是file->f_op->get_unmapped_area。

get_unmapped_area.png

這里進(jìn)程默認(rèn)的mm->get_unmapped_area應(yīng)該是由arch_pick_mmap_layout()決定的,傳統(tǒng)布局,mmap低地址由低到高申請;新布局則由高到低申請;分別由arch_get_unmapped_area()arch_get_unmapped_area_topdown()實現(xiàn)。

arch_pick_mmap_layout.png

判斷使用哪個布局,mmap_is_legacy()看三個條件:如果當(dāng)前進(jìn)程的屬性是有ADDR_COMPAT_LAYOUT,或者進(jìn)程的棧大小是沒有限制的,默認(rèn)返回是傳統(tǒng)的布局;否則根據(jù)sysctl參數(shù)決定。

mmap_is_legacy.png

當(dāng)前設(shè)備下的返回參數(shù)是0,即新布局:

/ # cat /proc/sys/vm/legacy_va_layout
0

傳統(tǒng)布局

arch_get_unmapped_area()獲取可以映射的虛擬內(nèi)存地址,這里有VIPT,VIVP的優(yōu)化,通過find_vma()找到一個具體的vma

arch_get_unmapped_area-1.png
arch_get_unmapped_area-2.png

目前代碼走的是arch_get_unmapped_area_topdown(),部分代碼放下面新布局下看。

新布局

布局在arch_pick_mmap_layout()里面選,基地址mmap_base也是里面?zhèn)鬟f的。先看一個普通進(jìn)程的mmap_base地址:(有一個奇怪的地方,這里棧的起始地址start_stack減去mmap_base起始地址,居然是小于ulimit -s的8M的?棧也向下增加,mmap也是向下映射的,那如果棧大于了start_stack減去mmap_base預(yù)留的這個值,不就會==導(dǎo)致mmap的地址和棧的地址重疊==了嗎?)

b_topdown.png

重新在應(yīng)用程序下執(zhí)行mmap()操作,并在特定長度的條件下打斷點:這個時候mmap的地址和棧的地址之間的范圍就超過8M的棧大小了。前面一開始打斷點,第一次進(jìn)入sys_mmap()系統(tǒng)調(diào)用的話還是在C庫里面,準(zhǔn)備加載程序、加載動態(tài)庫的一些過程。

b_topdown_if.png

arch_get_unmapped_area_topdown()函數(shù):

arch_get_unmapped_area_topdown-1.png
arch_get_unmapped_area_topdown-2.png

find_vma()先從當(dāng)前進(jìn)程的vmacache里面找,找不到再從紅黑樹里面找,找到后更新到當(dāng)前進(jìn)程的vmacache里。

find_vma.png

vmacache_find()遍歷一下當(dāng)前進(jìn)程的vmacache里,找一個合適的vma:根據(jù)vma的起始地址和結(jié)束地址來判定。

vmacache_find.png

如果mmmap()系統(tǒng)調(diào)用傳入的addr=0,或者上面的find_vma()失敗了,那就要走vm_unmapped_area()且找一個未映射過的地址:

vm_unmapped_area.png

unmapped_area_topdown()遍歷紅黑樹找一個未映射的地址,返回gap_end低地址。

unmapped_area_topdown-1.png
unmapped_area_topdown-2.png
unmapped_area_topdown-3.png

地址范圍加入VMA紅黑樹

這里主要是檢查一下要映射的地址范圍在當(dāng)前進(jìn)程的虛擬內(nèi)存是否可以滿足需求,如果是固定映射,內(nèi)存不足時可以回收之前固定映射的虛擬地址空間,來嘗試滿足這一次的固定映射。意味著:如果第一次固定映射的長度比較長,第二次固定映射長度較短,這個時候虛擬內(nèi)存是可以滿足的,否則,可能出現(xiàn)虛擬內(nèi)存不足。在查找要插入紅黑樹的位置過程中,如果出現(xiàn)了地址重疊,需要將地址范圍取消映射后再次找出要插入的位置。取消映射失敗,那么就返回內(nèi)存不足。

mmap_region-1.png

這里嘗試與前面找到的紅黑樹節(jié)點進(jìn)行合并,合并成功的話公用一個VMA結(jié)構(gòu)體,合并不成功就要申請一個新的VMA結(jié)構(gòu)體存放這個地址范圍空間。

mmap_region-2.png

這里主要是文件映射的文件系統(tǒng)的mmap回調(diào)處理,調(diào)用具體的mmap回調(diào),比如通用的generic_file_mmap()ext4文件系統(tǒng)的ext4_file_mmap(),或者底層驅(qū)動類似mmap_mem()都可以。

mmap_region-3.png

如果是匿名共享映射,會創(chuàng)建一個臨時文件,然后賦值給VMA。將VMA加入到紅黑樹,并統(tǒng)計內(nèi)存信息。

mmap_region-4.png

將新的VMA或者經(jīng)過擴(kuò)展的VMA標(biāo)記為軟臟狀態(tài),具體后續(xù)怎么處理的,暫時不了解。

mmap_region-5.png

may_expand_vm()檢查進(jìn)程的虛擬地址空間是否會超過限制:

may_expand_vm.png

count_vma_pages_range()計算傳入的addr~end地址范圍空間與當(dāng)前進(jìn)程VMA相交的頁面數(shù)量,結(jié)合在mmap_region()里面使用的場景,可以知道,如果第一次固定映射的地址長度比較長,后續(xù)進(jìn)程虛擬地址空間不足時,可以繼續(xù)通過固定映射傳入小長度,去減少第一次固定映射所申請的虛擬地址空間。

count_vma_pages_range.png

find_vma_intersection()檢查當(dāng)前進(jìn)程的VMA是否存在與傳入的地址范圍相交的,如果有,返回對應(yīng)的VMA。這里地址范圍與VMA相交的話,要求是VMA的起始地址小于等于傳入的起始地址,VMA的結(jié)束地址大于傳入的結(jié)束地址。

find_vma_intersection.png

find_vma_links()看代碼應(yīng)該是找與addr~end相鄰的一個紅黑樹節(jié)點已經(jīng)其父節(jié)點,用于后續(xù)將addr~end返回的VMA加入到紅黑樹去。

find_vma_links.png

accountable_mapping()檢查內(nèi)存是否帶有寫屬性:

accountable_mapping.png

shmem_zero_setup()會在內(nèi)存中創(chuàng)建一個臨時文件,叫/dev/zero,然后把這個文件的一些描述符信息給到匿名共享映射的VMA。

shmem_zero_setup.png

可以從這里的圖看到:(這里的地址與mmap()返回的地址一致,但代碼中好像沒有看到用到這個地址?)

dev_zero.png

在內(nèi)核中修改名字后:

mj_test.png

用戶內(nèi)存申請--進(jìn)行缺頁異常處理

如果mmap()映射的標(biāo)志帶有VM_LOCKEDMAP_POPULATE標(biāo)志位,這里要對內(nèi)存頁面進(jìn)行填充或者鎖定。

mm_populate.png
__mm_populate-1.png
__mm_populate-2.png

__mlock_vma_pages_range()嘗試將vma范圍內(nèi)的用戶地址進(jìn)行內(nèi)存申請。

__mlock_vma_pages_range-1.png
__mlock_vma_pages_range-2.png

__get_user_pages()將對地址范圍內(nèi)的用戶內(nèi)存進(jìn)行缺頁異常,以達(dá)到申請內(nèi)存的目的。

__get_user_pages-1.png
__get_user_pages-2.png
__get_user_pages-3.png
__get_user_pages-4.png
__get_user_pages-5.png

接下來就是faultin_page()->handle_mm_fault()->__handle_mm_fault()->pud_alloc(),pmd_alloc()->handle_pte_fault()->do_fault()->...等一系列操作,等后續(xù)對內(nèi)存管理等其他代碼進(jìn)行閱讀后再回過頭來看這部分代碼吧。

當(dāng)然,如果mmap()時沒有設(shè)置VM_LOCKEDMAP_POPULATE標(biāo)志位,缺頁異常是在用戶訪問返回的內(nèi)存時觸發(fā),而不是上面的主動觸發(fā)的一個流程。

先到這里吧,后續(xù)看有需求的時候再補(bǔ)上吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容