目录
1.共享内存实现通信的原理
2.如何使用共享内存实现通信
共享内存通信接口介绍
shmget
shmat
shmdt
shmctl
使用示例
key和shmid
3.共享内存通信的优缺点
缺点:不提供任何同步机制,可能会造成数据混乱。
优点:共享内存是进程间通信最快的方式。
1.共享内存实现通信的原理
“共享内存实现进程间通信”,我们从名字就可以看出,共享内存其实就是通过提供一块共享的内存来实现进程间通信。
共享内存在计算机系统中可能不止一个,而操作系统作为计算机系统中软硬件资源的管理者,肯定要管理好共享内存,就要为共享内存创建对应的数据结构,进程只要能够找到该数据结构对象的起始地址就能找到某个特定的共享内存来进行通信。
Linux2.6内核源代码中共享内存的数据结构如下:
struct shmid_ds {struct ipc_perm shm_perm; /* operation perms */int shm_segsz; /* size of segment (bytes) */__kernel_time_t shm_atime; /* last attach time */__kernel_time_t shm_dtime; /* last detach time */__kernel_time_t shm_ctime; /* last change time */__kernel_ipc_pid_t shm_cpid; /* pid of creator */__kernel_ipc_pid_t shm_lpid; /* pid of last operator */unsigned short shm_nattch; /* no. of current attaches */unsigned short shm_unused; /* compatibility */void *shm_unused2; /* ditto - used by DIPC */void *shm_unused3; /* unused */
};
共享内存进行通信的具体做法如下:
- 由操作系统在内存开辟一块区域,也就是提供进程间通信的场所。
- 构建进程的进程地址空间和共享内存的映射关系。保证不同的进程能够通过自己的进程地址空间来找到这块内存空间。
- 当通信结束的时候应该移除进程地址空间和共享内存的映射关系,减少系统资源的使用。
- 删除共享内存,避免造成资源泄漏的问题。
其原理图如下:
2.如何使用共享内存实现通信
为了使用共享内存进行进程间通信,操作系统给我们提供了一些系统调用接口,这些系统调用接口有不同的版本,我们主要学习 system V 版本 的共享内存接口,这些接口分别是 shmget、shmat、shmdt、shmctl。
共享内存通信接口介绍
shmget
功能:用于创建或获取一个共享内存段。
头文件:<sys/ipc.h> 、<sys/shm.h>
函数原型:int shmget(key_t key, size_t size, int shmflg)
参数:
- key_t key:这是共享内存段的键值,用于唯一标识共享内存段。可以通过
ftok
函数生成,也可以直接使用IPC_PRIVATE
创建一个新的共享内存段。如果key
是IPC_PRIVATE
,系统会创建一个新的共享内存段,且该段只能由当前进程及其子进程使用。- size_t size:表示共享内存段的大小,以字节为单位;如果是创建新的共享内存段,必须指定大小;如果是获取已存在的共享内存段,可以设置为 0。
- int shmflg:这是一个int类型的标志位,用于控制共享内存段的创建和访问权限;常用的创建标志位有这么两个,分别是 IPC_CREAT 和 IPC_EXEL,访问权限通过八进制数字来控制,两者通常使用按位或的方式来传参。
说明一下 IPC_CREAT 和 IPC_EXEL:
- IPC_CREAT:表示如果共享内存段不存在,则创建,如果共享内存已经存在,则获取。
- IPC_EXEL:单独使用时没意义,不能单独使用,需要和 IPC_CREAT 一起使用,表示共享内存不存在则创建,共享内存已经存在,则出错返回(一起使用时,需要使用 按位或 来操作)。
返回值:
- 成功时:返回共享内存段的标识符(shmid),用于后续操作(如
shmat
、shmctl
)。- 失败时:返回-1,并设置错误码来表示错误类型。
补充ftok函数:
前面说到了shmget的第一个参数可以通过ftok来获取,下面介绍一下ftok函数。
功能:ftok函数用于将一个文件路径和一个整数标识符(
proj_id
)转换为一个唯一的 IPC 键值(key_t 类型的值),这个键值可以用于创建或访问共享内存。头文件:<sys/ipc.h>
函数原型:key_t ftok(const char *pathname, int proj_id)
参数:
- const char* pathname:文件路径,通常是一个已存在的文件(这个参数由用户来指定);ftok会使用该文件的 inode 号 和 设备号 来生成键值,文件必须存在且可访问,否则 ftok 会失败。
- int proj_id:一个整数标识符(通常是一个字符,取值范围是
0
到255
),用于在同一个文件路径下生成不同的键值。返回值:
- 成功:返回生成的IPC键值。
- 失败:返回-1,并设置错误码指示错误类型。
shmat
功能:用于将共享内存段映射到当前进程的地址空间。
头文件: <sys/types.h> 、<sys/shm.h>
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg)
参数:
- int shmid:共享内存段的标识符,由
shmget
函数返回,用于指定要映射的共享内存段。- const void* shmaddr:指定共享内存段附加到当前进程地址空间的位置,通常设置为
NULL
,由系统自动选择合适的地址。(如果指定了地址,需要确保地址对齐和可用性,通常不建议手动指定)- int shmflg:标志位,用于控制共享内存段的映射行为。常用的标志位有
SHM_RDONLY和0。SHM_RDONLY
: 表示以只读方式映射共享内存段。0
: 以读写方式映射共享内存段(默认行为)。返回值:
- 成功时:返回共享内存段在进程地址空间中的起始地址(
void *
类型),我们可以通过这个地址直接访问共享内存。- 失败时:返回
(void *)-1
,并设置errno
以指示错误类型。
shmdt
功能:用于将共享内存段从当前进程的地址空间中分离。
头文件:<sys/types.h>、<sys/shm.h>
函数原型:int shmdt(const void *shmaddr)
参数:
- const void* shmaddr:共享内存段在进程地址空间中的起始地址(由
shmat
函数返回),用于指定要分离的共享内存段。返回值:
- 成功:返回0;
- 失败:返回-1,并设置错误码指示错误类型。
shmctl
在实际编程的时候,我们通常使用这个函数来释放共享内存。
需要注意:共享内存的生命周期是随内核的。也就是说,如果我们不释放使用完之后的共享内存,共享内存就会一直存在系统中,占用资源导致资源泄漏等问题;这一点基于文件的通信方式不同,文件的生命周期是随进程的,进程退出,文件自动就释放了。
功能:用于控制共享内存段的行为。它可以执行多种操作,例如获取共享内存段的信息、设置权限、删除共享内存段 等……
头文件:<sys/ipc.h>、<sys/shm.h>
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数:
- int shmid:共享内存段的标识符(由
shmget
函数返回),用于指定要操作的共享内存段。- int cmd:表示控制命令,用于指定要执行的操作。常用的选项有三个,分别是:IPC_STAT(获取共享内存段的状态信息,存储到
buf
指向的结构体中)
IPC_SET(设置共享内存段的权限和属性,如: UID、GID、模式等……)
IPC_RMID(标记共享内存段为删除状态,当最后一个进程分离该共享内存段时,它会被删除)- struct shmid_ds* buf:这是一个指向
shmid_ds
结构体 的指针(struct shmid_ds 就是共享内存的类型),用于存储或设置共享内存段的信息,当cmd被设置为 IPC_STAT 或IPC_SET 时,则需要提供该参数,否则可以设置为NULL。
返回值:
- 成功:返回0;
- 失败:返回-1,并设置错误码指示错误类型。
使用示例
我们编写两个程序:
- writer.c:往共享内存中一次写入一个字符。
- reader.c:将共享内存中的数据全部读出。
writer.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>#define SHM_SIZE 1024 // 共享内存段大小int main() {// 生成键值key_t key = ftok("./", 65);if (key == -1) {perror("ftok failed");exit(1);}// 创建共享内存段int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);if (shmid == -1) {perror("shmget failed");exit(1);}// 将共享内存段附加到当前进程的地址空间char *shmaddr = (char *)shmat(shmid, NULL, 0);if (shmaddr == (char *)-1) {perror("shmat failed");exit(1);}// 向共享内存写入数据char ch = 'a';char* addr = shmaddr;for(int i = 0; i < 26; ++i){*addr = ch+i;printf("我写入了一个字符:%c\n",*addr);addr++;sleep(1);}// 分离共享内存段if (shmdt(shmaddr) == -1) {perror("shmdt failed");exit(1);}// 删除共享内存段if (shmctl(shmid, IPC_RMID, NULL) == -1) {perror("shmctl failed");exit(1);}return 0;
}
reader.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#define SHM_SIZE 1024 // 共享内存段大小int main() {// 生成键值key_t key = ftok("./", 65);if (key == -1) {perror("ftok failed");exit(1);}// 获取共享内存段int shmid = shmget(key, SHM_SIZE, 0666);if (shmid == -1) {perror("shmget failed");exit(1);}// 将共享内存段附加到当前进程的地址空间char *shmaddr = (char *)shmat(shmid, NULL, 0);if (shmaddr == (char *)-1) {perror("shmat failed");exit(1);}// 读取共享内存中的数据while(1){printf("共享内存中的数据:%s\n", shmaddr);sleep(1);}// 分离共享内存段if (shmdt(shmaddr) == -1) {perror("shmdt failed");exit(1);}return 0;
}
运行结果:
- 从运行结果来看,两个进程之间成功通信了。
key和shmid
通过使用,你可能有这么一个疑问。key是共享内存的唯一标识,shmid也是共享内存的唯一标识,为什么要提供两个呢?
我们可以这么来理解:计算机系统中可能存在多个共享内存,操作系统肯定要管理多个共享内存,就需要能够唯一的确定一个共享内存,这一点是通过key来区分的;同时,创建出的共享内存需要被用户使用,如果直接将key提供给用户,不就代表用户可以访问操作系统内核的数据了吗?这一点是万万不可的,操作系统为了管理好整个计算机的软硬件资源,不能让用户直接访问其内核的数据,于是,操作系统提供给用户一个shmid,让用户通过shmid间接操作特定的共享内存。
也就是说,key是操作系统在内核中区分不同共享内存的全局唯一标识符;而shmid是内核分配给进程的,让用户在代码层面区分不同的共享内存的。
我们可以通过 ipcs -m 命令来查看系统中的共享内存:
共享内存使用完之后要记得释放,不然就会造成资源泄漏,我们除了使用代码删除共享内存,还可以使用 ipcrm -m shmid 命令来删除指定的共享内存:
3.共享内存通信的优缺点
缺点:不提供任何同步机制,可能会造成数据混乱。
先说缺点,管道是提供同步机制的,而共享内存并不提供任何进程同步的机制,写方不会管读方,读方也不会管写方,从我们前面的代码运行效果就可以看出:
读端刚开始读的时候,读到了三个abc,而没有读到a、ab这两个数据,说明写端一直在写,并没有管读端。
优点:共享内存是进程间通信最快的方式。
主要有以下几点原因:
直接访问内存减少拷贝:共享内存允许多个进程直接访问同一块物理内存区域,数据不需要在进程之间复制,而是直接在共享内存中读写,读写效率高。其他 IPC 机制(如管道、消息队列、Socket)通常需要在用户空间和内核空间之间复制数据,这会增加额外的开销。
减少进程上下文切换:其他 IPC 机制(如管道、消息队列)通常需要调用系统调用(如 read
、write
),这会触发用户态和内核态之间的上下文切换,增加开销;共享内存的读写操作完全在用户空间完成,不需要内核的介入。
不需要内核缓冲区:其他 IPC 机制(如管道、消息队列)需要内核维护缓冲区,增加了内存和 CPU 的开销。共享内存不需要内核维护额外的缓冲区,数据直接存储在共享内存区域中。