如何实现共享内存结构兼容
1.前言
共享内存作为最快的进程间通信手段,在对性能要求苛刻的系统中依然被广泛采用。以最原始的方式使用共享内存时,可能会遇到一些麻烦,本文将使用一个示例进行展示,并尝试用一些简单的方法解决。
2.读写示例
我们定义一个这样的结构体:
typedef struct shmhead
{
int field1;
int field2;
int field3;
} shmhead;
然后设计一个读者和写者。
写者创建共享内存,然后将三个字段分别赋值为1、2、3。
读者链接共享内存,从中读取三个字段的值,打印到屏幕上。
运行效果如下:
3.不兼容示例1
我们保持读者程序不变,单独升级写者程序,在写者程序的shmhead中增加一个字段:
typedef struct shmhead
{
int field1;
int field2;
int fieldnew;
int field3;
} shmhead;
然后将新字段赋值为4:
我们再运行一次:
可以看到,读者读到的数据发生了异常。
这是因为:
- 在写者看来,shmhead的第三个字段是filednew
- 在读者看来,shmhead的第三个字段是field3
要解决这个问题有两个方法:
一是读者写者的结构体需要时刻保持一致。这样一来,只要写者的结构体发生变更,读者就必须同时编译同时上线,这样不仅会造成变更影响范围过大,还会造成上线流程复杂,上线风险加大。
二是增加字段的时候追加到结构体的后面,不删除字段,也不修改原来字段的含义,这样对于读者来讲,原先的字段是没有变化的,新增的字段他看不到,所以对他没什么影响。
2.不兼容示例2
这次我们往共享内存中多放些内容,比如在shmhead之后紧邻存放一个shmdata结构体。
typedef struct shmhead
{
int field1;
int field2;
int field3;
} shmhead;
typedef struct shmdata
{
int data1;
int data2;
int data3;
} shmdata;
我们给shmdata的三个字段分别赋值11、22、33,然后用读者程序读出来,效果如下:
这次我们仍旧保持读者不变,写者的shmhead增加一个字段(这次我们以追加的方式添加字段):
typedef struct shmhead
{
int field1;
int field2;
int field3;
int fieldnew;
} shmhead;
typedef struct shmdata
{
int data1;
int data2;
int data3;
} shmdata;
运行结果如下:
可以看到,读者读到的数据错了。
不过这次出错的原因跟前面的不一样,这次并不是由于shmdata结构体本身发生了变化,而是共享内存中位于shmdata结构体前面的结构体发生变化所致。
这个问题的根本原因是读者和写者看到的shmhead结构体长度不一样,因此计算shmdata偏移量时用的长度,不能自己用sizeof(shmhead)取,应该让shmhead结构体自己告诉你。
我们给shmhead结构体添加一个size字段:
typedef struct shmhead
{
int size;
int field1;
int field2;
int field3;
int fieldnew;
} shmhead;
typedef struct shmdata
{
int data1;
int data2;
int data3;
} shmdata;
这个size由写者填写:
fprintf(stdout, "shm created!\n");
shm->size = sizeof(shmhead);
shm->field1 = 1;
shm->field2 = 2;
shm->field3 = 3;
data->data1 = 11;
data->data2 = 22;
data->data3 = 33;
fprintf(stdout, "shm written!\n");
读者在使用时直接取size字段,而非自己用sizeof计算:
char* shmpos = (char*)shmat(shmid, 0, 0);
shmhead* shm = (shmhead*)shmpos;
// shmdata* data = (shmdata*)(shmpos + sizeof(shmhead));
shmdata* data = (shmdata*)(shmpos + shm->size);
我们尝试运行一下:
正常了。
3.优化
出现上述不兼容问题时,我们希望程序能自己检查出来并提前报错,而不是悄无声息的,等业务数据出错了才发现问题。方法就是给shmhead添加一个版本号字段:
#define SHM_VERSION 2
typedef struct shmhead
{
int version;
int size;
int field1;
int field2;
int field3;
int fieldnew;
} shmhead;
写者负责维护:
shm->version = SHM_VERSION;
读者负责校验:
if (shm->version != SHM_VERSION)
{
fprintf(stderr, "shm version error\n");
return -1;
}
运行效果如下:
4.总结
要实现共享内存结构兼容,可以遵循如下建议:
- 修改结构体时,只在原结构体尾部追加字段,不修改或删除原有字段
- 每个结构体提供一个size字段
- 由写者填写为sizeof的结果
- 读者使用size字段进行共享内存偏移量的计算
- 共享内存头结构应提供version字段
- 由写者维护,如果修改后导致结构跟之前不兼容,则字段值加1,否则不变
- 由读者检查,如果共享内存中读到的版本号与当前版本号不相等,则报错退出
5.附件
完整示例代码: