共享内存、消息队列和信号量

目录

1.共享内存直接原理

 1.创建/获取共享内存

2.共享内存的挂接与去挂接

3.删除共享内存

4.共享内存的特点

5.共享内存的属性

 6.编码实现

2.消息队列直接原理 

1.与共享内存接口的对比

2.ipc在内核中数据结构的设计 

3.信号量基本原理

1.补充知识

2.理解信号量

3.总结


无论如何,进程间通信的本质是:先让不同的进程,看到同一份资源

1.共享内存直接原理

1.先申请内存

2.再将内存挂接到进程地址空间

如果要释放内存则正好相反。

对于共享内存我们也需要管理,因此也需要一个结构体,先描述再组织。

申请和挂接的操作都是操作系统来执行的,如果是进程执行,那么由于进程独立性的需求,这块空间就应该是你一个人的。 

 1.创建/获取共享内存

可是,我们怎么保证 让不同的进程看到同一个共享内存呢?

你怎么知道这个共享内存存在还是不存在呢?

key!!来标识

接下来我们来谈谈key

1.key是一个数字,这个数字是几,不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识

2.我们第一个进程可以通过key创建共享内存,对于第二个以及之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了。

3.对于一个已经创建好的共享内存。key在哪呢?key在描述这个共享内存的对象中,其中包含着该共享内存的各属性

4.我们创建共享内存的时候必须先获取一个key。(怎么获取?也是通过接口获取)

5.key本质上类似于路径,都是用于标识唯一性

                我们通过这样一个接口来创建一个key。这个接口并不会去内核里查找哪些key没被占用帮我们申请之类的,它只是一套算法,它根据特定算法通过pathname和proj_id来计算出一个key值,即相同的pathname和proj_id得出的是同一个key,而这两个参数都是由我们自由指定的。

        由于上述原因,key值可能会出现冲突,因此共享内存的创建也可能因此失败。那么我们为什么要用户来获取key值呢?由系统获取不是更好吗?

         由系统获取自然可以保持高度的差异性,但是哪些进程要通信是需要用户指定的,操作系统它不知道哪些进程需要通信。况且如果想要把系统获取的这个key值传给另一个进程那就需要进程间通信,而我们现在正在解决的也正是进程间通信问题,会产生冲突。(我们的确可以使用管道来让这些进程进行通信,但是这样共享内存就不是一个独立的进程间通信方案了)

key与shmid

key用来在操作系统中标识共享内存的唯一性,只在申请或者获取内存的时候使用

shmid用来在用户层面标识共享内存的唯一性,我们在命令行的删除共享内存等操作就是用shmid 

(小知识:共享内存的生命周期是随内核的,用户不主动关闭,共享内存就会一直存在,除非内核重启或者用户释放,其实和malloc有些相似) 

ipcs  -m查询共享内存

对于这里的perms(权限),我们可以在创建共享内存的时候一起设定 

删除则是ipcrm -m (+) 

再谈共享内存的大小

        我们在获取共享内存的时候设置是多少它就是多少。但是我们最好设置成4096(字节)的整数倍,因为系统获取内存是以4kb为单位的,这里系统实际上申请了4096* 2的大小,但是因为我们需要的是4097大小,所以分配给我们的就是4097了

2.共享内存的挂接与去挂接

第一个参数就是我们之前获取到的,在用户层标识共享内存唯一性的shmid

第二个参数是共享内存挂接的位置,我们可以手动设置,也可以设置成nullptr,让系统自行决定,挂接的位置确定后会通过返回值返回(可以联想到malloc的返回值,对于malloc,其实它申请空间是申请虚拟地址,如果后面发现物理内存并没有被建立,那么就会缺页中断,然后才申请)

第三个参数是关于权限的设置,我们可以手动设置,让666权限的共享内存变成只读或只写等权限,也可以设置成0,挂接的时候使用共享内存创建时的权限。

        去挂接更为简单,传入的参数就是我们挂接时候的返回值。 这里和与malloc对应的free很像,free也是只需要传入一个起始地址就可以了。我们要去关联,必须知道起始地址和共享内存的大小,这里不需要我们传入大小那么就说明,一定有用户层看不见的地方储存了大小并且让shmdt去关联的时候能看见,由于共享内存有专门管理的结构,我们能够想到共享内存的大小一定也会被存储。而malloc则是会多申请一些空间来记录用以维护这块空间,里面包括了空间的大小

3.删除共享内存

第一个参数不必多说

第三个参数就是管理共享内存的结构体,我们可以通过这个接口获取,修改结构体内的内容 

第二个参数是一个选项,可以由我们选择获取或者修改结构体内容等

在删除的时候我们并不关心这个结构体,所以第三个参数可以设置为nullptr。 

4.共享内存的特点

 1.共享内存没有同步互斥之类的保护机制

(因此我们可以借用管道的同步功能加以限制,来保证共享内存也能够进行同步)

2.共享内存是所有进程间通信中速度最快的  为什么?因为它拷贝少。对于已经挂接上的进程,这块共享内存就是它的一块空间,和自己malloc出来以后使用的空间没有任何区别。我们可以对比以下管道,管道需要我们标准输入把内容输到管道里,然后write到内核缓冲区,接着再调用read,最后显示到标准输出。write和read的过程都需要分别进行一次拷贝。

3.共享内存内部的数据,由用户自己来维护。

5.共享内存的属性

 6.编码实现

comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include<iostream>
#include<string>
#include<cstdlib>
#include<string.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include"log.hpp"


using namespace std;
const int size = 4096;
const string pathname="/home/myh";
const int proj_id = 0x666;
Log log;

key_t GetKey()
{
    key_t k = ftok(pathname.c_str(),proj_id);
    if(k < 0)
    {
        log(Fatal,"ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info,"ftok success, key is : 0x%x", k);
    return k;
}

int GetShareMemHelper(int flag)
{
    key_t k = GetKey();
    int shmid = shmget(k , size ,flag);
    if(shmid < 0)
    {
        log(Fatal,"create share memory error: %s",strerror(errno));
        exit(2); 
    }
    log(Info,"create share memory success, shmid: %d",shmid);

    return shmid;
}

int CreateShm()
{
    return  GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm()
{
    return  GetShareMemHelper(IPC_CREAT);
}
#endif

 log.hpp(插件,非核心代码)

#pragma once

#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.h>
#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

class Log
{
public:

    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch(level)
        {
            case Info: return "Info";
            case Debug: return "Debug";
            case Warning: return "Warning";
            case Error :return "Error";
            case Fatal :return "Fatal";
            default: return "None";
        }
    }
    // void logmessage(int level,const char *format, ...)
    // {
    //     time_t t = time(nullptr);//时间戳
    //     struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    //         ctime->tm_year+1900, ctime->tm_mon+1,ctime->tm_mday,
    //         ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//把字符串存到leftbuffer里面

    //     va_list s;
    //     va_start(s, format);
    //     char rightbuffer[SIZE];
    //     vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
        

    //     //格式 默认部分(左)+自定义部分(右)
    //     char logtxt[SIZE*2];
    //     snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);

    //     printLog(level, logtxt);//暂时打印
    // }
    void printLog(int level, std::string logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std:: endl;
            break;
        case Onefile:
            printOneFile("LogFile" ,logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
        default:
            break;
        }
    }
    void printOneFile(const std:: string logname, const std::string logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY|O_CREAT|O_APPEND, 0666);//LogFile
        if(fd < 0)return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string logtxt)
    {
        std::string filename = "LogFile";
        filename += ".";
        filename += levelToString(level);//LogFile.Debug/Warning/Fatal
        printOneFile(filename, logtxt);
    }
    ~Log()//这里析构只是为了让类看起来完整
    {

    }
    void operator()(int level,const char *format, ...)
    {
        time_t t = time(nullptr);//时间戳
        struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
            ctime->tm_year+1900, ctime->tm_mon+1,ctime->tm_mday,
            ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//把字符串存到leftbuffer里面

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
        

        //格式 默认部分(左)+自定义部分(右)
        char logtxt[SIZE*2];
        snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);

        printLog(level, logtxt);
    }
private:
    int printMethod;
    std :: string path;
};

 makefile

.PHONY:all
all:processa processb

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

.PHONY:clean
clean: 
	rm -f processa processb

processa.cc

#include"comm.hpp"

extern Log log;

int main()
{
 
    int shmid = CreateShm();
    log(Debug, "create shm done");

    sleep(5);

    char * shmaddr = (char*)shmat(shmid ,nullptr ,0);
    log(Debug, "attach shm done");
    sleep(5);

    //ipc code
    while(true)
    {
        //一旦有人把数据写到共享内存,其实我们立马就能看到了
        //不需要经过系统调用,直接就能看到数据了
        cout <<" client say@ " << shmaddr << endl;//直接访问共享内存
        sleep(1);
    }




    shmdt(shmaddr);
    log(Debug,"detach shm done, shmaddr: 0x%x",shmaddr);
    sleep(5);

    shmctl(shmid, IPC_RMID, nullptr);
    log(Debug, "destroy shm done, shmaddr:0x%x", shmaddr);
    sleep(5);

    return 0;
}

 processb.cc

#include"comm.hpp"

extern Log log;

int main()
{

    int shmid = GetShm();
    log(Debug, "create shm done");

    sleep(5);

    char * shmaddr = (char*)shmat(shmid ,nullptr ,0);
    log(Debug, "attach shm done");
    sleep(5);


    //ipc code
    while(true)
    {
        //char buffer[1024];我们可以再弄一个缓冲区,但其实这是多此一举,我们可以直接用这块共享内存
        //cout << "Please Enter@ ";
        //fgets(buffer, sizeof(buffer), stdin);
        //memcpy(shmaddr, buffer, strlen(buffer) + 1);//fgets会把获取的字符串后面加个\0,+1是为因为我们这里把消息看成字符串格式,特意加的。
        cout << "Please Enter@ ";
        fgets(shmaddr,4096, stdin);//直接用这块内存

        //一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可
        //不需要调用系统调用
    }


    shmdt(shmaddr);
    log(Debug,"detach shm done, shmaddr: 0x%x",shmaddr);
    sleep(5);


    return 0;
}

2.消息队列直接原理 

1.与共享内存接口的对比

(两种通信方式都为SystemV标准)

共享内存接口

消息队列接口

 

        我们看到,消息队列和共享内存中很大一部分接口都是类似的,下面这个是消息队列特有的收发信息的接口

        发消息,第一个参数就是要指定发给谁,第二个参数是你要发送的数据块的起始地址,第三个是数据块的大小,最后一个参数我们和共享内存中说的一样,可以设成nullptr

        收消息多了的部分是数据块的类型。

        我们收发数据时,要自行定义相应的数据块,

同样,我们的指令能查共享内存,也同样能查消息队列

ipcs -q 查

ipcm - q msgid 删除

实际上信号量的管理对象结构也类似

2.ipc在内核中数据结构的设计 

在操作系统中,所有 ipc资源,全部都被整合在ipc模块中

系统中有一个数组将这些结构管理起来,这个数组的下标就是我们前面所说的shmid  xxxid! 

(这个数组不隶属于进程,是操作系统层面上维护的一个数组)(数组下标线性增加)

        共享内存 消息队列 信号量他们的管理结构的第一项都是同样的数据结构,因此这个数据结构的地址可以统一地存储到数组中 ,而这个数据结构的起始地址也正好是整个管理结构的起始地址,因此我们只需要进行强转即访问这个管理结构中的其它部分。关于操作系统如何辨别并根据类别将数据结构正确地强转,操作系统它自己是可以辨别的,例如添加一个标志位等等。

        这一管理形态我们可以看成是c语言中实现的多态。

3.信号量基本原理

1.补充知识

        如下有一个场景,当我们的a 正在写入,写入了一部分,就被b拿走了导致双方发和收的数据不完整,这就是数据不一致问题。(管道有原子性以及自己的互斥机制,因此不会出现数据不一致问题)

1.a,b看到的同一份资源,这份资源叫做共享资源,但是如果不加以保护,会导致数据不一致问题

2.我们可以通过加锁来实现互斥访问(任何时刻,只允许一个执行流访问共享资源,这就是互斥)

3.共享的,任何时刻只允许ig执行流访问的资源被称为临界资源----一般是内存空间(管道就是临界资源,管道的缓冲区,不就是一段内存空间吗)。

4.举例:我们有100行代码,但是只有5-10行代码才是在在访问临界资源的代码,这样的代码我们称之为临界区

解释一个现象:多进程,多线程,并发打印

显示器上的消息:1.错乱的2.混乱的3.和命令行混在一起

为什么会这样呢?  显示器它实际上也是文件,我们向显示器写入,实际上先要向显示器的缓冲区写入数据,然后操作系统才把我们的数据刷新到显示器上。当我们有多个进程尝试着对一个显示器进行打印时,前提是他们都要看到这个显示器资源,那么显示器资源就是共享资源了,当各个进程进行打印时,你打印你的我打印我的,彼此之间并没有互斥或者同步保护机制,所以会产生数据不一致问题

2.理解信号量

        信号量的本质就是一个计数器,类似与int count = n。

        它被用于描述临界资源中资源数量的多少。

        下面有一个故事:

        现在有一个电影院,里面有100个座位,因此放出了100张票

        当我们想去看电影,我们需要先买票--------买票的本质就是对资源的预定机制

        此时我们需要一个票数计数器,每卖出一张票,这个计数器就要减1------即放映厅里面的资源就减1

        票数的计数器减到0之后,就说明票已经被申请完毕了

        现在我们有若干临界资源

        假如里面有15份资源,那我们int count = 15

        我们想要去访问资源,就要先申请,每次成功申请资源count 就会减1,count等于0时,就说明资源已经被申请完了。

        这个临界资源计数器就被我们称为信号量

        1.申请计数器成功,就表示我们有访问这一资源的权限了

        2.我们申请了资源,但是并不代表我们访问了资源,通过计数器申请资源,是对资源的预定机制

        3.计数器可以有效保证进入共享资源的执行流数量(我们计数器初始为15时,就绝对不可能让十六个执行流进入)

        4.所以每一个执行流,它想要访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。如同要看电影先买票!

        我们再回归到上面提及的例子,如果电影院放映厅里只有一个座位呢?那我们就只需要一个值为1的计数器

        也就是说只有一个人能抢到,只有一个人能进放映厅看电影,可以理解为这期间只有一个执行流在访问临界资源。这就是互斥!

        我们把值只能为1,0两态的计数器叫做二元信号量-----本质就是一个锁

        凭什么让计数器为1呢? 因为资源为1! 其实就是将临界资源看成一个整体,不把它分成多份。整体申请整体释放

        思考:我们要访问临界资源,先要申请信号量计数器资源。要申请必须要先看到它,那么信号量计数器,不也是共享资源吗? 

        信号量计数器要保护临界资源的访问安全,必须先保证自己的安全。

        这个整数减1是安全的吗?

        是不安全的,我们这里count--,在c语言上是一条语句,变成汇编就成了多条汇编语句

        1.cnt变量的内容,内存->cpu寄存器

        2.cpu内进行操作

        3.将计算结果写回cnt变量的内存位置

        可是我们知道,进程在运行的时候是可以随时被切换的。这个过程就可能出现问题,这里的问题后续再谈

        信号量

        申请信号量,本质是对计数器进行--。即P操作

        释放资源,释放信号量,本质是对计数器进行++操作,即V操作

        申请和释放操作(PV操作)原子的!----要么不做要么做完,是两态的。没有“正在做”这样的概念

3.总结

1.信号量本质上一把计数器,PV操作是原子的

2.执行流申请资源,必须先申请信号量资源,得到信号量之后才能访问临界资源

3.信号量是0.1两态的被称为二元信号量,就是互斥功能

4.申请信号量的本质就是对临界资源的预定机制

信号量凭什么是进程间通信中的一种?

1.因为通信不仅仅是通信数据,互相协同也是通信的一种

2.要协同,本质也是通信,信号量首先要被所有的通信进程看见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值