1、使用mmap需要注意的一個關鍵點是,mmap映射區域大小必須是物理頁大小(page_size)的整倍數(32位系統中通常是4k字節)。原因是,內存的最小粒度是頁,而進程虛擬地址空間和內存的映射也是以頁為單位。為了匹配內存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁。
再啰嗦幾句:
linux采用的是頁式管理機制。對於用mmap()映射普通文件來說,進程會在自己的地址空間新增一塊空間,空間大小由mmap()的len參數指定,注意,進程並不一定能夠對全部新增空間都能進行有效訪問。進程能夠訪問的有效地址大小取決於文件被映射部分的大小。簡單的說,能夠容納文件被映射部分大小的最少頁面個數決定了進程從mmap()返回的地址開始,能夠有效訪問的地址空間大小。超過這個空間大小,內核會根據超過的嚴重程度返回發送不同的信號給進程。
2、內核可以跟蹤被內存映射的底層對象(文件)的大小,進程可以合法的訪問在當前文件大小以內又在內存映射區以內的那些字節。也就是說,如果文件的大小一直在擴張,只要在映射區域范圍內的數據,進程都可以合法得到,這和映射建立時文件的大小無關。具體情形參見“情形三”。
3、映射建立之后,即使文件關閉,映射依然存在。因為映射的是磁盤的地址,不是文件本身,和文件句柄無關。同時可用於進程間通信的有效地址空間不完全受限於被映射文件的大小,因為是按頁映射。
作者:batbattle
鏈接:https://www.jianshu.com/p/472ea35448ca
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
——-------------------
原由二:進程高效通信和文件讀寫
無名知道對於像有名/無名管道和消息隊列等通信方式,需要在內核和用戶空間進行兩次運行級別切換(系統調用導致保護和恢復進程上下文環境)+四次數據拷貝,而共享內存則只拷貝兩次數據: 一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據后就解除映射,有新的通信時,不用再重新建立共享內存區域,而是保持共享區域,直到通信完畢為止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件(內核通過一定策略刷盤,后續專題介紹)。共享內存中的數據往往是在解除映射時才寫回文件的。因此,采用共享內存的通信方式效率是非常高的。
作者:batbattle
鏈接:https://www.jianshu.com/p/472ea35448ca
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
情形二:一個文件的大小是5000字節,mmap函數從一個文件的起始位置開始,映射15000字節到虛擬內存中,即映射大小超過了原始文件的大小。
分析:由於文件的大小是5000字節,和情形一一樣,其對應的兩個物理頁。那么這兩個物理頁都是合法可以讀寫的,只是超出5000的部分不會體現在原文件中。由於程序要求映射15000字節,而文件只占兩個物理頁,因此8192字節~15000字節都不能讀寫,操作時會返回異常。如下圖所示:

此時:
(1)進程可以正常讀/寫被映射的前5000字節(0~4999),寫操作的改動會在一定時間后反映在原文件中。
(2)對於5000~8191字節,進程可以進行讀寫過程,不會報錯。但是內容在寫入前均為0,另外,寫入后不會反映在文件中。
(3)對於8192~14999字節,進程不能對其進行讀寫,會報SIGBUS錯誤。
(4)對於15000以外的字節,進程不能對其讀寫,會引發SIGSEGV錯誤。
情形三:一個文件初始大小為0,使用mmap操作映射了1000*4K的大小,即1000個物理頁大約4M字節空間,mmap返回指針ptr。
分析:如果在映射建立之初,就對文件進行讀寫操作,由於文件大小為0,並沒有合法的物理頁對應,如同情形二一樣,會返回SIGBUS錯誤。
但是如果,每次操作ptr讀寫前,先增加文件的大小,那么ptr在文件大小內部的操作就是合法的。例如,文件擴充4096字節,ptr就能操作ptr到 [ (char)ptr + 4095]的空間。只要文件擴充的范圍在1000個物理頁(映射范圍)內,ptr都可以對應操作相同的大小。這樣,方便隨時擴充文件空間,隨時寫入文件,不造成空間浪費。
其他實戰實例,我會補充在GitHub。
作者:batbattle
鏈接:https://www.jianshu.com/p/472ea35448ca
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
--------------------------
內存映射mmap可以用來實現 IPC , 即進程間通訊。
例子如下:
a.go
package main import ( "os" "syscall" "log" "time" ) func main() { f, err := os.OpenFile("mmap.bin", os.O_RDWR|os.O_CREATE, 0644) if nil != err { log.Fatalln(err) } // extend file //if _, err := f.WriteAt([]byte{byte(0)}, 10); nil != err { // log.Fatalln(err) //} err = syscall.Ftruncate(int(f.Fd()), 1000) // 文件讀取的最小的單位為“一頁”, 一頁的大小一般為4k if err != nil { panic(err) } data, err := syscall.Mmap(int(f.Fd()), 0, 1<<12, syscall.PROT_WRITE, syscall.MAP_SHARED) if nil != err { log.Fatalln(err) } if err := f.Close(); nil != err { log.Fatalln(err) } //for i, v := range []byte("a\n") { // data[i+4094] = v // 這里4096會報錯,4095 就沒問題 //} for i:= 0; i< 100; i++ { log.Println(string(data)) for i, v := range []byte("hello syscall123") { data[i] = v } time.Sleep(time.Second * 2) } if err := syscall.Munmap(data); nil != err { log.Fatalln(err) } }
a2.go
package main import ( "os" "syscall" "log" "time" ) func main() { f, err := os.OpenFile("mmap.bin", os.O_RDWR|os.O_CREATE, 0644) if nil != err { log.Fatalln(err) } // extend file //if _, err := f.WriteAt([]byte{byte(0)}, 10); nil != err { // log.Fatalln(err) //} err = syscall.Ftruncate(int(f.Fd()), 1000) // 文件讀取的最小的單位為“一頁”, 一頁的大小一般為4k if err != nil { panic(err) } data, err := syscall.Mmap(int(f.Fd()), 0, 1<<12, syscall.PROT_WRITE, syscall.MAP_SHARED) if nil != err { log.Fatalln(err) } if err := f.Close(); nil != err { log.Fatalln(err) } //for i, v := range []byte("a\n") { // data[i+4094] = v // 這里4096會報錯,4095 就沒問題 //} for i:= 0; i< 100; i++ { log.Println(string(data)) for i, v := range []byte("i am from another process") { data[i] = v } time.Sleep(time.Second * 3) } if err := syscall.Munmap(data); nil != err { log.Fatalln(err) } }
分別在兩個shell窗口中執行這兩個進程go run a.go, go run a2.go
結果如下:
可以看出,兩個進程看到的兩個文件的內容是同步的。
------------------------------------
go語言里面內存映射的實現
package main import ( "os" "syscall" "log" ) func main() { f, err := os.OpenFile("mmap.bin", os.O_RDWR|os.O_CREATE, 0644) if nil != err { log.Fatalln(err) } // extend file //if _, err := f.WriteAt([]byte{byte(0)}, 10); nil != err { // log.Fatalln(err) //} err = syscall.Ftruncate(int(f.Fd()), 1000) // 文件讀取的最小的單位為“一頁”, 一頁的大小一般為4k if err != nil { panic(err) } data, err := syscall.Mmap(int(f.Fd()), 0, 1<<13, syscall.PROT_WRITE, syscall.MAP_SHARED) if nil != err { log.Fatalln(err) } if err := f.Close(); nil != err { log.Fatalln(err) } for i, v := range []byte("hello syscall123") { data[i] = v } for _, v := range []byte("a") { data[4096] = v // 這里4096會報錯,4095 就沒問題 } if err := syscall.Munmap(data); nil != err { log.Fatalln(err) } }
3、映射建立之后,即使文件關閉,映射依然存在。因為映射的是磁盤的地址,不是文件本身,和文件句柄無關。同時可用於進程間通信的有效地址空間不完全受限於被映射文件的大小,因為是按頁映射。
在上面的知識前提下,我們下面看看如果大小不是頁的整倍數的具體情況:
情形一:一個文件的大小是5000字節,mmap函數從一個文件的起始位置開始,映射5000字節到虛擬內存中。
分析:因為單位物理頁面的大小是4096字節,雖然被映射的文件只有5000字節,但是對應到進程虛擬地址區域的大小需要滿足整頁大小,因此mmap函數執行后,實際映射到虛擬內存區域8192個 字節,5000~8191的字節部分用零填充。映射后的對應關系如下圖所示:

此時:
(1)讀/寫前5000個字節(0~4999),會返回操作文件內容。
(2)讀字節5000~8191時,結果全為0。寫5000~8191時,進程不會報錯,但是所寫的內容不會寫入原文件中 。
(3)讀/寫8192以外的磁盤部分,會返回一個SIGSECV錯誤。
作者:batbattle
鏈接:https://www.jianshu.com/p/472ea35448ca
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。