1 什么是共享内存
进程具有独立性,通信的前提就是要保证两份毫不相关的进程看到同一份资源,在物理地址空间开 辟一块空间,这块空间被称之为共享内存,让不同的进程通过某些函数接口访问到这块共享内存,这样 就实现了进程间通信。
目前存在多种共享内存实现方式,其中包括:
传统 SYSTEM V 共享内存:早期UNIX系统中提供的共享内存方式,使用系统调用来进行共享内存操 作。它涉及的主要函数包括 shmget 、 shmat 、 shmdt 、 shmctl
POSIX 共享内存:利用 mmap 系统调用将文件映射到内存中,从而实现共享内存
merfd_create()接口和 fd跨进程式:使用 memfd_create() 系统调用创建内存文件描述符, 并将其通过文件描述符在多个进程之间共享
基于 dma_buf 的共享内存:主要应用于多媒体、图形领域,可以实现在不同设备间高效地共享数 据
System V 和 POSIX 都是 UNIX 系统中的标准,用于规范 UNIX 系统的接口和功能。 System V 是 AT&T公司发布的 UNIX 系统版本,它包括许多标准化的系统调用和库函数,例如 shmget() 、
semget() 、 msgget() 等,用于实现进程间通信和同步机制。 System V 的 IPC 机制具有较高的性能 和可定制性,但也较为复杂,使用起来需要一定的经验和技巧。
POSIX 是 Portable Operating System Interface 的缩写,是一个由 IEEE 组织发布的 UNIX 标准。它规定了操作系统的接口和功能,包括进程管理、文件系统、网络通信、线程、信号等方面。
POSIX 标准的目的是使不同的 UNIX 系统之间具有良好的兼容性,使程序在不同的 UNIX 系统上能够编 译和运行。
POSIX 的 IPC 机制包括信号量、消息队列、共享内存等,与 System V 的 IPC 机制类似,但更 加简单和易用。 POSIX 标准还规定了 Pthreads(POSIX threads) 线程库,用于实现多线程编程,具 有较高的可移植性和性能。
需要注意的是, System V 和 POSIX 是两种不同的标准,它们的 API 和实现方式并不完全相同。
在编写 UNIX 程序时,需要根据具体的需求和环境选择合适的标准和接口。
这里主要介绍一下 SYSTEM V 和 POSIX 共享内存。
| 共享内存 |
2.1 SYSTEM V 共享内存原理
在Linux中,每个进程都有属于自己的进程控制块 PCB 和地址空间 Addr Space ,并且都有一个与 之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元 MMU 进行管理。两个不 同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通 信。
在使用共享内存通信的时候,没有使用任何接口。双方进程在通信时,是直接对内存进行访问、操 作,属于内存级的读和写。因此在通信的时候,可以减少拷贝次数,不需要和内核进行交互,因此共享 内存是所有进程间通信最快的
注:关于页表的补充:
进程地址空间里有一个内核区域,它们也会在实际物理内存开辟空间,也会有页表与那块空间形成 映射关系,这个页表叫做内核级页表。因为内核只有一个,所以每个进程都相同的。说明进程都共用实 际物理内存上的内核空间。
除内核空间以外的空间,与实际物理空间之间的页表,称为用户级页表。每个进程可能不同。
2.2 SYSTEM V共享内存存在的缺陷
共享内存并未提供同步机制。也就是说,共享内存需要用户去通过某种同步技术,比如信号量、互 斥量或某种其他允许进程按照某种顺序访问资源的方式,来避免冲突和数据不一致的情况。
共享内存本身没有内建的引用计数机制。引用计数是一种资源管理技术,用于跟踪一个资源(例如 共享内存)被多少个进程或对象引用。当引用计数为零时,资源可以被安全地释放,从而避免资源 泄漏。也就是说,在使用共享内存时,需要开发者自行管理对共享内存的连接和断开,以确保正确 地使用共享内存资源。通常,创建共享内存的进程或对象需要记录连接的进程数或对象数,并在不
再需要共享内存时,适时地断开连接或释放共享内存。
2.3 SYSTEM V 共享内存接口
· ftok 函数:算出一个唯一的key返回
#include <sys/types.h> #include <sys/ipc.h> /** * @brief: convert a pathname and a project identifier to a System V IPC key * @param[in] pathname: 地址 * @param[in] proj_id: 至少8为的项目id * @return key_t: 如果成功返回一个key值,如果失败返回-1 */ key_t ftok(const char *pathname,int proj_id); |
shmget 函数:创建一个共享内存
#include <sys/types.h> #include <sys/ipc.h> /** * @brief: 创建或获取一个共享内存区 * @param[in] key: 为共享内存的名字,一般是ftok的返回值 * @param[in] size: 共享内存的大小,以page为单位,大小为4096的整数倍 * @param[in] shmflg: 权限标志,常用两个,IPC_CREAT和IPC_EXCL,一般后面还加一个权 限,相当于文件的权限,例如:IPC_CREAT | IPC_EXCL | 0666 * @param[in] IPC_CREAT: 创建一个共享内存返回,已存在打开返回 * @param[in] IPC_EXCL: 配合着IPC_CREAT使用,共享内存已存在则出错返回 * @return int: 共享内存区的标识符 */ int shmget(key_t key,size_t size,int shmflg); |
注1: key 是内核级别的,供内核标识, shmget() 返回值是用户级别的,供用户使用
注2: IPC 资源生命周期不随进程,而是随内核的,不释放会一直占用,除非重启
注3:可以使用 ipcs -m 查看SYSTEM V共享内存
注4: shmget 创建的共享内存要释放掉,不然会内存泄漏
注5:可以用命令行来释放共享内存: ipcrm -m shmid(shmget()返回值) ,但是如果挂接 进程数不为0时,并不会立即释放共享内存,而是先设置删除状态 dest ,直到挂接进程数为 0时才真正释放共享内存
shmat 函数 :使创建的共享内存与调用该函数进程的进程地址空间参数关联
#include <sys/shm.h> /** * @brief: 将共享内存段映射到调用进程的地址空间 * @param[in] shmid: shmget()函数返回的共享内存标识符 * @param[in] shmaddr: 指定映射地址的指针,如果为NULL,则由系统自动选择映射地址 * @param[in] shmflg: 标志参数,用于指定映射方式和权限 * @return void*: 返回指向共享内存段的指针,如果出错则返回(void *)-1 */ void *shmat(int shmid,const void *shmaddr,int shmflg); |
shmflg标志参数:
SHM_RDONLY :只读方式映射共享内存段
SHM_RND :将映射地址舍入为 SHMLBA 的倍数
SHMLBA 是一个常量,表示共享内存段的基本长度。它是一个系统级别的常量,在 Linux 系统中通常定义为 4096 (即 4KB),具体取决于系统架构和配置。
SHM_EXEC :允许在映射地址上执行程序代码
0 :系统默认
注:在使用完共享内存段后,应使用 shmdt() 函数将其从调用进程的地址空间中分离
shmdt 函数:删除共享内存与进程地址空间的映射关系,将页表映射关系删除,释放进程地址空间
#include <sys/shm.h> /** * @brief: 将共享内存段从调用进程的地址空间中分离 * @param[in] shmaddr: 共享内存映射到进程地址空间的地址,是shmat()返回值 * @return int: 成功返回0,失败返回-1 */ int shmdt(const void *shmaddr); |
注1:在使用共享内存时,每个进程都应该在使用完共享内存后使用 shmdt() 函数将其分
离,以释放系统资源。如果一个进程崩溃或退出而没有调用 shmdt() 函数,内核会自动将共 享内存段从该进程的地址空间中分离。但是,这样会导致共享内存的资源泄漏,因此应该尽可 能避免这种情况的发生。
注2:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl 函数:控制共享内存段的各种属性和操作
cmd命令:
IPC_STAT :获取共享内存段的状态信息,将其存储在 buf 指向的 shmid_ds 结构体 中
IPC_SET :设置共享内存段的状态信息,使用 buf 指向的 shmid_ds 结构体中的值
IPC_RMID :删除共享内存段,释放其占用的系统资源。
注意:
1. 这里只是将共享内存标记为删除状态,并不是直接删除。
2. 当共享内存被标记为删除状态之后,并不会马上被删除,直到所有的进程全部和共享 内存解除关联,共享内存才会被删除。
上面两条其实是和使用 ipcrm -m shmid 命令一样的。
3. 因为通过 shmctl() 函数只是能够标记删除共享内存,所以在程序中多次调用该操作 是没有关系的
2.4 关键点
前面提到过, 共享内存并未提供同步机制,因此需要应用自己去实现同步机制。在这里,我使用的 是互斥锁 mutex 。这里共享内存的结构示意图如下: