Linux中进程间通信--匿名管道和命名管道

        本篇将会进入 Linux 进程中进程间通信,本篇简要的介绍了 Linux 中进程为什么需要通信,进程间通信的常用方式。然后详细的介绍了 Linux 进程间的管道通信方式,管道通信分为匿名管道和命名管道,本篇分别介绍了其实现的原理,以及使用 Linux 中的系统调用,用代码实现了这两种通信方式。

        还详细的介绍了关于管道通信的五种特征和四种情况。

目录

Linux中进程间通信

1. 进程之间为什么需要通信

2. 进程之间如何进行通信

3. 进程间通信常用方式

匿名管道

1. 匿名管道实现原理

2. 匿名管道的实现规定

3. 模拟管道进程通信

5. 匿名管道在命令行中的方式

6. 基于匿名管道的进程池

6. 该代码容易出现的 bug

管道通信的特征与情况

1. 管道通信的五种特征

2. 管道通信的四种情况

命名管道 

1. 命名管道实现原理

2. 管道文件的特点

3. 使用命名管道通信

Linux中进程间通信

1. 进程之间为什么需要通信

        Linux 中的各种进程之间是相互独立的,但是进程和进程之间也是需要相互协调工作的,所以进程之间就会存在相互通信,一切协调完成工作。

        在用于通信的数据中,数据是有类别的,比如:用于通知就绪的、单纯传递给我的数据、控制相关的信息等等。

2. 进程之间如何进行通信

        首先,对于进程之间的通信,成本会比较高。因为进程之间相互独立,也就意味着进程之间的数据也是相互独立的,通信的本质就是信息之间的传递(数据之间的传递),所以进程之间并不能直接的进行通信(传递数据)。

        那么对于一个特殊的例子:父子进程之间算不算天然具有通信的能力呢?从严格意义上来说着并不算一种通信方式,虽然子进程可以继承父进程的数据和代码,但是子进程和父进程之间并不能一直交换数据(写时拷贝会将数据修改),所以并不能算得上是通信。

        所以对于进程间通信的前提:让不同的进程,看到同一份操作系统级别的资源,也就是同一段内存。如下:

        所以对于以上进程之间的通信:

        a. 一定是某个进程先需要通信,然后让操作系统创建一个共享资源;

        b. 操作系统必须提供很多的系统调用,进程需要使用系统调用让操作系统申请资源。

        c. 进程间通信会存在不同的种类,所以操作系统共享的资源会不同,系统调用接口也会不同。

3. 进程间通信常用方式

        进程间通信常用的两套标准为 system V 和 Posix 标准。对于这两个标准,只做介绍并不详解。

        对于 system V 标准来说,一共存在三种方式:消息队列、贡献内存、信号量。三种方式。但是在这些方式出来之前,还存在一种更为简单的通信方式:管道(直接复用内核代码进行直接通信)

        管道又分为命名管道匿名管道

匿名管道

1. 匿名管道实现原理

        我们首先先介绍关于匿名管道的实现原理,如下图:

        如上图:对于存在磁盘中的一个文件,父进程首先使用读方式和写方式分别打开该文件,然后就会依次创建出文件结构体以及文件的各种缓冲区,接着将文件结构体中的各种指针指向这些缓冲区,然后将文件结构体链接到父进程的文件描述符表中。

        然后父进程创建出它的子进程,子进程会开辟出空间,然后继承父进程的代码和数据,但是对于父进程的文件系统中的数据并不会创建出独立的空间用于存放,所以子进程创建出来的文件描述符表同样会指向之前父进程打开的文件结构体

        通过以上操作之后,子进程和父进程之间也就存在指向同一个文件了,也就是满足了进程之间通信的前提:不同进程可以看见同一块内存空间、同一份文件缓冲区(这个文件被我们称为管道文件)(其实这也是为什么每个进程会默认打开三个标准输入输出标准错误流(0、1、2)的原因:因为每个创建的子进程都是 bash 创建的,bash 是打开这三个流的,所以子进程会进程 bash 的文件描述符表,同样指向 0、1、2)。

        那么为什么我们子进程使用 close 关闭文件的时候,父进程也还可以使用呢?

        这是因为关于文件结构体 struct file 会存在着内存级的引用计数,几个进程指向该文件就有几个引用计数,当引用计数变为 0 的时候才会将文件关闭。

2. 匿名管道的实现规定

        对于管道而言,我们规定其只能单向通信,也就是意味只有两种通信方式:父进程发,子进程收 或者 子进程发、父进程收。所以我们在创建出了管道文件之后,我们需要将文件描述符表对应的管道文件关闭,比如父进程发消息、子进程收消息时,父进程关闭读方式的管道文件、子进程关闭写方式的管道文件。

        我们在以上创建出管道文件之后,我们只会用其进行进程之间的通信,也就是不需要将通信的内容刷新到磁盘中,所以我们可以重新设计出通信接口,只设计出内存级的管道文件,不用访问磁盘,如下:

        将其简化之后,就是下图:

        关于以上的规定,匿名管道只允许单向进行通信,那假设我们要进行双向通信呢,我们只需要将创建出两个管道文件就可以进行双向通信了

        为什么需要规定是单向通信呢?这是因为在设计管道通信时就是想要复用代码进行通信,也就是尽可能简单的进行通信,假若设计成双向的通信方式,则需要在管道内分别区分出来自不同进程的信息,这就会大大增加通信的复杂程度。

3. 模拟管道进程通信

        接下来我们将使用代码来模拟管道间的通信,其中有一个很重要的通信接口,如下:

int pipe(int pipefd[2]);

        该接口的底层实现就是调用了文件打开函数 open,不过我们并不需要提供文件路径和文件名,因为生成的是匿名管道文件,pipefd[0] 表示的是文件读端、pipefd[1] 表示的是文件写端,该接口的返回值若为0表示管道文件设置成功,非零则表示设置失败。

        现在我们通过如下代码来查看子进程向父进程发送消息,父进程接收信息之后将信息打印出来,如下代码:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string>const int size = 1024;std::string GetOtherMessage() {static int cnt = 0;std::string messageid = std::to_string(cnt);pid_t rid = getpid();std::string child_id = std::to_string(rid);std::string retMessage = "messageid: ";retMessage += messageid;retMessage += " my pid is ";retMessage += child_id;cnt++;return retMessage;
}void SubProcessWrite(pid_t wfd) {std::string message = "father, I am your son! ";while (true) {std::string info = message + GetOtherMessage();write(wfd, info.c_str(), info.size());sleep(1);}
}void FatherProcessRead(pid_t rfd) {char inbuff[size];while (true) {int n = read(rfd, inbuff, sizeof(inbuff) - 1);if (n > 0) {inbuff[n] = '\0';std::cout << "father get message: " << inbuff << std::endl;}}
}int main () {// 创建管道int pipefd[2];int n = pipe(pipefd);if (n != 0) {std::cout << "errno: " << errno << " errstring: " << strerror(errno) << std::endl;return 1;}// 创建出子进程pid_t id = fork();if (id == 0) {// 使用子进程进行写,关闭读端close(pipefd[0]);// 子进程发送信息SubProcessWrite(pipefd[1]);close(pipefd[1]);exit(0);}// 父进程进行读,关闭写端close(pipefd[1]);// 父进程接收信息FatherProcessRead(pipefd[0]);close(pipefd[0]);// 等待回收子进程int status;pid_t rid = waitpid(id, &status, 0);if (rid > 0) std::cout << "wait child process sucess!" << std::endl;elsestd::cout << "wait child process fail!" << std::endl;return 0;
}

        运行结果如下:

        由上可得,父进程会逐一从管道中读出子进程写入管道的数据。

5. 匿名管道在命令行中的方式

        在命令行中直接使用匿名管道,我们只需要在我们需要执行的命令间加上 " | ",就表示使用管道,如下:

        使用管道同时执行三个指令,这三个指令是同时进行的。

6. 基于匿名管道的进程池

        现在我们将基于以上的知识,使用代码来实现一份基于匿名管道通信的一个进程池项目。该进程池实现的功能为:父进程为任务派送方,然后生成若干子进程,然后将任务均匀的分发至子进程,让子进程分别执行派发下来的任务

        我是在 Linux 下完成的,.cc 后缀的也是 C++ 代码文件,.hpp 也是 C++ 代码的头文件,另外还给出了我的 makefile 文件。

        代码如下:

Task.hpp

#pragma once
#include <iostream>
#include <cstdlib>#define TASKNNUM 3typedef void (*task_t)();
task_t tasks[TASKNNUM];void Print() {std::cout << "I am print task" << std::endl;
}void DownLoad() {std::cout << "I am a dowmload task" << std::endl;
}void Flush() {std::cout << "I am a flush task" << std::endl;
}void LoadTask() {srand(time(0) ^ getpid());tasks[1] = DownLoad;tasks[2] = Flush;tasks[0] = Print;}size_t SelectTask() {int selectnum = rand() % TASKNNUM;return selectnum;
}void SubReadCommand(std::string name) {while (true) {int command = 1;int n = read(0, &command, sizeof(int));if (n == sizeof(int)) {// 执行对应的程序std::cout << "I am " << name << std::endl;size_t taskindex = command;tasks[taskindex]();} else if (n == 0) {// 读到0个数,说明读端已经关闭std::cout << "sub process : " << getpid() << " quit" << std::endl;break;}}
}

ProcessPool.cc

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <sys/wait.h>
#include "Task.hpp"const int size = 256;class Channel {
public:Channel(const std::string& name, pid_t subprocessid, size_t wfd): _name(name), _subprocessid(subprocessid), _wfd(wfd){} pid_t GetSubProcessId() {return _subprocessid;}std::string GetSubName() {return _name;}size_t GetSubWfd() {return _wfd;}// 等待子进程退出void SubWait() {int status;pid_t rid = waitpid(_subprocessid, &status, 0);if (rid > 0)std::cout << _name << " wait success!" << " exit signal: " << (status & 0x7F) << std::endl;elsestd::cout << _name << " wait fail!" << " exit signal: " << (status & 0x7F) << std::endl;}// 关闭写端void CloseWfd() {close(_wfd);// std::cout << _name << " wfd close success!" << std::endl;}private:std::string _name;pid_t _subprocessid;size_t _wfd;
};void FatherCreateWorkerProcess(std::vector<Channel>* channels, int num) {for (int i = 0; i < num; i++) {int pipefd[2];int n = pipe(pipefd);if (n == -1) {std::cerr << "Create pipe fail! " << "error string: " << strerror(errno) << std::endl;exit(1);}// 使用fork创建出子进程// 创建出子进程之后,让子进程关闭掉写端,父进程关闭掉读端std::string name("worker-");name += std::to_string(i);pid_t id = fork();if (id == 0) {if (!channels->empty()) {for (auto& channel : *channels)channel.CloseWfd();}close(pipefd[1]);// 子进程一直阻塞等待读入消息// 重定向子进程dup2(pipefd[0], 0);SubReadCommand(name);     close(pipefd[0]);       exit(0);    }close(pipefd[0]);Channel channel(name, id, pipefd[1]);channels->push_back(channel);}
}size_t SelectSubProcess(int num) {static size_t cnt = 0;int ret = cnt;cnt++;cnt %= num;return ret;
}void ControlProcessOnce(std::vector<Channel>& channels, int num) {size_t taskindex = SelectTask();// 然后让对应的子进程执行对应的程序size_t subindex = SelectSubProcess(num);write(channels[subindex].GetSubWfd(), &taskindex, sizeof(taskindex));std::cout << std::endl;sleep(1);
}void ControlProcess(std::vector<Channel>& channels, int num, int times = -1) {// 让父进程向管道中输入信息// 先选中要执行的任务if (times > 0) while (times--)ControlProcessOnce(channels, num);elsewhile (true)ControlProcessOnce(channels, num);
}void ClosePipeandSub(std::vector<Channel>& channels) {// 逐一关闭对应的,关闭了写端for (auto& channel : channels)channel.CloseWfd();for (auto& channel : channels)channel.SubWait();
}int main(int argc, char* argv[]) {if (argc != 2) {std::cout << "the parameter of command line is not equal two, please input again" << std::endl;return 1;}// 加载任务LoadTask();// 创建子进程std::vector<Channel> channels;int channelnums = std::stoi(argv[1]);FatherCreateWorkerProcess(&channels, channelnums);// for (auto& channel : channels)//     std::cout << channel.GetSubName() << std::endl;// 控制子进程ControlProcess(channels, channelnums, 5);// 退出,关闭对饮的管道和子进程ClosePipeandSub(channels);return 0;
}

makefile

processpool:ProcessPool.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f processpool
6. 该代码容易出现的 bug

        实现该代码容易出现的一个问题为,开辟出来的匿名管道可能有着多个进程指向同一个管道,如下:

        当我们 fork 出第一个子进程创建出管道的时候,我们可以建立出一个一对一的匿名管道通信,当我们继续进行 fork 子进程创建出管道的时候,父进程和子进程仍然会创建一个单独的匿名管道,然后分别关闭对应的读端和写端,但是子进程会继承父进程的数据,也就是会继承指向第一个创建出的管道的文件描述符。以此类推,每创建出一个子进程,指向前面管道的文件描述符都会加一。所以我们在创建子进程的时候就应该关闭这些指向匿名管道的文件描述符,如下:

void FatherCreateWorkerProcess(std::vector<Channel>* channels, int num) {for (int i = 0; i < num; i++) {int pipefd[2];int n = pipe(pipefd);if (n == -1) {std::cerr << "Create pipe fail! " << "error string: " << strerror(errno) << std::endl;exit(1);}// 使用fork创建出子进程// 创建出子进程之后,让子进程关闭掉写端,父进程关闭掉读端std::string name("worker-");name += std::to_string(i);pid_t id = fork();if (id == 0) {if (!channels->empty()) {for (auto& channel : *channels)channel.CloseWfd();}close(pipefd[1]);// 子进程一直阻塞等待读入消息// 重定向子进程dup2(pipefd[0], 0);SubReadCommand(name);     close(pipefd[0]);       exit(0);    }close(pipefd[0]);Channel channel(name, id, pipefd[1]);channels->push_back(channel);}
}

        我们只需要在除创建第一个子进程的时候,将指向其他管道给关闭就可以了。

管道通信的特征与情况

1. 管道通信的五种特征

        对于管道一共存在 5 种特征和 4 种情况。

        管道的 5 种特征

        1. 对于匿名管道:只用来进行具有血缘关系的进程之间进行通信,比如兄弟进程、爷孙进程、父子进程,常用于父子进程之间进行通信。因为具有血缘关系的进程可以建立匿名管道

        2. 管道内部,自带进程之间同步的机制,多执行流执行代码的时候,具有明显的顺序性。比如子进程写一个数据,父进程读一个数据;子进程不写数据,父进程就阻塞不读数据。这样一种同步的过程。

        3. 管道文件的生命周期是随进程的。当进程打开该文件并且没有主动关闭,只要指向该文件的进程都关闭了,该文件也会被关闭,管道文件亦是如此。

        4.. 管道文件在通信的时候,是面向字节流的。管道中的读端和写端分别在读和写的时候可以看成是流动的,不过读端和写端他们之间的读取次数和写入次数不一定是一一匹配的,写端可以一次写很多,读端分批读出,写端也可以一次写一点,读端一次读出来。

        5. 管道的通信模式,是一种特殊的半双工模式。半双工的通信模式就是一次只能有一方发消息另一方收消息,也可以反过来。但是管道的通信方式,发消息和收消息方已经确定。

2. 管道通信的四种情况

         管道的四种情况

        1. 管道内部为空且写端的文件描述符并没有关闭,还会写但是还没写。这种情况为:读取条件不具备读进程会被阻塞,读进程会等待读条件具备(也就是写端写入的时候)。所 以这个时候读进程就会被操作系统从运行队列中拿出来放到管道的等待队列中,直到管道中有内容的时候,才会将其唤醒。

        2. 如果管道被写满且读端的文件描述符没有关闭,还能读但是没有读,这种情况为写条件不具备,写进程将会被阻塞,因为在写下去将会覆盖原来的数据,只有当数据被读端读出之后,写段才可以继续写下去。

        3. 读端一直在读,但是写端关闭了写文件描述符,则读端 read 的返回值将会读到0,表示读到了文件末尾。

        4. 读端关闭,写端一直在写入的情况,这种情况为坏管道的情况。一个管道只会有一个读端一个写端,当读端关闭之后,写端在继续写下去就没有意义了,操作系统不会允许这样浪费时间浪费空间的事情发生,因为会无缘无故的浪费资源,所以将会被操作系统认定为一种异常情况,操作系统将会给目标进程发送 13号 SIGPIPE 信号,杀掉对应的写端。

另外,对于写端和写入信息被读取的时候还有两个特点,如下:

        当写端一次写入的数据小于 Linux 中规定的 PIPE_BUF 的时候,那么这个信息是原子的,也就是写入这份数据之后,读端并不能将数据读走。但是当大于 PIPE_BUF的时候,那么有可能写到一半就被读端读走了。

        PIPE_BUF 在 Linux 中一半是 4096 个字节。

命名管道 

1. 命名管道实现原理

        匿名管道用于存在血缘关系(父子、兄弟、爷孙)进程之间的通信,那么命名管道就是用于两个毫不相关之间的进程之间的管道通信。通信原理如下:

        如上图所示,两个毫不相关的进程之间通信会打开同一个文件(每个文件都有唯一的路径),命名管道的通信方式和匿名管道一样,只能一端写一端读。由于两端实时通信之间并不需要将数据刷新到磁盘,所以我们打开这个文件是一个特殊文件,并不需要将数据刷新到磁盘中,这种文件为管道文件

2. 管道文件的特点

        我们创建出管道文件可以使用命令 mkfifo,如下:

mkfifo - make FIFOs (named pipes)

        当写端向管道文件写入消息的之后,假若没有读端将信息读出来,那么写端将会一直阻塞,如下:

        这种情况也符合管道的一种情况,当写端在写信息,但是读端不读且文件描述符未关闭,这就会导致写端处于一种阻塞的状态。

        假若我们将在管道进行写入的时候,将读端的文件描述符给关闭了,那么就会导致我们的 shell 出问题,被强制退出,这是因为当我们将读端的文件描述符给关闭,但写端未关闭,这个时候就会给写端发送 SIGPIPE 信号,然后就会将写端给强制杀掉,但是这个时候写端是由 bash 打开的,所以就会导致我们的 shell 出问题。

        所以不管是命名管道还是匿名管道都遵循管道的四情况和五特征

        管道文件的大小不会变化,不管是否向管道文件中写入,都不会有变化。因为通信双方写入到文件缓冲区之后并不会将信息刷新到磁盘中。

3. 使用命名管道通信

        使用命名管道通信,仍然有一个很重要的系统调用,还是 mkfifo,不过这是在代码级别的,如下:

#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);

        对于返回值:创建成功返回 0,创建失败返回 -1。

        代码如下:

NamedPipe.hpp:

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>#define NPCreater 0
#define NPUser    1
#define NPMode 0666
#define NPRead  O_RDONLY
#define NPWrite O_WRONLY
#define DEFAULT_FDX -1
#define NPReadSize 1024const std::string ComPath = "./myfifo";class NamedPipe {
private:std::string GetName() {if (_who == NPCreater)return "Creater";else if (_who == NPUser)return "User";elsereturn "None";}// 以不同的方式打开对应的文件int OpenNamedPipe(mode_t mode) {int fd = open(_path.c_str(), mode);if (fd == -1) {std::cout << "open file fail" << " the reason is " << strerror(errno) << std::endl;}return fd;}
public:NamedPipe(const std::string& path, size_t who): _path(path), _who(who), _fd(DEFAULT_FDX){// 让服务器端读,让客户端写,服务器创建出对应的管道文件if (who == NPCreater) {int n = mkfifo(_path.c_str(), NPMode);if (n < 0) {std::cout << "create named pipe fail!" << " the reason is " << strerror(errno) << std::endl;}_fd = OpenNamedPipe(NPRead);std::cout << GetName() << " create the named pipe" << std::endl;} else {_fd = OpenNamedPipe(NPWrite);   }}int SomeoneUseToRead(std::string* out) {char inbuff[NPReadSize];int n = read(_fd, inbuff, sizeof(inbuff) - 1);if (n == -1) {std::cout << "read failed" << " the reason is " << strerror(errno) << std::endl;} inbuff[n] = '\0';*out = inbuff;return n;}void SomeoneUseForWrite(const std::string& info) {int n = write(_fd, info.c_str(), info.size());if (n == -1) {std::cout << "write failed" << " the reason is " << strerror(errno) << std::endl;}}~NamedPipe() {if (_who == NPCreater) {// 让创建者删除对应的管道文件int n = unlink(_path.c_str());if (n < 0) std::cout << "remove the named pipe fail!" << " the reason is " << strerror(errno) << std::endl;    }std::cout << GetName() << " unlink the named pipe file " << std::endl;if (_fd != DEFAULT_FDX) {close(_fd);}}
private:std::string _path;size_t _who;int _fd;
};

server.cc:

#include "NamedPipe.hpp"int main() {NamedPipe fifo(ComPath, NPCreater);// 让服务器不断地读while (true) {std::string message;int n = fifo.SomeoneUseToRead(&message);if (n == 0) {std::cout << "Client quit... Server too" << std::endl;break;}std::cout << "Client Say > " << message << std::endl;sleep(1);}return 0;
}

client.cc:

#include "NamedPipe.hpp"std::string GetInfo() {static int cnt = 0;std::string message = "Hello, I am client";message += std::to_string(cnt);pid_t id = getpid();message += " my pid is ";message += std::to_string(id);cnt++;return message;
}int main() {NamedPipe fifo(ComPath, NPUser);while (true) {std::string info;std::cout << "Please Enter > ";std::cin >> info;fifo.SomeoneUseForWrite(info);sleep(1);}return 0;
}

        代码测试如下:

        由上的测试我们可以得出,对于读端而言,当我们打开文件,但是写端还没有来,进程会阻塞在 open 调用阶段,只有当写端也打开文件之后,读端才会打开文件。这种机制实际上是一种进程同步

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

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

相关文章

基于VMware(虚拟机) 创建 Ubunton24.04

目录 1.设置 root 密码 2. 防火墙设置 2.1 安装防火墙 2.2 开启和关闭防火墙 2.3 开放端口和服务规则 2.4 关闭端口和删除服务规则 2.5 查看防火墙状态 3. 换源 3.1 源文件位置 3.2 更新软件包 1. 设置网络 1. 在安装ubuntu时设置网络 2.在配置文件中修改 2.设置 r…

17_高级进程间通信 UNIX域套接字1

非命名的UNIX域套接字 第1个参数domain&#xff0c;表示协议族&#xff0c;只能为AF_LOCAL或者AF_UNIX&#xff1b; 第2个参数type&#xff0c;表示类型&#xff0c;只能为0。 第3个参数protocol&#xff0c;表示协议&#xff0c;可以是SOCK_STREAM或者SOCK_DGRAM。用SOCK_STR…

HTTP 缓存

缓存 web缓存是可以自动保存常见的文档副本的HTTP设备&#xff0c;当web请求抵达缓存时&#xff0c;如果本地有已经缓存的副本&#xff0c;就可以从本地存储设备而不是从原始服务器中提取这个文档。使用缓存有如下的优先。 缓存减少了冗余的数据传输缓存环节了网络瓶颈的问题…

MySQL学习之InnoDB引擎,索引

Mysql中的引擎 我们先来看一下MySql提供的有哪些引擎 mysql> show engines; 从上图我们可以查看出 MySQL 当前默认的存储引擎是InnoDB,并且在5.7版本所有的存储引擎中只有 InnoDB 是事务性存储引擎&#xff0c;也就是说只有 InnoDB 支持事务。 查看MySQL当前默认的存储引…

算法力扣刷题记录 五十一【654.最大二叉树】

前言 二叉树篇&#xff0c;继续。 记录 五十一【654.最大二叉树】 一、题目阅读 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。…

套接字编程一(简单的UDP网络程序)

文章目录 一、 理解源IP地址和目的IP地址二、 认识端口号1. 理解 "端口号" 和 "进程ID"2. 理解源端口号和目的端口号 三、 认识协议1. 认识TCP协议2. 认识UDP协议 四、 网络字节序五、 socket编程接口1. socket 常见API2. sockaddr结构&#xff08;1&#…

WebGIS的Web服务概述

WebGIS是互联网技术应用于GIS开发的产物&#xff0c;是现代GIS技术的重要组成部分&#xff0c;其中的Web服务是现代WebGIS的核心技术和重要标志&#xff0c;它集GIS、程序组件和互联网的优点于一身&#xff0c;深刻改变了GIS开发和应用的方式&#xff0c;绕过了本地数据转换和本…

Unity 批处理详讲(含URP)

咱们在项目中&#xff0c;优化性能最重要的一个环节就是合批处理&#xff0c;&#xff0c;在早期Unity中&#xff0c;对于合批的处理手段主要有三种&#xff1a; Static Batching Dynamic Batching GPU Instancing 如今Unity 为了提升合批范围与效率&#xff0c;提供了…

ICT测试原理

目录&#xff1a; 一、什么是ICT 二、ICT在哪使用 三、ICT如何测试 1、隔离(Guarding)原理 2、电容器测试原理 3、电感器测试原理 4、普通二极管测试方法(MODE D) 5、晶体管的测量原理 (三端点)(MODE TR) 6、短/开路的测试原理 1&#xff09;学习短路表 2&#xff…

基于chrome插件的企业应用

一、chrome插件技术介绍 1、chrome插件组件介绍 名称 职责 访问权限 DOM访问情况 popup 弹窗页面。即打开形式是通过点击在浏览器右上方的icon&#xff0c;一个弹窗的形式。 注: 展示维度 browser_action:所有页面 page_action:指定页面 可访问绝大部分api 不可以 bac…

【数据结构】排序算法——Lessen1

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

【动态专修】2024年五菱维修手册和电路图资料更新

经过整理&#xff0c;2017-2024年五菱汽车全系列已经更新至汽修帮手资料库内&#xff0c;覆盖市面上99%车型&#xff0c;包括维修手册、电路图、新车特征、车身钣金维修数据、全车拆装、扭力、发动机大修、发动机正时、保养、电路图、针脚定义、模块传感器、保险丝盒图解对照表…

人、智能、机器人……

在遥远的未来之城&#xff0c;智能时代如同晨曦般照亮了每一个角落&#xff0c;万物互联&#xff0c;机器智能与人类智慧交织成一幅前所未有的图景。这座城市&#xff0c;既是科技的盛宴&#xff0c;也是人性与情感深刻反思的舞台。 寓言&#xff1a;《智光与心影》 在智能之…

Python自动化DevOps任务入门

目录 Python自动化DevOps任务入门 一、环境和工具配置 1. 系统环境与Python版本 2. 虚拟环境搭建 3. 必要的库安装 二、自动化部署 1. 使用Fabric进行流式部署 2. 使用Ansible编写部署剧本 三、持续集成和测试 1. 配置CI/CD工具 选择工具 配置工具 构建和测试自动…

【SLAM】最最最简单的直线拟合情形下的多种求解方法

本文我们讨论一个最最最简单情况下的拟合的情形&#xff0c;并尝试使用不同的方法来进行求解。 假如有一组数 x 1 , x 2 , x 3 , . . . , x n x_1,x_2,x_3,...,x_n x1​,x2​,x3​,...,xn​&#xff0c;对应的值为 y 1 , y 2 , y 3 , . . . , y n y_1,y_2,y_3,...,y_n y1​,y2…

10.11和10.8那个大(各种ai的回答)

问题&#xff1a;10.11和10.8两个数哪个大 腾讯混元 ✔️ chatGPT ❎ 通义千问 ❎ 文心一言 ✔️ 智谱清言 ❎ 讯飞星火 ✔️ C知道 ❎ 豆包 ✔️

TCP粘包问题详解和解决方案【C语言】

1.什么是TCP粘包 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是一种面向连接的、可靠的、基于字节流的传输协议&#xff0c;它保证了数据的可靠性和顺序性。然而&#xff0c;由于TCP是基于字节流而不是消息的&#xff0c;因此在传输过…

【接口自动化_08课_Pytest+Yaml+Allure框架】

上节课一些内容 的补充 1、openxl这个方法&#xff0c;第一个元素是从1开始的&#xff0c;不是从0开始 回写的列在程序里写的是11&#xff0c;是因为是固定值 一、1. Yaml入门及应用 1、什么是yaml YAML&#xff08;/ˈjməl/&#xff0c;尾音类似camel骆驼&#xff09;是一…

Finding columns with a useful data type 找到合适的数据列的类型

Finding columns with a useful data type 在确定了原始查询的数据列数之后&#xff0c;接下来就是要确定合适的数据列的数据类型。可以用 SELECT a 的方式判断对应的数据列方式&#xff0c;有时候可以通过错误信息判断数据列的类型。如果服务器的响应没有报错&#xff0c;而…

Docker启动PostgreSql并设置时间与主机同步

在 Docker 中启动 PostgreSql 时&#xff0c;需要配置容器的时间与主机同步。可以通过在 Dockerfile 或者 Docker Compose 文件中设置容器的时区&#xff0c;或者使用宿主机的时间来同步容器的时间。这样可以确保容器中的 PostgreSql 与主机的时间保持一致&#xff0c;避免在使…