文章目录
一、共享内存的原理
共享内存是通过在物理内存上开辟一块空间,然后让需要通信的进程都映射到这一块空间,这样就使它们看到同一块资源了。
共享内存区是最快的IPC形式。
⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递
不再涉及到内核,换句话说是进程不再通过执⾏进⼊内核的系统调⽤来传递彼此的数据
共享内存通信是双向的,
也就是说一个进程可以既读又写,使用起来就和C语言的malloc申请到的内存差不多。这种通信方式存在着数据安全问题,会在下文细说。
二、信道的建立
1.创建共享内存
创建共享内存使用shmget函数,它的作用是创建或获取共享内存段的系统调用。
对于shmget的使用来说,虽然操作起来相对简单,但要完全理解其各种参数的设定则较为困难。不过接下来我会进行详细讲解。
shmget声明如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 参数key:用户设定任意一个数,用于区分不同共享内存,通常由ftok生成。
- 参数size:设定共享内存的大小。
- 参数shmflg:标志位,用于指定共享内存段的创建方式和权限。常见的标志包括:
IPC_CREAT :如果共享内存段不存在,则创建它。
IPC_EXCL :与 IPC_CREAT 一起使用,确保创建的共享内存段是新的。
权限标志:如 0666,表示所有用户都有读写权限。 - 返回值: 成功时返回共享内存段的标识符 (shmid) 。 失败时返回 -1,并设置 errno 以指示错误类型。
1.key的作用
- 思考1:在用户层面如何让两个独立进程共享同一块内存?
- 思考2:在匿名管道和命名管道中,用户层面是如何让两个进程确定同一个资源的?
问题2很显然,管道的本质是文件,用户通过让两个程序打开同一个文件名来实现看到同一个资源。
因此,共享内存同样需要一个key来充当类似文件名的功能。
2.key的选取
key参数本质是一个int类型,我们可以直接指定一个数值传入,当然,为了更规范,更专业,我们通常都会使用ftok来生成。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 参数pathnme:一个存在的文件路径(例如 /tmp/myfile),文件必须存在,否则 ftok 会失败。
- 参数proj_id:一个整数,用于进一步区分不同的 IPC 对象。
- 返回值:
成功:返回生成的 key_t 键值。
失败:返回 -1,并设置 errno 以指示错误原因。
3.shmid的作用
shmid是一个int类型,由shmget返回,在作用上和物理意义上与文件系统中的fd类似。
它的作用主要是让用户找到指定的共享内存。
在操作系统内核中,shmid 的物理意义如下:
内核维护一个 共享内存段表(如 struct shmid_kernel),每个表项对应一块共享内存。
shmid 是该表的索引,通过它找到对应的共享内存段(含物理页、权限、挂载进程等信息)。
共享内存最终映射到 实际的物理页帧(通过页表机制)。
物理本质是内存页的映射:多个进程的虚拟地址映射到同一组物理页。
4.key和shmid的区别
key最终成为系统层
区分不同IPC的标志,而shmid则是用户层
用来区分不同IPC的标志。
5.内存设定的特性
这里的内存设定指的是shmget函数中的参数size。
当传入的内存不足4096字节(4KB)的倍数时,会扩到4096倍数。但是只会提供size大小的使用空间。这样做可以规避掉一些因为共享内存过多带来的问题。
6.shmflg的设定
对于共享内存,我们可以将程序简单分为创建端和使用端,它们的shmflg设定通常是:
- 创建端:IPC_CREAT | IPC_EXCL | 0666
- 使用端:IPC_CREAT
创建端要保证IPC是最新的,所以需要加IPC_EXCL,然后还需要设定权限。
使用端只需要获取共享内存段的系统调用,因此只用一个IPC_CREAT即可。
2.绑定共享内存
以上我们完成的只是共享内存的创建,接下来还需要把进程绑定到共享内存,使用函数shmat
,其中at指的是单词attach。
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 参数shmid:传入从shmget中返回的shmid来指定共享内存。
- 参数shmaddr:指定共享内存段附加到进程地址空间的位置,通常设为nullptr,系统会自动选择一个合适的地址。
- 参数shmflg:读写方式,常用的有:
SHM_RDONLY:以只读方式附加共享内存段。
0:以读写方式附加共享内存段。 - 返回值:
成功时,返回共享内存段附加到进程地址空间的起始地址。
失败时,返回 (void *) -1,并设置 errno。
3.代码示例
创建端程序:
int main()
{
//生成一个key
int key = ftok(".", 48);
//创建共享内存
int shmid = shmget(key, 4069, IPC_CREAT | IPC_EXCL | 0666);
//连接到共享内存
void* p = shmat(shmid,nullptr,0);
//使用共享内存
//... ...
return 0;
}
使用端程序:
int main()
{
//生成一个相同key
int key = ftok(".", 48);
//获取到共享内存的系统调用
int shmid = shmget(key, 4069, IPC_CREAT);
//连接到共享内存
void* p = shmat(shmid,nullptr,0);
//使用共享内存
//... ...
return 0;
}
注意:为了简洁和方便说明问题,以上代码省略了头文件的包含和返回值有效性的判断等等,在实际开发中可不敢省略。
三、利用共享内存通信
1.通信
上文我们只是完成了信道的建立,接下来我们进行通信,通过上面的操作,我们已经获取到共享内存的起始地址。
它的用法与C语言的malloc申请的内存用法相同,只是共享内存可以同时被两个进程访问。
如下写端:
int main()
{
int key = ftok(".", 48);
int shmid = shmget(key, 4069, IPC_CREAT | IPC_EXCL | 0666);
void* p = shmat(shmid,nullptr,0);
//使用共享内存
char* chp = (char*)p;
for(int i='a';i<='z';i++)
{
sleep(1);
*chp=i;
chp++;
}
return 0;
}
读端:
int main()
{
int key = ftok(".", 48);
int shmid = shmget(key, 4069, IPC_CREAT);
void* p = shmat(shmid,nullptr,0);
//使用共享内存
char* chp = (char*)p;
while(true)
{
sleep(1);
cout<<chp<<endl;
}
return 0;
}
注意:为了获取到同一个共享内存,我们设定的key必须一致。
2.解除绑定
如果进程退出时没有解除绑定,共享内存段仍然会保留在系统的共享内存资源中,直到显式删除(通过 shmctl 或系统重启)。
使用shmdt
来解除绑定,其中dt代表单词delete。
int shmdt(const void *shmaddr);
参数shmaddr:需要断开连接的共享内存的起始地址。
返回值:
成功:返回0。
失败:返回-1,并设置errno以指示错误原因。
一个共享内存,与它绑定的程序的个数是由一个引用计数机制进行维护的,当shmdt成功,引用计数减1。
3.销毁共享内存
共享内存不会随程序的结束而销毁,它是随内核的 ,因此需要显式地进行销毁,可以使用shmctl函数。或在命令行中使用指令进行销毁。
1.命令行销毁
查看共享内存信息
ipcs -m
如下:
nattch信息:它表示与该 共享内存连接的程序个数。
销毁共享内存
ipcrm -m 2
这里需要填入shmid(即这里的2)来指定共享内存。
2.程序中销毁
在程序中销毁我们使用函数shmctl
,其中ctl代表单词control。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 参数shmid:传入从shmget返回的shmid来指定需要销毁的共享内存。
- 参数cmd:需要传入一个操作选项,操作选项很多,而IPC_RMID就是用来销毁共享内存的。
- 参数shmid_ds:这是一个输出型参数,如果你需要获取共享内存的信息,则传入一个shmid_ds类型的指针来接收,如果不是通常传入nullptr即可。
- 返回值:
成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误类型。
注:命令行销毁和程序中销毁效果是一样的,因为命令行销毁底层还是调用了shmctl函数。
四、共享内存的生命周期
共享内存的生命周期是不随进程的,而是随内核,如果没有显示删除它就会一直存在,尽管相关的进程已经退出。直到重装系统才得以释放。
使用shmctl释放共享内存存在的情况
1.正常释放
当nattach(引用计数)为0时,即没有进程与它绑定,被正常释放。
2.共享内存段被标记为已删除,但仍有进程附加(shmat)
共享内存段已经被标记为已删除(不能附加到新的进程),但之前仍有一些进程附加到该共享内存段并正在使用。所以共享内存段不会被立即释放。只有当所有附加的进程都调用 shmdt 分离后,系统才会释放资源。
五、数据安全问题
共享内存最大的优点就是快
, 相比使用管道技术,它减少了中间复杂转化和拷贝工作,而是直接对物理内存进行访问。
但它也有一个致命的缺点,相比管道技术,共享内存它的读端和写端是不带有同步机制
的,这就很容易使得数据混乱,也就是造成数据不一致问题。
比如我们写端写入“hello world”,而读端读到的可能是“he”,“ll”,“o wor”,“ld”等等无法预测的奇葩数据。 读端一个劲地读,不会管写端这句话是否已经说完,而且也无法知道。
当我们不了解锁的情况下想要解决这个问题,可以利用命名管道来解决,因为命名管道带有同步机制,我们用它的write和read函数来保护数据的安全,当然write和read并不用写或读什么有意义的数据。
六、源码
Fifo.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
#include "Comm.hpp"
#define PATH "."
#define FILENAME "fifo"
//#define FIFO_FILE "fifo"
class NamedFifo
{
public:
NamedFifo(const string& path,const string& name)
:_path(path),_name(name)
{
_fifoname =_path + "/" + _name;
umask(0);
//创建管道
int n = mkfifo(_fifoname.c_str(), 0666);
if(n < 0)
{
cout << "mkdir fifo error" << endl;
ERR_EXIT("makefifo");
}
else
{
cout << "fifo success" << endl;
}
}
~NamedFifo()
{
//删除管道文件
int n = unlink(_fifoname.c_str());
if(n == 0)
{
cout << "unlink fifo" << endl;
}
else
{
// perror("remove fifo fail");
// exit(1);
ERR_EXIT("unlink");
}
}
private:
string _path;
string _name;
string _fifoname;
};
class FileOper
{
public:
FileOper(const string& path,const string& name)
:_path(path), _name(name), _fd(-1)
{
_fifoname = _path + "/" + _name;
}
~FileOper()
{
}
void OpenForRead()
{
// 打开, write 方没有执行open的时候,read方,就要在open内部进行阻塞
// 直到管道文件打开了,open才会返回!
_fd = open(_fifoname.c_str(), O_RDONLY);
if(_fd < 0)
{
ERR_EXIT("open");
}
cout << "open fifo success" << endl;
}
void OpenForWrite()
{
//写
_fd = open(_fifoname.c_str(), O_WRONLY);
if(_fd < 0)
{
ERR_EXIT("open");
}
cout << "open fifo success" << endl;
}
void Wakeup()
{
//写操作
char c = ' ';
int n = write(_fd, &c, 1);
printf("尝试唤醒: %d\n", n);
}
bool Wait()
{
//读操作
char c;
int n = read(_fd, &c, 1);
if(n > 0)
{
printf("唤醒成功: %d\n", n);
return true;
}
else
{
return false;
}
}
void Close()
{
if(_fd > 0)
{
close(_fd);
}
}
private:
string _path;
string _name;
string _fifoname;
int _fd;
};
Comm.hpp
#pragma once
#include <cstdio>
#include <cstdlib>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
Shm.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
using namespace std;
#include "Comm.hpp"
const int gdefaultid = -1;
const int gsize = 4096;
const string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"
class Shm
{
public:
Shm(const string& pathname,int projid,const string& usertype)
:_shmid(gdefaultid),
_size(gsize),
_start_mem(nullptr),
_usertype(usertype)
{
_key = ftok(pathname.c_str(), projid);
if(_key < 0)
{
ERR_EXIT("fotk");
}
if(_usertype == CREATER)
{
//创建共享内存
Create();
}
else if(_usertype == USER)
{
//得到共享内存
Get();
}
else
{
}
//链接共享内存
Attach();
}
void* VirtualAddr()
{
printf("VirtualAddr: %p\n", _start_mem);
return _start_mem;
}
int Size()
{
return _size;
}
void Attr()
{
struct shmid_ds ds;
//ds输出型参数
int n = shmctl(_shmid, IPC_STAT, &ds);
printf("shm_segsz: %ld\n", ds.shm_segsz);
printf("key: 0x%x\n", ds.shm_perm.__key);
}
~Shm()
{
cout << _usertype << endl;
if(_usertype == CREATER)
{
Destroy();
}
}
private:
//创建新的共享内存
void CreateHelper(int flg)
{
printf("key: 0x%x\n", _key);
//共享内存的生命周期,跟随内核
_shmid = shmget(_key, _size, flg);
if(_shmid < 0)
{
ERR_EXIT("shmget");
}
printf("shmid: %d\n", _shmid);
}
void Create()
{
CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
}
void Attach()
{
_start_mem = shmat(_shmid, nullptr, 0);
if((long long)_start_mem < 0)
{
ERR_EXIT("shmat");
}
printf("attach success\n");
}
void Detach()
{
int n = shmdt(_start_mem);
if(n == 0)
{
printf("detach success\n");
}
}
void Get()
{
CreateHelper(IPC_CREAT);
}
void Destroy()
{
if(_shmid == gdefaultid)
{
return;
}
Detach();
if(_usertype == CREATER)
{
int n = shmctl(_shmid, IPC_RMID,nullptr);
if(n > 0)
{
printf("shmctl delete shm: %d success!\n", n);
}
else
{
ERR_EXIT("shmctl");
}
}
}
private:
int _shmid;
key_t _key;
int _size;
void* _start_mem;
string _usertype;
};
server.cc
#include "Shm.hpp"
#include "Fifo.hpp"
int main()
{
Shm shm(pathname, projid, CREATER);
sleep(5);
shm.Attr();
NamedFifo fifo(PATH, FILENAME);
// 文件操作
FileOper readerfile(PATH, FILENAME);
readerfile.OpenForRead();
char* mem = (char*)shm.VirtualAddr();
//读写共享内存,没有使用系统调用
while(true)
{
if(readerfile.Wait())
{
printf("%s\n", mem);
}
else
{
break;
}
}
readerfile.Close();
cout << "server end normal!" << endl;
return 0;
}
client.cc
#include "Shm.hpp"
#include "Fifo.hpp"
int main()
{
FileOper writerfile(PATH, FILENAME);
writerfile.OpenForWrite();
Shm shm(pathname, projid, USER);
char* mem = (char*)shm.VirtualAddr();
int index = 0;
for(char c = 'A';c <= 'Z';c++,index += 2)
{
sleep(2);
mem[index] = c;
mem[index + 1] = c;
mem[index + 2] = 0;
writerfile.Wakeup();
sleep(1);
}
writerfile.Close();
return 0;
}