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

共享内存比管道快哦~

文章目录


前言

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据.

一、共享内存的实现原理

我们以上图为例,A和B是两个进程,他们都有自己的进程地址空间,进程地址空间经过页表映射到物理内存中,而共享内存是什么呢?就是有一个机制在物理内存中直接开好一块内存,这一块内存经过页表的映射到进程地址空间中的共享区(共享区在堆区和栈区之间),然后把共享区这块空间的起始地址返回给用户,两个进程进行同样的工作不就实现了让不同的进程看到同一份资源吗,那么如何让这两个进程取消关联呢?其实很简单,如下图:

 要取消两个进程间的关系只需要修改页表让进程地址空间不再通过页表的映射找到共享内存,并且将开好的空间释放掉,这就完成了进程间的取消关联。以上就是共享内存的基本原理。

二、实现共享内存的代码

首先我们创建所需要的文件,比如这里我创建了shmclient.cc和shmserver.cc以及makefile,然后我们需要用makefile帮我们创建两个可执行程序:

.PHONY:all
all:shmclient shmserver

shmclient:shmclient.cc
	g++ -o $@ $^ -std=c++11

shmserver:shmserver.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shmclient shmserver

当然我们还需要一个存放头文件的文件:

 我们为了防止头文件重复包含所以用了条件编译,下面我们开始正式写代码,在这之前我们需要了解shmget函数,这个函数是用来获取共享内存的:

 此函数有三个参数,第一个参数key我们稍后讲解,第二个参数size就是要开多大的共享内存,第三个参数开的模式,IPC_CREAT的意思是创建一个共享内存,如果共享内存不存在,就创建,如果已经存在了就获取已经存在的共享内存并返回。IPC_EXCL不能单独使用,要配合IPC_CREAT使用,使用方式是(IPC_CREAT | IPC_EXCL),这两个组合在一起就是创建一个共享内存,如果共享内存不存在,就创建一个新的共享内存,如果已经存在了共享内存,就立马出错返回,那么这有什么意义呢?意义就是保证给我们创建的共享内存一定是最新的没有被使用过。

下面我们来看看shmget函数的返回值:

 如果我们创建成功了就给我们返回一个共享内存的标志,如果错误则返回-1.现在我们来讲讲第一个参数key是什么,我们可以看到这个参数的类型key_t,而这个参数实际上我们是需要另一个函数ftok获取的,下面我们来看看ftok函数:

 ftok这个函数的第一个参数是路径,第二个参数是一个int类型的id,这两个参数是用户去填的,ftok函数会结合路径和id转化出一个很难被重复的key值,由于系统中一定会同时存在多个共享内存,所以共享内存不是我们想的那样只要在内存中开辟空间即可,而是系统为了管理共享内存,会构建结构体来描述共享内存,所以共享内存 = 共享内存的内核数据结构 + 真正开辟的内存空间。下面我们用画图的方式解释一下这里的关系:

 中间的4个共享内存结构体是被系统所管理的,红色的共享内存是被进程A开辟的,那么如何让B进程也指向A开辟的这个共享内存呢,只需要让他们两个进程都用同一个路径和同一个id这样就生成了同样的key值,有了这个值B进程就能指向A开辟的那个共享内存了。

理解了以上的知识我们就可以直接写代码了,先将所有的头文件包好,因为两个进程都需要同样的路径和id所以我们直接define一下:

 我们直接将路径定义为一个.,一个点就代表是在当前路径,那么到时候创建共享内存的时候就会创建在当前当前路径了。下面我们直接写一个获取key的函数:

在用ftok函数之前我们先看一下此函数的返回值:

 如果成功就返回已经创建好的key值,如果失败则返回-1.

#ifndef __COMM__HPP__
#define __COMM__HPP__

#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
using namespace std;
#define PATHNAME "."
#define PROJID 0x6666

key_t getKey()
{
    key_t k = ftok(PATHNAME,PROJID);
    if (k==-1)
    {
        cerr<<errno<<" : "<<strerror(errno)<<endl;
        exit(1);
    }
    return k;
}

有了key后下一步我们就可以开始创建共享内存了,从我们刚刚画的图可以看到,一定是有一个进程先创建了共享内存,然后另一个进程去找获取刚刚创建的共享内存,所以我们下一步就是在服务端创建一个共享内存:

 首先我们先用一个全局变量来控制共享内存的大小,然后直接用两个函数封装创建共享内存和打开共享内存:

static int createShmHelper(key_t k,int size,int flag)
{
    int shmid = shmget(k,gsize,flag);
    if (shmid==-1)
    {
        cerr<<errno<<" : "<<strerror(errno)<<endl;
        exit(2);
    }
    return shmid;
}
int creatShm(key_t k,int size)
{
    return createShmHelper(k,size,IPC_CREAT|IPC_EXCL);
}
int getShm(key_t k,int size)
{
    return createShmHelper(k,size,IPC_CREAT);
}

第一个静态函数就是我们创建共享内存的步骤,flag是我们要怎么创建的选项,第一次创建必须是一个全新的没有使用过的,这个给服务端用,第二个是打开已有的共享内存是给客户端用的,然后我们在客户端和服务端也把代码写完整:

 下面我们将代码运行起来测试一下:

 我们将程序运行起来发现并没有问题,下面我们查看共享内存是否创建成功,ipcs这个命令可以显示出三条资源:

第一条资源为消息队列,第二条资源为共享内存段,第三个为共享内存核心数组,我们主要看第二条,可以看到我们创建的1024大小的共享内存创建成功了。当然我们可以只查第二条,命令是ipcs -m :

 下面我们讲一下上面所代表的意思,key就像文件的inode一样,而shmid就像文件的fd一样shmid才是操作共享内存所需要的核心数据。owner是谁创建的,perms是权限,nattch是与这个共享内存的连接数。下面我们在讲一下删除一个共享内存的命令:ipcrm -m +shmid(刚刚讲过shmid是用来操作共享内存的,与文件的fd作用一样):

 我们成功的删除了刚刚创建的共享内存。我们接着代码往下写,创建了共享内存后可以让客户端往服务端写一些东西,然后退出的时候需要删除共享内存,我们先来看看如何删除共享内存:

 利用系统调用删除共享内存需要shmctl函数,第一个参数就是共享内存的shmid,第二个参数是指令,也就是说对共享内存做什么操作,我们看看指令有哪些:

 常见的指令有这么几个,而IPC_RMID就是真正删除共享内存的操作,第一个IPC_STAT是获取共享内存的属性,而第三个参数是一个共享内存的结构体的指针可以查看共享内存对应的属性:

 下面我们就写一个删除共享内存函数:

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

 因为我们删除共享内存的时候不关心它的属性,所以我们直接设为空即可。

 下面我们给大家演示一下查看共享内存的属性:

 下面我们可以在创建共享内存的时候将权限也设置好:

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

下面我们要将创建出来的共享内存与进程关联起来,要关联得需要shmat函数:

 第一个参数我们都知道,第二个参数的意思是可以指定将共享内存挂接到指定的地址处(在这里可以设为nullptr,nullptr后操作系统会自己选择合适的地址挂接),第三个参数是设置共享内存是只读的还是什么其他的权限(我们可以设为0,设为0默认是可读可写)

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

 当我们将进程和共享内存关联后,下一步就是退出前将他们去关联,去关联同样也需要函数,这个函数是shmdt:

 这个函数的参数我们已经讲过了,就是刚刚关联时候给我们返回的那个地址,所以我们可以直接写代码了:

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

 以上内存的所有点我们就讲完了,下面我们通过一个类封装一下让客户端和服务端的代码可以少一些:

#define SERVER 1
#define CLIENT 0
class Init
{
public:
    Init(int type)
    {
        key_t k = getKey();
        if (type==SERVER)
        {
            shmid = creatShm(k,gsize);
        }
        else 
        {
            shmid = getShm(k,gsize);
        }
        start = attachShm(shmid);
    }
    char* getStart()
    {
        return start;
    }
    ~Init()
    {
        detachShm(start);
        if (type==SERVER)
        {
            delShm(shmid);
        }
    }
private:
    char* start;
    int type;
    int shmid;
}

通过以上类的封装,我们的客户端和服务端就能简化为以下这样:

 

 我们可以看到当我们一开始两个端口都运行的时候有两个进程链接在共享内存上,当客户端指令输入完成退出后就只剩下服务端,当服务端也退出后那么连接数就为0了。

 以上就是我们共享内存的所有内容,学了共享内存要知道共享内存是进程间通信的几种方式中访问速度最快的。


总结

使用ipcrm -m 命令删除指定共享内存后,是不会直接释放共享内存的,因为共享内存的生命周期是随操作系统的,只有共享内存当前的映射链接数为0才会被删除释放。

使用ipcrm -a 可以删除所有进程间通信资源(a代表all也就是全部的意思)

ipcrm -m删除共享内存,-q删除消息队列,-s删除信号量

共享内存实现实现通信的原理是因为所有进程操作映射同一块物理内存。

共享内存的操作是非进程安全的,多个进程同时对共享内存读写是有可能造成数据的交叉写入或读取,造成数据混乱。

共享内存的删除操作并非是直接删除,而是拒绝后续映射,只有在当前映射链接数为0的时候表示没有进程访问了,才会真正的被删除。

共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统或者手动删除。

猜你喜欢

转载自blog.csdn.net/Sxy_wspsby/article/details/130441608