【Linux】17. 进程间通信 --- 管道

1. 什么是进程间通信(进程间通信的目的)

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

在之前的学习过程中我们了解到进程是具有独立性的,也就是说要实现通信成本一定不低
那我们为啥要实现通信呢?
是因为在使用操作系统的过程中是存在多进程协同的应用场景滴!完成某种业务需求 (例如:cat file | grep ’ hello ')

  • 那么该如何理解通信的本质问题呢?
  • 要实现通信,首先数据需要存放位置,如果将数据存放在某个进程当中,因为进程的独立性,直接由进程创建的资源其他进程无法看见
    所以要由操作系统直接或者间接的给通信双方的进程提供"内存空间"
  • 要让不同的进程看到同一份公共的资源

通过资源是OS中的不同模块提供所得到不同的通信种类:如果是文件模块 – 管道通信 如果是SystemV通信模块 – System V通信…

2. 进程间通信发展

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

其实在进程间通信的研究过程当中产生了一大堆的标准,但是主流的还是以上三种
POSIX — 让通信过程可以跨越主机
System V — 主要是聚焦在本地通信当中
管道 — 基于文件系统形成的

3. 管道

3.1 什么是管道

管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道
在这里插入图片描述
在这里插入图片描述
文件存储在磁盘当中,当数据写入到内核缓冲区时,会刷新到磁盘上
进程间通信是否会采取数据写入到磁盘上再从磁盘上读取到进程的上下文环境呢? – 不会
因为这样太慢了,我们要实现的是两进程的通信(从内存到内存) ,而将数据写入磁盘(内存 -> 磁盘 -> 内存)效率非常低
对于管道文件而言,需不需要在磁盘上占据内存空间(在磁盘上打开文件) – 不需要
操作系统非常强大,可以直接在内存当中创建文件(管道文件 – 内存级文件)
内存级文件不需要进行磁盘刷新,大大的提高了进程间通信的效率

3.2 匿名管道

上述的实现过程中是如何让两个进程看到同一个管道文件? 通过fork创建子进程完成
子进程会继承父进程的文件描述符表 父子进程会指向同一文件,但是该文件没有名字(内存级文件) 我们将其称之为匿名管道
在这里插入图片描述
为啥父子进程要以读写的方式打开同一文件呢?
因为子进程会继承父进程的文件描述符表和文件的打开方式,若是父进程只以读/写的方式打开文件,那么子进程就会继承对应的读写方式,无法构成管道的需求。
在这里插入图片描述
这里的pipefd [ 2 ] 是输出型参数。
在之前的学习过程中我们了解到只要打开一个文件,操作系统会对应打开0,1,2(标准输入,标准输出,标准错误)
那么文件描述符就是要从3开始

创建管道文件,操作系统以读写的方式打开文件,将进程的文件描述符表填写到数组当中,再将数组输出返回调用该函数就可以创建管道文件

写管道文件的描述符必须通过fd数组的下标访问,不能直接使用具体的fd:3 / 4
(因为我们并不清楚现在文件描述符当中是否还存在其他描述符)
fd【0】读取 – 0 像嘴巴
fd【1】写入 – 1 像钢笔

在这里插入图片描述

基本框架如下:
#include <iostream>
#include <unistd.h>
// 当我们在进行C语言、C++混编的时候
// 推荐将C语言的头文件引用为<c..>的格式
#include <cassert>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    // 第一步:创建管道文件,打开读写端
    int fds[2];
    int n = pipe(fds);
    // 管道创建成功,n返回0
    assert(n == 0);

    // 第二步:fork(创建子进程)
    pid_t id = fork();
    // 创建子进程成功
    assert(id >= 0);
    if (id == 0)
    {
        // 子进程模块
        // 关闭读接口
        close(fds[0]);
        // 进行父子进程通信

        close(fds[1]);
        exit(0);
    }
    // 父进程模块
    // 进行读取
    // 关闭写接口
    close(fds[1]);

    // 父进程进行等待
    n = waitpid(id, nullptr, 0);
    assert(n == id);
    close(fds[0]);
}

运行结果如下:
在这里插入图片描述
在这里插入图片描述
此时父进程完成读取,进入等待状态(R ——> S),在等待管道文件就会将父进程的PCB放入文件的等待队列当中
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3 管道读写规则

  • 当没有数据可读时
  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  • 当管道满的时候
  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程
    退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

3.4 管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
    在这里插入图片描述
    在这里插入图片描述
    sleep10000 | sleep 20000 bash命令行解释器,会将其划分成为两个进程, 竖划线创建的就是匿名管道

3.5 匿名管道进程池(重点)

1) 理清思路

在这里插入图片描述
我们想要实现的功能是首先父进程跟多个子进程之间建立管道,创建任务集,随机挑选子进程完成随机任务。
子进程根据父进程传入管道中的commandCode完成对应的任务。

2) 代码实现

① 加载任务集
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/子进程要完成的某种任务  --- 模拟实现

// 函数指针 类型
typedef void (*func_t)();

// 这里就模拟实现了三种任务
void downLoadTask()
{
    // 执行任务的同时获取一下子进程的pid -- 便于观察
    std::cout << getpid() << ": 下载任务\n"
              << std::endl;
}

void ioTask()
{
    std::cout << getpid() << ": IO任务\n"
              << std::endl;
}

void flushTask()
{
    std::cout << getpid() << ": 刷新任务\n"
              << std::endl;
}

// 往out中插入任务集
void loadTaskFunc(std::vector<func_t> *out)
{
    assert(out);
    out->push_back(downLoadTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}

int main()
{
    // 1. 加载任务集
    std::vector<func_t> funcMap;
    loadTaskFunc(&funcMap);
    return 0;
}
② 创建子进程
    // 2. 创建子进程
    std::vector<subEp> subs;
    createSubProcess(&subs, funcMap);
#define PROCESS_NUM 4

/子进程要完成的某种任务  --- 模拟实现

// 函数指针 类型
typedef void (*func_t)();

// 这里就模拟实现了三种任务
void downLoadTask()
{
    // 执行任务的同时获取一下子进程的pid -- 便于观察
    std::cout << getpid() << ": 下载任务\n"
              << std::endl;
}

void ioTask()
{
    std::cout << getpid() << ": IO任务\n"
              << std::endl;
}

void flushTask()
{
    std::cout << getpid() << ": 刷新任务\n"
              << std::endl;
}

// 往out中插入任务集
void loadTaskFunc(std::vector<func_t> *out)
{
    assert(out);
    out->push_back(downLoadTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}

///下面的代码模拟实现多进程程序
class subEp
{
public:
    subEp(pid_t subId, int writeFd)
        : subId_(subId), writeFd_(writeFd)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);
        name_ = nameBuffer;
    }

public:
    static int num;
    std::string name_;
    pid_t subId_;
    int writeFd_;
};

// 因为num为static类型 不属于任何一个对象(实例) 所以必须在这里进行初始化
int subEp::num = 0;

int recvTask(int readFd)
{
    int code = 0;
    // 判断是否读取到管道中的数据
    ssize_t s = read(readFd, &code, sizeof code);
    // 读取到 4个字节
    if (s == 4)
        return code;
    // 父进程退出
    else if (s <= 0)
        return -1;
    else
        return 0;
}

void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
    for (int i = 0; i < PROCESS_NUM; i++)
    {
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        (void)n;
        pid_t id = fork();
        if (id == 0)
        {
            close(fds[1]);
            while (true)
            {
                // 获取管道中的任务编号,如果没有发送,子进程应该进行阻塞等待
                int commandCode = recvTask(fds[0]);
                if (commandCode >= 0 && commandCode < funcMap.size())
                {
                    // 执行任务
                    funcMap[commandCode];
                }
                else if (commandCode == -1)
                {
                    // 检测到父进程退出 跳出循环
                    break;
                }
            }
            // 子进程退出
            exit(0);
        }
        // 关闭读文件描述符 fds[0]
        close(fds[0]);
        subEp sub(id, fds[1]);
        // 将创建出的子进程插入subEp对象当中
        subs->push_back(sub);
    }
}

这里构造的subEp(先描述再组织)用来管理子进程

③ 父进程控制子进程
    // 3. 父进程控制子进程,负载均衡的向子进程发生任务码
    int taskCnt = 3;
    loadBlanceControl(subs, funcMap, taskCnt);
void sendTask(const subEp &process, int taskNum)
{
    std::cout << "send task num: " << taskNum << " send to -> " << process.name_ << std::endl;
    int n = write(process.writeFd_, &taskNum, sizeof(taskNum));
    assert(n == sizeof(int));
    (void)n;
}

void loadBlanceControl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
    int processnum = subs.size();
    int tasknum = funcMap.size();

    while (count)
    {
        // 这里就是负载均衡的体现
        // 1. 选择一个子进程 --> std::vector<subEp> -> index - 随机数
        int subIdx = rand() % processnum;
        // 2. 选择一个任务 --> std::vector<func_t> -> index
        int taskIdx = rand() % tasknum;
        // 3. 将任务发送给选择的进程
        sendTask(subs[subIdx], taskIdx);
        // 每次发送完休息1秒
        sleep(1);
        count--;
    }
    // 当write写入退出 说明读到0了 不再需要给子进程发送任务
    for (int i = 0; i < processnum; i++)
    {
        close(subs[i].writeFd_); // waitpid();
    }
}
④ 回收子进程信息
// 4. 回收子进程信息
waitProcess(subs);
void waitProcess(std::vector<subEp> processes)
{
    int processnum = processes.size();
    for(int i =0;i<processnum;i++)
    {
        waitpid(processes[i].subId_,nullptr,0);
        std::cout<<"wait sub process success ...: " << processes[i].subId_ << std::endl;
    }
}

在这里插入图片描述
assert断言
意料之中用assert 意料之外用if判断
(void)n的作用 assert的作用只是在debug下,在release版本下该行代码会删除
那么之前定义的n变量就没有进行使用可能就会出现warning报错:定义了变量但未使用。

3.6 命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

创建一个命名管道

  • 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
  • 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);

匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

命名管道的打开规则

  • 如果当前打开操作是为读而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功

  • 如果当前打开操作是为写而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

命名管道通信

逻辑分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
即便两个文件在进行通信,管道文件的大小仍是0
在这里插入图片描述
在这里插入图片描述

代码分析
// 头文件 + 创建/删除管道
#pragma once

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

// 定义宏 将命名管道放在当前目录下(便于观察)
#define NAMED_PIPE "./mypipe"
// 创建管道
bool createFifo(const std::string &path)
{
    // 将文件掩码设置为0
    umask(0);
    int n = mkfifo(path.c_str(), 0600);
    if (n == 0)
        return true;
    else
    {
        std::cout << "errno: " << errno << " err stirng: " << strerror(errno) << std::endl;
        return false;
    }
}

// 删除管道
void removeFifo(const std::string &path)
{
    int n = unlink(path.c_str());
    assert(n == 0);
    (void)n;
}

mkfifo 创建管道文件,unlink 关闭文件

client端 – 客户端 往管道中写入数据

#include "comm.hpp"

int main()
{
    std::cout << "client begin" << std::endl;
    // 将管道文件以只写入的方式打开
    int wfd = open(NAMED_PIPE, O_WRONLY);
    std::cout << "client end" << std::endl;
    // 管道文件打开失败
    if (wfd < 0)
        exit(1);

    // write
    char buffer[1024];
    while (true)
    {
        std::cout << "Please say# ";
        fgets(buffer, sizeof(buffer), stdin);
        // 当管道中有数据 将最后一位置为0
        // 这里的if判断buffer>0虽然没必要 因为输入数据至少要按下回车键 就一定有数据
        // 但是逻辑正确
        if (strlen(buffer) > 0)
            buffer[strlen(buffer) - 1] = 0;
        ssize_t n = write(wfd, buffer, strlen(buffer));
        assert(n == strlen(buffer));
        (void)n;
    }

    close(wfd);
    return 0;
}

server端 — 服务端 从管道当中读出数据

#include "comm.hpp"

// sever作为主控制
int main()
{
    // 创建命名管道
    bool r = createFifo(NAMED_PIPE);
    assert(r);
    (void)r;

    std::cout << "sever begin" << std::endl;
    int rfd = open(NAMED_PIPE, O_RDONLY);
    std::cout << "sever end" << std::endl;
    if (rfd < 0)
        exit(1);

    // read
    char buffer[1024];
    while (true)
    {
        ssize_t s = read(rfd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "client->server#" << buffer << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "client quit,me too!" << std::endl;
            break;
        }
        else
        {
            std::cout << "err string: " << strerror(errno) << std::endl;
            break;
        }
    }

    close(rfd);
    // 等待10秒 观察当前路径下的命名管道是否删除
    // sleep(10);
    removeFifo(NAMED_PIPE);
    return 0;
}

细节1:
默认情况下 读取进程先运行,只有当写入打开后,读取进程才会继续往后进行
在这里插入图片描述
read进程会卡在open函数这, 因为此时管道文件只有读描述符,没有写入描述符(读没有意义,所以阻塞)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

sqlmodel实现唯一性校验3,检查多列同时重复

之前的方案虽然能够解决重复性问题&#xff0c;但是没有覆盖到多列同时重复的情况。 比如&#xff0c;我们可以认为用户名是可以重复的。但是用户名和年龄不能同时重复&#xff0c;那么这种情况该怎么解决呢&#xff1f; 之前的代码如下&#xff1a; from sqlalchemy import…

python直接发布到网站wordpress之三批量发布图片

在前面的文章中&#xff0c;实现了使用python操作wordpress发布文字内容和图片内容。 python直接发布到网站wordpress之一只发布文字-CSDN博客 python直接发布到网站wordpress之二发布图片-CSDN博客 不过&#xff0c;此时发布图片的数量只能是一张图片。但在实际应用中&…

效率跨越式提升的工农业对机器人专业的需求

需求 需要用人的地方一定会逐步收缩。 原来需要人的地方也会逐步被机器人取代。 机器人这个专业最强的悖论就是可以部分取代人。 此处&#xff1a;用人的地方是指“工农业”&#xff0c;包括工业和农业。 机器人工程行业算制造业吗 机器人工程终身学习和工作计划 趋势 工匠…

1077 互评成绩计算

solution 总成绩 &#xff08;老师成绩 同学去掉最高分去掉最低分的平均分&#xff09;/2&#xff0c;其中总成绩四舍五入取整 #include<iostream> #include<algorithm> using namespace std; int main(){int n, m, worst, better, sum, g, x, cnt;scanf("…

【数学建模】天然肠衣搭配问题

2011高教社杯全国大学生数学建模竞赛D题 天然肠衣&#xff08;以下简称肠衣&#xff09;制作加工是我国的一个传统产业&#xff0c;出口量占世界首位。肠衣经过清洗整理后被分割成长度不等的小段&#xff08;原料&#xff09;&#xff0c;进入组装工序。传统的生产方式依靠人工…

每日OJ题_DFS解决FloodFill⑤_力扣417. 太平洋大西洋水流问题

目录 力扣417. 太平洋大西洋水流问题 解析代码 力扣417. 太平洋大西洋水流问题 417. 太平洋大西洋水流问题 难度 中等 有一个 m n 的矩形岛屿&#xff0c;与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界&#xff0c;而 “大西洋” 处于大陆的右边界和下…

自动控制原理MATLAB:控制系统模型构建

在MATLAB中&#xff0c;常用的系统建模方法有传递函数模型、零极点模型以及状态空间模型等。 1系统传递函数模型描述&#xff1a; 命令格式&#xff1a; systf(num,den,Ts); 其中&#xff0c;num、den为分子多项式降幂排列的系数向量,Ts表示采样时间&#xff0c;缺省时描述…

AI 数据观 | TapData Cloud + MongoDB Atlas:大模型与 RAG 技术有机结合,落地实时工单处理智能化解决方案

本篇为「AI 数据观」系列文章第二弹&#xff0c;在这里&#xff0c;我们将进一步探讨 AI 行业的数据价值。以 RAG 的智能工单应用场景为例&#xff0c;共同探索如何使用 Tapdata Cloud MongoDB Atlas 实现具备实时更新能力的向量数据库&#xff0c;为企业工单处理的智能化和自…

在小黑框如何用Python写出多行代码

平时使用python自带的小黑框编译器只能一行代码一行代码的写&#xff0c; 方法一 可以新建一个文本txt格式&#xff0c;然后打开在里面输入你想要的Python代码&#xff0c;然后把名字改成xxx.py&#xff0c;然后点击小黑框&#xff0c;输入 python 把Py文件拖过来回车就行 方…

Hive内部表、外部表

Hive内部表、外部表 1. 内部表&#xff08;Managed Table&#xff09;&#xff1a; 内部表是由Hive完全管理的表&#xff0c;包括数据和元数据。当你删除内部表时&#xff0c;Hive会同时删除表的数据和元数据。内部表的数据存储在Hive指定的默认位置&#xff08;通常是HDFS上…

VBA 创建透视表,录制宏,自动化报表

目录 一. 数据准备二. 需求三. 准备好报表模板四. 执行统计操作&#xff0c;录制宏4.1 根据数据源创建透视表4.2 填充数据到报表4.3 结束宏录制 五. 执行录制好的宏&#xff0c;自动化报表 一. 数据准备 ⏹数据源1 姓名学科成绩丁志敏语文91李平平语文81王刚语文64张伊语文50…

【前端】HTML基础(1)

文章目录 前言一、什么是前端二、HTML基础1、 HTML结构1.1 什么是HTML页面1.2 认识HTML标签1.3 HTML文件基本结构1.3 标签层次结构1.4 创建html文件1.5 快速生成代码框架 三、Emmet快捷键 前言 这篇博客仅仅是对HTML的基本结构进行了一些说明&#xff0c;关于HTML的更多讲解以及…

新能源电燃灶:为人类社会贡献高品质的健康生活

华火新能源电燃灶&#xff0c;作为一种创新的厨房设备&#xff0c;近年来逐渐走进了千家万户&#xff0c;成为了现代家庭厨房的新宠。它不仅改变了传统的烹饪方式&#xff0c;更在环保、节能、安全等方面为人类带来了诸多贡献。本文将从多个方面探讨华火新能源电燃灶对人类的贡…

知行之桥EDI系统跨平台版本安装报错及解决方案

本文将为大家介绍如何在Windows系统中安装知行之桥EDI系统跨平台版本的常见报错以及解决方案。如下图所示&#xff1a; 在知行软件官网的导航栏中点击 下载 按钮&#xff0c;即可看到知行之桥EDI系统不同版本的下载选项&#xff0c;点击右侧跨平台版本&#xff0c;选择 Windows…

移动硬盘无法被识别怎么办?恢复移动硬盘3个正确做法

移动硬盘已成为我们日常生活和工作中不可或缺的数据存储设备。然而当移动硬盘突然无法被电脑识别时&#xff0c;往往会让人倍感焦虑。面对这种情况我们不必过于慌张&#xff0c;下面一起来看看指南解决。 解决方法一&#xff1a;检查硬件连接与供电 检查接口连接&#xff1a…

uniapp离线在Xcode上打包后提交审核时提示NSUserTrackingUsageDescription的解决方法

uniapp离线在Xcode上打包后提交审核时提示NSUserTrackingUsageDescription的解决方法 问题截图&#xff1a; 亲测有效的方法 方法一&#xff1a; 选择通过uniapp的开发工具Hbuilder来进行在线打包&#xff0c;取消默认勾选的以下选项。 然后进行在线打包就不会存在提交审…

山东省文史书画研究会成立20周年系列活动徽标征集胜选名单公布

2024年5月1日&#xff0c;山东省文史书画研究会成立20周年系列活动徽标征集落下帷幕。征稿启事下发后&#xff0c;得到社会各界人士的广泛关注与参与&#xff0c;共收到设计方案608件。经过初评&#xff0c;选出5幅作品进入复评&#xff0c;并经过网络投票和专家投票相结合的方…

linux——主从同步

1. 保证主节点开始二进制日志&#xff0c;从节点配置中继日志 2. 从节点的开启一个 I/O 线程读取主节点二进制日志的内容 3. 从节点读取主节点的二进制日志之后&#xff0c;会将去读的内容写入从节点的中继日志 4. 从节点开启 SQL 线程&#xff0c;读取中继日志的内容&a…

《软件方法(下)》8.3 建模步骤C-2 识别类的关系(202405更新)

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 8.3 建模步骤C-2 识别类的关系 首先重复本章开头所提到的&#xff1a; 虽然本书先讲解“识别类和属性”&#xff0c;再讲解“识别类的关系”&#xff0c;但在实际工作中&#xff0c;…

数据库管理-第184期 23ai:干掉MongoDB的不一定是另一个JSON数据库(20240507)

数据库管理184期 2024-05-07 数据库管理-第184期 23ai:干掉MongoDB的不一定是另一个JSON数据库&#xff08;20240507&#xff09;1 JSON需求2 关系型表设计3 JSON关系型二元性视图3 查询视图总结 数据库管理-第184期 23ai:干掉MongoDB的不一定是另一个JSON数据库&#xff08;20…