当前位置: 首页 > news >正文

【Linux】如何实现单机版QQ,来看进程间通信之管道

学会了管道,就可以实现简单的qq哦~

文章目录

  • 前言
  • 一、匿名管道
  • 总结


前言

为什么要进行进程间通信呢?因为需要以下这些事:

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

一、管道

1.匿名管道

首先我们前面说过,进程是具有独立性的,但是要实现两个进程通信就必须让这两个进程看到同一份资源,这该怎么办呢两个独立的进程如何看到同一份资源呢?其实这个操作是操作系统直接或间接提供的。下面我们先看看管道,然后再画图讲解如何让进程看到同一份资源。

who这个命令是一个进程,wc也是一个进程通过管道完成了进程间通信,那么如何证明这两个是进程呢?我们证明一下:

首先我们用sleep命令创建了3个进程,然后后面加个&符号是让这些进程在后台运行,否则如果在前台运行我们就不能输入指令了,然后通过查看 进程发现三个sleep进程都在并且他们的父进程都是bash。将sleep比作刚刚的who和wc指令就证明了进程间通信。

下面我们讲解一下管道的原理:

 首先先有进程,也就是task_struct,每个进程都有对应的文件描述符表,文件描述符表中有相应的数组,数组中存放了标准输入0,标准输出1,标准错误2,而每个进程描述符都会存放相应struct file的地址,在进程间通信的时候系统会提供一个内存文件,这个内存文件不会在磁盘刷新,这个文件被称为匿名文件,当我们以读和写方式打开一个文件,然后我们fork创建一个子进程,子进程也有task_struct,并且子进程会继承父进程的文件描述符表(但是不会复制父进程打开的文件对象),而文件描述符表中存放文件的地址都是相同的,所以子进程的文件描述符表也指向父进程的文件,正是因为这样,在父进程以读和写打开一份文件,而子进程也同样读和写打开和父进程打开的一样的一份文件,这就让两个进程看到了同一份资源。但是这种管道只能实现单向通信,比如我们关闭父进程的写端,关闭子进程的读端让子进程去写这两个进程就实现单向通信了。管道只能单向通信的原因是文件只有一个缓冲区,一个写入位置一个读取位置所以只能单向通信,要是想双向通信那就打开两个管道!而上面所讲的管道就是匿名管道。

下面我们写一个管道的程序:

 首先0 1 2是默认打开的标准输入,标准输出,标准错误,而3就是读端,4就是写端,我们要实现单向信道一定是父子进程一读一写。

首先我们在VScode中创建等会要用的.cc文件和makefile。

要创建管道,首先我们要知道创建管道的函数pipe:

此函数有一个参数是一个一维数组,这个一维数组只有两个元素,这个参数也叫输出型参数(这两个元素我们的例子中就是读端和写端)。

下面我们完成主体:

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <vector>
#include "task.hpp"
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main()
{//1.1创建管道int pipefd[2] = {0};int n = pipe(pipefd);if (n<0){std::cout<<"pipe error,"<<errno<<":"<<strerror(errno)<<std::endl;return 1;} std::cout<<"pipefd[0]:"<<pipefd[0]<<std::endl;std::cout<<"pipefd[1]:"<<pipefd[1]<<std::endl;//1.2创建进程pid_t id = fork();if (id==-1){//创建子进程失败exit(-1);}if (id==0){//子进程close(pipefd[0]);const std::string namestr = "hello,我是子进程";int cnt = 1;char buffer[1024];while (true){snprintf(buffer,sizeof buffer,"%s,计数器:%d,我的pid:%d\n",namestr.c_str(),cnt++,getpid());write(pipefd[1],buffer,strlen(buffer));sleep(1);}//写入成功后将写端关闭close(pipefd[1])exit(0);}//父进程close(pipefd[1]);//关闭不需要的fd,让父进程读取,子进程写入。char buffer[1024];while (true){int n = read(pipefd[0],buffer,sizeof(buffer-1));if (n>0){buffer[n] = '\0';std::cout<<"我是父进程,child give me message:"<<buffer<<std::endl;}}return 0;}

 我们的程序很简单,首先创建管道,如果创建失败就报错打印错误码,然后我们打印一下读端和写端(如果没问题的话读端是3,写端是4),然后第二步我们创建子进程,创建子进程同样要判断创建失败的情况,如果创建成功我们先关闭子进程的读端,然后搞一个1024的缓冲区让字符串写入到缓冲区里,sleep可以让现象更明显,写完后我们为了保证管道安全所以关闭刚刚使用的写端,在父进程中同理只不过是变成了读,下面我们运行一下代码:

 我们确实看到了两个进程,并且确实也完成了简单的单向通信,因为子进程把自己的数据给了父进程。通过上面的实例代码我们能知道管道的特点吗?

1.单向通信

2.管道的本质是文件,因为fd的生命周期随进程,管道的生命周期也是随进程的。

3.管道通信,通常用来进行具有"血缘"关系的进程,进行进程间通信,常用于父子通信----pipe打开管道,并不清楚管道的名字,所以是匿名管道。

下面我们修改一下代码,刚刚我们让子进程发的慢一点(sleep了1秒),现在我们让父进程读的慢一点看看是是什么样子:

 下面我们运行起来:

 通过结果我们发现写入的次数和读取的次数不是严格匹配的。所以这也是管道的一个特点。

 下面我们再将代码修改一下,让父进程读取恢复正常,让子进程写的慢一些我们再看看效果:

 

通过程序运行结果我们发现,当写入很慢的时候读取也变慢了,也就是说写入是会影响读取的。

下面我们再将代码修改一下,我们让子进程持续写入一个字符,父进程先等10秒再读:

 

 

通过上图我们可以看到当子进程写入65535个也就是2的16次方的数据后,父进程直接打印了,并且时间并没有到10秒,所以打印的原因是因为管道是有容量的,当我们的写端将管道文件写满了我们就不能继续写了,这与之前read端读完了所有的管道数据,如果写端不发就只能等待是相对应的。所以管道的另一个特点来了:具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信(自带同步机制)。

下面我们再试试将写端关闭了读端会怎么样呢?

 read函数的返回值可以判断读取了多少,比如返回值大于0就是将缓冲区的数据都读完了,返回值等于0就是读到了文件结尾,否则就是读取异常,下面我们将代码运行起来:

结果就是子进程写了一个字符退出后,父进程读取到\0就会读到文件结尾而退出。那么如果我们写端一直写,将读端关闭了会发生什么呢?因为这里的行为是无意义的,所以操作系统会杀死这个一直写入的进程(因为操作系统不会维护无意义,低效率,或者浪费资源的事情,操作系统会杀死一直写入的进程,会通过信号来终止进程(13号信号)),那么如何证明会被操作系统杀死呢,下面我们让父进程等待一下子进程然后看看:

 通过结果我们发现确实是这样,子进程被操作系统用13号信号终止了。以上就是匿名管道的相关知识,下面我们用一批管道实现控制进程的代码:

 首先我们创建一个ctrlProecess.cc的文件夹,然后写一个makefile,准备工作做好后我们就可以按照思路写代码了,我们第一步要先进行构建控制结构,父进程写入,子进程读取,并且既然是多个管道我们直接用个循环来控制:

 我们在判断是否成功创建子进程的时候直接用assert断言了,其实这里应该要用if语句判断的,但是由于这个函数基本不会出错并且我们只是演示一下代码所以就用断言了,当返回值等于0的时候那么一定是子进程了,否则就是父进程了,那么我们该如何通信呢,让父进程写入,子进程读取,所以我们先关闭不需要的端口。

现在前期准备工作已经完成了,下面如何让父进程管理自己创建的管道和进程呢?还记得我们之前说过的先描述,在组织吗?答案就是这个。

 我们为了描述这些创建的管道所以用一个类来封装,成员变量包括子进程的pid以及父进程写入所需要的端口,然后我们在构造函数中将这些变量初始化一下。

class EndPoint
{
public:pid_t _child_id;int _write_fd;EndPoint(int id,int fd):_child_id(id),_write_fd(fd){}~EndPoint(){}};

因为是多个管道所以我们将刚刚的自定义对象放到vector中去管理,这不就完成了先描述,在组织的任务吗,所需要的头文件大家记得加上,下面我们将vector定义到函数最开始。

我们的目的是让父进程写入,子进程读取,所以肯定是在父进程的地方来描述这个子进程:

 将一个个子进程和写端的对象放入向量后,父进程就拿到了一批Endpoint对象,这个结构里包含了要写入的文件描述符的写端和新的子进程。下面的工作就是要子进程读取指令,我们要让子进程在读取指令的时候,都从标准输入读取,要从标准输入读取我们就要用的以前学的知识了,那就是输入重定向,输入重定向的方式很多,我们主要用函数的方式:

 还记得我们之前学的dup2函数吗,在这里就可以派上用场了。我们的目的是要子进程都要从标准输入读取,所以函数的第一个参数old就是pipefd[0]了因为我们要让pipefd[0]重定向到0(标准输入),所以第二个参数new就是0了,然后我们让子进程开始等待获取命令,这里我们直接写一个函数WaitCommand()将所以的代码写入这个函数里,并且将刚刚创建进程的代码也放入一个creatProcess函数中,让各个函数完成相应的功能会让代码看的更简洁:

 下面在让子进程要执行方法的函数中先写一个read函数和一个死循环我们来看看代码能否正常创建子进程,然后再编写后序的代码:

 

 运行起来后我们可以看到之前写的创建进程的函数没毛病,下面我们来设计一下让子进程执行的方法的函数。在完成这个函数前先新建一个头文件task.hpp,然后创建一些简单的打印任务:

 我们在Task类中先定义一个vector,vector中存放的类型为函数指针类型,每一个函数指针都指向一个函数方法,这样我们就可以通过指令来调用不同的方法,对于指令command来讲我们之前也提到过其实就是位图,我们定义三个宏值分别代表三种打印方法,相要调用哪个方法就让command参数&上对应的宏值就完成了。我们在.cc文件中加上头文件,然后定义一个全局的Task类:

接下来我们继续实现子进程任务函数:

 在子进程函数中我们默认指令为int类型,然后通过read函数的返回值判断是否读取成功,如果返回值等于4字节说明读到了指令那么就调用对应的函数方法,如果返回值等于0就说明当前的子进程要退出了直接break,剩下的就是读取失败也是直接退出,当然我们不能只执行一次,所以我们加个循环让子进程周而复始的去执行任务:

 那么接下来的父进程还需要干什么呢?父进程需要选择任务,选择进程然后下发任务,下面我们来实现:

 这里的任务是可以随机选择的,但是我们为了测试就只用了LOG任务,然后选择进程的时候直接用随机函数选择即可,下发任务也就是让父进程写入,写入的是指令对应的方法,然后我们将之前答应的代码加上getpid,把子进程的pid也打印出来:

 以下是运行起来后的代码:

 这样我们就完成了要实现的功能,现在我们再把相应的函数优化一下,让选择任务的时候可以是交互式的,首先第一步让用户选择任务,这里我们先实现一个选择面板:

 将选择面板搞定后我们就可以实现让用户选择对应的任务了:

下面我们让选择进程的功能变成按顺序的,这样就不会出现每次随机让一个进程执行任务的情况:

 下面我们我们再实现一下每次拿到任务后打印正在执行哪个任务:

  我们先创建一个静态的int变量用来充当第几个进程,然后我们在类外将number初始化为0,string类型的进程名接收缓冲区里的字符,函数name可以返回一个进程名。然后我们在选择任务那块也打印一下:

 然后如果进程退出的话也要有提示,所以我们在子进程那块加上打印提示:

 当n等于0说明父进程将写端关闭了,随之就打印一下告诉用户。然后我们再将代码简化一下,把刚刚main函数中写的控制函数代码都放到ctrlProcess函数中:

 写到这里发现124行有个报错,是因为返回的name函数需要加上const让静态变量也能使用(因为我们的ctrlProcess函数的参数是const对象)

 

 下面我们运行一下程序:

 程序运行起来后我们发现输入3退出不了,并且输入其他指令程序也没有打印动作,所以我们用if语句判断一下:

 除了这个问题我们还要解决子进程退出问题,还记得我们之前说过的僵尸进程吗,如果子进程没有被父进程等待那么子进程就变成了僵尸状态,为了防止这种情况我们再写一个进程等待函数函数:

 回收子进程之前我们需要将父进程的所有写端都关了,然后等待10秒后我们让父进程把每个子进程都回收了,这样就完成了简单的回收子进程函数。

当我们运行起来后发现可以完成我们的需求,并且在退出后也能正常的回收子进程。

不知道有没有人会有疑问,为什么我们刚刚在同一个循环内不能直接边关闭边回收子进程呢,就像下图这样:

 我们先用这样的代码运行一下:

 我们发现这样写后程序就卡在了回收进程处,这是为什么呢?其实是和子进程有关,我们早就说过子进程会继承父进程打开的文件描述符(这就是管道的原理),这里的坑就在于当第二次及第二次以后的创建子进程时,这个子进程会继承前面所有父进程的端口,也就是说越往后的子进程继承的端口越多,如下图所示:

也就是说刚刚卡主的原因是写端没有全部关完就回收了子进程,就造成了阻塞。而我们用一个循环单独关闭父进程的端口时是不会出现这个问题的,因为我们是先关闭了父进程的写端,都关闭后才去回收子进程。

由于上面的讲解都是通过图片的形式,下面我们将此次的实例代码发出来:

首先是ctrlProcess.cc文件中的:

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <vector>
#include "task.hpp"
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
const int gnum = 3;
Task t;
class EndPoint
{
private:static int number;
public:pid_t _child_id;int _write_fd;std::string processname;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;}std::string &name(){return processname;}~EndPoint(){}};
int EndPoint::number = 0;
//子进程要执行的方法
void WaitCommand()
{while (true){int command = 0;int n = read(0,&command,sizeof(int));if (n==sizeof(int)){t.Execute(command);}else if(n==0){std::cout<<"父进程让我退出,我就退出了: "<<getpid()<<std::endl;break;}else {break;}}}
void CreatProcess( vector<EndPoint>* end_points)
{//1.先进行构建控制结构,父进程写入,子进程读取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);//返回值等于0一定是子进程if (id==0){//先关闭不要的fdclose(pipefd[1]);//我们期望所有的子进程读取指令的时候,都从标准输入读取//1.3.1输入重定向dup2(pipefd[0],0);  //读0就像读管道一样//1.3.2让子进程开始等待获取命令WaitCommand();close(pipefd[0]);exit(0);}//一定是父进程//1.3关闭不要的fdclose(pipefd[0]);//1.4将新的子进程和他的管道写端,构建对象end_points->push_back(EndPoint(id,pipefd[1]));}
}
int ShowBoard()
{std::cout<<"################################"<<std::endl;std::cout<<"##0. 执行日志任务 1. 执行数据库任务##"<<std::endl;std::cout<<"##2.执行请求任务  3.退出#########"<<std::endl;std::cout<<"################################"<<std::endl;std::cout<<"请选择#";int command = 0;std::cin>>command;return command;
}
void ctrlProcess(vector<EndPoint>& end_points) 
{//2.1我们可以写成自动化的,也可以写成交互式的int cnt = 0;int num = 0;while (true){//1.选择任务int command = ShowBoard();if (command==3){break;}if (command<0|| command>2){continue;}//2.选择进程int index = cnt++;cnt%=end_points.size();std::cout<<"选择了进程:"<<end_points[index].name()<<" | 处理任务"<<command<<std::endl;//3.下发任务write(end_points[index]._write_fd,&command,sizeof(command));//sleep(1);}
}
void ExitProcess()
{exit(0);
}
void waitProcess(const vector<EndPoint>& end_points)
{//1.我们需要让子进程全部退出 -- 只需要让父进程关闭所有的写端//for (const auto &ep:end_points)for (int end = end_points.size()-1;end>=0;end--){std::cout<<"父进程让所有的子进程全部退出"<<std::endl;//先关闭最后一个写端倒着一直关闭,因为子进程会继承父进程的文件描述符所以//后面的子进程都会链接到第一个父进程的写端,如果关第一个无法全部关闭,会造成//阻塞close(end_points[end]._write_fd);std::cout<<"父进程回收了所有的子进程"<<std::endl;waitpid(end_points[end]._child_id,nullptr,0);}//std::cout<<"父进程让所有的子进程全部退出"<<std::endl;sleep(10);//2.父进程要回收子进程的僵尸状态//for (const auto& ep:end_points)//{//   waitpid(ep._child_id,nullptr,0);//}//std::cout<<"父进程回收了所有的子进程"<<std::endl;//sleep(10);
}
int main()
{//1.先进行构建控制结构,父进程写入,子进程读取vector<EndPoint> end_points;CreatProcess(&end_points);//2.我们得到了什么?ctrlProcess(end_points);//3.处理所有的退出问题waitProcess(end_points);return 0;
}

下面是头文件task.hpp中的:

#include <iostream>
#include <vector>
#include <unistd.h>
typedef void(*fun_t)();  //函数指针void PrintLog()
{std::cout<<"pid:"<<getpid()<<",打印日志任务,正在被执行..."<<std::endl;
}
void InsertMySQL()
{std::cout<<"执行数据库任务,正在被执行..."<<std::endl;
}
void NetRequest()
{std::cout<<"执行网络请求任务,正在被执行..."<<std::endl;
}
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQUEST 2
class Task
{
public:Task(){funcs.push_back(PrintLog);funcs.push_back(InsertMySQL);funcs.push_back(NetRequest);}~Task(){}void Execute(int command){if (command >= 0&& command < funcs.size()){funcs[command]();}}public:std::vector<fun_t> funcs;
};

总结

管道读写规则:

当没有数据可读时:
O_NONBLOCK disableread调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enableread调用返回-1errno值为EAGAIN
当管道满的时候:
O_NONBLOCK disable write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致writ进程退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
管道特点:
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

相关文章:

【Linux】如何实现单机版QQ,来看进程间通信之管道

学会了管道&#xff0c;就可以实现简单的qq哦~ 文章目录 前言一、匿名管道总结 前言 为什么要进行进程间通信呢&#xff1f;因为需要以下这些事&#xff1a; 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程 资源共享&#xff1a;多个进程之间共享同样的资源。 …...

从一到无穷大 #6 盘满排查过程

文章目录 引言df/du 原理排查思路文件系统预留空间进程占用句柄挂载覆盖 引言 核心在于执行df和du的时候发现显示的存储量完全不同&#xff0c;我本地系统盘有99G空间&#xff0c;du显示占用了45G&#xff0c;但是df却显示使用了99G&#xff0c;排查的过程本文所示。 先记录几…...

ChatGPT技术原理 第九章:数据集和训练技巧

目录 9.1 对话数据集 9.2 数据预处理 9.3 预训练技巧 9.4 微调技巧 9.5 多任务学习...

NCR被攻击后服务中断!原是BlackCat勒索软件作祟

近日&#xff0c;在遭到BlackCat勒索软件攻击后&#xff0c;NCR 的 Aloha 销售点平台出现中断。 NCR公司是全球关系管理技术解决方案领导供应商&#xff0c;为全球零售、金融、传讯、制造、旅游、交通及保安等客户提供服务。凭著累积多年的业界知识、专业顾问经验、专业增值应用…...

带你认识什么是BMS(电池管理系统)

文章目录 概述BMS的硬件拓扑BMS的电气架构BMS的功能BMS的总压采集&#xff08;主板功能&#xff09;BMS的电流采集&#xff08;主板功能&#xff09;BMS的电芯电压和温度采集&#xff08;从板功能&#xff09;BMS的SOC、SOP和SOH&#xff08;ASW计算&#xff09;BSM的绝缘检测B…...

安装Ubuntu22.04虚拟机的一些常见问题解决方法

文章目录 VirttalBox 开启共享剪切板文件夹、拖放的功能VirtualBox 安装 ubuntu后安装增强工具无效的解决办法解决ubuntu您没有权限查看“ 某某文件夹”的内容所需的权限linux更换源的两种方法[如何在 Ubuntu 20.04 上安装 Visual Studio Code - ](https://zhuanlan.zhihu.com/…...

银河麒麟操作系统,安装Gitlab 基于docker

不废话。直接上干货 操作系统信息 ############## Kylin Linux Version ################# Release: Kylin Linux Advanced Server release V10 (Sword) Kernel: 4.19.90-24.4.v2101.ky10.aarch64 Build: Kylin Linux Advanced Server release V10 (SP2) /(Sword)-aarch64-…...

基于Python实现个人手机定位分析

TransBigData是一个为交通时空大数据处理、分析和可视化而开发的Python包。本文就来用它实现个人手机定位分析&#xff0c;感兴趣的小伙伴可以了解一下 但其实交通时空大数据并不仅仅局限于交通工具产生的数据&#xff0c;我们的日常生活中也会产生大量的数据。比如我们的手机…...

Unity Navgation系统杂记

立即停止寻路 使用agent.isStoppedtrue&#xff0c;可以停止寻路&#xff0c;但是有很大的延迟&#xff0c;视觉体验很不好。 使用agent.enabledfalse&#xff0c;通过禁用NavMeshAgent组件的方式实现立即停止寻路。因为组件被禁用可能会产生其它问题&#xff0c;比如失去了Ob…...

[2021.11.9]lighteffect架构优化详细设计文档

1 lighteffect系统架构图 图1-1 整改前lighteffect系统架构图 上图为整改前lighteffect系统架构图&#xff0c;存在的问题如下&#xff1a; (1)代码bug 原因&#xff1a;由于系统中兼容了lighteffect和lighteffect2&#xff0c;写代码时只记了一个&#xff0c;出现代码bug。…...

经典回归算法

回归的概念 回归方程&#xff1a; 写成矩阵&#xff1a; 核心问题&#xff0c;构建预测函数z来映射特征矩阵x和标签y的线性关系 预测的目标值&#xff0c;有连续值也有离散值 连续值&#xff0c;就直接预测输出就行离散值&#xff0c;需要在输出端加一个变换函数例如。Si…...

Python两三行代码轻松批量添加~防韩还是很有必要的~

人生苦短&#xff0c;我用python 一直想做一个这种系列的但是因为七七八八的事情总是忘记&#xff0c; 今天正好有空&#xff0c;来开整一下~ 首先&#xff0c; 天冷防韩是什么梗&#xff1f; 【天冷防韩】 “天冷防韩”是“天冷防寒”的谐音&#xff0c; 不过“寒”指的…...

开心消消乐

给定一个 N 行 M 列的二维矩阵&#xff0c;矩阵中每个位置的数字取值为 0 或 1&#xff0c;矩阵示例如&#xff1a; 1 1 0 0 0 0 0 1 0 0 1 1 1 1 1 1 现需要将矩阵中所有的 1 进行反转为 0&#xff0c;规则如下&#xff1a; 当点击一个 1 时&#xff0c;该 1 被反转为 0&am…...

有效日志管理在软件开发和运营中的作用

作者&#xff1a;Luca Wintergerst, David Hope, Bahubali Shetti 当今存在的快速软件开发过程需要扩展和复杂的基础架构和应用程序组件&#xff0c;并且操作和开发团队的工作不断增长且涉及多个方面。 有助于管理和分析遥测数据的可观察性是确保应用程序和基础架构的性能和可靠…...

【五一创作】【笔记】Git|如何将仓库中所有的 commit 合成一个?又名,如何清除所有 git 提交记录?(附 git rebase 机制的简要分析)

在对代码进行开源时&#xff0c;我们往往并不希望代码开发过程中的提交记录被其他人看到&#xff0c;因为提交的过程中往往会涵盖一些敏感信息。因此会存在 将仓库中所有 commit 合成一个 的需求。 直觉上&#xff0c;往往会用 rebase 和 squash 或 reset&#xff0c;不过我尝…...

如何写出高质量代码?

作为一名资深开发人员&#xff0c;写出高质量的代码是我们必须要追求的目标。然而&#xff0c;在实际开发中&#xff0c;我们常常会遇到各种问题。比如&#xff0c;代码的可读性、可维护性、健壮性和灵活性等&#xff0c;这些都会影响代码的质量。那么&#xff0c;究竟如何才能…...

外卖项目优化-01-redis缓存短信验证码、菜品数据、Spring Cache(注解开发缓存)、(注解开发)缓存套餐数据

文章目录 外卖项目优化-01课程内容前言1. 环境搭建1.1 版本控制解决branch和tag命名冲突 1.2 环境准备 2. 缓存短信验证码2.1 思路分析2.2 代码改造2.3 功能测试 3. 缓存菜品信息3.1 实现思路3.2 代码改造3.2.1 查询菜品缓存3.2.2 清理菜品缓存 3.3 功能测试3.4 提交并推送代码…...

Chapter1:控制系统数学模型(下)

第一章:控制系统数学模型 Exercise1.13 已知控制系统结构图如下图所示,求系统的输出 C 1 ( s ) C_1(s) C...

排序算法总结

常见排序算法的时间复杂度、空间复杂度及稳定性分析&#xff1a; 时间复杂度空间复杂度是否有稳定性基于比较的排序算法选择排序 O(N^2)O(1)否 冒泡排序O(N^2)O(1)是插入排序O(N^2)O(1)是归并排序O(N*logN)O(N)&#xff0c;每次需要额外一个数组用于拷贝是快排O(N*log…...

java+jsp企业物流货运快递管理系统servlet

功能需求具体描述&#xff1a; (1)用户功能模块包括用户登录注册&#xff0c;用户信息的修改&#xff0c;用户发布货物信息&#xff0c;给客服人员留言&#xff0c;对运输公司进行评价。 (2)企业功能模块包括企业注册登录&#xff0c;企业信息的修改&#xff0c;受理用户发布的…...

【ROS仿真实战】获取机器人在gazebo位置真值的三种方法(三)

文章目录 前言一. 使用ROS tf库二、 使用Gazebo Model Plugin三、 使用libgazebo_ros_p3d插件四、总结 前言 在ROS和Gazebo中&#xff0c;获取机器人的位置信息通常通过ROS消息传递进行。在这篇文章中&#xff0c;我们将介绍三种获取机器人在Gazebo中位置真值的方法&#xff1…...

Winform从入门到精通(35)——FontDialog(史上最全)

文章目录 前言一、属性1、Name2、AllowScriptChange3、AllowSimulations4、AllowVectorFonts5、AllowVerticalFonts6、Color7、FixedPitchOnly8、Font9、FontMustExist10、MaxSize11、MinSize12、 ScriptsOnly13、ShowApply14、ShowColor15、ShowEffects16、ShowHelp...

AcWing 854. Floyd求最短路Floyd模板

Floyd算法&#xff1a; 标准弗洛伊德算法&#xff0c;三重循环&#xff0c;基于动态规划。 循环结束之后 d[i][j]存储的就是点 i 到点 j 的最短距离。 需要注意循环顺序不能变&#xff1a;第一层枚举中间点&#xff0c;第二层和第三层枚举起点和终点。 特点&#xff1a; 1.复杂…...

Graph Theory(图论)

一、图的定义 图是通过一组边相互连接的顶点的集合。 In this graph, V { A , B , C , D , E } E { AB , AC , BD , CD , DE } 二、图的类型 2.1 Finite Graph A graph consisting of finite number of vertices and edges is called as a finite graph. Null Graph Tri…...

[Python]生成 txt 文件

前段时间有位客户问: 你们的程序能不能给我们生成个 txt 文件,把新增的员工都放进来,字段也不需要太多,就要 员工姓名/卡号/员工编号/员工职位/公司 这些字段就行了,然后我们的程序会去读取这个 txt 文件,拿里面的内容,读完之后会这个文件删掉 我: 可以接受延迟吗?可能没办法实…...

GeoTools实战指南: 自定义矢量样式并生成截图

GeoTools实战指南: 自定义矢量样式并生成截图 介绍 本段代码的主要功能是将矢量数据(Shapefile)渲染成一张图片。 准备环境 首先,您需要将GeoTools库添加到您的项目中。使用Maven或Gradle添加依赖项,或者直接下载GeoTools的jar文件并添加到您的类路径中。 Maven <…...

深度学习超参数调整介绍

文章目录 深度学习超参数调整介绍1. 学习率2. 批大小3. 迭代次数4. 正则化5. 网络结构总结 深度学习超参数调整介绍 深度学习模型的性能很大程度上取决于超参数的选择。超参数是指在训练过程中需要手动设置的参数&#xff0c;例如学习率、批大小、迭代次数、网络结构等等。选择…...

Bootloader

本篇不作太过的技术了解&#xff0c;仅可作为初学者的参考。用嘴简单的语言讲清楚一件事。 项目中遇到Bootloader升级MCU&#xff0c;我很好这是什么软件&#xff0c;逻辑是什么&#xff0c;怎么升级的。 术语及定义 指纹信息fingerprint诊断仪用于标识特定的下载尝试的信息 …...

安卓开发_广播机制_广播的最佳实践:实现强制下线功能

安卓开发_广播机制_广播的最佳实践&#xff1a;实现强制下线功能 ActivityCollector类用于管理所有的ActivityBaseActivity类作为所有Activity的父类创建一个LoginActivity来作为登录界面布局LoginActivity 在MainActivity中加入强制下线功能布局MainActivity在BaseActivity中注…...

国民技术N32G430开发笔记(10)- IAP升级 Application 的制作

IAP升级 Application 的制作 1、App程序跟Bootloader程序最大的区别就是&#xff0c; 程序的执行地址变成了之前flash设定的0x08006000处&#xff0c; 大小限制为20KB 所以修改Application工程的ld文件 origin 改成 0x08006000 length 改成0x5000 烧录是起始地址也要改为x0x…...

烟台高端网站建设/百度广告

2019年人工智能产业发展调研报告 人工智能&#xff08;AI&#xff09;起源于上世纪50年代&#xff0c;在此后的半个多世纪中&#xff0c;历经了“逻辑推理”、“知识工程”的两起两落。目前&#xff0c;随着智能算力与数据瓶颈得以解决&#xff0c;在AlphaGo战胜围棋世界冠军的…...

史丹利网站开发团队/网络运营师

小知识点注&#xff1a;外面双引号&#xff0c;里面的双引号改为单引号&#xff1b; 在div里面行高设置和整个外面高度一样&#xff0c;才能用竖直居中&#xff0c;居中是行居中 文本框取出来的值是字符串&#xff0c;需要用parseint()转化为数字 Window.document对象 一、找到…...

宿州网站建设时间/搜索引擎优化不包括

运营商数据分析挖掘在广告行业的应用 在前两天的分享中&#xff0c;同事提到了&#xff0c;目前广告行业正在经历一场深刻的变革&#xff0c;由‘注重触达’转为‘注重精准营销’&#xff0c;有‘单向营销’转为‘互动营销’。 实时上&#xff0c;这场变革对广告技术影响也非常…...

网站设计制作需要多少钱/企业查询平台

文章目录:HashMap的底层原理面试必考题。一&#xff1a;HashMap的节点&#xff1a;二&#xff1a;HashMap的数据结构&#xff1a;三&#xff1a;HashMap存储元素的过程&#xff1a;哈希码的特点是:四&#xff1a;HashMap中的两个重要的参数&#xff1a;HashMap的底层原理面试必…...

公司做网站的费用计什么科目/郑州百度seo网站优化

再看case语句&#xff0c;case语句只处理单条记录&#xff0c;而不是set 列名的使用&#xff0c;可以当做数值来使用&#xff1b; case when 后面简直是完美的的&#xff0c;什么东西都是能放的&#xff0c;只要是一个逻辑上的true/false的逻辑就可以&#xff1b; 执行顺序之加…...

dw做的网站不显示/营销 推广

按单词反转字符串是一道很常见的面试题。在Python中实现起来非常简单。def reverse_string_by_word(s):lst s.split() # split by blank space by defaultreturn .join(lst[::-1])s Power of Loveprint reverse_string_by_word(s)# Love of Powers Hello World!print rever…...