【Linux】进程间通信——管道

目录

写在前面的话

什么是进程间通信

为什么要进行进程间通信

进程间通信的本质理解

进程间通信的方式

管道

System V IPC

POSIX IPC

管道

什么是管道

 匿名管道

什么是匿名管道

匿名管道通信的原理

pipe()的使用

匿名管道通信的特点

拓展代码

 命名管道

什么是命名管道

命名管道通信的原理

mkfifo的使用

代码模拟命名管道通信过程


写在前面的话

        本章是首次提出进程间通信的概念,所以会先介绍进程间通信的相关概念,以及整体的框架结构。

        而本文重点是先介绍进程间通信的基本概念,然后重点介绍进程间通信的第一种方式:管道。

什么是进程间通信

        进程间通信(Inter-Process Communication,IPC)是指操作系统或计算机系统中,不同进程之间进行数据交换和通信的机制或技术。由于进程是操作系统中独立运行的程序实例,而进程间通信允许这些独立的进程之间相互协作、共享资源和进行数据交换。

为什么要进行进程间通信

        根据我们前面讲的,进程间是相互独立的,进程具有独立性啊,那通信不就不独立了吗?

    答案是正确的,进程通信的确会破坏进程的完全独立性,因为进程通信的目的是为了实现进程之间的数据共享、同步和协作。通过进程通信,各个进程可以相互交互和共享资源,这意味着它们不再完全独立,而是具有一定的相互依赖性和关联性。

        尽管进程通信破坏了进程的完全独立性,但这种破坏是有意义且必要的。在实际的计算机系统和操作系统中,进程往往需要协同工作、共享资源和交换数据才能完成复杂的任务。进程通信提供了一种机制,使得不同进程之间可以进行必要的协作和交流,并提供了相应的同步和保护机制来确保数据的正确性和一致性。

        所以这是一种权衡和折中的方案,但大部分情况下进程是相互独立的。


综上,进程间通信主要是为了完成下面这些作用:

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

进程间通信的本质理解

        1.我们知道进程具有独立性,是通过虚拟地址空间 + 页表映射的方式来保持独立性的,所以通信起来成本会比较高。

        2.既然通信,那么前提是一定要让不同的进程看到同一块“内存”(特定的结构组织),这块"内存"不能隶属于任何一个进程,而更应该强调共享

进程间通信不是目的,而是手段!


进程间通信的方式

        大体上可以分为3种通信方式:

  • 管道

    • 匿名管道pipe
    • 命名管道
  • System V IPC

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

System V只能用于单机通信(本地通信).

  • POSIX IPC

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

POSIX IPC可以在单机通信的基础上,进行网络通信(远程资源共享)。

        以上所提到的方式,我会在后面的章节逐一讲解,这个是进程间通信的方式.

今天我们将首要讲解进程间通信方式(一)——管道.

管道

什么是管道

        管道(Pipe)是一种进程间通信机制,用于在相关进程之间传输数据。它是一种特殊的文件描述符,它可以连接一个进程的输出(写入端)到另一个进程的输入(读取端),从而使得这两个进程可以通过管道进行数据传输。

        也就是说管道是单向传输的!现实生活中,我们所看听到的天然气管道、石油管道基本上都是单向传输的.

 匿名管道

什么是匿名管道

匿名管道(Anonymous Pipe)是进程间通信的一种机制,用于在具有亲缘关系(例如父子进程)或共享同一终端的兄弟进程之间传输数据。

        匿名管道是一种单向的数据流通道,它可以用于在进程之间传递数据。通常,一个进程作为管道的写入端(称为管道写入端),将数据写入管道;另一个进程作为管道的读取端(称为管道读取端),从管道中读取数据。

        匿名管道的创建是通过系统调用 pipe() 来完成的。pipe的使用后面会讲。

匿名管道通信的原理

        管道通信的背后是进程之间通过管道进行通信。

        我们知道一个进程要运行,首先要加载到内存,然后创建一个task_struct结构体,里面会有一个files_struct结构体,然后这个结构体里又有一个fd_array[]数组,每个元素指向对应的文件struct_file,里面包含了文件内容等.

        此时我们fork之后的子进程会重新创建一份task_struct,内容继承父进程的,此时fd_array[]里的内容也被子进程继承,即父进程打开的文件 子进程也继承了下来。它们指向的文件是 相同的.

        假设父进程3号文件描述符是读取文件的,4号文件描述符也用来写入文件的,子进程继承以后,fd=3也是用来读取文件的,fd=4也是用来写入文件的

         此时我们想让父进程进行写入fd=4,子进程进行读取fd=3,所以父进程就要关闭读端fd=3,子进程关闭写端fd=4。

这样我们就做到了不同的进程看到了同一份资源(通过fork子进程),而且通过文件描述符的方式完成了进程间的单向通信。

综上,管道内部本质大体是如下流程:

        1.父进程分别以读写方式打开一个文件

        2.fork()创建子进程

        3.双方各自关闭不需要的文件描述符

整体图如下:

pipe()的使用

        既然我们知道了思路,那我们可以用代码来使用一下管道。

        首先,父进程如何使用读和写方式分别打开文件呢?

这里使用到了pipe函数,函数用法及原型如下:

        参数pipefd为输出型参数,我们提前在外部定义好数组,然后传入,结果就会保存在这个数组中,分别为pipefd[0],代表的是读端,pipefd[1],代表的是写端.

        第二步我们利用fork创建子进程。

        最后,子进程用来读取文件的内容,并关闭写端pipefd[1],父进程用来写入内容,同时关闭读端。

匿名管道通信的特点

一个小demo如下:

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

int main()
{
    // 1.创建管道
    int pipefd[2] = {0}; // pipefd[0] :读端, pipefd[1] :写端
    int n = pipe(pipefd);
    assert(n != -1);
    #ifdef DEBUG
    #endif
    // cout << pipefd[0] << "  " << pipefd[1] << endl;
    // 2.创建子进程
    pid_t id = fork();
    assert(id != -1);

    if (id == 0)
    {
        // 子进程
        // 3.构建单向通信的信道,父进程写入,子进程读取
        // 3.1关闭子进程不需要的fd
        close(pipefd[1]);
        char buffer[1024];
        while (true)
        {
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                buffer[s] = 0;
                cout << "child get a message [" << getpid() << "] Father# " << buffer << endl;
            }
        }
        exit(0);
    }

    // 父进程
    // 构建单向通信的信道
    // 3.1 关闭父进程不需要的fd
    close(pipefd[0]);
    string message = "我是父进程,我正在给你发消息";
    int count = 0;
    char send_buffer[1024];
    while (true)
    {
        // 3.2 构建一个变化的字符串
        snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d", message.c_str(), getpid(), count++);
        // 3.3 写入
        write(pipefd[1], send_buffer, strlen(send_buffer));
        // 3.4 sleep
        sleep(1);
    }

    pid_t ret = waitpid(id, nullptr, 0);
    assert(ret > 0);
    close(pipefd[1]);
    return 0;
}

然后此时我们编译运行 :

 可以看到,父进程写入的内容,子进程全部读到了。        

这里是静态图,看不出效果,这些信息其实是时隔1秒打印一条的.

        我们看只有子进程在读取,可子进程我们并没有加任何的sleep啊,理论上应该子进程一直打印才对啊。

        显示器是一个文件,而管道也是一个文件,父子进程同时向显示器 写入时,没有说一个进程会等另一个进程,而是各自打印各自的消息,互相干扰,这是缺乏访问控制。而管道文件提供了访问控制,使得子进程读取完得等待父进程写入才能读取。

        这样,如果我们让子进程读取先sleep 10秒,期间父进程每隔1秒写入,等10秒过后,子进程开始读取,但会把 父进程写入10次的文件的内容全部一下读出来。这说明写入次数和读取次数没有直接关系。即管道是面向字节流的,具体怎么读需要定制协议,后面会说,

        这里就针对与管道的特点做一些总结:

  • 管道是用来进行具有血缘关系的进程进行进程间通信 --- 常用于父子间通信
  • 管道具有通过让进程间协同,提供了访问控制
  • 管道提供的是面向字节流式的通信服务 --- 面向字节流 --- 通过定制协议实现
  • 管道是基于文件的,文件的生命周期是随进程的,即管道的生命周期也随进程的!
  • 管道是单向通信的,就是半双工通信的一种特殊方式.

        上面最后一条提到了半双工概念,这里来解释一下:

        半双工通信的双方只能在同一时间点单向的传输数据,即两个参与者不能同时发送和接收数据。在半双工通信中,通信双方必须交替使用共享的通信信道。例如,当一个人在对讲机上说话时,另一个人必须停止接收,然后才能回应。典型的半双工通信方式包括对讲机和卫星电台。

        全双工:全双工通信允许在同一时间点双向地传输数据。这意味着通信的两个参与者能够同时发送和接收数据,而不需要交替使用通信信道。在全双工通信中,通信双方可以同时进行发送和接收操作,彼此之间的数据传输互不干扰。例如,电话通话是一个典型的全双工通信场景,双方可以同时说话和倾听对方的声音。

         顺带总结一下管道的几种情况:

        a.写快,读慢,写满就不能再写了

        b.写慢,读快,管道没有数据时,读必须等待

        这两种是由访问控制提供的.

        c.写关,读继续读,会标识读到了文件结尾

        d.写继续写,读关,OS会终止写进程

拓展代码

        利用匿名管道的方式,创建多个子进程,然后父进程分别派发随机的任务,

        总代码流程是:父进程首先load()加载方法,然后for循环创建多个进程,每次创建完成后,该进程都要与父进程(pipefd[1])建立关联,以方便父进程管理这些子进程。

        其中每个子进程调用 waitCommand函数,会阻塞在read,等待着父进程的写入,然后父进程开始分发任务,当是对应的子进程时,子进程会执行对应的任务,然后继续while循环等待。

共两个文件,第一个文件ProcessPool.cc文件

#include <iostream>
#include <vector>
#include <ctime>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
using namespace std;

#define PROCESS_NUM 5
int waitCommand(int waitFd, bool quit) // 如果对方不发任务就阻塞
{
    uint32_t command = 0;
    ssize_t s = read(waitFd, &command, sizeof(command));
    if (s == 0)
    {
        quit = true;
        return -1;
    }
    assert(s == sizeof(command));

    return command;
}
void sendAndWakeup(pid_t who, int fd, uint32_t command)
{
    write(fd, &command, sizeof(command));
    cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl;
}

int main()
{
    load();
    // pid : pipefd
    vector<pair<pid_t, int>> slots;
    // create multiple child process
    for (int i = 0; i < PROCESS_NUM; i++)
    {
        // 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);

        pid_t id = fork();
        assert(id != -1);
        // 让子进程读取
        if (id == 0)
        {
            // 关闭写端
            close(pipefd[1]);
            // child process
            while (true)
            {
                // 等命令
                bool quit = false;
                int command = waitCommand(pipefd[0], quit); // 如果对方不发任务就阻塞
                if (quit)
                    break;
                // 执行对应的命令
                if (command >= 0 && command < handlerSize())
                {
                    callbacks[command]();
                }
                else
                {
                    cout << "非法 command" << endl;
                }
            }
            exit(1);
        }
        // father process
        close(pipefd[0]);
        slots.push_back(pair<pid_t, int>(id, pipefd[1]));
    }
    // 父进程派发任务
    srand((unsigned long)time(nullptr) ^ getpid() ^ 2311156L);
    while (true)
    {   
        //选择一个任务 
        int command = rand() % handlerSize();
        //选择一个进程,采用随机数的方式,选择进程来完成任务,随机数的方式负载均衡
        int choice = rand() % slots.size();
        // 把任务给指定的进程
        sendAndWakeup(slots[choice].first, slots[choice].second, command);
        sleep(1);
        int select;
        //以下是手动派发任务
        // int command = 0;
        // cout << "######################################" << endl;
        // cout << "1.show functions        2.send command" << endl;
        // cout << "######################################" << endl;
        // cout << "Please Select > ";
        // cin >> select;
        // if (select == 1)
        //     showHandler();
        // else if (select == 2)
        // {
        //     cout << "Enter your Command > ";
        //     // 选择任务
        //     cin >> command;
        //     // 选择进程
        //     int choice = rand() % slots.size();
        //     // 把任务给指定的进程
        //     sendAndWakeup(slots[choice].first, slots[choice].second, command);
        // }
        // else
        // {
        // }
    }

    // 关闭fd,结束所有进程
    for (auto &slot : slots)
    {
        close(slot.second);
    }
    // 回收所有子进程
    for (auto &slot : slots)
    {
        waitpid(slot.first, nullptr, 0);
    }

    return 0;
}

        第二个文件为Task.hpp文件,主要是包含了任务的加载及任务的执行方法。

        

#pragma once
#include<iostream>
#include<vector>
#include<string>
#include<unordered_map>
#include<unistd.h>
#include<functional>
using namespace std;

typedef function<void()> func;

vector<func> callbacks;
unordered_map<int,string> desc;


void readMySQL()
{
    cout << "sub process[" << getpid() << "] 执行数据库被访问的任务\n" << endl;
}
void executeURL()
{
    cout << "sub process[" << getpid() << "] 执行url解析任务\n" << endl;
}
void cal()
{
    cout << "sub process[" << getpid() << "] 执行加密任务\n" << endl;
}
void save()
{
    cout << "sub process[" << getpid() << "] 执行数据持久化\n" << endl;
}

void load()
{
    desc.insert({callbacks.size(),"readMySQWL:读取数据库"});
    callbacks.push_back(readMySQL);

    desc.insert({callbacks.size(),"executeURL:解析URL"});
    callbacks.push_back(executeURL);

    desc.insert({callbacks.size(),"cal:进行加密计算"});
    callbacks.push_back(cal);

    desc.insert({callbacks.size(),"save:进行数据的文件保存"});
    callbacks.push_back(save);
}
void showHandler()
{
    for(auto& iter: desc)
    {
        cout << iter.first << "\t" << iter.second << endl;
    }
}
int handlerSize()
{
    return callbacks.size();
}

 

这样每次父进程都会随机给子进程派发随机的任务:

 

 命名管道

         与匿名管道不同,命名管道不需要亲缘关系的进程之间,也不需要共享同一终端。任意进程可以通过打开命名管道的读取端和写入端来与其进行通信。

什么是命名管道

命名管道(Named Pipe)是一种独立进程之间通信的机制,用于在无关的进程之间进行数据传输。

        命名管道通过在文件系统中创建一个特殊的文件来实现通信。这个特殊的文件被称为FIFO(First-in, First-out)或命名管道。

命名管道通信的原理

        和匿名管道一样,想让双方通信,必须先让双方看到同一份资源!它和匿名管道本质是一样的,只是看到资源的方式不同

        匿名管道是通过父子进程继承来看到同一份资源的,也叫做管道文件,这个文件是纯内存级的,所以没有名字,叫做匿名管道。

        而命名管道是在磁盘上有一个特殊的文件,这个文件可以被打开,但是打开后不会将内存中的数据刷新到磁盘。在磁盘上就有了路径,而路径是唯一的,所以双方就可以通过文件的路径 来看到同一份资源,即管道文件。

这是命名管道的流程:

  1. 创建命名管道:通过调用系统调用 mkfifo() 在文件系统中创建一个特殊的文件,这个文件就是命名管道。创建命名管道时,需要指定管道的名称和所需的权限。

  2. 打开命名管道:进程通过调用系统调用 open() 来打开命名管道,得到一个文件描述符。进程可以通过打开具有相同名称的文件来打开命名管道的读取端和写入端。

  3. 进程通信:一旦命名管道被打开,进程就可以使用文件描述符进行通信。每个进程可以选择读取端或写入端与命名管道进行交互。

  4. 数据传输:进程在读取端通过调用 read() 系统调用从命名管道中读取数据,而在写入端通过调用 write() 系统调用将数据写入命名管道中。读取端和写入端可以通过文件描述符进行数据的发送和接收。

  5. 关闭命名管道:进程完成通信后,可以通过调用 close() 关闭命名管道的文件描述符来释放资源。当所有对命名管道的引用都被关闭时,管道的文件系统条目将被删除。

mkfifo的使用

        上面提到了需要使用mkfifo来创建这个特殊的文件,来让独立的进程之间通过它进行通信,下面来看一下它的用法。

mkfifo [选项] 文件名

         非常简单的使用方法,选项我们一般不用,所以直接mkfifo + 文件名即可

        我们在当前路径下创建一个name_pipe的文件.

注意权限的最前面是p,代表是管道文件。


我们此时echo一句消息到这个管道文件中:

        我们发现这里阻塞住了,这是因为一方向管道文件里写入了,但是另外一方还没有读,所以此时我们新建一个窗口,然后读取name_pipe里的内容:

 

         这样信息便成功的被读取出来了,这就是mkfifo的简单使用。

代码模拟命名管道通信过程

        其实过程和匿名管道类似,只是看到同一资源的手段不一样。

        上面讲的mkfifo是指令创建,但是如果我想用代码该如何实现呢?这里有一个mkfifo函数:

         第一个参数是创建的管道文件的路径,第二个是权限。

         当创建成功时,mkfifo返回0,否则返回-1.

整体的流程是是这样的:

我们首先可以分成 服务端 和 客户端,服务端负责

        1.创建管道文件并打开

        2.进行与客户端正常的通信

        3.最后关闭并删除管道文件

而客户端

        1.首先要打开管道文件

        2.然后进行与服务端正常的通信流程即可

        这里为了方便,我们加入了日志,可以看到每一步的动作。

        所以一共四个文件,分别为comm.hpp,client.cc,server.cc,Log.hpp.

comm.hpp

#pragma once
#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<vector>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

using namespace std;

#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";

client.cc

#include "comm.hpp"
int main()
{
    //1.获取管道文件
    int fd = open(ipcPath.c_str(),O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(1); 
    }
    //2.通信过程
    string buffer;
    while(true)
    {
        cout << "please Enter Message Line :> ";
        getline(cin,buffer);
        write(fd,buffer.c_str(),buffer.size());
    }


    //3.关闭文件
    return 0;
}

server.cc

#include"comm.hpp"
#include"Log.hpp"
int main()
{
    //1.创建管道文件
    if(mkfifo(ipcPath.c_str(),MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }
    Log("创建管道文件成功",Debug) << "step 1" << endl;
    //2.正常的文件操作
    int fd = open(ipcPath.c_str(),O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        exit(2);
    }
    Log("打开管道文件成功",Debug) << "step 2" << endl;

    //3.编写正常的通信代码
    char buffer[SIZE]; 
    while(true)
    {
        memset(buffer,'\0',sizeof(buffer));
        ssize_t s = read(fd,buffer,sizeof(buffer)-1);
        if(s > 0)
        {
            cout << "client say> " << buffer << endl;
        }
        else if(s == 0)
        {
            //end of file
            cerr << "read emd of file, client quit, server quit too!" << endl;
            break;
        }
        else
        {
            //read error
            perror("read");
        }
    }
    //4.关闭文件
    close(fd);
    Log("关闭管道文件成功",Debug) << "step 3" << endl;
    unlink(ipcPath.c_str());//通信完毕就删除文件
    Log("删除管道文件成功",Debug) << "step 4" << endl;
    return 0;
}

Log.hpp

#pragma once
#include <iostream>
#include <ctime>
#include<string>
using namespace std;

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

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

ostream& Log(string message,int level)
{
    cout << " | " << (unsigned)time(NULL) << " | " << msg[level] << " | " << message;

    return cout;
}

 然后我们再编译运行,可以再创建一个Makefile文件,直接编译好所有的文件,内容如下:

.PHONY:all
all:client server

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

.PHONY:clean
clean:
	rm -rf client server

此时我们直接make即可。然后会得到两个可执行文件client和server.

我们打开两个窗口,首先运行server.

 第一步创建管道完成,然后我们在另一个窗口运行客户端.

运行起来后,显示打开文件也成功了,这个时候,我们在客户端输入,服务端都能读取到:

 然后我们ctrl + c 退出客户端,此时服务端也会break跳出循环,然后结束.

 这样,利用命名管道通信的代码流程也就完成了.

 

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

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

相关文章

问题:idea启动项目错误提示【command line is too long. shorten command line】

问题&#xff1a;idea启动项目错误提示【command line is too long. shorten command line】 参考博客 问题描述 启动参数过长&#xff0c;启动项目&#xff0c;错误提示 原因分析 出现此问题的直接原因是&#xff1a;IDEA集成开发环境运行你的“源码”的时候&#xff08…

java:解决报错非法字符: ‘\ufeff‘以及什么是BOM

背景 运行 JAVA 项目后&#xff0c;报错提示&#xff1a;非法字符: \ufeff&#xff0c;如图&#xff1a; 但是我在这个报错的文件中并没有搜到这个字符&#xff0c;那到底是什么原因 什么是BOM BOM&#xff08;Byte Order Mark&#xff09;&#xff0c;隐藏字符&#xff0c…

Pytorch深度学习-----神经网络之线性层用法

系列文章目录 PyTorch深度学习——Anaconda和PyTorch安装 Pytorch深度学习-----数据模块Dataset类 Pytorch深度学习------TensorBoard的使用 Pytorch深度学习------Torchvision中Transforms的使用&#xff08;ToTensor&#xff0c;Normalize&#xff0c;Resize &#xff0c;Co…

python与深度学习(十一):CNN和猫狗大战

目录 1. 说明2. 猫狗大战2.1 导入相关库2.2 建立模型2.3 模型编译2.4 数据生成器2.5 模型训练2.6 模型保存2.7 模型训练结果的可视化 3. 猫狗大战的CNN模型可视化结果图4. 完整代码5. 猫狗大战的迁移学习 1. 说明 本篇文章是CNN的另外一个例子&#xff0c;猫狗大战&#xff0c…

Go -- 测试 and 项目实战

没有后端基础&#xff0c;学起来真是费劲&#xff0c;所以打算速刷一下&#xff0c;代码跟着敲一遍&#xff0c;有个印象&#xff0c;大项目肯定也做不了了&#xff0c;先把该学的学了&#xff0c;有空就跟点单体项目&#xff0c;还有该看的书.... 目录 &#x1f34c;单元测试…

realsense-viewer 不识别 T265——Realsense SDK 在 v2.54.1 版本以后不再支持T265相机的解决办法

由于T265停产&#xff0c;Intel RealSense™ SDK 2.0 (v2.54.1) 在该版本中移除了对T265相机的支持&#xff0c;以后的版本也不会支持了。为了继续使用 T265 相机&#xff0c;最好千万不要升级 realsense 相关的 package&#xff0c;但是还有新装机的需求啊。经测试Intel RealS…

深度学习Redis(4):哨兵

前言 在 Redis&#xff08;3&#xff09;&#xff1a;主从复制 中曾提到&#xff0c;Redis主从复制的作用有数据热备、负载均衡、故障恢复等&#xff1b;但主从复制存在的一个问题是故障恢复无法自动化。本文将要介绍的哨兵&#xff0c;它基于Redis主从复制&#xff0c;主要作…

js中exec与match的区别

const regex1 RegExp(f(o.?),g); const str1 table foatball, fobsball; let array1; let array2; array1 regex1.exec(str1) array2 str1.match(regex1)console.log(array1, array1); console.log(array2, array2); //没有g的情况下,都是找到第一个匹配,并且如果有分组,…

【C#学习笔记】引用类型(1)

文章目录 引用类型class匿名类 记录引用相等和值相等record声明 接口delegate 委托合并委托/多路广播委托 引用类型 引用类型的变量存储对其数据&#xff08;对象&#xff09;的引用&#xff0c;而值类型的变量直接包含其数据。 对于引用类型&#xff0c;两种变量可引用同一对…

软件开发和测试开发选哪个更好?一文讲清!

1、岗位需求分析 随着科技的发展&#xff0c;软件测试领域对人才的要求越来越高&#xff0c;特别测试开发岗位已成行业热点关注对象。 做开发的同学也对测试开发岗位感到好奇&#xff0c;为什么做测试还要写代码做开发&#xff1f; 他们都在开发些什么软件&#xff1f; 到底…

【C++】开源:Eigen3矩阵与线性代数库配置使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍Eigen3矩阵与线性代数库配置使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&…

ElementUI el-table 鼠标滚动失灵的问题及解决办法

Bug&#xff1a;ElementUI el-table 鼠标滚轮下滑动失灵的情况 我测出来的这个问题条件很苛刻&#xff0c;需要达到以下几个条件才会触发&#xff1a; 1.element plus&#xff08;其他版本没试&#xff09; 2.el-table-column组件有fixed属性时 3.template标签中有el-butto…

C++ | 位图与布隆过滤器

目录 前言 一、位图 1、位图的引入 2、位图的实现 &#xff08;1&#xff09;基本结构 &#xff08;2&#xff09;构造函数 &#xff08;3&#xff09;插入数据 &#xff08;4&#xff09;删除数据 &#xff08;5&#xff09;是否存在 3、位图的优缺点 4、位图的应用…

pytorch入门

详细安装教程和环境配置可以看&#xff1a;Python深度学习&#xff1a;安装Anaconda、PyTorch&#xff08;GPU版&#xff09;库与PyCharm_哔哩哔哩_bilibili 跟学课程&#xff1a;B站我是土堆 pytorch中两个实用函数&#xff1a; dir()&#xff1a;打开 help():说明书…

Java POI 百万规模数据的导入和导出

目录 1、百万数据导入1.1 需求分析1.2 思路分析1.3 代码实现1.3.1 步骤分析1.3.2 自定义处理器1.3.3 自定义解析1.3.4 测试 2、百万数据导出2.1、概述2.2、解决方案分析2.3、原理分析2.4、百万数据的导出2.4.1、模拟数据2.4.2、思路分析2.4.3、代码实现2.4.4、测试结果 1、百万…

python-网络爬虫.Request

Request python中requests库使用方法详解&#xff1a; 一简介&#xff1a; Requests 是Python语言编写&#xff0c;基于urllib&#xff0c; 采用Apache2 Licensed开源协议的 HTTP 库。 与urllib相比&#xff0c;Requests更加方便&#xff0c;处理URL资源特别流畅。 可以节约我…

如何消除浮动

第一种方法: 1、创建一个general.css文件&#xff1a; charset "utf-8"; .clearfix:after {content: "";display: block;clear: both;} /* flex */ .flex,.flexA,.flexB,.flexC {display: flex;flex-wrap: wrap;} .flexA {justify-content: space-aroun…

iPhone 6透明屏是什么?原理、特点、优势

iPhone 6透明屏是一种特殊的屏幕技术&#xff0c;它能够使手机屏幕变得透明&#xff0c;让用户能够透过屏幕看到手机背后的物体。 这种技术在科幻电影中经常出现&#xff0c;给人一种未来科技的感觉。下面将介绍iPhone 6透明屏的原理、特点以及可能的应用。 iPhone 6透明屏的原…

if语句实现成绩等级判断

if语句实现成绩等级判断 案例分析代码实现小结Time 案例分析 使用键盘输入一个成绩&#xff0c;然后通过if判断语句实现成绩等级的判断 代码实现 import java.util.Scanner;public class DetermineDemo {public static void main(String[] args) {Scanner scanner new Scanne…