linux库函数mmap()原理及用法详解
目錄
1.mmap基本概念
2.mmap內(nèi)存映射原理
3.mmap和常規(guī)文件操作的區(qū)別
4.mmap優(yōu)點總結
目錄
1. 內(nèi)存映射的概念
2. mmap基本概念
3. mmap相關函數(shù)
3.2 參數(shù)說明
3.3?相關函數(shù)
4. 系統(tǒng)調(diào)用mmap()用于共享內(nèi)存的兩種方式:
4.1 使用普通文件提供的內(nèi)存映射:
4.2 使用特殊文件提供匿名內(nèi)存映射:
5. mmap內(nèi)存映射原理
5.1 內(nèi)存映射的步驟:
5.2 mmap內(nèi)存映射的實現(xiàn)過程,總的來說可以分為三個階段:
5.3 mmap和常規(guī)文件操作的區(qū)別
6. mmap優(yōu)點總結
7. mmap使用細節(jié)
8. mmap使用demo
5.mmap相關函數(shù)
6.mmap使用細節(jié)
7.mmap使用demo
1. 內(nèi)存映射的概念
內(nèi)存映射,簡而言之就是將用戶空間的一段內(nèi)存區(qū)域映射到內(nèi)核空間,映射成功后,用戶對這段內(nèi)存區(qū)域的修改可以直接反映到內(nèi)核空間,同樣,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間。那么對于內(nèi)核空間<---->用戶空間兩者之間需要大量數(shù)據(jù)傳輸?shù)炔僮鞯脑捫适欠浅8叩摹?br /> 以下是一個把普遍文件映射到用戶空間的內(nèi)存區(qū)域的示意圖。
圖一:
?
2. mmap基本概念
mmap是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現(xiàn)這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實現(xiàn)不同進程間的文件共享。如下圖所示:
由上圖可以看出,進程的虛擬地址空間,由多個虛擬內(nèi)存區(qū)域構成。虛擬內(nèi)存區(qū)域是進程的虛擬地址空間中的一個同質(zhì)區(qū)間,即具有同樣特性的連續(xù)地址范圍。上圖中所示的text數(shù)據(jù)段(代碼段)、初始數(shù)據(jù)段、BSS數(shù)據(jù)段、堆、棧和內(nèi)存映射,都是一個獨立的虛擬內(nèi)存區(qū)域。而為內(nèi)存映射服務的地址空間處在堆棧之間的空余部分。
linux內(nèi)核使用vm_area_struct結構來表示一個獨立的虛擬內(nèi)存區(qū)域,由于每個不同質(zhì)的虛擬內(nèi)存區(qū)域功能和內(nèi)部機制都不同,因此一個進程使用多個vm_area_struct結構來分別表示不同類型的虛擬內(nèi)存區(qū)域。各個vm_area_struct結構使用鏈表或者樹形結構鏈接,方便進程快速訪問,如下圖所示:
vm_area_struct結構中包含區(qū)域起始和終止地址以及其他相關信息,同時也包含一個vm_ops指針,其內(nèi)部可引出所有針對這個區(qū)域可以使用的系統(tǒng)調(diào)用函數(shù)。這樣,進程對某一虛擬內(nèi)存區(qū)域的任何操作需要用要的信息,都可以從vm_area_struct中獲得。mmap函數(shù)就是要創(chuàng)建一個新的vm_area_struct結構,并將其與文件的物理磁盤地址相連。具體步驟請看后面。
3. mmap相關函數(shù)
3.1 函數(shù)原型
void?*mmap(void?*start,?size_t?length,?int?prot,?int?flags, int?fd,?off_t?offset);該函數(shù)主要用途有三個:
1、將一個普通文件映射到內(nèi)存中,通常在需要對文件進行頻繁讀寫時使用,這樣用內(nèi)存讀寫取代I/O讀寫,以獲得較高的性能;
2、將特殊文件進行匿名內(nèi)存映射,可以為關聯(lián)進程提供共享內(nèi)存空間;
3、為無關聯(lián)的進程提供共享內(nèi)存空間,一般也是將一個普通文件映射到內(nèi)存中。
3.2 參數(shù)說明
參數(shù)start:指向欲映射的內(nèi)存起始地址,通常設為 NULL,代表讓系統(tǒng)自動選定地址,映射成功后返回該地址。
參數(shù)length:代表將文件中多大的部分映射到內(nèi)存。
參數(shù)prot:映射區(qū)域的保護方式。可以為以下幾種方式的組合:
PROT_EXEC 映射區(qū)域可被執(zhí)行
PROT_READ 映射區(qū)域可被讀取
PROT_WRITE 映射區(qū)域可被寫入
PROT_NONE 映射區(qū)域不能存取
參數(shù)flags:影響映射區(qū)域的各種特性。在調(diào)用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果參數(shù)start所指的地址無法成功建立映射時,則放棄映射,不對地址做修正。通常不鼓勵用此旗標。
MAP_SHARED對映射區(qū)域的寫入數(shù)據(jù)會復制回文件內(nèi),而且允許其他映射該文件的進程共享。
MAP_PRIVATE 對映射區(qū)域的寫入操作會產(chǎn)生一個映射文件的復制,即私人的“寫入時復制”(copy on write)對此區(qū)域作的任何修改都不會寫回原來的文件內(nèi)容。
MAP_ANONYMOUS建立匿名映射。此時會忽略參數(shù)fd,不涉及文件,而且映射區(qū)域無法和其他進程共享。
MAP_DENYWRITE只允許對映射區(qū)域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
MAP_LOCKED 將映射區(qū)域鎖定住,這表示該區(qū)域不會被置換(swap)。
參數(shù)fd:要映射到內(nèi)存中的文件描述符。如果使用匿名內(nèi)存映射時,即flags中設置了MAP_ANONYMOUS,fd設為-1。有些系統(tǒng)不支持匿名內(nèi)存映射,則可以使用fopen打開/dev/zero文件,然后對該文件進行映射,可以同樣達到匿名內(nèi)存映射的效果。
參數(shù)offset:文件映射的偏移量,通常設置為0,代表從文件最前方開始對應,offset必須是分頁大小的整數(shù)倍。
返回值:若映射成功則返回映射區(qū)的內(nèi)存起始地址,否則返回MAP_FAILED(-1),錯誤原因存于errno 中。
錯誤代碼:
EBADF 參數(shù)fd 不是有效的文件描述詞
EACCES 存取權限有誤。如果是MAP_PRIVATE 情況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
EINVAL 參數(shù)start、length 或offset有一個不合法。
EAGAIN 文件被鎖住,或是有太多內(nèi)存被鎖住。
ENOMEM 內(nèi)存不足。
3.3?相關函數(shù)
int?munmap(void*?addr,?size_t?len?) ;成功執(zhí)行時,munmap()返回0。失敗時,munmap返回-1,error返回標志和mmap一致;
該調(diào)用在進程地址空間中解除一個映射關系,addr是調(diào)用mmap()時返回的地址,len是映射區(qū)的大小;
當映射關系解除后,對原來映射地址的訪問將導致段錯誤發(fā)生。
int msync(void *addr, size_t len, int flags );一般說來,進程在映射空間的對共享內(nèi)容的改變并不直接寫回到磁盤文件中,往往在調(diào)用munmap()后才執(zhí)行該操作。
可以通過調(diào)用msync()實現(xiàn)磁盤上文件內(nèi)容與共享內(nèi)存區(qū)的內(nèi)容一致。
4. 系統(tǒng)調(diào)用mmap()用于共享內(nèi)存的兩種方式:
4.1 使用普通文件提供的內(nèi)存映射:
適用于任何進程之間。此時,需要打開或創(chuàng)建一個文件,然后再調(diào)用mmap()。
典型調(diào)用代碼如下:
通過mmap()實現(xiàn)共享內(nèi)存的通信方式有許多特點和要注意的地方,可以參看UNIX網(wǎng)絡編程第二卷。
4.2 使用特殊文件提供匿名內(nèi)存映射:
適用于具有親緣關系的進程之間。由于父子進程特殊的親緣關系,在父進程中先調(diào)用mmap(),然后調(diào)用 fork()。那么在調(diào)用fork()之后,子進程繼承父進程匿名映射后的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進程就可以通過映射區(qū) 域進行通信了。注意,這里不是一般的繼承關系。一般來說,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。 對于具有親緣關系的進程實現(xiàn)共享內(nèi)存最好的方式應該是采用匿名內(nèi)存映射的方式。此時,不必指定具體的文件,只要設置相應的標志即可。
5. mmap內(nèi)存映射原理
5.1 內(nèi)存映射的步驟:
用open系統(tǒng)調(diào)用打開文件, 并返回描述符fd.
用mmap建立內(nèi)存映射, 并返回映射首地址指針start.
對映射(文件)進行各種操作, 顯示(printf), 修改(sprintf).
用munmap(void *start, size_t lenght)關閉內(nèi)存映射.
用close系統(tǒng)調(diào)用關閉文件fd.
5.2 mmap內(nèi)存映射的實現(xiàn)過程,總的來說可以分為三個階段:
(一)進程啟動映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
1、進程在用戶空間調(diào)用庫函數(shù)mmap,原型:void?*mmap(void?*start,?size_t?length,?int?prot,?int?flags, int?fd,?off_t?offset);
2、在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址
3、為此虛擬區(qū)分配一個vm_area_struct結構,接著對這個結構的各個域進行了初始化
4、將新建的虛擬區(qū)結構(vm_area_struct)插入進程的虛擬地址區(qū)域鏈表或樹中
(二)調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實現(xiàn)文件物理地址和進程虛擬地址的一一映射關系
5、為映射分配了新的虛擬地址區(qū)域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內(nèi)核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護著和這個已打開文件相關各項信息。
6、通過該文件的文件結構體,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap,其原型為:int mmap(struct?file?*filp,?struct?vm_area_struct?*vma),不同于用戶空間庫函數(shù)。
7、內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址。
8、通過remap_pfn_range函數(shù)建立頁表,即實現(xiàn)了文件地址和虛擬地址區(qū)域的映射關系。此時,這片虛擬地址并沒有任何數(shù)據(jù)關聯(lián)到主存中。
(三)進程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝
注:前兩個階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當進程發(fā)起讀或?qū)懖僮鲿r。
9、進程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址,通過查詢頁表,發(fā)現(xiàn)這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁異常。
10、缺頁異常進行一系列判斷,確定無非法操作后,內(nèi)核發(fā)起請求調(diào)頁過程。
11、調(diào)頁過程先在交換緩存空間(swap?cache)中尋找需要訪問的內(nèi)存頁,如果沒有則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中。
12、之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內(nèi)容,一定時間后系統(tǒng)會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程。
注:修改過的臟頁面并不會立即更新回文件中,而是有一段時間的延遲,可以調(diào)用msync()來強制同步, 這樣所寫的內(nèi)容就能立即保存到文件里了。
5.3 mmap和常規(guī)文件操作的區(qū)別
對linux文件系統(tǒng)不了解的朋友,請參閱博文《從內(nèi)核文件系統(tǒng)看文件讀寫過程》,我們首先簡單的回顧一下常規(guī)文件系統(tǒng)操作(調(diào)用read/fread等類函數(shù))中,函數(shù)的調(diào)用過程:
1、進程發(fā)起讀文件請求。
2、內(nèi)核通過查找進程文件符表,定位到內(nèi)核已打開文件集上的文件信息,從而找到此文件的inode。
3、inode在address_space上查找要請求的文件頁是否已經(jīng)緩存在頁緩存中。如果存在,則直接返回這片文件頁的內(nèi)容。
4、如果不存在,則通過inode定位到文件磁盤地址,將數(shù)據(jù)從磁盤復制到頁緩存。之后再次發(fā)起讀頁面過程,進而將頁緩存中的數(shù)據(jù)發(fā)給用戶進程。
總結來說,常規(guī)文件操作為了提高讀寫效率和保護磁盤,使用了頁緩存機制。這樣造成讀文件時需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內(nèi)核空間,不能被用戶進程直接尋址,所以還需要將頁緩存中數(shù)據(jù)頁再次拷貝到內(nèi)存對應的用戶空間中。這樣,通過了兩次數(shù)據(jù)拷貝過程,才能完成進程對文件內(nèi)容的獲取任務。寫操作也是一樣,待寫入的buffer在內(nèi)核空間不能直接訪問,必須要先拷貝至內(nèi)核空間對應的主存,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據(jù)拷貝。
而使用mmap操作文件中,創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤地址和虛擬內(nèi)存區(qū)域映射這兩步,沒有任何文件拷貝操作。而之后訪問數(shù)據(jù)時發(fā)現(xiàn)內(nèi)存中并無數(shù)據(jù)而發(fā)起的缺頁異常過程,可以通過已經(jīng)建立好的映射關系,只使用一次數(shù)據(jù)拷貝,就從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中,供進程使用。
總而言之,常規(guī)文件操作需要從磁盤到頁緩存再到用戶主存的兩次數(shù)據(jù)拷貝。而mmap操控文件,只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程。說白了,mmap的關鍵點是實現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)直接交互而省去了空間不同數(shù)據(jù)不通的繁瑣過程。因此mmap效率更高。
6. mmap優(yōu)點總結
由上文討論可知,mmap優(yōu)點共有一下幾點:
1、對文件的讀取操作跨過了頁緩存,減少了數(shù)據(jù)的拷貝次數(shù),用內(nèi)存讀寫取代I/O讀寫,提高了文件讀取效率。
2、實現(xiàn)了用戶空間和內(nèi)核空間的高效交互方式。兩空間的各自修改操作可以直接反映在映射的區(qū)域內(nèi),從而被對方空間及時捕捉。
3、提供進程間共享內(nèi)存及相互通信的方式。不管是父子進程還是無親緣關系的進程,都可以將自身用戶空間映射到同一個文件或匿名映射到同一片區(qū)域。從而通過各自對映射區(qū)域的改動,達到進程間通信和進程間共享的目的。
? ? ?同時,如果進程A和進程B都映射了區(qū)域C,當A第一次讀取C時通過缺頁從磁盤復制文件頁到內(nèi)存中;但當B再讀C的相同頁面時,雖然也會產(chǎn)生缺頁異常,但是不再需要從磁盤中復制文件過來,而可直接使用已經(jīng)保存在內(nèi)存中的文件數(shù)據(jù)。
4、可用于實現(xiàn)高效的大規(guī)模數(shù)據(jù)傳輸。內(nèi)存空間不足,是制約大數(shù)據(jù)操作的一個方面,解決方案往往是借助硬盤空間協(xié)助操作,補充內(nèi)存的不足。但是進一步會造成大量的文件I/O操作,極大影響效率。這個問題可以通過mmap映射很好的解決。換句話說,但凡是需要用磁盤空間代替內(nèi)存的時候,mmap都可以發(fā)揮其功效。
7. mmap使用細節(jié)
1、使用mmap需要注意的一個關鍵點是,mmap映射區(qū)域大小必須是物理頁大小(page_size)的整倍數(shù)(32位系統(tǒng)中通常是4k字節(jié))。原因是,內(nèi)存的最小粒度是頁,而進程虛擬地址空間和內(nèi)存的映射也是以頁為單位。為了匹配內(nèi)存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁。
2、內(nèi)核可以跟蹤被內(nèi)存映射的底層對象(文件)的大小,進程可以合法的訪問在當前文件大小以內(nèi)又在內(nèi)存映射區(qū)以內(nèi)的那些字節(jié)。也就是說,如果文件的大小一直在擴張,只要在映射區(qū)域范圍內(nèi)的數(shù)據(jù),進程都可以合法得到,這和映射建立時文件的大小無關。具體情形參見“情形三”。
3、映射建立之后,即使文件關閉,映射依然存在。因為映射的是磁盤的地址,不是文件本身,和文件句柄無關。同時可用于進程間通信的有效地址空間不完全受限于被映射文件的大小,因為是按頁映射。
在上面的知識前提下,我們下面看看如果大小不是頁的整倍數(shù)的具體情況:
情形一:一個文件的大小是5000字節(jié),mmap函數(shù)從一個文件的起始位置開始,映射5000字節(jié)到虛擬內(nèi)存中。
分析:因為單位物理頁面的大小是4096字節(jié),雖然被映射的文件只有5000字節(jié),但是對應到進程虛擬地址區(qū)域的大小需要滿足整頁大小,因此mmap函數(shù)執(zhí)行后,實際映射到虛擬內(nèi)存區(qū)域8192個 字節(jié),5000~8191的字節(jié)部分用零填充。映射后的對應關系如下圖所示:
此時:
(1)讀/寫前5000個字節(jié)(0~4999),會返回操作文件內(nèi)容。
(2)讀字節(jié)5000~8191時,結果全為0。寫5000~8191時,進程不會報錯,但是所寫的內(nèi)容不會寫入原文件中 。
(3)讀/寫8192以外的磁盤部分,會返回一個SIGSECV錯誤。
情形二:一個文件的大小是5000字節(jié),mmap函數(shù)從一個文件的起始位置開始,映射15000字節(jié)到虛擬內(nèi)存中,即映射大小超過了原始文件的大小。
分析:由于文件的大小是5000字節(jié),和情形一一樣,其對應的兩個物理頁。那么這兩個物理頁都是合法可以讀寫的,只是超出5000的部分不會體現(xiàn)在原文件中。由于程序要求映射15000字節(jié),而文件只占兩個物理頁,因此8192字節(jié)~15000字節(jié)都不能讀寫,操作時會返回異常。如下圖所示:
此時:
(1)進程可以正常讀/寫被映射的前5000字節(jié)(0~4999),寫操作的改動會在一定時間后反映在原文件中。
(2)對于5000~8191字節(jié),進程可以進行讀寫過程,不會報錯。但是內(nèi)容在寫入前均為0,另外,寫入后不會反映在文件中。
(3)對于8192~14999字節(jié),進程不能對其進行讀寫,會報SIGBUS錯誤。
(4)對于15000以外的字節(jié),進程不能對其讀寫,會引發(fā)SIGSEGV錯誤。
情形三:一個文件初始大小為0,使用mmap操作映射了1000*4K的大小,即1000個物理頁大約4M字節(jié)空間,mmap返回指針ptr。
分析:如果在映射建立之初,就對文件進行讀寫操作,由于文件大小為0,并沒有合法的物理頁對應,如同情形二一樣,會返回SIGBUS錯誤。
但是如果,每次操作ptr讀寫前,先增加文件的大小,那么ptr在文件大小內(nèi)部的操作就是合法的。例如,文件擴充4096字節(jié),ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要文件擴充的范圍在1000個物理頁(映射范圍)內(nèi),ptr都可以對應操作相同的大小。
這樣,方便隨時擴充文件空間,隨時寫入文件,不造成空間浪費。
8. mmap使用demo
#include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <errno.h> #include <sys/stat.h> #include <unistd> int main(int argc, char *argv[]) {int fd = 0;char *ptr = NULL;struct stat buf = {0};if (argc < 2){printf("please enter a file!\n");return -1;}if ((fd = open(argv[1], O_RDWR)) < 0){printf("open file error\n");return -1;}if (fstat(fd, &buf) < 0){printf("get file state error:%d\n", errno);close(fd);return -1;}ptr = (char *)mmap(NULL, buf.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);if (ptr == MAP_FAILED){printf("mmap failed\n");close(fd);return -1;}close(fd);printf("length of the file is : %d\n", buf.st_size);printf("the %s content is : %s\n", argv[1], ptr);ptr[3] = 'a';printf("the %s new content is : %s\n", argv[1], ptr);munmap(ptr, buf.st_size);return 0; }原文鏈接:https://blog.csdn.net/bbzhaohui/article/details/81665370
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的linux库函数mmap()原理及用法详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 父组件给子组件传值方法_【Vue】小学生
- 下一篇: mysql navicat安装_MySQ