【Linux系统编程】进程间的本地通信——共享内存

目录

前言:

一,认识并创建共享内存

1-1,认识共享内存

1-2,创建共享内存

1-3,共享内存的结构属性

二,进程眏射共享内存

三,进程分离共享内存

四,共享内存的删除

2-1,系统指令删除

2-2,代码层删除

五,共享内存的优缺点

5-1,共享内存的优点

5-2,共享内存的缺点

六,共享内存的综合代码实现


前言:

        进程的管道通信方式中,匿名管道通信只能用于具有血缘关系的进程;命名管道的通信要建立起管道文件。下面我们来认识进程通信的另一种方式:system V(一套由操作系统引入本地通信机制的标准)中的共享内存(另两种方式:消息队列和信号量网络部分会说明),它是进程间通信中最快的方式之一。


一,认识并创建共享内存

1-1,认识共享内存

        我们先来回顾一下有关进程结构的知识:进程task_struct结构、进程地址空间mm_struct、页表、内存。当创建进程时,物理内存会开辟一块空间,系统通过页表将此眏射到进程地址空间中的共享区里,将页表中的虚拟地址返回给用户。这里要说明的是,由于不同进程在物理内存中开辟的空间不同,也就导致了进程间的独立性,但若不同进程指向的物理内存都是同一块区域,那么不同进程间就可以形成通信。此内存就是共享内存,这也就是共享内存的原理。

1-2,创建共享内存

        由共享内存的原理可知,共享内存是可以将毫不相干的进程进行互相通信。系统中提供了系统调用shmget函数,用于创建共享内存,具体说明如下(最后会有综合代码演示):

头文件:

        #include <sys/ipc.h>

        #include <sys/shm.h>

原型:

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

参数:

        key:这个共享内存段名字,用于唯一标识一个共享内存段(共享内存往往存在多个)

        size:创建共享内存大小,以字节为单位。注意:在内核中,共享内存的大小是以4KB为基本单位的。因此系统中实际开辟的内存大小是n*4KB

        shmflg:一组标志位和权限码,标志位本质是一个宏,它们的用法和创建文件时使用的chmod 命令中的权限码相同,而常用的标志位有以下两个:       

  • IPC_CREAT:如果指定的共享内存段不存在,则创建它;如果共享内存已经存在,直接获取它
  • IPC_EXCL:不能单独使用(单独使用无意义),它与 IPC_CREAT 一起使用,当它们共同使用(IPC_CREAT | IPC_EXCL)使用时,如果共享内存段已存在,则调用失败返回;如果共享内存不存在,则创建它。此选项的意义是在于表示如果创建共享内存是成功的,则一定是一个新的共享内存。

返回值:

        成功返回一个非负整数——该共享内存段的标识符;失败返回-1,并设置错误码

注意:

        key_t通常被定义为一个整数类型,通常是至少32位的整数

        下面重点来说明下该函数的参数key和返回值。key用于在系统中查找或创建共享内存段,可以理解为共享内存的名字,而返回的标识符是用于后续对该共享内存段进行操作的唯一标识符。一个key可能对应多个标识符(例如,如果多个进程同时调用 shmget 并请求创建相同的共享内存段),但每个标识符都唯一地标识了一个特定的共享内存段实例,保证多个进程对应的是同一个共享内存。key的作用只有一个——在内核角度上区分共享内存(shm)的唯一性。shmget函数返回值(shmid:标识符)类似文件描述符,它是站在用户角度上对共享内存进行控制,无论是指令级,还是代码级,最后对共享内存进行控制用的都是函数返回值(shmid)。

        shmget函数中的key用户可随意取名,但是系统中往往存在多个共享内存,用户随意取名可能导致key重复,因此,系统中提供了ftok函数生成一个唯一的key(本质上是一种算法,根据参数生成对应的key)。虽然也有可能重复,但此算法大大降低了重复的概率,且没有访问内核,保证了效率。

头文件:

        #include <sys/types.h>  
        #include <sys/ipc.h>  

函数原型:
        key_t ftok(const char *pathname, int proj_id);

参数说明:

        pathname:指向文件路径的指针,这个文件通常是项目中的一个已知文件。这个路径不需要指向一个实际存在的文件,但必须是唯一的,以便在不同的项目或实例中生成不同的key。

        proj_id:虽然类型是int(4字节)型,但系统只会拿proj_id的低序8位(1字节),一般自定义为单个字符区分,通常用于进一步区分同一路径下的不同key。

返回值:

        如果成功,ftok函数返回一个唯一的key;如果失败,返回-1,并设置相应的错误码。

注意:

        由于key_t值是通过组合有限的信息位生成的,因此不能保证两个不同的路径名和proj_id组合一定会产生不同的key_t值。在极端情况下,可能会存在冲突。

1-3,共享内存的结构属性

        共享内存创建后,Linux内核会为每个共享内存段创建一个struct shmid_ds结构体对象管理这些共享内存段。这个结构体包含了共享内存段的所有属性信息,类似于文件系统中的inode节点。

struct shmid_ds

{

    struct ipc_perm shm_perm;  /*包含共享内存段的权限和所有者信息(key值就存储在该结构体中)*/

    int shm_segsz;  /*共享内存段的大小(以字节为单位)*/

    __kernel_time_t shm_atime;  /*最后一次访问共享内存段的时间*/

    __kernel_time_t shm_dtime;  /*最后一次从系统中删除共享内存段的时间(仅当使用 IPC_RMID 时才设置)*/

    __kernel_time_t shm_ctime;  /*共享内存段最后一次更改的时间*/

    __kernel_ipc_pid_t shm_cpid;  /*创建共享内存段的进程ID*/

    __kernel_ipc_pid_t shm_lpid;  /*最后一个操作该共享内存段的进程ID*/

    unsigned short shm_nattch;  /*当前附加到共享内存段的进程数*/

    void *shm_unused2;

    void *shm_unused3;

};


二,进程眏射共享内存

        共享内存由程序创建完毕后还不能直接被挂接到对应的进程中。shmget本身并不将共享内存段映射到任何进程的地址空间,它只是在内存中为共享内存段分配或预留了空间,即创建了共享内存,虽然该函数已为共享内存段分配了空间并返回了一个标识符,但这个标识符本身并不能直接用于访问共享内存中的数据,进程还不能使用它。若是进程要使用共享内存,必须将共享内存段映射到进程的地址空间。系统中专门提供了shmat函数解决这块问题。

        shmat用于将共享内存段映射到调用进程的地址空间。这是使进程能够访问共享内存段的关键步骤。shmat通过shmget函数返回的标识符找到该共享内存,然后返回一个指向共享内存段在调用进程地址空间中位置的指针。通过这个指针,进程可以像访问普通内存一样访问共享内存段中的数据。具体说明如下(最后会有综合代码演示)。

功能:

        将共享内存段映射到进程的地址空间

原型:

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

参数:

        shmid:共享内存标识

        shmaddr:指定连接共享内存段的起始地址。如果此参数为 NULL,则由系统选择起始地址。通常设置为NULL,让系统为你选择合适的地址,一般不会自行设置,因为可能存在地址冲突或系统策略不允许使用特定的地址范围。

        shmflg:此参数是连接标志,它的取值通常与 SHM_RDONLY 位或操作进行组合。如果设置了 SHM_RDONLY,则共享内存段将以只读方式连接到调用进程。默认情况下设为0(未设置 SHM_RDONLY),共享内存段以读写方式连接。此参数通常设置为0。

返回值:

        成功时返回共享内存在进程地址空间中实际的起始地址;失败时返回-1,并设置错误码


三,进程分离共享内存

        在Linux系统编程中,shmdt函数是用于从当前进程的地址空间中分离(或解除映射)一个由shmget创建的共享内存段。当一个或多个进程不再需要访问某个共享内存段时,它们应该调用shmdt来释放该内存段在它们各自地址空间中的映射。然而,需要注意的是,shmdt只是解除了进程与共享内存段的映射关系,它并不会删除或释放该共享内存段本身。共享内存段的删除和释放需要通过shmctl函数配合IPC_RMID命令来完成。具体说明如下(最后会有综合代码演示)。

头文件:

        #include <sys/shm.h> 

功能:

        将共享内存段与当前进程脱离

原型:

        int shmdt(const void *shmaddr);

参数:

        shmaddr:这是一个指向共享内存段在调用进程地址空间中的起始地址的指针。由shmat所返回的指针。

返回值:

        成功返回0;失败返回-1,并设置错误码

注意:

        该函数只是将共享内存段与当前进程脱离(解除了眏射),不等于删除共享内存段,使用该函数后共享内存段中的数据在物理内存中仍然存在,直到它被shmctl删除或系统重启。

        总结:shmgetshmat 和 shmdt 是Linux下操作共享内存的主要系统调用之一,它们分别用于创建共享内存段、将共享内存段映射到进程的地址空间以及从进程的地址空间解除映射。


四,共享内存的删除

        共享内存不跟文件一样——进程退出时文件由系统自动释放,但是如果我们没有特意释放共享内存,那么即便进程退出了,该共享内存还一值存在,下次若使用IPC_CREAT | IPC_EXCL标志位重新将源代码编译并执行可执行程序时将会报错说明该共享内存已存在。

        总的来说共享内存的生命周期随内核,文件的生命周期随进程。因此,我们要学会共享内存的删除操作。共享内存的删除有两种方式——指令删除代码删除

2-1,系统指令删除

        Linux系统专门提供了有关IPC(ipc表示不同进程间进行信息交换的机制)相关操作的系统调用。其中可以使用 ipcs指令查看共享内存是否已经被创建。ipcs默认会查找到三种资源:Message Queues(消息队列)、Shared Memory Segments(共享内存)、Semaphore Arrays(信号量)。该指令的选项 -m 表示查看指定用户创建的共享内存,即:ipcs -m。如下图:

        其中,shmid:标识符(函数返回值)。owner:当前用户。perms:权限。bytes:大小(单位是字节)。

        此外,IPC中还有专门用于删除进程间通信资源的ipcrm指令。说明如下:

格式:

        ipcrm [options] [id]

参数:

        options:表示选项,用于指定要删除的IPC资源的类型;

        id:是要删除的IPC资源的标识符,即生成指定通信方式函数的返回值。

常用选项:

 -m:删除共享内存资源。标识符:shmid

 -q:删除消息队列资源。标识符:msqid

 -s:删除信号量资源。标识符:semid

2-2,代码层删除

        代码层中,系统提供了用于控制共享内存段的系统调用shmctl函数,它可以用来查询或修改与共享内存段相关的属性以及删除共享内存。这里我们只重点说明它的删除功能。

        使用shmctl函数进行共享内存的代码删除这里要说明一下,这里的删除只是删除共享内存标识符,并不是直接清除共享内存中的数据,即删除共享内存的物理空间。删除共享内存标识符和删除共享内存的物理空间是两个不同的概念。删除标识符只是使得新的进程不能通过该标识符访问共享内存,但已经附加的进程仍然可以访问这块共享内存直到它们显式地解除附加(使用shmdt函数)。只有当最后一个附加的进程也解除了附加,并且没有其他的引用(如文件系统中的映射)指向这块共享内存时,系统才可能会回收这块内存(删除共享内存的物理空间)。因此,如果你想要确保共享内存被完全删除并回收其物理空间,你需要确保两点:

  1. 所有的进程都解除了对共享内存的附加(使用shmdt函数)。
  2. 没有其他的引用(如文件系统中的映射)指向这块共享内存。

格式与说明如下:

头文件:

        #include <sys/ipc.h>  
        #include <sys/shm.h>  

函数格式:
        int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

        shmid:共享内存段的标识符,即 shmget 调用成功时的返回值。

        cmd:指定要执行的操作,这是一个操作码。常用的值包括:    

  • IPC_STAT:获取共享内存段的状态,并将其存储在 buf 指向的 shmid_ds 结构体中(系统管理共享内存的数据结构)。
  • IPC_SET:在进程有足够权限的前提下,将 buf 指向 shmid_ds 结构体中的值设置共享内存段的状态值。
  • IPC_RMID:删除一个共享内存的标识符(不是删除共享内存的物理空间)。使用该选项时buf无需关心,将其设为nullptr即可。当使用它执行删除操作时,共享内存段中的数据不会立即被清除。只有在没有任何进程再附加到该共享内存段,并且具有执行指定操作的权限时,内核才会自动从内存中删除它。

        buf:一个指向管理共享内存 shmid_ds 结构体的指针。   

返回值:

        成功后返回 0,如果失败则返回 -1 ,并设置相应的错误码error。


五,共享内存的优缺点

5-1,共享内存的优点

        共享内存是所有进程间通信中速度最快的,原因主要有以下两点。

        首先,共享内存是直接内存访问的,数据交换可以直接在内存中完成,而无需通过内核进行中转,减少了数据在用户态和内核态之间的拷贝次数(通常,其他通信方式在数据交换时,需要先将数据从用户态拷贝到内核态,再由内核态拷贝到目标进程的用户态,这个过程涉及两次数据拷贝,而共享内存则完全避免了这些拷贝过程。)。

        其次,共享内存减少了系统调用。在共享内存的通信过程中,进程主要通过内存映射、访问和分离等操作来与共享内存段交互,这些操作相对于其他需要频繁进行系统调用的通信方式(如信号量、消息队列等)来说,系统调用的次数大大减少(系统调用是用户态与内核态之间交互的桥梁,其开销相对较大,因此减少系统调用次数可以有效提高通信效率)。

5-2,共享内存的缺点

        共享内存的通信默认情况下没有提供进程间协同的任何机制(同步机制),导致写入数据和读取数据的操作不一致,单独自己工作,完全不配合另一方,出现数据混乱,比如:读端运行时会一值读取数据,完全不管写端的任何操作;写端运行时会一直写入数据,完全不管读端是否读取。因此,共享内存通常需要用户自己提供同步机制。

        共享内存需要考虑互斥问题。对同一块共享内存区域进行访问时,必须采取一种机制,以确保在同一时间内只有一个进程或线程能够对该内存区域进行写操作,或者在读写操作中保证数据的一致性和完整性。这种机制是为了防止多个进程或线程同时修改同一数据而导致的竞态条件和数据不一致问题。


六,共享内存的综合代码实现

        共享内存在做完预备操作完,下面就要开始通信了,由于共享内存在数据同步和互斥访问方面存在问题,而管道通信正好解决了这一问题,所以这里可以利用管道机制来实现共享内存的同步和互斥机制(这里可自由选择设计)。有不少人可能疑惑,居然都要使用管道了,那么为什么还要共享内存呢?因为共享内存效率高,速度最快,这里使用管道只是利用管道自身的优势,并没有使用管道通信,相比直接使用管道,效率还是很高的。

        这里设置了一个Comm.hpp方法函数集,包含程序所必要的功能;还有利用管道优点和实现通信方法的Fifo.hpp类集;最后就是负责读取数据的服务端程序ShmServer.cc和负责写入数据的客户端程序ShmClient.cc。详细的代码说明及运用请在此链接下观看:Linux共享内存的代码运用

        最后说明一下,system V通信中的消息队列如今已很少使用了(本地通信其实也很少使用,目前流行的是网络通信),基本也被淘汰了,所以可自行简单了解即可。信号量是一个重点,下篇文章会详细说明。

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值