前言
进程间通信的方式有很多种,如果两个进程分别在不同的机器上,那么使用 socket 通信;如果在同一台机器上,共享内存机制是一种快速高效的方式。
本文实现一个 go 语言二进制程序和 C 语言二进制程序通过共享内存交换数据。
提到共享内存主要有两种:
- System V 标准的 shmget/shmdt 等接口
- POSIX 标准的 shm_open 等接口
另外 Linux 下 mmap() 匿名映射也是最常用的进程间共享内存方法。
创建了共享内存以后,一般会显示在系统的 /dev/shm 目录下。Linux 默认 /dev/shm 为实际物理内存的1/2, 比如我的机器上物理内存为 16G,运行 df 命令后可以看到 /dev/shm 的大小为 7.8G 。
$ df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 1.6G 3.2M 1.6G 1% /run
tmpfs 7.8G 4.0K 7.8G 1% /dev/shm
tmpfs, ramfs 和 ramdisk
tmpfs是一个虚拟内存文件系统,在Linux内核中,虚拟内存资源由物理内存(RAM)和交换分区组成,Tmpfs可以使用物理内存,也可以使用交换分区。
ramdisk 是一个块设备,只不过它是存在于内存上的。
ramfs 也是文件系统,不过已经被 tmpfs 替代了。
说完了这些基础之后,那么如何用 golang 操作共享内存呢? 先来看看 golang 为我们提供的一组 package。
unsafe 包
Go 语言是强类型语言,出于安全的考虑不允许不同类型的指针互相转换,但是 unsafe 包给我们提供了一个途径。首先来看一下会用到的两个函数。
unsafe.Sizeof() 计算出结构体在内存中占用的实际大小。
unsafe.Pointer() 类似于 C 语言中的 void *
, 任何类型的指针值都可以转换为unsafe.Pointer,unsafe.Pointer 也可以转换为任何类型的指针值。 有了这个函数,在内存开辟一段 share memory 以后,就可以把这段内存映射为 golang 中的数据结构。
- uintptr是一个整数类型,即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收
- unsafe.Pointer是一个指针类型,如果变量仍然有效,则对应的地址处的数据不会被GC回收。
- uintptr可以转换为unsafe.Pointer。
- unsafe.Pointer可以转换为uintptr
所以可以看出 uintptr 是一个整数值,通常用来加上一个 offset 得到新的地址,再转换成 unsafe.Pointer 类型,在下面的例子中将会看到这个用法。
syscall 包
syscall 包提供了 go 语言包装的 mmap() 系统调用 syscall.Mmap()
, 函数原型如下:
func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error)
reflect 包
relect 翻译过来是反射,反射就是在运行时动态的调用对象的方法和属性。
比如 reflect.TypeOf() 可以返回一个变量的类型,reflect.ValueOf() 返回变量的值。
package main
import (
"fmt"
"reflect"
)
func main() {
var num float64 = 1.2345
fmt.Println("type: ", reflect.TypeOf(num))
fmt.Println("value: ", reflect.ValueOf(num))
}
运行结果:
type: float64
value: 1.2345
而我们需要用的是 reflect.SliceHeader,SliceHeader是 Slice 运行时的具体表现。
前面提到 Mmap() 返回的是 data []byte
, 这个 slice 其实就是刚刚创建的共享内存,那么怎么把它转化成可以使用的地址呢?
bytes, _ := syscall.Mmap(fd, 0, size, syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED)
slicehdr := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
shm := (*Shmem)((unsafe.Pointer)(slicehdr.Data))
reflect.SliceHeader 结构对应 slice 的三个属性:数据指针,长度,容量。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
通过把一个 slice 类型反射,得到了它 Data 成员的地址,这个地址就是我们创建的共享内存。
实验
type Shmem struct {
size uint32
name [16]byte
}
const (
SHMEM_HEAD_SIZE = unsafe.Sizeof(Shmem{})
)
func ShmemOpen(name string, shmSize uint32, create bool) (*Shmem, error) {
flag := os.O_RDWR
if create {
flag |= os.O_CREATE
}
filename := filepath.Join("/dev/shm", name)
fd, _ := unix.Open(filename, flag, 0644)
// calculate total size = Shm struct size + data area size
size := int(shmSize) + int(SHMEM_HEAD_SIZE)
bytes, _ := syscall.Mmap(fd, 0, size, syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED)
slicehdr := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
shm := (*Shmem)((unsafe.Pointer)(slicehdr.Data))
shm.size = shmSize
copy(shm.name[0:], name)
return shm, nil
}
func main() {
shm, err := ShmemOpen("test_shm", 100, true)
if err != nil {
panic(err)
}
fmt.Printf("share memory %s is created\n", shm.name)
d := &reflect.SliceHeader{
// type of Data is uintptr
Data: uintptr(unsafe.Pointer(shm)) + SHMEM_HEAD_SIZE,
Len: int(shm.size),
Cap: int(shm.size),
}
data := *(*[]byte)(unsafe.Pointer(d))
payload := []byte("hello, world")
copy(data, payload)
}
运行这段代码,将会在 /dev/shm/ 目录下创建一个 test_shm
文件,cat
这个文件将会看到写入的字符串”hello, world“
$ cat /dev/shm/test_shm
dtest_shmhello, world
然后再用 C 语言写个程序调用 mmap 函数映射同样的一块区域,就可以读取 ”hello, world“。
代码就不在这里贴出来了。