Linux socket编程(6):IO复用之select原理及例子

文章目录

  • 1 五种I/O模型
    • 1.1 阻塞I/O模型
    • 1.2 非阻塞I/O模型
    • 1.3 I/O复用模型
    • 1.4 信号驱动I/O模型
    • 1.5 异步I/O模型
  • 2 select函数
  • 3 select实战:实现多个套接字监听
    • 3.1 客户端
    • 3.2 服务端
    • 3.3 实验结果
    • 3.4 完整代码

在之前的网络编程中,我们遇到了一个问题:

  • 客户端需要一边监听来自stdin的键盘输入,一边监听来自服务端的消息

  • 服务端要一边获取来自客户端的消息,一边accept新的设备连接

也就是我们希望在一个或多个I/O条件准备就绪时,能够得到通知。在前面的文章中,我们使用Linux中的fork实现这些功能:利用fork实现服务端与多个客户端建立连接。但在Linux中还有一个I/O多路复用的概念,它由selectpoll函数实现,这篇文章就来介绍一下多路复用的概念。

1 五种I/O模型

通常,一个输入操作有两个阶段:

  1. 等待数据准备就绪:等待数据在网络上到达,当数据包到达时,它被复制到内核的缓冲区中
  2. 将数据从内核复制到进程:将准备好的数据从内核缓冲区复制到应用程序缓冲区

下面来了解一下Linux中的五种I/O模型:

1.1 阻塞I/O模型

I/O的最常见模型是阻塞I/O模型。默认情况下,所有套接字都是阻塞的。用户通过调用recv/recvfrom/read等函数阻塞等待数据的到来,这些函数称为系统调用,会从应用程序中切换到内核中运行,在得到了一定的数据后返回到应用程序。如下图所示:

在这里插入图片描述

在上图中,进程调用recvfrom,系统调用在数据到达时将数据复制到应用程序缓冲区,然后返回。或者发生错误时也会返回,最常见的错误是系统调用被信号中断。

1.2 非阻塞I/O模型

当一个套接字被设置为非阻塞时,recv/recvfrom/read会立即返回,有数据则返回数据,而没数据也不会等待条件满足,而是立即返回一个错误EWOULDBLOCK

在这里插入图片描述

对于前三次的recvfrom,没有要返回的数据,内核立即返回EWOULDBLOCK错误。 在第四次调用recvfrom时,一个数据报已准备好,它被复制到我们的应用程序缓冲区中,recvfrom成功返回。应用程序不断地轮询内核,以查看是否有某个操作准备就绪,这通常很占CPU的资源,一般很少使用。

当然除了用在读函数中,一个我实际中用到的例子是在connect函数中使用非阻塞。比如我们设备中有一个以太网,正常阻塞连接的话还需要以太网相关的驱动支持,如果在没插网线或者对端服务器不存在的情况下进行连接,根据不同内核代码的处理就会阻塞在connect函数十几二十秒再返回,而影响后面代码的执行。此时就可以设置为非阻塞,然后后续再监听套接字上是否有消息以判断是否连接成功。

1.3 I/O复用模型

I/O多路复用调用selectpoll,然后阻塞在这两个系统调用中,而不是在实际的I/O系统调用中阻塞。

在这里插入图片描述

select可以监听多个套接字,当select返回时,表示某个套接字可读/写,我们就可以执行相应的操作。

1.4 信号驱动I/O模型

内核在描述符准备就绪时通过SIGIO信号通知应用层。如下图所示:

在这里插入图片描述

  1. 激活信号驱动模型: 应用程序通过系统调用(如sigaction)向内核注册信号处理程序。信号处理程序是在特定事件发生时由内核调用的函数。对于信号驱动I/O,常用的信号包括SIGIOSIGURG
  2. 启用信号驱动套接字: 对于需要信号驱动的套接字,应用程序需要调用fcntl系统调用,将套接字设置为非阻塞,并启用FASYNC标志。这样,当套接字上的I/O事件发生时,内核会发送相应的信号给应用程序。
  3. 事件发生时的处理: 当套接字上发生I/O事件时,内核会生成相应的信号(如SIGIO)。这时,注册的信号处理程序将被调用。在信号处理程序中,应用程序可以执行必要的操作,比如读取数据、处理数据或通知主循环有关事件的发生。
  4. 异步通知: 信号驱动模型提供了一种异步通知的机制,使得应用程序可以继续执行其他任务而无需等待事件的发生。这与阻塞I/O模型不同,后者需要在等待事件时阻塞应用程序。

1.5 异步I/O模型

异步I/O由POSIX规范定义,它允许应用程序启动一个I/O操作,而无需等待这个操作完成。相比于阻塞I/O,异步I/O能够在I/O操作进行的同时执行其他任务,而不必一直等待数据的读取或写入完成。请参见下图示例:

在这里插入图片描述

  1. POSIX异步I/O函数: 在POSIX标准中,异步I/O由一组函数组成,这些函数的名称通常以aio_lio_开头,如aio_readaio_write等。
  2. 启动异步操作: 应用程序通过调用异步I/O函数来启动I/O操作,通常需要指定文件描述符、缓冲区指针、缓冲区大小、文件偏移等参数。
  3. 通知机制: 异步I/O操作的关键特点是通知机制。应用程序可以指定在I/O操作完成时如何通知它,通常是通过信号、回调函数或事件通知等方式。
  4. 立即返回: 异步I/O函数通常是非阻塞的,它们在启动I/O操作后会立即返回,而不会等待操作完成。这使得应用程序能够继续执行其他任务。
  5. 适用场景: 异步I/O常用于需要处理大量并发I/O操作的情况,例如网络服务器或高性能的文件处理应用程序。通过异步I/O,程序能够更有效地利用系统资源,提高响应性能。

同步I/O(前四种都是同步I/O)操作会导致请求的进程被阻塞,直到该I/O操作完成。而异步I/O操作不会导致请求的进程被阻塞。

2 select函数

这里主要介绍一下select在网络编程的使用。select函数可以等待多个事件中的任何一个发生,并且仅在其中一个或多个事件发生时唤醒进程。这意味着我们告诉内核我们对哪些描述符感兴趣(用于读取、写入或异常条件),以及等待的时间有多长。我们可以使用select监听任何描述符,而不仅限于套接字。

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,const struct timeval *timeout);
  1. maxfdp1 :这是待测试的最大文件描述符值加一。它指定了被测试的文件描述符的范围。例如,如果最大的文件描述符是N,那么maxfdp1的值应该是N + 1。这个参数告诉select要检查多少个文件描述符。
  2. readset :这是一个指向fd_set结构的指针,用于指定希望监视其可读性的文件描述符集合。如果readset中的任何一个文件描述符变得可读,select将返回。
  3. writeset: 类似于readset,这是一个指向fd_set结构的指针,用于指定希望监视其可写性的文件描述符集合。如果writeset中的任何一个文件描述符变得可写,select将返回。
  4. exceptset: 同样是一个指向fd_set结构的指针,用于指定希望监视其异常条件的文件描述符集合。如果exceptset中的任何一个文件描述符发生异常,select将返回。
  5. timeout:这是一个指向struct timeval结构的指针,表示等待的最长时间。如果timeoutNULLselect将一直等待,直到有文件描述符就绪或出错。如果timeout不为NULL,则指定等待的最大时间,当超过这个时间后,即使没有文件描述符就绪,select也将返回。

select函数在成功时返回就绪文件描述符的总数,如果超时返回0,如果出错返回-1。可以通过检查readsetwritesetexceptset中的具体位来确定哪些文件描述符已经就绪。

这里的fd_set需要使用以下几个API来设置:

void FD_ZERO(fd_set *fdset);         /* 清除fdset中的所有位 */
void FD_SET(int fd, fd_set *fdset);  /* 置fdset中的某个位 */
void FD_CLR(int fd, fd_set *fdset);  /* 清除fdset中的某个位 */
int FD_ISSET(int fd, fd_set *fdset); /* 判断某个位是否被置 */

3 select实战:实现多个套接字监听

这里就来实现一个服务端和客户端的模型,从代码中来深入理解select函数的使用。

3.1 客户端

客户端需要能够监听标准输入stdin的消息,然后转发个服务端;还需要监听服务端的套接字,以接收服务端发来的消息。代码如下:

fd_set readfds;
while (1)
{FD_ZERO(&readfds);  				//清空读描述符FD_SET(STDIN_FILENO, &readfds);		//设置标准输入描述符FD_SET(clientSocket, &readfds);		//设置要监听的(服务端的)套接字/* 由于输入描述符为0,所以这里maxfdp1就为clientSocket+1 */select(clientSocket + 1, &readfds, NULL, NULL, NULL);//无限阻塞到有消息/* 判断哪个套接字上有消息 */if (FD_ISSET(STDIN_FILENO, &readfds)) {//标准输入有消息:发送给服务端fgets(buffer, BUFFER_SIZE, stdin);send(clientSocket, buffer, strlen(buffer), 0);}if (FD_ISSET(clientSocket, &readfds)) {//服务端套接字有消息:接收并打印出来memset(buffer, 0, sizeof(buffer));recv(clientSocket, buffer, BUFFER_SIZE, 0);printf("Server: %s", buffer);}
}
  • STDIN_FILENO是(通常为0)一个在头文件 <unistd.h> 中定义的宏,用于表示标准输入文件描述符的。除此之外还有标准输出的描述符STDOUT_FILENO(通常为1)和标准错误的描述符STDERR_FILENO(通常为2)。

3.2 服务端

服务端则是一边要accept新的客户端连接请求,一边接收来自客户端的消息并回显回去。代码如下:

#define BUFFER_SIZE 1024
int serverSocket, clientSockets[BUFFER_SIZE], maxSockets, activity, i, valread;
//serverSocket为服务端自身的套接字,代码略
maxSockets = serverSocket;
memset(clientSockets, 0, sizeof(clientSockets));//记录建立连接的客户端套接字,0表示没有使用
while (1)
{FD_ZERO(&readfds);					//清空读描述符FD_SET(serverSocket, &readfds);		//设置服务端套接字,用于accept客户端连接请求/* 设置所有已连接上的客户端的套接字 */for (i = 0; i < MAX_CLIENTS; i++){int clientSocket = clientSockets[i];if (clientSocket > 0){FD_SET(clientSocket, &readfds);if (clientSocket > maxSockets)//更新maxfdp1maxSockets = clientSocket;}}/* 监听套接字 */activity = select(maxSockets + 1, &readfds, NULL, NULL, NULL);/* 有新的客户端连接请求,这里accept它们 */if (FD_ISSET(serverSocket, &readfds)){int newSocket;socklen_t addrlen = sizeof(address);if ((newSocket = accept(serverSocket, (struct sockaddr*)&address, &addrlen)) < 0){perror("Accept failed");exit(EXIT_FAILURE);}printf("New connection, socket fd is %d, ip is : %s, port : %d\n", newSocket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));/* 找一个数组中没使用的项目填入 */for (i = 0; i < MAX_CLIENTS; i++){if (clientSockets[i] == 0){clientSockets[i] = newSocket;break;}}}/* 处理所有连接上的客户端的消息 */for (i = 0; i < MAX_CLIENTS; i++){int clientSocket = clientSockets[i];if (FD_ISSET(clientSocket, &readfds)){valread = read(clientSocket, buffer, BUFFER_SIZE);/* 返回0表示客户端断开连接,这里也断开连接 */if (valread == 0){// Client disconnectedprintf("Host disconnected, socket fd is %d\n", clientSocket);close(clientSocket);clientSockets[i] = 0;}/* 将收到的客户端的消息回显给客户端 */else{buffer[valread] = '\0';printf("Received: %s", buffer);send(clientSocket, buffer, strlen(buffer), 0);}}}
}

3.3 实验结果

运行服务端程序,然后打开一个客户端程序发送hello,再打开一个客户端程序发送hi!,实验效果如下:

在这里插入图片描述

3.4 完整代码

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUFFER_SIZE 1024int main() {int clientSocket;struct sockaddr_in serverAddress;fd_set readfds;char buffer[BUFFER_SIZE];// Create client socketif ((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("Socket creation failed");exit(EXIT_FAILURE);}serverAddress.sin_family = AF_INET;serverAddress.sin_port = htons(8888);serverAddress.sin_addr.s_addr = inet_addr("127.0.0.1");// Connect to serverif (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) < 0) {perror("Connection Failed");exit(EXIT_FAILURE);}printf("Connected to server\n");while (1) {FD_ZERO(&readfds);FD_SET(STDIN_FILENO, &readfds);FD_SET(clientSocket, &readfds);select(clientSocket + 1, &readfds, NULL, NULL, NULL);if (FD_ISSET(STDIN_FILENO, &readfds)) {// Read from stdin and send to serverfgets(buffer, BUFFER_SIZE, stdin);send(clientSocket, buffer, strlen(buffer), 0);}if (FD_ISSET(clientSocket, &readfds)) {// Read from server and printmemset(buffer, 0, sizeof(buffer));recv(clientSocket, buffer, BUFFER_SIZE, 0);printf("Server: %s", buffer);}}return 0;
}

服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024int main() {int serverSocket, clientSockets[MAX_CLIENTS], maxSockets, activity, i, valread;int opt = 1;struct sockaddr_in address;fd_set readfds;char buffer[BUFFER_SIZE];// Create server socketif ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("Socket creation failed");exit(EXIT_FAILURE);}// Set socket optionsif (setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("Setsockopt failed");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(8888);// Bind the socketif (bind(serverSocket, (struct sockaddr*)&address, sizeof(address)) < 0) {perror("Bind failed");exit(EXIT_FAILURE);}// Listen for incoming connectionsif (listen(serverSocket, MAX_CLIENTS) < 0) {perror("Listen failed");exit(EXIT_FAILURE);}printf("Server listening on port 8888\n");maxSockets = serverSocket;memset(clientSockets, 0, sizeof(clientSockets));while (1) {FD_ZERO(&readfds);FD_SET(serverSocket, &readfds);for (i = 0; i < MAX_CLIENTS; i++) {int clientSocket = clientSockets[i];if (clientSocket > 0) {FD_SET(clientSocket, &readfds);if (clientSocket > maxSockets) {maxSockets = clientSocket;}}}activity = select(maxSockets + 1, &readfds, NULL, NULL, NULL);if (FD_ISSET(serverSocket, &readfds)) {// Handle new connectionint newSocket;socklen_t addrlen = sizeof(address);if ((newSocket = accept(serverSocket, (struct sockaddr*)&address, &addrlen)) < 0) {perror("Accept failed");exit(EXIT_FAILURE);}printf("New connection, socket fd is %d, ip is : %s, port : %d\n", newSocket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));for (i = 0; i < MAX_CLIENTS; i++) {if (clientSockets[i] == 0) {clientSockets[i] = newSocket;break;}}}for (i = 0; i < MAX_CLIENTS; i++) {int clientSocket = clientSockets[i];if (FD_ISSET(clientSocket, &readfds)) {// Handle data from clientvalread = read(clientSocket, buffer, BUFFER_SIZE);if (valread == 0) {// Client disconnectedprintf("Host disconnected, socket fd is %d\n", clientSocket);close(clientSocket);clientSockets[i] = 0;} else {// Echo received message back to clientbuffer[valread] = '\0';printf("Received: %s", buffer);send(clientSocket, buffer, strlen(buffer), 0);}}}}return 0;
}

这里都没有判断select函数的返回值,我们最好也判断一下

if (activity == -1) {// 错误发生if (errno == EINTR) continue;//继续下一次selectperror("select");exit(EXIT_FAILURE);
} else if (activity == 0) {// 超时
} else {// 有文件描述符准备好
}
  • select返回值-1且errnoEINTR时,表示select被中断。这通常是由于接收到信号而导致的中断。在这种情况下,你可以选择重新调用select

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

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

相关文章

Qt TCP网络上位机的设计(通过网络编程与下位机结合)

目录 TCP 协议基础 QTcpServer 和 QAbstractSocket 主要接口函数 TCP 应用程序 1.服务端 2.客户端 上位机通过网络编程与下位机实现通信 TCP 协议基础 传输控制协议&#xff08;TCP&#xff0c;Transmission Control Protocol&#xff09;是一种面向连接的、可靠的、基于…

mysql从库设置为只读

直奔主题&#xff0c;mysql设置为只读后&#xff0c;无法增删改。 设置命令&#xff1a; mysql> set global read_only1; #1是只读&#xff0c;0是读写 mysql> show global variables like %read_only%; 以下是相关说明&#xff1a; 1、对于数据库读写状态&#xf…

详解RT-DETR网络结构/数据集获取/环境搭建/训练/推理/验证/导出/部署

论文地址&#xff1a;RT-DETR论文地址 代码地址&#xff1a;RT-DETR官方下载地址 目录 一、本文介绍 二、RT-DETR的网络结构 2.1、模型概览 2.2、高效混合编码器 2.3、IoU感知查询选择 2.4、 可扩展的RT-DETR 三、RT-DERT的环境搭建 四、免费数据集获取 五、获取RT-D…

爬楼梯(力扣LeetCode)动态规划

爬楼梯 题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方法可以爬到楼顶。 1 阶 1 阶2 阶 示…

使用python 实现华为设备的SFTP文件传输

实验目的&#xff1a; 公司有一台CE12800的设备&#xff0c;管理地址位172.16.1.2&#xff0c;现在需要编写自动化脚本&#xff0c;通过SFTP实现简单的上传下载操作。 实验拓扑&#xff1a; 实验步骤&#xff1a; 步骤1&#xff1a;将本地电脑和ensp的设备进行桥接&#xff…

菜单的hover不同动画背景

CSS常用示例100专栏目录 本专栏记录的是经常使用的CSS示例与技巧&#xff0c;主要包含CSS布局&#xff0c;CSS特效&#xff0c;CSS花边信息三部分内容。其中CSS布局主要是列出一些常用的CSS布局信息点&#xff0c;CSS特效主要是一些动画示例&#xff0c;CSS花边是描述了一些CSS…

SWT/Jface(3): 表格中添加超链接

背景 实际业务中经常需要展示某个网站, 并且希望在展示的时候单击网站可直接访问, 本节演示在表格中如何添加超链接支持. 需求 假设我需要渲染一个Study类, 它只有三个属性id,name和website, 其中id只支持展示, name只支持编辑, 而website只支持单击时跳转到相应的网站, 效果…

Linux7设置ssh秘钥登录并关闭密码登录

说明&#xff1a;场景为windows使用WinScp远程登录linux服务 winscp安装教程&#xff1a;winscp安装及关联putty使用_putty.exe没有找到_cherishSpring的博客-CSDN博客 1.在window上生成公钥和秘钥&#xff0c;操作方式参考以下文章第3点&#xff1a; git关联云效使用教程_云…

Ps:使用钢笔工具绘制自由路径的技巧

只有熟练掌握使用钢笔工具绘制自由路径的技巧&#xff0c;才能快速完成复杂形状的创建以及精准抠图等工作。 钢笔工具是 Photoshop 中绘制路径的主要工具。无论是直线路径还是曲线路径&#xff0c;钢笔工具都能够提供高度的控制和精确度。 ◆ ◆ ◆ 绘制直线路径 绘制直线路径…

下载网页内容成HTML文件

今天遇到了一个非常好用的、开源的网页下载插件: SingleFile&#xff0c;它可以将当前网页里的文字、图片、超链接等&#xff0c;合并成单一的.html文件&#xff0c;便于保存和浏览查看。下面介绍SingleFile的安装和使用。 1、下载SingleFile插件 SingleFile官网地址&#xff…

高并发系统:它的通用设计方法是什么?

Java全能学习面试指南&#xff1a;https://javaxiaobear.cn 我们知道&#xff0c;高并发代表着大流量&#xff0c;高并发系统设计的魅力就在于我们能够凭借自己的聪明才智设计巧妙的方案&#xff0c;从而抵抗巨大流量的冲击&#xff0c;带给用户更好的使用体验。这些方案好似能…

Mybatis-Plus 租户使用

Mybatis-Plus 租户使用 文章目录 Mybatis-Plus 租户使用一. 前言1.1 租户存在的意义1.2 租户框架 二. Mybatis-plus 租户2.1 租户处理器2.2 前置准备1. 依赖2. 表及数据准备3. 代码生成器 2.3 使用 三. 深入使用3.1 前言3.2 租户主体设值&#xff0c;取值3.3 部分表全量db操作3…

【古诗生成AI实战】之三——任务加载器与预处理器

本章内容属于数据处理阶段&#xff0c;将分别介绍任务加载器task和预处理器processor。 [1] 数据集 在深入探讨数据处理的具体步骤之前&#xff0c;让我们先了解一下我们将要使用的数据集的形式。 本项目采用的是七绝数据集&#xff0c;总计83072条古诗&#xff0c;其形式如下&…

大语言模型损失函数详解

我们可以把语言模型分为两类&#xff1a; 自动回归式语言模型&#xff1a;自动回归式语言模型在本质上是单向的&#xff0c;也就是说&#xff0c;它只沿着一个方向阅读句子。正向&#xff08;从左到右&#xff09;预测&#xff1b;反向&#xff08;从右到左&#xff09;预测。…

linux复习笔记04(小滴课堂)

软件安装rpm方式介绍&#xff1a; 先去挂载光盘&#xff1a; 要确保这是已连接状态。 我们查看到已经挂载成功了。 进到这个目录下。 我们可以看到这有很多rpm软件包。 man rpm: 可以看到很多参数&#xff0c;但是我们不需要全部掌握。 举例&#xff1a; 这就是告诉我们需要安…

2017年五一杯数学建模C题宜居城市问题值解题全过程文档及程序

2017年五一杯数学建模 C题 宜居城市问题 原题再现 城市宜居性是当前城市科学研究领域的热点议题之一&#xff0c;也是政府和城市居民密切关注的焦点。建设宜居城市已成为现阶段我国城市发展的重要目标,对提升城市居民生活质量、完善城市功能和提高城市运行效率具有重要意义。…

深信服超融合一体机提示:内存ECC

PS&#xff1a;此事件分享主要来源于季度巡检时发现的超融合一体机红灯闪烁异常&#xff0c;接入IPMI端口查看日志发现持续提示内存ECC&#xff1b; 因为是只有3.05这一天发现了有这个告警的提示&#xff0c;所以当时清除了日志以后重启了BMC服务就解决了&#xff1b;但是如果清…

【虚拟机】在VM中安装 CentOS 7

1.2.创建虚拟机 Centos7是比较常用的一个Linux发行版本&#xff0c;在国内的使用比例还是比较高的。 大家首先要下载一个Centos7的iso文件&#xff0c;我在资料中给大家准备了一个mini的版本&#xff0c;体积不到1G&#xff0c;推荐大家使用&#xff1a; 我们在VMware《主页》…

ctfshow刷题web入门--1--ljcsd

文章目录 ctf.show。信息搜集web1web2web3web4web5web6web7web8web9web10web11web12web13web14web15web16web17web18web19web20。爆破。知识1.1 播种随机数生成器-mt_srand。参考web21--重点web22--做不出来web23web24web25web26web27web28。。。命令执行。知识1 绕过正则表达式…

Windows安装Python环境(V3.6)

文章目录 一&#xff1a;进入网址&#xff1a;https://www.python.org/downloads/ 二&#xff1a;执行安装包 默认C盘&#xff0c;选择自定义安装目录 记得勾选add python path 下面文件夹最好不要有 . 等特殊符号 可以创建 python36 如果安装失败Option可以选默认的&#x…