概述
mmap() 系統(tǒng)調(diào)用在調(diào)用進(jìn)程的虛擬地址空間中創(chuàng)建一個(gè)新的內(nèi)存映射,映射分為兩種:
文件映射:將一個(gè)文件的一部分直接映射到調(diào)用進(jìn)程的虛擬內(nèi)存中,一旦一個(gè)文件被映射之后,就可以通過在相應(yīng)的內(nèi)存區(qū)域中操作字節(jié)來訪問文件內(nèi)容,映射的分頁會在需要的時(shí)候從文件中自動加載,這種映射也稱為基于文件的映射或內(nèi)存映射文件
匿名映射:沒有對應(yīng)的文件,相反,這種映射的分頁會被初始化為 0
一個(gè)進(jìn)程的映射中的內(nèi)存可以與其他進(jìn)程中的映射共享(即各個(gè)子進(jìn)程的頁表?xiàng)l目指向 RAM 中相同的分頁)。這種情況會存在2種情況發(fā)生:
當(dāng)兩個(gè)進(jìn)程映射了一個(gè)文件的同一個(gè)區(qū)域時(shí),它們會共享物理內(nèi)存的相同分頁
通過 fork() 創(chuàng)建的子進(jìn)程會繼承父進(jìn)程的映射的副本,并且這些映射所引用的物理內(nèi)存分頁與父進(jìn)程中相應(yīng)映射所引用的分頁相同
當(dāng)兩個(gè)或者多個(gè)進(jìn)程共享相同分頁時(shí),每個(gè)進(jìn)程都有可能看到其他進(jìn)程對分頁內(nèi)容作出的改變,這當(dāng)然要取決于映射是私有的還是共享的:
私有映射(MAP_PRIVATE) :在映射內(nèi)容上發(fā)生的變更對其他進(jìn)程是不可見的,對于文件映射來說,變更將不會在底層文件上進(jìn)行。盡管一個(gè)私有映射的分頁在上面介紹的情況中, 初始時(shí)是共享的,但對映射內(nèi)容作出的變更對各個(gè)進(jìn)程來講是私有的。內(nèi)核使用了寫時(shí)復(fù)制技術(shù)完成了這個(gè)任務(wù)。這意味著,當(dāng)一個(gè)進(jìn)程試圖修改一個(gè)分頁 的內(nèi)容時(shí),內(nèi)核首先會為該進(jìn)程創(chuàng)建一個(gè)新分頁,并將需要修改的分頁中的內(nèi)容復(fù)制到新分頁中(以及調(diào)整進(jìn)程的頁表)。正因?yàn)檫@個(gè)原因,MAP_PRIVATE 映射會被稱為私有,寫時(shí)復(fù)制映射
共享映射(MAP_SHARED):在映射內(nèi)容上發(fā)生的變更對所有共享同一個(gè)映射的其他進(jìn)程都是可見的,對于文件映射來說,變更將會發(fā)生在底層文件上
各種映射的用途:
私有文件映射:映射的內(nèi)容被初始化為一個(gè)而文件區(qū)域的內(nèi)容。多個(gè)映射同一個(gè)文件的進(jìn)程初始時(shí)會共享同樣的內(nèi)存物理分頁,但系統(tǒng)使用寫時(shí)復(fù)制技術(shù)使得一個(gè)進(jìn)程對映射所做的變更對其他進(jìn)程不可見。這種映射的主要用途是使用一個(gè)文件的內(nèi)容來來初始化一塊內(nèi)存區(qū)域。一些常見的例子包括根據(jù)二進(jìn)制可執(zhí)行文件或共享文件的相應(yīng)部分來初始化一個(gè)進(jìn)程的文件和數(shù)據(jù)段
私有匿名映射:每次調(diào)用 mmap() 創(chuàng)建一個(gè)私有映射時(shí)都會產(chǎn)生一個(gè)新映射,該映射與同一進(jìn)程創(chuàng)建的其他匿名映射是不同的(即不會共享物理分頁)。盡管子進(jìn)程會繼承父進(jìn)程的映射,但寫時(shí)復(fù)制語義確保了 fork() 之后父進(jìn)程和子進(jìn)程不會看到其他進(jìn)程對映射所做的變更。匿名映射的主要用途是為一個(gè)進(jìn)程分配新(用0填充)的內(nèi)存(如在分配大塊內(nèi)存時(shí)用 malloc() 會為此使用 mmap())
共享文件映射:所有映射一個(gè)文件同一區(qū)域的進(jìn)程會共享同樣的內(nèi)存物理分頁,這些分頁的內(nèi)容將被初始化為該文件區(qū)域。對映射內(nèi)容的修改將直接在文件中進(jìn)行。這種映射主要用于2個(gè)用途:
允許內(nèi)存映射 IO, 這表示一個(gè)文件會被加載到進(jìn)程的虛擬內(nèi)存中的一個(gè)區(qū)域中并且對該區(qū)域的變更會自動被寫入到這個(gè)文件中。因此,內(nèi)存映射 IO 為使用 read(),write() 來執(zhí)行文件 IO 這種做法提供了一種替代方案。
允許無關(guān)進(jìn)程共享一塊內(nèi)容以便以一種類似于 System V 共享內(nèi)存段的方式來執(zhí)行(快速) IPC
共享匿名映射:與私有匿名映射一樣,每次調(diào)用 mmap() 創(chuàng)建一個(gè)共享匿名映射時(shí)都會產(chǎn)生一個(gè)新的,與任何其他映射不共享分頁的截然不同的映射。這里的區(qū)別是映射的分頁不會被寫時(shí)復(fù)制。這意味著, 當(dāng)一個(gè)子進(jìn)程在 fork() 之后繼承映射,父進(jìn)程和子進(jìn)程共享同一的 RAM 分頁,并且一個(gè)進(jìn)程對映射內(nèi)容所作出的變更會對其他進(jìn)程可見。共享匿名映射允許以一種類似于 System V 共享內(nèi)存段的方式來進(jìn)行 IPC,但只有相關(guān)進(jìn)程可以這么做
一個(gè)進(jìn)程在執(zhí)行 exec() 后映射會丟失,但通過 fork() 創(chuàng)建的子進(jìn)程會繼承映射,映射類型(MAP_PRIVATE 和 MAP_SHARED) 也會被繼承。通過 Linux 下特有的 /proc/PID/maps 文件能夠查看與一個(gè)進(jìn)程的映射有關(guān)的所有信息。
創(chuàng)建一個(gè)映射
#includevoid*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);
addr建立映射區(qū)的首地址,由 Linux 內(nèi)核指定。用戶程序調(diào)用時(shí)直接傳遞NULL,那么內(nèi)核會為映射選擇一個(gè)合適地址,如果不是NULL,內(nèi)核會再選擇將映射放置在何處時(shí)將這個(gè)參數(shù)值作為一個(gè)提示信息來處理
成功時(shí)返回創(chuàng)建的映射區(qū)首地址,失敗時(shí)返回MAP_FAILED
length指定了映射的字節(jié)數(shù),盡管length無需是一個(gè)系統(tǒng)分頁大小的倍數(shù),但是內(nèi)核會以分頁大小為單位來創(chuàng)建映射,因此,實(shí)際上lenght會被向上提升為分頁大小的下一個(gè)倍數(shù)
prot映射區(qū)的權(quán)限:
flags` 標(biāo)志位參數(shù),常用于設(shè)定更新物理區(qū)域、設(shè)置共享、創(chuàng)建匿名映射區(qū):
MAP_SHARED:創(chuàng)建一個(gè)共享映射,映射區(qū)所做的修改會反映到物理設(shè)備(磁盤)上
MAP_PRIVATE:創(chuàng)建一個(gè)私有映射,映射區(qū)所做的修改不會反映到物理設(shè)備上
fd是用來建立映射區(qū)的文件描述符。
offset映射文件的偏移量,它必須是系統(tǒng)分頁大小的整數(shù)倍,可以映射整個(gè)文件,也可以只映射一部分內(nèi)容
有關(guān)內(nèi)存保護(hù)的更多細(xì)節(jié)
如果一個(gè)進(jìn)程在訪問一個(gè)內(nèi)存區(qū)域時(shí)違反了該區(qū)域上的保護(hù)位,那么內(nèi)核會向該進(jìn)程發(fā)送一個(gè)SIG_SEGV信號。
標(biāo)記為PROT_NONE的分頁內(nèi)存的一個(gè)用途是作為一個(gè)進(jìn)程分配的內(nèi)存區(qū)域的起始位置或結(jié)束位置的守護(hù)分頁,如果進(jìn)程意外地訪問了標(biāo)記為PROT_NONE的分頁,那么內(nèi)核會通過一個(gè)SIGSEGV信號來通知該進(jìn)程。
內(nèi)存保護(hù)信息駐留在進(jìn)程的私有虛擬內(nèi)存表中,因此不同的進(jìn)程可能會使用不同的保護(hù)位來映射同一個(gè)內(nèi)存區(qū)域。
標(biāo)準(zhǔn)中規(guī)定的對offset和addr的對齊約束
SUSv3 規(guī)定mmap()的offset參數(shù)必須要與分頁對齊,而addr參數(shù)在指定了MAP_FIXED的情況下也必須要與分頁對齊。
SUSV4 放寬了這些要求:
一個(gè)實(shí)現(xiàn)可能會要求offset為系統(tǒng)分頁大小的倍數(shù)
如果指定了MAP_FIXED,那么一個(gè)實(shí)現(xiàn)可能會要求addr是分頁對齊的
如果指定了MAP_FIXED,并且addr為非零值,那么addr和offset除以系統(tǒng)分頁大小所得的余數(shù)應(yīng)該相等
解除映射區(qū)域
#includeintmunmap(void*addr,size_tlength);
munmap()與mmap()` 操作相反,即從調(diào)用進(jìn)程的虛擬地址空間中刪除一個(gè)映射
addr參數(shù)是待解除映射的地址范圍的起始地址,它必須與一個(gè)分頁邊界對齊
length參數(shù)是一個(gè)非負(fù)整數(shù),它指定了待解除映射區(qū)域的大小,范圍為系統(tǒng)分頁大小的下一個(gè)倍數(shù)的地址空間將會解除映射
通常會解除整個(gè)映射,因此將addr指定為上一個(gè)mmap()調(diào)用返回的地址,并且length的值與mmap()調(diào)用中使用的length的值一樣。也可以解除一個(gè)映射中的部分映射,這樣原來的映射要么會收縮,要么會被分為兩個(gè),這取決于在何處開始解除映射。還可以指定一個(gè)跨越多個(gè)映射的地址范圍,這樣的話所有范圍內(nèi)的映射都會被解除
如果在addr和length指定的地址范圍中不存在映射,那么munmap()將不起任何作用并返回 0
在解除映射期間,內(nèi)核會刪除進(jìn)程持有的在指定地址范圍內(nèi)的所有內(nèi)存鎖
當(dāng)一個(gè)進(jìn)程終止或執(zhí)行了一個(gè)exec()之后進(jìn)程中所有的映射會自動解除
為確保一個(gè)共享文件映射的內(nèi)容會被寫入到底層文件中,在使用munmap()解除一個(gè)映射之前需要調(diào)用msync()
文件映射
創(chuàng)建一個(gè)文件映射的步驟:
獲取一個(gè)文件的描述符,通常通過調(diào)用open()來獲取
將文件描述符作為fd參數(shù),傳入mmap()調(diào)用中
執(zhí)行上面操作之后,mmap()會將打開的文件的內(nèi)容映射到調(diào)用進(jìn)程的地址空間。一旦mmap()被調(diào)用之后就能夠關(guān)閉文件描述符了,而不會對映射產(chǎn)生任何影響。
除了普通的磁盤文件,使用mmap()還能夠映射各種真實(shí)和虛擬設(shè)備的內(nèi)容,如硬盤,光盤以及/dev/mem。
在打開描述符fd引用的文件時(shí)必須要具備與port和flags參數(shù)值匹配的權(quán)限。
offset參數(shù)指定了從文件區(qū)域中的哪個(gè)字節(jié)開始映射,它必須是系統(tǒng)分頁大小的倍數(shù),將offset指定為 0 會導(dǎo)致從文件的起始位置開始映射,length參數(shù)指定了映射的字節(jié)數(shù)。
私有文件映射
私有文件映射用途:
允許多個(gè)執(zhí)行同一程序或者使用同一個(gè)共享庫的進(jìn)程共享同樣(只讀的)文本段,它是從底層可執(zhí)行文件或庫文件的相應(yīng)部分映射而來的
映射一個(gè)可執(zhí)行文件或共享庫的初始化數(shù)據(jù)段。這種映射會被處理成私有,使得對映射數(shù)據(jù)段內(nèi)容的變更不會發(fā)生在底層文件上
mmap()的這兩種用法通常對程序是不可見的,因?yàn)檫@些映射是由程序加載器和動態(tài)鏈接器創(chuàng)建的,可以在/proc/PID/maps中發(fā)現(xiàn)這兩種映射。
共享文件映射
當(dāng)多個(gè)進(jìn)程創(chuàng)建了同一個(gè)文件區(qū)域的共享映射時(shí),它們會共享同樣的內(nèi)存物理分頁。此外,對映射內(nèi)容的變更將會反應(yīng)到文件上。共享文件映射有兩個(gè)用途:內(nèi)存映射 IO 和 IPC。
內(nèi)存映射 IO
由于共享文件映射中的內(nèi)容是從文件初始化而來,并且對映射內(nèi)容所做的變更都會自動反應(yīng)到文件上,因此可以簡單的通過訪問內(nèi)存中的字節(jié)來執(zhí)行文件 IO,而 依靠內(nèi)核來確保對內(nèi)存的變更會被傳遞到映射文件上。(一般來說,一個(gè)程序會定義一個(gè)結(jié)構(gòu)化數(shù)據(jù)類型來與磁盤文件中的內(nèi)容對應(yīng)起來,然后使用該數(shù)據(jù)類型來轉(zhuǎn)換映射的內(nèi)容)這項(xiàng)技術(shù)被稱為內(nèi)存映射 IO,它是使用read()和write()來訪問文件內(nèi)容這種方法的替代方案。
內(nèi)存映射 IO 具備兩個(gè)潛在的優(yōu)勢:
使用內(nèi)存訪問來代替read()和write()系統(tǒng)調(diào)用能夠簡化一些應(yīng)用程序的邏輯
在一些情況下,它能夠比使用傳統(tǒng)的 IO 系統(tǒng)調(diào)用執(zhí)行文件 IO 這種做法提供更好的性能
內(nèi)存映射 IO 之所以能夠帶來性能的優(yōu)勢的原因如下:
正常的read()或者write()需要兩次傳輸,一次是在文件和內(nèi)核高速緩沖區(qū)之間,另一次是在高速緩沖區(qū)和用戶空間緩沖區(qū)之間,使用mmap()就無需第二次傳輸:
對于輸入:一旦內(nèi)核將相應(yīng)的文件塊映射進(jìn)內(nèi)存之后用戶進(jìn)程就能夠使用這些數(shù)據(jù)
對于輸出:用戶進(jìn)程僅僅需要修改內(nèi)存中的內(nèi)容,然后可以依靠內(nèi)核內(nèi)存管理器來自動更新底層的文件
mmap()還能夠通過減少所需使用的內(nèi)存來提升性能,當(dāng)使用read()或write()時(shí),數(shù)據(jù)將被保存在兩個(gè)緩沖區(qū)中:一個(gè)位于用戶空間,另一個(gè)位于內(nèi)核空間,當(dāng)使用mmap()時(shí),內(nèi)核空間和用戶空間會共享同一個(gè)緩沖區(qū),此外,如果多個(gè)進(jìn)程正在在同一個(gè)文件上執(zhí)行 IO,那么它們通過使用mmap()就能夠共享同一個(gè)內(nèi)核緩沖區(qū),從而又能夠節(jié)省內(nèi)存的消耗
內(nèi)存映射 IO 所帶來的性能優(yōu)勢在大型文件執(zhí)行重復(fù)隨機(jī)訪問時(shí)最有可能提現(xiàn)出來。對于小數(shù)據(jù)量 IO 來講,內(nèi)存映射 IO 的開銷實(shí)際上要比簡單的read()或write()大。
使用共享文件映射的 IPC
由于所有使用同樣文件區(qū)域的共享映射的進(jìn)程共享同樣的內(nèi)存物理分頁,因此共享文件映射的第二個(gè)用途就是作為一種 IPC 方法。這種共享內(nèi)存區(qū)域與 System V 共享內(nèi)存對象之間的區(qū)別在于,區(qū)域上的內(nèi)容的更變會反應(yīng)到底層的映射文件上。這種特性對于那些需要共享內(nèi)容在應(yīng)用程序或系統(tǒng)重啟時(shí)能夠持久化的應(yīng)用程序來說是非常有用的。
邊界情況
在很多情況下,一個(gè)映射的大小是系統(tǒng)分頁大小的整數(shù)倍,并且映射會完全落入映射文件的范圍之內(nèi),但這個(gè)要求不是必須的。
映射完全落入映射文件的范圍之內(nèi)但區(qū)域大小并不是系統(tǒng)分頁大小的一個(gè)整數(shù)倍的情況,假設(shè)分頁大小為 4096 字節(jié):
由于映射大小不是系統(tǒng)分頁大小的整數(shù)倍,因此它會被向上舍入到系統(tǒng)分頁大小的下一個(gè)整數(shù)倍。當(dāng)映射擴(kuò)充過了底層文件的結(jié)尾處時(shí),情況更加復(fù)雜:
雖然向上舍入的字節(jié)是可以訪問的,但它們不會被映射到底層文件上,這部分會被初始化為0。
通過擴(kuò)展文件的大小(如使用ftruncate()或write()) 可以使得這種映射中之前不可訪問的部分變得可用。
內(nèi)存保護(hù)和文件訪問模式交互
一般從原則來講:
PROT_READ和PROT_EXEC保護(hù)要求被映射的文件使用O_RDONLY或O_RDWR打開
PROT_WRITE保護(hù)要求被映射的文件使用O_WRONLY或O_RDWR打開
但是,由于一些硬件架構(gòu)提供的內(nèi)存保護(hù)粒度有限,因此情況會變得復(fù)雜:
所有內(nèi)存保護(hù)組合與使用O_RDWR標(biāo)記打開文件是兼容的
沒有內(nèi)存保護(hù)組合
使用O_RDONLY標(biāo)記打開一個(gè)文件的結(jié)果依賴于在調(diào)用mmap()時(shí)是否指定了可以指定任意的內(nèi)存保護(hù)組合,因?yàn)閷τ谝粋€(gè)MAP_PRIVATE分頁內(nèi)容做出的變更不會被寫入到文件中
同步映射區(qū)域
內(nèi)核會自動將發(fā)生在MAP_SHARED映射內(nèi)容上的變更寫入到底層文件中的,但默認(rèn)情況下,內(nèi)核不保證這種同步操作會在何時(shí)發(fā)生。
#includeintmsync(void*addr,size_tlength,intflags);
msync()系統(tǒng)調(diào)用讓應(yīng)用程序能夠顯式地控制何時(shí)完成共享映射與映射文件之間的同步
addr和length指定了需同步的內(nèi)存區(qū)域的起始地址和大小
flags參數(shù)的可取值:
MS_SYNC:執(zhí)行一個(gè)同步的文件寫入。這個(gè)調(diào)用會阻塞直到內(nèi)存區(qū)域中所有被修改過的分頁被寫入底層磁盤為止
MS_ASYNC:執(zhí)行一個(gè)異步寫入。內(nèi)存區(qū)域中被修改的分頁會在后面某個(gè)時(shí)刻被寫入磁盤并立即在相應(yīng)文件區(qū)域中執(zhí)行read()的其他進(jìn)程可見
另外一種區(qū)分這兩個(gè)值的方式可以表述為,在MS_SYNC操作之后,內(nèi)存區(qū)域會與磁盤同步,而在MS_ASYNC之后,內(nèi)存區(qū)域僅僅是與內(nèi)核高速緩沖區(qū)同步。
flags參數(shù)還可以加上下面這個(gè)值:
MS_INVALIDATE:使映射數(shù)據(jù)的緩存副本失效,當(dāng)內(nèi)存區(qū)域中所有被修改過的分頁被同步到文件中之后,內(nèi)存區(qū)域中所有與底層文件不一致的分頁會被標(biāo)記為無效,當(dāng)下次引用這些分頁時(shí)會從文件的相應(yīng)位置處復(fù)制相應(yīng)的分頁內(nèi)容,其結(jié)果是其他進(jìn)程對文件做出的所有更新將會在內(nèi)存區(qū)域中可見
其他mmap()標(biāo)記
mmap()的flags參數(shù)的位掩碼值:
MAP_ANONYMOUS:創(chuàng)建一個(gè)匿名映射,即沒有底層文件對應(yīng)的映射
MAP_HUGETLB:與SHM_HUGETLB標(biāo)記在 System V 共享內(nèi)存段中所起的作用一樣
MAP_LOCKED:按照mlock()的方式預(yù)加載映射分頁并將映射分頁鎖進(jìn)內(nèi)存
MAP_NORESERVE:這個(gè)標(biāo)記用來控制是否提前為映射的交換空間執(zhí)行預(yù)留操作
MAP_POPULATE:填充一個(gè)映射的分頁,對于文件映射來將,這將會在文件上執(zhí)行一個(gè)超前讀取
MAP_UNINITIALIZED:指定一個(gè)標(biāo)記會防止一個(gè)匿名映射被清零,它能夠帶來性能上的提升,但同時(shí)也帶來了安全風(fēng)險(xiǎn),因?yàn)橐逊峙涞姆猪撝锌赡軙弦粋€(gè)進(jìn)程留下來的敏感信息
匿名映射
匿名映射是沒有對應(yīng)文件的一種映射。
MAP_ANONYMOUS和/dev/zero
在Linux 中,使用mmap()創(chuàng)建匿名映射存在2種不同但等級的方法:
在flags中指定MS_ANONYMOUS并將fd指定為 -1
打開/dev/zero設(shè)備文件,并將得到的文件描述符傳遞給mmap()
不管使用哪種方法,得到映射中的字節(jié)會被初始化為 0。offset參數(shù)都將會被忽略,因?yàn)闆]有底層文件,所以也無法指定偏移量。
MAP_PRIVATE匿名映射
MAP_PRIVATE匿名映射用來分配進(jìn)程私有的內(nèi)存塊并將其中的內(nèi)容初始化為 0。
下面的代碼使用/dev/zero技術(shù)創(chuàng)建一個(gè)MAP_PRIVATE匿名映射:
fd=open("/dev/zero",O_RDWR); if(fd==-1) errExit("open"); addr=mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0); if(addr==MAP_FAILED) errExit("mmap");
MAP_SHARED匿名映射
MAP_SHARED匿名映射允許相關(guān)進(jìn)程共享一塊內(nèi)存區(qū)域而無需一個(gè)對應(yīng)的映射文件。
使用MAP_ANONYMOUS技術(shù)創(chuàng)建一個(gè)MAP_SHARED匿名映射:
addr=mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,-1,0); if(addr==MAP_FAILED) errExit("mmap");
重新映射一個(gè)映射區(qū)域
UNIX 實(shí)現(xiàn)上一旦映射被創(chuàng)建,其位置和大小就無法改變了。Linux 提供mremap()系統(tǒng)調(diào)用可以執(zhí)行此類變更。
#define_GNU_SOURCE #includevoid*mremap(void*old_address,size_told_size,size_tnew_size,intflags,.../*void*new_address*/);
old_address和old_size指定了需要擴(kuò)展或收縮的既有映射的位置和大小
在執(zhí)行重映射的過程中內(nèi)核可能會為映射進(jìn)程的虛擬地址空間中重新指定一個(gè)位置,是否允許這種行為由flag參數(shù)來控制:
MREMAP_MAYMOVE:內(nèi)核可能會為映射在進(jìn)程的虛擬地址空間中重新指定一個(gè)位置,如果沒有指定這個(gè)標(biāo)記,并且在當(dāng)前位置處沒有足夠的空間來擴(kuò)展這個(gè)映射,那么就返回ENOMEM錯誤
MREMAP_FIXED:只能和MREMAP_MAYMOVE一起使用,如果指定了這個(gè)標(biāo)記,那么mremap()會接收一個(gè)額外的參數(shù)void *new_address,該參數(shù)指定了一個(gè)分頁對齊的地址,并且映射將會被遷移至該地址處
mremap()成功時(shí)返回映射的起始地址
MAP_NORESERVE和過度利用交換空間
一些應(yīng)用程序會創(chuàng)建大的映射,但之用映射區(qū)域中的一小部分。如果內(nèi)核總是為此類映射分配(或者預(yù)留)足夠的交換空間,那么很多交換空間可能會被浪費(fèi)。相反,內(nèi)核可以只在需要的時(shí)候用到映射分頁的時(shí)候(即當(dāng)應(yīng)用程序訪問分頁時(shí))為它們預(yù)留交換空間,這種方法稱為懶交換預(yù)留。它的一個(gè)優(yōu)點(diǎn)是,應(yīng)用程序總共使用的虛擬內(nèi)存量能夠超過 RAM 加上交換空間的總量。
懶交換預(yù)留允許交換空間被過度利用,這種方式能夠很好地工作,只要所有進(jìn)程都不試圖訪問整個(gè)映射,但如果所有應(yīng)用程序都試圖訪問整個(gè)映射,那么 RAM 和交換空間就會被耗盡。在這種情況下,內(nèi)核會通過殺死系統(tǒng)中的一個(gè)或多個(gè)進(jìn)程來降低內(nèi)存壓力。
內(nèi)核如何處理交換空間的預(yù)留是由調(diào)用mmap()時(shí)是否使用了MAP_NORESERVE標(biāo)記以及影響系統(tǒng)層面的交換空間過度利用操作的/proc接口來控制的:
Linux 特有的/proc/sys/vm/overcommit_memory文件包含了一個(gè)整數(shù)值,它控制著內(nèi)核對交換空間過度利用的處理。
過度利用監(jiān)控只適用于下面的這些映射:
私有可寫映射,這種映射的交換開銷等于所有使用該映射的進(jìn)程為該映射所分配的空間總和
共享匿名映射,這種映射的交換開銷等于映射的大小
為只讀私有映射預(yù)留交換空間是沒有必要的,因?yàn)橛成渲械膬?nèi)容是不可變更的,從而無需使用交換空間,共享文件映射也不需要使用交換空間,因?yàn)橛成湮募旧頁?dān)當(dāng)了映射的交換空間。
當(dāng)一個(gè)子進(jìn)程在fork()調(diào)用中繼承了一個(gè)映射時(shí),它將會繼承該映射的MAP_NORESERVER設(shè)置。
OOM 殺手
內(nèi)核中用來在內(nèi)存被耗盡時(shí)選擇殺死哪個(gè)進(jìn)程的代碼通常被稱為out-of-memory(OOM) 殺手,OOM 殺手會嘗試選擇殺死能夠緩解內(nèi)存消耗情況的最佳進(jìn)程,這里的 “最佳” 是由一組因素決定的。如一個(gè)進(jìn)程消耗的內(nèi)存越多,它越可能成為 OOM 殺手選擇的目標(biāo)。內(nèi)核一般不會殺死下列進(jìn)程:
特權(quán)進(jìn)程,因?yàn)樗鼈兛赡苷趫?zhí)行重要的任務(wù)
正在訪問裸設(shè)備的進(jìn)程,因?yàn)闅⑺浪鼈兛赡軙?dǎo)致設(shè)備處理一個(gè)不可用的狀態(tài)
已經(jīng)運(yùn)行了很長時(shí)間或已經(jīng)消耗了大量 CPU 的進(jìn)程,因?yàn)闅⑺浪鼈兛赡軙?dǎo)致丟失很多 "工作"
為殺死一個(gè)被選中的進(jìn)程,OOM 殺手會向其發(fā)送一個(gè)SIGKILL信號。
Linux 特有的/proc/PID/oom_score文件給出了在需要調(diào)用 OOM 殺手時(shí)內(nèi)核賦予給每個(gè)進(jìn)程的權(quán)重。在這個(gè)文件中,進(jìn)程的權(quán)重越大,那么在必要的時(shí)候被 OOM 殺手選中的可能性就越大。
Linux 特有的/proc/PID/oom_adj文件能夠用來影響一個(gè)進(jìn)程的oom_score值,這個(gè)文件可以被設(shè)置成[-16,15]之間的任意一個(gè)值,其中負(fù)數(shù)會減小oom_score的值,而整數(shù)則會增大oom_score的值,-17會完全將進(jìn)程從 OOM 殺手的候選目標(biāo)中刪除。
MAP_FIXED標(biāo)記
在mmap()中的flags參數(shù)指定MAP_FIXED標(biāo)記會強(qiáng)制內(nèi)核原樣地解釋addr中的地址,而不是只將其作為一種提示信息。如果指定了MAP_FIXED,那么addr就必須是分頁對齊的。
一般在一個(gè)可移植的應(yīng)用程序不應(yīng)該使用MAP_FIXED,并且需要將addr指定為NULL。
如果在調(diào)用mmap()時(shí)指定了MAX_FIXED,并且內(nèi)存區(qū)域的起始位置為addr,覆蓋的 length 字節(jié)與之前的映射分頁重疊了,那么重疊的分頁會被新映射替換,使用這個(gè)特性可以可移植地將一個(gè)文件或者多個(gè)文件的多個(gè)部分映射進(jìn)一塊連續(xù)的內(nèi)存區(qū)域:
使用mmap()創(chuàng)建一個(gè)匿名映射,addr指定為NULL,并且不指定MAP_FIXED,這樣就允許內(nèi)核為映射選擇一個(gè)地址
使用一系列指定了MAP_FIXED標(biāo)記的mmap()調(diào)用來將文件區(qū)域映射進(jìn)在上一步中創(chuàng)建的映射的不同部分
從 Linux 2.6 開始,使用remap_file_pages()系統(tǒng)調(diào)用也能夠取得同樣的效果,但是使用MAP_FIXED的可移植性更強(qiáng),因?yàn)閞emap_file_pages()是 Linux 特有的。
非線性映射
使用mmap()創(chuàng)建的文件映射是連續(xù)的:映射文件的分頁與內(nèi)存區(qū)域的分頁存在一個(gè)順序的,一對一的對應(yīng)關(guān)系。
但是一些應(yīng)用程序需要創(chuàng)建大量的非線性映射:
使用多個(gè)帶MAP_FIXED標(biāo)記的mmap()調(diào)用,然而這種方法的伸縮性不夠好,其問題在于其中每個(gè)mmap()調(diào)用都會創(chuàng)建一個(gè)獨(dú)立的內(nèi)核虛擬內(nèi)存區(qū)域 VMA 數(shù)據(jù)結(jié)構(gòu)。每個(gè) VMA 的配置需要花費(fèi)時(shí)間并且會消耗一些不可交換的內(nèi)核內(nèi)存。此外,大量的 VMA 會降低虛擬內(nèi)存管理器的性能。/proc/PID/maps中的一行表示一個(gè) VMA。
Linux 提供的remap_file_pages()調(diào)用來在無需創(chuàng)建多個(gè) VMA 的情況下創(chuàng)建非線性映射,具體如下:
使用mmap()創(chuàng)建一個(gè)映射
使用一個(gè)或者多個(gè)remap_file_pages()調(diào)用來調(diào)整內(nèi)存分頁和文件分頁之間的對應(yīng)關(guān)系
#define_GNU_SOURCE #includeintremap_file_pages(void*addr,size_tsize,intprot,size_tpgoff,intflags);
remap_file_pages()所做的工作是操作進(jìn)程的頁表,僅適用于MAP_SHARED映射
pgoff和size參數(shù)標(biāo)識了一個(gè)在內(nèi)存中的位置待改變的文件區(qū)域,pgoff參數(shù)指定了文件區(qū)域的起始位置,其單位是系統(tǒng)分頁代銷(sysconf(_SC_PAGESIZE)的返回值),size參數(shù)指定了文件區(qū)域的長度,其單位是字節(jié)
addr參數(shù)起兩個(gè)作用:
它標(biāo)識了分頁需要調(diào)整的既有映射,addr必須是一個(gè)位于之前通過mmap()映射的區(qū)域中的地址
它指定了通過pgoff和size標(biāo)識出的文件分頁所處的內(nèi)存地址
addr和size都應(yīng)該是系統(tǒng)分頁大小的整數(shù)倍,如果不是,那么它們會被向下舍入到最近的分頁大小的整數(shù)倍
prot參數(shù)會被忽略,必須指定為 0
flags參數(shù)當(dāng)前未被使用
-
Linux
+關(guān)注
關(guān)注
87文章
11335瀏覽量
210067 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
3045瀏覽量
74200 -
文件
+關(guān)注
關(guān)注
1文章
569瀏覽量
24791 -
共享內(nèi)存
+關(guān)注
關(guān)注
0文章
16瀏覽量
8329 -
進(jìn)程
+關(guān)注
關(guān)注
0文章
204瀏覽量
13973
原文標(biāo)題:Linux應(yīng)用開發(fā)之共享內(nèi)存
文章出處:【微信號:嵌入式應(yīng)用研究院,微信公眾號:嵌入式應(yīng)用研究院】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論