共享内存原理
简而言之,就是两个进程指向了同一块物理空间。(它们都能看到同一块内存)
共享内存在内核中同时可以存在很多个,OS要管理所有的共享内存。
如何保证两个不同进程看到的是同一个共享内存呢???要给共享内存提供唯一性标识(后文提到的key)!!!
使用共享内存通信,一定是一个进程创建新的shm,另一个直接获取共享内存即可。
类比:共享内存 vs 文件操作
共享内存,如果进程结束,我们没有主动释放它,则共享内存一直存在。——共享内存的生命周期随内核。(除非重启系统,否则共享内存一直存在)。
文件操作,一个进程打开一个文件,进程退出时,这个被打开的文件就会被系统自动释放掉。——文件的生命周期随进程。
shmget 系统调用函数
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明
第一个参数 key
1.这个key意义是什么?怎么形成的?
意义:标识共享内存的唯一性。
如何形成:由用户随意指定key值。
2.为什么要让用户传入?
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数由用户指定,由ftok函数的一套算法生成一个key,只要两个进程约定使用同样的字符串,同样的数字,就可以生成同样的key,从而标识同一块内存。
第二个参数 size
指定共享内存段的大小(以字节为单位)。如果是在获取一个已经存在的共享内存段,这个参数可以设置为 0。
注意在内核中共享内存的大小时以4kb为基本单位的。
例子:使用ipcs -m
指令,查看共享内存。(这里申请了4096b大小)
开辟共享内存是向上取整的,例如申请1b,实际上申请了4096b,还有4095b不能使用被浪费了,申请4097b,实际上申请了2×4096b,还有4095b不能使用被浪费了。
所以建议申请大小为n×4 kb。
第三个参数 shmflg
IPC_CREAT:如果共享内存不存在就创建,如果共享内存已经存在,就直接获取它
IPC_EXCL:不能单独使用,没意义。
IPC_CREAT | IPC_EXCL:如果共享内存不存在就创建,如果共享内存已经存在,出错返回!(如果创建成功,一定是全新的共享内存)。
IPC_CREAT | 八进制权限:赋予权限。
关于共享内存的权限
使用shmget(key, size, IPC_CREAT | IPC_EXCL);
使用shmget(key, size, IPC_CREAT | IPC_EXCL| 0666);
通过按位或的方式给共享内存加权限。
实际上第三个标志位的标志定义如下图,最右边三位始终是0,这就给权限位留出了空间。
删除共享内存
指令删除
ipc指令
ipcs
查看系统中指定用户创建的共享内存,消息队列,信号量。
ipcs -m
查看系统中指定用户创建的共享内存
使用 ipcrm -m shmid 删除共享内存
key vs shmid
key:在内核的角度,区分shm的唯一性。(类似于struct file*)
shmid:指令级,代码级,最后对共享内存进行控制,用的都是shmid(类似于文件描述符fd)
shmdt
代码删除
shmctl系统调用函数
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
第一个参数 int shmid:
共享内存的标识符
第二个参数 int cmd:
控制命令。常用的命令包括:
IPC_STAT: 获取共享内存段的状态信息,并将其存储在 buf 指向的 shmid_ds 结构中。
IPC_SET:
设置共享内存段的权限和其他属性(如 UID、GID、模式等),使用 buf 中的数据。
IPC_RMID
:(用于删除共享内存)
标记共享内存段为已删除。当最后一个进程分离该共享内存段时,它将被真正删除。
IPC_INFO (Linux 特有):
获取系统级别的共享内存信息。 SHM_INFO (Linux 特有): 获取共享内存资源的使用情况。
SHM_STAT (Linux 特有): 类似于 IPC_STAT,但通过索引访问共享内存段。
第三个参数buf:获取共享内存的相关属性
- 指向
shmid_ds
结构的指针,用于存储或提供共享内存段的状态信息。 - 如果
cmd
是IPC_RMID
,则可以设置为 NULL。
shmid_ds
定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; // 权限信息
size_t shm_segsz; // 共享内存段的大小
time_t shm_atime; // 最后附加时间
time_t shm_dtime; // 最后分离时间
time_t shm_ctime; // 最后修改时间
pid_t shm_cpid; // 创建进程的 PID
pid_t shm_lpid; // 最后操作进程的 PID
shmatt_t shm_nattch; // 当前附加的进程数
};
返回值
成功时,返回值取决于 cmd:
- 对于 IPC_STAT、IPC_SET 和 IPC_RMID,成功时返回 0。
- 对于 IPC_INFO 和 SHM_INFO,返回内核中共享内存段的最大索引值。
- 对于 SHM_STAT,返回共享内存段的标识符。
失败时,返回 -1,并设置 errno 来指示错误类型。
例子:
代码删除共享内存
void DeleteShm(int shmid)
{
int n = shmctl(shmid, IPC_RMID, NULL);
if (n < 0)
{
cerr << "shmctl error" << endl;
}
else
{
cout << "shmctl delete shm success ,shmid: " << shmid << std::endl;
}
}
挂接共享内存
shmat函数
shmat (shm sttach)函数是 Unix/Linux 系统中用于将 System V 共享内存段附加到进程地址空间的系统调用。
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
第一个参数 int shmid:
共享内存的标识符
第二个参数 const void *shmaddr
指明用户将 shm 挂接到哪里。
指定共享内存挂接到进程地址空间的具体地址。
如果为 NULL,系统会自动选择一个合适的地址。
第三个参数 int shmflg
控制共享内存段附加行为的标志位。常用标志包括:
SHM_RDONLY:以只读方式挂接。
0:默认行为,读写方式挂接。
返回值
成功:返回共享内存在进程地址空间中的起始地址。所以我们可以使用返回值,直接访问共享内存。
失败:返回 (void *) -1,并设置 errno 以指示错误。
例子:
void* ShmAttack(int shmid)
{
void* addr = shmat(shmid,nullptr,0);
if((int)addr == -1)
{
std::cerr << "shmat error" << std::endl;
return nullptr;
}
return addr;
}
使用ipcs -m
查看
nattch
指示的是该共享内存,当前有多少个内存挂接。
shmdt系统调用函数
shmdt(shm detach) 函数是 Unix/Linux 系统中用于将共享内存从进程地址空间分离的系统调用。当一个进程不再需要访问共享内存段时,应该调用 shmdt 将其分离,以避免资源泄漏。
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数说明
const void *shmaddr: 指定共享内存挂接到进程地址空间的具体地址。
返回值
成功:返回 0。
失败:返回 -1,并设置 errno 以指示错误。
// 客户端
#include "Comm.hpp"
int main()
{
// 1.获取key
key_t key = GetShmKeyOrDie(); // 与服务器以同样的方式获取key
cout << "key: " << ToHex(key) << std::endl;
// 2.获取共享内存
int shmid = GetShm(key, defaultsize);
std::cout << "shmid: " << shmid << std::endl;
sleep(5);
// 3.挂接共享内存
char *addr = (char *)ShmAttach(shmid);
cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
sleep(10);
// 4.将共享内存与进程分离
ShmDetach(addr);
cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
sleep(5);
return 0;
}
// 服务端
#include "Comm.hpp"
int main()
{
// 1.获取key
key_t key = GetShmKeyOrDie();
std::cout << "key: " << ToHex(key) << std::endl;
// 2.创建共享内存
int shmid = CreateShm(key, defaultsize);
std::cout << "shmid: " << shmid << std::endl;
sleep(3);
// 3.将共享内存和进程进行挂接(关联)
char *addr = (char *)ShmAttach(shmid);
cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
sleep(7);
// 4.将共享内存与进程分离
ShmDetach(addr);
cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
sleep(10);
// 5.删除共享内存
DeleteShm(shmid);
return 0;
}
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
const char *pathname = "/home";
const int proj_id = 0x66;
const int defaultsize = 4096; // 单位是字节
std::string ToHex(key_t k) // 转16进制
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%x", k);
return buffer;
}
key_t GetShmKeyOrDie()
{
key_t k = ftok(pathname, proj_id);
if (k < 0)
{
std::cerr << "ftok error,errno:" << errno << ", errno string:" << std::endl;
exit(1);
}
return k;
}
int CreateShmOrDie(key_t key, int size, int flag)
{
int shmid = shmget(key, size, flag);
if (shmid < 0)
{
std::cerr << "shmget error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
int CreateShm(key_t key, int size)
{
// IPC_CREAT: 不存在就创建,存在就获取
// IPC_EXCL: 没有意义
// IPC_CREAT | IPC_EXCL: 不存在就创建,存在就出错返回
return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm(key_t key, int size)
{
return CreateShmOrDie(key, size, IPC_CREAT);
}
void DeleteShm(int shmid)
{
int n = shmctl(shmid, IPC_RMID, nullptr);
if (n < 0)
{
cerr << "shmctl error" << endl;
}
else
{
cout << "shmctl delete shm success ,shmid: " << shmid << std::endl;
}
}
void ShmDebug(int shmid)
{
struct shmid_ds shmds;
int n = shmctl(shmid, IPC_STAT, &shmds);
if (n < 0)
{
std::cerr << "shmctl error" << std::endl;
return;
}
std::cout << "shmds.shm_segsz: " << shmds.shm_segsz << std::endl;
std::cout << "shmds.shm_nattch:" << shmds.shm_nattch << std::endl;
std::cout << "shmds.shm_ctime:" << shmds.shm_ctime << std::endl;
std::cout << "shmds.shm_perm.__key:" << ToHex(shmds.shm_perm.__key) << std::endl;
}
void *ShmAttach(int shmid)
{
void *addr = shmat(shmid, nullptr, 0);
if ((long long)addr == -1) // 64位机器下指针大小为8b,而int是4b,强转成int会有精度损失
{
std::cerr << "shmat error" << std::endl;
return nullptr;
}
return addr;
}
void ShmDetach(void *addr)
{
int n = shmdt(addr);
if (n < 0)
{
cout << "shmdt error" << endl;
}
}
缺点:共享内存不提供进程中协同的任何机制。会引起数据不一致。(进程1向内存写了数据,而进程2只读了一部分数据,数据没有读完整)如何解决?使用信号量,或者使用管道来完成同步。
优点:共享内存是所有进程间通信速度最快的
共享内存只要进程1把数据拷贝到内存,进程2直接可以对该共享内存进行访问。
对于管道,需要进程1,现将数据通过系统调用write拷贝到管道里,然后进程2,再将数据通过系统调用read拷贝出来,需要2次拷贝。(read,write函数本质上是拷贝函数)