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

相关文章
|
1月前
麒麟系统mate-indicators进程占用内存过高问题解决
【10月更文挑战第7天】麒麟系统mate-indicators进程占用内存过高问题解决
169 2
|
2月前
|
存储 Linux 调度
深入理解操作系统:从进程管理到内存分配
【8月更文挑战第44天】本文将带你深入操作系统的核心,探索其背后的原理和机制。我们将从进程管理开始,理解如何创建、调度和管理进程。然后,我们将探讨内存分配,了解操作系统如何管理计算机的内存资源。最后,我们将通过一些代码示例,展示这些概念是如何在实际操作系统中实现的。无论你是初学者还是有经验的开发者,这篇文章都将为你提供新的视角和深入的理解。
|
13天前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。
|
13天前
|
消息中间件 存储 供应链
进程间通信方式-----消息队列通信
【10月更文挑战第29天】消息队列通信是一种强大而灵活的进程间通信机制,它通过异步通信、解耦和缓冲等特性,为分布式系统和多进程应用提供了高效的通信方式。在实际应用中,需要根据具体的需求和场景,合理地选择和使用消息队列,以充分发挥其优势,同时注意其可能带来的复杂性和性能开销等问题。
|
20天前
|
存储 JavaScript 前端开发
如何优化代码以避免闭包引起的内存泄露
本文介绍了闭包引起内存泄露的原因,并提供了几种优化代码的策略,帮助开发者有效避免内存泄露问题,提升应用性能。
|
1月前
|
缓存 算法 调度
深入浅出操作系统:从进程管理到内存优化
本文旨在为读者提供一次深入浅出的操作系统之旅。我们将从进程管理的基本概念出发,逐步深入到内存管理的复杂世界,最终探索如何通过实践技巧来优化系统性能。文章将结合理论与实践,通过代码示例,帮助读者更好地理解操作系统的核心机制及其在日常技术工作中的重要性。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开一扇通往操作系统深层次理解的大门。
|
1月前
|
存储 Python
Python中的多进程通信实践指南
Python中的多进程通信实践指南
19 0
|
1月前
麒麟系统mate-indicators进程占用内存过高问题解决
【10月更文挑战第5天】麒麟系统mate-indicators进程占用内存过高问题解决
130 0
|
2月前
|
监控 Ubuntu API
Python脚本监控Ubuntu系统进程内存的实现方式
通过这种方法,我们可以很容易地监控Ubuntu系统中进程的内存使用情况,对于性能分析和资源管理具有很大的帮助。这只是 `psutil`库功能的冰山一角,`psutil`还能够提供更多关于系统和进程的详细信息,强烈推荐进一步探索这个强大的库。
42 1
|
2月前
|
Java Android开发 数据安全/隐私保护
Android中多进程通信有几种方式?需要注意哪些问题?
本文介绍了Android中的多进程通信(IPC),探讨了IPC的重要性及其实现方式,如Intent、Binder、AIDL等,并通过一个使用Binder机制的示例详细说明了其实现过程。
307 4