文章目录
- 1.初识共享内存
- 1.0浅谈System V
- 1.1什么是共享内存?
- 1.2Linux-System V共享内存
- 1.3图解共享内存
- 1.4对共享内存的理解
- 2.创建共享内存
- 2.1共享内存如何创建?
- 2.2代码运行与测试
- 2.3shm与pipe的区别
- 2.4shm缺乏访问控制
- 3.代码理解shm
- 3.1Log.hpp
- 3.2common.hpp
- 3.3shmServer.cpp
- 3.4shmClient.cpp
1.初识共享内存
1.0浅谈System V
在Linux下一切皆文件的情况下,这套模式并不太好。
- 使用复杂
- System V的一套接口是OS单独拉出来的一个模块,在大项目中不易于集成化,需要单独特殊化处理。【但仍与文件有关 】
1.1什么是共享内存?
共享内存是Unix系统下的多进程间的通信方法,这种方法通常用于一个程序的多进程间通信,实际上多个程序间也可以通过共享内存来传递信息。它允许多个不相关的进程访问同一个物理内存区域,从而使它们能够相互通信和共享数据。这是进程间通信中最简单的方式之一。
共享内存允许多个进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。这使得共享内存成为进程间共享数据的一种最快的方法。
然而,由于多个进程可以同时访问共享内存,因此需要某种同步机制(如互斥锁和信号量)来避免对共享内存的冲突访问。当一个进程正在向共享内存区写数据时,其他进程在数据被写入完成前不应该去读或写这些数据。
共享内存的使用通常包括以下步骤:
创建或打开共享内存对象。
将指定的共享内存映射到进程的地址空间,以便进行访问。
在完成通信后,撤销对共享内存的映射。
删除共享内存对象。
需要注意的是,共享内存是在多个进程之间共享一段内存区域,因此,对于这段内存的访问和管理需要谨慎处理,以避免出现数据不一致或冲突的情况。
总的来说,共享内存是一种高效的进程间通信方式,它允许多个进程直接读写同一块内存区域,而不需要进行数据的拷贝,从而大大提高了通信的效率。但是,它也需要适当的同步机制来确保对共享内存的正确访问。
1.2Linux-System V共享内存
Linux下的System V共享内存是Unix系统V版本引入的一种进程间通信(IPC)机制。它允许多个进程共享同一块物理内存区域,以实现高效的数据交换和通信。
在System V共享内存中,进程通过系统调用(如shmget、shmat、shmdt等)来创建、映射和解除映射共享内存段。这些系统调用提供了对共享内存的管理和操作功能。
具体来说,shmget函数用于创建或获取一个共享内存段的标识符。它需要提供一个唯一的键值(key)来标识共享内存段,并指定共享内存的大小和访问权限。成功时,shmget返回一个共享内存标识符(shmid),失败时返回-1。
shmat函数用于将共享内存段映射到进程的地址空间中。它接受共享内存标识符(shmid)和可选的映射地址作为参数。如果映射地址为空,则系统会自动选择一个合适的地址进行映射。成功时,shmat返回映射后的地址,失败时返回-1。
一旦共享内存映射到进程的地址空间,多个进程就可以通过直接读写这块内存区域来进行通信。由于共享内存是直接在物理内存中分配的,因此它的访问速度非常快,是进程间通信中最快的方式之一。
需要注意的是,多个进程同时访问共享内存时,可能会出现数据竞争和不一致的问题。因此,需要使用同步机制(如信号量)来确保对共享内存的正确访问。
当不再需要共享内存时,可以使用shmdt函数来解除映射,并使用shmctl函数来删除共享内存段。这些操作可以确保资源被正确释放,避免内存泄漏和不必要的资源浪费。
总的来说,Linux下的System V共享内存是一种高效的进程间通信机制,它允许多个进程直接读写同一块内存区域,而不需要进行数据的拷贝和传输。然而,由于共享内存访问的复杂性,需要谨慎处理同步和并发访问的问题。
1.3图解共享内存
- 共享内存和动态库都被映射到进程地址空间的共享区
- 通信原理图
1.4对共享内存的理解
- 共享内存提供者,是操作系统
- 操作系统要管理共享内存 ==> 先描述在组织
- 共享内存 == 共享内存块 + 共享内存的内核数据结构
- 对共享内存的管理转变成对数据结构的增删查改
OS怎么知道:
- 如何知道是否需要释放这块内存时
- 当前有多少进程和共享内存关联?
- 这块内存有多大?
- 如何禁止一些进程访问这块内存(权限问题)?
需要知道该内存的一系列属性信息 ⇒ 通过内核数据结构获取属性信息
2.创建共享内存
共享内存是有很多个的 类似于这样
2.1共享内存如何创建?
假设A进程(server)与B进程(client)通信
A进程调用shmget()接口
返回值
共享内存的用户层标识符(类似fd但不同)
key
- 通过调用ftok() 获取的系统中唯一的一个值(数值是几不重要,只要唯一即可–>用来唯一标识指定共享内存块的)
- A创建了共享内存块,B怎么看到这个共享内存块,如何保证二者可以访问同一块资源呢?AB通过唯一的key在内核中访问同一个共享内存块 ⇒ 看到了同一块“资源”
- AB如何获取同一个key?AB调用同一个ftok()根据相同的参数通过某种算法计算出同一个key(这个经过算法加算出来的key一般在内核中是唯一的,存在出错的情况,此处不讨论)
- A调用ftok()获取一个key,在内存中创建一个共享内存块,以此key标识这个shm(将此key存入该shm的数据结构中),B通过ftok()相同的参数相同的算法获取相同的key,根据这个key,去内核中寻找对应的shm,至此,AB两个进程看到了同一块“资源”
size
创建的shm的大小
共享内存的大小 最好是页(PAGE: 4096)的整数倍 4kb
OS管理物理内存时 page的大小以4kb为单位
若申请4097 OS会开辟2个page【对page向上取整】
而第二个page的4095 申请shm的人无法使用 OS其他进程也无法使用 ==> 大大的浪费/zz行为
shmflg
一般有两个选项
IPC_CREAT单独使用:创建shm时,若底层已经存在想要创建的shm,获取并返回shmid;若不存在,创建并返回shmid
IPC_EXCL单独使用:无意义
IPC_CREAT and IPC_EXCL:若底层不存在,创建并返回shmid;若底层存在想要创建的shm,出错返回。⇒ 只要返回成功,此时使用的一定是一个全新的shm
ftok()
shmctl
建立/删除映射
shmid:要将哪个共享内存建立映射
shmaddr:要建立映射的虚拟地址 不建议指定 传空指针让OS帮我们建立映射
shmflg:可以传参以只读方式挂接等
shmid: 这是一个共享内存标识符,通常是通过 shmget 函数获得的。
nullptr: 这是一个指向 shmat 应该放置附加内存的起始地址的指针。由于这里传入的是 nullptr,内核将自动选择一个合适的地址。
0: 这是一个标志位,用于控制共享内存的访问权限和附加方式。在这里,0 表示使用默认的读/写权限。
返回值:
shmaddr: 这是一个指向附加的共享内存段的指针。如果 shmat 调用成功,shmaddr 将指向共享内存段的起始地址。如果调用失败,shmaddr 将是 (char *) -1,并且 errno 将被设置为指示错误原因的值。
注意:在实际代码中,你应该检查 shmaddr 是否为 (char *) -1 来确定 shmat 调用是否成功,并相应地处理任何错误。
另外,请注意,shmat 的返回值类型是 void*,但在这个例子中,它被强制转换为 char*。这样做是为了方便进行指针算术和内存访问,因为 char 类型的大小是 1 字节,所以 char* 指针可以逐字节地遍历内存。
与malloc的使用类似
int shmid = shmget(k, SHM_SIZE, 0);
shmget 是一个在 Unix 和类 Unix 系统(如 Linux)中用于创建或获取共享内存段的系统调用。共享内存是多个进程间通信的一种方式,允许多个进程共享同一块内存区域。
k: 这是共享内存段的键(key)。通常,这是一个整数,用于唯一标识共享内存段。如果键的值为 IPC_PRIVATE,那么将创建一个新的、唯一的共享内存段。
SHM_SIZE: 这是你希望创建的共享内存段的大小(以字节为单位)。如果共享内存段已经存在,这个值将被忽略,但通常你会希望它与你期望的大小相匹配。
第三个参数是共享内存段的权限标志。当你传递 0 作为这个参数时,你实际上没有指定任何权限标志。那么函数将尝试获取一个已经存在的共享内存段,并且不会检查或修改其权限。
函数的返回值 shmid 是一个整数,代表共享内存段的标识符。如果函数成功,它将返回这个标识符;如果失败,它将返回 -1。
int shmid = shmget(k, SHM_SIZE, 0);
这行代码尝试获取一个与键 k 关联的共享内存段,如果它不存在并且没有 IPC_CREAT 标志,则调用将失败。返回的标识符(如果成功)将存储在 shmid 变量中。
shmid vs key
shmid: 用户层标定shm唯一性
key:内核层面上唯一标识shm的
只创建时用key,大多数情况用户访问shm用shmid
【匿名/命名管道等基于文件的生命周期随进程】
当进程结束,shm还在吗?
若不显示删除,sm仍然存在,System V shm生命周期随内核。
怎么删?命令行命令删/代码调用接口删(建议第二种)
2.2代码运行与测试
while : ; do ipcs -m ; sleep 1 ; done
物理内存中的shm映射到进程地址空间后,进程地址空间的shm属于内核空间还是用户空间?
映射到虚拟地址空间的shm属于用户空间,当AB两个进程将同一个物理内存的shm映射到各自的PAS(Process Address Space),AB进程通信时,直接进行内存级的读写即可,无需经过其他调用。
匿名管道/命名管道中的pipe/fifo为什么要通过调用系统接口read/write来进行通信?
pipe/fifo都是文件,是OS进行维护和管理的,而映射到PAS的shm相当于一个进程的
以上的工作:创建key/创建shm/映射shm/删除映射/删除shm是在干嘛?
让不同的进程看到同一份“资源”(内存)
2.3shm与pipe的区别
shm在通信方法中速度较快
pipe的通信方式
shm的通信方式
2.4shm缺乏访问控制
- 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方就可以立马看到对方写入的数据。
共享内存是所有进程间通信(IPC),速度最快的,不需要过多的拷贝(不需要将数据给操作系统) - 共享内存缺乏访问控制 会带来并发问题
缺乏访问控制导致:写入方写了一部分数据 读取方就读走了并对这半个数据进行了处理得到了错误的结果
写入方不在/写入方不写 读取方仍在读 双方压根不知道对方的存在 即写入方只知道一味的写 读取方只知道一味的读
3.代码理解shm
3.1Log.hpp
#ifndef _LOG_H_
#define _LOG_H_#include <iostream>
#include <ctime>#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3const std::string tip[] = {"Debug","Notice","Warning","Error"};std::ostream &Log(std::string message, int option)
{// 获取时间戳 time_t timestamp; time(timestamp);time_t timestamp = time(nullptr);if (timestamp == std::time_t(-1)){std::cerr << "获取时间失败" << std::endl;exit(1);}// 获取格式化时间 tm *localtime(const time_t *__timer)tm *timeinfo = std::localtime(×tamp);std::cout << " | "<< 1900 + timeinfo->tm_year << "-"<< 1 + timeinfo->tm_mon << "-"<< timeinfo->tm_mday << " "<< timeinfo->tm_hour << ":"<< timeinfo->tm_min << ":"<< timeinfo->tm_sec<< " | "<< tip[option]<< " | "<< message;return std::cout;
}#endif
3.2common.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"using namespace std; // 实际项目中 不展开#define PATH_NAME "/home/lhr/linux" // 此路径一定要有访问权限
#define PROJ_ID 0x66 // 0~255
#define SHM_SIZE 4096 // 共享内存的大小 最好是页(PAGE: 4096)的整数倍 4kb
#define FIFO_NAME "./fifo" // 当前路径下创建一个fifo管道文件string ConvertToHex(key_t k)
{char buffer[32];snprintf(buffer, sizeof buffer, "0x%x", k);return buffer;
}class Init
{
public:Init(){umask(0);int n = mkfifo(FIFO_NAME, 0666);assert(n == 0);(void)n;Log("create fifo success", Notice) << std::endl;}~Init(){unlink(FIFO_NAME);Log("remove fifo success", Notice) << std::endl;}
};#define READ O_RDONLY
#define WRITE O_WRONLYint OpenFIFO(std::string pathname, int flags)
{int fd = open(pathname.c_str(), flags);assert(fd >= 0);return fd;
}//服务端调用read去阻塞等待数据 直到有人向管道写数据 等待停止
void Wait(int fd)
{//no_use仅仅是实现“读数据”这一动作的对象 其中的数据无意义Log("等待中....", Notice) << std::endl;//read接口从fd读存到tmp 无数据则阻塞等待uint32_t no_use = 0;ssize_t s = read(fd, &no_use, sizeof(uint32_t));assert(s == sizeof(uint32_t));(void)s;
}//客户端调用write向管道写数据 此动作使得管道中添加数据 服务端停止等待
void Wake(int fd)
{//no_use仅仅是实现“写数据”这一动作的对象 其中的数据无意义uint32_t no_use = 1;ssize_t s = write(fd, &no_use, sizeof(uint32_t));assert(s == sizeof(uint32_t));(void)s;Log("唤醒中....", Notice) << std::endl;
}void CloseFifo(int fd)
{close(fd);
}
3.3shmServer.cpp
#include "common.hpp"// 程序加载时自动构建全局对象 -- 调用构造函数创建管道文件
// 程序退出时自动析构全局对象 -- 调用析构函数删除管道文件
Init init;int main()
{// 1. 创建公共的Key值key_t k = ftok(PATH_NAME, PROJ_ID);assert(k != -1);Log("create key done", Debug) << " server key : " << ConvertToHex(k) << endl;// 2. 创建shm shm创建成功后会自动全部置0// server是通信的发起者 建议创建一个全新的共享内存int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);if (shmid == -1){perror("shmget");exit(1);}Log("create shm done", Debug) << " shmid : " << shmid << endl;//sleep(10);// 3. 将指定的共享内存 挂接到自己的地址空间char *shmaddr = (char *)shmat(shmid, nullptr, 0);Log("attach shm done", Debug) << " shmid : " << shmid << endl;//sleep(10);// 开始进行通信 将共享内存看成一个大字符串 char buffer[SHM_SIZE];int fd = OpenFIFO(FIFO_NAME, READ);while (true){Wait(fd);// 临界区printf("%s\n", shmaddr);if (strcmp(shmaddr, "quit") == 0)break;}CloseFifo(fd);// 4. 将指定的共享内存 从自己的地址空间中删除映射int n = shmdt(shmaddr);assert(n != -1);(void)n;Log("detach shm done", Debug) << " shmid : " << shmid << endl;//sleep(10);// 5. 删除共享内存 IPC_RMID: 即便是有进程和当下的shm挂接 依旧删除共享内存n = shmctl(shmid, IPC_RMID, nullptr);assert(n != -1);(void)n;Log("delete shm done", Debug) << " shmid : " << shmid << endl;return 0;
}
3.4shmClient.cpp
#include "common.hpp"int main()
{Log("Client pid is : ", Debug) << getpid() << endl;// 创建公共的Key值key_t k = ftok(PATH_NAME, PROJ_ID); // typedef int key_tif (k < 0){Log("create key failed", Error) << " client key : " << ConvertToHex(k) << endl;exit(1);}Log("create key done", Debug) << " client key : " << ConvertToHex(k) << endl;// 获取共享内存int shmid = shmget(k, SHM_SIZE, 0);if (shmid < 0){Log("obtain shm failed", Error) << " shmid : " << shmid << endl;exit(2);}Log("obtain shm success", Debug) << " shmid : " << shmid << endl;//sleep(10);// 将指定的共享内存 挂接到自己的地址空间char *shmaddr = (char *)shmat(shmid, nullptr, 0);if (shmaddr == nullptr){Log("attach shm failed", Error) << " shmid : " << shmid << endl;exit(3);}Log("attach shm success", Debug) << " shmid : " << shmid << endl;//sleep(10);// 开始通信 client将共享内存看做一个char 类型的bufferint fd = OpenFIFO(FIFO_NAME, WRITE);while (true){ssize_t s = read(0, shmaddr, SHM_SIZE - 1); // 从stdin读数据存到shmif (s > 0){shmaddr[s - 1] = 0; // abcd\n ==> abcd0Wake(fd);if (strcmp(shmaddr, "quit") == 0)break;}}CloseFifo(fd);/* 每一次都向共享内存的起始地址写入char *msg = "hello server, I'm Client. my pid: %d, inc: %c\n";for (char a = 'a'; a <= 'z'; a++){shmaddr[a - 'a'] = a; // 直接当成字符数组来看待snprintf(shmaddr, SHM_SIZE - 1, msg, getpid(), a); // 库函数格式化输入数据sleep(1);}strcpy(shmaddr, "quit");*/// 删除映射int n = shmdt(shmaddr);assert(n != -1);(void)n;Log("detach shm success", Debug) << " shmid : " << shmid << endl;//sleep(10);// client不需要chmctl删除return 0;
}