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


    共享内存

    共享内存是SystemV标准进程间通信的一种,该标准还有消息队列和信号量,但下文主要介绍共享内存,然后在谈一下信号量的内容。SystemV标准的进程间通信可以看做单机版的进程间通信。

    // 1. log.hpp
    #pragma once
    
    #include 
    
    enum ErrLevel
    {
        lev_0,
        lev_1,
        lev_2,
        lev_3,
        lev_4
    };
    
    const std::string error[] = {
        "err_0",
        "err_1",
        "err_2",
        "err_3",
        "err_4"
    };
    
    std::ostream& Log(const std::string& msg, int level)
    {
        std::cout << " | " << (unsigned int)time(0) << " | " << error[level] << " | " << msg << " |";
        return std::cout;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    // 2. comm.hpp
    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    #include "log.hpp"
    
    // key_t ftok(const char *pathname, int proj_id);
    #define PATH_NAME "/home/zs/linux/testcpp"
    #define PROJ_ID 0x20231118
    
    // 共享内存的大小,最好是页(PAGE:4096)的整数倍
    #define SHM_SIZE 4096
    
    #define MODE 0666
    #define FIFO_PATH "./fifo"
    
    class FIFO
    {
    public:
        FIFO()
        {
            if(0 != mkfifo(FIFO_PATH, MODE))
            {
                perror("mkfifo");
                exit(1);
            }
            Log("create fifo success", lev_1) << endl;
        }
    
        ~FIFO()
        {
            if(0 != unlink(FIFO_PATH))
            {
                perror("unlink");
                exit(2);
            }
            Log("unlink fifo success", lev_2) << endl;
        }
    };
    
    int openFIFO(const string& pathname, int flags)
    {
        int fd = open(pathname.c_str(), flags);
        if(fd == -1)
        {
            perror("open");
            exit(3);
        }
    
        return fd;
    }
    
    void Wait(int fd)
    {
        uint32_t i = 0;
        ssize_t size = read(fd, &i, sizeof(i));
        if(size == -1)
        {
            perror("Wait::read");
            exit(4);
        }
    }
    
    void Signal(int fd)
    {
        uint32_t i = 1;
        ssize_t size = write(fd, &i, sizeof(i));
        if(size == -1)
        {
            perror("Signal::write");
            exit(5);
        }
    }
    
    void closeFIFO(int fd)
    {
        close(fd);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    创建共享内存使用shmget函数。
    key:只有创建的时候用到key(key是调用ftok用算法形成的,标识了系统层上的唯一性);大部分情况下用户访问共享内存用的还是shmid(shmget的返回值,它的使用类似文件描述符fd,标识了用户层上的唯一性)。
    size:设置创建的共享内存的大小。
    shmflg

    • IPC_CREAT:单独使用,如果共享内存不存在,创建并返回;如果已经存在,获取并返回。
    • IPC_EXCL:单独使用,没有意义。
    • IPC_CREAT | IPC_EXCL:共同使用,如果共享内存不存在,创建并返回;如果已经存在,出错返回(如果返回成功,一定是一个全新的共享内存)。
      在这里插入图片描述
      创建共享内存之前key的生成由ftok接口完成。
      ftok可以确保接收到相同的pathnameproj_id会使用算法生成相同的key
      而不同进程通过拿到相同的key值来看到同一份资源。
      在这里插入图片描述
    // 3. server.hpp
    #include "comm.hpp"
    
    // 程序在加载时,自动构建全局变量,自动调用fifo的构造函数,创建管道文件
    // 程序退出时,全局变量会被析构,自动调用fifo的析构函数,删除管道文件
    FIFO fifo;
    
    void test()
    {
        // 通信的前置工作,让不同进程看到同一份资源(内存)
        // 1.创建公共的key值
        key_t key = ftok(PATH_NAME, PROJ_ID);
        if(key == -1)
        {
            perror("ftok");
            exit(1);
        }
        Log("server create key success", lev_1) << " server key: " << key << endl;
    
        // 2.创建共享内存 -- 建议创建一个全新的共享内存
        int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | MODE);
        if(shmid == -1)
        {
            perror("shmget");
            exit(2);
        }
        Log("server create shm success", lev_2) << " shmid: " << shmid << endl;
    
        // 3.将共享内存挂接到自己的地址空间
        char* shmaddr = (char*)shmat(shmid, nullptr, 0);
        if((void*)shmaddr == (void*)-1)
        {
            perror("shmaddr");
            exit(3);
        }
        Log("server attach shm success", lev_3) << " shmaddr: " << (void*)shmaddr << endl;
    
        // 4.通信 -- 将共享内存看做字符串存储空间
        // 关于共享内存的通信有两个结论:
        // a.通信双方一方可以直接向共享内存写数据,另一方可以立刻看到写的数据
        //   共享内存是所有进程间通信(IPC)速度最快的。因为它不需要过多的拷贝(表现就是不需要将数据给到操作系统)
        //   共享内存映射进进程地址空间的共享区(在用户空间内),所以不需要经过系统调用,可以直接访问,双方进程通信属于内存级的读和写。
        // b.共享内存缺乏访问控制,会引起并发问题
        // 此处采用管道的访问控制功能,来对共享内存进行一定的访问控制
        int fd = openFIFO(FIFO_PATH, O_RDONLY);
        while(true)
        {
            Log("server wait...", lev_2) << endl;
            Wait(fd);
    
            cout << "server output: " << shmaddr << endl;
            if(strcmp(shmaddr, "quit") == 0)
            {
                cout << "client quit, then server quit" << endl;
                break;
            }
            // sleep(1);
        }
        closeFIFO(fd);
        // sleep(10);
    
        // 5.将共享内存从地址空间中祛关联
        int dt = shmdt(shmaddr);
        if(dt == -1)
        {
            perror("shmdt");
            exit(4);
        }
        Log("server detach shm success", lev_4) << endl;
    
        // 6.删除共享内存 -- IPC_RMID: 即使当前还有进程和shm挂接,依旧会删除shm
        int ipcrm = shmctl(shmid, IPC_RMID, nullptr);
        if(ipcrm == -1)
        {
            perror("shmctl");
            exit(5);
        }
        Log("server rm shm success", lev_1) << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    共享内存的提供者是操作系统,当进程运行结束时,共享内存是还存在着的,其生命周期是随操作系统的。共享内存有两种删除方式,一是手动删除,二是利用代码调用接口删除。
    在这里插入图片描述
    可以调用shmctl接口进行删除。
    cmd:填写删除指令IPC_RMID
    buf:是用于描述共享内存结构的结构体指针,使用buf可以对共享内存的结构进行更改。
    这里可以引出对共享内存的重新理解:共享内存 = 共享内存块 + 对应的共享内存的内核数据结构。
    操作系统为了管理大量的共享内存,需要先描述再组织,进行管理。
    在这里插入图片描述
    buf所指向的共享内存的结构体。
    在这里插入图片描述
    共享进程创建好后,进程要访问共享进程还需要对其进行挂接(将共享内存映射进进程所在地址空间中)。
    shmaddr:除非自己特别清楚要将共享内存挂接在什么位置,否则nullptr让系统自己处理。
    在这里插入图片描述

    // 4. client.cpp
    #include "comm.hpp"
    
    void test()
    {
        // 1.获取key
        Log("client pid is:", lev_0) << " " << getpid() << endl;
        key_t key = ftok(PATH_NAME, PROJ_ID); // key_t -- int
        if(key == -1)
        {
            perror("ftok");
            exit(1);
        }
        Log("client create key success", lev_1) << " client key: " << key << endl;
    
        // 2.获取共享内存
        int shmid = shmget(key, SHM_SIZE, IPC_CREAT);
        if(shmid == -1)
        {
            perror("shmget");
            exit(2);
        }
        Log("client get shm success", lev_2) << " shmid: " << shmid << endl;
    
        // 3.将共享内存挂接到自己的地址空间
        char* shmaddr = (char*)shmat(shmid, nullptr, 0);
        if((void*)shmaddr == (void*)-1)
        {
            perror("shmaddr");
            exit(3);
        }
        Log("client attach shm success", lev_3) << " shmaddr: " << (void*)shmaddr << endl;
    
        // 4.通信
        int fd = openFIFO(FIFO_PATH, O_WRONLY);
        while(true)
        {
            write(1, "client input: ", strlen("client input: "));
            ssize_t size = read(0, shmaddr, SHM_SIZE);
            if(size == -1)
            {
                perror("read");
                exit(5);
            }
    
            shmaddr[size - 1] = '\0';
            Signal(fd);
            if(strcmp(shmaddr, "quit") == 0) break;
        }
        closeFIFO(fd);
    
        // 5.祛关联
        int dt = shmdt(shmaddr);
        if(dt == -1)
        {
            perror("shmdt");
            exit(4);
        }
        Log("client detach shm success", lev_4) << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    在这里插入图片描述

    信号量

    这里不谈消息队列的问题,但消息队列的接口使用和共享内存都是相似的。
    在这里插入图片描述
    临界资源:多个进程(执行流)看到的一份公共的资源。
    临界区:进程里,访问临界资源的代码部分。
    由临界资源和临界区的概念可以知道,多个进程(执行流),同时运行时会互相干扰,主要是不加保护地访问了同一份资源(临界资源)。而再分临界区,多个进程(执行流)是互不影响的。
    互斥:为了更好地进行临界资源的保护,可以让多个进程(执行流)在任何时刻,只能有一个进入临界区。
    原子性:要么不做,要么做完,没有中间状态。

    信号量的出现让任何一个进程要访问临界资源时,不能直接访问,而是要先申请信号量(这里可以看出信号量也属于临界资源,可以被多个进程看到)。
    信号量本质是一个计数器,申请信号量的本质就是让计数器减一。
    只要申请信号量成功,在临界资源内部一定会预留有该进程要访问的资源。
    而申请信号量的本质,是对临界资源的一种预定机制。
    申请信号量(P操作)和释放信号量(V操作)必须是原子的。
    在这里插入图片描述

  • 相关阅读:
    基于simulink的Passive anti-islanding-UVP/OVP and UFP/OFP被动反孤岛模型仿真
    uniapp --- 实现图片压缩(兼容H5)
    2022年最新《谷粒学院开发教程》:12 - 项目完结篇
    推荐系统专题 | CTR预测跨域处理的解决方案
    计算机算法分析与设计(12)---贪心算法(最优装载问题和哈夫曼编码问题)
    QT 消息对话框
    numpy的部分通用函数浅谈
    23款奔驰GLC260L升级小柏林音响 全新15个扬声器
    Cilium系列-15-7层网络CiliumNetworkPolicy简介
    16.0、Java多线程——线程同步机制
  • 原文地址:https://blog.csdn.net/weixin_62172209/article/details/134477527