【Linux】进程间通信 —— 共享内存

📕 共享内存的原理

我们知道,如果想实现进程间通信,那么必须要让两个进程看到同一份资源。匿名管道、命名管道 可以实现进程间通信。但是其涉及到文件的创建,所以速度上慢了些。而共享内存就没有这种问题,它是让两个进程直接看到同一份物理内存空间,这样就可以实现进程间通信!!

如下,如果有一种接口,可以在物理内存中开辟出一块空间。
在这里插入图片描述

其次,在进程A中,通过页表,将之前在物理空间开辟的那块空间的地址,映射到A进程的地址空间的共享区中的某个区域,然后就可以将其返回给用户。这样,用户就可以通过进程 A 的共享区,进而访问到物理内存开辟的空间。
进程B同理。这样子,进程A和进程B,就可以看到物理内存中同一块空间,具备了进程间通信的条件!如下图。

而进程 A、B 看到的物理内存中的同一块空间,就是共享内存!!

在这里插入图片描述

管道是让两个进程看到同一个文件,而共享内存是让两个进程看到同一块物理地址,清楚两者的差别!

当不需要进行进程间通信的时候,只需要通过页表,将进程的虚拟地址和物理地址(共享内存)之间的映射关系去掉,然后释放共享内存块,就可以了!

当然,这只是一个宏观上的感知, 要深入理解,必然是要通过写代码的方式!!

📕 代码实现 & 深入理解共享内存

shmget() 函数

申请共享内存块,需要通过 shmget() 函数实现,如下是其介绍。

shmget()
功能:用来创建共享内存
原型
int shmget ( key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的(位图结构)
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

对于该函数第一个参数 key ,它随便怎么设置都可以,只要是一个唯一标识即可,但是一般而言,不会随便传入参数,而是通过另一个函数 ftok() 来得到。
为什么要确保 key 的唯一性呢?这是因为,在操作系统中,不一定 只有一对进程 在进行 进程间通信,可能有多对进程同时通信,每一对通信的进程(假设都使用共享内存方式),都要创建新的共享内存块来维持其通信。那么系统中一定同时存在大量的共享内存,那么操作系统就需要管理它们!当然是先描述、再组织

所以,共享内存 并不是 只需要在内存中开辟空间那么简单,操作系统还要为共享内存创建结构体,里面存放的是共享内存的属性。

那么,共享内存 = 内核数据结构(伪代码 struct shm) + 物理内存开辟的空间

如下,进程 A 创建一块共享内存(红色的),然后进程 B 想要访问这块共享内存,和进程A实现通信。那么,进程 B 就要遍历内存中的 struct shm 对象,找到进程 A 创建的,然后通过该对象,找到共享内存。
在这里插入图片描述

如下,有了 ftok() 创建出的唯一标识,就可以在多个 struct shm 结构体对象中,A进程创建的,从而实现A、B进程的进程间通信。

进程 A 在创建共享内存的时候,调用的 shmget() 函数的第一个参数 key,这是通过 ftok() 函数唯一生成的一个标识,进程A创建共享内存的时候,会把这个唯一标识放到对应的 struct shm 对象里面,进程 B 只需要知道 进程A 创建的 key 值(只要 ftok() 的两个参数一样,生成的 key 值就一样,所以实际上是知道 pathname 和 proj_id),即可找到进程A创建的共享内存。
在这里插入图片描述


如下是对 ftok() 的介绍。第一个参数是路径,第二个参数是项目的 ID。

在这里插入图片描述
如下,可以直接调用 getkey() 函数获得特定的 key 值。 两个进程可以分别调用该函数,获得同一个 key 值。

#define PATH "."
#define PROJID 0x1111

key_t getkey()
{
    key_t k=ftok(PATH,PROJID);
    if(k == -1)
    {
        cout<<"ftok error:"<<errno<<strerror(errno)<<endl;
        exit(1);
    }
    return k;
}

然后就是使用 shmget() 函数,创建共享内存。

如下是对其封装,第三个参数是位图结构,和文件系统里面的 bitmap 有异曲同工之妙。主要用到 IPC_CREAT 和 IPC_EXCL 。

  • 单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回。
  • IPC_EXCL不能单独使用,一般都要配合IPC_CREAT。
  • IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 —— 如果创建成功,对应的shm,一定是最新的!

这里设计 CreateShm 和 Getshm 接口的目的也就很清楚了:如果 进程 A 创建共享内存,那么必定是使用 IPC_CREAT | IPC_EXCL ,那么进程 B 就要通过 key 值找到共享内存,根据上面的规则,需要使用 IPC_CREAT ,所以要设计两个接口,一个给创建共享内存的进程,一个给获取共享内存的进程

当然了,创建共享内存要涉及到权限问题,这里让 拥有者、所属组、other 都是具有读写权限。


#define SIZE 4096  // 共享内存的大小


static int tocreateshm(key_t k,int size,int flag)
{
    int shmid=shmget(k,size,flag);
    if(shmid == -1)
    {
        cout<<"shmget error:"<<errno<<strerror(errno)<<endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t k,int size)
{
    umask(0);
    return tocreateshm(k,size,IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t k,int size)
{
    return tocreateshm(k,size,IPC_CREAT);
}


shmget() 的返回值是 shmid,以后对这块共享内存的一切操作,都是依靠 shmid 的。但是要区分 shmid 和 key ,key 只是用于创建/找到 共享内存,对共享内存进行操作(应用层),是依靠 shmid。

shmctl() 、shmdt()、shmat()

一个进程创建了共享内存,这个进程也无法直接使用该共享内存,因为进程还没有和共享内存关联起来。还需要链接,需要用到 shmat() ,调用该函数之后,进程就和共享内存关联起来,共享内存就可以映射到进程地址空间的共享区, 该函数返回的是 虚拟内存的地址

shmat()
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址(虚拟地址,一般而言我们不知道挂接在哪里,所以设为 nullptr)
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1

如下,可以验证链接这个过程。 使用 ipcs -m 可以查看共享内存,其中 nattch 就代表其链接数。

在这里插入图片描述

链接成功,就可以开始进程间通信啦!通信结束再取消链接。

当进程A、B通信结束,就可以将进程与共享内存去关联,要用到 shmdt() 。

shmdt()
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

如下是代码。

char * AttachShm(int shmid)
{
    char* start=(char*)shmat(shmid,nullptr,0);
    return start;
}

void DetachShm(char* start)
{
    int n=shmdt(start);
    assert(n != -1);
    (void)n;
}

shmctl()
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

该函数可以用来删除共享内存块。如下是删除接口,只需要传入 shmid 即可,这是 shmget() 的返回值。
删除共享内存的原因是,如果两个进程都运行结束了,但是进程并没有删除共享内存块,共享内存块依然保存在那里,它不会自己删除。会造成资源浪费(共享内存的生命周期随操作系统)。

void DelShm(int shmid)
{
    int n=shmctl(shmid,IPC_RMID,nullptr);
    assert(n != -1);
    (void)n;
}

特点

  • 共享内存的内存分配是按照 PAGE 为单位的。
  • 进程链接上共享内存之后,不需要额外的接口,就可以直接通信。(管道需要 write、read )
  • 共享内存没有任何保护机制(同步互斥)

如下,画个图简单理解。
左边代表管道, c 代表客户端,s 代表 服务器端,两个进程通信(假设 c 写、s读),c 要先把数据放到自己的缓冲区,然后拷贝到内核,再从内核拷贝到 s 自己的缓冲区。
右边代表共享内存, c 直接把数据放到共享内存,然后 s 就可以直接看到,不需要多次拷贝,所以速度要快很多!!

共享内存的这种特性,使得它是所有进程间通信方案里面,速度最快的。

在这里插入图片描述

📕 源代码

当然了,使用的时候可以封装成为一个类,这样用起来就更简单了!!

comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP

#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<sys/stat.h>
#include<cassert>
#include<unistd.h>

using namespace std;

#define PATH "."
#define PROJID 0x1111
#define SIZE 4096
#define CLIENT 0
#define SERVER 1


key_t getkey()
{
    key_t k=ftok(PATH,PROJID);
    if(k == -1)
    {
        cout<<"ftok error:"<<errno<<strerror(errno)<<endl;
        exit(1);
    }
    return k;
}

static int tocreateshm(key_t k,int size,int flag)
{
    int shmid=shmget(k,size,flag);
    if(shmid == -1)
    {
        cout<<"shmget error:"<<errno<<strerror(errno)<<endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t k,int size)
{
    umask(0);
    return tocreateshm(k,size,IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t k,int size)
{
    return tocreateshm(k,size,IPC_CREAT);
}

void DelShm(int shmid)
{
    int n=shmctl(shmid,IPC_RMID,nullptr);
    assert(n != -1);
    (void)n;
}

char * AttachShm(int shmid)
{
    char* start=(char*)shmat(shmid,nullptr,0);
    return start;
}

void DetachShm(char* start)
{
    int n=shmdt(start);
    assert(n != -1);
    (void)n;
}

class Init
{
public:
    Init(int t)
        :type(t)
    {
        key_t key=getkey();  // 创建 key 值
        if(type == SERVER)   
              shmid=CreateShm(key,SIZE); // 服务器端创建共享内存
        else  shmid=GetShm(key,SIZE);    // 用户端使用共享内存
        start=AttachShm(shmid);          // 关联
    }

    char* getstart()
    {
        return start;
    }

    ~Init()
    {
        DetachShm(start);   // 去关联
        if(type == SERVER) DelShm(shmid);  // 服务器端删除共享内存
    }

private:
    int type;
    char* start;
    int shmid;
};



#endif // !

server.cc

#include"comm.hpp"

int main()
{
    Init init(SERVER);
    char* start=init.getstart();

    int n = 0;
    while (n <= 30)
    {
        cout << "client -> server# " << start << endl;
        sleep(1);
        n++;
    }  

    return 0;
}

client.cc

#include"comm.hpp"

int main()
{
    Init init(CLIENT);
    char *start = init.getstart();

    char c = 'A';

    while (c <= 'Z')
    {
        start[c - 'A'] = c;
        c++;
        start[c] = '\0';
        sleep(1);
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力努力再努力.xx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值