【Linux C | I/O模型】IO复用 | poll、ppoll函数详解

news2024/10/6 2:29:59

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍poll、ppoll函数 🍭
😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你——泰戈尔🍭
⏰发布时间⏰:2024-02-02 13:51:20

本文未经允许,不得转发!!!

目录

  • 🎄一、概述
  • 🎄二、poll 函数介绍
  • 🎄三、poll 函数使用步骤
  • 🎄四、poll 函数使用例子
  • 🎄五、ppoll 函数及例子
  • 🎄六、总结


在这里插入图片描述

🎄一、概述

在Unix / Linux系统中,有五种IO模型:阻塞I/O模型、非阻塞I/O模型、复用式I/O模型、信号驱动式I/O模型、异步I/O模型。其中,复用式I/O模型的实现方式之一就是使用poll函数来阻塞等待内核准备好数据,等数据准备好了之后,再通知应用层的进程调用IO操作函数来获取数据。

在这里插入图片描述

下面将介绍poll函数以及poll函数的使用方法。


在这里插入图片描述

🎄二、poll 函数介绍

poll 函数原型:

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll函数与select函数有类似的任务:它等待一组文件描述符中的一个描述符的I/O操作准备就绪。需要了解select函数的,可以看文章:IO复用 | select、pselect函数详解。

参数说明

  • fds:传入传出参数,指向struct pollfd类型数组的首元素,每个数组元素指定一个描述符以及对其关心的状态,关于这个结构体的说明在本小节后面阐述。
  • nfds:指明fds指向的数组元素个数。
  • timeout:该参数指定poll阻塞等待文件描述符就绪的毫秒数。会被四舍五入到系统时钟粒度。这个参数有三种可能:
    • 1、timeout设置为负数:一直阻塞等待,直到有描述符准备就绪;
    • 2、timeout设置为 0:不等待,检查描述符后直接返回。
    • 3、timeout设置为正数:阻塞等待timeout设置的毫秒数,期间有描述符准备就绪就返回,没有就等到时间结束返回。

返回值:成功返回已准备就绪的描述符个数;超时返回0;失败返回-1。


看完上面poll函数的介绍,可以发现,与select不同的是,poll函数不是为每个状态构造一个描述符集,而是构造一个struct pollfd结构体的数组,每个数组元素指定一个描述符以及对其关心的状态。当成功返回时,内核也会对该结构体赋值,将准备就绪的描述符和状态给到应用层进程,所以fds参数是传入传出参数。

struct pollfd结构体说明:

struct pollfd {
	int   fd;         /* file descriptor */
	short events;     /* requested events */
	short revents;    /* returned events */
};

调用时,fd表示描述符,events字段指明感兴趣的读写事件;
成功返回时,fd表示准备就绪描述符,revents字段指明准备就绪的描述符的状态。

events字段、revents字段取值如下:
在这里插入图片描述


在这里插入图片描述

🎄三、poll 函数使用步骤

poll函数的使用步骤主要分成2步:

  • 1、将我们感兴趣的描述符和事件添加到struct pollfd数组,调用poll 函数并指定超时毫秒数;
    #define POLLFDS_NUM	10
    struct pollfd pollfds[POLLFDS_NUM];
    pollfds[0].fd = listenFd;
    pollfds[0].events = POLLIN;
    
    int ret = poll(&pollfd, 1, 120 * 1000);
    
  • 2、查询准备就绪描述符。函数成功返回后,如果返回值大于0,表示准备就绪的描述符个数,需要遍历数组,查看各个元素revents字段表示的事件。
    if(pollfd[0].revents & POLLIN)
    {
    	...
    }
    

在这里插入图片描述

🎄四、poll 函数使用例子

下面使用TCP套接字来演示select的使用例子,如果要执行的话,分别保存服务端、客户端代码为poll_tcpSer.cpoll_tcpCli.c,然后执行下面代码编译:

gcc poll_tcpSer.c -o ser 
gcc poll_tcpCli.c -o cli

然后先在一个命令行窗口运行服务端./ser,再重新打开一个命令行窗口运行客户端./cli。下面是服务端的运行结果:
在这里插入图片描述


TCP服务端代码如下:使用poll阻塞等待可读描述符返回。

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include <sys/poll.h>

#define POLLFDS_NUM	11

int poll_wait(int listenFd, int connFd[10], int connNum)
{
	if(connNum >= POLLFDS_NUM)
	{
		printf("error connNum=%d\n", connNum);
		return -1;
	}
	
	// 1、准备感兴趣的描述和读事件
	struct pollfd pollfds[POLLFDS_NUM];
	pollfds[0].fd = listenFd;
	pollfds[0].events = POLLIN;
	
	int i = 0;
	for(i=0; i<connNum; i++)
	{
		pollfds[1+i].fd = connFd[i];
		pollfds[1+i].events = POLLIN;
	}
	
	int nfds = connNum+1;
	int ret = poll(pollfds, nfds, 120 * 1000);// 阻塞等待120秒
	
	// 2、查询准备就绪描述符
	if(ret > 0)// 即使有多个fd可读,我们也只第一个准备就绪描述符,其他的下次处理
	{
		printf("success, Number of descriptors returned is %d\n", ret);
		for(i=0; i<nfds; i++)
		{
			if(pollfds[i].revents & POLLIN)
			{
				return pollfds[i].fd;
			}
		}
	}
	else if(ret == 0)
	{
		printf("poll timeout\n");
	}
	return -1;
}

int del_connfd(int connFds[10], int connNum, int delFd) // 从connFds数组中删除delFd
{
	int tmpFds[10]={0,};
	int i = 0, index = 0;;
	memcpy(tmpFds, connFds, connNum*sizeof(connFds[0]));
	for(i=0; i<connNum; i++)
	{
		if(tmpFds[i] != delFd)
		{
			connFds[index++] = tmpFds[i];
		}
	}
	return index;
}

int main()
{
	// 1、创建TCP套接字socket
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd<0)
	{
		perror("socket error" );
		return -1;
	}
	
	// 2、准备服务端ip和端口
	struct sockaddr_in servaddr;
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons (10086);
	servaddr.sin_addr.s_addr = INADDR_ANY; // 指定ip地址为 INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任一网络接口上接受客户端的连接
	
	// 3、绑定 bind
	if (bind(sockfd,(struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
	{
		perror("bind error" );
		return -1;
	}
	
	// 4、监听 listen
	if(listen(sockfd, 10) != 0)
	{
		perror("listen error");
		return -1;
	}
	
	printf("TcpSer sockfd=%d, start listening\n",sockfd);
	char recvline[256];
	int connFds[10];
	int connNum = 0;
	while(1)
	{
		int readableFd = poll_wait(sockfd, connFds, connNum);
		
		if(readableFd == -1)
		{
			return -1;
		}
		
		if(readableFd == sockfd)// 监听描述符可读,使用accept获取客户端套接字
		{
			printf("listenFd %d is alread to read, calling accept\n", sockfd);
			int connfd = accept(sockfd, NULL, NULL);
			if(connfd < 0)
			{
				perror("accept error" );
				return -1;
			}
			connFds[connNum] = connfd;
			connNum++;
		}
		else // 客户端发数据过来,读取打印
		{
			int connfd = readableFd;
			printf("connfd %d is alread to read\n", connfd);
			int n = read(connfd, recvline, sizeof(recvline));
			if(n>0)
			{
				recvline[n] = 0 ;/*null terminate */
				printf("recv connfd=%d [%s]\n",connfd,recvline);
				write(connfd, "Hello,I am tcp server", strlen("Hello,I am tcp server"));
			}
			printf("close connfd=%d\n",connfd);
			close(connfd);
			connNum = del_connfd(connFds, connNum, connfd);
		}
	}
	
	// 7、关闭
	close(sockfd);

	return 0;
}

TCP客户端代码如下,连接服务端后发送数据、读取数据:

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
	// 1、创建TCP套接字socket
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd<0)
	{
		perror("socket error" );
		return -1;
	}
	
	// 2、准备服务端ip和端口
	struct sockaddr_in servaddr;
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons (10086);
	if (inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr) <= 0) // 设置本机IP为服务端IP
		perror("inet_pton error");
	
	// 3、连接 connect
	if (connect(sockfd,(struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
	{
		perror("connect error");
		return -1;
	}
	
	// 4、交换数据
	printf("TcpCli sockfd=%d, send msg\n",sockfd);
	write(sockfd, "Hello,I am tcp client", strlen("Hello,I am tcp client"));
	
	char recvline[256];
	int n = 0;
	while ( (n = read (sockfd, recvline, sizeof(recvline))) > 0)
	{
		recvline[n] = 0 ;/*null terminate */
		printf("recv[%s]\n",recvline);
	}
	
	if (n < 0)
		perror("read error" );
	
	// 5、关闭
	printf("close fd %d\n",sockfd);
	close(sockfd);

	return 0;
}

在这里插入图片描述

🎄五、ppoll 函数及例子

在Linux系统中还有个ppoll函数,其函数原型如下:

#include <sys/poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
               const struct timespec *timeout_ts, const sigset_t *sigmask);

可以看到,和poll基本差不多,但有下面三个区别:

  • 1、poll函数使用的timeout参数是int表示的毫秒数,而ppoll函数使用struct timespec(以秒或纳秒表示)。
  • 2、poll可以更新timeout参数以指示剩余的时间。ppoll的timeout参数声明为const确保其不会更改此参数。
  • 3、ppoll可以选择使用的信号屏蔽字。若sigmask为空,那么在与信号有关的方面,ppoll的运行状况和poll相同。否则,sigmask指向一信号屏蔽字,在调用ppoll时,以原子操作的方式安装该信号屏蔽字。在返回时恢复以前的信号屏蔽字。

下面将poll函数例子的服务端修改为ppoll函数,且屏蔽掉ctrl+c产生的信号,修改后代码如下,客户端还是用前面的客户端即可:

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include <sys/poll.h>

#include <signal.h>

#define POLLFDS_NUM	11

int poll_wait(int listenFd, int connFd[10], int connNum)
{
	if(connNum >= POLLFDS_NUM)
	{
		printf("error connNum=%d\n", connNum);
		return -1;
	}
	
	// 1、准备感兴趣的描述和读事件
	struct pollfd pollfds[POLLFDS_NUM];
	pollfds[0].fd = listenFd;
	pollfds[0].events = POLLIN;
	
	int i = 0;
	for(i=0; i<connNum; i++)
	{
		pollfds[1+i].fd = connFd[i];
		pollfds[1+i].events = POLLIN;
	}
	
	struct timespec timeout;
	timeout.tv_sec = 2*60;
	timeout.tv_nsec= 0;
	
	sigset_t sigmask;
	sigemptyset(&sigmask);
	sigaddset(&sigmask, SIGINT);  // ctrl + c
	
	int nfds = connNum+1;
	int ret = ppoll(pollfds, nfds, &timeout, &sigmask);// 阻塞等待120秒
	
	// 2、查询准备就绪描述符
	if(ret > 0)// 即使有多个fd可读,我们也只第一个准备就绪描述符,其他的下次处理
	{
		printf("success, Number of descriptors returned is %d\n", ret);
		for(i=0; i<nfds; i++)
		{
			if(pollfds[i].revents & POLLIN)
			{
				return pollfds[i].fd;
			}
		}
	}
	else if(ret == 0)
	{
		printf("poll timeout\n");
	}
	return -1;
}

int del_connfd(int connFds[10], int connNum, int delFd) // 从connFds数组中删除delFd
{
	int tmpFds[10]={0,};
	int i = 0, index = 0;;
	memcpy(tmpFds, connFds, connNum*sizeof(connFds[0]));
	for(i=0; i<connNum; i++)
	{
		if(tmpFds[i] != delFd)
		{
			connFds[index++] = tmpFds[i];
		}
	}
	return index;
}

int main()
{
	// 1、创建TCP套接字socket
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd<0)
	{
		perror("socket error" );
		return -1;
	}
	
	// 2、准备服务端ip和端口
	struct sockaddr_in servaddr;
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons (10086);
	servaddr.sin_addr.s_addr = INADDR_ANY; // 指定ip地址为 INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任一网络接口上接受客户端的连接
	
	// 3、绑定 bind
	if (bind(sockfd,(struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
	{
		perror("bind error" );
		return -1;
	}
	
	// 4、监听 listen
	if(listen(sockfd, 10) != 0)
	{
		perror("listen error");
		return -1;
	}
	
	printf("TcpSer sockfd=%d, start listening\n",sockfd);
	char recvline[256];
	int connFds[10];
	int connNum = 0;
	while(1)
	{
		int readableFd = poll_wait(sockfd, connFds, connNum);
		
		if(readableFd == -1)
		{
			return -1;
		}
		
		if(readableFd == sockfd)// 监听描述符可读,使用accept获取客户端套接字
		{
			printf("listenFd %d is alread to read, calling accept\n", sockfd);
			int connfd = accept(sockfd, NULL, NULL);
			if(connfd < 0)
			{
				perror("accept error" );
				return -1;
			}
			connFds[connNum] = connfd;
			connNum++;
		}
		else // 客户端发数据过来,读取打印
		{
			int connfd = readableFd;
			printf("connfd %d is alread to read\n", connfd);
			int n = read(connfd, recvline, sizeof(recvline));
			if(n>0)
			{
				recvline[n] = 0 ;/*null terminate */
				printf("recv connfd=%d [%s]\n",connfd,recvline);
				write(connfd, "Hello,I am tcp server", strlen("Hello,I am tcp server"));
			}
			printf("close connfd=%d\n",connfd);
			close(connfd);
			connNum = del_connfd(connFds, connNum, connfd);
		}
	}
	
	// 7、关闭
	close(sockfd);

	return 0;
}

保存为ppoll_tcpSer.c,编译运行后,结果如下,

执行完按ctrl+c无法结束进程,可以用kill -9 pid进程号终止该进程,操作如下:

$ ps aux | grep ser
wkd        51448  0.0  0.0   4212  1436 pts/12   S+   17:09   0:00 ./ser
wkd        51451  0.0  0.1  15960  2092 pts/17   S+   17:10   0:00 grep --color=auto ser
$ kill -9 51448

或者等待ppoll超时后,会处理ctrl+c的信号的,也就退出了


在这里插入图片描述

🎄六、总结

👉本文先是介绍poll函数,然后介绍使用poll的步骤,并给出了使用poll的C语言例子,最后介绍了ppoll函数并给出C语言使用例子。看完有不懂的,可以留意。

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

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

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

相关文章

社区商铺投资指南:鲜奶吧——最具潜力的开店项目

作为一位开店5年的鲜奶吧创业者&#xff0c;我深知在社区商铺中寻找一个具有潜力的项目并非易事。 今天为大家分享鲜奶吧项目的好处&#xff0c;帮助你在社区商铺投资中做出明智的选择。 一、鲜奶吧的市场潜力与前景 随着健康饮食观念的普及&#xff0c;鲜奶及酸奶制品越来越…

【AIGC风格prompt深度指南】掌握绘画风格关键词,实现艺术模仿的革新实践

[小提琴家]ASCII风格&#xff0c;点&#xff0c;爆炸&#xff0c;光&#xff0c;射线&#xff0c;计算机代码 由冰和水制成的和平标志]非常详细&#xff0c;寒冷&#xff0c;冰冻&#xff0c;大气&#xff0c;照片逼真&#xff0c;流动&#xff0c;16K 胡迪尼模拟火和水&#x…

【复现】万户 ezOFFICE SQL注入漏洞_42

目录 一.概述 二 .漏洞影响 三.漏洞复现 1. 漏洞一&#xff1a; 四.修复建议&#xff1a; 五. 搜索语法&#xff1a; 六.免责声明 一.概述 万户ezOFFICE协同管理平台分为企业版和政务版。 解决方案由五大应用、两个支撑平台组成&#xff0c;分别为知识管理、工作流程、沟…

【Vue】mvc,mvp,mvvm 的区别

总结&#xff1a; MVC : view 视图层依靠 model 来进行更新渲染&#xff0c;当数据发生改变时。第一步: 通知 controller 进行更新&#xff0c;然后第二步 controller 再通知 model 进行更新。最后 model 才将 view 更新 , 这样大量的逻辑以及更新操作&#xff0c;都需要在con…

设计师常用的软件有哪些?推荐5款设计工具

设计软件的使用对设计师来说非常重要。设计工具的使用是否直接影响到最终结果的质量&#xff0c;然后有人会问&#xff1a;设计需要使用什么软件&#xff1f;这里有一些设计师和那些对设计感兴趣的朋友列出了五个有用的设计工具。 1、即时设计 即时设计操作简单&#xff0c;内…

机器学习 | 一文看懂SVM算法从原理到实现全解析

目录 初识SVM算法 SVM算法原理 SVM损失函数 SVM的核方法 数字识别器(实操) 初识SVM算法 支持向量机&#xff08;Support Vector Machine&#xff0c;SVM&#xff09;是一种经典的监督学习算法&#xff0c;用于解决二分类和多分类问题。其核心思想是通过在特征空间中找到一…

Pymysql之Cursor常用API

Cursor常用API 1、cursor.execute(query, argsNone)&#xff1a;执行sql语句。 参数: query (str)&#xff1a;sql语句。 args (tuple, list or dict)&#xff1a;sql语句中如果有变量&#xff0c;或者格式化输出&#xff0c;会在这里填充数据。 Returns&#xff1a;返…

编程实例分享,手表养护维修软件钟表维修开单管理系统教程

编程实例分享&#xff0c;手表养护维修软件钟表维修开单管理系统教程 一、前言 以下教程以 佳易王钟表维护维修管理系统软件V16.0为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 左侧为导航栏&#xff0c; 1、系统设置&#xff1a;可以设置打…

代码随想录算法训练营DAY15 | 二叉树 (2)

一、LeetCode 102 二叉树的层序遍历 题目链接&#xff1a; 102.二叉树的层序遍历https://leetcode.cn/problems/binary-tree-level-order-traversal/ 思路&#xff1a;利用队列的先进先出特性&#xff0c;在处理本层节点的同时将下层节点入队&#xff0c;每次处理一层的节点&…

C#中的浅度和深度复制(C#如何复制一个对象)

文章目录 浅度和深度复制浅度复制深度复制如何选择 浅度和深度复制 在C#中&#xff0c;浅度复制&#xff08;Shallow Copy&#xff09;和深度复制&#xff08;Deep Copy&#xff09;是两种不同的对象复制方式&#xff0c;满足不同的应用场景需求&#xff0c;它们主要区别在于处…

【JavaWeb】头条新闻纯JavaWeb项目实现 项目搭建 数据库工具类导入 跨域问题 Postman 第一期 (前端Vue3+Vite)

文章目录 一、项目简介1.1 微头条业务简介1.2 技术栈介绍 二、项目部署三、准备工具类3.1 异步响应规范格式类3.2 MD5加密工具类3.3 JDBCUtil连接池工具类3.4 JwtHelper工具类3.4 JSON转换的WEBUtil工具类 四、准备各层的接口和实现类4.1 准备实体类和VO对象4.2 DAO层接口和实现…

Guitar Pro正版多少钱 Guitar Pro购买后永久使用吗

相信很多玩吉他的小伙伴都听说过Guitar Pro这款软件&#xff0c;Guitar Pro是一款传奇的吉他谱软件&#xff0c;可以用来打谱&#xff0c;看谱&#xff0c;midi音序制作等等&#xff0c;同时做为一款吉他学习辅助软件有着强大的优势&#xff0c;那大家知道Guitar Pro正版多少钱…

渗透测试-信息打点与架构分析细节梳理

渗透测试-信息打点与架构分析细节梳理 为了保障信息安全&#xff0c;我在正文中会去除除靶场环境的其他任何可能的敏感信息 什么是网站架构 网站架构包括网站的方方面面&#xff0c;下面是常见的内容&#xff1a; 前端&#xff08;Front-End&#xff09;&#xff1a; 使用Reac…

算法学习——LeetCode力扣哈希表篇1

算法学习——LeetCode力扣哈希表篇1 242. 有效的字母异位词 242. 有效的字母异位词 - 力扣&#xff08;LeetCode&#xff09; 描述 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 注意&#xff1a;若 s 和 t 中每个字符出现的次数都相同…

请查收,你的2023京东零售技术年度好文

新春佳节&#xff0c;万象更新&#xff01;京东零售技术在2023年度发布文章内容145篇&#xff0c;全年阅读量超过20万次&#xff5e;衷心感谢每一位读者一直以来的关注和支持。 在新春到来之际&#xff0c;我们精选年度好文分享给大家&#xff0c;希望大家温故知新&#xff0c…

基于 NXP S32K344 的汽车通用评估板方案

S32K3xx 系列是 NXP 基于 ARMCortex-M7 的汽车工业级 MCU&#xff0c;符合 ISO26262 ASIL-D 汽车功能安全等级&#xff0c;支持 ASIL B/D 安全应用&#xff0c;提供了一个可扩展的平台&#xff0c;具有下一代安全性、可扩展性、连接性和低功耗特性。适用于可能会在严酷环境下工…

政安晨:机器学习快速入门(四){pandas与scikit-learn} {随机森林}

咱们将在这篇文章中使用更复杂的机器学习算法。 随机森林 基本定义 随机森林(Random Forest)是一种机器学习算法&#xff0c;属于集成学习(ensemble learning)的一种。它是通过构建多个决策树&#xff08;即森林&#xff09;来进行预测和分类的。 随机森林的主要特点是采用了…

「云原生可观测团队」获选「InfoQ 年度技术内容贡献奖」

随着云原生、人工智能逐渐成为各行各业的创新生产力工具。可以预见&#xff0c;我们即将进入全新的智能化时代。随着数据成为新型生产要素&#xff0c;云和 AI 正走向深度融合。云原生通过提供大规模多元算力的高效供给&#xff0c;可观测成为业务创新的核心基础设施&#xff0…

Future和FutureTask

Future和FutureTask Future类Future主要方法get()get(long timeout,TimeUnit unit)cancel()isDone()isCancelled() 用线程池的submit方法返回Future对象用FutureTask来创建Future Future类 FutureTask叫未来任务&#xff0c;可以将一个复杂的任务剔除出去交给另外一个线程来完…

基于LLM的Agent的兴起及其潜力:综述

原文链接&#xff1a;https://arxiv.org/pdf/2309.07864v1.pdf 1. Introduction LLM-based Agent的基本构成。本文认为&#xff0c;构成LLM-based Agent的核心部件有三个&#xff1a; brain: 主要目标有2个—信息记忆、信息处理perception: 主要目标在于让agent能够感受到更…