【Linux】进程间通信 - 匿名/命名管道与System V共享内存

news2024/9/23 11:20:34

目录

前言

一.管道

0.什么是管道

1).管道的概念

2).管道的本质

3).管道指令: "|"

1.匿名管道

1).如何创建匿名管道

2).如何使用匿名管道进行通信

3).匿名管道的特点总结 

2.命名管道

0).指令级的命名管道的通信

1).如何在编程时创建命名管道

2).如何在编程中使用命名管道在进程A与B之间相互通信

3.总结(匿名管道vs命名管道)

1.管道的共性:

2).管道的异性:

二.基于System V标准的共享内存

1.System V标准共享内存的通信原理

2.基于System V标准的共享内存通信的性质

3.System V标准共享内存的系统调用接口

1).常用接口

2).key VS shmid

4.代码实现System V共享内存进程通信

1).纯共享内存版本(没有访问控制)

2).手动添加访问控制版本

5.通过指令操作ipc资源

6.补充: 关于共享内存的删除

7.补充: 关于共享内存与管道的读取的一些区别


前言

为什么需要进程间通信

为了进程之间能够互相协同工作, 从而更高效的完成任务, 就需要进程间通信这样的技术

进程间通信的技术背景

进程是具有独立性的, 其独立性体现在: 每个进程都有属于自己的虚拟地址空间并且通过页表与物理内存映射起来, 所以进程与进程之间是互不干扰的

进程间通信的成本高, 因为想要通信就要打破这种独立性

进程间通信的本质

进程间通信的本质实际上就是让不同的进程可以看到同一块内存

几种进程间通信的技术

1.Linux原生支持 - 管道文件 - 适用于不同进程的单机通信

匿名管道

命名管道

2.System V - 适用于不同进程的单机通信

共享内存

消息队列

信号量

3.Posix - 适用于多线程/多进程间的网络通信

一.管道

0.什么是管道

1).管道的概念

管道有出口, 有入口, 管道中传输的都是数据

管道是单向通信, 是一种特殊的半双工通信方式

半双工: 一个进程不是在读, 就是在写, 不可能同时读写

例如: 老师给学生讲课(就是一种半双工), 老师在讲时,  学生只可以听, 学生在提问时, 老师也只可以听

对于匿名管道而言, 一个管道, 有一个出口, 有一个入口

对于命名管道而言, 一个管道, 可以有多个出口, 可以有多个入口

2).管道的本质

管道本质上就是一个文件, 这个管道文件属于"内存文件", 其实就是内核中的一块缓冲区

"内存文件": 管道文件与普通文件不同的是, 管道文件属于"内存文件", 不向硬盘中刷新, 其实就是一个特殊的struct file内核数据结构, 数据的传输在内核级缓冲区中直接完成, 从而大大提高通信效率

结论:

Linux为何天然就支持单机的进程通信呢? 本质就是Linux的一切皆文件理念, 从而拥有了一种特殊的"内存文件"管道文件, 通过文件进行进程间的通信以及数据的传输

3).管道指令: "|"

每个指令实际上都是一个进程, 进程A | 进程B, 本质上|就是管道, 进程A处理后的数据经过管道交给进程B继续处理, 这是一个单向的过程, 进程A与进程B进行了通信

1.匿名管道

1).如何创建匿名管道

创建匿名管道的系统调用: int pipe(int pipefd[2])

参数:

int pipefd[2], 这是一个输出型参数, 传入一个pipefd[2]整数数组

pipefd[0]代表读端(以读方式打开匿名管道并且存入对应的fd)

pipefd[1]代表写端(以写方式打开匿名管道并且存入对应的fd)

返回值:

如果创建成功返回0, 创建失败返回-1, 并且错误码被设置

官方文档:

图解说明:

2).如何使用匿名管道进行通信

通过进程A创建出匿名管道

进程A调用fork创建子进程B

各自关闭一个读端一个写端 ---> 从而达到A - B之间的进程间通信

图解说明:

代码演示:

#include "log.hpp"

// 让父进程写, 子进程读

int main()
{
    // 1.创建匿名管道
    int pipefd[2] = {0};
    int ret = pipe(pipefd);
    assert(ret != -1);
    (void)ret;
    Log("创建匿名管道成功", Debug);

    // 2.fork子进程
    int id = fork();
    if (id == 0)
    {
        Log("创建子进程成功", Debug);
        // 子进程的逻辑 - 关闭子进程的写端
        close(pipefd[1]);

        while (true)
        {
            // 接收匿名管道中的数据
            char buffer[SIZE];
            memset(buffer, 0, SIZE);
            ssize_t n = read(pipefd[0], buffer, SIZE - 1);
            if (n > 0)
            {
                printf("子进程已接收数据: %s\n", buffer);
                if (strcmp("quit", buffer) == 0)
                {
                    break;
                }
            }
            else if (n == 0)
            {
                break;
            }
        }

        // 结束
        close(pipefd[0]);
        exit(25); // 结束子进程
    }
    else if (id > 0)
    {
        // 父进程的逻辑 - 关闭父进程的读端
        close(pipefd[0]);
        while (true)
        {
            // 向匿名管道中写入数据
            string msgBuffer; // 存放写入数据
            getline(cin, msgBuffer);
            write(pipefd[1], msgBuffer.c_str(), msgBuffer.size());
            if (strcmp("quit", msgBuffer.c_str()) == 0)
            {
                break;
            }
        }

        // 等待并回收子进程
        close(pipefd[1]);
        int status = 0;
        int ret = wait(&status);
        if (ret > 0)
        {
            cout << "-------------------------------\n";
            cout << "我是父进程, 我已经成功回收子进程\n";
            cout << "子进程pid: " << ret << endl;
            cout << "退出码(status次低八位): " << ((status >> 8) & 0xFF) << endl;
            cout << "退出信号(status低七位): " << (status & 0x7F) << endl;
            cout << "-------------------------------\n";
        }
        else if (ret == -1)
        {
            cout << "我是父进程, 我回收子进程失败\n";
        }
    }
    else
    {
        // 子进程fork失败
        perror("fork");
        exit(-1);
    }
    return 0;
}

3).匿名管道的特点总结 

1.只有拥有血缘关系的进程间通信才可以使用匿名管道(常用于父子通信)

2.匿名管道具有访问控制, 这是linux在设计匿名管道的时候就已经设计好的

访问控制:

如果写快, 读慢, 当匿名管道被写满就不能再写了, 只能等读走一部分再继续写

如果写慢, 读快, 管道没有数据的时候, 读进程必须等待

如果写关闭, 则读进程读到0个字节, 表示读到了文件结尾

如果读关闭, 则系统自动关闭写进程

3.管道提供的是面向流式的通信服务(面向字节流), 如果想要控制读取内容则需要手动添加"协议"

4.管道属于文件, 文件描述符的生命周期是伴随进程的, 进程结束就随之释放, 管道做为特殊的文件 - 管道文件, 生命周期也是伴随进程的

当读端关闭了, 写端进程被OS发送信号终止, 管道自然也就被释放

当写端关闭了, 读端进程读到文件结束标志, 读取了0个字节, 手动的也就将读端关闭, 管道自然也就被释放

再或者手动的关闭了两端读写, 管道文件自然也就被释放 -- 这就是管道的生命周期是伴随进程的

5.管道是单项通信的, 因为管道被创建好之后只有一端读, 一端写, 所以是单向通信, 这是一种半双工通信的特殊情况

2.命名管道

命名管道的使用方式就和普通文件一样, 底层实际上与匿名管道相同, 也是一块内核级缓冲区, 与匿名管道不同的就是命名管道被标识了, 有对应的inode, 但该inode仅仅只作为标识, 只是为了让其他进程可以找到该命名管道文件, 仅此而已

0).指令级的命名管道的通信

使用指令创建命名管道: mkfifo [管道名]

指令级的进程通信

如果echo向name_pipe命名管道做输出重定向, 如果此时没有输入重定向, 也就是没有进程读取, 那么向管道输出的进程就会一直阻塞直到有进程读取这块命名管道, 当另一个进程cat从named_pipe做输入重定向也就是读取的时候, 管道中的数据就会被读走, 这也就是一个进程向另一个进程通过命名管道通信的过程

1).如何在编程时创建命名管道

系统调用: int mkfifo(const char* pathname, mode_t mode)

参数:

第一个参数const char* pathname: 所创建的命名管道的路径及名称

(注: 最好填写绝对路径, 因为相对路径会根据运行进程路径的不同而改变, 这样如果两个通信进程的运行时路径不统一那么在通信时, 这个pathname也就不确定了)

第二个参数mode_t mode: 创建出来的命名管道的权限

返回值:

如果创建命名管道成功返回0, 创建失败返回-1, 并且errno被设置

2).如何在编程中使用命名管道在进程A与B之间相互通信

本次让server端读, client端写

log.hpp(包含一些头文件/宏定义/日志信息输出函数)

#ifndef LOG_H
#define LOG_H

#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<unistd.h>
#include<assert.h>
#include<cstring>

using namespace std;

#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3

string pathName = "/home/zsl/2022_12_29/named_pipe/namedPipe";

#define SIZE 1024

string flags[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"
};

ostream& Log(const string& msg, int flag)
{
    cout << msg << ", " << flags[flag] << endl;
    return cout;
}

#endif

server端代码

#include "log.hpp"

// 让server读, client写

int main()
{
    // 1.创建命名管道
    int ret = mkfifo(pathName.c_str(), 0666);
    assert(ret != -1);
    (void)ret;
    Log("创建命名管道成功", Debug);
    // 2.server以读的方式打开命名管道
    int fd = open(pathName.c_str(), O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(-1);
    }
    Log("Server已经以读形式打开管道文件", Debug);
    // 3.进行通信
    Log("开始进行通信", Debug);
    char buffer[SIZE];
    memset(buffer, 0, SIZE);
    while (true)
    {
        ssize_t n = read(fd, buffer, SIZE - 1);
        if (n > 0)
        {
            if (strcmp("quit", buffer) == 0)
            {
                break;
            }
            printf("我是server端, 我已经接收到client端发送的数据: %s\n", buffer);
        }
        else if(n == 0)
        {
            printf("client(写端)已退出, 我server端(读端)也退出\n");
            break;
        }
    }
    // 4.释放读端
    close(fd);
    Log("读端已释放", Debug);
    // 5.删除管道文件
    unlink(pathName.c_str());
    Log("管道文件已删除", Debug);
    return 0;
}

client端代码

#include"log.hpp"

int main()
{
    //server端已经创建好命名管道
    //做为client端直接使用即可: client做为写端, 以写的形式打开管道

    //1.以写形式打开命名管道
    int fd = open(pathName.c_str(), O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(-2);
    }
    //2.与server进行通信
    string buffer;
    while(true)
    {
        cout << "我是client端, 请输入要传输的数据:";
        getline(cin , buffer);
        if(buffer == "quit")
        {
            break;
        }
        write(fd, buffer.c_str(), buffer.size());
    }

    //3.关闭写端
    close(fd);

    return 0;
}

刚开始执行server端(读端)时, 由于写端还没有开始执行所以会在连接管道时阻塞

当启动client(写端时), 读端继续执行, 开始进行进程通信 

进程通信的过程 -- 输入quit就退出通信了

3.总结(匿名管道vs命名管道)

1.管道的共性:

1.有访问控制

对于管道而言:

        如果写快, 读慢, 当管道被写满就不能再写了, 只能等读走一部分再继续写

        如果写慢, 读快, 管道没有数据的时候, 读进程必须等待

        如果写关闭, 则读进程读到0个字节, 表示读到了文件结尾

        如果读关闭, 则系统自动关闭写进程

单独的, 对于命名管道多增加出来的访问控制:

        如果命名管道被创建出来, 并且读端被执行, 写端还未被执行, 读端会阻塞在打开明明管道文件这一步, 直到写端被执行

2.单向通信

特殊的半双工通信方式

3.本质上都是一块内核级缓冲区

进程之间通过管道文件进行通信, 但是不会向磁盘刷新从而大大提高通信效率

4.面向字节流通信

读写时如果没有人为干预, 是以字节流的方式进行的, 也就是直接读写"范围内的一堆数据", 这个"一堆"指的是一堆字节, 没有格式上的控制

5.管道的生命周期伴随进程

管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信,  直至最后通信间的进程也结束之后, 管道才会真正释放

6.读写时的原子性问题(补充内容)

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

2).管道的异性:

对于匿名管道:

        1.用于有血缘关系的进程之间进行通信, 多用于父子进程之间通信

        2.匿名管道只是操作系统给开辟的一块内核级缓冲区, 没有标识, 通过系统调用pipe, 输出型参数int pipefd[2]中的两个整数, 来控制读写端

对于命名管道:

        1.不需要血缘关系, 只要能找到命名管道的两个文件, 都可以进行通信

        2.命名管道也是操作系统给开辟的一块内核级缓冲区, 拥有管道名称, 有对应的inode编号, 但仅仅是用来让不同的进程通过该管道名找到管道

        从而进行通信, 仅此而已, 没有实际的管道文件内容

二.基于System V标准的共享内存

1.System V标准共享内存的通信原理

想让不同进程间达到通信的目的, 就必须要让这不同的进程看到同一块"空间", 即内存

原理:

由操作系统给开辟的一块专门用来进行通信的物理内存空间, 这块空间不属于任何进程, 然后将这块空间通过页表映射挂接到进程的虚拟地址空间的共享区, 通过获得挂接的起始地址就可以操作整个开辟的共享内存, 即可进行通信

操作系统创建好共享内存之后, 需要对其进行管理, 需要先描述, 再组织

故共享内存 = 物理上的共享内存块 + 对应的共享内存的内核数据结构

所以, 创建好共享内存之后都会返回一个整数, 这个整数类似于文件描述符的fd, 即共享内存描述符shmid

操作:

              A进程                                                                B进程

                              1.通过ftok获取一个相同的key值        

2.通过key值创建共享内存                        2.通过key值获取A创建好的共享内存

                              3.将共享内存挂接到进程

                              4.进行进程间通信

                              5.去掉进程与共享内存的挂机

6.删除共享内存

图示:

2.基于System V标准的共享内存通信的性质

1.没有访问控制

具体表现为:

如果此时共享内存为空, 读端仍一直读取, 只不过每次读到的内容为空

2.是最快的进程间通信的形式

一但由OS开辟出内存并且映射挂接到进程中, 这些进程间数据传输不再涉及到内核, 也就是不在通过执行内核的系统调用来完成传输, 本质上是通过减少了传输数据时不必要的拷贝, 直接由 进程A ---> 进程B

3.System V标准共享内存的系统调用接口

1).常用接口

1.shmget创建共享内存

包含头文件: #include<sys/ipc.h>

                    #include<sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

参数:

key: 使用ftok创建的key值

size: 指定创建的共享内存的大小, 一般为4096的倍数, 因为页表1页是4096字节, 如果开4097, 系统会预留两页, 实际使用只有4097, 则大大浪费空间

shmflg:

常用基础选项1.IPC_CREAT

常用基础选项2.IPC_EXCL

并且要带上权限, 例如0666, 与创建文件同理

单独使用IPC_CREAT, 如果创建的共享内存底层已经存在则获取, 如果不存在则创建

单独使用IPC_EXCL, 没有任何意义

IPC_EXCL必须与IPC_CREAT共同使用, 如果创建的共享内存底层不存在, 则创建, 底层存在则报错

此参数的传入方式通过或运算传入n个标记位

例如: IPC_CREAT | IPC_EXCL | 0666

注:一般如果是非创建共享内存的一端在获取共享内存时, shmflg传入0即可

返回值:

一个整数, 类似于文件描述符的fd, 用来操作该块共享内存, 如果失败返回-1

2.ftok获取创建共享内存的key值(钥匙)

包含头文件:#include<sys/types.h>

                   #include<sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

参数:

pathname: 路径

proj_id: 整数

返回值:

成功返回key值, 失败返回-1, 并且设置错误码errno

3.挂接共享内存至进程

包含头文件:#include <sys/types.h>
                   #include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:

shmid: 通过shmget创建的共享内存的返回值

shmaddr: 指定挂接地址, 通常传入nullptr即可, 让OS自动选择挂接地址

shmflg: 通常为0即可

返回值:

返回一个void*指针, 这里类似于malloc, 返回值需要根据使用来强制类型转换, 返回的地址是共享内存的起始地址, 如果失败则返回(void*)-1, 并且错误码errno被设置

4.去挂接

包含头文件:#include <sys/types.h>
                   #include <sys/shm.h>

int shmdt(const void *shmaddr);

参数:

shmaddr: 传入shmat挂接时返回的地址

返回值:

成功返回0, 失败返回-1, 并且设置错误码errno

5.删除共享内存

包含头文件:#include <sys/ipc.h>
                   #include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:

shmid: 共享内存块描述符

cmd: 标记位, 传入IPC_RMID宏即为删除

buf: 一般传入nullptr

返回值:

有多种情况, 一般成功返回0, 失败返回-1

2).key VS shmid

key:

通过ftok创建出来的key, key是让想要通信的不同进程, 获取到同一个共享内存shmid的钥匙

进一步解释: 进程A想要与进程B通信, 由进程A创建出了共享内存, 那么进程B如何找到这块共享内存呢? 就是通过key来找到的, 因为A创建shm(share memory)时是通过key创建的, 而key存在的目的也在于此, 进程B也用shmget通过传入key来找到对应的共享内存

shmid:

类似于文件系统的文件描述符, 共享内存不止一块, 所以需要被OS管理起来, OS管理的方式: 先描述, 再组织, 所以创建一块共享内存, 也需要创建对应的内核数据结构来将其描述起来, 而shmid就像是找到这块内核数据结构的标识, 一切的想要操作这块共享内存的系统调用接口都需要通过传入shmid来确定具体操作哪一块共享内存

总体来看:

key是对内核的, 是不同进程用来找到同一shm的钥匙

shmid是对用户的, 是通过shmid来让用户操作这块shm, 例如: shmat, shmdt, shmctl

4.代码实现System V共享内存进程通信

注: 如何体现共享内存是最快的进程通信的方式

直接将共享内存看作是buffer, 也就是读写操作直接在共享内存, 即返回的shmaddr地址进行操作即可

1).纯共享内存版本(没有访问控制)

log.hpp

#ifndef LOG_H
#define LOG_H

#include<iostream>
#include<string>
#include<assert.h>
#include<cstring>

#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

using namespace std;

#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3

#define SIZE 4096

#define PROJ_ID 5

string pathName = "/home/zsl/2022_12_29/shm_ipc";

string flags[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"
};

ostream& Log(const string& msg, int flag)
{
    cout << msg << ", " << flags[flag] << endl;
    return cout;
}

#endif

server.cxx

#include "log.hpp"

int main()
{
    key_t key = ftok(pathName.c_str(), PROJ_ID);
    assert(key != -1);
    (void)key;
    Log("server端已获取key", Debug);

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid == -1)
    {
        perror("shmget");
        exit(-1);
    }
    Log("server端已创建shm", Debug);

    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    assert(shmaddr != (void*)-1);
    Log("server端已挂接shm", Debug);

    Log("server端开始通信", Debug);
    while (true)
    {
        cout << "请输入通信内容: "; 
        fflush(stdout);
        ssize_t n = read(0, shmaddr, SIZE);
        if (n > 0)
        {
            shmaddr[n - 1] = '\0';
            if (strcmp("quit", shmaddr) == 0)
            {
                break;
            }
        }
    }
    Log("server端结束通信", Debug);

    int ret = shmdt(shmaddr);
    assert(ret != -1);
    Log("server端去挂接shm", Debug);

    ret = shmctl(shmid, IPC_RMID, nullptr);
    assert(ret != -1);
    Log("server端已删除shm", Debug);
    return 0;
}

client.cxx

#include "log.hpp"

int main()
{
    key_t key = ftok(pathName.c_str(), PROJ_ID);
    assert(key != -1);
    (void)key;
    Log("client端已获取key", Debug);

    int shmid = shmget(key, SIZE, 0);
    if (shmid == -1)
    {
        perror("shmget");
        exit(-1);
    }
    Log("client端已获取shm", Debug);

    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    assert(shmaddr != (void*)-1);

    Log("client端已挂接shm", Debug);

    Log("client端开始通信", Debug);
    while (true)
    {
        printf("client端已接收到通信内容: %s\n", shmaddr);
        if (strcmp("quit", shmaddr) == 0)
        {
            break;
        }
        sleep(1);
    }
    Log("client端结束通信", Debug);

    int ret = shmdt(shmaddr);
    assert(ret != -1);
    Log("client端去挂接shm", Debug);

    return 0;
}

纯共享内存的通信方式, 缺乏访问控制, 所以就会出现, 不管共享内存内部没有数据, 读端会一直读取, (如果为空就一直读取空的内容, 如果内容没有更新就一直读取旧数据), 这就是缺乏访问控制的表现

2).手动添加访问控制版本

管道自带访问控制, 如果想让共享内存也有访问控制, 来进行"有规则"的读写, 可以通过用管道来加锁的方式进行控制

AccessControl.hpp

#ifndef ACCESS_CONTROL_HPP
#define ACCESS_CONTROL_HPP

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <unistd.h>

#include "log.hpp"

#define NAMED_PIPE "/home/zsl/2022_12_29/shm_ipc_access_control/named_pipe"

class Init
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(NAMED_PIPE, 0666);
        assert(n != -1);
        (void)n;
        Log("已创建命名管道", Debug);
    }

    ~Init()
    {
        int n = unlink(NAMED_PIPE);
        assert(n != -1);
        (void)n;
        Log("已删除命名管道", Debug);
    }
};

int OpenNamedPipe(string pathname, int flags)
{
    int fd = open(pathname.c_str(), flags);
    if (fd < 0)
    {
        perror("open");
        exit(-1);
    }
    return fd;
}

void Wait(int fd)
{
    Log("等待中...", Notice);
    int temp = 0;
    // 从管道中读取, 如果管道中没有有效数据就会堵塞(等待)
    ssize_t n = read(fd, &temp, sizeof(int));
    assert(n == sizeof(int));
    (void)n;
}

void Signal(int fd)
{
    // 向管道中写入一个整数,来唤醒Wait
    int temp = 1;
    ssize_t n = write(fd, &temp, sizeof(temp));
    assert(n == sizeof(int));
    (void)n;
    Log("唤醒中...", Notice);
}

void Close(int fd)
{
    close(fd);
}

#endif

log.hpp

#ifndef LOG_H
#define LOG_H

#include<iostream>
#include<string>
#include<assert.h>
#include<cstring>

#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

using namespace std;

#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3

#define SIZE 4096

#define PROJ_ID 5

string pathName = "/home/zsl/2022_12_29/shm_ipc";

string flags[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"
};

ostream& Log(const string& msg, int flag)
{
    cout << msg << ", " << flags[flag] << endl;
    return cout;
}

#endif

server.cxx

#include "log.hpp"
#include "AccessControl.hpp"

int main()
{
    Init init;

    int fd = OpenNamedPipe(NAMED_PIPE, O_WRONLY);
    Log("server端以写端打开管道, 做为唤醒", Debug);

    key_t key = ftok(pathName.c_str(), PROJ_ID);
    assert(key != -1);
    (void)key;
    Log("server端已获取key", Debug);

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid == -1)
    {
        perror("shmget");
        exit(-1);
    }
    Log("server端已创建shm", Debug);

    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    assert(shmaddr != (void*)-1);
    Log("server端已挂接shm", Debug);

    Log("server端开始通信", Debug);
    while (true)
    {
        cout << "请输入通信内容: "; 
        fflush(stdout);
        ssize_t n = read(0, shmaddr, SIZE);
        if (n > 0)
        {
            shmaddr[n - 1] = '\0';
            Signal(fd);//唤醒
            if (strcmp("quit", shmaddr) == 0)
            {
                break;
            }
        }
    }
    Log("server端结束通信", Debug);
    Close(fd);
    int ret = shmdt(shmaddr);
    assert(ret != -1);
    Log("server端去挂接shm", Debug);

    ret = shmctl(shmid, IPC_RMID, nullptr);
    assert(ret != -1);
    Log("server端已删除shm", Debug);
    return 0;
}

client.cxx

#include "log.hpp"
#include "AccessControl.hpp"

int main()
{
    int fd = OpenNamedPipe(NAMED_PIPE, O_RDONLY);
    Log("server端以读端打开管道, 做为等待", Debug);

    key_t key = ftok(pathName.c_str(), PROJ_ID);
    assert(key != -1);
    (void)key;
    Log("client端已获取key", Debug);

    int shmid = shmget(key, SIZE, 0);
    if (shmid == -1)
    {
        perror("shmget");
        exit(-1);
    }
    Log("client端已获取shm", Debug);

    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    assert(shmaddr != (void*)-1);

    Log("client端已挂接shm", Debug);

    Log("client端开始通信", Debug);
    while (true)
    {
        Wait(fd);//等待
        printf("client端已接收到通信内容: %s\n", shmaddr);
        if (strcmp("quit", shmaddr) == 0)
        {
            break;
        }
    }
    Log("client端结束通信", Debug);
    Close(fd);
    int ret = shmdt(shmaddr);
    assert(ret != -1);
    Log("client端去挂接shm", Debug);

    return 0;
}

5.通过指令操作ipc资源

三种进程通信资源

如何查看

ipcs -m --- 查看所有共享内存

ipcs -q --- 查看所有消息队列

ipcs -s --- 查看所有信号量

如何删除

ipcrm -m shmid --- 删除指定共享内存

ipcrm -q msqid --- 删除指定消息队列

ipcrm -s semid --- 删除指定信号量

ipcrm -a --- 删除所有进程通信资源

6.补充: 关于共享内存的删除

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

7.补充: 关于共享内存与管道的读取的一些区别

管道:

类似于一个循环队列, 数据挨着写入, 挨着读取, 读取过的数据就会被标记为无效, 等下次管道写满了就会从头开始写, 覆盖掉无效数据

共享内存:

类似于malloc的操作方式, 所写入/读取数据的内容, 完全通过人为控制(通过起始地址shmaddr去控制), 如果两次都是从同一地址写入, 那么新数据会直接覆盖旧数据

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/132349.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

你好2023-使用msys64 openssl 制作QSslSocket实验所需证书

2023年开始了&#xff0c;第一篇&#xff0c;记录最近帮朋友制作QSslSocket所需证书的过程。 使用传统的TCP连接依旧是很多工业软件的常见通信方法。但如果恰好不希望别人通过抓包等方法研究上位机和控制器模块之间的协议格式&#xff0c;那使用SSL连接是一种掩耳盗铃的好办法&…

Pyinstaller - 你的“神”队友

哈哈&#xff01;今天是我在2023年发布的第一篇文章呀&#xff01; 这两天&#xff0c;我在做一个爬虫项目。因为我做好后准备给我的朋友看看&#xff0c;但我朋友没有 Python 环境。所以&#xff0c;只好想办法把 .py 打包成 .exe 。 在网上搜了一下&#xff0c;发现目前相对…

设计模式 ——工厂模式

前言 有一些重要的设计原则在开篇和大家分享下&#xff0c;这些原则将贯通全文&#xff1a; 面向接口编程&#xff0c;而不是面向实现。这个很重要&#xff0c;也是优雅的、可扩展的代码的第一步&#xff0c;这就不需要多说了吧。 职责单一原则。每个类都应该只有一个单一的功…

第三十一讲:神州路由器策略路由的配置

从局域网去往广域网的流量有时需要进行分流&#xff0c;即区别了不同用户又进行了负载分担&#xff0c;有时这种目标是通过对不同的源地址进行区别对待完成的&#xff0c;通过策略路由的方法可以解决此问题。 实验拓扑图如下所示 R1 R2 R3 F0/0 1.1.3.1/24 F0/0 1.1.3.2…

【AcWing每日一题】4261. 孤独的照片

Farmer John 最近购入了 N 头新的奶牛&#xff0c;每头奶牛的品种是更赛牛&#xff08;Guernsey&#xff09;或荷斯坦牛&#xff08;Holstein&#xff09;之一。 奶牛目前排成一排&#xff0c;Farmer John 想要为每个连续不少于三头奶牛的序列拍摄一张照片。 然而&#xff0c…

java多线程(11):线程协作

1 线程通信 应用场景 : 生产者和消费者问题 假设仓库中只能存放一件产品 , 生产者将生产出来的产品放入仓库 , 消费者将仓库中产品取走消费 如果仓库中没有产品 , 则生产者将产品放入仓库 , 否则停止生产并等待 , 直到仓库中的产品被消费者取走为止 如果仓库中放有产品 ,…

Chrome Extension 基础篇

Extensions are software programs, built on web technologies (such as HTML, CSS, and JavaScript) that enable users to customize the Chrome browsing experience. 扩展程序是基于 Web 技术&#xff08;例如 HTML、CSS 和 JavaScript&#xff09;构建的软件程序&#xf…

C语言递归

递归指的是在函数的定义中使用函数自身的方法。 举个例子&#xff1a; 从前有座山&#xff0c;山里有座庙&#xff0c;庙里有个老和尚&#xff0c;正在给小和尚讲故事呢&#xff01;故事是什么呢&#xff1f;"从前有座山&#xff0c;山里有座庙&#xff0c;庙里有个老和尚…

redis的安装

1.Redis是基于C语言编写的&#xff0c;因此首先需要安装Redis所需要的gcc依赖&#xff1a; yum install -y gcc tcl2.上传安装包并解压 tar -xzf redis-6.2.6.tar.gz3.解压后&#xff0c;进入redis目录 cd redis-6.2.64.运行编译命令 make && make install如果没有…

3_运行时数据区概述及线程

前言 本节主要讲的是运行时数据区&#xff0c;也就是下图这部分&#xff0c;它是在类加载完成后的阶段 当我们通过前面的&#xff1a;类的加载-> 验证 -> 准备 -> 解析 -> 初始化 这几个阶段完成后&#xff0c;就会用到执行引擎对我们的类进行使用&#xff0c;同时…

56. 数据增广 / 图像增广

1. CES上的真实故事 2. 数据增强 增加一个已有数据集&#xff0c;使得有更多的多样性 在语言里加入各种不同的背景噪音改变图片的颜色和形状 例如&#xff0c;我们可以以不同的方式裁剪图像&#xff0c;使感兴趣的对象出现在不同的位置&#xff0c;减少模型对于对象出现位置…

Linux系统如何添加磁盘分区基本情况

Linux系统如何添加磁盘&&分区基本情况 原理介绍 Linux来说无论有几个分区&#xff0c;分给哪一目录使用&#xff0c;它归根结底就只有一个根目录&#xff0c;一个独立且唯一的文件结构&#xff0c;Linux中每个分区都是用来组成整个文件系统的一部分。 Linux采用了一种…

JavaScript-DOM和BOM详解

文章目录DOM 和 BOM1. DOM2. BOM2.1 BOM 简介2.2 分类2.3 语法1) Navigator 当前浏览器2&#xff09;Histry 向前或向后翻页3&#xff09;Location 地址栏的信息DOM 和 BOM 1. DOM 浏览器已经为我们提供了文档节点的对象&#xff0c;这个对象是 window 对象的属性可以在页面中…

Netconf协议讲解

目录 什么是Netconf 为什么要提出Netconf 数据的类别 传统网络配置协议 Netconf配置协议 Netconf协议架构 安全传输层 消息层 操作层 内容层 Netconf配置设备流程 通过Python进行Netconf配置 什么是Netconf NETCONF&#xff08;Network Configuration Protocol&…

Unity运行时代码编辑插件介绍-InGame Code Editor-IDE类文本编辑器

因为某些原因,需要在Runtime显示一下代码,也方便做样式设计 所以找到了这个插件 特色什么的都不展开说了,开源的代码都是好代码,样式什么的就不能要求过多 基础使用方法 导入TextMeshPro 基于这个插件的,所以需要先从Package Manager先下载TextMeshPro 创建编辑器 T…

【SpringBoot应用篇】SpringBoot集成j2cache二级缓存框架

【SpringBoot应用篇】SpringBoot集成j2cache二级缓存框架j2cache介绍j2cache入门使用pomapplication.ymlcaffeine.propertiesCacheTestController启动类j2cache介绍 j2cache是OSChina(开源中国)目前正在使用的两级缓存框架。 j2cache的两级缓存结构&#xff1a; L1&#xff…

《悠悠岁月》悠悠岁月,浅藏浅忆,且行且珍惜

《悠悠岁月》悠悠岁月&#xff0c;浅藏浅忆&#xff0c;且行且珍惜 安妮埃尔诺&#xff0c;法国当代著名女作家&#xff0c;2022年获诺贝尔文学奖。埃尔诺从1974年开始创作&#xff0c;至今已出版了约十五部作品。《悠悠岁月》这部历经二十余年思考和推敲的杰作&#xff0c;使她…

Apache Shiro(一)

1.Apache Shiro Apache Shiro Reference Documentation | Apache Shiro Apache Shiro 是一个功能强大且易于使用的 Java 安全(权限)框架。Shiro 可以完 成&#xff1a;认证、授权、加密、会话管理、与 Web 集成、缓存 等。借助 Shiro 您可以快速轻松 地保护任何应用程序——从最…

PHPExcel基本使用(2) 导入图片

一、效果二、代码一、效果 基于这篇 PHPExcel基本使用&#xff08;2&#xff09; 导入图片 调整 二、代码 基于thinkphp5.1 <?phpnamespace app\index\controller;use think\facade\Env;class Test {public function test(){self::excelAction();}/*** todo 导出报表*…

Microcontent - 微内容

这两年&#xff0c;微内容不断被人提及。微内容是什么&#xff1f;微内容解决什么问题&#xff1f;今天我们一起来看看这个话题。 作者&#xff1a;Sarah Cuellar - 1 - 什么是微内容 什么是微内容&#xff1f;微内容指的是小块的内容&#xff0c;它们遵循具体的的结构规则…