Linux mmap()系統(tǒng)調(diào)用
mmap()系統(tǒng)調(diào)用的作用與使用
我們可以通過man mmap來查看一下mmap()的說明:

名字
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)建一個(gè)新映射。 新映射的起始地址在 addr 中指定。 length 參數(shù)指定映射的長(zhǎng)度。
如果 addr 為 NULL,則內(nèi)核選擇創(chuàng)建映射的地址; 這是創(chuàng)建新映射的最便攜方法。 如果 addr 不為 NULL,則內(nèi)核將其作為關(guān)于放置映射位置的提示; 在 Linux 上,映射將在附近的頁(yè)面邊界處創(chuàng)建。 新映射的地址作為調(diào)用結(jié)果返回。
文件映射的內(nèi)容(與匿名映射相反;請(qǐng)參閱下面的 MAP_ANONYMOUS)使用從文件描述符 fd 引用的文件(或其他對(duì)象)中的偏移偏移量開始的長(zhǎng)度字節(jié)進(jìn)行初始化。 offset 必須是 sysconf(_SC_PAGE_SIZE)返回的頁(yè)面大小的倍數(shù)。
prot 參數(shù)描述了映射所需的內(nèi)存保護(hù)(并且不得與文件的打開模式?jīng)_突)。 它是 PROT_NONE 或以下一個(gè)或多個(gè)標(biāo)志的按位或:
- PROT_EXEC:頁(yè)面帶執(zhí)行屬性。
- PROT_READ:頁(yè)面帶可讀屬性。
- PROT_WRITE:頁(yè)面帶可寫屬性。
- PROT_NONE:頁(yè)面可能不能訪問。
flags 參數(shù)確定映射的更新是否對(duì)映射同一區(qū)域的其他進(jìn)程可見,以及更新是否傳遞到底層文件。 此行為是通過在標(biāo)志中準(zhǔn)確包含以下值之一來確定的:
- MAP_SHARED:共享此映射。 映射的更新對(duì)映射此文件的其他進(jìn)程可見,并傳遞到底層文件。 (要精確控制何時(shí)將更新傳遞到底層文件需要使用 msync(2)。)
- MAP_PRIVATE:創(chuàng)建私有的寫時(shí)復(fù)制映射。 映射的更新對(duì)映射同一文件的其他進(jìn)程不可見,也不會(huì)傳遞到底層文件。 未指定在調(diào)用 mmap() 之后對(duì)文件所做的更改在映射區(qū)域中是否可見。
此外,可以在標(biāo)志中對(duì)以下零個(gè)或多個(gè)值進(jìn)行 OR 運(yùn)算:
MAP_32BIT:將映射放入進(jìn)程地址空間的前 2 GB。 對(duì)于 64 位程序,此標(biāo)志僅在 x86-64 上受支持。 添加它是為了允許在前 2GB 內(nèi)存中的某處分配線程堆棧,以提高某些早期 64 位處理器上的上下文切換性能。 現(xiàn)代 x86-64 處理器不再有這個(gè)性能問題,所以在這些系統(tǒng)上不需要使用這個(gè)標(biāo)志。 設(shè)置 MAP_FIXED 時(shí),將忽略 MAP_32BIT 標(biāo)志。
MAP_ANON:MAP_ANONYMOUS 的同義詞。 已棄用。
MAP_ANONYMOUS:映射不受任何文件的支持; 它的內(nèi)容被初始化為零。
fd和offset參數(shù)被忽略; 但是,如果指定了MAP_ANONYMOUS(或MAP_ANON),某些實(shí)現(xiàn)要求fd為-1,并且便攜式應(yīng)用程序應(yīng)確保這一點(diǎn)。MAP_ANONYMOUS與MAP_SHARED結(jié)合使用僅在 Linux 內(nèi)核 2.4 后才支持。MAP_EXECUTABLE:該標(biāo)志被忽略。
MAP_FILE:兼容性標(biāo)志。 忽略。
MAP_FIXED:不要將 addr 解釋為提示:將映射準(zhǔn)確放置在該地址處。 addr 必須是頁(yè)面大小的倍數(shù)。 如果 addr 和 len 指定的內(nèi)存區(qū)域與任何現(xiàn)有映射的頁(yè)面重疊,則現(xiàn)有映射的重疊部分將被丟棄。 如果無法使用指定的地址, mmap() 將失敗。 因?yàn)樾枰粋€(gè)固定地址的映射不太容易移植,所以不鼓勵(lì)使用這個(gè)選項(xiàng)。
MAP_GROWSDOWN:用于堆棧。 向內(nèi)核虛擬內(nèi)存系統(tǒng)指示映射應(yīng)在內(nèi)存中向下擴(kuò)展。
MAP_HUGETLB:(since Linux 2.6.32)使用“大頁(yè)面”分配映射。 有關(guān)更多信息,請(qǐng)參閱 Linux 內(nèi)核源文件 Documentation/vm/hugetlbpage.txt,以及下面的 NOTES。
-
MAP_HUGE_2MB, MAP_HUGE_1GB:與
MAP_HUGETLB結(jié)合使用以在支持多個(gè)Hugetlb頁(yè)面大小的系統(tǒng)上選擇替代的Hugetlb頁(yè)面大?。ǚ謩e為2 MB和1 GB)。
更一般地,可以通過在偏移MAP_HUGE_SHIFT處的六位中編碼所需頁(yè)面大小的以 2 為底的對(duì)數(shù)來配置所需的大頁(yè)面大小。 (該位域中的零值提供了默認(rèn)的大頁(yè)面大??;可以通過/proc/meminfo公開的Hugepagesize字段發(fā)現(xiàn)默認(rèn)大頁(yè)面大小。)因此,上述兩個(gè)常量定義為:#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)支持的大頁(yè)面大小范圍。 MAP_LOCKED:(since Linux 2.5.37)以與
mlock()相同的方式標(biāo)記要鎖定的mmaped區(qū)域。 此實(shí)現(xiàn)將嘗試填充(預(yù)錯(cuò))整個(gè)范圍,但如果失敗,則mmap調(diào)用不會(huì)因ENOMEM而失敗。 因此,稍后可能會(huì)發(fā)生重大故障。 所以語(yǔ)義不如mlock()強(qiáng)。 當(dāng)映射初始化后無法接受主要故障時(shí),應(yīng)使用mmap()加mlock()。MAP_LOCKED標(biāo)志在較舊的內(nèi)核中被忽略。MAP_NONBLOCK:僅與
MAP_POPULATE結(jié)合使用才有意義。 不要執(zhí)行預(yù)讀:只為RAM中已經(jīng)存在的頁(yè)創(chuàng)建頁(yè)表?xiàng)l目。 從 Linux 2.6.23 開始,這個(gè)標(biāo)志會(huì)導(dǎo)致MAP_POPULATE什么也不做。 有一天,可能會(huì)重新實(shí)現(xiàn)MAP_POPULATE和MAP_NONBLOCK的組合。MAP_NORESERVE:不要為此映射保留交換空間。 保留交換空間時(shí),可以保證可以修改映射。 當(dāng)沒有保留交換空間時(shí),如果沒有可用的物理內(nèi)存,則可能會(huì)在寫入時(shí)獲得
SIGSEGV。 另請(qǐng)參閱 proc(5) 中對(duì)文件/proc/sys/vm/overcommit_memory的討論。 在 2.6 之前的內(nèi)核中,此標(biāo)志僅對(duì)私有可寫映射有效。MAP_POPULATE:(since Linux 2.5.46)為映射填充頁(yè)表(prefault,預(yù)先觸發(fā)page fault)。 對(duì)于文件映射,這會(huì)導(dǎo)致對(duì)文件進(jìn)行預(yù)讀。 這將有助于減少以后的頁(yè)面錯(cuò)誤阻塞。
MAP_POPULATE僅自 Linux 2.6.23 起支持私有映射。MAP_STACK:在適合進(jìn)程或線程堆棧的地址分配映射。 這個(gè)標(biāo)志目前是一個(gè)空操作,但在 glibc 線程實(shí)現(xiàn)中使用,因此如果某些架構(gòu)需要對(duì)堆棧分配進(jìn)行特殊處理,稍后可以透明地為 glibc 實(shí)現(xiàn)支持。
MAP_UNINITIALIZED:不要清除匿名頁(yè)面。 此標(biāo)志旨在提高嵌入式設(shè)備的性能。 僅當(dāng)內(nèi)核配置了 CONFIG_MMAP_ALLOW_UNINITIALIZED 選項(xiàng)時(shí),才會(huì)使用此標(biāo)志。 由于安全隱患,該選項(xiàng)通常僅在嵌入式設(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_AUTOGROW、MAP_AUTORESRV、MAP_COPY 和 MAP_LOCAL。
由 mmap()映射的內(nèi)存跨 fork() 保留,具有相同的屬性。
文件以頁(yè)面大小的倍數(shù)進(jìn)行映射。 對(duì)于不是頁(yè)面大小倍數(shù)的文件,剩余內(nèi)存在映射時(shí)清零,并且對(duì)該區(qū)域的寫入不會(huì)寫出到文件中。 未指定在對(duì)應(yīng)于文件的添加或刪除區(qū)域的頁(yè)面上更改映射的基礎(chǔ)文件大小的影響。
munmap()系統(tǒng)調(diào)用刪除指定地址范圍的映射,并導(dǎo)致對(duì)該范圍內(nèi)地址的進(jìn)一步引用以生成無效的內(nèi)存引用。 當(dāng)進(jìn)程終止時(shí),該區(qū)域也會(huì)自動(dòng)取消映射。 另一方面,關(guān)閉文件描述符不會(huì)取消區(qū)域映射。
地址 addr 必須是頁(yè)面大小的倍數(shù)(但長(zhǎng)度不必是)。 包含指定范圍一部分的所有頁(yè)面都未映射,對(duì)這些頁(yè)面的后續(xù)引用將生成 SIGSEGV。 如果指示的范圍不包含任何映射頁(yè)面,這不是錯(cuò)誤。
返回值與錯(cuò)誤碼就不看了, mmap()系統(tǒng)調(diào)用的主要作用總結(jié)下來有這么幾個(gè):
- 創(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í)可以設(shè)置為共享映射, 那么修改的內(nèi)容在其他進(jìn)程可見。
- 映射設(shè)備的地址到進(jìn)程內(nèi)存,那么內(nèi)核與進(jìn)程的數(shù)據(jù)可以不需要常規(guī)的
copy_to/from_user()接口去拷貝,實(shí)現(xiàn)內(nèi)核與進(jìn)程內(nèi)存共享的功能,減少拷貝,對(duì)一些比如USB驅(qū)動(dòng)等比較有用。 - 創(chuàng)建匿名映射,作用暫時(shí)不了解。
內(nèi)核mmap()系統(tǒng)調(diào)用的實(shí)現(xiàn)
整體mmap()流程
這里的代碼基于linux 4.0的arm代碼。
我們可以在arch/arm/kernel/entry-common.S找到這個(gè)sys_mmap2的定義,還有一個(gè)sys_mmap系統(tǒng)調(diào)用,但這里看起來是沒有實(shí)現(xiàn)的,只實(shí)現(xiàn)了sys_mmap2。sys_mmap與sys_mmap2的差別是off參數(shù)一個(gè)單位是字節(jié),一個(gè)單位是page。
可以看到這里的宏實(shí)際是調(diào)用的sys_mmap_pgoff():

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

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

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

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

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

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

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

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

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

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

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

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

當(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()找到一個(gè)具體的vma。


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

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

看arch_get_unmapped_area_topdown()函數(shù):


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

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

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

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



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

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

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

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

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

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

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

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

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

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

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

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

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

用戶內(nèi)存申請(qǐng)--進(jìn)行缺頁(yè)異常處理
如果mmap()映射的標(biāo)志帶有VM_LOCKED或MAP_POPULATE標(biāo)志位,這里要對(duì)內(nèi)存頁(yè)面進(jìn)行填充或者鎖定。



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


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





接下來就是faultin_page()->handle_mm_fault()->__handle_mm_fault()->pud_alloc(),pmd_alloc()->handle_pte_fault()->do_fault()->...等一系列操作,等后續(xù)對(duì)內(nèi)存管理等其他代碼進(jìn)行閱讀后再回過頭來看這部分代碼吧。
當(dāng)然,如果mmap()時(shí)沒有設(shè)置VM_LOCKED或MAP_POPULATE標(biāo)志位,缺頁(yè)異常是在用戶訪問返回的內(nèi)存時(shí)觸發(fā),而不是上面的主動(dòng)觸發(fā)的一個(gè)流程。
先到這里吧,后續(xù)看有需求的時(shí)候再補(bǔ)上吧。