突破传统通信模式:利用共享内存实现极速服务端与客户端对话
共享内存是一种高效的进程间通信(IPC)方式,允许多个进程访问同一块内存区域。通过共享内存,进程之间可以直接读写数据,而无需经过操作系统的中介,从而减少了上下文切换的开销。在服务端和客户端通信中,服务端通常会创建共享内存并将数据写入该区域,客户端可以直接读取该数据。由于共享内存不需要通过管道、套接字等其他通信方式,因此在高性能应用中非常有用。实现简单的服务端与客户端通信时,服务端负责创建并初始化共享内存,客户端则连接到该内存并读取或写入数据。这样,双方可以通过共享内存高效地交换信息。
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!
👍点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
🚀分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对Linux OS感兴趣的朋友,让我们一起进步!
前言
System V标准(又称System V IPC)是UNIX操作系统中的一组标准,定义了进程间通信(IPC)机制,旨在使不同UNIX系统之间的通信操作具有一致性。System V提供多种IPC机制,包括共享内存,消息队列,信号量等。
一. 共享内存(Shared Memory)
1.1 基本概念
允许多个进程访问同一块物理内存区域。这是最有效的IPC机制,速度最快,因为数据不需要通过内核传递,而是直接在进程之间共享。
1.2 共享内存相关函数接口
1.2.1 shmget()函数
- 语法如下:
int shmget(key_t key, size_t size, int shmflg);
- 功能:
shmget 是在 System V 标准中用于获取共享内存段的系统调用。它的作用是创建一个新的共享内存段,或者打开一个已存在的共享内存段,并返回一个标识符,供后续操作使用。
参数:
- key:共享内存段的键值。通过它标识共享内存,可以在不同进程间共享。
- size:共享内存段的大小,单位是字节。
- shmflg:标志位,用于指定权限和其他控制选项(例如 IPC_CREAT 用于创建共享内存,如果不存在就创建;否则打开这个已经创建的共享内存,并返回它的shmid。IPC_EXCL单独使用无意义,需结合是使用:IPC_CREAT | IPC_EXCL,如果要创建的共享内存不存在,就创建;如果已经存在就返回-1,表示发生错误)。
- 返回值:
返回值是共享内存的标识符(shmid),用于后续的操作。如果返回值是 -1,则表示发生错误。
注意上述的key值需要用户手动传入给内核:调用ftok()系统调用。
原型如下:
key_t ftok(const char *pathname, int proj_id);
- 参数:
pathname:指定一个存在的文件路径。该文件用作生成键值的基础。该路径必须存在,可以与当前的工作区不相关联。
proj_id:是一个单字节的项目标识符。它通常是一个字母或数字(范围为 0 到 255)。与文件路径结合,生成一个唯一的键值。
- 返回值:
成功时,返回生成的唯一键值(key_t 类型)。该键值通常是一个整数。
失败时,返回 -1,并设置 errno。
因为创建的共享内存随内核,所以要手动删除,如果要删除共享内存,可以使用指令:
- 语法如下:
ipcrm -m [shmid值],注意:shmid不是key值,key只使用OS来识别的,而用户使用shmid来进行操作。
1.2.2 shmat()函数
- 函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 功能:
shmat 是在 System V 标准中用于将共享内存段附加到当前进程地址空间的系统调用。它允许进程访问通过 shmget 创建的共享内存段。调用 shmat 后,进程可以像访问普通内存一样读取和写入共享内存段中的数据。简单点说就是:将共享内存与进程地址空间建立映射关系。
参数: - shmid:共享内存段的标识符,它是通过 shmget 获取的。它用于标识要附加的共享内存段。
- shmaddr:建议附加共享内存段的地址。通常设置为 NULL,让操作系统自动选择一个合适的地址。如果不为
NULL,系统会尽量将共享内存段附加到指定的地址。 - shmflg:标志位。常见的标志包括:SHM_RDONLY:以只读模式操作共享内存。0:以读写模式操作共享内存(默认)。
返回值:
- 成功时,shmat 返回共享内存段的起始地址(是一个指向共享内存的指针)。这个地址是物理地址,可以直接使用,不再需要进行映射了。
- 失败时,返回 -1,并设置 errno。
1.2.3 shmdt()函数
- 函数原型:
int shmdt(const void *shmaddr);
- 功能:
shmdt 是 System V IPC(进程间通信)中的一个系统调用,用于 分离 共享内存段。它的作用是将之前通过 shmat 映射到进程地址空间的共享内存段从进程的地址空间中移除。调用 shmdt 后,进程不能再通过该地址访问共享内存。
参数: - shmaddr:指向共享内存的起始地址,通常是通过 shmat 返回的地址。它是指向共享内存区域的指针。
返回值:
- 成功时,返回 0。
- 失败时,返回 -1,并设置 errno。
注意事项:
- 共享内存的删除:shmdt 只是从进程的地址空间中分离共享内存,并不会删除共享内存段。如果共享内存不再需要,可以使用 shmctl
函数来删除共享内存段。 - 多进程共享内存:当多个进程共享同一块内存时,每个进程在完成共享内存的操作后,都应调用 shmdt
来分离共享内存。如果不再需要共享内存,最终应通过 shmctl 删除共享内存段。 - 清理:在分离共享内存后,如果其他进程仍然附加该共享内存,系统不会立即删除共享内存,直到最后一个进程调用 shmdt并且没有其他进程附加它时,才会被完全清理。
1.2.4 shmctl()函数
- 函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能:
shmctl 是一个用于控制共享内存段的 System V IPC 函数。通过 shmctl,你可以对共享内存段进行多种管理操作,如获取共享内存的状态、删除共享内存段等。它是对共享内存资源的高级管理工具。
参数:
- shmid:共享内存段的标识符,这是通过 shmget 返回的 ID。
- cmd:命令,指定对共享内存段执行的操作。常见的命令有:
<一> IPC_STAT:获取共享内存段的状态,并将信息存储在 shmid_ds 结构中。
<二> IPC_SET:修改共享内存段的权限或其他属性。
<三> IPC_RMID:删除共享内存段,标记该共享内存段为待删除。只有在所有进程都断开对该共享内存的连接后,内存段才会被系统回收。
- buf:指向 shmid_ds 结构体的指针,该结构体用于存储共享内存段的信息(用于 IPC_STAT 和 IPC_SET 命令)。对于IPC_RMID,该参数可以为 NULL。
示例代码:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// 创建共享内存
int shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget failed");
return -1;
}
// 获取共享内存的状态
struct shmid_ds shm_info;
if (shmctl(shmid, IPC_STAT, &shm_info) == -1) {
perror("shmctl IPC_STAT failed");
return -1;
}
// 打印共享内存信息
printf("Shared memory size: %zu bytes\n", shm_info.shm_segsz);
printf("Last attach time: %s", ctime(&shm_info.shm_atime));
// 删除共享内存
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID failed");
return -1;
}
return 0;
}
二. 服务端与客户端通信的实现
2.1 简介
在操作系统中,共享内存是进程间通信(IPC)的一种高效方式,因为它允许多个进程直接访问同一块内存区域,而不需要通过管道、套接字等其他通信机制。使用共享内存,服务端和客户端可以通过读写共享内存来交换数据。
2.2 实现通信
2.2.1 Shm 类(共享内存管理)
功能:
- 用于创建、获取、附加、分离和销毁共享内存。通过 shmget 获取共享内存的标识符,shmat 将共享内存附加到进程的地址空间。
- 使用 ftok 生成唯一的共享内存 key,并通过 IPC_CREAT 创建共享内存。shmctl 用于删除共享内存。
- 这个类区分了两种用户类型:CREATER 和 USER。CREATER 创建共享内存并将数据写入共享内存,而 USER从共享内存读取数据。
示例代码:
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
const int defaultid = 1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0X66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"
class Shm
{
private:
void CreateHelper(int flg)
{
printf("key: 0x%x\n", _key);
// 共享内存的生命周期,随内核
// shmget() 会失败(因为共享内存已存在),导致程序直接终止。
_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 Destory()
{
Detach();
if (_usertype == CREATER)
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n >= 0)
{
printf("shmcyl delete shm: %d success!\n", _shmid);
}
else
{
ERR_EXIT("shmctl");
}
}
}
public:
Shm(const std::string &pathname, int projid, const std::string &usertype)
: _shmid(defaultid),
_size(gsize),
_start_mem(nullptr),
_usertype(usertype)
{
_key = ftok(pathname.c_str(), projid); // 手动创建key值,用于让不同的进程看到一份资源(共享内存)
if (_key < 0)
{
ERR_EXIT("ftok");
}
if (_usertype == CREATER)
Create();
else if (_usertype == USER)
Get();
else
{
}
Attach();
}
void *VirtualAddr()
{
printf("VirtualAddr: %p\n", _start_mem); // 存在于堆栈区间的虚拟地址,堆栈之间的空间是共享区,
// 属于用户的空间,不需要用系统调用
// 共享内存是进程间通信最快的方式:原因1:映射之后直接被对方看到,原因2:不需要系统调用,花费额外的时间开销
return _start_mem;
}
int Size()
{
return _size;
}
void Attr()
{
struct shmid_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()
{
if (_usertype == CREATER)
Destory();
}
private:
int _shmid;
key_t _key;
int _size;
void *_start_mem;
std::string _usertype;
};
2.2.2 nameFifo 类(命名管道管理)
- 用于创建和删除命名管道(FIFO)。mkfifo 用于创建管道,unlink 删除管道文件。
- 提供了管道路径和名称的配置,允许在指定路径下创建命名管道。
注意:该类主要用了实现数据的同步,防止数据不一致,同时当客户端端有数据时(wakeup函数)通知服务端读取(wait函数)数据,真正的数据存在共享内存中。
示例代码:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include "comm.hpp"
#define PATH "."
#define FIFENAME "fifo"
class NameFifo
{
public:
NameFifo(const std::string &path, const std::string &name)
: _path(path), _name(name)
{
_filoname = _path + "/" + _name;
umask(0);
// 新建管道
int n = mkfifo(_filoname.c_str(), 0666);
if (n < 0)
{
ERR_EXIT("mkfifo");
}
else
{
std::cout << "mkfifo success" << std::endl;
}
}
~NameFifo()
{
// 删除管道文件
int n = unlink(_filoname.c_str());
if (n == 0)
{
//ERR_EXIT("unlink");
}
else
{
std::cout << "remove fifo failed" << std::endl;
}
}
private:
std::string _path;
std::string _name;
std::string _filoname;
};
2.2.3 FileOper 类(管道文件操作)
- 提供了打开管道的功能,支持读写操作。
- OpenForRead 打开管道以进行读取,OpenForWrite 打开管道以进行写入。
- Wait 方法等待从管道中读取数据,而 Wakeup 方法用于唤醒阻塞的读取操作。
- 使用 close 方法关闭文件描述符。
示例代码:
class FileOper
{
public:
FileOper(const std::string &path, const std::string &name)
: _path(path), _name(name), _fd(-1)
{
_filoname = _path + "/" + _name;
}
void OpenForRead()
{
// 打开管道文件
_fd = open(_filoname.c_str(), O_RDONLY);
if (_fd < 0)
{
ERR_EXIT("open");
}
std::cout << "open fifo sucess" << std::endl;
}
void OpenForWrite()
{
// write
_fd = open(_filoname.c_str(), O_WRONLY);
if (_fd < 0)
{
ERR_EXIT("open");
}
std::cout << "open fifo success" << std::endl;
}
void Wakeup()
{
// 写入操作
char c = 'c';
int n = write(_fd, &c, 1);
printf("尝试唤醒: %d\n", n);
}
bool Wait()
{
char c;
int number = read(_fd, &c, 1);
if (number > 0)
{
printf("醒来: %d\n", number);
return true;
}
return false;
}
void Close()
{
if (_fd > 0)
close(_fd);
}
~FileOper()
{
}
private:
std::string _path;
std::string _name;
std::string _filoname;
int _fd;
};
2.2.4 总结
通过共享内存和命名管道的协作,程序实现了一个高效的进程间通信机制。共享内存提供了高效的直接数据交换通道,而管道则充当了同步信号的角色,确保数据的可靠传输。
三. 最后
本文介绍了利用共享内存实现高效服务端-客户端通信的方法。共享内存作为进程间直接访问的内存区域,极大提升了数据传输效率,避免了传统IPC方式的数据拷贝开销。配合命名管道(FIFO)进行同步控制,服务端写入数据后通过管道发送唤醒信号,客户端阻塞等待信号后读取共享内存,确保数据一致性。Shm类封装了共享内存的创建、映射与销毁,FileOper类处理管道读写,二者协作实现了通知-读取的高效通信模式,适用于高频数据交换场景,兼顾性能与可靠性。