前言
总结一下最近对于共享内存的学习, 可能比较浅显或者有疏漏, 欢迎指正!
原理初探
我们知道, 进程空间相互隔离, 互相对立, 但是共享内存允许多个
进程可以访问同一块
内存来达到进程间通信的目的.
共享内存是最高效的 IPC 机制, 它不涉及任何进程间的数据传输, 而且他和进程同处于用户空间, 不像消息队列, 信号量是内核空间的系统对象, 不需要花费额外的数据拷贝, 但是同时他并没有预防竞态条件, 也就是说在多进程利用共享内存进行通信的情况下, 我们需要自己去利用锁等操作来进行同步
共享内存的创建过程
当我们创建了一块共享内存, 其实是在 tmpfs
中创建了一个文件 (这个文件是存储于内存的), 也就意味着在 tmpfs 中创建了一个 iNode
节点
然后我们需要将这个创建好的文件映射到进程中 (如下图, 此图来自网络, 应该是哪个博客或者知乎吧…已经记不清了)
我们创建的这块共享内存不会随着进程的结束而被释放, 他会在关机前一直存在于内存中, 除非被进程明确的删除
也可以使用 `ipcrm + 共享内存 id ` 命令删除
比如这里我在刚开始练习的时候没有删除创建的共享内存
系列 API 的使用
ftok
#include <sys/shm.h>
#include <sys/types.h>
key_t ftok ( const char* fname, int fd);
成功返回一个key_t值, 失败返回-1
fname
参数是一个必须存在且能访问到的目录
id
子序号, 自己约定, 只有8个比特位被指用
注意, 当文件路径和子序号都相同时返回的并不一定永远返回一样的key值, 如果该路径指向的文件或者目录被删除而又重新创建, 就算名字还是一样, 但是文件系统会赋予它不同的 iNode 信息, 所以返回的 key 值就不同了
shmget
#include <sys/shm.h>
int shmget (key_t key, size_t size, int shmflag);
用来创建一块共享内存并返回其 id
或者获得一块已经被创建的共享内存的 id
成功返回一个正整数值
, 这个整数是共享内存的标识符, 失败时返回 -1
, 错误存储于 errno
key_t key
参数用来唯一标识一段全局共享内存, 通常通过 ftok
函数获得,
size_t size
参数是创建的共享内存的大小, 单位为字节, 如果是要创建一块共享内存, 此参数必须被指定, 如果是要获取一块创建好的共享内存的 id, 可以将其设置为 0
int shmglag
参数为 0 为获取共享内存 id, 为 IPC_CREAT
时是创建一个新的共享内存, 通常要同时指定权限 (和权限进行 | 运算)
同时还能取IPC_EXCL
, 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
使用
shmget
创建的共享内存段会全部被初始化为 0, 同时和他关联的内核数据结构shmid_ds
将被创建和初始化但是我尝试发现并没有被初始化为0, 请谨慎使用
struct shmid_ds {
struct ipc_perm shm_perm; /* 操作权限 */
size_t shm_segsz; /* 大小,单位是字节 */
__kernel_time_t shm_atime; /* 对这段内存最后一次调用shmat的时间 */
__kernel_time_t shm_dtime; /* 最后一次调用shmdt的时间*/
__kernel_time_t shm_ctime; /* 最后一次调用shmctl的时间*/
__kernel_ipc_pid_t shm_cpid; /* 创建者的pid*/
__kernel_ipc_pid_t shm_lpid; /* 最后一次执行shmat或shmdt的进程的pid*/
unsigned short shm_nattch; /*目前关联到次共享内存的进程的数量*/
unsigned short shm_unused; /* 以下为填充 */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
shmat
前面我们也说道, 我们创建共享内存其实是创建了一个文件, 然后要把它映射到当前进程, 即, 我们在使用 shmget
创建了一块共享内存之后, 要使用 shmat
将他关联到当前进程, 同样的使用完不再需要之后, 也需要使用 shmdt
将其分离, 让我们先看 shmat
#include <sys/shm.h>
void* shmat ( int shm_id, const void* shm_addr, int shmflag );
成功返回映射到进程的地址空间 失败返回 (void *)-1并将错误存储于 errno
shm_id
参数是 shmget 返回的共享内存 id,
shm_addr
参数是指定共享内存在进程内存地址的映射位置, 推荐使用 NULL
, 由内核自己决定
shmflag
一般为0
shmat
调用成功后, 会修改 shmid_ds
的部分字段:
将 shm_nattach 加一
将 shm_lpid 设置为调用进程 pid
将 shm_atime 设置为当前时间
shmdt
在共享内存使用完之后使用此函数将其从进程地址空间分离
#include <sys/shm.h>
int shmdt ( const void* shm_addr );
成功返回 0, 失败返回 -1
shm_addr
参数是共享内存在进程的映射地址, 即 shmat 返回的值
调用 shmdt 并不会删除共享内存
调用成功时修改内核数据结构 shmid_ds
部分字段:
将 shm_nattach 减一
将 shm_lpid 设置为调用进程的pid
将 shm_dtime 设置为当前时间
shmctl
管理共享内存
#include
int shmctl ( int shm_id, int command, struct shmid_ds* buf );
失败返回-1, 并存储错误于 errno, 成功时的返回值取决于 command
shm_id
是共享内存标识符
command
指定要执行的命令
常用命令为 IPC_RMID
, 即, 删除共享内存
如果共享内存已经与所有访问它的进程断开了连接,则调用IPC_RMID子命令后,系统将立即删除共享内存的标识符,并删除该共享内存区,以及所有相关的数据结构;
如果仍有别的进程与该共享内存保持连接,则调用IPC_RMID子命令后,该共享内存并不会被立即从系统中删除,而是被设置为IPC_PRIVATE状态,并被标记为"已被删除";直到已有连接全部断开,该共享内存才会最终从系统中消失。
共享内存实例
通过共享内存进行同机间的进程间通信
创建一个长度为10的 Stu 数组, 向其中写入数据, 另一个进程读取
代码很简单, 就不做注释了
/*
write.cpp
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
typedef struct Stu
{
int age;
char name[10];
} Stu;
int main()
{
int id = shmget((key_t)1234, sizeof(Stu) * 10, IPC_CREAT | 0644);
if (id == -1)
{
perror("shmget "), exit(1);
}
Stu* t = (Stu *)shmat(id, NULL, 0);
for (int i = 0; i < 5; i++) //初始化前五个
{
(t + i)->age = i;
strcpy((t + i)->name, "lvbai");
}
shmdt(t);
return 0;
}
/*
read.cpp
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
typedef struct Stu
{
int age;
char name[10];
} Stu;
int main()
{
int id = shmget((key_t)1234, 0, IPC_CREAT);
if (id == -1)
{
perror("shmget "), exit(1);
}
void* p = NULL;
p = shmat(id, NULL, 0);
if (p == (void *)-1)
{
perror("****** : ");
}
Stu* ptr = (Stu *)p;
printf("here is message : \n");
for (int i = 0; i < 10; i++)
{
printf("age = %d, name = %s\n", (ptr + i)->age, (ptr + i)->name);
}
shmdt(p);
shmctl(id, IPC_RMID, 0);
return 0;
}