【进程通信】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中读或者写数据。这也就意味着共享内存通信的模式是全双工的。

相关文章
|
2月前
麒麟系统mate-indicators进程占用内存过高问题解决
【10月更文挑战第7天】麒麟系统mate-indicators进程占用内存过高问题解决
274 2
|
3天前
|
运维 监控 Ubuntu
【运维】如何在Ubuntu中设置一个内存守护进程来确保内存不会溢出
通过设置内存守护进程,可以有效监控和管理系统内存使用情况,防止内存溢出带来的系统崩溃和服务中断。本文介绍了如何在Ubuntu中编写和配置内存守护脚本,并将其设置为systemd服务。通过这种方式,可以在内存使用超过设定阈值时自动采取措施,确保系统稳定运行。
18 4
|
16天前
|
C语言 开发者 内存技术
探索操作系统核心:从进程管理到内存分配
本文将深入探讨操作系统的两大核心功能——进程管理和内存分配。通过直观的代码示例,我们将了解如何在操作系统中实现这些基本功能,以及它们如何影响系统性能和稳定性。文章旨在为读者提供一个清晰的操作系统内部工作机制视角,同时强调理解和掌握这些概念对于任何软件开发人员的重要性。
|
15天前
|
Linux 调度 C语言
深入理解操作系统:从进程管理到内存优化
本文旨在为读者提供一次深入浅出的操作系统之旅,从进程管理的基本概念出发,逐步探索到内存管理的高级技巧。我们将通过实际代码示例,揭示操作系统如何高效地调度和优化资源,确保系统稳定运行。无论你是初学者还是有一定基础的开发者,这篇文章都将为你打开一扇了解操作系统深层工作原理的大门。
|
19天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
46 5
|
20天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
49 1
|
25天前
|
算法 调度 开发者
深入理解操作系统:从进程管理到内存分配
本文旨在为读者提供一个深入浅出的操作系统知识之旅,从进程管理的基础概念出发,探索内存分配的策略与技巧。我们将通过实际代码示例,揭示操作系统背后的逻辑与奥秘,帮助读者构建起对操作系统工作原理的直观理解。文章不仅涵盖理论知识,还提供实践操作的指导,使读者能够将抽象的概念转化为具体的技能。无论你是初学者还是有一定基础的开发者,都能在这篇文章中找到有价值的信息和启发。
|
29天前
|
算法 调度 C++
深入理解操作系统:从进程管理到内存分配
【10月更文挑战第42天】本文将带你进入操作系统的神秘世界,探索其核心概念和关键技术。我们将从进程管理开始,了解操作系统如何协调和管理多个程序的运行;然后,我们将深入研究内存分配,看看操作系统如何有效地分配和管理计算机的内存资源。通过这篇文章,你将获得对操作系统工作原理的深入理解,并学会如何编写高效的代码来利用这些原理。
|
1月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
1月前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。