【Linux】匿名管道代码实现-mypipe

news2024/10/2 22:23:52

文章目录

  • 管道介绍
    • 什么是管道:
    • 管道的原理
    • 管道的特点
  • 具体代码详写
    • 创建初始文件
    • makefile编写
    • 定义任务列表-task.hpp
      • 分阶段代码编写
      • 总代码展示:
    • ctrlProcess.cc 编写
      • 头文件包含(如有不会,自己查谷歌)
      • 定义全局变量以解耦
      • main,函数框架
      • EndPoint定义
      • creatProcess 创建管道
      • WaitCommand-子进程开始读取
      • ctrlProcess 开始指点天下
        • 写个小菜单,顺便接收command
      • waitProcess - 最后一步
      • 最最最后一步,改一个小bug(creatProcess中子进程继承父进程文件描述符问题)
      • 总代码展示
  • 总结

管道介绍

什么是管道:

  • 管道是unix中最古老的进程间通信的形式
  • 我们把从一个进程连接到另一个进程的数据流的叫做管道
    在这里插入图片描述
    管道也是一种文件

管道的原理

前提知识: 创建子进程的时候,fork子进程,只会复制进程相关的数据结构对象,不会复制父进程曾经打开的文件对象!
这就是为什么fork之后,父子进程cout,printf都会向同一个显示器终端打印数据的原因

我们在代码中需要做的,其实就是让父子进程看到同一份文件,对于父进程而言,只写该文件,对于子进程而言,只读该文件,即可进行进程间的通信

管道的特点

  1. 单向通信–>半双工的一种特殊情况(类似于对讲机)–>吵架是"全双工"
  2. 管道的本质是文件,因为fd的声明周期是随进程的,因此管道的生命周期也是随进程的
  3. 管道通信,通常用来进行具有"血缘"关系的进程,进行进程间的通信,常用于父子通信–pipe打开管道,并不清楚管道的名字,我们把这个管道称为匿名管道
  4. 在管道通信中,写入的次数和读取的次数不是严格匹配的–>读写的次数多少没有强相关–>字节流
    所以管道叫"面向字节流"
  5. 具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信–自带同步机制

四种场景:

  1. 如果我们read读取完毕了所有的管道数据,如果对方不发,我们就只能等待
  2. 如果我们write端将管道写满了,我们还能写吗?不能
  3. 如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾
  4. 写端一直写,读端关闭,会发生什么呢?此时再写也没有意义了,OS不会维护无意义低效率的,或者浪费资源的事情.OS会杀死一直在写入的进程,OS通过信号来终止进程,13)SIGPIPE

具体代码详写

创建初始文件

mkdir unnamedpipe //创建一个新的文件夹
cd unnamedpipe //进入该文件夹
touch makefile //编写makefile
touch mypipe.cc //主要程序
touch task.hpp //任务列表文件

以上文件具体内容及各自的作用将在下文中详细叙述.

makefile编写

我们是在linux上进行编程,而不是VS之类的集成开发环境,因此,写一个makefile方便我们调试代码

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

.PHONY:clean
clean:
	rm -f mypipe

定义任务列表-task.hpp

分阶段代码编写

对于这个操作,其实我们在这个阶段已经做了很多次了,无非就是将不同的任务做成不同的函数,并且封装成函数指针数组,并在细节处不断解耦,已达到低耦合,高内聚的特点.
首先,要使用函数指针数组,我们得先重命名(也叫声明)函数指针类型:

typedef void(*fun_t)();
//参数列表为空,返回值为void函数类型,重命名为fun_t

然后我们就可以肆无忌惮地写一些函数(没有具体实现,仅仅是为了测试用):
我们假设有以下三个任务可供用户选择:

#include<iostream>
using namespace std;
//getpid的头文件:
#include<unistd.h>
void PrintLog()
{
    cout << getpid() << ":PrintLog..." << endl;
}
void InsertMySQL()
{
    cout << getpid() << " InsertMySQL..." << endl;
}
void NetRequest()
{
    cout << getpid() << " NetRequest..." << endl;
}

同时,为了更加解耦,更加方便我们用户操作,我们将上述操作宏定义为数字:
并且,我们约定每一个command都应该是四字节发送,接收端四字节接收.

//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define INSERTMYSQL 1
#define NETREQUEST 2

我们在创建好三个函数之后,需要"组织"和"管理"好这些数据:

class Task
{
public:
    Task(){
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    void Excute(int command)//成员函数,执行某个命令
    {
        if(command >= 0 && command < funcs.size())
        {
            funcs[command]();
        }
    }
public:
    vector<fun_t> funcs;//定义一个函数指针数组
};

至此,我们的task.hpp便创建好了,专门用来储存任务的数据结构.

总代码展示:

#pragma once
#include<iostream>
#include<functional>
#include<vector>
#include<unistd.h>
#define COMMAND_LOG 0
#define INSERTMYSQL 1
#define NETREQUEST 2
using namespace std;
typedef void(*fun_t)();//函数指针

void PrintLog()
{
    cout << getpid() << ":PrintLog..." << endl;
}

void InsertMySQL()
{
    cout << getpid() << " InsertMySQL..." << endl;
}

void NetRequest()
{
    cout << getpid() << " NetRequest..." << endl;
}

class Task
{
public:
    Task(){
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    void Excute(int command)
    {
        if(command >= 0 && command < funcs.size())
        {
            funcs[command]();
        }
    }
public:
    vector<fun_t> funcs;
};

ctrlProcess.cc 编写

头文件包含(如有不会,自己查谷歌)

这个文件我们主要用于编写主要控制程序
我们先把所有需要的头文件包含了:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
//记得包含这个任务列表头文件
#include "Task.hpp"
using namespace std;

定义全局变量以解耦

为了方便之后的修改,我们设置一个全局变量来保存管道个数,以及将一个Task对象实例化

const int gnum = 0;
Task t;

main,函数框架

整个程序,主要分为三个步骤:

  1. 初始化:构建控制结构并且创建管道
  2. 开启控制进程
  3. 回收子进程(退出整个系统)
int main()
{
	//1.先进行构建控制结构,父进程写入,子进程读取
	vector<EndPoint> end_points;//先描述,再组织,需要我们单独再定义EndPoint的结构
	creatProcess(&end_points);//为end_points向量创建管道
	//此时我们得到了一堆管道和多个正在等待的读取命令的子进程
	
	//2.开启控制进程函数
	ctrlProcess(end_points);

	//3.此时整个系统都退出了
	waitProcess(end_points);
	return 0;
}

EndPoint定义

class EndPoint
{
    //父进程要对他所创建的管道进行管理->先描述再组织
private:
    static int number;//在类中声明,类外定义
    
public:
    pid_t _child_id;//子进程id
    int _write_fd;//写端文件描述符
    std::string processname;//该进程的名字
    
public:
    EndPoint(int id,int fd):_child_id(id),_write_fd(fd)
    {
        //process-0[pid:fd]
        char namebuff[64];
        snprintf(namebuff,sizeof(namebuff),"process-%d[%d:%d]",number++,_child_id,_write_fd);
        //每写一个记得将number++
        processname = namebuff;
    }
    
    string name() const
    {
        return processname;//以这种方式获得该进程的名字更加好
    }


    ~EndPoint()
    {}
};

int EndPoint::number = 0;//静态成员变量必须在类外定义,在类里面声明

creatProcess 创建管道

void creatProcess(vector<EndPoint> *end_points)
{
    for(int i = 0; i < gnum; i++)//需要创建gnum个管道
    {
        int pipefd[2] = {0};//pipe函数返回值是一个数组,含有两个元素,其中fd[0]是读端,fd[1]是写端
        int n = pipe(pipefd);//这里接收返回值仅仅是为了检查合法性
        assert(n == 0);
        (void)n;//为了使用一下n,否则编译器会以为这是定义了但是没有使用的冗余变量
        pid_t id = fork();//创建子进程
        assert(id != -1);
        
        //子进程
        if(id == 0)
        {
            close(pipefd[1]);//关闭写端,只留下读端
            
            //我们期望,所有的子进程读取指令的时候,都从标准输入读取->输入重定向
            dup2(pipefd[0],0);//将屏幕输入重定向到pipefd[0](即读端)
            
            //->子进程开始等待获取命令
            WaitCommand();

			//->命令完成以后关闭读端
            close(pipefd[0]);
            exit(0);
        }
        
        //父进程
        close(pipefd[0]);

        //将新的子进程和他的管道写端,构建对象,插入到管理列表中
        (*end_points).push_back(EndPoint(id,pipefd[1]));
    }
}

WaitCommand-子进程开始读取

void WaitCommand()
{
	while(true)
	{
		int command = 0;
		int n = read(0,&command,sizeof(int));//以四字节为单位读取,read返回的是读到的数据字节数大小
		if(n == sizeof(int))//如果得到的是一个合法的命令-即四字节数
		{
			t.Excute(command);//->调用命令
		}
		else if(n == 0)
		{
			//此时已经读取完毕,即父进程不再发送,管道可以关闭了
			cout << "父进程让我退出: " << getpid() << endl;
			break;
		}
		else break;
	}
}

到此为止,我们现在已经得到了gnum个管道以及对应数量的子进程,正在嗷嗷待哺.
我们也通过pushback将他们的数据结构管理进了end_points,到时候我们只需要用下标访问end_points就能通过不同的管道实现父子进程的通信了

ctrlProcess 开始指点天下

我们再回顾一下,我们已经走了哪些路了:
在这里插入图片描述
我们现在开始真正控制进程进行管道通信了.

void ctrlProcess(const vector<EndPoint> & end_points)
{
	int num = 0;
	int cnt = 0;
	while(true)
	{
		cout << endl;
		int command = ShowBoard();//创建一个菜单
		if(command == 3) break;//令3命令为退出子进程
		if(command < 0 || command > 2) continue;//合法性检查

		int index = cnt++;//循环进程进行运行(从0-2进程)
		cnt %= end_points.size();
		cout << "选择了进程" << end_points[index].name() << "处理任务" << endl;
		write(end_points[index]._write_fd,&command,sizeof(command));
	}
}

写个小菜单,顺便接收command

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

waitProcess - 最后一步

void waitProcess(const vector<EndPoint> & end_points)
{
    //1.我们需要让子进程全部退出 -- 只需要让父进程关闭所有的write_fd就可以了
    for(const auto &ep : end_points)
    {
        close(ep._write_fd);
    }
    cout << "父进程让所有子进程全部退出" << endl;
    sleep(10);
    //2.父进程要回收子进程的僵尸状态
    for(const auto &ep : end_points)
    {
        waitpid(ep._child_id,nullptr,0);
    }
    cout << "父进程让所有子进程全部退出" << endl;
    sleep(10);
}

这里我们是先关闭所有的读端之后再统一回收进程,就完美避免了我们下一步即将修改的小bug.
如果我们是用下面这种写法:

 for(int end = 0; end < end_points.size(); end++)
    {
        std::cout << "父进程让子进程退出:" << end_points[end]._child_id << std::endl;
        close(end_points[end]._write_fd);

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

这样的话我们会只能关闭第一个子进程,而不能关闭后面打开的几个子进程.

最最最后一步,改一个小bug(creatProcess中子进程继承父进程文件描述符问题)

在这里插入图片描述
这里有个极不容易发现的问题:
当我们创建第一个进程的时候,没有毛病
但是当我们创建后面的进程的时候,子进程会继承父进程的写端文件!
举个例子

子进程1:打开了fd[0] -> 此时父进程有子进程1的写端
子进程2:打开了fd[0] -> 此时还继承了子进程1的写端!

从以往后,每次创建子进程都会继承前面那个进程的写端.在我们关闭的时候,如果只把父进程对应的写端关闭了,还会有子进程没有关闭这个写端,仍然无法关闭我们想要关的那个子进程.
所以,我们在每次创建子进程的时候要记得把相应的写端关闭了!
具体写法如下:

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

        // 1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        // 一定是子进程
        if (id == 0)
        {
            for(auto &fd : fds) close(fd);
                        
            close(pipefd[1]);
            dup2(pipefd[0], 0);
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }

        // 一定是父进程
        close(pipefd[0]);
        end_points->push_back(EndPoint(id, pipefd[1]));
        fds.push_back(pipefd[1]);
    }
}

总代码展示

#include<iostream>
#include<string>
#include<unistd.h>
#include<cassert>
#include<vector>
#include "task.hpp"
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
const int gnum = 3;

class EndPoint
{
    //父进程要对他所创建的管道进行管理->先描述再组织
private:
    static int number;
public:
    pid_t _child_id;
    int _write_fd;
    std::string processname;
public:
    EndPoint(int id,int fd):_child_id(id),_write_fd(fd)
    {
        //process-0[pid:fd]
        char namebuff[64];
        snprintf(namebuff,sizeof(namebuff),"process-%d[%d:%d]",number++,_child_id,_write_fd);
        processname = namebuff;
    }
    string name() const
    {
        return processname;
    }
    ~EndPoint()
    {}

};

int EndPoint::number = 0;

Task t;

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

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

        // 1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        // 一定是子进程
        if (id == 0)
        {
            for(auto &fd : fds) close(fd);
            // 1.3 关闭不要的fd
            close(pipefd[1]);
            // 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
            // 1.3.1 输入重定向,可以不做
            dup2(pipefd[0], 0);
            // 1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }

        // 一定是父进程
        //  1.3 关闭不要的fd
        close(pipefd[0]);

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

        fds.push_back(pipefd[1]);
    }
}

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

void waitProcess(const vector<EndPoint> & end_points)
{
    //1.我们需要让子进程全部退出 -- 只需要让父进程关闭所有的write_fd就可以了
    for(const auto &ep : end_points)
    {
        close(ep._write_fd);
    }
    cout << "父进程让所有子进程全部退出" << endl;
    sleep(10);
    //2.父进程要回收子进程的僵尸状态
    for(const auto &ep : end_points)
    {
        waitpid(ep._child_id,nullptr,0);
    }
    cout << "父进程让所有子进程全部退出" << endl;
    sleep(10);
}

void ctrlProcess(const vector<EndPoint> & end_points)
{
    //我们可以写成自动化的,也可以搞成交互式的
    int num = 0;
    int cnt = 0;
    // while(true)
    // {
    //     srand((unsigned)time(NULL));
    //     //1.选择任务
    //     int command = INSERTMYSQL;
    //     //2.选择进程
    //     int index = rand() % end_points.size();
    //     //3.下发任务
    //     write(end_points[index]._write_fd,&command,sizeof(command));
    //     sleep(1);
    // }
    while(true)
    {
        cout << endl;
        int command = ShowBoard();
        if(command < 0 || command > 2) continue;
        if(command == 3) break;
        int index = cnt++;
        cnt %= end_points.size();

        cout << "选择了进程" << end_points[index].name() << "处理任务" << endl;
 
        write(end_points[index]._write_fd,&command,sizeof(command));
    }
}
int main()
{
    //1.构建控制结构,父进程写,子进程读
    vector<EndPoint> end_points;

    //2.此时我们得到了一批end_points结构
    creatProcess(&end_points);
    ctrlProcess(end_points);

    //3.此时整个系统都退出了
    waitProcess(end_points);
    return 0;
}

总结

以上,我们写的都是匿名管道,并且只能在两个有亲缘关系的进程中进行通信.
最后再复习一下管道的五种特点及四种场景:

  1. 单向通信–>半双工的一种特殊情况(类似于对讲机)–>吵架是"全双工"
  2. 管道的本质是文件,因为fd的声明周期是随进程的,因此管道的生命周期也是随进程的
  3. 管道通信,通常用来进行具有"血缘"关系的进程,进行进程间的通信,常用于父子通信–pipe打开管道,并不清楚管道的名字,我们把这个管道称为匿名管道
  4. 在管道通信中,写入的次数和读取的次数不是严格匹配的–>读写的次数多少没有强相关–>字节流
    所以管道叫"面向字节流"
  5. 具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信–自带同步机制

四种场景:

  1. 如果我们read读取完毕了所有的管道数据,如果对方不发,我们就只能等待
  2. 如果我们write端将管道写满了,我们还能写吗?不能
  3. 如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾
  4. 写端一直写,读端关闭,会发生什么呢?此时再写也没有意义了,OS不会维护无意义低效率的,或者浪费资源的事情.OS会杀死一直在写入的进程,OS通过信号来终止进程,13)SIGPIPE

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

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

相关文章

Apollo配置中心使用篇

Apollo配置中心使用篇 常见配置中心对比Apollo核心概念Apollo核心特性Apollo架构设计各模块介绍服务端设计客户端设计Apollo与Spring集成的底层原理 Apollo安装安装apollo-portalconfig service和admin service部署多网卡问题解决修改Portal环境配置调整ApolloPortal配置 Apoll…

【产品设计】用户操作日志

日志记录了代码的执行过程&#xff0c;根据目的不同&#xff0c;可以分为系统日志和操作日志。 一、什么是日志 日志记录了代码的执行过程。根据目的不同&#xff0c;可分为系统日志和操作日志。 1&#xff09;系统日志 记录系统中硬件、软件和系统问题的信息&#xff0c;同…

C#基础学习--枚举器和迭代器

目录 枚举器和可枚举类型 IEnumerator 接口 IEnumerable 接口 实现 IEnumerable 和 IEnumerator的示例 泛型枚举接口 迭代器 迭代器块 使用迭代器来创建枚举器 使用迭代器来创建可枚举类型 常见迭代器模式 产生多个可枚举类型 将迭代器作为属性 迭代器实质 枚举器和可…

【分享】比ChatGPT还厉害?可以自主解决复杂任务的Auto-GPT迅速走红(内含体验地址)

哈喽&#xff0c;大家好&#xff0c;我是木易巷~ 最近木易巷在了解Auto GPT&#xff0c;今天给大家分享一下~ 自主解决复杂任务的Auto-GPT 什么是Auto-GPT&#xff1f; Auto-GPT 是一款开源 Python 应用程序&#xff0c;由开发者用户 Significant Gravitas 于 2023 年 3 月 30…

钉钉接入“通义千问”大模型,输入斜杠“/”唤起智能服务

4月18日&#xff0c;钉钉总裁叶军在2023春季钉峰会上宣布&#xff0c;钉钉正式接入阿里巴巴“通义千问”大模型&#xff0c;输入“&#xff0f;”在钉钉即可唤起 10 余项 AI 能力&#xff0c;叶军现场演示了群聊、文档、视频会议及应用开发四个场景。 现场展示中&#xff0c;只…

C++:智能指针(auto_ptr/unique_ptr/shared_ptr/weak_ptr)

为什么需要智能指针&#xff1f; C没有垃圾回收机制。 #include<iostream> using namespace std;int div() {int a, b;cin >> a >> b;if (b 0)throw invalid_argument("除0错误");return a / b; }void Func() {// 1、如果p1这里new 抛异常会如何…

网络原理数据链路层

嘿嘿,又见面了,今天为大家带来数据链路层的相关知识.这个层面的知识离咱们程序员太遥远了,我们简单介绍一下就行 1.以太网 2.认识Mac地址 3.区分Mac地址和IP地址 4.MTU 5.DNS 1.以太网 以太网是数据链路层和物理层的使用的网络,物理层用的不咋多,我们就先不讲了,直接看数…

论文阅读 - Segment Anything

文章目录 0 前言1 预备知识1.1 深度学习训练框架1.2 语义分割训练框架 2 SAM的任务3 SAM的模型3.1 模型整体结构3.2 Image encoder3.3 Prompt encoder3.4 Mask decoder3.5 训练细节 4 SAM的数据4.1 模型辅助的手动标注阶段4.2 半自动阶段4.3 全自动阶段 5 SAM的应用5.1 拿来主义…

什么是感知机——图文并茂,由浅入深

什么是感知机——图文并茂&#xff0c;由浅入深 文章目录 什么是感知机——图文并茂&#xff0c;由浅入深引言感知机的引入宝宝版青年版老夫聊发少年狂版激活函数 感知机的应用与门或门 感知机与深度学习感知机与神经网络感知机和深度学习什么关系呢&#xff1f; 引言 生活中常…

【4月比赛合集】19场可报名的「创新应用」和「程序设计」大奖赛,任君挑选!

CompHub 实时聚合多平台的数据类(Kaggle、天池…)和OJ类(Leetcode、牛客…&#xff09;比赛。本账号同时会推送最新的比赛消息&#xff0c;欢迎关注&#xff01; 更多比赛信息见 CompHub主页 或 点击文末阅读原文 以下信息仅供参考&#xff0c;以比赛官网为准 目录 创新应用赛&…

【SpringBoot】一:SpringBoot的基础(上)

文章目录 1. 脚手架创建项目1.1使用Spring Initializr1.2 IDEA中使用脚手架创建项目 2. 代码结构2.1 单一结构2.2 多模块2.3 包和主类2.4 pom文件2.4.1 父项目2.4.2 启动器2.4.3 不使用父项目 3. 运行SpringBoot项目 1. 脚手架创建项目 脚手架辅助创建程序的工具&#xff0c;S…

《Java8实战》第12章 新的日期和时间 API

原来的Java的时间类Date、java.util.Calendar类都不太好&#xff0c;以语言无关方式格式化和解析日期或时间的 DateFormat 方法也有线程安全的问题 12.1 LocalDate、LocalTime、LocalDateTime、Instant、Duration 以及 Period 12.1.1 使用 LocalDate 和 LocalTime LocalDate…

Maven的概述

Maven是干什么用的 maven提供了一套标准的项目结构&#xff0c;这样可以让不同编译器所写的代码在任何一个编译器上都可以运行。 maven提供了一套标准化的构建流程 编译&#xff0c;测试&#xff0c;打包&#xff0c;发布->maven提供了简单的命令可以完成这些操作&#xf…

1秒解决notion客户端所有问题-历史上最简单

1 前言 你是否安装了enhancer后&#xff0c;notion打不开&#xff0c;一直报错&#xff1f;你是否为实现notion客户端汉化和大纲的各种操作而各种苦恼&#xff1f;你是否不习惯使用网页的开始&#xff0c;很想有一个客户端的notion&#xff01; 全部解决&#xff01; 2 网页…

如何理解线程池

线程池的核心状态 核心状态说明 在线程池的核心类ThreadPoolExecutor中&#xff0c;定义了几个线程池在运行过程中的核心状态&#xff0c;源码如下&#xff1a; private static final int COUNT_BITS Integer.SIZE - 3;private static final int CAPACITY (1 << CO…

不良事件报告系统源码,PHP医院安全(不良)事件报告系统源码,在大型医院稳定运行多年

PHP医院安全&#xff08;不良&#xff09;事件报告系统源码&#xff0c;不良事件系统源码&#xff0c;有演示&#xff0c;在大型医院稳定运行多年。 系统技术说明 技术架构&#xff1a;前后端分离&#xff0c;仓储模式 开发语言&#xff1a;PHP 开发工具&#xff1a;VSco…

AE开发20210531之色彩设置、渐变色、符号颜色、属性框内数据操作、另存图层、设计添加属性对话框

笔记 选择ID变化后&#xff0c;清空symbol&#xff0c;添加进新的来&#xff0c;渐变色设置符号颜色对属性框中数据进行操作另存图层&#xff0c;save方法savelayer打开属性对话框自己设计添加属性对话框 课程设计下一节课&#xff0c;图层的渲染 点符号&#xff0c;线符号&…

satoken+ gateway网关统一鉴权 初版

一&#xff1a;感谢大佬 本博客内容 参考了satoken官网实现&#xff0c;satoken官网地址&#xff1a; https://sa-token.cc/doc.html#/micro/gateway-auth 二&#xff1a;项目层级介绍 jinyi-gateway 网关服务jinyi-user-service 用户服务 2.1 jinyi-user-api 2.2 jinyi-use…

Docker 快速上手

目录 一、初始Docker 二、Docker基本操作 1、镜像操作命令 2、容器相关命令 3、数据卷 三、Deckerfile自定义镜像 1、镜像结构 2、自定义镜像 四、DockerCompose 一、初始Docker 镜像(lmage):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起&am…

Docker中配置Mysql主从复制

新建主服务器容器实例3307 进入/mydata/mysql-master/conf目录下新建my.cnf vim my.cnf [mysqld] ## 设置server_id&#xff0c;同一局域网中需要唯一 server_id101 ## 指定不需要同步的数据库名称 binlog-ignore-dbmysql ## 开启二进制日志功能 log-binmall-mysql-bin …