当前位置: 首页 > news >正文

Linux网络——IO模型和多路转接

通常所谓的IO,其本质就是等待通信和进行通信,即IO = 等 + 拷贝。

那么想要做到高效的IO,就要在单位时间内,减少“等”的比重。


一.五种IO模型

  1. 阻塞 IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。阻塞 IO 是最常见的 IO 模型。
  2. 非阻塞 IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK 错误码. 非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对 CPU 来说是较大的浪费, 一般只有特定场景下才使用。
  3. 信号驱动 IO: 内核将数据准备好的时候, 使用 SIGIO 信号通知应用程序进行 IO 操作.
  4. IO 多路转接: 和阻塞 IO 类似. 核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。
  5. 异步 IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。

此处展开分享一下非阻塞IO。 


二.非阻塞IO

系统和网络中的文件描述符,其默认情况下都是阻塞IO,下面来看怎么将其设置为非阻塞IO。


1.fcntl函数

#include <unistd.h>

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

fcntl 函数有 5 种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD.
  • 获得/设置文件描述符标记(cmd=F_GETFD F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL F_SETFL).
  • 获得/设置异步 I/O 所有权(cmd=F_GETOWN F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK F_SETLKW).

此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞。


2.代码实现非阻塞

void SetNoBlock(int fd)
{int fl = fcntl(fd, F_GETFL);if (fl < 0){perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

使用 F_GETFL 将当前的文件描述符的属性取出来(这是一个位图)。

然后再使用 F_SETFL 将文件描述符设置回去. 设置回去的同时, 加上一个 O_NONBLOCK 参数,表示为非阻塞

前边提到,在非阻塞IO下,如果数据没有就绪,那么IO就会以出错的形式返回,那么如何区分到底是数据没有就绪,还是真的出错了呢???

通过判断errno错误码,如果错误码为EWOULDBLOCK,表示数据没有就绪,此时可以设计程序去做其他事,并通过轮询方式去检测数据是否就绪,反之则为真的出错,程序退出。

此外,如果进程长期阻塞,可能会收到系统的信号,中断程序运行,此时返回的错误码为EINTR,所以如果不想程序被系统中断,就可以通过此错误码在做判断。


三.多路转接

多路转接,即等待多个fd上的新事件就绪,然后通知程序员,事件已经就绪,可以进行IO拷贝了。


1.select

(1)概述

系统提供 select 函数来实现多路复用输入/输出模型。

  • select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

IO = 等 + 拷贝,select负责的就是等待,并且是等待多个新事件的到了。


(2)接口

#include <sys/select.h>

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

参数:

  • nfds :是需要监视的最大的文件描述符值+1
  • rdset,wrset,exset :分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合,是输入输出型参数。
  • timeout :为结构体 timeval类型,用来设置 select()的等待时间。

fd_set结构:

这个结构就是一个整数数组, 更严格的说,是一个 "位图",使用位图中对应的位来表示要监视的文件描述符。

  • 输入时,比特位的位置表示文件描述符的编号,比特位的内容表示是否关心该fd事件。
  • 输出时,比特位的位置表示文件描述符的编号,比特位的内容表示对应的fd事件是否发生。

下面是OS提供了一组操作 fd_set 的接口, 来比较方便的操作位图:

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组 set 中相关 fd 的位

int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关 fd 的位是否为真

void FD_SET(int fd, fd_set *set); // 用来设置描述词组 set 中相关 fd 的位

void FD_ZERO(fd_set *set); // 用来清除描述词组 set 的全部位

timeval结构:

struct timeval

{

        __time_t tv_sec;//秒

        __suseconds_t tv_usec;//微秒
}

timeval 结构用于描述一段时间长度,比如(5,0)则表示在0-5秒内;如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。

参数 timeout 取值:

  • nullptr:则表示 select()没有 timeoutselect 将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生,即按照非阻塞轮询的方式。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。

函数返回值:

  • 执行成功则返回文件描述词状态已改变的个数。
  • 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回。
  • 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfdswritefds, exceptfds 和 timeout 的值变成不可预测。

(3)缺点

每次调用 select,都需要手动设置 fd 集合, 从接口使用角度来说也非常不便。

每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。

同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 多时也很大。

select 支持的文件描述符数量太小。


2.poll

(1)概述

poll的作用与select完全相同,也是等待多个fd,等待fd上的新事件就绪,随后派发事件,可以理解为是select的优化版本。


(2)接口

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数:

  •  fds是一个 poll 函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合。

  • nfds 表示 fds 数组的长度。

  • timeout:以毫秒为单位,设定的超时时间,设为0表示非阻塞,-1表示阻塞。

pollfd结构体:

struct pollfd {

int

fd; /* file descriptor */

short events; /* requested events */

short revents; /* returned events */

};

同样是位图结构,short16位,每一位可代表一个事件,eventsrevents的取值: 

 这些事件都是,且分别表示为不同的二进制位,因此可以自由组合搭配,形成事件集合。

返回值:

  • 大于0,表示有几个fd就绪。
  • 等于0,超时。
  • 小于0,poll出错。

(3)优点

pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select“参数-传递的方式. 接口使用比 select 更方便。

poll 并没有最大数量限制 (但是数量过大后性能也是会下降)。


3.epoll

(1)概述

epoll是除了select和poll之外公认为 Linux 下性能最好的多路 I/O 就绪通知方法。


(2)接口

使用epoll接口需要包含头文件 #include<sys/epoll.h>。 

int epoll_create(int size);

创建一个 epoll 的句柄.

  • size 参数可以被忽略。
  • 用完之后, 必须调用 close()关闭。

返回值epfd供接下来的函数使用。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll 的事件注册函数. 它不同于 select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型。

epoll_ctl在底层会将用户让内核关心的fd及其事件添加进由内核构成的红黑树中进行维护。

  • 第一个参数是 epoll_create()的返回值(epoll 的句柄).
  • 第二个参数表示动作,用三个宏来表示.
  • 第三个参数是需要监听的 fd.
  • 第四个参数是告诉内核需要监听什么事.

第二个参数的取值:

  • EPOLL_CTL_ADD:注册新的 fd epfd 中;
  • EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
  • EPOLL_CTL_DEL:从 epfd 中删除一个 fd

struct epoll_event 结构如下:

typedef union epoll_data {
        void *ptr;
        int fd;
        uint32_t u32;
        uint64_t u64;
} epoll_data_t;

        struct epoll_event {
        uint32_t events;/* Epoll events */
        epoll_data_t data;/* User data variable */
};

需要关注一下epoll_data_t结构体中的fd成员,其要存放事件的fd,当后续事件就绪时,需要通过该fd来获取事件

其中events同样为位图结构,可以是以下几个宏的集合:

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

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

收集在 epoll 监控的事件中已经发送的事件.

epoll_wait会检测内核中构成的就绪队列中是否有事件已经就绪, 并将已经就绪的事件按照严格顺序放入我们定义的用户缓冲区数组中。

  • 参数 events 是分配好的 epoll_event 结构体数组. epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存).
  • maxevents 告诉内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size.
  • 参数 timeout 是超时时间 (毫秒,0 会立即返回,-1 是永久阻塞).
  • 如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时, 返回小于 0 表示函数失败.

对应的事件节点,会同时包含红黑树和就绪队列两个指针,从而使得该节点既可以存在于红黑树中,也可以存在于就绪队列中,从而无需新建新节点来进行转移。 


(3)LT工作模式

LT即水平触发 Level Triggered 工作模式。

epoll 默认状态下就是 LT 工作模式.

当 epoll 检测到 socket 上事件就绪的时候, 可以不立刻进行处理,或者只处理一部分,当缓冲区还有事件未处理时,epoll_wait 会不断地立刻返回并通知 socket 读事件就绪,直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回。

支持阻塞读写和非阻塞读写。


(4)ET工作模式

ET即边缘触发 Edge Triggered 工作模式。

在第 1 步将 socket 添加到 epoll 描述符的时候使用 EPOLLET 标志, epoll 将进入 ET 工作模式。

当 epoll 检测到 socket 上事件就绪时, 必须立刻处理。如果未处理或未一次性处理完,在第二次调用epoll_wait 的时候, epoll_wait 不会再返回了。

也就是说, ET 模式下, 文件描述符上的事件就绪后, 只有一次处理机会。

ET 的性能比 LT 性能更高( epoll_wait 返回的次数少了很多). Nginx 默认采用 ET 模式使用 epoll。

只支持非阻塞的读写。


LT epoll 的默认行为.

使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.

相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.

另一方面, ET 的代码复杂程度更高。


相关文章:

Linux网络——IO模型和多路转接

通常所谓的IO&#xff0c;其本质就是等待通信和进行通信&#xff0c;即IO 等 拷贝。 那么想要做到高效的IO&#xff0c;就要在单位时间内&#xff0c;减少“等”的比重。 一.五种IO模型 阻塞 IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方…...

【计网】自定义序列化反序列化(二) —— 实现网络版计算器【上】

&#x1f30e; 实现网络版计算器【上】 文章目录&#xff1a; 实现网络版计算器【上】 自定义协议       制定自定义协议 Jsoncpp序列化反序列化       Json::Value类       Jsoncpp序列化       Jsoncpp反序列化 自定义协议序列化反序列化      …...

数据结构2:顺序表

目录 1.线性表 2.顺序表 2.1概念及结构 2.2接口实现 1.线性表 线性表是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串 线性表在逻辑上是线性结构&#xff0c;也就说…...

python学习——元组

在 Python 中&#xff0c;元组&#xff08;tuple&#xff09;是一种内置的数据类型&#xff0c;用于存储不可变的有序元素集合。以下是关于 Python 元组的一些关键点&#xff1a; 文章目录 定义元组1. 使用圆括号 ()2. 使用 tuple() 函数3. 使用单个元素的元组4. 不使用圆括号…...

apache实现绑定多个虚拟主机访问服务

1个网卡绑定多个ip的命令 ip address add 192.168.45.140/24 dev ens33 ip address add 192.168.45.141/24 dev ens33 在linux服务器上&#xff0c;添加多个站点资料&#xff0c;递归创建三个文件目录 分别在三个文件夹下&#xff0c;建立测试页面 修改apache的配置文件http.…...

无需插件,如何以二维码网址直抵3D互动新世界?

随着Web技术的飞速发展&#xff0c;一个无需额外插件&#xff0c;仅凭二维码或网址即可直接访问的三维互动时代已经悄然来临。这一变革&#xff0c;得益于WebGL技术与先进web3D引擎的完美融合&#xff0c;它们共同构建了51建模网这样一个既便捷又高效的在线三维互动平台&#x…...

系统思考—感恩自己

生命中&#xff0c;真正值得我们铭记与感恩的&#xff0c;不是路途上的苦楚与风雨&#xff0c;而是那个在风雨中依然清醒、勇敢前行的自己&#xff0c;和那些一路同行、相互扶持的伙伴们。 感恩自己&#xff0c;感恩每一个与我们携手并进的人&#xff0c;也期待更多志同道合的…...

Java多线程详解①①(全程干货!!!) 实现简单的线程池 || 定时器 || 简单实现定时器 || 时间轮实现定时器

这里是Themberfue 上一节讲了 线程池 线程池中的拒绝策略 等相关内容 实现简单的线程池 一个线程池最核心的方法就是 submit&#xff0c;通过 submit 提交 Runnable 任务来通知线程池来执行 Runnable 任务 我们简单实现一个特定线程数量的线程池&#xff0c;这些线程应该在…...

DAMODEL丹摩|部署FLUX.1+ComfyUI实战教程

本文仅做测评体验&#xff0c;非广告。 文章目录 1. FLUX.1简介2. 实战2. 1 创建资源2. 1 ComfyUI的部署操作2. 3 部署FLUX.1 3. 测试5. 释放资源4. 结语 1. FLUX.1简介 FLUX.1是由黑森林实验室&#xff08;Black Forest Labs&#xff09;开发的开源AI图像生成模型。它拥有12…...

请求(request)

目录 前言 request概述 request的使用 获取前端传递的数据 实例 请求转发 特点 语法 实例 实例1 实例2 【关联实例1】 域对象 组成 作用范围&#xff1a; 生命周期&#xff1a; 使用场景&#xff1a; 使用步骤 存储数据对象 获得数据对象 移除域中的键值…...

关于VNC连接时自动断联的问题

在服务器端打开VNC Server的选项设置对话框&#xff0c;点左边的“Expert”&#xff08;专家&#xff09;&#xff0c;然后找到“IdleTimeout”&#xff0c;将数值设置为0&#xff0c;点OK关闭对话框。搞定。 注意,服务端有两个vnc服务,这俩都要设置ide timeout为0才行 附件是v…...

C语言strtok()函数用法详解!

strtok 是 C 标准库中的字符串分割函数&#xff0c;用于将一个字符串拆分成多个部分&#xff08;token&#xff09;&#xff0c;以某些字符&#xff08;称为分隔符&#xff09;为界限。 函数原型 char *strtok(char *str, const char *delim);参数&#xff1a; str&#xff1a…...

【docker 拉取镜像超时问题】

问题描述 在centosStream8上安装docker&#xff0c;使用命令sudo docker run hello-world 后出现以下错误&#xff1a; Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Ti…...

模拟手机办卡项目(移动大厅)--结合面向对象、JDBC、MYSQL、dao层模式,使用JAVA控制台实现

目录 1. 项目需求 2. 项目使用的技术 3.项目需求分析 3.1 实体类和接口 4.项目结构 5.业务实现 5.1 登录 5.1.1 实现步骤 5.1.2 原生代码问题 ​编辑 5.1.3 解决方法 1.说明&#xff1a; 2. ResultSetHandler结果集处理 5.1.4 代码 5.1.5 实现后的效果图 登录成功​…...

机器学习—大语言模型:推动AI新时代的引擎

云边有个稻草人-CSDN博客 目录 引言 一、大语言模型的基本原理 1. 什么是大语言模型&#xff1f; 2. Transformer 架构 3. 模型训练 二、大语言模型的应用场景 1. 文本生成 2. 问答系统 3. 编码助手 4. 多语言翻译 三、大语言模型的最新进展 1. GPT-4 2. 开源模型 …...

C++:探索哈希表秘密之哈希桶实现哈希

文章目录 前言一、链地址法概念二、哈希表扩容三、哈希桶插入逻辑四、析构函数五、删除逻辑六、查找七、链地址法代码实现总结 前言 前面我们用开放定址法代码实现了哈希表&#xff1a; C&#xff1a;揭秘哈希&#xff1a;提升查找效率的终极技巧_1 对于开放定址法来说&#…...

具身智能高校实训解决方案——从AI大模型+机器人到通用具身智能

一、 行业背景 在具身智能的发展历程中&#xff0c;AI 大模型的出现成为了关键的推动力量。这些大模型具有海量的参数和强大的语言理解、知识表示能力&#xff0c;能够为机器人的行为决策提供更丰富的信息和更智能的指导。然而&#xff0c;单纯的大模型在面对复杂多变的现实…...

【消息序列】详解(8):探秘物联网中设备广播服务

目录 一、概述 1.1. 定义与特点 1.2. 工作原理 1.3. 应用场景 1.4. 技术优势 二、截断寻呼&#xff08;Truncated Page&#xff09;流程 2.1. 截断寻呼的流程 2.2. 示例代码 2.3. 注意事项 三、无连接外围广播过程 3.1. 设备 A 启动无连接外围设备广播 3.2. 示例代…...

【RL Base】强化学习核心算法:深度Q网络(DQN)算法

&#x1f4e2;本篇文章是博主强化学习&#xff08;RL&#xff09;领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对相关等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅…...

深入浅出 Python 网络爬虫:从零开始构建你的数据采集工具

在大数据时代&#xff0c;网络爬虫作为一种数据采集技术&#xff0c;已经成为开发者和数据分析师不可或缺的工具。Python 凭借其强大的生态和简单易用的语言特点&#xff0c;在爬虫领域大放异彩。本文将带你从零开始&#xff0c;逐步构建一个 Python 网络爬虫&#xff0c;解决实…...

R语言AI模型部署方案:精准离线运行详解

R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

vue3 字体颜色设置的多种方式

在Vue 3中设置字体颜色可以通过多种方式实现&#xff0c;这取决于你是想在组件内部直接设置&#xff0c;还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法&#xff1a; 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

srs linux

下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935&#xff0c;SRS管理页面端口是8080&#xff0c;可…...

稳定币的深度剖析与展望

一、引言 在当今数字化浪潮席卷全球的时代&#xff0c;加密货币作为一种新兴的金融现象&#xff0c;正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而&#xff0c;加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下&#xff0c;稳定…...

使用 SymPy 进行向量和矩阵的高级操作

在科学计算和工程领域&#xff0c;向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能&#xff0c;能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作&#xff0c;并通过具体…...

LeetCode - 199. 二叉树的右视图

题目 199. 二叉树的右视图 - 力扣&#xff08;LeetCode&#xff09; 思路 右视图是指从树的右侧看&#xff0c;对于每一层&#xff0c;只能看到该层最右边的节点。实现思路是&#xff1a; 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...

IP如何挑?2025年海外专线IP如何购买?

你花了时间和预算买了IP&#xff0c;结果IP质量不佳&#xff0c;项目效率低下不说&#xff0c;还可能带来莫名的网络问题&#xff0c;是不是太闹心了&#xff1f;尤其是在面对海外专线IP时&#xff0c;到底怎么才能买到适合自己的呢&#xff1f;所以&#xff0c;挑IP绝对是个技…...

【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制

目录 节点的功能承载层&#xff08;GATT/Adv&#xff09;局限性&#xff1a; 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能&#xff0c;如 Configuration …...

数据结构:递归的种类(Types of Recursion)

目录 尾递归&#xff08;Tail Recursion&#xff09; 什么是 Loop&#xff08;循环&#xff09;&#xff1f; 复杂度分析 头递归&#xff08;Head Recursion&#xff09; 树形递归&#xff08;Tree Recursion&#xff09; 线性递归&#xff08;Linear Recursion&#xff09;…...

用神经网络读懂你的“心情”:揭秘情绪识别系统背后的AI魔法

用神经网络读懂你的“心情”:揭秘情绪识别系统背后的AI魔法 大家好,我是Echo_Wish。最近刷短视频、看直播,有没有发现,越来越多的应用都开始“懂你”了——它们能感知你的情绪,推荐更合适的内容,甚至帮客服识别用户情绪,提升服务体验。这背后,神经网络在悄悄发力,撑起…...