在多进程或多线程编程中,进程间通信(Inter-Process Communication, IPC) 是实现数据交换与协作的关键技术。共享内存(Shared Memory)作为一种高效的IPC机制,因其卓越的性能和低延迟性,广泛应用于各种需要快速数据交换的场景中。本文将从理论和底层原理出发,全面解析共享内存的工作机制、优势、应用场景以及在实际开发中需要注意的问题。
共享内存是一种允许多个进程直接访问同一块物理内存区域的通信机制。通过共享内存,进程无需通过内核频繁拷贝数据,可以实现高效的数据交换。这种机制在需要频繁、大量数据传输的应用中,如实时数据处理、图像处理、数据库缓存等,表现尤为出色。
- 物理内存映射:共享内存区域映射到多个进程的虚拟地址空间中,使得这些进程可以直接读写这块内存。
- 内存标识符:每块共享内存都有一个唯一的标识符,用于进程间的引用和管理。
- 生命周期管理:共享内存的创建、连接、分离和销毁需要明确的管理,以避免内存泄漏和资源冲突。
因此也就是说这些工作都是OS一个人做的,是不是就代表着这些共享内存可以存在很多份呢?既然有不同份数,不同的地址,就注定了操作系统要对其进行管理!
也就是共享内存也有它自己的结构体,有他自己的task_struct和数据,因此OS就一定会有相关的接口供我们使用
共享内存的实现通常依赖于操作系统提供的系统调用。在Linux系统中,常见的共享内存实现是System V共享内存
System V共享内存通过以下步骤实现:
1. 创建或获取共享内存:使用shmget
系统调用,根据键值创建或获取一个共享内存段。
2. 附加共享内存:使用shmat
将共享内存段映射到进程的地址空间。
3. 访问共享内存:进程可以通过指针直接访问共享内存区域,实现数据的读写。
4. 分离共享内存:使用shmdt
将共享内存段从进程的地址空间中分离。
5. 删除共享内存:使用shmctl
删除共享内存段,释放资源。
那么具体是如何调用的呢?
shmget
系统调用shmget
函数用于创建新的共享内存段或访问一个已经存在的共享内存段。它的主要参数包括键值、内存大小和权限标志。
函数原型:
int shmget(key_t key, size_t size, int shmflg);
ftok()
函数生成,用于标识共享内存段。IPC_CREAT
(如果不存在则创建)、IPC_EXCL
(与 IPC_CREAT
同用,确保创建新的段)等。使用示例:
key_t key = ftok("pathname", id);
int shmid = shmget(key, 1024, 0666|IPC_CREAT);
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
shmat
系统调用shmat
函数将共享内存段映射到调用进程的地址空间,使得进程可以通过指针直接访问内存。
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmget
返回的共享内存标识符。NULL
,由系统选择。SHM_RND
(将 shmaddr
向下舍入到共享内存段大小的最近倍数)或0。使用示例:
char* shm_ptr = (char*) shmat(shmid, NULL, 0);
if (shm_ptr == (char *) -1) {
perror("shmat failed");
exit(1);
}
进程可以通过返回的指针直接对共享内存进行读写操作,就如同对普通内存数组或结构体的操作一样。
示例:
strcpy(shm_ptr, "Hello, shared memory!");
printf("Shared memory contains: %s\n", shm_ptr);
shmdt
系统调用shmdt
用于将共享内存段从当前进程的地址空间中分离。尽管分离了内存,但该内存段仍然存在系统中,不会被自动销毁。
函数原型:
int shmdt(const void *shmaddr);
使用示例:
if (shmdt(shm_ptr) == -1) {
perror("shmdt failed");
exit(1);
}
shmctl
系统调用shmctl
允许你对共享内存段进行控制操作,包括删除共享内存段。
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
IPC_RMID
用于删除共享内存段。NULL
。使用示例:
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID failed");
exit(1);
}
通过以上步骤,你可以在程序中有效地创建、使用和管理共享内存,这对于需要高效 IPC 机制的应用尤为重要。
那么接下来就让我们详细的通过代码来学习共享内存,话不多说,上代码!
以下是Shm类的头文件代码:
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include
#include
#include
#include
#include
#include
#include
#include
// 全局常量定义
const int gCreater = 1;
const int gUser = 2;
const std::string gpathname = "./shm";
const int gproj_id = 0x66;
const int gShmSize = 4096; // 4096*n
class Shm
{
private:
// 私有成员函数
key_t GetCommKey();
int GetShmHelper(key_t key, int size, int flag);
std::string RoleToString(int who);
void* AttachShm();
void DetachShm(void* shmaddr);
std::string ToHex(key_t key);
bool GetShmUseCreate();
bool GetShmForUse();
public:
// 构造函数与析构函数
Shm(const std::string &pathname, int proj_id, int who);
~Shm();
// 公有成员函数
void Zero();
void* Addr();
void DebugShm();
private:
// 类成员变量
key_t _key;
int _shmid;
std::string _pathname;
int _proj_id;
int _who;
void* _addrshm;
};
#endif
接下来,我们将逐步解析Shm
类的各个部分。
Shm
类详解Shm
类主要负责管理共享内存的生命周期,包括创建、连接、分离和销毁。通过封装底层的系统调用,Shm
类提供了简洁易用的接口,使得共享内存的操作更加直观和安全。
在头文件的开头,我们定义了一些全局常量和宏:
const int gCreater = 1;
const int gUser = 2;
const std::string gpathname = "/home/whb/code/111/code/lesson22/4.shm";
const int gproj_id = 0x66;
const int gShmSize = 4097; // 4096*n
gCreater
和gUser
:用于区分进程在共享内存中的角色。gCreater
表示创建者,gUser
表示使用者。gpathname
:用于生成共享内存键值的路径名。ftok
函数需要一个存在的路径名。gproj_id
:项目标识符,用于与路径名一起生成唯一的键值。gShmSize
:共享内存的大小。这里设置为4097字节,通常是4096的整数倍。private:
key_t _key;
int _shmid;
std::string _pathname;
int _proj_id;
int _who;
void* _addrshm;
_key
:共享内存的键值,由ftok
生成,用于标识共享内存区域。_shmid
:共享内存标识符,由shmget
返回,用于后续操作。_pathname
和_proj_id
:用于生成共享内存键值的路径名和项目标识符。_who
:角色标识,区分创建者和使用者。_addrshm
:共享内存的地址指针,通过shmat
映射到进程的地址空间。public:
Shm(const std::string &pathname, int proj_id, int who)
: _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr)
{
_key = GetCommKey();
if (_who == gCreater)
GetShmUseCreate();
else if (_who == gUser)
GetShmForUse();
_addrshm = AttachShm();
std::cout << "shmid: " << _shmid << std::endl;
std::cout << "_key: " << ToHex(_key) << std::endl;
}
pathname
:用于生成键值的路径名。proj_id
:项目标识符,用于生成键值。who
:角色标识,gCreater
表示创建者,gUser
表示使用者。GetCommKey()
生成共享内存键值。GetShmUseCreate()
或GetShmForUse()
获取共享内存标识符。AttachShm()
将共享内存映射到进程的地址空间。~Shm()
{
if (_who == gCreater)
{
int res = shmctl(_shmid, IPC_RMID, nullptr);
}
std::cout << "shm remove done..." << std::endl;
}
shmctl
删除共享内存,释放资源。GetCommKey()
key_t GetCommKey()
{
key_t k = ftok(_pathname.c_str(), _proj_id);
if (k < 0)
{
perror("ftok");
}
return k;
}
ftok
根据路径名和项目ID生成共享内存键值。ftok
需要路径存在且有读取权限,否则会失败。GetShmHelper(key_t key, int size, int flag)
int GetShmHelper(key_t key, int size, int flag)
{
int shmid = shmget(key, size, flag);
if (shmid < 0)
{
perror("shmget");
}
return shmid;
}
shmget
函数,获取共享内存标识符。key
:共享内存键值。size
:共享内存大小。flag
:权限标志,如IPC_CREAT
、IPC_EXCL
等。shmget
失败,输出错误信息。RoleToString(int who)
std::string RoleToString(int who)
{
if (who == gCreater)
return "Creater";
else if (who == gUser)
return "gUser";
else
return "None";
}
AttachShm()
void* AttachShm()
{
if (_addrshm != nullptr)
DetachShm(_addrshm);
void* shmaddr = shmat(_shmid, nullptr, 0);
if (shmaddr == nullptr)
{
perror("shmat");
}
std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;
return shmaddr;
}
DetachShm
进行分离。shmat
附加共享内存。shmat
失败,输出错误信息。DetachShm(void* shmaddr)
void DetachShm(void* shmaddr)
{
if (shmaddr == nullptr)
return;
shmdt(shmaddr);
std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;
}
shmdt
分离共享内存。ToHex(key_t key)
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
GetShmUseCreate()
bool GetShmUseCreate()
{
if (_who == gCreater)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid >= 0)
return true;
std::cout << "shm create done..." << std::endl;
}
return false;
}
GetShmHelper
尝试创建共享内存,使用IPC_CREAT | IPC_EXCL
标志确保共享内存不存在时才创建,避免重复创建。true
,否则输出创建完成信息。GetShmForUse()
bool GetShmForUse()
{
if (_who == gUser)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);
if (_shmid >= 0)
return true;
std::cout << "shm get done..." << std::endl;
}
return false;
}
GetShmHelper
获取共享内存标识符,使用IPC_CREAT
标志在共享内存不存在时创建,并设置权限为可读写(0666
)。true
,否则输出获取完成信息。Zero()
void Zero()
{
if(_addrshm)
{
memset(_addrshm, 0, gShmSize);
}
}
Addr()
void* Addr()
{
return _addrshm;
}
DebugShm()
void DebugShm()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
if(n < 0) return;
std::cout << "ds.shm_perm.__key : " << ToHex(ds.shm_perm.__key) << std::endl;
std::cout << "ds.shm_nattch: " << ds.shm_nattch << std::endl;
}
虽然共享内存(Shared Memory)在进程间通信(IPC)中具有高效、低延迟的优势,但其本身也存在一些安全性和同步性的问题。由于多个进程可以同时访问共享内存区域,如果缺乏适当的同步机制,可能会导致数据竞争、数据不一致甚至安全漏洞。为了在保证高性能的同时提升共享内存的安全性,我们可以将共享内存与管道(FIFO)结合使用。
共享内存允许多个进程直接访问同一块物理内存区域,这带来了以下潜在问题:
为了解决上述问题,我们可以将共享内存与命名管道(Named Pipe)结合使用。具体来说:
通过这种组合,管道负责控制数据的读写时机,而共享内存则负责实际的数据传输。这不仅提高了数据传输的效率,还增强了通信过程的安全性和可靠性。
以下是一个结合共享内存和命名管道的示例,包括服务器端(server.cc
)和客户端(client.cc
)的实现。该示例展示了如何通过管道进行通知,从而安全地在共享内存中交换数据。
// server.cc
#include "shm.hpp"
#include "namedPipe.hpp"
#include
#include
#include
#include
#include
// 定义FIFO路径
const std::string CLIENT_TO_SERVER_FIFO = "./client_to_server_fifo";
const std::string SERVER_TO_CLIENT_FIFO = "./server_to_client_fifo";
const std::string SHM_PATH = "./shm"; // 共享内存路径
const int PROJ_ID = 65; // 项目标识符
const int SHMBASESIZE = 4096; // 共享内存大小
// 定义CREATER和USER
#define CREATER 1
#define USER 2
int main() {
// 创建用于接收客户端消息的管道(CREATER)
NamedPipe pipe_recv(CLIENT_TO_SERVER_FIFO, CREATER);
// 打开用于接收客户端消息的管道
if (!pipe_recv.OpenRead()) {
std::cerr << "Failed to open " << CLIENT_TO_SERVER_FIFO << " for reading." << std::endl;
return 1;
}
std::cout << "Opened " << CLIENT_TO_SERVER_FIFO << " for reading." << std::endl;
// 创建用于发送消息到客户端的管道(CREATER)
NamedPipe pipe_send(SERVER_TO_CLIENT_FIFO, CREATER);
// 打开用于发送消息到客户端的管道
if (!pipe_send.OpenWrite()) {
std::cerr << "Failed to open " << SERVER_TO_CLIENT_FIFO << " for writing." << std::endl;
return 1;
}
std::cout << "Opened " << SERVER_TO_CLIENT_FIFO << " for writing." << std::endl;
// 创建共享内存(CREATER)
Shm shared_memory(SHM_PATH, PROJ_ID, CREATER);
if (shared_memory.Addr() == nullptr) {
std::cerr << "Failed to attach shared memory." << std::endl;
return 1;
}
std::cout << "Shared memory attached successfully." << std::endl;
std::cout << "Server is running. Waiting for client messages..." << std::endl;
while (true) {
std::string notify;
// 等待客户端发送通知
int bytes_read = pipe_recv.ReadNamedPipe(¬ify);
if (bytes_read > 0) {
std::cout << "Received notification from client." << std::endl;
// 读取共享内存中的数据
char* data = static_cast<char*>(shared_memory.Addr());
std::string client_data(data);
std::cout << "Data from shared memory: " << client_data << std::endl;
// 处理数据(示例:将数据转换为大写)
std::transform(client_data.begin(), client_data.end(), client_data.begin(), ::toupper);
std::cout << "Processed data: " << client_data << std::endl;
// 将处理后的数据写回共享内存
strncpy(data, client_data.c_str(), SHMBASESIZE);
std::cout << "Processed data written back to shared memory." << std::endl;
// 通过管道通知客户端处理完成
std::string response = "Data processed by server.";
if (pipe_send.WriteNamedPipe(response) == -1) {
std::cerr << "Failed to write response to " << SERVER_TO_CLIENT_FIFO << "." << std::endl;
} else {
std::cout << "Sent response to client." << std::endl;
}
} else if (bytes_read == 0) {
std::cout << "Client disconnected." << std::endl;
break;
} else {
std::cerr << "Error reading from " << CLIENT_TO_SERVER_FIFO << "." << std::endl;
break;
}
}
return 0;
}
代码解析:
管道初始化:
client_to_server_fifo
用于接收来自客户端的通知。server_to_client_fifo
用于向客户端发送处理完成的通知。共享内存初始化:
Shm
类创建或获取共享内存段,并将其附加到服务器进程的地址空间。主循环:
pipe_recv.ReadNamedPipe
等待客户端的通知。pipe_send.WriteNamedPipe
通知客户端处理完成。// client.cc
#include "shm.hpp"
#include "namedPipe.hpp"
#include
#include
#include
#include
// 定义FIFO路径
const std::string CLIENT_TO_SERVER_FIFO = "./client_to_server_fifo";
const std::string SERVER_TO_CLIENT_FIFO = "./server_to_client_fifo";
const std::string SHM_PATH = "./shm"; // 共享内存路径
const int PROJ_ID = 65; // 项目标识符
const int SHMBASESIZE = 4096; // 共享内存大小
// 定义CREATER和USER
#define CREATER 1
#define USER 2
int main() {
// 打开用于发送消息到服务器的管道(USER)
NamedPipe pipe_send(CLIENT_TO_SERVER_FIFO, USER);
if (!pipe_send.OpenWrite()) {
std::cerr << "Failed to open " << CLIENT_TO_SERVER_FIFO << " for writing." << std::endl;
return 1;
}
std::cout << "Opened " << CLIENT_TO_SERVER_FIFO << " for writing." << std::endl;
// 打开用于接收服务器响应的管道(USER)
NamedPipe pipe_recv(SERVER_TO_CLIENT_FIFO, USER);
if (!pipe_recv.OpenRead()) {
std::cerr << "Failed to open " << SERVER_TO_CLIENT_FIFO << " for reading." << std::endl;
return 1;
}
std::cout << "Opened " << SERVER_TO_CLIENT_FIFO << " for reading." << std::endl;
// 连接共享内存(USER)
Shm shared_memory(SHM_PATH, PROJ_ID, USER);
if (shared_memory.Addr() == nullptr) {
std::cerr << "Failed to attach shared memory." << std::endl;
return 1;
}
std::cout << "Shared memory attached successfully." << std::endl;
std::cout << "Client is running. Type messages to send to the server." << std::endl;
std::string input;
while (true) {
// 获取用户输入
std::cout << "Enter message: ";
std::getline(std::cin, input);
if (input.empty()) {
// 输入为空时退出
break;
}
// 将数据写入共享内存
char* data = static_cast<char*>(shared_memory.Addr());
strncpy(data, input.c_str(), SHMBASESIZE);
std::cout << "Data written to shared memory." << std::endl;
// 发送通知给服务器
if (pipe_send.WriteNamedPipe("Data ready") == -1) {
std::cerr << "Failed to write notification to " << CLIENT_TO_SERVER_FIFO << "." << std::endl;
continue;
}
std::cout << "Notification sent to server." << std::endl;
// 等待服务器的响应通知
std::string response;
int bytes_read = pipe_recv.ReadNamedPipe(&response);
if (bytes_read > 0) {
std::cout << "Received from server: " << response << std::endl;
// 读取处理后的数据
std::string processed_data(data);
std::cout << "Processed data from shared memory: " << processed_data << std::endl;
} else if (bytes_read == 0) {
std::cerr << "Server disconnected." << std::endl;
break;
} else {
std::cerr << "Error reading from " << SERVER_TO_CLIENT_FIFO << "." << std::endl;
break;
}
}
return 0;
}
代码解析:
管道初始化:
client_to_server_fifo
用于向服务器发送通知。server_to_client_fifo
用于接收服务器的响应通知。共享内存初始化:
Shm
类连接到已存在的共享内存段,并将其附加到客户端进程的地址空间。主循环:
pipe_send.WriteNamedPipe
发送通知给服务器,表示数据已准备好。pipe_recv.ReadNamedPipe
等待服务器的响应通知。初始化阶段:
数据交换阶段:
终止阶段:
通过将共享内存与管道结合使用,我们能够充分发挥两者的优势,同时克服各自的局限性:
共享内存:
命名管道:
在实际开发中,结合使用共享内存和管道时,需要注意以下几点:
同步机制:
错误处理:
资源管理:
权限控制:
通过以上措施,可以构建一个高效、安全、可靠的进程间通信机制,充分利用共享内存的性能优势,同时确保通信过程的安全性和数据的一致性。
如果您对结合使用共享内存和管道有更多的兴趣,建议进一步研究以下内容:
通过不断学习和实践,您将能够构建更加复杂和高效的系统,充分发挥共享内存和管道在进程间通信中的潜力。
通过结合共享内存与命名管道的方式,我们不仅能够实现高效的数据传输,还能够增强通信过程的安全性和可靠性。希望本文的内容能够帮助您在实际项目中灵活运用这些技术,构建高性能且安全的进程间通信机制。