如何实现共享内存结构兼容

如何实现共享内存结构兼容

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.附件

完整示例代码:

发表回复