序言
在前一篇文章中,我们介绍了名为 👉管道 的进程间通信的方式,该种方式又可分为 匿名管带,命名管道。前者最大的特点就是 仅支持包含血缘关系两进程之间的通信
,而后者 支持任意进程间的通信
。
在本篇文章中,我们将介绍一种新的进程间通信方式 共享内存,它允许多个进程或线程访问同一块内存区域,以实现数据的高效共享和同步。
1. 共享内存的原理
我们在之前提到一个概念就是 让不同的进程看到同一份操作系统的资源(如文件,内存等)
,共享内存无非就是让不同的进程看到内存中的同一块内存区域。但是具体的细节是怎么样的呢?请看图:
现在我们看图说话,描述怎么实现使用共享内存进行通信:
申请空间
:需要向操作系统申请在内存上的一块指定大小的空间。内存映射
:虽然申请到了空间,但是该空间并不能为进程所用,因为进程地址空间上实际并未拿到那块空间,所以需要使用页表建立映射关系
,将该地址的虚拟地址存储到进程地址空间的共享区中。数据访问
:程序可以通过指针来写入或读取这些内存的数据。释放空间
:当我们结束通信时,一定要释放申请的空间。但是管道都不需要手动释放呀,为什么共享内存需要呢?因为管道的生命周期随进程,而共享内存的生命周期随系统
。
2. 相关的指令和函数
环境:Linux 操作系统
2.1 指令:ipcs -m
通过该指令我们可以用于显示当前系统中的共享内存段的信息:
当前我们系统下就存在三个共享存储段。
2.2 指令:ipcs -m [shmid]
通过该指令可以删除指定的共享内存段。
注意:使用 shmid,而不是 key!!!
2.3 函数:shmget
该函数用于创建或者是获取共享内存段的系统调用函数:
int shmget(key_t key, size_t size, int shmflg);
:
key
:共享内存的键值(key
)。这是一个用于标识共享内存段的唯一标识符
。size
:共享内存段的大小(以字节为单位)。这个值必须大于 0,并且需要是系统页面大小的整数倍(尽管大多数系统允许自动向上舍入到最近的页面大小)。shmflg
:模式标志和权限。这个参数是一个位掩码,可以组合多个标志位和权限位。
- 返回值:成功时,返回共享内存段的标识符(一个非负整数)。这个标识符是后续
对共享内存段进行操作的唯一标识。
在这里我们好好聊聊其中几个大家可能有疑惑的参数:
首先是 key
和 返回值
:
为什么我们需要指定 key
呢?我反问大家一个问题,两个进程要使用共享内存通信,但两个进程怎么获取同一块内存空间呢?举个栗子:操作系统给我们分配内存空间就像给我们建立一个房子,现在有多少个进程之间需要通信,那操作系统就分配多少个房间,那怎么知道哪间房间是谁的呢?进程为自己房间都装一个门牌号!而 key
就是门牌号的内容。
那返回值呢,又是干嘛的呢?操作系统把门牌号给你装好了,返回值就是给你配的房卡,你不用记住门牌号,用房卡就行。
其次就是 shmflg
:
常见的标志位包括 IPC_CREAT
(如果共享内存不存在,则创建它)和 IPC_EXCL
(与 IPC_CREAT
一起使用,确保创建的共享内存是新的,如果已存在则返回错误)。权限位(如 0666
)指定了共享内存的访问权限,这些权限将和进程的umask值进行逻辑与操作来确定最终的权限,
所以一般情况下,你需要创建一个共享内存,那你的标志位就是 IPC_CREAT | IPC_EXCL | 0666
;
需要获取一个共享内存,标志位是:IPC_CREAT | 0666
;
2.4 函数:ftok
我们需要对 shmget
函数传入一个唯一的 key
(不能和其它进程起冲突) ,我们可以直接使用这个函数为我们生成:
key_t ftok(const char *pathname, int proj_id);
pathname
:指向文件路径的指针,这个文件通常是项目中的一个已知文件或目录。虽然文件路径不需要指向一个实际存在的文件,但必须是唯一的,以便在不同的项目或实例中生成不同的键。proj_id
:一个 8 位( 1 字节)的整数,通常用于进一步区分同一路径下的不同键。尽管proj_id
是int
类型,但实际上只使用其低 8 位(即0-255的范围)。- 返回值:如果成功,
ftok
函数返回一个唯一的键(key_t
类型)。失败返回 -1 。
2.5 函数:shmat
在原理中,我们说到,我们需要将操作系统为我们申请的空间映射到进程的进程地址空间上,这个函数 void *shmat(int shmid, const void *shmaddr, int shmflg);
就是实现这一功能的函数:
-
shmid
:共享内存标识符,由shmget
函数返回。 -
shmaddr
:指定共享内存出现在进程内存地址的什么位置。如果直接指定为NULL
,则让内核自己决定一个合适的地址位置。 -
shmflg
:标志位,通常使用SHM_RDONLY表示以只读模式映射共享内存,否则为读写模式。
注意:一般没什么特殊需求后两位都使用默认
-
返回值
: 成功时,返回映射的共享内存区域的起始地址
;出错时,返回(void *)-1
,并将错误原因存储在errno
中。
大家可能对 void *
指针有些疑惑,这类指针就是不指向任何类型的指针,就像一张白纸,你需要告诉他,他具体操作的数据。(malloc 函数的返回的也是 void 指针,需要强转为你需要的指针
)
2.6 函数:shmdt
这个函数的作用是 分离共享内存段
,请注意!这不是释放共享内存,相当于只是解除了映射关系,但是该共享内存还存在!int shmdt(const void *shmaddr);
shmadd
r:这是一个指向先前由shmat
调用返回的共享内存段的指针。它指定了要分离的共享内存段的起始地址。- 返回值: 成功时,返回 0;出错时,返回 -1,并设置
errno
以指示错误类型。
2.7 函数:shmctl
该函数不仅可以释放共享内存,还可以查询共享内存的状态、修改其属性,
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
:共享内存标识符,即要控制的共享内存段的标识符。- cmd:控制命令,用于指定要执行的操作。常用的命令包括:
1.IPC_STAT
:获取共享内存的状态,将共享内存的shmid_ds
结构复制到buf
中。
2.IPC_SET
:改变共享内存的状态,将buf
所指的shmid_ds
结构中的uid、gid、mode
等成员复制到共享内存的shmid_ds
结构内。
3.IPC_RMID
:删除共享内存段。注意,删除利用了引用计数,当所有进程都进行删除后,才会真正删除。
buf
:指向struct shmid_ds
结构的指针。- 返回值:成功时,返回 0‘出错时,返回 -1,并设置
errno
以指示具体的错误原因。
在这里如果我们想要删除:
shmctl(shmid, IPC_RMID, NULL)
如果我们想要获取相应信息:
struct shmid_ds shmbuf;
shmctl(shmid, IPC_STAT, &shmbuf);
修改信息使用的很少,在这里就不介绍了。
3. 共享内存的特点
3.1 访问速度快
共享内存是进程间通信最快的方式。这是因为将数据写入内存,他只需要一次拷贝即可(读取一样)。与管道作比较,首先需要将你的内容保存在字符串中,再利用 write
函数将内容写入内存,需要两次拷贝。
3.2 动态分配
我们可以指定申请内存的大小,但是该大小必须是系统页面大小的整数倍。(Linux 系统下是 4KB
)
3.3 缺乏同步机制
共享内存本身不具有同步机制(比如,我正在写,还没写完,你就来读)。这可能会导致数据竞争和不一致的问题。但是我们可以使用 互斥锁,信号量
等来完善同步机制。
## 4. 总结 在这篇文章中向大家介绍了共享内存的原理以及常用的指令函数,希望大家有所收获!:grin: