前言

进程间通信的方式有很多种,如果两个进程分别在不同的机器上,那么使用 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“。

代码就不在这里贴出来了。