IO复用技术(1)——select/poll/epoll原理介绍及使用案例

文章目录

  • 1.Select
    • 1.1 工作流程
    • 1.2 fd_set函数
    • 1.3 select函数
    • 1.4 例程
  • 2.poll
    • 2.1 poll函数
    • 2.2 例程
  • 3.epoll
  • 3.1 工作流程
    • 3.2 相关函数
    • 3.3 epoll的两种工作模式
    • 3.4 示例代码
  • 4.总结

原理:使用一个线程来检查多个文件描述符,委托内核进行检查,如果有一个文件描述符就绪,则返回,否则阻塞直到超时,大大减少需要的线程数量、内存开销和上下文切换的CPU开销(比如一个事用1000个线程去做,但如果使用IO复用,可以只用一个线程)。

1.Select

1.1 工作流程

需要进行IO操作的socket 添加到socket
阻塞直到select系统调用返回(委托内核进行操作)
用户线程发起read请求
内核进行数据拷贝,给用户线程,完成read

image.png

有一张图非常的形象
image.png

1.2 fd_set函数

#define __FD_SETSIZE 1024typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;// 将文件描述符fd从set集合中删除 
void FD_CLR(int fd, fd_set *set); // 判断文件描述符fd是否在set集合中 
int  FD_ISSET(int fd, fd_set *set); // 将文件描述符fd添加到set集合中 
void FD_SET(int fd, fd_set *set); // 将set集合中, 所有文件描述符对应的标志位设置为0
void FD_ZERO(fd_set *set); 

主要用于将文件描述符fd与fd_set集合进行关联

1.3 select函数

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

readfds:内核检测该集合中的IO是否可读。
writefds:内核检测该集合中的IO是否可写
exceptfds:内核检测该集合中的IO是否异常
nfds:以上三个集合中最大的文件描述符数值 + 1,例如集合是{0,1,4},那么 maxfd 就是 5
timeout:用户线程调用select的超时时长
timeout = NULL,等待无限长时间
timeout = 0,不等待,立刻返回
timeout>0,等待指定时间
返回值
大于0:成功,返回集合中已就绪的IO总个数
等于-1:调用失败
等于0:没有就绪的IO

1.4 例程

先用FD_ZERO将位置0,然后使用FD_SET设置所监听的文件描述符到fd_set,select函数进行监听,当select返回大于0,则使用FD_ISSET遍历所有fd到maxfd,如果可操作,则去操作,操作完后需要用FD_CLR清除已产生的事件
假设fd = 1,fd = 2上发生事件,则select返回时,rset的值为0x0003,当处理完fd = 1的事件,调用FD_CLR后,则值变为0x0002,以此类推
服务端代码

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>int main() 
{// 创建socketint lFd = socket(PF_INET, SOCK_STREAM, 0);if (lFd < 0) {printf("socket error\n");return -1;}struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;int iRet = 0;// 绑定iRet = bind(lFd, (struct sockaddr *)&saddr, sizeof(saddr));if (iRet < 0) {printf("bind error\n");return -1;}// 监听iRet = listen(lFd, 8);if (iRet < 0) {printf("listen error\n");return -1;}int maxFd = lFd;fd_set allFdSets, tmpFdSets;FD_ZERO(&allFdSets);FD_SET(lFd, &allFdSets);while(1) {memcpy(&tmpFdSets, &allFdSets, sizeof(tmpFdSets));iRet = select(maxFd + 1, &tmpFdSets, NULL, NULL, 0);if (iRet == -1){perror("select error:");continue;}else if (iRet == 0){printf("select return no results\n");continue;}else{for (int i = lFd; i < maxFd + 1; i++){if (i == lFd){/// new clientif (FD_ISSET(lFd, &tmpFdSets)){struct sockaddr_in addr = {0};int iLen = sizeof(addr);int clientFd = accept(lFd, (struct sockaddr *)&addr, &iLen);FD_SET(clientFd, &allFdSets);maxFd = clientFd > maxFd ? clientFd : maxFd;FD_CLR(lFd, &tmpFdSets);}}else{/// msgif (FD_ISSET(i, &tmpFdSets)){char acBuf[1024] = {0};int iLen = read(i, acBuf, sizeof(acBuf));if (iLen == -1){printf("fd:%d error read ret:%d\n", i, iLen);continue;}else if (iLen == 0){printf("fd %d closed\n", i);}else{printf("fd %d, recv buf :%s, return ok\n", i, acBuf);write(i, "ok", strlen("ok"));}}}FD_CLR(i, &tmpFdSets);}}}return 0;
}

客户端代码

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {// 创建socketint fd = socket(PF_INET, SOCK_STREAM, 0);if(fd == -1) {perror("socket");return -1;}struct sockaddr_in seraddr;inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);seraddr.sin_family = AF_INET;seraddr.sin_port = htons(9999);// 连接服务器int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret == -1){perror("connect");return -1;}int num = 0;while(1) {char sendBuf[1024] = {0};sprintf(sendBuf, "data%d", num++);printf("write buf:%s\n", sendBuf);write(fd, sendBuf, strlen(sendBuf) + 1);char recvBuf[1024] = {0};// 接收int len = read(fd, recvBuf, sizeof(recvBuf));if(len == -1) {perror("read");return -1;}else if(len > 0) {printf("read buf = %s\n", recvBuf);} else {printf("服务器已经断开连接...\n");break;}sleep(1);}close(fd);return 0;
}

image.png

2.poll

2.1 poll函数

pol和select原理基本相同,使用起来稍微有点差别,它没有最大1024文件描述符限制,也不需要每次重置fd_set数组的值

struct pollfd {int fd; /* file descriptor */short events; /* events to look for */short revents; /* events returned */
};int poll(struct pollfd *fds, unsigned long nfds, int timeout);    

fds:struct pollfd类型的数组, 存储了待检测的文件描述符,struct pollfd有三个成员
fd:委托内核检测的文件描述符
events:委托内核检测的fd事件(输入、输出、错误),每一个事件有多个取值
revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
其中event的取值如下,不同事件对应不同值

nfds:描述的是数组 fds 的大小
timeout: 指定poll函数的阻塞时长 ,-1代表无限等待
返回值
-1:失败,并设置errno,可以用perror打印
大于0:检测的集合中已就绪的文件描述符的总个数

2.2 例程

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <poll.h>
#include <signal.h>int main() 
{// 创建socketint lFd = socket(PF_INET, SOCK_STREAM, 0);if (lFd < 0) {printf("socket error\n");return -1;}struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;int iRet = 0;// 绑定iRet = bind(lFd, (struct sockaddr *)&saddr, sizeof(saddr));if (iRet < 0) {printf("bind error\n");return -1;}// 监听iRet = listen(lFd, 8);if (iRet < 0) {printf("listen error\n");return -1;}int nFds = 0;struct pollfd fds[512] = {0};int maxFds = sizeof(fds)/ sizeof(fds[0]);for (int i = 0; i < maxFds; i++){fds[i].fd = -1;fds[i].events = POLLIN;}fds[0].fd = lFd;fds[0].events = POLLIN;while(1) {iRet = poll(fds, nFds + 1, -1);if (iRet == -1){perror("poll error:");continue;}else if (iRet == 0){printf("poll return no results\n");continue;}else{/// new clientif (fds[0].revents & POLLIN){struct sockaddr_in cliaddr;int len = sizeof(cliaddr);int cfd = accept(lFd, (struct sockaddr *)&cliaddr, &len);printf("new client connect\n");for (int i = 1; i < maxFds; i++){if (fds[i].fd == -1){fds[i].fd = cfd;fds[i].events = POLLIN;nFds = i > nFds ? i : nFds;printf("new client connect success, fd:%d, nFds:%d\n", fds[i].fd, nFds);break;}}}/// client have datafor (int i = 1; i <= nFds; i++){if (fds[i].revents & POLLIN){char acBuf[1024] = {0};int iLen = read(fds[i].fd, acBuf, sizeof(acBuf));if (iLen == -1){printf("fd:%d error read ret:%d\n", i, iLen);continue;}else if (iLen == 0){if (i == nFds){nFds--;}printf("fd %d closed, relase source, nFds:%d\n", fds[i].fd, nFds);fds[i].fd = -1;fds[i].events = POLLIN;}else{printf("fd %d, recv buf :%s, return ok\n", fds[i].fd, acBuf);write(fds[i].fd, "ok", strlen("ok"));}}}}}return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {// 创建socketint fd = socket(PF_INET, SOCK_STREAM, 0);if(fd == -1) {perror("socket");return -1;}struct sockaddr_in seraddr;inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);seraddr.sin_family = AF_INET;seraddr.sin_port = htons(9999);// 连接服务器int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret == -1){perror("connect");return -1;}int num = 0;while(1) {char sendBuf[1024] = {0};sprintf(sendBuf, "data%d", num++);printf("write buf:%s\n", sendBuf);write(fd, sendBuf, strlen(sendBuf) + 1);char recvBuf[1024] = {0};// 接收int len = read(fd, recvBuf, sizeof(recvBuf));if(len == -1) {perror("read");return -1;}else if(len > 0) {printf("read buf = %s\n", recvBuf);} else {printf("server disconnect...\n");break;}sleep(1);}close(fd);return 0;
}

image.png

3.epoll

Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,且epoll采用红黑树管理文件描述符,效率会更高

3.1 工作流程

正如这个图一样,epoll相比较select和poll一个比较大的优点就在于,它能够准确告知应用层是哪一个事件来了,而不需要去一个个遍历,减少很大一部分开销
image.png

epoll整体流程如下
在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是 需检测文件描述符信息(红黑树),还有一个是就绪列表,存放已改变的文件描述符信息(双向链表)
image.png

3.2 相关函数

创建epoll句柄

int epoll_create(int size);  
int epoll_create1(int flags);

控制epoll实例,主要是增加或删除需要监控的IO事件

struct epoll_event {__uint32_t events;epoll_data_t data;
};union epoll_data {void     *ptr;int       fd;uint32_t  u32;uint64_t  u64;
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfd:epoll句柄
op:操作选项
EPOLL_CTL_ADD: 向 epoll 句柄注册文件描述符对应的事件
EPOLL_CTL_DEL:向 epoll 句柄删除文件描述符对应的事件
fd:操作的文件描述符
event:注册的事件类型,并且可以通过这个结构体设置用户自定义数据
events:注册的事件类型
data:用户自定义数据

events可以是以下几个宏的集合
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

等待I/O事件就绪

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

**epfd:**epoll句柄
events:出参,代表发生变化的文件描述符信息,可能是多个
maxevents:events的结构体数组大小
timeout
-1,一直阻塞,直到有事件就绪后返回
0,不阻塞,函数马上返回
大于0:等待指定时间后返回

3.3 epoll的两种工作模式

epoll由两种工作模式,分别为LT模式(条件触发)、ET模式(边缘触发),默认为条件触发
条件触发:只要输入缓冲有数据,便一直触发事件
a. 用户不读数据,数据一直在缓冲区,epoll 会一直通知
b. 用户只读了一部分数据,epoll会通知
c. 缓冲区的数据读完了,不通知
边缘触发:只有描述符从未就绪变为就绪时,才会为文件描述符发送一次就绪通知,之后不再通知
减少了事件被重复触发的次数,效率比LT模式高,且可以分离接收数据和处理数据的时间点,工作在该模式必须要使用非阻塞等待
a. 用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不再次通知了
b. 用户只读了一部分数据,epoll不再次通知
c. 缓冲区的数据读完了,不再次通知

3.4 示例代码

条件触发模式

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>int main() {// 创建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;// 绑定bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));// 监听listen(lfd, 8);// 调用epoll_create()创建一个epoll实例int epfd = epoll_create(100);// 将监听的文件描述符相关的检测信息添加到epoll实例中struct epoll_event epev;epev.events = EPOLLIN;epev.data.fd = lfd;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);struct epoll_event epevs[1024];while(1) {int ret = epoll_wait(epfd, epevs, 1024, -1);if(ret == -1) {perror("epoll_wait");exit(-1);}printf("ret = %d\n", ret);for(int i = 0; i < ret; i++) {int curfd = epevs[i].data.fd;if(curfd == lfd) {// 监听的文件描述符有数据达到,有客户端连接struct sockaddr_in cliaddr;int len = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);epev.events = EPOLLIN;epev.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);} else {if(epevs[i].events & EPOLLOUT) {continue;}   // 有数据到达,需要通信char buf[1024] = {0};int len = read(curfd, buf, sizeof(buf));if(len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);} else if(len > 0) {printf("read buf = %s\n", buf);write(curfd, buf, strlen(buf) + 1);}}}}close(lfd);close(epfd);return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {// 创建socketint fd = socket(PF_INET, SOCK_STREAM, 0);if(fd == -1) {perror("socket");return -1;}struct sockaddr_in seraddr;inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);seraddr.sin_family = AF_INET;seraddr.sin_port = htons(9999);// 连接服务器int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret == -1){perror("connect");return -1;}int num = 0;while(1) {char sendBuf[1024] = {0};// sprintf(sendBuf, "send data %d", num++);fgets(sendBuf, sizeof(sendBuf), stdin);write(fd, sendBuf, strlen(sendBuf) + 1);// 接收int len = read(fd, sendBuf, sizeof(sendBuf));if(len == -1) {perror("read");return -1;}else if(len > 0) {printf("read buf = %s\n", sendBuf);} else {printf("服务器已经断开连接...\n");break;}}close(fd);return 0;
}

4.总结

IO复用中epoll会更高效,内存拷贝次数少,时间复杂度低,且不受fd数量的限制

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

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

相关文章

Mac OS系统如何更新

用了好几年的Mac Book安装软件经常提示需要更高的系统版本&#xff0c;因此要升级系统版本&#xff0c;但是开始在系统设置里面找了一下没有找到升级的按钮&#xff0c;找了资料后才知道如何升级。有以下两种入口 一、App Store搜索MacOs&#xff0c;在出现的搜索结果中选择下载…

微服务----nacos配置及简单使用

目录 什么是nacos 项目在nacos上进行注册 注入nacos依赖 配置application.yml文件 nacos写入配置文件 首先&#xff0c;还是需要导入依赖 然后在nacos中编写配置文件 prod是我自定义的一个命名空间&#xff0c;在这里面进行配置文件编写~ 启动类上加上注解 编写Patt…

SpringBoot+Vue项目企业客户管理系统

一、前言介绍 本文主要论述了如何使用JAVA语言开发一个企业客户管理系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述企业客户管理系统的当前背景以及系统开…

Qt之信号与槽

槽的本质&#xff1a;对信号响应的函数。 信号函数和槽函数通常位于某个类中&#xff0c;和普通的成员函数相⽐&#xff0c;它们的特别之处在于&#xff1a; 信号函数⽤ signals 关键字修饰&#xff0c;槽函数⽤ public slots、protected slots 或者 private slots 修饰。sign…

数据结构---单链表

题目&#xff1a;构造一个单链表。 使用的软件&#xff1a;VS2022使用的语言&#xff1a;C语言使用的项目&#xff1a;test.c Setlist.h Setlish.c 项目实践&#xff1a; Setlist.h的代码为&#xff1a; #pragma once#include<stdio.h> #include<stdlib.h> #incl…

【算法小白周赛2】A.朋友遍天下(easy)C++代码和题解

题目链接&#xff1a;https://www.starrycoding.com/problem/165 题目描述 本题与hard版本有一定区别&#xff0c;仅“拜访时间安排表”的规则相同&#xff0c;hard版本需要进行一定修改才能通过easy版本。 醋酸锌有许多的好友&#xff0c;为了能好好拜访他的每一个好友&…

Java | Leetcode Java题解之第67题二进制求和

题目&#xff1a; 题解&#xff1a; class Solution {public String addBinary(String a, String b) {StringBuffer ans new StringBuffer();int n Math.max(a.length(), b.length()), carry 0;for (int i 0; i < n; i) {carry i < a.length() ? (a.charAt(a.leng…

单调栈|496.下一个更大元素I

力扣题目链接 class Solution { public:vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {stack<int> st;vector<int> result(nums1.size(), -1);if (nums1.size() 0) return result;unordered_map<int, …

连接和使用vCenter Server嵌入式vPostgres数据库

vCenter Server 早期支持内嵌(embedded)和外部(external)数据库,内嵌数据库就是vPostgres,基于VMware Postgres数据库(PostgreSQL数据库),外部数据库用的多的是Oracle数据库和SQL Server数据库。因为早期使用内嵌的PostgreSQL数据库只能用于小型环境,比如仅支持几十台…

SpringBoot对接前端传递的base64编码的图片信息,转成图片以Get请求进行浏览器文件下载,不下载到本地。

一、问题描述 1.1需求描述。 前端将浏览器展示的图片以base64编码的形式传递给后端&#xff0c;以此实现文件下载的功能&#xff0c;在浏览器弹出文件下载框。效果如下 1.2实现思路 将前端传递的base64进行解码&#xff0c;设置响应头返回响应体&#xff0c;代码如下。 pu…

ubuntu22.04 cmake 配置mysql

报错信息&#xff1a; CMake Error at CMakeLists.txt:33 (find_package): By not providing “FindMySQL.cmake” in CMAKE_MODULE_PATH this project has asked CMake to find a package configuration file provided by “MySQL”, but CMake did not find one. Could not…

【02358单片机原理及应用】第三、四、五章考试复习自考复习

第3章 80C51单片机指令系统 考试知识点&#xff1a; 1、寻址方式 &#xff08;1&#xff09;立即寻址&#xff08;#data&#xff0c;#data16&#xff09;例&#xff1a;MOV A&#xff0c;#00H &#xff08;2&#xff09;直接寻址&#xff08;direct&#xff09;内部RAM…

古典密码学简介

目录 C. D. Shannon: 一、置换密码 二、单表代替密码 ① 加法密码 ② 乘法密码 ③密钥词组代替密码 三、多表代替密码 代数密码 四、古典密码的穷举分析 1、单表代替密码分析 五、古典密码的统计分析 1、密钥词组单表代替密码的统计分析 2、英语的统计规…

电商核心技术揭秘四十七:社会化营销中的社区建设

相关系列文章 电商技术揭秘相关系列文章合集&#xff08;1&#xff09; 电商技术揭秘相关系列文章合集&#xff08;2&#xff09; 电商技术揭秘相关系列文章合集&#xff08;3&#xff09; 电商技术揭秘四十一&#xff1a;电商平台的营销系统浅析 电商技术揭秘四十二&#…

【业务场景】京东实际场景,频繁GC引起的CPU飙高问题的解决

目录 1.业务介绍 2.判断任务类型 3.CPU飙高的原因 1.业务介绍 本文的业务场景是京东零售线公开的一篇文章&#xff0c;文章内容详细介绍了京东零售线如何将广告相关的定时任务从半小时优化到秒级的&#xff0c;原文链接&#xff1a; 半小时到秒级&#xff0c;京东零售定时…

登封授牌,花落郑州

近日&#xff0c;“大禹故里故都”授牌仪式在河南省登封市隆重举行&#xff0c;河南省社科院有关单位将匾牌授予登封市。报道称&#xff1a;至此&#xff0c;千百年来备受争议的大禹故里、故都问题&#xff0c;终于尘埃落定&#xff0c;华夏立国始祖大禹终于魂归故里。 略有微词…

QT+串口调试助手+基本版

一、创建串口调试助手UI界面 1、首先生成串口连接必要参数界面&#xff0c;删除关闭串口控件 2、给参数下拉框添加常见的选项&#xff0c;删除关闭串口控件 3、将串口调试助手参数界面布局整齐&#xff0c;删除关闭串口控件 4、更改控件名字&#xff0c;方便后续编程&#xff…

OpenHarmony实战开发-动画曲线、如何实现动画衔接

UI界面除了运行动画之外&#xff0c;还承载着与用户进行实时交互的功能。当用户行为根据意图变化发生改变时&#xff0c;UI界面应做到即时响应。例如用户在应用启动过程中&#xff0c;上滑退出&#xff0c;那么启动动画应该立即过渡到退出动画&#xff0c;而不应该等启动动画完…

特斯拉FSD落地分析

再续前缘 媒体的神经从马斯克的湾流私人飞机起飞那一刻开始,就开始被牵动着。28/4 号的突然访华,在大多数人看来其实已经早已是计划之中,从摆在台面上的消息来看,主要目的是为了在大陆推广FSD的落地,也为8月份FSD 的正式版本做预热,和中国上海的第一次联姻造就了特斯拉m…

孪生网络、匹配网络和原型网络:详解与区分

孪生网络、匹配网络和原型网络 孪生网络、匹配网络和原型网络&#xff1a;详解与区分孪生网络&#xff08;Siamese Networks&#xff09;核心概念工作原理 匹配网络&#xff08;Matching Networks&#xff09;核心概念工作原理 原型网络&#xff08;Prototypical Networks&…