【Linux篇】共享内存实战:打造高性能服务端与客户端通信的终极指南(赋源码)


共享内存是一种高效的进程间通信(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,你可以对共享内存段进行多种管理操作,如获取共享内存的状态、删除共享内存段等。它是对共享内存资源的高级管理工具。
参数:

  1. shmid:共享内存段的标识符,这是通过 shmget 返回的 ID。
  2. cmd:命令,指定对共享内存段执行的操作。常见的命令有:

<一> IPC_STAT:获取共享内存段的状态,并将信息存储在 shmid_ds 结构中。

<二> IPC_SET:修改共享内存段的权限或其他属性。

<三> IPC_RMID:删除共享内存段,标记该共享内存段为待删除。只有在所有进程都断开对该共享内存的连接后,内存段才会被系统回收。

  1. 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类处理管道读写,二者协作实现了通知-读取的高效通信模式,适用于高频数据交换场景,兼顾性能与可靠性。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值