【linux】进行间通信——共享内存+消息队列+信号量

共享内存+消息队列+信号量

进程间通信方式目前我们已经学了匿名管道,命名管道。让两个独立的进程通信,前提是看到同一份资源。匿名管道适用于血缘关系的进程,一个打开写端一个打开读端实现的。命名管道适用于完全独立的进程,打开同一份文件实现的。

接下来我们看看剩下的实现进程间通信的方式。

1.共享内存

1.1共享内存的原理

可执行程序加载到内存,OS会对进程进行管理。进程的内核结构是独立的,加载到物理地址的地址也独立,因此进程具有独立性,互不影响。

那共享内存是如何实现两个独立的进程进行通信的呢?

1.申请一块空间(用户给OS发信号,然后OS去申请)

2.将创建好的内存经过页表映射到进程地址空间中的一段区域(将这块区域的起始地址返回给用户,用户通过访问这里的起始地址方式来进行对这块区域的访问)

3.未来不想通信了
a.取消进程和内存的映射关系
b.释放内存

上面就是共享内存的原理。

这块申请的内存--->共享内存

进程和内存建立映射关系过程--->进程和共享内存挂接

取消进程和内存的映射关系--->去关联

释放内存--->释放共享内存

如何理解上面这个过程?

在内存上申请空间,然后把地址返回,是不是像C/C++空间的申请,如malloc函数,底层都是一样的先去物理内存申请空间,然后经过页表映射到进程地址空间,最后把这块空间的起始地址返回用户。虽然过程都是一样,但是malloc申请的空间没有办法让另一个进程看见,因为这块空间是在堆上申请的,OS并没有专门为malloc这样的机制和其他进程建立映射关系策略。

a.进程间通信,是专门设计的,用来IPC
b.共享内存是一种通信方式,所有想通信的进程,都可以用
c.OS中一定可能会同时存在很多的共享内存

1.2共享内存的概念

通过让不同的进程,看到同一个内存的方式:共享内存。

1.3接口的认识

1.创建共享内存。


size 要申请多大的内存空间

shmflg 常用参数。

看到这里这个参数是不是和open接口有点类似,大写的宏。

shmflg是一个标志位。

IPC_CREAT:如果想创建的共享内存不存在,就创建,如果存在就获取
IPC_EXCL:无法单独使用。
IPC_CREAT | IPC_EXCL:如果不存在就创建,如果存在就出错返回。用户创建共享内存如果成功,一定是一个新的共享内存。

创建共享内存非常容易,那如果保证进程看到的是同一块共享内存呢?

key用来保证。


key是什么不重要,能进行唯一性标识最重要。

将路径名和项目标识符转换为key。

随便写个路径和项目id。经过算法转换成key。

两个进程传一样参数,能保证是同一个key,因此可以在系统中找到同一个内存。

返回值。成功是返回共享内存的标识符,失败返回-1。

再来理解key_t key

OS中一定可能会同时存在很多的共享内存。

OS也要对共享内存进行管理--->先描述,在组织。

申请一块空间--->共享内存=物理内存块+共享内存的相关属性

key是什么不重要,能进行唯一标识最重要。

创建共享内存的时候,key能保证共享内存在系统中的唯一性。两个进程如何看到同一份资源,只要另一个进程也看到同一个key。

key在那?
key在共享内存的相关属性这个结构体里。

创建共享内存把key传到shmget,本质上是把key设置进创建好的共享内存的某个属性里,另一个进程获取时,查这么多的共享内存,不是查共享内的物理内存块,而是去遍历共享内存对于的相关属性去查找key。

key传到shmget,设置到共享内存属性中,用来表示共享内存在内核中的唯一性。

返回值返回的共享内存的标识符取名shmid。

shmid vs key的关系

shmid是为了用户去访问共享内存的。就像fd vs inode的关系。

2.共享内存和进程关联

shmid:和哪一个共享内存关联
shmaddr:把共享内存映射到地址空间的那一块区域
shmflg:与读写权限有关,默认设置为0

成功是返回的是对应进程地址空间的起始地址,失败返回-1。

3.删除共享内存之前要先去关联

将共享内存从当前调用这个函数的进程地址空间进行卸装


shmaddr:进程地址空间的首地址

4.删除共享内存

删除共享内存接口是shmctl,本质上控制共享内存,不过常用的是删除选项。

shmid:控制哪一个共享内存
cmd:做什么控制,常用选项IPC_RMID
buf:如果不想获得共享内存的属性可以设置nullptr,不然就传一个对象接收共享内存部分属性信息。

1.4实操

comm.hpp

将写端和读端用的代码封装起来。

cpp 复制代码
#include<iostream>
#include<cerrno>
#include<cstring>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

#define PATHNAME "."
#define PROJ_JD  "0x11"

#define MAX_SIZE 4096

key_t GetKey()
{
    key_t k=ftok(PATHNAME,PROJ_JD);//获得唯一标识key
    if(k < 0)
    {
        //cin cout cerr->stdin stdout stderr(默认打开的三个文件)-(fd)>0,1,2->键盘,显示器,显示器
        cerr<<errno<<":"<<strerror(errno)<<endl;
        exit(1);
    }
    return k;
}

//获得共享内存表示符shmid
int getShmHelper(int key,int shmflg)
{
    int shmid=shemget(key,MAX_SIZE,shmflg);
    if(shmid < 0)
    {
        
        cerr<<errno<<":"<<strerror(errno)<<endl;
        exit(1);
    }
    return shmid;
}

//写端创建共享内存
int CreateShm(key_t key)
{
	//这里运行时会报错。下面再看运行结果有解决方法
    return getShmHelper(key,IPC_CREAT|IPC_EXCL);
}

//读端获取共享内存
int GetShm()
{
    return getShmHelper(key,IPC_CREAT);
}

看运行结果写端和读端的key是一样的,shmid也是一样的。

当我再次执行一样的操作,发现不能再创建共享内存了。显示已经存在了。可是我已经退出进程了啊。OS不会帮我自动关闭吗。

共享内存的生命周期是随操作系统的,不是随进程的

查看IPC资源

ipcs查看IPC资源

ipcs -m 查看共享内存

ipcs -q 查看队列

ipcs -s 查信号量

ipcrm -m shmid 删除共享内存

代码删除共享内存

cpp 复制代码
//删除共享内存
void DelShm(int shmid)
{
//删除共享内存
void DelShm(int shmid)
{
    if(shmctl(shmid,IPC_RMID,nullptr) == -1)
    {
        cerr<<errno<<":"<<strerror(errno)<<endl;
    }

}
}

通信之前需要关联(挂接:将共享内存经页表映射到进程地址空间)

cpp 复制代码
void* attachShm(int shimid)
{
    void* mem=shmat(shimid,nullptr,0);//linux 64位机器指针大小位9
    if((long long)men == -1L)//1L代表是长整型
    {
        cerr<<errno<<":"<<strerror(errno)<<endl;
        exit(1);
    }
    return men;
}

删除共享内存之前需要去关联

cpp 复制代码
void detachShm(const void* adder)
{
    if(shmdt(adder) == -1)
    {
        cerr<<errno<<":"<<strerror(errno)<<endl;
        exit(1);
    }
}

完整代码如下

cpp 复制代码
#include<iostream>
#include<cerrno>
#include<cstring>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>

using namespace std;

#define PATHNAME "."
#define PROJ_JD  0x11

#define MAX_SIZE 4096


key_t GetKey()
{
    key_t k=ftok(PATHNAME,PROJ_JD);
    if(k < 0)
    {
        //cin cout cerr->stdin stdout stderr(默认打开的三个文件)-(fd)>0,1,2->键盘,显示器,显示器
        cerr<<errno<<":"<<strerror(errno)<<endl;
        exit(1);
    }
    return k;
}

//获得共享内存表示符shmid
int getShmHelper(int key,int flage)
{
    int shmid=shmget(key,MAX_SIZE,flage);
    if(shmid < 0)
    {    
        cerr<<errno<<":"<<strerror(errno)<<endl;
        exit(1);
    }
    return shmid;
}

//写端创建共享内存
int CreateShm(key_t key)
{
	//这里运行时会报错。下面再看运行结果有解决方法
    return getShmHelper(key,IPC_CREAT|IPC_EXCL);
}

//读端获取共享内存
int GetShm(key_t key)
{
    return getShmHelper(key,IPC_CREAT);
}

//关联
void* attachShm(int shimid)
{
    void* mem=shmat(shimid,nullptr,0);//linux 64位机器指针大小位9
    if((long long)mem == -1L)//1L代表是长整型
    {
        cerr<<errno<<":"<<strerror(errno)<<endl;
        exit(1);
    }
    return mem;
}

//去关联
void detachShm(const void* adder)
{
    if(shmdt(adder) == -1)
    {
        cerr<<errno<<":"<<strerror(errno)<<endl;
        exit(1);
    }
}

//删除共享内存
void DelShm(int shmid)
{
    if(shmctl(shmid,IPC_RMID,nullptr) == -1)
    {
        cerr<<errno<<":"<<strerror(errno)<<endl;
    }

}

service.cc (写)

cpp 复制代码
#include"comm.hpp"

int main()
{
    key_t key=GetKey();
    printf("key->%d\n",key);
    int shmid=CreateShm(key);
    printf("shimid->%d\n",shmid);

    //关联
    char*start=(char*)attachShm(shmid);//我想将这个地址认为是一字符串;
    printf("attach success, address start: %p\n", start);
    
    //写
    while(true)
    {

    }

    //删除
    DelShm(shmid);

    return 0;
}

共享内存没有read和write这样的接口。
写--->直接把内容写到这块内存
读--->直接打印这块内存

以往我们可能是这样的写法,对于往内存中写有点麻烦了。

cpp 复制代码
    //写
    char buffer[1024];
    const char* messge="hello clint,我是另一个进程,我正在和你通信";
    int id=getpid();
    int cnt=0;
    while(true)
    {
        //以往我们的做法
        snprintf(buffer,sizeof buffer,"%s([%d]->[%d])",messge,cnt++,id);
        memcpy(start,buffer,strlen(buffer)+1);
    }

看这个函数,我们可以直接把内容写到内存。

完整代码如下

cpp 复制代码
#include"comm.hpp"

int main()
{
    key_t key=GetKey();
    printf("key->%d\n",key);
    int shmid=CreateShm(key);
    printf("shimid->%d\n",shmid);

    //关联
    char* start=(char*)attachShm(shmid);//我想将这个地址认为是一字符串;
    printf("attach success, address start: %p\n", start);
    
    //写
    char buffer[1024];
    const char* messge="hello clint,我是另一个进程,我正在和你通信";
    int id=getpid();
    int cnt=0;
    while(true)
    {
        snprintf(start,MAX_SIZE,"%s([%d]->[%d])",messge,cnt++,id);
        //以往我们的做法
        // snprintf(buffer,sizeof buffer,"%s([%d]->[%d])",messge,cnt++,id));
        // memcpy(start,buffer,strlen(buffer)+1);
    }

    //去关联
    detachShm(start);

    //删除
    DelShm(shmid);

    return 0;
}

clint.cc (读)

cpp 复制代码
#include"comm.hpp"

int main()
{
    key_t key=GetKey();
    printf("key->%d\n",key);
    int shmid=GetShm(key);
    printf("shimid->%d\n",shmid);

    char* start=(char*)attachShm(shmid);
    printf("attach success, address start: %p\n", start);

    while(true)
    {
         printf("service say : %s\n", start);
    }

    return 0;
}

运行时报了这个没有权限的错误。

这是因为再创建共享内存的时候没有给权限。

补上权限即可

看运行结果,虽然两个进程进行了通信,但是这个通信有点问题。

共享内存不像管道那样,阻塞等待,而是一直在读。

1.5共享内存的总结

共享内存的特点:

1.共享内存的生命周期是随OS的,不是随进程的。
2.所有进程间通信速度最快的(能大大的减少数据的拷贝次数)--->优点。

同样的代码,如果用管道来实现,综合考虑管道和共享内存,考虑键盘输入和显示器输出,共享内存有几次数据拷贝?管道呢?

3.不给我进行同步和互斥的操作,没有对数据做任何保护---->缺点

1.6共享内存的内核结构

给这个接口传一个struct shmid_de 对象,就看到共享内存一些信息。

cpp 复制代码
#include"comm.hpp"

int main()
{
    key_t key=GetKey();
    printf("key->%d\n",key);
    int shmid=GetShm(key);
    printf("shimid->%d\n",shmid);

    char* start=(char*)attachShm(shmid);
    printf("attach success, address start: %p\n", start);

    while(true)
    {
         printf("service say : %s\n", start);
         struct shmid_ds ds;
         shmctl(shmid,IPC_STAT,&ds);
         printf("获得属性: size :%d,link :%d\n",ds.shm_segsz,ds.shm_nattch);
         sleep(1);
    }

    return 0;
}

我们确实获得了共享内核的一些属性。

我们的key在struct shmid_ds结构体的第一个变量里。

cpp 复制代码
#include"comm.hpp"

int main()
{
    key_t key=GetKey();
    printf("key->%d\n",key);
    int shmid=GetShm(key);
    printf("shimid->%d\n",shmid);

    char* start=(char*)attachShm(shmid);
    printf("attach success, address start: %p\n", start);

    while(true)
    {
         printf("service say : %s\n", start);
         struct shmid_ds ds;
         shmctl(shmid,IPC_STAT,&ds);
         printf("获得属性: size :%d,link :%d,key :%d\n",\
                ds.shm_segsz,ds.shm_nattch,ds.shm_perm.__key);
         sleep(1);
    }

    return 0;
}

共享内存大小一般建议4KB(4096)的整数倍

系统分配共享内存是以4KB为单倍的!------内存划分内存块的基本单位page

虽然申请大小是4097,但是内核给你的实际是40962,内核给你向上取整。
虽然内核给的是4096
2,但是注意内核给你的,和你能用的是两码事。

2.消息队列

在前面说过System V IPC--->聚焦在本地通信,目前已经陈旧了,共享内存还是值得我们好好学习一番,剩下的我们看一看原理和接口就可以了。

2.1原理

进程A,B可以相互为读写段,一端把数据放到队列里,一端去拿。

那如何保证两个进程不会拿到自己的,而去拿对方的呢。

这个队列内核数据结构中有一个type,用来标识,这个信息是谁发的,不是自己的不拿。

2.2接口

创建一个消息队列

看到没这些参数和共享内存的参数非常相似。

成功是返回消息队列标识符,失败返回-1。

向消息队列中放数据

msgp:发送的数据
msgsz:数据大小
msgflg:默认为0

接收数据


msgp:接收数据放到这里
msgsz:大小
msgtyp:类型

删除队列

这些接口都和共享内核相似。

3.信号量

关于信号量这里补充一些概念,也是为了后面的学习。

3.1信号量是什么

本质上是一个计数器,通常用来表示公共资源中,资源数量的多少问题。

信号量本质是一个计数器,那可以直接设置一个全局变量用来充当计数器吗?

不可以的,就如在匿名管道,设置一个全局变量,因为写时拷贝,父子进程看到的根本不是同一个全局变量。

进程通信之前要看到同一份资源,然后才能通信。
公共资源:被多个进程同时可以访问的资源。

以管道的方式通信的双方,注意到写端没写,读端一直在阻塞等待,读端没读,写端写完也在阻塞等待等等,而共享内存的方式呢,不管写端是否写完,读端一直在读。假设写一段数据,结果数据没写完就读。就造成了问题,这些数据就在共享内存中没有被保护。

访问没有被保护的公共资源导致数据不一致的问题。

我们来捋一捋这个过程。

为什么要让不同的进程看到同一份资源呢?

因为我想通信,让进程之间实现协同,但是进程具有独立性,因此需要先让进程看到同一份资源。 提出了方法,然后也引入了新的问题------>数据不一致问题。

我们未来将被保护起来的公共资源:临界资源

进程大部分的资源是独立的。

资源(内存,文件,网络)是要被使用的,如何被进程使用呢?
一定是该进程有对应的代码来访问这部分临界资源,这个代码临界区,其他代码叫非临界区。

那如何保护公共资源呢?
互斥和同步

互斥:当有两个进程想访问一个公共资源的时候,我们只能让一个进程进行完整的访问。

同步,在多线程哪里再说。

还有一组概念比较不好理解。

假如我想向缓冲区写一段数据,要求我必须要把这段数据写完你才能读,我没写完你就读不了。对于我来讲,我在写的时候,我要么不写,要写就写完才对你有意义。

这种要么不做,要做就做完,两态的这种情况:原子性。

假设张三要去银行给李四转账200

张三1000 李四1000

1000-200 1000+200

但是网络出现问题,转账失败,那银行不能就不管了,必须要把这200还给张三保持原样。

转账对于我们来说就只有两态,要么不转,要转就转完,虽然可能有中间过程,但是最终结果就是要么不转,要转就转完。不会说正在转账中。

这就是我们所有的原子性。

上面说这么多主要是为了说明,让多进程和多线程用来进行原子性的互斥和同步,信号量是其中一种解决方案。

3.2为什么要信号量

举个例子

去电影票看电影,我是不是只要坐到座位上,这个位置才是属于我的?

并不是,买票(票号,座位号)之后,这个位置在那个时间段就是属于我的。

看电影买票的本质:对放映厅中座位进行进行预定机制。

当我们想要某种资源的时候,我们可以进行预定

电影院就相当于共享资源。

每个进程想访问某些公共资源时,先申请信号量,申请成功就相当于预定了共享资源的某一部分资源,才能允许进入这个小资源里进行访问,申请失败,就不允许这个基础进入共享资源,进而达到保护共享资源以及其他基础的目的。

所有进程在访问公共资源之前,都必须先申请sem信号量---->必须申请sem信号量的前提,是所有进程必须先看到同一个信号量---->信号量本身就是公共资源 ---->

信号量是不是也要保护自己的安全呢?(++,- - 操作)---->信号量必须要保护这种操作的安全性,++,- -操作是原子的!!!

++,- -就是我们所说的PV操作

信号量本质是一把计数器,这个计数器可以在多进程环境中,可以被多进程先看到,必须给我们匹配上两种操作,P操作,V操作,让进程对我们的共享资源进行访问。

如果信号量初始值是1,代表共享资源当作整体来使用,一个进程申请成功了,其他进程不能申请------->互斥。

提供互斥功能的信号量---->二元信号量。

这里可能有这样一个问题,进程申请成功了信号量,是有资格去访问共享资源,但具体是去访问那个资源子部分不知道,可能存在多个进程访问同一个资源的问题,(也就是买票(票号有了,座位号还没有))该怎么办?

这部分其实是由我们在写代码时来确认不同进程去访问那个资源的。

3.3接口

获取信号量


nsems:想申请几个信号量

申请成功返回一个信号量集的标识符。失败返回-1。

删除信号量,或者获取信号量属性


semnum:可能你申请很多信号量,这个是信号量对应的下标,申请一个信号量,下标为0,申请10个信号量,假设我想对第10个信号量操作,下标就是9。

cmd:删除,或者获取信号量相关属性。

对信号量做PV操作

对指定信号量,做对应的操作

struct sembuf结构有三个变量

sem_num:对申请的多个信号量,哪一个进行操作。

sem_op:一般只有两种值,一种是1代表++,V操作。一种是-1代表- -,P操作。

sem_flg:默认为0

nspos:有几个这样的结构体。

允许对多个信号量同时进行PV操作。

4.IPC资源的组织方式

我们注意到共享内存,消息队列,和信号量的接口都非常相似。都有struct ...id_ds这样的结构体,还有对应描述IPC属性资源的第一个字段都是一样的。

这三种共性可以看出来一个细节,这三种都叫做System V标准的进程间通信

所谓的标准就是,大家用的方式,接口设计,数据结构的设置都必须遵守某种标准。

那OS如何对管理这些(比如一会申请共享内存,一会消息队列,信号量)的呢?

OS并不是对它们本身进行管理,而是先描述在组织,对匹配的相关资源的内核结构进行管理。

因为当前三种方式的属性的第一个字段都是一样的。

在内核中操作系统可以维护一个指针数组。

结构体第一个成员的地址,在数字上,和结构体对象本身的地址数字是相等的!!

OS有对应的方式知道强转成什么结构体指针。

这就是OS对IPC资源的组织方式。

相关推荐
二十七剑1 小时前
jvm中各个参数的理解
java·jvm
东阳马生架构2 小时前
JUC并发—9.并发安全集合四
java·juc并发·并发安全的集合
anddddoooo2 小时前
域内证书维权
服务器·网络·网络协议·安全·网络安全·https·ssl
zhoupenghui1683 小时前
golang时间相关函数总结
服务器·前端·golang·time
计算机小白一个3 小时前
蓝桥杯 Java B 组之岛屿数量、二叉树路径和(区分DFS与回溯)
java·数据结构·算法·蓝桥杯
lllsure3 小时前
Linux 实用指令
linux·物联网
菠菠萝宝3 小时前
【Java八股文】10-数据结构与算法面试篇
java·开发语言·面试·红黑树·跳表·排序·lru
不会Hello World的小苗3 小时前
Java——链表(LinkedList)
java·开发语言·链表
努力的小T3 小时前
使用 Docker 部署 Apache Spark 集群教程
linux·运维·服务器·docker·容器·spark·云计算
Nerd Nirvana3 小时前
OpenSSL crt & key (生成一套用于TLS双向认证的证书密钥)
linux·ssl·shell·认证·加密·tls·oepnssl