【进程通信】Syetem V 共享内存(结合代码模拟通信)

简介: 【进程通信】Syetem V 共享内存(结合代码模拟通信)

前言

进程之间除了使用管道来进行通信,还能通过申请共享内存的方式使多个进程访问同一片共享资源。下面介绍System V标准下的共享内存是如何使进程间通信的。

共享内存的原理

其实每一个进程的虚拟空间分布中都有一块区域专门用来存放公共资源,这个区域就是“数据共享区‘。

值得注意的是,这个数据共享区里的数据不属于任何一个进程,而是被多个进程共享。当一个进程连接到共享内存区域时,操作系统会将这块共享内存段映射到进程的虚拟地址空间中,使得进程可以直接访问这块共享内存。共享内存的生命周期是随操作系统的,也就意味着,如果我们不主动删除共享内存,即使创建共享内存的进程已经终止,该共享内存也会一直存在,直到重启或者用指令删除。

所以使用共享内存进行通信有以下步骤:

  1. 创建共享内存
  2. 连接共享内存
  3. 访问共享内存
  4. 分离共享内存
  5. 删除共享内存。

下面将一一介绍上述步骤。

创建共享内存区域

进程可以通过调用shmget函数来创建一个共享内存标识符,并且指定内存大小和权限等参数。该标识符类似与文件描述符fd,是操作系统给用户使用的,用来标识共享内存区域。

关于getshm函数:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

int shmget(key_t key,size_t size,int shmflag);

  • key是一个键值,用于唯一标识共享内存区域。通常使用ftok函数来生成一个键值。
  • size是要创建的共享内存的大小,一般建议是4096的整数倍。
  • shmflg是权限标志,用来指定共享内存的权限和创建的方式,和打开文件的系统调用类似。
  • 创建成功返回一个非负整数(shmid),否则返回-1.

key和shmget返回值的区别

简单来说,key是一个键值,用来标识共享内存。而shmget的返回值即shmid主要用于操作共享内存的标识符。一个是用户在创建时交给操作系统的,另一个则是创建后操作系统给用户来操作的。就好像我们的文件系统中fd与inode的区别。一个文件没有被打开的时候,需要通过inode来找到并打开,而打开之后给用户一个描述符fd用于后续对文件的操作。

参数shmflag

shmflag是一个位图结构,通过按位|来传递多个选项信息。常见的选项有:

  1. IPC_CREAT:如果指定了这个选项,并且键值key对应的共享内存区不存在,则创建一个新的共享内存区。如果已经存在,就返回这个共享内存标识符。
  2. IPC_EXCL:与IPC_CREAT一起使用时,如果指定的共享内存区存在,shmget就返回错误。如果不存在,就创建一个。IPC_EXCL不单独使用。
  3. 一般来说,创建共享内存的进程需要使用IPC_CREAT|IPC_EXCL选项,因为这样创建出来的共享内存一定是最新的,就是目前没有其它进程使用的。而想使用这个已经被创建出来的共享内存的进程就使用IPC_CREAT.

参数key

我们可以通过ftok函数来获取一个key

#include<sys/types.h>
#include<sys/ipc.h>
key_t ftok(const char*pathname,int proj_id);

ftok函数可以根据一个已存在的文件路径pathname和一个整型的proj_id来转换成一个key。我们可以将pathname和proj_id的组合看成是一个规则,同样的规则生成的key相同。也就意味着,当我们创建shm之后,后面要是有进程想使用这个shm,我们就可以通过同样的pathname和proj_id的组合来获得同一个key,用同一个key获得的shmid也就是一样的。这样就能让不同的进程对同一个共享内存操作了!(通信的前提)

共享内存数据结构

为了管理这些共享内存,操作系统对这些共享内存做了描述并组织。下面给出共享内存是如何被描述的:

struct shmid_ds {
 struct ipc_perm shm_perm; /* operation perms */
 int shm_segsz; /* size of segment (bytes) */
 __kernel_time_t shm_atime; /* last attach time */
 __kernel_time_t shm_dtime; /* last detach time */
 __kernel_time_t shm_ctime; /* last change time */
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short shm_nattch; /* no. of current attaches */
 unsigned short shm_unused; /* compatibility */
 void *shm_unused2; /* ditto - used by DIPC */
 void *shm_unused3; /* unused */
};

结构体shmid_ds其实就是一个共享内存的所有属性集合。包括了共享内存的创建时间、权限、修改时间、挂接数等。操作系统把这些shmid_ds集合在一起管理,所有的shmid_ds组合在一起就形成了一个数组。于是对共享内存的管理就变成了对shmid_ds数组的管理。而shmget函数的返回值shmid其实就是这个数组的下标。

用指令查询共享内存

使用ipcs -m指令可以查看所有的共享内存的信息。

同样,我们也可以使用ipcrm -m shmid来删除标识符为shmid的共享内存。

连接共享内存

我们已经知道如何创建一个共享内存了,那么该如何使用共享内存呢?我们需要将创建的共享内存段连接到进程的虚拟地址空间,并通过页表建立与物理内存空间的映射 。这样进程就能使用共享内存了。如何连接共享内存呢?可以使用shmat函数。

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

  • shmid,共享内存的用户级标识符。
  • shmaddr,用于指定shm映射到进程虚拟地址空间的位置。如果设置为NULL,系统会自动找一个合适的位置来加载shm。如果不为空,且shmlag选项包括了SHM_RND,系统就会在地址shmaddr处加载shm.
  • shmflag,用于设置连接shm的一些属性,是一个位图,可以表示多个选项。
  • 如果shmat调用成功,返回shm加载到进程地址空间的起始位置,否则返回(void*)-1

分离共享内存

既然我们能通过shmat连接一个shm,也能”卸载“这个shm。

通过shmdt函数分离shm:

#include<sys/types.h>
#include<sys/shm.h>

int shmdt(const void* shmaddr);

shmaddr即进程地址空间中的shm的起始地址

删除共享内存

既然能创建一个共享内存,同样也能删除一个共享内存。

使用shmctl函数来释放shm.

#include<sys/ipc.h>
#include<sys/shm.h>

int shmctl(int shmid,int cmd,struct shmid_ds *buf);


  1. cmd表示对共享内存的操作。具体操作有以下几种:
  • IPC_STAT:将与shmid相关的内核级数据结构shmid_ds的数据拷贝到buf中
  • IPC_SET:将buf指向的shmid_ds的值拷贝到shmid的数据中
  • IPC_RMID:删除共享内存
  1. buf一个指向shimd_ds结构的指针

用共享内存模拟Server与Client的通信

Shm.hpp

用来描述封装共享内存的操作集和属性集

#ifndef __SHM_HPP__
#define __SHM_HPP__

#include <iostream>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string>
#include <cstring>
#include <unistd.h>

using namespace std;
const int gCreater = 1;
const int gUser = 2;
const string pathname = "/home/tsx/linux/24-5-2/myShm/4.shm";
const int proj_id = 0x66;
const size_t gShmSize = 4096;

class Shm
{
private:
    key_t GetKey()
    { // 获得key
        key_t k = ftok(_pathname.c_str(), _proj_id);
        if (k < 0)
        {
            perror("ftok");
        }
        return k;
    }

    int GetShm(key_t key, int size, int flag)
    { // 通过系统调用获得shm
        int shmid = shmget(key, size, flag);
        if (shmid < 0)
        {
            perror("shmget");
        }
        return shmid;
    }

    string PrintWho()
    {
        if (_who == 1)
            return "gCrearter";
        else if (_who == 2)
            return "gUser";
        else
            return "None";
    }

    void *AttachShm()
    {
        if (_shmaddr != nullptr)
        {
            DetachShm();
        }
        void *shmaddr = shmat(_shmid, nullptr, 0);
        if (shmaddr == nullptr)
        {
            perror("shmat");
        }
        cout << "who: " << PrintWho() << " attachshm :" << endl;
        return shmaddr;
    }

    void DetachShm()
    {
        if (_shmaddr == nullptr)
        {
            return;
        }
        shmdt(_shmaddr);
        cout << "who: " << PrintWho() << " deattachshm :" << endl;
    }

    void DeleteShm()
    {
        if (_who == gCreater)
        {
            int res = shmctl(_shmid, IPC_RMID, nullptr);
            if (res == -1)
            {
                perror("shmclt");
            }
        }
    }

public:
    Shm(const string &pathname, const int proj_id, int who)
        : _pathname(pathname), _proj_id(proj_id), _who(who), _shmaddr(nullptr)
    {
        // 获得_key
        _key = GetKey();
        // 创建shm
        if (_who == gCreater)
        {
            GetShmForCreater();
        }
        else if (_who == gUser)
        {
            GetShmForUse();
        }
        _shmaddr = AttachShm(); // 连接
    }
    ~Shm()
    {
        DetachShm(); // 分离连接shm
        DeleteShm(); // 删除shm
    }

    bool GetShmForCreater()
    { // server创建shm
        _shmid = GetShm(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
        if (_shmid < 0)
            return false;
        cout << "server成功创建shm" << endl;
        return true;
    }

    void InitShmaddr()
    {
        if (_shmaddr)
        {
            memset(_shmaddr, 0, sizeof gShmSize);
        }
    }
    void *GetShmaddr()
    {
        return _shmaddr;
    }
    bool GetShmForUse()
    {
        _shmid = GetShm(_key, gShmSize, IPC_CREAT | 0666);
        if (_shmid < 0)
            return false;
        cout << "client成功获得shm" << endl;
        return true;
    }

private:
    key_t _key;
    int _shmid;
    int _id;
    string _pathname;
    int _who;
    int _proj_id;
    void *_shmaddr;
};

#endif

NamePipe.hpp

封装了命名管道的属性集和方法集,在共享内存中使用命名管道可以保证数据的同步。

#include <iostream>
#include <string>
#include <cstdio>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;

const string comm_path = "/home/tsx/linux/24-5-2/myShm/myfifo";
#define Creater 1 // 身份码,1表示创建管道者,2表示使用者
#define User 2
#define Defaultfd -1
#define Read_Mode O_RDONLY
#define Write_Mode O_WRONLY
#define BaseSize 4096 // 默认读管道的数据大小

class NamePipe
{
private:
  void CreatFifo()
  {
    int res = mkfifo(_fifo_path.c_str(), 0777);
    if (res != 0)
    {
      perror("mkfifo");
    }
  }

  bool OpenNameFifo(int mode)
  {
    _fd = open(_fifo_path.c_str(), mode);
    if (_fd < 0)
    {
      cout << _fd << endl;
      return false;
    }

    return true;
  }

public:
  NamePipe(const string &path, int who) // 构造函数,根据身份码来决定谁创建管道
      : _fifo_path(path), _id(who), _fd(Defaultfd)
  {
    if (_id == Creater)
    {
      CreatFifo(); // 创建管道
      cout << "creat a fifo" << endl;
    }
  }

  // 读写操作
  bool OpenForWrite()
  {
    return OpenNameFifo(Write_Mode);
  }

  bool OpenForRead()
  {
    return OpenNameFifo(Read_Mode);
  }

  int ReadNamePipe(string &out)
  { // 向管道中读取数据
    char buffer[BaseSize];
    int n = read(_fd, buffer, sizeof(buffer) - 1);
    if (n > 0)
    {
      buffer[n] = '\0';
      out = buffer;
    }
    return n;
  }

  int WriteNamePipe(string in)
  { // 像管道中写数据
    return write(_fd, in.c_str(), in.size());
  }

  ~NamePipe() // 析构,删除管道文件,关闭文件
  {
    if (_id == Creater)
    {
      int res = unlink(_fifo_path.c_str()); // 删除管道文件
      if (res != 0)
      {
        perror("unlink");
      }
    }
    if (_fd != Defaultfd)
    { // 关闭文件
      close(_fd);
    }
    }
  void Print()
  {
    cout << _fd << " " << _fifo_path << " " << _id << endl;
  }

private:
  int _fd;                 // FIFO描述fu
  const string _fifo_path; // 管道路径
  int _id;                 // 身份码
};

Server.cpp

接收数据,当命名管道中有数据的时候才会读共享内存的数据。

#include "shm.hpp"
#include "NamePipe.hpp"
#include <iostream>

using namespace std;

int main()
{
    // 创建共享内存x
    Shm shm(pathname, proj_id, gCreater);
    char *shmaddr = (char *)shm.GetShmaddr();
    shm.InitShmaddr();
    sleep(3);
    // 创建命名管道
    NamePipe fifo(comm_path, Creater);
    fifo.OpenForRead();

    while (true)
    {
        string message = "";
        fifo.ReadNamePipe(message);
        cout << "shm memory content: " << shmaddr << endl; // 读取共享内存中的数据
    }
    return 0;
}

Client.cpp

发送数据,先用命名管道给读端发送,提示共享内存已经被写入了数据。

#include "shm.hpp"
#include "NamePipe.hpp"
#include <iostream>

using namespace std;

int main()
{
    // 创建共享内存
    Shm shm(pathname, proj_id, gUser);
    // shm.InitShmaddr();
    char *shmaddr = (char *)shm.GetShmaddr();

    sleep(3);
    // 创建命名管道
    NamePipe fifo(comm_path, User);
    fifo.OpenForWrite();

    char ch = 'A';
    while (ch <= 'Z')
    {
        shmaddr[ch - 'A'] = ch;
        string message = "weakup";
        fifo.WriteNamePipe(message);
        cout << "add: " << ch << " to Server" << endl;
        sleep(2);
        ch++;   
    }
    return 0;
}

总结

共享内存是进程通信最快的方法,因为没有额外的拷贝过程,直接对内存进行读取。此外,共享内存并不保证通信的同步,数据在读取之后不会自动清空释放,也就意味着无论何时只要想读就能读。所以很多时候,为了保证数据的同步性,即写入端进程写入数据后才让读端进程读取数据,我们可以让管道做一个提示作用。当有数据要写入的时候,就让管道发出一个提示,读端读到提示之后才从shm中读取数据。否则,读端就会堵塞等待。

此外,根据共享内存的特性,通信双方都可以同时向shm中读或者写数据。这也就意味着共享内存通信的模式是全双工的。

相关文章
|
7天前
|
安全 Python
告别低效编程!Python线程与进程并发技术详解,让你的代码飞起来!
【7月更文挑战第9天】Python并发编程提升效率:**理解并发与并行,线程借助`threading`模块处理IO密集型任务,受限于GIL;进程用`multiprocessing`实现并行,绕过GIL限制。示例展示线程和进程创建及同步。选择合适模型,注意线程安全,利用多核,优化性能,实现高效并发编程。
22 3
|
6天前
|
算法 安全 调度
深入理解操作系统:进程调度与内存管理
【7月更文挑战第10天】本文将深入探讨操作系统的核心机制,即进程调度和内存管理。我们将从理论和实践的角度出发,解释这些机制如何影响系统性能和用户体验。通过分析不同的调度算法和内存分配策略,我们旨在揭示操作系统设计背后的复杂性和精妙之处。
|
12天前
|
消息中间件 分布式计算 网络协议
从管道路由到共享内存:进程间通信的演变(内附通信方式经典面试题及详解)
进程间通信(Inter-Process Communication, IPC)是计算机科学中的一个重要概念,指的是运行在同一系统或不同系统上的多个进程之间互相发送和接收信息的能力。IPC机制允许进程间共享数据、协调执行流程,是实现分布式系统、多任务操作系统和并发编程的基础。
从管道路由到共享内存:进程间通信的演变(内附通信方式经典面试题及详解)
|
15天前
|
存储 安全 Linux
深入理解操作系统:从进程管理到内存分配
【6月更文挑战第30天】在数字时代的心脏,操作系统是现代计算不可或缺的组成部分。本文将深入探讨操作系统的核心功能,包括进程管理、内存分配以及文件系统管理。我们将通过实际案例分析,揭示这些机制如何在提高计算机性能的同时保证资源的有效利用。文章旨在为读者提供对操作系统工作原理的深刻理解,并展示其在现代技术中的应用价值。
|
2天前
|
Unix Linux Python
`subprocess`模块是Python中用于生成新进程、连接到它们的输入/输出/错误管道,并获取它们的返回(退出)代码的模块。
`subprocess`模块是Python中用于生成新进程、连接到它们的输入/输出/错误管道,并获取它们的返回(退出)代码的模块。
6 0
|
4天前
|
消息中间件 Linux
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(下)
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(下)
14 0
|
4天前
|
消息中间件 存储 Linux
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(上)
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(上)
11 0
|
4天前
|
安全 Linux 数据格式
【Linux】进程通信----管道通信(下)
【Linux】进程通信----管道通信(下)
14 0
|
4天前
|
Unix Linux
【Linux】进程通信----管道通信(上)
【Linux】进程通信----管道通信(上)
21 0
后端登录接口使用postman,无法接收返回数据,怎样解决,认真比较与原项目的代码,看看有没有写的不一样的,问题就能解决,不要多少写,根据postman的提示先找到错误的进程,看错误进程出现在那个进程
后端登录接口使用postman,无法接收返回数据,怎样解决,认真比较与原项目的代码,看看有没有写的不一样的,问题就能解决,不要多少写,根据postman的提示先找到错误的进程,看错误进程出现在那个进程