【Linux】-进程间通信-匿名管道通信(以及模拟一个进程池)

在这里插入图片描述

💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

文章目录

  • 前言
  • 一、进程间通信的三个问题
    • 1.1什么是进程间通信??
    • 1.2 为什么要进程间通信??
    • 1.3 怎么做到进程间通信??
  • 二、管道的原理
  • 三、接口的测试
  • 四、编写代码进行通信
    • 4.1管道的四种情况
    • 4.2 管道的五大特性
  • 五、基于管道设计一个简单的进程池
  • 六、总结


讲解逻辑:

  1. 根据前面的知识来推测进程之间大致是怎么通信的
  2. 直接讲解基于文件级别的通信方式
  3. 关于进程间通信的接口。五大特征以及四种情况
  4. 谈谈应用场景

前言

今天我们开始讲解进程间通信,我们之前讲过进程具有独立性,那么有的时候进程还是需要进行一些数据性的交换,但是又不能破怪独立性,这两者看着自相矛盾,但又不冲突,博主就是来带大家去解决这个问题,从原理到模拟实现一个进程间通信的程序,需要大家对之前的进程创建,进程等待,尤其是文件系统那一章节熟悉,那今天的内容才容易理解,所以希望没有这些知识储备的小伙伴可以先看我前面的博客讲解,再来看这篇,效果会更好,接下来我们开始进入正文的讲解。


一、进程间通信的三个问题

1.1什么是进程间通信??

简单的来说就是两个或者多个进程实现数据层面的交互,因为进程独立性的存在,导致了进程间通信的成本比较高,所以在一会的讲解过程种,大家可能会觉得进程间的通信挺费劲的,这都是情理之中的。

1.2 为什么要进程间通信??

  1. 数据传输:一个进程需要将它的数据发送给另一个进程
  2. 资源共享:多个进程之间共享同样的资源。
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  4. 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变.这点一会会模拟实现一个类似于进程池的,让主进程控制子进程做任务。

1.3 怎么做到进程间通信??

以两个进程间通信为例:由于进程间是独立性的,想要实现通信,需要找一个公共的资源,让这两个进程看到同一份资源,但这份资源又不属于这两个进程的任意一个,这样就不会破坏两者的独立性。

(1)“资源”指的是什么??是一块特定的内存空间。
(2) 这个“资源”谁提供??一般情况下os提供。
为什么不是两个进程的其中一个,假设是其中一个,另一个读取这个数据或者修改,就对拥有这个资源的进程产生影响,破坏独立性,用反证法也可以论述我开头说的第一句话。

(3) 由上面两点我们得出结论,我们进程通过访问这个“资源”,也就是这一块内存空间,进行通信,本质就是在访问os,我们的进程是通过用户编写代码形成可执行程序,形成进程,运行在os上,所以可以间接认为进程代表的就是用户,既然是用户,os系统最不信任的其实就是用户,所以在进程间进行通信的过程,就是用户之间进程通信,中间os从创建这个‘’资源‘’,使用,释放一般(今天博主讲的方式是要通过系统接口去调用,但是其他方式可能不需要)都需要通过系统接口去调用,从底层设计,这些接口设计,都需要os独立去设计的,一般操作系统都会设计一个独立的通信模块(IPC通信模块),归属于文件系统。一会再来介绍为什么归属于文件系统,这些通信之间想要实现再任何一台主机,或者不同的主机上运行,就必须采取同一种方式,就是制定一套标准,再网络部分也有标准由了这些标准才有了我们现在的互联网。

上面的一切都是我们之前学过的知识来推测出来进程间通信会这样搞的,这也更好的衔接我们的知识,那我们有哪些标准,对于本机内部(system V),对于网络(posix)
system V:

  1. System V 消息队列
  2. System V 共享内存
  3. System V 信号量

posix:

  1. 消息队列
  2. 共享内存
  3. 信号量
  4. 互斥量
  5. 条件变量
  6. 读写锁

对于system V,只讲第二点,其余两种不介绍,对于posix我们等到讲解网络的时候再讲,现在知道我们有这两个标准就好了。


管道
还有一种通信方式就是管道,这种通信方式可以说非常的简单,因为他是基于文件级别的通信方式,目前也可以简单理解复用了文件的那一套。他的大致想法是,进程管理系统和文件系统这两者是独立的,进程之间打开相同的文件,只不过把这个文件的引用计数改变一下而已,这个文件不属于两个进程中的任意一个。通过对文件的读写来交换数据,这样就保证了进程之间是独立的。

但是里面的细节还是比较多,接下来我们开始进入下一个话题。

二、管道的原理

什么是管道:
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道
再我们命令行之前使用|来表示管道,每条命令就是一个进程
在这里插入图片描述

管道的特点就是一头进一头出,是单向的,一会介绍也是单向通信方式,下面介绍的是匿名管道

因为管道是基于文件级别的通信方式,我们刚讲解完文件系统没多久,所以大家能更好的理解。以父子进程为例:因为我们现在没有办法控制两个没有关系的进程,所以使用父子进程(这也是一个铺垫)。
我们先来讲解原理一:来看图解(解释是怎么让不同的进程看到同一份资源)
在这里插入图片描述
讲解原理二:来看图解(单向通信的设计)
在这里插入图片描述

大家看到这里对于单向通信的原理应该理解了吧。
再来看一个生动的图:这里面的系统调用接口一会再介绍
在这里插入图片描述
在这里插入图片描述

通过上面的原理介绍,我们还有问题:

  1. 怎么实现双向通信?? 建立多个管道
  2. 我们上面进程是父子关系,如果两个进程没有任何关系,可以这样去设计吗??不能,只有父子,兄弟,爷孙,有血缘关系的进程才可以,常用于父子间。
  3. 我们刚才的管道有名字吗??没有,因为是内存级文件,再内存中,要名字没啥意义,所以这就是匿名文件,也就是匿名管道。
  4. 那我们进程之间进行通信了吗??没有,我们知识建立了通信信道
  5. 为什么这么费劲?进程间具有独立性,通信是有成本的。

至此我们管道的原理就讲解完毕,接下来我们去使用一些接口来进行测试一下。

三、接口的测试

我们来认识一个系统调用接口pipe,来看文档:
在这里插入图片描述
pipe就是建立进程间通信的,按照刚才的分析,自己写的程序出来默认打开的三个文件,就没有再打开其他文件,如果使用pipe建立信道,那么pipe会在内存给我们创建一个内存级文件,传进去的参数pipefd数组的返回就会带出两个文件描述符3和4,一会就来测试会不会出现这样的效果:
来看代码:

#include<stdio.h>
#include<iostream>
#include<unistd.h>
using namespace std;int main()
{int pipefd[2];int n=pipe(pipefd);if(n<0)return 1;cout<<"pipefd[0]:"<<pipefd[0]<<" "<<"pipefd[1]:"<<pipefd[1]<<endl;return 0;
}

在这里插入图片描述
我们看到结果我们分析原理的时候是一模一样的。接下来我将写一个程序来带大家实现父子进程间通信,也是为了更好介绍管道的四种情况。

为什么没有创建子进程就可以进程pipe呢??原因是我们的管道文件只要一个进成建立好。子进程拷贝父进程数据,增加一个执行那个就可以,就好比下面这个图:
在这里插入图片描述
pipefd[0]:读端
pipefd[1]:写端

四、编写代码进行通信

#include<stdio.h>
#include<iostream>
#include<unistd.h>
#include<string>
#include<stdlib.h>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;
#define N 2
#define NUM 1024void Writer(int wfd)
{string s="hello,pipe";pid_t cid=getpid();//获得当前进程的pidchar buffer[NUM];//减少io交互次数int numbers=0;int cnt=5;while(true){sleep(1);方法1//sleep(1);//buffer[0]=0;// snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),cid,numbers++);//因为多写一些内容上去,所以使用一个buffer过度一下// write(wfd, buffer, strlen(buffer));//发送给父进程方法2// string s1=s;// s+="-";// s+=to_string(cid);// s+="-";// s+=to_string(numbers++);// write(wfd,s.c_str(),strlen(s.c_str()));//发送给父进程// s=s1;/因为是不终止的给父进程写数据,所以使用了buffer,现在只给父进程写5次,每次写一个字符char c='c';write(wfd,&c,1);numbers++;cout<<"->"<<numbers<<endl;if(numbers==5){break;}}
}void Reader(int rfd)
{char buffer[NUM];while(true){buffer[0]=0;size_t n=read(rfd,buffer,sizeof(buffer));if(n>0){buffer[n]=0;//因为读取出来没有结束标志cout<<"father get msg:["<<getpid()<<"]:"<<buffer<<endl;}else if(n==0){printf("father read file done\n");break;}else{break;}}
}
int main()
{int pipefd[N]={0};int n=pipe(pipefd);if(n<0)return 1;//cout<<"pipefd[0]:"<<pipefd[0]<<" "<<"pipefd[1]:"<<pipefd[1]<<endl;pid_t id=fork();//让子进程写,父进程读取if(id<0){perror("fork");return 2;}else if(id==0){//child;//cout<<"pipefd[0]:"<<pipefd[0]<<" "<<"pipefd[1]:"<<pipefd[1]<<endl;close(pipefd[0]);//子写关闭读端//cout<<"pipefd[0]:"<<pipefd[0]<<" "<<"pipefd[1]:"<<pipefd[1]<<endl;Writer(pipefd[1]);close(pipefd[1]);exit(0);}//fatherclose(pipefd[1]);//父读关闭写端Reader(pipefd[0]);int status=0;pid_t ret=waitpid(id,&status,0);if(ret==id)//等待成功{cout<<"child:"<<ret<<",exitcode:"<<((status>>8)&0xFF)<<",signlcode:"<<(status&0x7F)<<endl;}close(pipefd[0]);sleep(5);return 0;
}

我们来看结果一:我们通过子进程给父进程不断的发送一句话

写端一直写,读端就一直读

来看结果二:子进程只给父进程发送五次,每次发送一个字符
在这里插入图片描述

写入五次之后,放下管道里面为空,就读不到数据了。

4.1管道的四种情况

  1. 读写端正常,如果管道为空,读端就要被阻塞
    我们来验证一下,我们将刚才的程序小改一下,把子进程的write函数注释掉,这样管道就是一直为空,而父进程的read不管读取成功还是失败都会返回一个值,把这个值打印出来,如果没打印出来,就说明read这个函数在等待读取,还没有返回值。为空为什么不结束,因为写端可能随时写数据进来,所以读端要阻塞等待。这个要和第三种情况区分开。
    在这里插入图片描述
  2. 读写端正常,如果管道被写满,写端就要被阻塞。
    我们的子进程一直往管道文件里面写入,一次写入一个字符,而父进程不读取,就会一直往管道文件里面写,写满了,就被阻塞了。使用一个变量记录写了多少次把管道文件写满,方便我们去计算管道文件的大小。
    在这里插入图片描述

通过结果发现我们写到65536次就开始写端就开始阻塞了,因为我们是一次往管道里面写一个字符,所以我们的管道文件大小为65536/1024=64kb, 我们使用ulimit -a来查看:cat /etc/redhat-release查看内核版本
在这里插入图片描述
在这里插入图片描述
在我这台机器的版本下,我们的管道文件大小居然是4kb与我们计算的不一样啊,这是为什么呢,我们来查看一下官方文档:man 7 page -> /page 跳转到这个文档。
在这里插入图片描述
前两点我们刚才已经认证过了,第三点提到了一个原子性,这是什么,给大家举个例子,假设子进程想给父进程写一句话hello,world,当刚写完hello,准备写world的时候,父进程看到管道里面有数据,直接就把hello读取走了,这样父进程就不是一起读到这个hello,world这个数据,所以在posix标准里面规定,写入的数据小雨这个pipe_buf大小的时候,即使管理里面有数据,父进程也不读取,这样就保证了原子性,我们可以理解为刚才看到的pipe size就是这个pipe_buf的大小,这个知识大家了解一下就可以了,主要记住管道文件是有固定大小的,看不同的内核版本,大家可以按照我上面的测试方法去计算一下自己的管道文件大小是多少。

  1. 读端正常读,写端被关闭,读端就会读到0,表明读到文件尾了,不会被阻塞。
    我们子进程先给父进程给5次,父进程一直在读取,然后直接关闭自己的写端,前五次的写入已经被读取走了,所以当子进程关闭自己的写端,那么父进程此时就发现管道文件为空,就读取到文件结尾,read函数返回的是0,因为写端已经被关闭,里面不会有数据了,所以父进程的读端如果阻塞没有意义,还占资源,所以读端就直接读结束了。
    在这里插入图片描述

我们的程序并没有想第一种情况一种在阻塞等待,而是直接结束了

  1. 写端正常写,读端被关闭,os就会直接杀掉正在写的进程
    让子进程的写端不断的给父进程写数据,父进程读取五次后就关闭自己的读端,按照结论,子进程会被杀死,这就是之前的知识,进程进程,进程没运行完异常退出了,看退出码和系统信号。
    在这里插入图片描述

我们看到我们的读端一旦关闭,子进程就被杀死退出,被父进程的waitpid获取到了,我们来看一下13号信号是什么:
在这里插入图片描述
就是管道信号,符合我们的测试。这也是博主为什么设计出父进程读,子进程写的目的,就是为了第四种情况了做实验的,等到讲解一个简单的进程池的时候,反过来让读者去感受一下

4.2 管道的五大特性

通过上面的四种情况以及管道的原理,我们很清楚的知道你名管道具有下面的五点特性

  1. 匿名管道是具有血缘关系的进程间进行通信。
  2. 只能进行单向通信
  3. 管道是基于文件操作的,而文件的生命周期是随进程的,后面介绍的有名还有共享内存都不是随着进程的生命周期的
  4. 通过管道的前两种情况,匿名管道是具有同步互斥机制的,而后面说的共享内存不具有。
  5. 管道是面向字节流的,这个后面说到多线程的时候再讲。

总结:
对于上面的情况,都是基于最上面的代码进行修改去测试的,每种情况要修改那部分博主也截出来了,大家先把我一开始写的程序理解了,然后在测试这些情况,不然很摸不清楚头脑。

五、基于管道设计一个简单的进程池

大家还记得我们的内存吃,他的见到理解就是,我们如果想要100mb的空间,我们不需要一次申请10mb,申请十次,这样会增加消耗,所以一次申请100mb,放到内存池里面,和自己打交道总比和内存打交道省事,我们的进程池也是类似的道理,先创建对歌子进程,然后想要哪个进程做事就直接分配,不需要在创建进程了,就好比公司,先做人才储备,需要的时候上,如果没有人才储备,到时候在要人就来不及了,让我们一起来看看这个进程池怎么去实现吧。

test.cpp

#include<iostream>
#include<stdio.h>
#include<string>
#include<vector>
#include<stdlib.h>#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#include "test.hpp"
using namespace std;#define process 5   //这是标志一个进程里面有多少个子进程
#define N 2  //这是管道文件返回的数组的大小 vector<task> _task;//任务数组//本程序是让父进程
class channls
{
public:channls(const int&fd,const pid_t&id,const string&processname):_fd(fd),_id(id),_processname(processname){}
public:int _fd;//文件描述符pid_t _id;//进程pidstring _processname;//进程名字,方便我们观察
};void slaver();//声明
int Initprocess(vector<channls>& cls)
{for(int i=0;i<process;i++){int pipefd[N];int n=pipe(pipefd);if(n<0){perror("pipe:");return 1;}pid_t id=fork();if(id<0){perror("fork:");return 2;}else if(id==0){//childclose(pipefd[1]);//关闭写端dup2(pipefd[0],0);close(pipefd[0]);//放在这里也可以slaver(); //为了不给这个函数传参数,才使用上面的函数,一会从键盘文件进行读取就可以了,不然就需要这样 slaver(pipefd[0]);//close(pipefd[0]);cout<<"process:"<<getpid()<<"quit"<<endl;exit(-1);}//fatherclose(pipefd[0]);//关闭读端string name="process"+to_string(i+1);cls.push_back(channls(pipefd[1],id,name));//父进程会返回子进程的id,所以这里面的id是子进程的id,将自己的写端给子进程,到时候直接往子进程里面写入//和每个进程之间都会建立一个管道文件,按照文件描述符分配规则,父进程的写端的下标会递增。}
}
void debug(const vector<channls>& cls)//测试有没有初始对不对,有没有建立进程池,将每个进程进行初始化
{for(const auto&e:cls){cout<<e._fd<<" "<<e._id<<" "<<e._processname<<endl;}
}void slaver()//子进程收到任务去执行任务
{int cmdcode=0;//收到父进程发过来的任务指令while(true){size_t n=read(0,&cmdcode,sizeof(int));//如果父进程一直没有给子进程发送数据,就会阻塞等待if(n==sizeof(int))//读取到父进程给我发送的任务了{cout<<"slaver say get a command:"<<getpid()<<": cmdcode:"<<cmdcode<<endl;if(cmdcode>0&&cmdcode<=_task.size())//下标的映射关系{_task[cmdcode-1]();//调用对应的任务cout<<endl;}}if(n==0)break;}
}void menu()
{cout<<"********************************"<<endl;cout<<"*****1. 任务1     2. 任务2*******"<<endl;cout<<"*****3. 任务3     4. 任务4*******"<<endl;cout<<"*************0.退出*************"<<endl;cout<<"********************************"<<endl;
}void quitprocess(const vector<channls>& cls);
void ctrlSlaver(const vector<channls>& cls)
{//这是一直随机给子进程分派任务// while(true)//一直给子进程发送任务,如果想控制次数,再循环里面操作break即可// {//     int which=rand()%cls.size();//随机得出子进程所在数组的下标//     int cmdcode=rand()%_task.size()+1;//因为任务也是数组存储起来的,所以父进程给子进程发一个存储任务数组的下标消息就可以了//     cout<<"father say:"<<"cmdcode:"<<cmdcode<<"already sento"<<cls[which]._id<<" processname:"<<cls[which]._processname<<endl;//     write(cls[which]._fd,&cmdcode,sizeof(int));//发送任务//     sleep(1);//每隔一秒发送一次任务给子进程// }//这是轮转的给子进程发任务// int which=0;//这是选择哪一个进程// while(true)//一直给子进程发送任务,如果想控制次数,再循环里面操作break即可// {//     //随机得出子进程所在数组的下标//     int cmdcode=rand()%_task.size()+1;//因为任务也是数组存储起来的,所以父进程给子进程发一个存储任务数组的下标消息就可以了//     cout<<"father say:"<<"cmdcode:"<<cmdcode<<"already sento"<<cls[which]._id<<" processname:"<<cls[which]._processname<<endl;//     write(cls[which]._fd,&cmdcode,sizeof(int));//发送任务//     sleep(1);//每隔一秒发送一次任务给子进程//     which++;//     which%=cls.size();// }//自己制作一个菜单给子进程发送任务menu();while(true){int which=rand()%cls.size();//随机得出子进程所在数组的下标cout<<"请输入你的选择:";int cmdcode=0;cin>>cmdcode;if(cmdcode<=0||cmdcode>4){break;}cout<<"father say:"<<"cmdcode:"<<cmdcode<<"already sento"<<cls[which]._id<<" processname:"<<cls[which]._processname<<endl;write(cls[which]._fd,&cmdcode,sizeof(int));//发送任务sleep(1);//每隔一秒发送一次任务给子进}
}void quitprocess(const vector<channls>& cls)
{for(const auto& e:cls) close(e._fd);//子进程读端被关闭,就会被信号杀掉,等着父进程回收for(const auto& e:cls) waitpid(e._id,NULL,0);
}
int main()
{LoadTask(&_task);srand(time(nullptr)^getpid()^1023);//种一个随机数种子vector<channls> cls;//就类似于进程池cout<<getpid()<<endl;Initprocess(cls);//初始化ctrlSlaver(cls);//父进程开始控制子进程quitprocess(cls);return 0;
}

test.hpp:声明和定义可以在一起的头文件,再模板那一节应该提到过

#include<stdio.h>
#include<iostream>
#include<vector>
using namespace std;
typedef void(*task)();void task1()
{cout<<"任务1"<<endl;
}void task2()
{cout<<"任务2"<<endl;
}void task3()
{cout<<"任务3"<<endl;
}void task4()
{cout<<"任务4"<<endl;
}void LoadTask(vector<task>* _t)
{_t->push_back(task1);_t->push_back(task2);_t->push_back(task3);_t->push_back(task4);
}

在这里插入图片描述

通过这个程序大家应该感觉有点意思了,这个代码的所有注释都写了,大家下去好好研究一下。

上面程序的bug
上面的程序有一个隐藏的bug,但是影响不大,大家有没有发现博主的退出进程的函数,两个循环是分成写,为什么不一起写,例如下面这样:

void quitprocess(const vector<channls>& cls)
{for(const auto& e:cls){close(e._fd);waitpid(e._id,NULL,0);}
}

给大家画个图:
在这里插入图片描述

因为我们是循环创建子进程,这样就导致,后面创建子进程的时候,父进程和上一个子进程的写端还没有关闭,就被继承下来了,这样就导致,我们的父子进程之间不是只有一个写端和一个读端的单向通信了,我们的子进程之间也可以进行互相通信,如果按照一个循环的方式去写,目的是想让写端关闭,读端就会读到文件尾,符合第三种情况,但是我们的第一个进程不止一个写端,所以一个循环是解决不了的。

但是我们的最后一个进程的管道是只有一个写端和一个读端的,我们先把这个写端关闭,这个进程就会终止,他上面继承下来的写端就会关闭,导致在他创建进程的前面的所有子进程的管道的写端指向都少一个,这样关闭释放到最后一个子进程的时候,也是只有一个写端了。

void quitprocess(const vector<channls>& cls)
{int last=cls.size()-1;for(int i=last;i>=0;i--)//从后往前释放,根据进程中指,对应的写端下标也就没有了。{close(cls[i]._fd);waitpid(cls[i]._id,NULL,0);}
}

但是我们的bug还是没有解决,因为还是存在子进程之间互相通信的可能,所以我们想要解决这个问题,我们就要使用一个数组讲父进程的写端下标保存起来,在子进程里面遍历数组将其关闭即可。在这里插入图片描述
加这三处代码也可以解决这个问题,该说不说这个bug藏的挺身,但是对这个程序影响不大,也是通过这个bug让大家可以更好的理解父子进程间通信的原理,博主认为,人只要一学过难一点的知识,前面那些一开始认为难的也会变得简单容易理解多了,这也是为什么要坚持下去的原因,只有坚持到后面,理解东西的成本就会越低,这样学起来才更有信心。

六、总结

今天讲的知识,是匿名管道,他的原理不难理解,我们要明白进程间通信的本质是什么,让不同的进程看到同一份资源,这个在后面讲解命名管道和共享内存的时候都会讲,所以希望大家好好的理解这篇的知识点,尤其要好好理解进程池这个代码,我们下篇再见

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

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

相关文章

ESP32 Arduino实战协议篇-搭建独立的 Web 服务器

在此项目中,您将创建一个带有 ESP32 的独立 Web 服务器,该服务器使用 Arduino IDE 编程环境控制输出(两个 LED)。Web 服务器是移动响应的,可以使用本地网络上的任何浏览器设备进行访问。我们将向您展示如何创建 Web 服务器以及代码如何逐步工作。 项目概况 在直接进入项目…

在线预览excel,luckysheet在vue项目中的使用

一. 需求 需要在内网项目中在线预览excel文档&#xff0c;并可以下载 二.在项目中下载并引入luckysheet 1.打开项目根目录&#xff0c;npm i luckyexcel 安装 npm i luckyexcel2.在项目的index.html文件中引入依赖 外网项目中的引入&#xff08;CDN引入&#xff09;&#…

Facebook内容的类型

随着人们日益依赖的社交媒体来进行信息获取与交流&#xff0c;Facebook作为全球最大的社交媒体平台之一&#xff0c;那么Facebook的内容都有哪些类型呢&#xff1f;下面小编来讲讲吧&#xff01; 1、实时发生的事 我们需要实时了解时事动态&#xff0c;这样可以使用户对品牌发…

003 OpenCV filter2D

目录 一、环境 二、图像卷积 三、代码演示 3.1、锐化 3.2、sobel边缘&#xff0c;x方向 3.3、sobel边缘&#xff0c;y方向 3.4、高斯模糊 3.5、完整代码 一、环境 本文使用环境为&#xff1a; Windows10Python 3.9.17opencv-python 4.8.0.74 二、图像卷积 在OpenCV…

聊聊近些年 CPU 在微架构、IO 速率上的演进过程

大家好&#xff0c;我是飞哥&#xff01; 在上一篇《深入了解 CPU 的型号、代际架构与微架构》 中我们介绍了我手头的一颗 Intel(R) Core(TM) i5 的型号规则&#xff0c;以及它的物理硬件的 Die 图结构。以及它对应的 Skylake 核的微架构实现。 不少同学开始问我其它型号的 CPU…

中移链共识机制介绍

01 为什么需要共识 共识是对某事达成的共同看法&#xff0c;它是区块链的灵魂&#xff0c;对确保区块链的完整性和安全性起着至关重要的作用。在传统的集中式系统中&#xff0c;单个实体或一组实体有权验证和记录交易。然而&#xff0c;区块链中的一个核心概念是去中心化&…

4、FFmpeg命令行操作8

生成测试文件 找三个不同的视频每个视频截取10秒内容 ffmpeg -i 沙海02.mp4 -ss 00:05:00 -t 10 -codec copy 1.mp4 ffmpeg -i 复仇者联盟3.mp4 -ss 00:05:00 -t 10 -codec copy 2.mp4 ffmpeg -i 红海行动.mp4 -ss 00:05:00 -t 10 -codec copy 3.mp4 如果音视…

整理低秩的理解

秩的定义是矩阵中非零特征值的个数。比如一个NxN的矩阵&#xff0c;它的秩为r&#xff0c;r远小于N&#xff0c;我们可以说它是低秩的。 但还有另一种情况&#xff1a;这个矩阵的的秩接近N&#xff0c;但它的特征值大多数接近于0&#xff0c;只有少数几个特征值特别大&#xf…

【DevOps】Git 图文详解(四):Git 使用入门

Git 图文详解&#xff08;四&#xff09;&#xff1a;Git 使用入门 1.创建仓库2.暂存区 add3.提交 commit 记录4.Git 的 “指针” 引用5.提交的唯一标识 id&#xff0c;HEAD~n 是什么意思&#xff1f;6.比较 diff 1.创建仓库 创建本地仓库的方法有两种&#xff1a; 一种是创建…

LoRA低秩微调技术详解

在当今快节奏的技术环境中&#xff0c;大型AI模型正在推动不同领域的突破。 然而&#xff0c;根据特定任务或数据集定制这些模型可能是一项计算和资源密集型工作。 LoRA是一种突破性且高效的微调技术&#xff0c;它利用这些高级模型的强大功能来执行自定义任务和数据集&#xf…

ForkLift:macOS文件管理器/FTP客户端

ForkLift 是一款macOS下双窗口的文件管理器&#xff0c;可以代替本地的访达。ForkLift同时具备连接Ftp、SFtp、WebDav以及云服务器。 ForkLift还具备访达不具备的小功能&#xff0c;比如从文件夹位置打开终端&#xff0c;显示隐藏文件&#xff0c;制作替换等功能。ForkLift 是一…

Spring Cloud学习(九)【Elasticsearch 分布式搜索引擎01】

文章目录 初识 elasticsearch了解 ES倒排索引ES 的一些概念安装es、kibana安装elasticsearch部署kibana 分词器安装IK分词器ik分词器-拓展词库 索引库操作mapping 映射属性索引库的 CRUD 文档操作添加文档查看、删除文档修改文档Dynamic Mapping RestClient 操作索引库什么是Re…

代码随想录二刷 | 数组 | 移除元素

代码随想录二刷 &#xff5c; 数组 &#xff5c; 移除元素 题目描述解题思路 & 代码实现暴力解法双指针法 题目描述 27. 移除元素 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要使用…

手搓哈希表、列表、队列,只为了用C语言快速求解华容道游戏,我不是大佬,只是一个游戏算法爱好者

背景 多年前曾经写过C语言求解华容道&#xff0c;当时没有用到哈希表&#xff0c;导致整个查重搜索数组过大&#xff0c;每次求解都得花上数分钟的时间&#xff0c;如今时过境迁&#xff0c;对数据结构和算法有了更深的理解&#xff0c;所以得把这一块补上了。(其实就是最近想…

Redis面经

Redis使用场景 1、缓存&#xff1a; 缓存三兄弟(穿透、击穿、雪崩) 、双写一致、持久化、数据过期策略&#xff0c;数据淘汰策略 2、分布式锁 setnx、redisson 3、消息队列 4、延迟队列 何种数据类型&#xff08;list、zset&#xff09; 缓存三兄弟 缓存穿透 缓存穿透…

Python 如何实现职责链设计模式?什么是职责链设计模式?Python 职责链设计模式示例代码

什么是职责链&#xff08;Chain of Responsibility&#xff09;设计模式&#xff1f; 职责链&#xff08;Chain of Responsibility&#xff09;设计模式是一种行为型设计模式&#xff0c;旨在构建一个对象链&#xff0c;每个对象都有机会处理请求&#xff0c;并且可以将请求传…

动手学深度学习——循环神经网络的从零开始实现(原理解释+代码详解)

文章目录 循环神经网络的从零开始实现1. 独热编码2. 初始化模型参数3. 循环神经网络模型4. 预测5. 梯度裁剪6. 训练 循环神经网络的从零开始实现 从头开始基于循环神经网络实现字符级语言模型。 # 读取数据集 %matplotlib inline import math import torchfrom torch import …

五、hdfs常见权限问题

1、常见问题 2、案例 &#xff08;1&#xff09;问题 &#xff08;2&#xff09;hdfs的超级管理员 &#xff08;3&#xff09;原因 没有使用Hadoop用户对hdfs文件系统进行操作。 在Hadoop文件系统中&#xff0c;Hadoop用户相当于Linux系统中的root用户&#xff0c;是最高级别用…

react实现步进器

创建一个步进器组件&#xff0c;包含当前步骤&#xff08;currentStep&#xff09;的状态以及前进和后退的操作&#xff1a; import React, { useState } from react;function Stepper() {const [currentStep, setCurrentStep] useState(1);const handleNext () > {setCu…

⑩② 【MySQL索引】详解MySQL`索引`:结构、分类、性能分析、设计及使用规则。

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ MySQL索引 ⑩② 【MySQL索引】1. 索引2. 索引的…