学科分类
目录
Linux编程

共享内存

共享内存允许两个或多个进程访问给定的同一块存储区域。已知当一个进程被启动时,系统会为其创建一个0~4G的虚拟内存空间,根据虚拟地址与物理地址之间的映射关系,进程可以通过操作虚拟地址,实现对物理页面的操作。一般情况下,每个进程的虚拟地址空间会与不同的物理地址进行映射,但是当使用共享内存进行通信时,系统会将同一段物理内存映射给不同的进程。两个进程的虚拟地址空间与共享内存之间的映射关系如图1所示。

图1 映射关系

系统中的物理内存和虚拟内存都通过页面来管理,为多个进程分配共享内存,实际是为进程分配一个或多个物理页面,因此,共享内存的大小必须是系统中页面大小的整数倍。若进程需要使用共享内存,应首先将虚拟内存空间与共享内存进行映射,映射完成后,进程对虚拟地址的读写,就相当于直接对物理内存的读写。另外,与申请堆空间类似,当通信完成之后,也应释放物理内存,解除进程与共享内存的映射关系。

内存映射也是效率最高的一种进程通信方式,它节省了不同进程间多次读写的时间:若有多个进程将自己的虚拟地址与此块物理内存进行绑定,那么当一个进程对此块内存中的数据进行修改时,其它进程可以直接获得修改后的数据。当然,在写进程的操作尚未完成时,不应有进程从共享内存中读取数据,共享内存自身不限制进程对共享内存的读写次序,但程序开发人员应自觉遵循读写规则,一般情况下,共享内存应与信号量一起使用,由信号量帮它实现读写操作的同步。

Linux内核提供了一些系统调用,用于实现共享内存的申请、管理与释放,这些函数分别为:shmget()、shmat()、shmdt()和shmctl(),下面将分别对它们进行讲解。

① shmget()

shmget()函数的功能是创建一块新的共享内存,或打开一块已经存在的共享内存,该函数存在于函数库sys/shm.h中,其定义如下:

int shmget(key_t key, size_t size, int shmflg);

shmget()函数若调用成功,将会返回一个共享内存标识符(该标识符是一个非负整数);若调用失败,将会返回-1,并对errno进行设置。

shmget()函数中的第一个参数key通常为整数,代表共享内存的键值;参数size用于设置共享内存的大小;参数shmflg用于设置shmget()函数的创建条件(一般设置为IPC_CREAT或IPC_EXCL)及进程对共享内存的读写权限。

② shmat()

shmat()函数的功能是进行地址映射,将共享内存映射到进程虚拟地址空间中,该函数存在于函数库sys/shm.h中,其定义如下:

void *shmat(int shmid, const void *shmaddr, int shmflg);

shmat()函数若调用成功,会返回映射的地址,并更改共享内存shmid_ds结构中的属性信息;若调用失败,会返回-1,并设置errno。

shmat()函数中的第一个参数shmid为共享内存标识符,该标识符一般由shmget()函数返回;参数shmaddr为一个指针类型的传入参数,用于指定共享内存映射到虚拟内存时的虚拟地址,当设置为NULL时,映射地址由系统决定;参数shmflg用于设置共享内存的使用方式,若shmflg设置为SHM_RDONLY,则共享内存将以只读的方式进行映射,当前进程只能从共享内存中读取数据。

③ shmdt()

shmdt()函数的功能是解除物理内存与进程虚拟地址空间的映射关系,该函数存在于函数库sys/shm.h中,其定义如下:

int shmdt(const void *shmaddr);

shmdt()函数中的参数为shmat()函数返回的虚拟空间地址,函数调用成功则返回0,并修改共享内存的shmid_ds结构中的属性信息;否则返回-1。

④ shmctl()

shmctl()函数的功能是对已存在的共享内存进行操作,具体的操作由参数决定,该函数存在于函数库sys/shm.h中,其定义如下:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmctl()函数调用成功则返回0,否则返回-1,并设置errno。该函数中的参数shmid表示共享内存标识符;参数cmd表示要执行的操作,常用的设置为IPC_RMID,功能为删除共享内存;参数buf用于对共享内存的管理信息进行设置,该参数是一个结构体指针,这个结构体是一个为了方便对共享内存进行管理,由内核维护的存储共享内存属性信息的结构体,该结构体的类型定义如下:

struct shmid_ds {
    struct ipc_perm shm_perm;        //所有者和权限标识
    size_t          shm_segsz;        //共享内存大小
    time_t          shm_atime;        //最后映射时间
    time_t          shm_dtime;        //最后解除映射时间
    time_t          shm_ctime;        //最后修改时间
    pid_t           shm_cpid;            //创建共享内存进程的id
    pid_t           shm_lpid;            //最近操作共享内存进程的id
    shmatt_t        shm_nattch;        //与共享内存发生映射的进程数量
    ...
};

需要注意的是,共享内存与消息队列以及信号量相同,在使用完毕后都应该进行释放。另外,当调用fork()函数创建子进程时,子进程会继承父进程已绑定的共享内存;当调用exec()函数更改子进程功能以及调用exit()函数时,子进程中都会解除与共享内存的映射关系,因此在必要时仍应使用shmctl()函数对共享内存进行删除。

下面通过一个案例,来展示共享内存通信中函数接口的使用方法。

案例6:创建两个进程,使用共享内存机制实现这两个进程间的通信。

shm_w.c

 1    #include <stdio.h>
 2    #include <sys/ipc.h>
 3    #include <sys/shm.h>
 4    #include <sys/types.h>
 5    #include <unistd.h>
 6    #include <string.h>
 7    #define SEGSIZE 4096            //定义共享内存容量
 8    typedef struct{                    //读写数据结构体
 9        char name[8];
 10        int age;
 11    } Stu;
 12    int main()
 13    {
 14        int shm_id, i;
 15        key_t key;
 16        char name[8];
 17        Stu *smap;
 18        key = ftok("/", 0);            //获取关键字
 19        if (key == -1)
 20        {
 21            perror("ftok error");
 22            return -1;
 23        }
 24        printf("key=%d\n", key);
 25         //创建共享内存
 26        shm_id = shmget(key, SEGSIZE, IPC_CREAT | IPC_EXCL | 0664);
 27        if (shm_id == -1)
 28        {
 29            perror("create shared memory error\n");
 30            return -1;
 31        }
 32        printf("shm_id=%d\n", shm_id);
 33        smap = (Stu*)shmat(shm_id, NULL, 0);    //将进程与共享内存绑定
 34        memset(name, 0x00, sizeof(name));
 35        strcpy(name, "Jhon");
 36        name[4] = '0';
 37        for (i = 0; i < 3; i++)                    //写数据
 38        {
 39            name[4] += 1;
 40            strncpy((smap + i)->name, name, 5);
 41            (smap + i)->age = 20 + i;
 42        }
 1        if (shmdt(smap) == -1)                    //解除绑定
 2        {
 3            perror("detach error");
 4            return -1;
 5        }
 43        return 0;
 44    }

shm_r.c

 6    #include <stdio.h>
 7    #include <string.h>
 8    #include <sys/ipc.h>
 9    #include <sys/shm.h>
 10    #include <sys/types.h>
 11    #include <unistd.h>
 12    typedef struct{
 13        char name[8];
 14        int age;
 15    } Stu;
 16    int main()
 17    {
 18        int shm_id, i;
 19        key_t key;
 20        Stu *smap;
 21        struct shmid_ds buf;
 22        key = ftok("/", 0);                //获取关键字
 23        if (key == -1)
 24        {
 25            perror("ftok error");
 26            return -1;
 27        }
 28        printf("key=%d\n", key);
 29        shm_id = shmget(key, 0, 0);        //创建共享内存
 30        if (shm_id == -1)
 31        {
 32            perror("shmget error");
 33            return -1;
 34        }
 35        printf("shm_id=%d\n", shm_id);
 36        smap = (Stu*)shmat(shm_id, NULL, 0);    //将进程与共享内存绑定
 37        for (i = 0; i < 3; i++)                    //读数据
 38        {
 39            printf("name:%s\n", (*(smap + i)).name);
 40            printf("age :%d\n", (*(smap + i)).age);
 41        }
 42        if (shmdt(smap) == -1)                    //解除绑定
 43        {
 44            perror("detach error");
 45            return -1;
 46        }
 47        shmctl(shm_id, IPC_RMID, &buf);            //删除共享内存
 48        return 0;
 49    }

分别使用如下两条命令编译程序shmserver.c和shmclient.c:

[itheima@localhost ~]$ gcc shm_w.c -o shm_w
[itheima@localhost ~]$ gcc shm_r.c -o shm_r

在终端中运行可执行程序,先执行shm_w创建共享内存,并向共享内存中写入数据;之后使用shm_r从共享内存中读取数据,数据读取完毕之后将共享内存删除。程序执行后终端打印的信息分别如下。

./shm_w //写

key=131074
shm_id=819217

./shm_r //读

key=131074
shm_id=819217
name:Jhon1
age20
name:Jhon2
age21
name:Jhon3
age22

之后再次执行程序shm_r,终端打印的信息如下:

key=131074
shmget error: No such file or directory

由打印结果可知,共享内存区域已在程序shm_r执行结束前被删除。

结合以上打印结果可知,案例6实现成功。

多学一招:struct ipc_perm结构体

观察消息队列、信号量及共享内存机制的函数接口与属性信息结构体,可以发现,它们包含一个相同的结构体,即struct ipc_perm结构体。该结构体同样由内核管理,用于设置Sys V IPC系列通信机制中介质(队列、信号量、共享存储段)的访问权限和权限标识,该结构体的定义如下:

struct ipc_perm {
    key_t          __key;            //键值
    uid_t          uid;            //所有者有效用户id
    gid_t          gid;            //所属组有效组id
    uid_t          cuid;            //创建者有效用户id
    gid_t          cgid;            //创建者有效组id
    unsigned short mode;        //访问权限
    unsigned short __seq;        //序列号
};

ipc_perm结构体在Sys V IPC系列通信机制调用各自的get函数(msgget()、semget()、shmget())时创建,创建时除序列号外,其它信息都会被初始化;之后该结构体由内核进行管理,除超级用户外,只有创建该结构体的进程可以通过各自的ctl函数(msgctl()、semctl()、shmctl())修改uid、gid和mode信息,其中mode信息与open()函数中的mode类似,但ipc_perm的mode没有执行权限。

点击此处
隐藏目录