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