前言
业务需要从给定的共享内存中读取数据,但写入以及更新方都是C代码,本文以Go为例,读取共享内存具体数据。
阅读完本文将获得以下技能:
- unsafe包基本使用
- 理解变量在内存中布局
- 能够通过 syscall 使用 shmget/at/dt 系统调用
- 类型转换与内存地址运算
数据准备
使用C代码开辟一块System V共享内存,并写入数据
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
typedef struct
{
char name[32];
int age;
} people;
int main(int argc, char** argv)
{
int shm_id,i;
key_t key;
people* p_map;
char temp = 'a';
const char* name = "/dev/shm/my_systemv_shm1";
key = ftok(name,0);
if (key == -1)
{
perror("ftok error");
return -1;
}
shm_id=shmget(key, 4096, IPC_CREAT);
if(shm_id == -1)
{
perror("shmget error");
return -1;
}
p_map=(people*)shmat(shm_id,NULL,0);
for(int i = 0; i < 10; i++)
{
memcpy( ( *(p_map+i) ).name, &temp, 1);
( *(p_map+i) ).name[1] = 0;
( *(p_map+i) ).age = 20+i;
temp += 1;
}
printf("initialize over\n");
if(shmdt(p_map) == -1)
{
perror(" detach error ");
return -1;
}
return 0;
}
共享内存布局
- 得到System V共享内存的key
[root@localhost xcxing1]# ipcs -m
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 nattch 状态
0x00130d5e 4 root 0 4096 0
[root@localhost xcxing1]#
- 了解内存中的数据布局,要具体到字节。
示例中是往共享内存写入了10个People结构体,共享内存总大小为4096字节,内存布局如下:
[name1 age1 name2 age2 name3 age3 ...]
| |
首地址 末地址
- 使用GO建立对应结构模型:
type pn [32]byte
func (p pn) String() string {
return string(p[:])
}
//name 本质还是一个[32]byte数组,为了方便转换,重定义了pn类型
type People struct {
name pn
age int32
}
- 通过系统调用读取
shmId, _, err := syscall.Syscall(syscall.SYS_SHMGET, 0x00130d5e, 4096, 0)
if err < 0 {
log.Fatal(err)
}
shmaddr, _, err := syscall.Syscall(syscall.SYS_SHMAT, shmId, 0, 0)
if err != 0 {
fmt.Printf("syscall error, err: %v\n", err)
os.Exit(-2)
}
fmt.Printf("shmaddr: %v\n", shmaddr)
defer syscall.Syscall(syscall.SYS_SHMDT, shmaddr, 0, 0)
sizeOfPeople := unsafe.Sizeof(People{})
for i := 0; i < 10; i++ {
p := shmaddr + sizeOfPeople*uintptr(i)
data := (*People)(unsafe.Pointer(p))
fmt.Println(data.name.String())
fmt.Println(data.age)
}
代码讲解
-
shmget 返回值是什么?
通过shmget系统调用,返回的是共享内存段的首地址
image.png - unsafe包的魔力
官网地址:https://golang.google.cn/pkg/unsafe/
正如包名所说,使用它必须要格外小心,可以很危险,也可以很高效,特别是处理系统调用的时候,往往返回的只是一个首地址,剩下的则需要通过控制结构体与C结构对应,必须要是相同的内存结构才能正确读取,这种时候unsafe包是唯一选择。
- 无视go的类型系统,完成任何类型与内建uintptr类型之间的转换
- 主角:unsafe.Pointer
Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types:
- A pointer value of any type can be converted to a Pointer.//任何类型的指针都可以转化为一个 unsafe.Pointer
- A Pointer can be converted to a pointer value of any type.//一个 unsafe.Pointer 可以转化成任何类型的指针
- A uintptr can be converted to a Pointer.//一个 uintptr 可以转化成一个 unsafe.Pointer
- A Pointer can be converted to a uintptr.//一个 unsafe.Pointer 可以转化成一个 uintptr
- 类型转换
/*
>> 一个People结构体占内存空间是40bytes
>> syscall(SYS.SHMGET,...) 返回的第一个参数是 uintptr (即内存地址)
>> (People)(unsafe.Pointer(shmAddr)) 意思是一次从shmAddr指向的内存读取大小为40字节的内存,所以类型转换后,可以直接通过 people1.age 方式访问内存中的age
*/
people1 := (*People)(unsafe.Pointer(shmAddr))
fmt.Println(people1.name.String())
fmt.Println(people1.age)
完整go代码
package main
/*
//C 代码定义的People结构体
typedef struct
{
char name[32];
int age;
} people;
*/
/*
**内存数据布局**
[name1 age1 name2 age2 name3 age3 ...] (示例中是往共享内存写入了10个People,共享内存总大小为4096字节)
| |
首地址 末地址
**主要踩坑点**
没有将Go的People结构体内存大小与C的People对齐,导致每次通过unsafe包转换成People的时候都失败
如果不清楚C和GO类型内存大小对应关系,参考:https://www.cntofu.com/book/73/ch2-cgo/ch2-03-cgo-types.md
*/
import (
"fmt"
"log"
"os"
"syscall"
"unsafe"
)
type pn [32]byte
func (p pn) String() string {
return string(p[:])
}
type People struct {
name pn
age int32
}
func main() {
shmId, _, err := syscall.Syscall(syscall.SYS_SHMGET, 0x00130d5e, 4096, 0)
if err < 0 {
log.Fatal(err)
}
shmaddr, _, err := syscall.Syscall(syscall.SYS_SHMAT, shmId, 0, 0)
if err != 0 {
fmt.Printf("syscall error, err: %v\n", err)
os.Exit(-2)
}
fmt.Printf("shmaddr: %v\n", shmaddr)
defer syscall.Syscall(syscall.SYS_SHMDT, shmaddr, 0, 0)
sizeOfPeople := unsafe.Sizeof(People{})
for i := 0; i < 10; i++ {
p := shmaddr + sizeOfPeople*uintptr(i)
data := (*People)(unsafe.Pointer(p))
fmt.Println(data.name.String())
fmt.Println(data.age)
}
}
运行效果:

image.png
