【Linux】管道

目录

一、前言

二、管道

1、匿名管道

1.1、基本原理

1.2、代码实现

1.3、管道的特点

1.4、基于管道的简单设计

2、命名管道

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

2.2、代码实现命名管道通信


一、前言

 为了满足各种需求,进程之间是需要通信的。进程间通信的主要目的包括如下几个方面:

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

 因为进程具有独立性,这增加了通信的成本。要让两个不同的进程通信,前提条件是先让两个进程看到同一份“资源”,这份“资源”通常是由OS直接或间接提供的。

 所以任何进程通信手段,无非都包含如下步骤:

  1. 想办法先让不同的进程看到同一份资源。
  2. 让一方写入,另一方读取,完成通信过程。
  3. 至于通信目的与后续工作,要结合具体场景具体分析。

 进程间通信可以分为三类:

  1. 管道
  2. System V IPC
  3. POSIX IPC

 这三类方法,都是在解决第一个步骤,即让不同的进程看到同一份“资源”。本篇博客针对于管道进行讲解。

二、管道

 管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

 管道也是文件。在使用管道符 "|" 时, "|" 左边的进程以写的方式打开管道文件,将标准输出重定向到管道之中, "|" 右边的进程以读的方式打开管道文件,将标准输入重定向到管道之中。

1、匿名管道

1.1、基本原理

 在文章《文件描述符》中,对已经打开的文件与进程的关系进行了较为详细的说明,文件描述符的 0、1、2 默认为标准输入、标准输出、标准错误。这些文件都有对应的缓冲区。

 匿名管道文件是OS提供的内存文件,仅存在于内存,而并不需要将该文件的内容刷新到磁盘之中。OS通过某些方式,使用读方式和写方式分别打开这个匿名管道文件。

 该进程通过 fork 创建一个子进程,子进程拷贝了父进程的PCB结构,包括 task_struct 与 struct files_struct 。因此子进程的文件描述符表中存储的指针也被拷贝下来了,指向同一批文件对象(创建子进程的时候,fork子进程,只会复制进程相关的数据结构对象,不会复制父进程曾经打开的文件对象)。这时,我们就做到了进程间通信的前提:让不同的进程看到同一份“资源”。

 这种管道只支持单向通信,因此在进程通信的时候,需要确定数据的流向,分别关闭和保留父子进程文件描述符表中的读与写端。这是因为文件对象只有一个缓冲区,难以做到同时读写。

 进程创建管道的具体过程如下:

1.2、代码实现

创建管道函数:

int pipe(int pipefd[2]);

 pipe 系统调用函数的参数列表中有一个数组,是一个输出型参数。如果创建成功,函数返回值是 0 ,失败返回值为 -1 。因为系统调用接口的底层是使用C语言编写的,所以错误码 errno 会被设置。具体用法如下:

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <cerrno>
#include <assert.h>
#include <string.h>
using namespace std;

int main()
{
    int pipefd[2] = {0};
    //1.创建管道
    int n = pipe(pipefd);
    if(n < 0)
    {
        cout << "pipe error, " << errno << ": " << strerror(errno) << endl;
        return 1;
    }
    cout << "pipefd[0]: " << pipefd[0] << endl; //读端
    cout << "pipefd[1]: " << pipefd[1] << endl; //写端
    
    //2.创建子进程
    pid_t id = fork();
    assert(id != -1);
    if(id == 0)
    {
        //子进程
    //3.关闭不需要的fd
        //这里让父进程读,子进程写
        close(pipefd[0]);

    //4.开始通信
        string namestr = "子进程";
        int cnt = 1;
        char buffer[1024];
        while(1)
        {
            snprintf(buffer, sizeof(buffer), "%s, 计数器:%d, PID: %d\n", namestr.c_str(), cnt++, getpid());
            write(pipefd[1], buffer, strlen(buffer));
            sleep(1);
        }

        exit(0);
    }
    //父进程
    close(pipefd[1]);

    char buffer[1024];
    while(1)
    {
        int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = '\0';
            cout << "父进程" << endl;
            cout << "子进程发送消息: " << buffer << endl;
        }
    }

    return 0;
}

编译并运行:

1.3、管道的特点

  1.  管道是单向通信的.
  2.  管道本质是文件。因为文件描述符的生命周期是随进程的,所以管道的生命周期也是随进程的。
  3.  管道通信,通常用来进行具有“血缘”关系的进程之间的通信。如父子进程间通信。
  4.  pipe系统调用打开的管道,并不清楚它的名字,称之为匿名管道。
  5.  在管道通信中,写入的次数与读取的次数,不是严格匹配的。
  6.  管道具有一定的协同能力,让读端与写端按照一定的步骤进行通信:
    1)如果读端读取完毕了所有的管道数据,此时如果写端不写,读端就只能等待。
    2)如果写端将管道写满了,就无法再继续写入,等读端读取之后才能继续写。
    3)如果关闭了写端,读端读取完毕管道数据后,再读,read就会返回 0 ,表明读到了文件结尾。
    4)如果写端一直在写,并关闭了读端,那么OS会通过信号终止一直在写入的进程。因为OS不会维护无意义、低效率、浪费资源的事情。
  7. 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
     

1.4、基于管道的简单设计

 现在我们来实现由一个父进程通过管道向多个子进程写入特定消息,让子进程定向执行某种任务的代码:

//task.hpp
#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>

typedef void (*fun_t)();

void Print()
{
    std::cout << "pid: " << getpid() << "打印任务正在执行" << std::endl;
}

void InsertMySQL()
{
    std::cout << "pid: " << getpid() << "数据库任务正在执行" << std::endl;
}

void NetRequest()
{
    std::cout << "pid: " << getpid() << "网络请求任务正在执行" << std::endl;
}

//约定每一个command都是4个字节
#define COMMAND_PRINT 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2

class Task
{
public:
    Task()
    {
        funcs.push_back(Print);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    void Execute(int command)
    {
        if(command >= 0 && command < funcs.size())
        {
            funcs[command];
        }
    }
    ~Task(){}

public:
    std::vector<fun_t> funcs;
};


//ctrlProcess.cc
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "task.hpp"
using namespace std;

const int gnum = 3;
Task t;

class EndPoint
{
private:
    static int number;
public:
    pid_t _child_id;
    int _write_fd;
    string processname;
public:
    EndPoint(int id, int fd):_child_id(id),_write_fd(fd)
    {
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
        processname = namebuffer;
    }
    const string& name() const
    {
        return processname;
    }

    ~EndPoint(){}
};
int EndPoint::number = 0;

//子进程要执行的方法
void WaitCommand()
{
    while(1)
    {
        char command = 0;
        int n = read(0, &command, sizeof(int));
        if(n == sizeof(int))
        {
            t.Execute(command);
        }
        else if(n == 0)
        {
            cout << "父进程让我退出,我的pid:" << getpid() << endl;
            break;
        }
        else
        {
            break;
        }
    }
}

void creatProcesses(vector<EndPoint>* end_points)
{
    for(int i = 0; i < gnum; ++i)
    {
        //创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        //创建进程
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)
        {
            //子进程
            close(pipefd[1]);
            //我们期望,所有的子进程读取“指令”,都从标准输入读取
            //输入重定向,也可以不重定向,只要在WaitCommand函数里传参fd就可以了
            dup2(pipefd[0], 0);
            //子进程开始等待获取命令
            WaitCommand();

            exit(0);
        }
        //父进程
        close(pipefd[0]);

        //将新的子进程和他的管道写端构造对象
        end_points->push_back(EndPoint(id, pipefd[1]));
    }
}

int ShowBoard()
{
    cout << "########################################" << endl;
    cout << "### 0.执行打印任务     1.执行数据库任务###" << endl;
    cout << "### 2.执行请求任务     3.退出         ###" << endl;
    cout << "########################################" << endl;
    cout << "请选择" << endl;
    int command = 0;
    cin >> command;
    return command;
}

void ctrlProcess(const vector<EndPoint>& end_points)
{
    int num = 0;
    int cnt = 0;
    while(1)
    {
        //选择任务
        int command = ShowBoard();
        if(command == 3) break;
        if(command < 0 || command > 2) continue;
        //选择进程
        int index = cnt++;
        cnt %= end_points.size();
        cout << "选择了进程: " << end_points[index].name() << " | 处理任务: " << command << endl;

        //下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));
    }
}

// void WaitProcess(const vector<EndPoint>& end_points)
// {
//     //让子进程全部退出,通过关闭写端的方式
//     for(const auto& ep : end_points) close(ep._write_fd);
//     cout << "父进程让所有子进程退出" << endl;
//     sleep(5);

//     //父进程回收子进程的僵尸状态
//     for(const auto& ep : end_points) waitpid(ep._child_id, nullptr, 0);
//     cout << "父进程回收了所有的子进程" << endl;
//     sleep(1);
// }

void WaitProcess(const vector<EndPoint>& end_points)
{
    for(int end = end_points.size() - 1; end >= 0; --end)
    {
        cout << "父进程让我退出: " << end_points[end]._child_id << endl;
        close(end_points[end]._write_fd);

        waitpid(end_points[end]._child_id, nullptr, 0);
        cout << "父进程回收了子进程:" << end_points[end]._child_id << endl;
    }
    sleep(10);
}

int main()
{
    //先进行构建控制结构,父进程写入,子进程读取
    vector<EndPoint> end_points;
    creatProcesses(&end_points);
    ctrlProcess(end_points);

    //处理退出问题
    WaitProcess(end_points);

    return 0;
}

其中, WaitProcess 函数内,退出进程的循环要在 vector 中从后向前遍历:

 这是因为我们使用的进程退出方式,是通过关闭父进程写端的文件对象,让OS杀死子进程实现的。而在我们创建子进程与管道的 creatProcesses 函数中,是通过循环一个一个创建的:

 这种创建方式在创建第一组管道与子进程时不会有任何问题。但是在之后创建第二、第三组乃至更多时,由于它们的 task_struct 也是父进程的拷贝,就会导致后面子进程的文件描述符表里保留指向前几个管道的指针:

 如果是从前向后依次关闭父进程的写端,那么因为第一个子进程对应的管道的写端不只有父进程一个,还有其他两个子进程,因此第一个子进程就不会被OS杀死,从而在下面执行 waitpid 函数时造成堵塞。

为了真正构建每一个管道的写端都只有父进程一个的构造,则可以改写 creatProcesses 函数的代码:

 在创建时就直接关闭子进程对应的写端文件。

2、命名管道

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

命名管道的打开规则:

  • 如果当前打开操作是为读而打开FIFO时
    O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
    O_NONBLOCK enable:立刻返回成功
  • 如果当前打开操作是为写而打开FIFO时
    O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
    O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

创建命名管道指令:

mkfifo [OPTION]... NAME...

具体用法如下: 

 

 可以看到 fifo 是管道文件。

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

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

  命名管道虽然有自己的 inode ,存在于磁盘上,但也仅仅只有 inode ,代表它存在,而没有自己的 datablock 。与匿名管道相同,命名管道也是内存级的文件,不存在刷盘操作。

 我们通过创建命名管道,也可以让不同的进程通过文件路径 + 文件名找到同一个文件,并打开它,让不同的进程看到同一份资源,具备了进程间通信的前提。

2.2、代码实现命名管道通信

创建命名管道文件的系统调用:

int mkfifo(const char *pathname, mode_t mode);

 mkfifo 函数的参数列表中, pathname 为创建文件的路径与文件名,如果不指定路径,默认为当前路径。 mode 为创建文件的权限。创建成功就返回 0 。创建失败返回 -1,并且错误码会被设置。

具体代码如下:

//makefile

.PHONY:all
all:server client

server:server.cc 
	g++ -o $@ $^ -std=c++11
client:client.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f server client


//comm.hpp

#pragma once
#include <iostream>
#include <string>

#define NUM 1024

const std::string fifoname = "./fifo";
uint32_t mode = 0666;


//server.cc

#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;

int main()
{
    //创建一个管道文件
    umask(0); //这个设置不会影响系统默认设置,只会影响当前进程
    int n = mkfifo(fifoname.c_str(), mode);
    if(n != 0)
    {
        cout << errno << " : " << strerror(errno) << endl;
        return 1;
    }

    //让服务器开启管道文件
    int rfd = open(fifoname.c_str(), O_RDONLY);
    if(rfd <= 0)
    {
        cout << errno << " : " << strerror(errno) << endl;
        return 2;
    }

    //正常通信
    char buffer[NUM];
    while(1)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            cout << "client: " << buffer << endl;
        }
        else if(n == 0)
        {
            cout << "client quit, me too" << endl;
            break;
        }
        else
        {
            cout << errno << " : " << strerror(errno) << endl;
            break;
        }
    }

    //关闭文件
    close(rfd);

    unlink(fifoname.c_str()); //把管道文件删掉

    return 0;
}


//client.cc

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;

int main()
{
    //不需要创建管道文件,只需要打开就可以了
    int wfd = open(fifoname.c_str(), O_WRONLY);
    if(wfd < 0)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        return 1;
    }

    //正常通信
    char buffer[NUM];
    while(1)
    {
        cout << "输入数据:";
        char* msg = fgets(buffer, sizeof(buffer), stdin); //不用 -1
        assert(msg);
        (void)msg;

        if(strcasecmp(buffer, "quit") == 0) break;

        ssize_t n = write(wfd, buffer, strlen(buffer)); //不用 +1
        assert(n > 0);
        (void)n;
    }


    return 0;
}

运行观察现象:

 与匿名管道不同,匿名管道在创建时,默认读写端都是打开的。而命名管道在打开文件阶段会被卡住,只有当我们把读端和写端都手动打开后,程序才能继续向下运行。

 之所以在 server 端读取的数据中间有一行空行,是因为发送的时候会按下回车键,回车键也会被读取。为了解决这个问题,可以做如下更改:

 观察结果:


关于管道的内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!

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

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

相关文章

python函数的递归调用

引入 函数既可以嵌套定义也可以嵌套调用。嵌套定义指的是在定义一个函数时在该函数内部定义另一个函数&#xff1b;嵌套调用指的是在调用一个函数的过程中函数内部有调用另一个函数。而函数的递归调用指的是在调用一个函数的过程中又直接或者间接的调用该函数本身。 函数递归…

PySpark基础入门(3):RDD持久化

RDD的持久化 RDD 的数据是过程数据&#xff0c;因此需要持久化存储&#xff1b; RDD之间进行相互迭代的计算&#xff0c;新的RDD的生成代表着旧的RDD的消失&#xff1b;这样的特性可以最大化地利用资源&#xff0c;老旧地RDD可以及时地从内存中清理&#xff0c;从而给后续地计…

aop切面调用失效问题排查

应用里有较多的地方访问外部供应商接口&#xff0c;由于公网网络不稳定或者外部接口不稳定&#xff08;重启&#xff0c;发版&#xff0c;ip切换&#xff09;的原因&#xff0c;经常报502或者504错误。为了解决HTTP调用的500报错&#xff0c;选择使用spring的retryable注解进行…

Pyinstaller将python文件打包成exe程序——封装LoFTR开源匹配代码

Pyinstaller将python文件打包成exe程序——封装LoFTR开源匹配代码 1.LoFTR代码下载及环境搭建 源码下载&#xff1a;https://github.com/bodhisatan/LoFTR-Stitch 环境搭建&#xff1a;按照github项目中的readme文档进行搭建即可&#xff0c;几乎没有遇到问题&#xff0c;代码…

【Unity入门】22.动态创建实例

【Unity入门】动态创建实例 大家好&#xff0c;我是Lampard~~ 欢迎来到Unity入门系列博客&#xff0c;所学知识来自B站阿发老师~感谢 &#xff08;一&#xff09;脚本实例化预制体对象 &#xff08;1&#xff09;Instantiate克隆创建对象 昨天我们学习了预制体这个概念&#…

文献阅读(50)—— Transformer 用于肺癌诊断预测

文献阅读&#xff08;50&#xff09;—— Transformer 用于肺癌诊断预测 文章目录 文献阅读&#xff08;50&#xff09;—— Transformer 用于肺癌诊断预测先验知识/知识拓展文章结构背景文章方法1. 文章核心网络结构2. Time Encoding ViT &#xff08;TeViT&#xff09;3. Tim…

力扣刷题2023-05-04-1——题目:2614. 对角线上的质数

题目&#xff1a; 给你一个下标从 0 开始的二维整数数组 nums 。 返回位于 nums 至少一条 对角线 上的最大 质数 。如果任一对角线上均不存在质数&#xff0c;返回 0 。 注意&#xff1a; 如果某个整数大于 1 &#xff0c;且不存在除 1 和自身之外的正整数因子&#xff0c;…

Leetcode——66. 加一

&#x1f4af;&#x1f4af;欢迎来到的热爱编程的小K的Leetcode的刷题专栏 文章目录 1、题目2、暴力模拟(自己的第一想法)3、官方题解 1、题目 给定一个由 整数 组成的 非空 数组所表示的非负整数&#xff0c;在该数的基础上加一。最高位数字存放在数组的首位&#xff0c; 数组…

不同主题增删改查系统【控制台+MySQL】(Java课设)

有很多顾客都是只要实现各种各样的增删改查系统即可&#xff0c;只是主题和数据库表不一样&#xff0c;功能都是增删改查这四个功能&#xff0c;做出来的效果和下面的截图是一样的&#xff0c;后续这样的增删改查系统的运行效果请参考下面的截图&#xff0c;我就不一一演示了&a…

MATLAB实现工业PCB电路板缺陷识别和检测

PCB&#xff08;PrintedCircuitBoard印刷电路板&#xff09;是电子产品中众多电子元器件的承载体&#xff0c;它为各电子元器件的秩序连接提供了可能&#xff0c;PCB已成为现代电子产品的核心部分。随着现代电子工业迅猛发展&#xff0c;电子技术不断革新&#xff0c;PCB密集度…

【Git】‘git‘ 不是内部或外部命令,也不是可运行的程序

一、问题 我想利用git clone命令从github上下载项目源代码&#xff0c;发现报错&#xff1a; git 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。我用cmd跑一下git命令&#xff0c;发现报错&#xff1a; 二、问题分析 这个错误提示表明您的系统中没有安装…

电脑视频删除了怎么恢复回来?很着急

案例分享&#xff1a;“电脑视频删除了怎么恢复回来&#xff1f;我是一名影楼的摄像师&#xff0c;我的主要工作就是拍摄婚礼视频&#xff0c;最近拍了一场婚礼视频&#xff0c;当时由于相机的内存不足&#xff0c;于是将宣传片等视频都导入进了电脑里面&#xff0c;清空摄像机…

《软件工程教程》(第2版) 主编:吴迪 马宏茹 丁万宁 第八章课后习题参考答案

第八章 面向对象技术与UML 课后习题参考答案 一、单项选择题 D &#xff08;2&#xff09;C &#xff08;3&#xff09;B &#xff08;4&#xff09;D &#xff08;5&#xff09;C &#xff08;6&#xff09;B &#xff08;7&#xff09;A &#xff08;8&#xff09;C&…

2023华中杯数学建模C题完整模型代码

已完成全部模型代码&#xff0c;文末获取。 摘要 随着工业化和城市化的快速发展&#xff0c;空气污染已经成为全球性的环境问题。细颗粒物&#xff08;PM2.5&#xff09;等污染物对人类健康、生态环境和社会经济造成了严重影响。本研究旨在深入探究影响PM2.5浓度的主要因素&a…

ESP32(二):GPIO

一.创建例程 打开命令面板&#xff1a;ctrlshiftp&#xff0c;输入&#xff1a;esp-idf:example&#xff1b;选择hello_world工程&#xff0c;点击 Create project using example hello_world&#xff0c;选择保存工程&#xff1b;工具使用代码&#xff1a; #include <stdi…

【图像分割】视觉大模型SEEM(Segment Everything Everywhere All at Once)原理解读

文章目录 摘要&#xff08;效果&#xff09;二、前言三、相关工作四、method4.1 多用途4.2 组合性4.3 交互式。4.4 语义感知 五、实验 论文地址&#xff1a;https://arxiv.org/abs/2304.06718 测试代码&#xff1a;https://github.com/UX-Decoder/Segment-Everything-Everywher…

【Python】【进阶篇】14、Django创建第一个项目

目录 Django创建第一个项目1. 第一个项目BookStore1) BookStore项目创建 2. Django项目配置文件1) manage.py文件2) __init__.py文件3) settings.py文件4) urls.py文件5) wsgi.py文件 Django创建第一个项目 在上一章中&#xff0c;我们完成了开发环境的搭建工作。 本章我们将学…

NLP实战:基于Pytorch的文本分类入门实战

目录 一、前期准备 1.环境准备 2.加载数据 二、代码实战 1.构建词典 2.生成数据批次和迭代器 3. 定义模型 4. 定义实例 5.定义训练函数与评估函数 6.拆分数据集并运行模型 三、使用测试数据集评估模型 四、总结 这是一个使用PyTorch实现的简单文本分类实战案例。在…

【Java】内部类Object类

目录 1.内部类 1.1实例内部类 1.2静态内部类 1.3局部内部类 1.4匿名内部类 2.Object类 2.1getClass方法 2.2equals方法 2.3hashcode方法 1.内部类 定义&#xff1a;一个类定义在另一个类或一个方法的内部&#xff0c;前者称为内部类&#xff0c;后者称为外部类。 分…

spring常用的事务传播行为

事务传播行为介绍 Spring中的7个事务传播行为: 事务行为 说明 PROPAGATION_REQUIRED 支持当前事务&#xff0c;假设当前没有事务。就新建一个事务 PROPAGATION_SUPPORTS 支持当前事务&#xff0c;假设当前没有事务&#xff0c;就以非事务方式运行 PROPAGATION_MANDATORY…