Linux操作系统之进程间通信:共享内存

前言:昨天我们已经了解到了管道的另外一种形式,命名管道的有关知识。

而今天,我将会为大家带来另外一种IPC方式:共享内存的知识点。

希望对大家有所帮助!

一、什么是共享内存 共享内存是指被多个进程共同映射到各自地址空间的一段物理内存区域。我们都知道,一个进程有着自己的PCB,mm_struct,同时,mm_struct管理着虚拟地址空间,自然会有一张页表,来实现我们从虚拟地址找到物理地址的转化。

一般来说,虚拟地址是可能存在相同的情况的,但他们在页表的映射关系会映射到不同的物理地址上。

而共享内存,他的物理地址是被多个进程同时映射到了,这也就满足到了我们所说,想要满足进程间通信,就需要看到同一份资源的前提:我们看到了同一个物理地址空间。

不同于管道,共享内存是速度最快的IPC方式,因为共享内存写入了数据,另外一个进程就可以立马看见,而不需要掉用什么系统调用来读,来拷贝。

由于我们操作系统中存在大量进程,所以共享内存明显也可以存在多个,那么我们的操作系统难免要对共享内存进行管理。

如何管理呢?

还是那句老话:先描述,再组织!!

所以我们知道,共享内存其实就等于=共享内存的内核数据结构+内存块

代码语言:javascript复制struct shmid_kernel {

struct kern_ipc_perm shm_perm; // 权限和基本信息

struct file *shm_file; // 关联的伪文件

unsigned long shm_nattch; // 当前附加计数

unsigned long shm_segsz; // 段大小(字节)

time_t shm_atim; // 最后访问时间

time_t shm_dtim; // 最后分离时间

time_t shm_ctim; // 最后改变时间

struct pid *shm_cprid; // 创建者PID

struct pid *shm_lprid; // 最后操作PID

// ...其他字段...

};共享内存,其实也是通过mm_struct,以结构体链表的形式管理起来的

我们这里对他的管理不再过多赘述,感兴趣的同学可以去看一下内核的源代码。

二、共享内存的接口我们首先来了解一下共享内存的接口,包括我们如何申请共享内存,如何让这个共享内存(物理内存)与虚拟地址挂钩,在这之后,我们再来带大家看看代码,具体深刻理解一下共享内存的使用与实现。

shgget:shmget函数的作用是创建/获取共享内存段,其有三个参数,作用分别如下:

key_T key:是一个共享内存段的关键标识符。这个标识符通常由自己确定,可以是1,可以是2.由于由自己决定,所以难免出现重复,我们一般用ftok函数来生成指定的key。

size_t size:这是请求的共享内存段的大小(字节)。另外提一嘴,在操作系统中,我们通常申请的空间是以块为单位的(就是我们在文件系统中学习的块),所以大小是4kb,也就是4096字节。如果我我们申请4097个字节的共享内存,实际会申请两个段,也就是8kb,但只会让你使用4097字节,剩下的4095字节就相当于被浪费了。

int shmflg:这是控制标志与该物理地址的权限信息,常用标志:IPC_CREAT、IPC_EXCL、IPC_NOWAIT,权限信息通常是八进制,如:0666

ftok:这个函数的作用就是生成一个独一无二的键值,第一个参数pothname,指向一个实际存在的文件路径,第二个参数是随便的一个8位的项目标识符(0-255),该函数会通过这两个信息,生成一个key,以免遇见重复key的情况。

所以我们两个要进行通信的进程,应该约定好这个路径与id,就能生成同一个key,找到同一份物理共享内存。

shmat:该函数的作用就是把共享内存挂载到一个虚拟地址上,随后返回这个虚拟地址,如果失败,就返回(void*)-1。

shmaddr我们一般填NULL,意思是让系统自己分配,当然我们也可以手动填一个具体的虚拟地址,但需要对齐,所以一般不推荐。shmflg代表方式,0就是可读写,SHM_RDONLY就是只读

shmdt:用于将共享内存段从进程地址空间分离,作用其实就与shmat的反作用,一个将物理地址与虚拟地址联系起来,一个取消映射

shmctl:shmctl 提供了对共享内存段的控制接口,我们一般用其来删除共享内存

shmctl(shmid,IPC_RMID,nullptr)

该函数的第二个参数cmd是控制指令,通常有以下指令:

第三个参数buf 就是个「情报中转站」——你要么通过它读取共享内存的详细信息(比如 IPC_STAT),要么通过它传递修改参数(比如 IPC_SET),不用时就设为 nullptr(比如删内存段 IPC_RMID)。

三、共享内存的使用了解接口后,我们现在就带着大家来使用一下共享内存的,帮助大家理解共享内存:

我们有Share.hpp(负责提供一下通用信息,比如我们创建key所需要用到的path之类的,没有这个其实也行),reader.cc(我们后期主要把这个实现偏向成读信息的一方),wirter.cc(偏向写信息):

首先我们在share.hpp中设置好我们共同的路径与id:

代码语言:javascript复制#pragma once

#include

#include

#include

#include

const std::string path="/home/ubuntu/dailycode/共享内存博客代码/code";//记得在你的路径底下添加一个code文件

int pid=0x666;以及创建我们的key,在reader中使用ftok

代码语言:javascript复制#include"Share.hpp"

int main()

{

//创建key

key_t key=ftok(path.c_str(),pid);

if(key<0)

{

std::cout<<"ftok error"<

return -1;

}

//申请共享内存

int shmid=shmget(key,4096,IPC_CREAT | IPC_EXCL |0666);

if (_shmid < 0)

{

std::cerr << "shmget error" << std::endl;

return -1;

}

std::cout<<"申请共享内存成功"<

return 0;

}同学们,不着急,我们写到这里先来做一个小实验。

我们编译以上的reader代码,运行多次:

这是怎么回事呢?

答案是:共享内存的生命周期,是随内核的,而不是进程。

当你第一次运行该代码,会申请一份共享内存,但是第二次以后,你用的key是同一个,还想继续申请,就出错啦!!

所以。用户必须手动让OS释放掉,

那么如何释放呢?

有两种办法,一种是指令ipcrm,另外一种,就是我们之前提到的shmctl

我们有一个指令ipcs来查看当前系统中已经申请的共享内存:

这个key,就是我们ftok出的key,但是我们要管理,却不能用这个管理。

key是给我们用户看的,如果我们要使用ipcrm -m 删除共享内存,需要输入 ipcrm -m 1(即shmid),而不是0x66020a78。

如果要使用代码删除,就在我们的reader.cc中后面加入:

//删除共享内存

::shmctl(shmid,IPC_RMID,nullptr);

我们上面写的代码其实是为了方便我们做这个实验。同学们,你们有没有发现我们的reader.cc的代码在writer上也会重复用到,包括创建key啊,申请空间啊,所以我们最正确的写法就是把这些封装为一个专门的类,把这些方法封装成类函数。

所以我们更改share.hpp的代码:

代码语言:javascript复制class shareshm

{

public:

void CreateShm()

{

_key = ::ftok(path.c_str(), pid);

if (_key < 0)

{

std::cout << "ftok error" << std::endl;

}

_shmid = ::shmget(_key, 4096, IPC_CREAT | IPC_EXCL | 0666);

if (_shmid < 0)

{

std::cout << "shmget error" << std::endl;

}

std::cout << "shmid: " << _shmid << std::endl;

}

void GetShm()

{

_key = ::ftok(path.c_str(), pid);

if (_key < 0)

{

std::cout << "ftok error" << std::endl;

}

_shmid = ::shmget(_key, 4096, IPC_CREAT);

if (_shmid < 0)

{

std::cout << "shmget error" << std::endl;

}

std::cout << "shmid: " << _shmid << std::endl;

}

private:

int _shmid;

key_t _key;

void *_adr;

};我们写到这里,难免会发现,GetShm与CreateShm的重合度还是很高,他们之间只有shmget的参数不一样,这个时候该怎么办呢?

为了简化代码,我们需要灵活运用类的特性,所以我们可以新增一个内部函数,通过传参来控制:

代码语言:javascript复制class shareshm

{

private:

void CreateShmHelper(int shmflg)

{

_key = ::ftok(path.c_str(), pid);

if (_key < 0)

{

std::cout << "ftok error" << std::endl;

}

_shmid = ::shmget(_key, 4096, shmflg);

if (_shmid < 0)

{

std::cout << "shmget error" << std::endl;

}

std::cout << "shmid: " << _shmid << std::endl;

}

public:

void CreateShm()

{

CreateShmHelper(IPC_CREAT | IPC_EXCL | 0666);

}

void GetShm()

{

CreateShmHelper(IPC_CREAT);

}

private:

int _shmid;

key_t _key;

void *_adr;

};我们继续在share.hpp中创建一个全局的shm对象,就能同时在reader与writer中调用该接口:

代码语言:javascript复制class shareshm

{

private:

void CreateShmHelper(int shmflg)

{

_key = ::ftok(path.c_str(), pid);

if (_key < 0)

{

std::cout << "ftok error" << std::endl;

}

_shmid = ::shmget(_key, 4096, shmflg);

if (_shmid < 0)

{

std::cout << "shmget error" << std::endl;

}

std::cout << "shmid: " << _shmid << std::endl;

}

public:

shareshm() : _shmid(-1), _key(0), _addr(nullptr)

{

}

void CreateShm() // 创建共享内存

{

CreateShmHelper(IPC_CREAT | IPC_EXCL | 0666);

}

void GetShm() // 获取共享内存

{

CreateShmHelper(IPC_CREAT);

}

void AttachShm() // 挂载到虚拟地址

{

_addr = shmat(_shmid, nullptr, 0); // 这里默认让系统分配虚拟地址

if ((long long)_addr == -1) // 因为我们这里是64位的系统,void*指针大小为8,如果是32位,就可以使用int

{

std::cout << "attach error" << std::endl;

}

}

void DetachShm() // 取消物理地址与虚拟地址的映射关系

{

if (_addr != nullptr)

::shmdt(_addr);

std::cout << "detach done: " << std::endl;

}

void DeleteShm() // 删除共享内存

{

shmctl(_shmid, IPC_RMID, nullptr);

}

void *GetAddr()

{

return _addr;

}

private:

int _shmid;

key_t _key;

void *_addr;

};最后就是在reader与writer的调用了,这里有一点值得注意,我们之前说过,二者是用的同一个物理内存,所以不会调用系统调用接口如read之类的。

所以我们写入数据的办法就是sprintf,strcpy等拷贝函数,读取数据的方法就是打印指针内容的方法。

reader.cc:

代码语言:javascript复制#include "Share.hpp"

int main()

{

shm.CreateShm();

shm.AttachShm();

// //读取共享内存

while (true)

{

char *p = (char *)shm.GetAddr();

std::cout << p << std::endl;

sleep(1);

}

shm.DetachShm();

shm.DeleteShm();

return 0;

}writer.cc:

代码语言:javascript复制#include"Share.hpp"

int main()

{

shm.GetShm();

shm.AttachShm();

// //写入共享内存

int count=0;

while (true)

{

sprintf((char *)shm.GetAddr(),"%d",count++);

sleep(1);

}

shm.DetachShm();

shm.DeleteShm();

return 0;

}分别启动两个bash运行结果如下:

当我们杀死writer进程时,共享内存的地址的数据不会被修改,就一直打印14 。

大家可以想到,我们这样没有调用read来阻塞的代码,会出现一定的弊端,如果我写端写数据写到一半,你读端就读走了呢?

这就是数据不一致问题。

四、解决数据不一致问题 这个问题,我们可以通过临界区,加锁,或者同步来解决。

我们这里可以选择使用命名管道的方式来处理。

即我们可以在双方的while循环中,先让读端的while循环的每次循环开始进行阻塞状态,等待写端的信号。写端在写之后,发送一个信号表示数据已经写完了。

所以我们可以在shareshm中新增我们以前学过的命名管道:

代码语言:javascript复制#pragma once

#include

#include

#include

#include

#include

#include

#include

#include

const std::string path = "/home/ubuntu/dailycode/共享内存博客代码/code"; // 记得在你的路径底下添加一个code文件

int pid = 0x666;

const std::string fifo_path = "/tmp/shm_sync_fifo"; // 命名管道路径

class shareshm

{

private:

void CreateShmHelper(int shmflg)

{

_key = ::ftok(path.c_str(), pid);

if (_key < 0)

{

std::cout << "ftok error" << std::endl;

}

_shmid = ::shmget(_key, 4096, shmflg);

if (_shmid < 0)

{

std::cout << "shmget error" << std::endl;

}

std::cout << "shmid: " << _shmid << std::endl;

}

void CreateFifo()

{

if (mkfifo(fifo_path.c_str(), 0666))

{

if (errno != EEXIST)

{

perror("mkfifo error");

exit(EXIT_FAILURE);

}

}

}

public:

shareshm() : _shmid(-1), _key(0), _addr(nullptr)

{

}

void CreateShm() // 创建共享内存

{

CreateShmHelper(IPC_CREAT | IPC_EXCL | 0666);

}

void GetShm() // 获取共享内存

{

CreateShmHelper(IPC_CREAT);

}

void AttachShm() // 挂载到虚拟地址

{

_addr = shmat(_shmid, nullptr, 0); // 这里默认让系统分配虚拟地址

if ((long long)_addr == -1) // 因为我们这里是64位的系统,void*指针大小为8,如果是32位,就可以使用int

{

std::cout << "attach error" << std::endl;

}

}

void DetachShm() // 取消物理地址与虚拟地址的映射关系

{

if (_addr != nullptr)

::shmdt(_addr);

std::cout << "detach done: " << std::endl;

}

void DeleteShm() // 删除共享内存

{

shmctl(_shmid, IPC_RMID, nullptr);

}

void *GetAddr()

{

return _addr;

}

// 新增命名管道方法

void WaitForSignal()

{

int fd = open(fifo_path.c_str(), O_RDONLY);

char buf[1];

read(fd, buf, 1); // 阻塞等待信号

close(fd);

}

void SendSignal()

{

int fd = open(fifo_path.c_str(), O_WRONLY);

char buf[1] = {'1'};

write(fd, buf, 1); // 发送信号

close(fd);

}

private:

int _shmid;

key_t _key;

void *_addr;

};

shareshm shm;reader与writer中调用:

代码语言:javascript复制#include "Share.hpp"

int main()

{

// //创建key

// key_t key=::ftok(path.c_str(),pid);

// if(key<0)

// {

// std::cout<<"ftok error"<

// return -1;

// }

// //申请共享内存

// int shmid=::shmget(key,4096,IPC_CREAT | IPC_EXCL |0666);

// if (shmid < 0)

// {

// std::cerr << "shmget error" << std::endl;

// return -1;

// }

// std::cout<<"申请共享内存成功"<

// //删除共享内存

// ::shmctl(shmid,IPC_RMID,nullptr);

shm.CreateShm();

shm.AttachShm();

// //读取共享内存

// while (true)

// {

// char *p = (char *)shm.GetAddr();

// std::cout << p << std::endl;

// sleep(1);

// }

while (true)

{

// 等待写端信号

shm.WaitForSignal();

// 读取并显示共享内存内容

char *p = (char *)shm.GetAddr();

std::cout << "Received: " << p << std::endl;

}

shm.DetachShm();

shm.DeleteShm();

return 0;

}代码语言:javascript复制#include"Share.hpp"

int main()

{

shm.GetShm();

shm.AttachShm();

// //写入共享内存

int count=0;

// while (true)

// {

// sprintf((char *)shm.GetAddr(),"%d",count++);

// sleep(1);

// }

while (true)

{

// 写入数据

sprintf((char *)shm.GetAddr(), "%d", count++);

// 发送信号通知读端

shm.SendSignal();

sleep(1);

}

shm.DetachShm();

shm.DeleteShm();

return 0;

}总结:

共享内存虽然性能优异,但也带来了更复杂的管理需求。理解其底层原理,掌握正确的使用方法,才能在项目中充分发挥它的优势。希望这些内容对大家的系统编程实践有所帮助。