Linux的进程间通信 管道 进程池

news2024/11/18 5:55:36

目录

前言

进程间通信的基本概念

管道

匿名管道

pipe函数

cfc

管道的四种情况

管道的五种特征

进程池

ProcessPool.cpp:

Task.cpp:


前言

ubuntu系统的默认用户名不为root的解决方案(但是不建议):轻量应用服务器 常见问题-文档中心-腾讯云 (tencent.com)

进程间通信的基本概念

进程间通信目的:进程间也是需要协同的,比如数据传输、资源共享、通知事件、进程控制

进程间通信的前提:让不同进程看到同一份OS中的资源(一段内存) 

  1. 一定是某个进程提出了进程间通信的请求,OS才会创建一个共享资源
  2. 为了防止进程在通信时直接访问OS,OS会提供很多系统调用接口
  3. OS创建的共享资源的不同 + OS提供的系统调用接口的不同 = 进程间通信会有不同的种类 

 注意事项:

1、进程间能通信不等于能一直通信,fork函数子进程继承父进程不属于进程间通信

2、进程间通信的成本可能会稍微高一点

进程间通信依赖的标准:system V标准(主要用于本地通信) 和posix标准

system V标准规定的三种进程间通信的方案:消息队列、共享内存、信号量

管道

基本概念:System V 标准中提供了多种 IPC 机制,如消息队列、共享内存和信号量,但是使用这些 IPC 机制需要考虑很多细节问题(例如缓冲区大小、同步与互斥等),并且需要编写复杂的代码来实现,而管道只需一条简单命令即可创建,并且它们支持两个相关联地运行在同一系统上地程序之间互相传输信息(管道是最初人们实现进程间通信的一种方式)

问题:为什么父子进程会向同一个显示器终端打印数据?

解释:进行写操作时,父子进程都会向同一个内核级文件缓冲区中写入(内核级文件缓冲区不属于任何文件)当操作系统定期刷新时会将该缓冲区中的内容刷新到指定的文件中(在这里就是显示器文件)

问题:进程怎么做到默认打开三个标准流0、1、2?

解释:因为所有的进程都是bash的子进程,当bash打开(指向这三个流的文件,具体细节不再描述)了,bash的子进程也就打开了

问题:为什么子进程主动close(0或1或2)不影响父进程继续使用显示器文件呢?

解释:struct file中存在一个内存级的引用计数,父子进程同时指向一个struct file则该引用计数为2,close子进程的某个标准流文件时,只会将该引用计数减一,父进程依然可以访问(file - > ref_count--; if(ref_count == 0)才会释放文件资源)

问题:什么是管道文件?

解释:内核级文件缓冲区(重新设计后的) + struct file,父子进程一个负责向内核级文件缓冲区中写,另一个读取内核级文件缓冲区中的内容就形成了进程间通信的定义(让不同的进程看到同一份OS中的资源,文件系统也属于OS),此外为了保证父子进程间通信的合理性,管道文件只允许单向通信,同时读取会发生数据紊乱

补充:为了提高进程间通信的效率,避免写入文件缓冲区后还要向磁盘文件中刷新,所以OS设计者基于原来内核级文件缓冲区的代码,在OS中重新设计了一个不需要向磁盘中定时刷新的内核级文件缓冲区 

问题:如何实现父进程读文件,子进程写文件?

解释:父进程仍然打开3号文件描述符close(4),子进程仍然打开4号文件描述符close(3)

问题:父子既然要关闭不需要的fd,为何之前还要打开?可以不关闭吗?

解释:①为了让子进程继承,通过继承后不用了再关闭的这种方式形成的进程间通信是由设计者深思熟虑后的结果(父进程只打开一个3后续子进程还要再关闭3再打开4,还不如父子进程都打开3和4按照实际情况再进行关闭,前者也可以但是后者更简单)可以不关闭但是可能会造成父子进程同时写入,所以建议关闭,同时由于存放文件描述符的是一个数组,数组是有大小范围的,所以如果为了保证通信的单向性子进程让某个文件描述符空闲,如果还有其它情况造成的文件描述符在数组中处于空闲状态,就会造成文件描述符泄漏

匿名管道

pipe函数

函数原型:int pipe(int pipefd[2]);

包含头文件:<unistd.h>

参数:输出型参数,是一个由两个整数构成的数组,第一个元素表示读端的文件描述符,第二个元素表示写端的文件描述符,写端和读端的文件描述符由OS自行填写

返回值:调用成功返回0,否则返回-1

功能:在OS中创建一个用于进程间通信的没有名字的内核级文件缓冲区,即匿名管道

注意事项:

1、pipe函数的底层是open函数,只不过这里不需要提供文件路径、文件名以及初始权限

2、如果想要双向通信可以使用两个管道

3、将pipe创建的内核级文件缓冲区叫做管道,是因为它在本质上还是一个内核级文件缓冲区,只不过正常情况下一个进程对文件进行写的时候就是写入内核级文件缓冲区然后由OS负责定时刷新到管道,而新建的缓冲区不会向磁盘中刷新而是刷新给进程,刷新的目的地改变了,只需将原来内核级文件缓冲区的代码稍加更改就可以实现这一功能,并且“一进一出”还符合我们日常生活中对管道的理解

cfc

1、进程进程间通信是有成本的,需要做准备工作:

//2、创建子进程
pid_t id = fork();
if(id == 0)
{
    //子进程---写端
    //3、关闭不需要的fd
    close(pipefd[0]);


    close(pipefd[1]);//完成通信后也将子进程的写端关闭
    exit(0);
}

//父进程---读端
close(pipefd[1]);

close(pipefd[0]); // 完成通信后也将子进程的读端关闭

2、进程间通信:

#include <iostream>
#include <unistd.h>
#include <cerrno>  //c++版本的errno.h
#include <cstring> //c++版本的string.h
#include <sys/wait.h>
#include <sys/types.h>
#include <string>

// 携带发送的信息
std::string getOtherMessage()
{
    // 获取要返回的信息
    static int cnt = 0; // 计数器
    std::string messageid = std::to_string(cnt);
    cnt++;                    // 每使用一次计数器就++
    pid_t self_id = getpid(); // 获取当前进程的pid
    std::string stringpid = std::to_string(self_id);

    std::string message = " my messageid is : ";
    message += messageid;
    message += " my pid is : ";
    message += stringpid; // 逐渐向要传回的string字符串中追加要返回的信息

    return message;
}

// 子进程进行写入
void ChildProcessWrite(int wfd)
{
    std::string message = "father, I am your child process!";
    while (true)
    {
        std::string info = message + getOtherMessage(); // 子进程尝试向父进程传递的所有信息
        write(wfd, info.c_str(), info.size());          // write函数传入的字符串需要是c语言格式的,c_str将string字符串变为c语言格式的字符串
        sleep(1);                                       // 让子进程写慢一点,这样父进程就不会一直读并打印在显示器上
    } // write是由操作系统提供的接口,而操作系统又是C语言编写的,所以后续学习中可能会碰到c语言的接口和c++的接口混合使用的情况
} // info最后有/0但是文件不需要

const int size = 1024; // 定义父进程可以读取的数组大小

// 父进程进行读取
void FatherProcessRead(int rfd)
{
    char inbuffer[size]; // 普通的c99标准不支持变长数组,但是这里使用的是gnb的c99标准,gun的c99标准支持变长数组
    while (true)
    {
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer)); // 因为文件不需要\0,所以读取管道中内容到缓冲区时可以少读取一个并将/0变为0
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << "父进程获取的消息: " << inbuffer << std::endl;
        }
    }
}

int main()
{
    // 1、创建管道
    int pipefd[2];
    int n = pipe(pipefd); // 输出型参数,rfd,wfd
    if (n != 0)
    {
        std::cerr << "errno" << errno << ":" << "errstring" << strerror(errno) << std::endl;
        return 1;
    }

    // pipefd[0]即读端fd,pipefd[1]即写端fd
    std::cout << "pipefd[0] = " << pipefd[0] << ", pipefd[1] = " << pipefd[1] << std::endl;

    sleep(1); // 便于看到管道创建成功

    // 2、创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "子进程关闭不需要的fd,准备发消息了" << std::endl;
        sleep(1); // 便于感受到发消息的过程

        // 子进程---写端
        // 3、关闭不需要的fd
        close(pipefd[0]);

        ChildProcessWrite(pipefd[1]); // 子进程的写函数

        close(pipefd[1]); // 完成通信后也将子进程的写端关闭
        exit(0);
    }
    std::cout << "发进程关闭不需要的fd,准备收消息了" << std::endl;
    sleep(1); // 便于感受到收消息的过程

    // 父进程---读端
    close(pipefd[1]);
    FatherProcessRead(pipefd[0]); // 父进程的读函数
    close(pipefd[0]);             // 完成通信后也将子进程的读端关闭

    pid_t rid = waitpid(id, nullptr, 0);
    if (rid > 0)
    {
        std::cout << "wait child process done" << std::endl;
    }

    return 0;
}

结论:因为可以用write和read读取管道,所以管道也是文件

管道的四种情况

1、如果管道内部为空,不具备读取条件,读进程会被阻塞(wait)等到管道不为空时才会读取

2、管道被写满 && rfd不关闭也不读取:此时管道会被写满,写进程会被阻塞,等到管道不为满时才会继续写入

3、管道一直在读 && wfd关闭:读端read函数的返回值最后为0,表示读取到了文件结尾

4、rfd直接关闭 && 写端一直入:写端进程会被OS直接用13号信号杀掉(OS判断出进程异常)

管道的五种特征

1、对于匿名管道:只能用来进行具有“血缘关系”的进程间的通信,但常用于父子进程间通信

2、管道内部自带进程之间的同步机制(子进程写一条写父进程读一条(但也不绝对),管道在实现时内部做了保护,不会出现多进程同时访问共享资源导致的共享区数据不一致问题)

3、管道文件按的生命周期是随进程的

4、管道文件在通信的时候,是面向字节流的,读写次数不一定是一一匹配的(写十次一次一条,读一次一次读十条,水管一直流,但是可以选用不同的容器去接水)

5、管道的通信模式,是一种特殊的半双工模式(正常的半双工是双方都写入和接收,但同时只能有一个人写入另一个人负责接收,管道是永远只能有一个人进行写入另一个人进行接收)

进程池

产生原因:OS处理任务过多时,频繁的创建和销毁新的进程去执行这些任务会造成极大的资源浪费,OS不会做浪费资源的事情

基本概念:提前创建多个用于执行任务的子进程,当父进程派发任务时子进程去处理父进程的任务,处理完成后继续阻塞等待(这些进程组成了一个类似于“池子”的空间)

ProcessPool.cpp:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"

// 管道
class Channel
{
public:
    Channel(int wfd, pid_t id, const std::string &name)
        : _wfd(wfd), _subprocessid(id), _name(name)
    {
    }

    ~Channel()
    {
    }

    // 获取写端的wfd、目标子进程的pid,当前管道名
    int GetWfd() { return _wfd; }

    pid_t GetProcessID() { return _subprocessid; }

    std::string GetName() { return _name; }

    void CloseChannel() // 关闭连接当前管道的wfd
    {
        std::cout << "关闭当前进程连接到管道的wfd: " << _wfd << std::endl;
        close(_wfd);
    }

    void wait() // 子进程阻塞等待
    {
        pid_t rid = waitpid(_subprocessid, nullptr, 0); // (阻塞等待的子进程pid,指向子进程的退出信息(因为没有写就直接设置为空指针),选择阻塞等待的方式)返回值是阻塞成功的子进程的pid
        if (rid > 0)
        {
            std::cout << "pid = " << rid << " 的子进程变为阻塞等待 " << std::endl; // 打印阻塞成功的子进程的pid
            std::cout << std::endl;
        }
    }

private:
    int _wfd;
    pid_t _subprocessid;
    std::string _name;
};

// 形参命名规范
// const & 修饰的应该是一个输入型参数
//& 修饰的应该是一个输入输出型参数
//* 修饰的应该是一个输出型参数

// 创建管道和进程池
void CreatChannelAndSub(int num, std::vector<Channel> *channels, task_t task) // task_t task是回调函数,当子进程执行fork时会去回调指定好的任务文件中的work函数,实现了任务文件和进程文件间的解耦
{
    for (int i = 0; i < num; i++) // 循环创建子进程和对应的管道
    {
        // 1、创建管道
        int pipefd[2] = {0};  // 存放读写端文件描述符的数组(该数组在每次循环时都重置)
        int n = pipe(pipefd); // 每次循环时都由OS向数组中写入分配给新建管道的读端和写端的文件描述符(OS依据会fd的占用情况分配不同的fd给新的管道)
        if (n < 0)
            exit(1); // 创建管道失败进程退出

        // 2、创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            // 处理第二次创建管道时的子进程中还有指向第一个管道的rfd
            if (!channels->empty()) // 管道数组不为空,即到了第二次创建管道时才会执行该判断语句
            {
                for (auto &channel : *channels) // 循环遍历之前的管道并拿到这些管道的rfd,然后关闭当前进程的这些rfd
                {
                    channel.CloseChannel();
                }
            }
            std::cout << std::endl;

            sleep(5);

            // 子进程
            close(pipefd[1]); // 关闭子进程的wfd
            // work(pipefd[0]);  // 子进程等待并处理父进程派发的任务

            // dup2(pipefd[0],0);//子进程不仅可以从管道中,还可以从标准输入中获取任务码
            // work();//我们不给work传rfd就可以断绝子进程从管道中获取任务码,这样就进一步完成了管道和子进程间逻辑的解耦

            dup2(pipefd[0], 0); // 子进程不仅可以从管道中,还可以从标准输入中获取任务码,这种方法使得子进程可以像处理标准输入一样处理来自管道的数据,从而提高了代码的通用性和可移植性。
            task();             // 将work也视为一个任务

            close(pipefd[0]); // 关闭子进程的rfd
            exit(0);          // 子进程退出
        }

        // 3、父进程构建管道名
        std::string Channel_name = std::to_string(i) + "号 Channel"; // 每次循环i+1,管道名即为i号 Channel

        close(pipefd[0]); // 关闭父进程的rfd

        // 向数组中尾插管道
        channels->push_back(Channel(pipefd[1], id, Channel_name)); // 会向当前管道写入父进程的wfd,当前管道对应的子进程pid,当前管道
    }
}

// 检测进程池和管道是否创建成功
void TestForProcessPoolAndSub(std::vector<Channel> &channels)
{
    std::cout << "=========================================================" << std::endl;
    std::cout << "   管道名    " << "   管道对应的子进程pid   " << "   会向当前管道写入的wfd   " << std::endl;
    for (auto &Channel : channels)
    {
        std::cout << " " << Channel.GetName() << "          " << Channel.GetProcessID() << "                  " << Channel.GetWfd() << std::endl;
    }
    std::cout << "=========================================================" << std::endl;
}

// 获取一个管道下标(利用取模 + static变量 在0~channelnum间循环)(使得每个管道都会被使用到的轮询方案)
int NextChannel(int channelnum)
{
    static int next = 0;
    int index = next;
    next++;
    next %= channelnum;
    return index;
}

// 发送任务
void SendTaskCommand(Channel &channels, int taskcommand) // 此时是向数组中的一个管道派发任务,所以形参应该是一个管道类类型的对象
{
    write(channels.GetWfd(), &taskcommand, sizeof(taskcommand)); // 像指定的wfd中写入(OS会通过该wfd找到对应的管道,这里本质上就是向管道中写入),要写入的任务码,任务码的大小
} // 这里不是要截取任务码所存放地址的前四个字节,而是获取任务码本身,别搞错了

// 向子进程派发一次任务(也叫通过管道控制子进程,因为只要向某一个管道中派发任务后该管道对应的子进程就可以接收到该任务,二者的连接关系提前已经建立好了)
void CtrlProcessOnce(std::vector<Channel> &Channels)
{
    sleep(1);
    // 1、选择一个任务(获取一个任务码,本质是获取一个函数指针)
    int taskcommand = SelectTask();

    // 2、选择一个管道进行任务的派发
    int channel_index = NextChannel(Channels.size());

    // 3、发送任务
    SendTaskCommand(Channels[channel_index], taskcommand); // 向指管道发送任务码,因为管道和子进程建立了关联,向管道中输入内容时子进程在自己的work函数中就会读取到管道中的任务码,然后子进程就会依据该任务码去执行相应的任务

    std::cout << "分配的随机任务码为:" << taskcommand << " 派发给的管道名为: "
              << Channels[channel_index].GetName() << " 处理任务的子进程pid为: " << Channels[channel_index].GetProcessID() << std::endl;
}

// 向子进程派发任务
void CtrlProcess(std::vector<Channel> &Channels, int time = -1) // 默认一直向子进程派发任务
{
    if (time > 0)
    {
        while (time--)
        {
            CtrlProcessOnce(Channels);
        }
    }
    else
    {
        while (true)
        {
            CtrlProcessOnce(Channels);
        }
    }
}

// 回收管道和子进程(释放而不是等待)
void CleanUpChannelAndSubProcess(std::vector<Channel> &Channels)
{
    for (auto &i : Channels)
    {
        i.CloseChannel(); // 先关闭写端wfd
        i.wait();         // 然后让子进程阻塞等待
    }
}

// 创建进有五个进程的进程池,在命令行中的命令行字符串是./processpool 5,一共有命令行字符串的数量应该为2时才能进行创建进程池
int main(int agrc, char *argv[])
{

    if (agrc != 2) // 命令行参数不为2那么就报错并返回
    {
        std::cerr << "Usage: " << argv[0] << " processnum" << std::endl;
        return 1;
    }

    int num = std::stoi(argv[1]); // 将argv数组中获取到的命令行字符串经stoi函数转为整型并赋值给num,num表示要进程池中子进程的个数

    LoadTask(); // 加载任务
    std::cout << "加载任务成功..." << std::endl;

    std::vector<Channel> Channels; // 对管道的处理变成了对数组中Channels对象的增删查改

    // 1、创建管道和进程池
    CreatChannelAndSub(num, &Channels, work); // 规定子进程创价后会回调work函数

    TestForProcessPoolAndSub(Channels); // 检测进程池和管道是否创建成功(到这里所有管道和子进程的连接关系已经建立完成)
    std::cout << "创建并关联子进程与管道成功..." << std::endl;
    std::cout << std::endl;

    // 2、向子进程派发任务
    CtrlProcess(Channels, 4);
    sleep(2);
    std::cout << "子进程处理任务成功..." << std::endl;
    std::cout << std::endl;

    // 3、回收管道和子进程
    CleanUpChannelAndSubProcess(Channels);
    std::cout << "回收管道和子进程成功.." << std::endl;
    std::cout << std::endl;
    return 0;
}

Task.cpp:

/*
 * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
 * @Date: 2024-05-10 21:01:58
 * @LastEditors: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
 * @LastEditTime: 2024-05-12 10:20:05
 * @FilePath: /2024.5.10/Task.hpp
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#pragma once
#include <iostream>
#include <ctime>
#include <cstdlib> //c++风格的c语言的stdlib.h头文件
#include <sys/types.h>
#include <unistd.h>

#define TaskNum 3 // 定义要处理的任务类型个数

typedef void (*task_t)(); // task_t 函数指针类型(task_t是一个类型不是一个指针)

task_t tasks[TaskNum]; // 创建一个task_t函数指针类型的数组tasks,数组中存放的都是函数指针

// 打印任务
void Print()
{
    std::cout << "I am a print task" << std::endl;
}

// 下载任务
void DownLoad()
{
    std::cout << "I am a DownLoad task" << std::endl;
}

// 刷新任务
void Flush()
{
    std::cout << "I am a Flush task" << std::endl;
}

// 加载任务(将任务放入函数指针数组中)
void LoadTask()
{
    srand(time(nullptr) ^ getpid() ^ 17777); // 依据时间戳 与 当前进程的pid亦或的结果使得“种子”更加的随机,当然也可以再亦或上其它内容
    tasks[0] = Print;                        // 第一个函数指针指向打印任务
    tasks[1] = DownLoad;                     // 第二个函数指针指向下载任务
    tasks[2] = Flush;                        // 第三个函数指针指向刷新任务
}

// 执行任务
void ExcuteTask(int number)
{
    if (number < 0 || number > 2)
        return;
    tasks[number](); // 根据传入的任务码确定要调用的函数
}

// 选择任务码
int SelectTask()
{
    return rand() % TaskNum; // 返回随机的任务码
}

// // 版本一:
// //  子进程处理派发的任务(子进程会从依据rfd从管道中拿到任务码)
// void work(int rfd)
// {
//     // 子进程循环等待
//     int i = 1;
//     while (1)
//     {
//         int command = 0;
//         int n = read(rfd, &command, sizeof(command)); // OS会依据rfd帮助子进程获取与它关联的管道中的内容
//         if (n == sizeof(int))
//         {
//             std::cout << "pid = " << getpid() << " 的子进程正在执行任务" << std::endl;
//             ExcuteTask(command); // 依据任务码执行任务
//             std::cout << std::endl;
//         }
//         else if (n == 0) // 读端读取不到内容时结束子进程的work
//         {
//             std::cout << "pid = " << getpid() << " 的子进程读取不到内容了" << std::endl;
//             break;
//         }
//     }
// }

// 版本二:
// 子进程的任务
void work()
{
    // 子进程循环处理任务
    int i = 1;
    while (1)
    {
        int command = 0;
        int n = read(0, &command, sizeof(command)); // OS会依据rfd帮助子进程获取与它关联的管道中的内容
        std::cout << std::endl;
        if (n == sizeof(int))
        {
            std::cout << "pid = " << getpid() << " 的子进程正在执行任务" << std::endl;
            ExcuteTask(command); // 依据任务码执行任务
            std::cout << std::endl;
            sleep(1);
        }
        else if (n == 0) // 读端读取不到内容时结束子进程的work
        {
            std::cout << "pid = " << getpid() << " 的子进程读取不到内容了" << std::endl;
            break;
        }
    }
}
  • 最后每次运行的示意图都会因使用的sellp或者进程的执行顺序等因素产生差异,可以自行尝试修改,作者懒得修改了

注意事项:子进程从管道中拿到的是一个任务码,后根据该任务码去寻找具体任务

这篇文章也很好可惜是Python的:http://t.csdnimg.cn/yerqf

rand、time和srand函数:http://t.csdnimg.cn/BK9g7 

write、dup2和read函数:http://t.csdnimg.cn/a5xdS 

waitpid函数:http://t.csdnimg.cn/pXPTb 

~over~

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

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

相关文章

Fabric实现多GPU运行

官方的将pytorch转换为fabric简单分为五个步骤&#xff1a; 步骤 1&#xff1a; 在训练代码的开头创建 Fabric 对象 from lightning.fabric import Fabricfabric Fabric() 步骤 2&#xff1a; 如果打算使用多个设备&#xff08;例如多 GPU&#xff09;&#xff0c;就调用…

运维别卷系列 - 云原生监控平台 之 02.prometheus exporter 实践

文章目录 [toc]exporter 简介常用的 exporternode-exporter 实践创建 svc创建 daemonsetprometheus 配置服务发现 exporter 简介 随着 Prometheus 的流行&#xff0c;很多系统都已经自带了用于 Prometheus 监控的接口&#xff0c;例如 etcd、Kubernetes、CoreDNS 等&#xff0c…

苹果电脑卡顿反应慢怎么办 苹果电脑卡顿严重解决方法 mac电脑太卡了怎么办

作为Mac用户&#xff0c;你是否正在经历或者曾经遭遇过电脑卡顿、反应慢的困扰&#xff1f;这可能是由于多种原因导致的&#xff0c;包括自启动程序过多、系统与应用未及时更新、内存管理不当等。今天和你一起来探讨下&#xff0c;苹果电脑卡顿反应慢时怎么办。希望能够帮助你解…

记一次跨域问题

线上跨域问题&#xff0c;在自己配置确认没问题下&#xff0c;要及时找运维看看是不是nginx配置问题。 两个方面&#xff1a; 项目代码 nginx配置 SpringBoot 解决跨域问题的 5 种方案&#xff01; SpringBoot解决CORS跨域问题 SpringBoot-实现CORS跨域原理及解决方案

JVM 自定义类加载器

文章目录 1. 为什么要自定义类加载器1.1 隔离加载类1.2 修改类加载的方式1.3 扩展加载源1.4 防止源码泄漏 2. 自定义类加载器应用场景有哪些3. 两种实现方式 自定义类加载器是Java中的一个高级特性&#xff0c;允许您在运行时动态加载类。通过自定义类加载器&#xff0c;您可以…

windows无法启动硬件设备,代码(19)解决办法

遇到一台电脑&#xff0c;摄像头无法使用&#xff0c;查看设备管理器看到设备名前出现感叹号&#xff0c;双击打开看到“由于其配置信息&#xff08;注册表中的&#xff09;不完整或已损坏&#xff0c;windows无法启动硬件设备&#xff0c;代码&#xff08;19&#xff09;”错误…

测试 vs2019 c++ 在 32 位系统和 64 位系统里的 sizeof ( void * )

再看下反汇编&#xff1a; 接着给出 32 位系统的结果&#xff1a; 谢谢阅读

voceChat - 支持独立部署的个人云社交媒体聊天服务(使用场景及体验分享)

序言 在工作室的发展中&#xff0c;我们急需一个更加简单便捷&#xff0c;高效&#xff0c;适用于团队内部交流的组织平台。起因是我们团队一直是直接使用QQ进行活动&#xff0c;发现QQ很多功能不是很方便并且过于臃肿&#xff0c;越来越不契合工作室的生产环境&#xff0c;于…

Power query与Excel的区别,优势?

Power Query是Microsoft Excel的一个强大数据导入、转换和自动化的插件工具&#xff0c;它在Excel 2010之后的版本中被发布出来&#xff0c;随着时间的发展&#xff0c;功能不断增强。 以下是Power Query的一些优势以及它与Excel传统数据处理方式的区别和一些令人印象深刻的功…

网络完全精通版

一、目录结构 1.1目的的特点 windows和linux windows中C、D、E盘&#xff0c;每个都是一个根系统【多跟系统】 linux中只有一个根【单根系统】 1.2各个目录存储的内容 /root&#xff1a;linux中挂管理员用户的家目录 /home&#xff1a;linux中挂存储普通用户的家目录的目…

RS422一主多从MAX3490

RS422一主多从MAX3490 最近项目用到了RS422一主多从&#xff0c;一个主机4个从机。芯片用的MAX3490&#xff0c;几经折腾&#xff0c;最终只能从一拖4改为一拖2。 主机发送端&#xff0c;从机4个接收端都是正常的&#xff0c;没有问题。波形非常完美&#xff0c;没有太大变形 …

el-table组件选中后使用toggleRowSelection无法取消已选中的数据——bug记录-骚操作解决

先说本文重点解决的问题&#xff1a; 存在的问题&#xff1a;当右侧已选中的数据中&#xff0c;删除了左侧其他页面的数据&#xff0c;但是左侧数据切换到其他页面后&#xff0c;左侧还保留选中的状态。 最近在写后台管理系统的时候&#xff0c;遇到一个需求&#xff1a; 左…

纯电动汽车的发展趋势简述

纯电车简介 纯电动汽车是使用电池驱动电动马达而不是传统的内燃机的汽车。它们通常使用电池组储存能量&#xff0c;然后通过电动马达转化为动力来驱动车辆。相比于传统的燃油车&#xff0c;纯电动汽车具有零排放、低噪音、低维护成本等优点&#xff0c;因此在环保和能源效率方…

【知识拓展】大白话说清楚:IP地址、子网掩码、网关、DNS等

前言 工作中常听别人说的本地网络是什么意思&#xff1f;同一网段又是什么意思&#xff1f;它俩有关系吗&#xff1f; 在工作中内经常会遇到相关的网络问题&#xff0c;涉及网络通信中一些常见的词汇&#xff0c;如IP地址、子网掩码、网关和DNS等。具体一点&#xff1a;经常会…

YOLO数据集制作(二)|json文件转txt验证

以下教程用于验证转成YOLO使用的txt格式&#xff0c;适用场景&#xff1a;矩形框&#xff0c;配合json格式文件转成YOLO使用的txt格式脚本使用。 https://blog.csdn.net/StopAndGoyyy/article/details/138681454 使用方式&#xff1a;将img_path和label_path分别填入对应的图…

分布式光伏监控系统功能模块详解

目前&#xff0c;分布式光伏发电系统的总容量比较小&#xff0c;并且光伏电站的功率受外界环境影响容易出现大起大落的现象。这使电压调整变得很困难。光伏电站运行维护人员不足&#xff0c;长时间不保养维护会影响光伏电站的发电效率。针对上述问题&#xff0c;鹧鸪云基于无线…

pip镜像源

1.1 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple 1.2 阿里云 https://mirrors.aliyun.com/pypi/simple/ 1.3 网易 https://mirrors.163.com/pypi/simple/ 1.4 豆瓣 https://pypi.douban.com/simple/ 1.5 百度云 https://mirror.baidu.com/pypi/simple/ 1.6 中科大 ht…

LLM Agent智能体综述(超详细)

前言 &#x1f3c6;&#x1f3c6;&#x1f3c6;在上一篇文章中&#xff0c;我们介绍了如何部署MetaGPT到本地&#xff0c;获取OpenAI API Key并配置其开发环境&#xff0c;并通过一个开发小组的多Agent案例感受了智能体的强大&#xff0c;在本文中&#xff0c;我们将对AI Agent…

Java获取请求参数

1.简单参数接收 前端请求参数与Controller接受变量名一致 如果参数名不一致&#xff0c;接受不成功。 可以用RequestParam指定参数名&#xff0c;可以用username接收&#xff08;不推荐&#xff09;。 required true&#xff0c;表示参数必须传递&#xff0c;如果不传递会报错…

c#多态性的应用

设计一个电脑游戏&#xff0c;游戏中有猪、牛、兔子、青蛙、鸭子等动物&#xff0c;这些动 物都继承于Vertebrata 类&#xff08;脊椎动物类&#xff09;&#xff0c;Vertebrata类有一个抽象方法Display()&#xff0c;每个动 物都从Vertebrata 类那里继承并重写了Display()方法…