网络io模型、同步异步及Sanic和uvloop

news2024/12/25 22:19:12

网络io模型及基础概念

概念说明

用户空间与内核空间

  • 操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
  • 为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
  • 内核空间:仅供内核使用。
  • 用户空间:供各个进程使用。

进程切换

  • 为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为被称为进程切换。
  • 切换过程非常消耗资源。
  • 从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
  1. 保存处理机上下文,包括程序计数器和其他寄存器。
  2. 更新PCB信息。
  3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
  4. 选择另一个进程执行,并更新其PCB。
  5. 更新内存管理的数据结构。
  6. 恢复处理机上下文。

进程的阻塞

  • 正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。

文件描述符fd

  • 文件描述符(File descriptor):是指用于表述指向文件的引用的抽象化概念。它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

缓存 IO

  • 缓存IO,又被称作 标准IO,大多数文件系统的默认IO操作都是缓存 IO。
  • 在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
  • 缓存 IO 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

网络IO模型

  • 网络IO的本质是对socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。
  • 对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
  1. 第一阶段:等待数据准备 (Waiting for the data to be ready)。
  2. 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
  • 对于socket流而言:
  1. 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
  2. 第二步:把数据从内核缓冲区复制到应用进程缓冲区。
  • 网络应用需要处理的无非就是两大类问题:网络IO 和 数据计算。网络IO的延迟,给应用带来的性能瓶颈往往大于后者。

常见的网络IO模型清单

  1. 同步模型(synchronous IO)
  2. 阻塞IO(bloking IO)
  3. 非阻塞IO(non-blocking IO)
  4. 多路复用IO(multiplexing IO)
  5. 信号驱动式IO(signal-driven IO)
  6. 异步IO(asynchronous IO)
  • 基本 Linux IO 模型的简单矩阵:
    在这里插入图片描述

  • 以一个生动形象的例子来说明这四个概念。周末我和女友去逛街,中午饿了,我们准备去吃饭。周末人多,吃饭需要排队,我和女友有以下几种方案。

同步阻塞 IO(blocking IO)

场景描述

我和女友点完餐后,不知道什么时候能做好,只好坐在餐厅里面等,直到做好,然后吃完才离开。女友本想还和我一起逛街的,但是不知道饭能什么时候做好,只好和我一起在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待做饭的时间浪费掉了。这就是典型的阻塞。

模型介绍

  • 同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在linux中,默认情况下所有的socket都是blocking。它符合人们最常见的思考逻辑。阻塞就是进程 “被” 休息,CPU处理其它进程去了。
  • 在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程,大致如下图:
    在这里插入图片描述

流程描述

  • 当用户进程调用了recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。
  • 第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

模型特点及优缺点

  • blocking IO的特点:就是在IO执行的两个阶段都被block了。
  • 优点:
    1. 能够及时返回数据,无延迟;
    2. 对内核开发者来说这是省事了;
  • 缺点:对用户来说处于等待就要付出性能的代价了。

同步非阻塞 IO(nonblocking IO)

场景描述

我女友不甘心白白在这等,又想去逛商场,又担心饭好了。所以我们逛一会,回来询问服务员饭好了没有,来来回回好多次,饭都还没吃都快累死了啦。这就是非阻塞。需要不断的询问,是否准备好了。

模型介绍

  • 同步非阻塞就是“每隔一会儿瞄一眼进度条”的轮询(polling)方式。在这种模型中,设备是以非阻塞的形式打开的。
  • 这意味着 IO 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK)。
  • 在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,非阻塞将大的整片时间的阻塞分成N多的小的阻塞,所以进程不断地有机会“CPU光顾”。
  • 也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为 轮询
  • 轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需注意,拷贝数据整个过程,进程仍然是属于阻塞的状态
  • 在linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程如图所示:
    在这里插入图片描述

模型特点及优缺点

  • nonblocking IO的特点:是用户进程需要不断的主动询问kernel数据好了没有。
  • 优点:相对于同步阻塞方式,能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
  • 缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

IO 多路复用( IO multiplexing)

场景描述

与第二个方案差不多,餐厅安装了电子屏幕用来显示点餐的状态,这样我和女友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就可以了。这样每个人的餐是否好了,都直接看电子屏幕就可以了,这就是典型的IO多路复用。

模型介绍

  • 由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而“后台”可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。如果轮询不是进程的用户态,而是有人帮忙就好了。那么这就是所谓的“IO 多路复用”。
  • UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。
  • select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于:select可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。
  • select或poll调用之后,会阻塞进程,与blocking IO阻塞不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。也可以理解为“非阻塞”。
  • I/O多路复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。
  • 对于多路复用,也就是轮询多个socket。多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了,当然也可以针对不同的编号。具体流程,如下图所示:
    在这里插入图片描述

模型特点

  • io多路复用的特点:是通过一种机制一个进程能同时等待IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll函数就可以返回。对于监视的方式,又可以分为 select, poll, epoll三种方式。
  • 与blocking IO模型相比,select的优势在于它可以同时处理多个connection。
  • 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
IO多路复用的优势和应用场景
  • 常见于:同时处理多个客户端接入请求。
  • 也可用于服务器需要同时处理多种网络协议的套接字。
  • I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
  • 与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源。

同步模型与异步模型

  • 从整个IO过程来看,前三个模型都是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。
  • 高并发的程序一般使用同步非阻塞方式,而非多线程 + 同步阻塞方式。
  • 通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。
  • 在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。
  • 而同步非阻塞方式可以把多个 IO 请求丢到后台去,这就可以在一个进程里服务大量的并发 IO 请求。
同步与异步的根本性区别
  • 同步是需要主动等待消息通知。
  • 异步是被动接收消息通知,通过回调、通知、状态等方式来被动获取消息。
  • IO多路复用在阻塞到select阶段时,用户进程是主动等待并调用select函数获取数据就绪状态消息,并且其进程状态为阻塞。所以,把IO多路复用归为同步阻塞模式。

select/poll与epoll

均由Linux提供

  • select/poll是​​顺序扫描​​​fd是否就绪,而且受支持的fd数量限制。
  • epoll使用基于事件驱动方式​​​代替顺序扫描,因此性能更高。​​当有fd就绪时,立即回调函数rollback​​。
epoll的改进
  1. 支持一个进程打开的socket描述符(fd)不受限制(仅受限于操作系统的最大文件句柄数)。
  2. IO效率不会随着FD数目的增加而线性下降。
  3. 使用mmap加速内核与用户空间的消息传递。
  4. epoll的api更加简单。

信号驱动式IO(signal-driven IO)

场景描述

我和女友在餐厅留了一个电话号码进行排号,然后就去逛街,餐厅会在有空位的时候给我们打电话通知可以就餐。

模型介绍

  • 信号驱动式I/O:首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
  • 该模型不太常用。
  • 过程如下图所示:
    在这里插入图片描述

异步非阻塞 IO(asynchronous IO)

场景描述

女友不想逛街,又餐厅太吵了,回家好好休息一下。于是我们叫外卖,打个电话点餐,然后我和女友可以在家好好休息一下,饭好了送货员送到家里来。这就是典型的异步,只需要打个电话说一下,然后可以做自己的事情,饭好了就送来了。

模型介绍

  • 相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
  • 告知内核启动某个操作,并让​​内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们​​。
  • Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。
  • 异步过程如下图所示:
    在这里插入图片描述

模型特点及优缺点

  • 这种模型与信号驱动模型的主要区别是:信号驱动模型由内核通知我们何时可以开始一个IO操作;异步IO模型由内核通知我们IO操作何时完成。
  • 异步 API 说来轻巧,做来难,这主要是对 API 的实现者而言的。Linux 的异步 IO(AIO)支持是 2.6.22 才引入的,还有很多系统调用不支持异步 IO。Linux 的异步 IO 最初是为数据库设计的。
  • 虽然 Linux 上的 IO API 略显粗糙,但每种编程框架都有封装好的异步 IO 实现。操作系统少做事,把更多的自由留给用户,正是 UNIX 的设计哲学

五种IO模型总结

blocking(阻塞)和non-blocking(非阻塞)区别

  • 调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还在准备数据的情况下会立刻返回。

synchronous IO(同步io)和asynchronous IO(异步io)区别

  • 两者的区别就在于synchronous IO做“IO operation”的时候会将process阻塞。
  • 按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
  • non-blocking IO看似并没有被blocked,这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。kernel的数据未准备好的时候非阻塞,但当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了。
  • asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
各个IO Model的比较示意图

在这里插入图片描述

  • 如图所示,non-blocking IO和asynchronous IO的区别还是很明显的。
  • 在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。
  • 而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

Sanic和uvloop # TODO

  • sanic使用基于libuv的uvloop事件循环替代python asnycio自带的事件循环替代,以此提高异步性能。

uvloop

libuv

参考资料

  • 聊聊Linux 五种IO模型
  • Linux(UNIX)五种网络I/O模型与IO多路复用

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

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

相关文章

银河麒麟系统安装mysql数据库[mysql-5.7.28-linux-glibc2.12-x86_64]

银河麒麟系统安装mysql数据库 1.1 准备材料 mysql-5.7.28-linux-glibc2.12-x86_64.tar.gz MySQL5.7下载地址 https://cdn.mysql.com/archives/mysql-5.7/mysql-5.7.28-linux-glibc2.12-x86_64.tar.gz 1.1 安装前准备工作 1、检查是否已经安装MySQL [rootlocalhost ~]# rpm …

使用虚拟引擎创建海盗船长角色分解

介绍 你好!我叫 Stapan,我是电影和电影的角色艺术家,我有在动画工作室担任自由艺术家和角色艺术家的经验,现在我正在与 Artem Gansior 领导的 ODEE 团队一起制作一部短片。 参考 对于这个项目,最初的角色概念是由超…

MAYA曲线粒子

不从粒子中心发射 nParticleShape1.massrand(0.5,0.7);

【Linux】Linux服务器硬件条件的查看

【Linux】Linux服务器硬件条件的查看 文章目录 【Linux】Linux服务器硬件条件的查看1. CPU条件查看1.1 查看CPU 个数1.2 查看 CPU 物理核数1.3 查看CPU的运行模式1.4 查看 CPU 的型号 2. GPU 条件查看3. 查看磁盘空间、内存、Linux内核当前的系统版本号3.1 磁盘空间3.2 内存3.3…

电源自动化测试系统一站式软硬件解决方案ATECLOUD-Power

*测试系统:本系统是专门针对各类电源模块测试的一体化智能解决方案,此框架结构可根据客户实际测试需求以及预算进行调整,经济适用。 *测试产品:DC-DC电源模块、AC-DC电源模块、车用电源模块、机载电源模块、军用电源模块、特种电源…

A2B音频传输误码率测量方法

+hezkz17进数字音频系统研究开发交流答疑 如何测量音频传输误码率? 2023/6/27 14:03:58 要测量音频传输误码率,可以采取以下步骤: 选择合适的测试设备和仪器:使用专门用于音频传输测试的设备,例如音频信号发生器、示波器和频谱分析仪等。 设置测试场景:确保测试环境符合…

C语言time(NULL)学习

根据资料&#xff1b;如下一段代码&#xff1b; clock_t a,b; float cputime; atime(NULL);for (intz0;z<10000000;z){ float f0; for (int i 0; i<n; i)f h_idata[i]; }btime(NULL); cputime(float)(b-a); printf("cuptime%fmseconds\n",cputime); 前后2个…

Linux——2Linux基础命令

目录 2.1 Linux的目录结构 2.2 Linux命令入门 ls命令 Home目录和工作目录 2.3 目录切换相关命令 cd 切换工作目录 pwd 查看当前工作目录 2.4 相对路径、绝对路径和特殊路径符 相对路径和绝对路径 特殊路径符 2.5 创建目录命令mkdir 2.6 文件操作命令 touch、cat、…

文本分析-使用jieba库实现TF-IDF算法提取关键词

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

合宙Air724UG Cat.1模块硬件设计指南--外部SPI Flash

概述 我们可以通过标准的SPI接口和QSPI接口来外挂flash。区别如下 使用标准SPI接口外挂flash&#xff0c;需要自己实现flash驱动&#xff0c;和自己移植文件系统 使用QSPI接口外挂flash&#xff0c;不需要自己实现flash驱动&#xff0c;也不需要移植文件系统 。通过mount接口挂…

医院电气能源管理与节能措施讨论

摘要&#xff1a;随着我国经济建设的不断加快&#xff0c;我国能源消耗也越来越大&#xff0c;如何通过电气能源管理实现节能是各行各业都在研究的课题。医院作为我国重要的医疗服务机构&#xff0c;能源管理是医院管理中的重要组成部分。本文将针对医院电气能源管理与节能措施…

maven Nexus 私库安装使用总结

maven Nexus私库安装使用总结 大纲 nexus安装总结nexus使用总结域名配置admin账号密码修改创建仓库组创建仓库创建权限创建角色创建用户mvn deploy 部署配置 nexus安装总结 本例子使用的是常见的nexus2.x版本 注意需要先安装JDK Nexus 专业版是需要付费的&#xff0c;这里…

硬件学习 软件 Cadence day11 PCB 设计中 临时 的翻转/旋转快捷键设置等

1. PCB设计中多种快捷键设置 (临时的) 1.1 旋转 90 快捷键设置 &#xff08;角度自己可以改 &#xff09; alias ~R angle 90 #CtrlR 1.2 元器件 底层 和 顶层交换的快捷键 &#xff08;临时的&#xff09; alias ~F mirror #Ctrl F 参考文章&#xff1a;Cadenc…

数据结构--单链表的建立

数据结构–链表的建立 目标&#xff1a; Step 1:初始化一个单链表 Step 2:每次取一个数据元素&#xff0c;插入到表尾/表头 尾插法建立单链表 代码实现 typedef struct LNode {int data;struct LNode *next; }LNode, *LinkList;LinkList List_TailInsert(LinkList &L) {…

vditor 自定渲染,lute.js api

渲染器函数 目前支持的渲染回调场景如下&#xff1a; 即使渲染&#xff1a;IR -> SpinVditorIRDOM 分屏&#xff1a;sv -> SpinVditorSVDOM 所见所得&#xff1a;wysiwyg -> SpinVditorDOM // 自定义回调 vue 为例子 after: function () {this.vditor.vditor.lute.Se…

2023年城市分站系统源码采用php语言

系统源码介绍&#xff1a; 本系统采用了php语言&#xff0c;根据现有了城市分布&#xff0c;包含了省市区&#xff0c;划分&#xff0c;具备了高级少选功能&#xff0c;按照每个市级城市为分站点&#xff0c;和主站点同样的模式。 安装方法&#xff1a; 快速创建几百个城市分…

如何使用配置文件参数 - 实现预训练模型训练

如何使用配置文件参数 - 实现预训练模型训练 引言为什么使用配置文件来预训练模型呢 配置文件结构举例实现通过配置文件训练模型如何微调配置文件训练出优秀的模型呢数据集特征模型架构先前研究和经验超参数调优迭代实验和评估 引言 预训练模型在各个领域的应用取得了显著的成…

实现简单的自定义曲线图

实现的效果 废话不多说看源码 package com.unite.module_index_pad.utils;import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import androi…

浅谈“信创”时代,恒辉软件发展前景

随着中国的发展&#xff0c;很多技术因为一些原因越来越受制于人&#xff0c;尤其是上游核心技术。为了解决这个问题&#xff0c;中国明确了“数字中国”建设战略&#xff0c;抢占数字经济产业链制高点。 自主创新与国产化已成为我国实现科技强国、经济强国的发展趋势与行业共…

MySQL之MHA高可用配置及故障切换实例

目录 一、MHA概述1.1 什么是MHA&#xff1f;1.2 MHA的组成<font colorblue>MHA Node &#xff08;数据节点&#xff09;<font colorblue>MHA Manager &#xff08;管理节点&#xff09;1.3 MHA 的特点 二、 MHA搭建准备2.1 实验思路2.2 实验准备 三、 MHA搭建的步骤…