go 实现内存映射和进程间通信
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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。