进程间通信--共享内存篇

19 篇文章 2 订阅

共享内存的概念

共享内存字面理解就是进程间共同享有的存储空间,不同于管道通信,共享内存就像是进程自己的空间一样,不像管道文件还得使用文件描述符去访问文件,通过文件交流信息。共享内存则是实时信息交流,几乎不存在信息的中间转换。那么共享内存在哪里呢?就真的是在每个进程中都保留一份么?也不是,开辟的共享内存在整个内存空间中独一份,但是可以通过页表映射到不同的进程中去,让各个进程都能够看到这份资源,实现通信。下面是结构图式的理解:

image-20221212220923003

这里要注意区别于父子进程的写时拷贝现象,与其相反,共享内存内的数据在被一个进程修改时,其余进程所看到的资源都会是被修改过的。只有这样,进程间的通信才成为可能。

🙋‍♂️:为什么父子进程不像共享内存那样处理呢,舍弃写时拷贝,这样父子进程间不就可以直接通过一些变量进行通信了嘛?

👨‍🏫:理论上在设计时可以这么处理,但是有些场景下我们可能并不需要父子进程间的变量强相关,这会增加使用某些变量的风险。假如一个变量在父进程中作为计数器,在子进程中作为判断条件,那么就会造成严重的逻辑BUG,处理不好整个程序直接玩完。因此为了降低风险,就有了写时拷贝并设计了共享内存这样的结构,只有在需要的时候,用户自己去使用共享内存,就会安全不少。

共享内存使用须知

根据共享内存的概念特性,我们不难发现,共享内存的使用就像是堆上申请的空间一样,可以直接进行访问和修改,那么也就意味着空间的使用不受控制,用户想怎么来就怎么来,进程间容易出现空间操作混乱的情况,因此使用时需要控制一下空间的使用时序。

除了使用要受到控制这个特点之外,貌似也没有什么需要特别注意的地方,整个流程如下:

image-20221212224142102

创建共享内存

image-20221212224451870

创建共享内存需要使用shmget函数,在内存中开辟空间。这里的参数有点含金量,下面阐述一下各个参数的含义。

key:共享内存的唯一标识。共享内存有存在多个的可能情况,因此得各自区分开来,为创建的与创建好的也得区分开,因此唯一标识符的存在就很好理解了。这个唯一标识符并不是系统自动生成的,而是用户自己提供的,为了方便多个进程想准确快速的使用同一个共享内存,又给我们提供了另一个函数ftok函数:

image-20221212225733548

根据已经存在的文件名pathname(目录也是文件!),再加上一个非零的id值(随意,看用户自己创建不同共享内存的区别规则),系统会根据这两个参数生成一个唯一确定的key值。只要pathname和proj_id相同,生成的key值就会相同。这也就保证了不同进程可以根据同一个路径名和id值,访问到同一个共享内存。

size:共享内存的大小,单位是字节数。这也是共享内存的特点之一,内存空间是按字节数来创建与访问的。

shmflg:共享内存开辟时的属性设置,共有三个参数可以传:

image-20221214163752211

IPC_CREAT:创建一块共享内存,如果已经存在了,就获取它,如果不存在,就黄建一块新的共享内存,并获取它。

IPC_EXCL:与IPC_CREAT搭配使用,根据key值,如果该共享内存存在了,就会出错。不存在的话正常创建并获取它,这也就保证了如果创建内存成功,该共享内存一定是一个全新的。

mode_flags:开辟的空间的权限,与open函数中的mode参数是一个意思,对应 所有者、所属组、其它 三个组的权限。例如:0x666就是所有使用者都能进行读和写的操作。

返回值:返回一个整型数字,类似于文件描述符的东西,是提供给我们用户使用的共享内存的标识,注意区别key这个系统调用接口的标识符,虽然是一个意思,但数值不同,面向的对象也不同。

共享内存的映射与链接

一个进程无论创建不创建共享内存,要想使用共享内存,就必须得将要使用的共享内存映射到自己的共享区上,保证在使用的时候可以找到该共享内存的首地址。

image-20221214173243540

映射与链接的话需要使用shmat函数。

参数:

shmid:就是shmget的返回值,也就是用户级的共享内存标识符,这个参数意义就是链接对应的共享内存。

shmaddr:指针参数,如果shmaddr为NULL,系统将选择一个合适的(未使用的)地址来附加段。一般我们在不涉及到特殊情况下默认传空指针就行。

shmflg:默认传0(还没涉及到复杂的场景,这里就先这样用着)。

返回值:共享内存的首地址,类型是void* ,用户可以自己转换成需要的类型。如果出错的话会返回void* ( -1)。

拿到返回值就意味着可以正常使用该共享内存了。

共享内存的映射取消

如果某个进程不想使用某个共享内存了,就可以将其在共享区的映射给删掉。

image-20221215101950956

参数:

shmaddr:shmat的返回值。

返回值:成功时返回0;错误时返回-1,并设置errno以指示错误的原因。

共享内存的删除

共享内存删除意味着所有的进程都不能使用该内存了,注意区分于映射的取消。

image-20221215101843154

参数:

shmid:共享内存的用户级标识符

cmd:一般传IPC_RMID,当最后一个进程取消映射后,直接删除掉共享内存。

buf:默认我们传空指针。

共享内存实现进程通信

这里我们使用客户端于服务端之间的通信来进行测试。

准备工作:两个可执行程序(客户端与服务端)、两个程序共同看到的头文件(方便看见同一个共享内存)。

注意事项:通信的时候需要进行访问控制,这里我使用的是管道的阻塞式读来控制。

头文件Common.hpp:

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


using namespace std;
#define PATH_NAME "/home/jia/learning_code-college-age"//创建共享内存所使用的文件名
#define PROJ_ID 0x46 //创建共享内存的id值
#define SHM_SIZE 4096 //共享内存的大小
#define FIFO_FILE ".fifo"//管道文件的名字
#define READER O_RDONLY//管道文件的打开方式
#define WRITER O_WRONLY//管道文件的打开方式
key_t CreateKey()//创建key值
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if (key < 0)
    {
        cerr << "ftok: " << strerror(errno) << endl;
        exit(1);
    }
    return key;
}
void CreateFifo(const string fifofile)//创建管道文件
{
    umask(0);
    if(mkfifo(fifofile.c_str(),0666)<0)
    {
        cerr<<"mkfifo: "<<strerror(errno)<<endl;
        exit(2);
    }
}
int Open(const string fifofile, int mode)//以某种方式打开管道文件
{
    return open(fifofile.c_str(),mode);
}

int Wait(int fd)//阻塞式等待
{
    uint32_t values=0;
    return read(fd,&values,sizeof(values));
}

void Signal(int fd)//向文件中写入信息,使得阻塞式等待解除
{
    uint32_t values=1;
    write(fd,&values,sizeof(values));
}

int Close(int fd,const string fifofile)//关闭管道,并删除管道文件
{
    close(fd);
    unlink(fifofile.c_str());
}

服务端IpcShmSer.cc:

#include "Common.hpp"
int main()
{
    cout<<"Ser Begin"<<endl;
    CreateFifo(FIFO_FILE);//创建管道文件
    key_t key = CreateKey();
    cout << "key: " << key << endl;
    int shmid=shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);//创建共享内存
    if(shmid<0)
    {
        cerr<<"shmget: "<<strerror(errno)<<endl;
        return 2;
    }
    cout<<"shmget success, shmid: "<<shmid<<endl;
    int fd=Open(FIFO_FILE,READER);//服务端以读的方式打开管道文件
    cout <<"Open Success,fd: "<<fd<<endl;
    char* str=(char*)shmat(shmid,nullptr,0);//建立映射关系,并获取共享内存首地址
    while(true)
    {
        if(Wait(fd)<=0) break;//阻塞式等待,只要客户端没有向管道写入内容,就一直卡在此处,不进行输出操作。
        printf("%s",str);//使用共享内存,这里是直接输出
    }

    shmdt(str);//取消映射

    shmctl(shmid,IPC_RMID,nullptr);//服务端退出的话要删除共享内存
    Close(fd,FIFO_FILE);//关闭管道文件并删除管道文件
    return 0;
}

客户端IpcShmCli.cc:

#include "Common.hpp"
int main()
{
    cout << "Cli Begin" << endl;
    key_t key = CreateKey();
    cout << "key: " << key << endl;
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT);//获取客户端已经创建好的共享内存
    if (shmid < 0)
    {
        cerr << "shmget: " << strerror(errno) << endl;
        return 2;
    }
    cout << "shmget success, shmid: " << shmid << endl;
    int fd = Open(FIFO_FILE, WRITER);//以写的方式打开管道文件
    cout << "Open Success,fd: " << fd << endl;
    char *str = (char *)shmat(shmid, nullptr, 0);//与共享内存建立映射
    while (true)
    {
        printf("Please Enter# ");
        fflush(stdout);
        ssize_t s = read(0, str, SHM_SIZE);//从标准输入流中拿取数据放到str中,也就是向共享内存写入数据
        if (s > 0 && s < SHM_SIZE)
        {
            str[s] = '\0';
        }
        Signal(fd);//给服务端发信号,表示可以使用共享内存了
    }
    shmdt(str);//结束的话取消映射,客户端并不负责共享内存与管道文件的处理。
    return 0;
}

运行结果:

image-20221215123345706

总结

共享内存的使用其实并不是重点,重点在于如何理解共享内存的概念与特性。

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值