欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 文化 > Linux——高级IO

Linux——高级IO

2025/2/28 5:02:53 来源:https://blog.csdn.net/2301_80687320/article/details/145862374  浏览:    关键词:Linux——高级IO

一、前言概念

IO=拷贝+等待

1. 同步(Synchronous) vs 异步(Asynchronous)

  • 核心区别:关注的是消息通知的机制

    • 同步:调用方主动等待结果,需持续检查任务是否完成。

    • 异步:调用方发起任务后无需等待,被调用方完成后主动通知(如回调函数、信号)。

生活例子
  • 同步
    你去餐厅点餐后,坐在座位上一直盯着取餐屏,直到显示你的订单号,然后去取餐。

  • 异步
    你通过外卖APP下单,之后去做其他事,外卖小哥送到后打电话通知你


2. 阻塞(Blocking) vs 非阻塞(Non-Blocking)

  • 核心区别:关注的是等待时的线程状态

    • 阻塞:调用方在等待结果时线程被挂起,无法执行其他操作。

    • 非阻塞:调用方在等待结果时线程可继续执行其他任务,需通过轮询或事件驱动获取结果。

生活例子
  • 阻塞
    你在快递柜前排队取快递,队伍不动时你干站着等待,无法做其他事。

  • 非阻塞
    你在快递柜扫码后,发现快递未到,系统让你“稍后再试”,于是你先去买菜,过会儿再来扫码查看。


组合场景:同步/异步 + 阻塞/非阻塞

例子对比
组合类型生活场景编程类比
同步阻塞排队等咖啡,队伍不动时你一直盯着柜台,不能玩手机。read() 调用后线程挂起等待。
同步非阻塞等水烧开时,你每隔1分钟去看一眼,其他时间可以看书。轮询检查文件是否可读。
异步非阻塞你启动扫地机器人打扫房间,它完成后自动发消息通知你,期间你正常办公。异步I/O + 回调函数。

关键总结

  1. 同步 vs 异步

    • 同步需要主动关注结果,异步由对方通知结果

    • 同步的例子:刷微博等待页面加载;异步的例子:下载大文件时后台运行,完成后弹窗提醒。

  2. 阻塞 vs 非阻塞

    • 阻塞会卡住当前流程,非阻塞允许并行处理其他任务

    • 阻塞的例子:ATM机转账时界面卡住;非阻塞的例子:微信发消息后界面仍可操作。

  3. 常见误区

    • 非阻塞 ≠ 异步:非阻塞可能仍需轮询(同步),而异步一定依赖通知机制。

    • 同步可以是非阻塞:比如边轮询边做其他事(如等洗衣机时拖地)。


技术场景举例

  • 同步阻塞:传统HTTP请求(等待服务器响应时页面卡住)。

  • 同步非阻塞:游戏循环中轮询键盘输入,同时更新画面。

  • 异步非阻塞:Node.js通过回调函数处理高并发网络请求。


二、五种I/O

1. 阻塞I/O(Blocking I/O)

  • 原理:程序发起I/O操作后,线程被挂起,直到数据完全准备好并拷贝到用户空间。

  • 特点:全程等待,无法执行其他任务。

  • 生活例子
    你去餐厅点餐后,站在柜台前一直等待,直到厨师做好并递给你,期间不能做其他事。


2. 非阻塞I/O(Non-Blocking I/O)

  • 原理:程序发起I/O操作后立即返回状态(未就绪则报错),需轮询检查数据是否就绪,就绪后仍需等待数据拷贝。

  • 特点:轮询消耗资源,但等待期间可处理其他任务。

  • 生活例子
    点餐后你回到座位,每隔5分钟去问“好了吗?”,期间可以玩手机。但餐好后仍需在柜台等待打包(拷贝数据阶段阻塞)。


3. I/O多路复用(I/O Multiplexing)

  • 原理:通过select/epoll等机制,单线程监控多个I/O事件,任一就绪时通知程序处理。

  • 特点:高效管理多个连接,适合高并发场景。

  • 生活例子
    餐厅安排一个服务员监听多桌顾客的需求,当某桌的餐准备好时,服务员主动通知该桌取餐。


4. 信号驱动I/O(Signal-Driven I/O)

  • 原理:数据准备阶段内核发送信号(如SIGIO)通知程序,但数据拷贝阶段仍需程序主动处理(可能阻塞)。

  • 特点:避免轮询,但信号处理复杂且可能延迟。

  • 生活例子
    点餐后餐厅给你一个叫号器,餐准备好时震动提醒,但取餐时仍需等待店员打包(拷贝阶段阻塞)。


5. 异步I/O(Asynchronous I/O)

  • 原理:程序发起I/O操作后立即返回,内核完成**全部操作(准备+拷贝)**后通知程序。

  • 特点:全程无阻塞,效率最高。

  • 生活例子
    你通过外卖APP下单,之后继续工作。外卖小哥从制作到送货全程处理,送到后敲门通知你。


总结对比

模型等待阶段数据拷贝阶段生活场景类比
阻塞I/O全程阻塞阻塞柜台前干等餐
非阻塞I/O轮询检查阻塞反复询问餐是否做好
I/O多路复用单线程监控多路阻塞服务员监听多桌需求
信号驱动I/O信号通知阻塞叫号器提醒后取餐
异步I/O完全不阻塞内核完成外卖全程无需等待,送货上门

通过以上例子,可以直观理解不同I/O模型在资源利用和响应方式上的差异。

三、阻塞及非阻塞I/O函数

1. 阻塞I/O的readwrite

功能
  • read:从文件描述符(如文件、套接字、管道等)读取数据,若数据未就绪,线程会被挂起,直到数据到达或发生错误。

  • write:向文件描述符写入数据,若缓冲区已满,线程会被挂起,直到缓冲区可用或发生错误。

特点
  • 同步阻塞:调用后线程无法执行其他任务,直到操作完成。

  • 简单易用:适合简单场景,但高并发时性能差。

代码示例
// 阻塞读取数据
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {perror("read error");
}// 阻塞写入数据
ssize_t bytes_written = write(fd, buffer, bytes_read);
if (bytes_written == -1) {perror("write error");
}
生活例子
  • 你打电话给朋友,对方未接听时,你一直举着手机等待,直到对方接听或挂断(类似read阻塞等待数据)。


2. 非阻塞I/O的fcntl函数

功能
  • fcntl(File Control):用于修改文件描述符的属性,例如设置非阻塞模式。

  • 关键操作:通过F_SETFL命令设置O_NONBLOCK标志,使后续的read/write调用变为非阻塞。

  • 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)

特点
  • 控制文件描述符状态:不直接执行I/O操作,而是修改I/O行为。

  • 需配合循环检查:非阻塞模式下,需处理EAGAINEWOULDBLOCK错误(表示数据未就绪)。

代码示例
#include <fcntl.h>// 将文件描述符设置为非阻塞模式
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);// 非阻塞读取数据
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {// 数据未就绪,稍后重试} else {perror("read error");}
}// 非阻塞写入数据(需处理部分写入情况)
ssize_t bytes_written = write(fd, buffer, bytes_read);
if (bytes_written == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {// 缓冲区已满,稍后重试} else {perror("write error");}
}
生活例子
  • 你给朋友发短信后,每隔几分钟检查手机是否有回复,期间可以处理其他事情(类似非阻塞read轮询检查数据)。

  • 实现函数SetNoBlock
     

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

    轮询方式读取标准输入
     

    void SetNoBlock(int fd)
    {int fl = fcntl(fd, F_GETFL);if (fl < 0){perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
    }
    int main()
    {SetNoBlock(0);while (1){char buf[1024] = {0};ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0){perror("read");sleep(1);continue;}printf("input:%s\n", buf);}return 0;
    }
    

阻塞 vs 非阻塞I/O的核心区别

特性阻塞I/O非阻塞I/O
线程状态调用后线程挂起,直到操作完成调用后立即返回,线程可执行其他任务
错误处理通常直接返回错误需检查EAGAINEWOULDBLOCK
适用场景简单任务、低并发高并发、实时响应需求
代码复杂度简单需处理轮询或事件驱动机制

关键注意事项

  1. 非阻塞I/O的局限性

    • 非阻塞read/write可能只处理部分数据(如TCP套接字),需循环调用直到完成。

    • 需结合select/poll/epoll等I/O多路复用技术,避免忙等待(CPU空转)。

  2. fcntl的常见用途

    • 设置非阻塞模式(O_NONBLOCK)。

    • 获取/设置文件描述符状态(如F_GETFD/F_SETFD)。


总结

  • 阻塞I/O:通过read/write实现简单同步操作,但线程效率低。

  • 非阻塞I/O:通过fcntl设置O_NONBLOCK标志,使read/write立即返回,需结合轮询或事件驱动机制。

  • 实际应用:非阻塞I/O常用于高性能服务器(如Nginx、Redis)或需要实时响应的场景。

四、I/O多路转接select

1. select 的作用

select 是一种 同步I/O多路复用 机制,允许程序同时监听多个文件描述符(如套接字、管道等),并在一或多个描述符就绪(可读、可写、异常)时通知程序处理。

  • 核心目标:避免为每个I/O操作创建独立线程,用单线程高效管理多个I/O任务。

  • 适用场景:网络服务器、需要同时处理多客户端请求的场景。


2. select 函数原型

#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解析
参数说明
nfds监听的文件描述符最大值 +1(例如最大描述符为5,则 nfds=6)。
readfds监听可读事件的文件描述符集合。
writefds监听可写事件的文件描述符集合。
exceptfds监听异常事件的文件描述符集合(如带外数据)。
timeout超时时间(NULL表示阻塞等待,0表示非阻塞立即返回,其他为具体时间)。
返回值
  • 成功:返回就绪的文件描述符总数。

  • 超时:返回 0

  • 错误:返回 -1,并设置 errno


3. select 的工作流程

  1. 初始化描述符集合
    使用 FD_ZEROFD_SET 等宏操作 fd_set 结构,设置需要监听的文件描述符。

  2. fd_set 是 select 实现多路复用的核心数据结构,本质是 位数组

// 通常定义在 <sys/select.h>
#define FD_SETSIZE 1024   // 最大支持的文件描述符数量typedef struct {unsigned long fds_bits[FD_SETSIZE / (8 * sizeof(unsigned long))];
} fd_set;
  1. 调用 select
    阻塞或非阻塞等待监听的文件描述符就绪。

  2. 检查就绪状态
    通过 FD_ISSET 宏遍历所有描述符,确定哪些描述符已就绪。

  3. 处理就绪的I/O
    对就绪的描述符执行读/写操作,处理完成后重置监听集合。


4. 关键宏函数

// 清空集合
FD_ZERO(fd_set *set);// 将描述符加入集合
FD_SET(int fd, fd_set *set);// 将描述符移出集合
FD_CLR(int fd, fd_set *set);// 检查描述符是否在集合中
FD_ISSET(int fd, fd_set *set);

5. 代码示例(TCP服务器监听客户端连接和数据)

#include <sys/select.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main() {int server_fd = socket(AF_INET, SOCK_STREAM, 0);// 绑定、监听等操作(省略)...fd_set readfds;int max_fd = server_fd;while (1) {FD_ZERO(&readfds);FD_SET(server_fd, &readfds); // 监听服务器套接字// 设置超时时间为5秒struct timeval timeout = {5, 0};// 调用select监听可读事件int ret = select(max_fd + 1, &readfds, NULL, NULL, &timeout);if (ret == -1) {perror("select error");exit(1);} else if (ret == 0) {printf("Timeout, no data.\n");continue;}// 检查服务器套接字是否有新连接if (FD_ISSET(server_fd, &readfds)) {int client_fd = accept(server_fd, NULL, NULL);FD_SET(client_fd, &readfds);max_fd = (client_fd > max_fd) ? client_fd : max_fd;}// 遍历其他客户端套接字检查数据for (int fd = server_fd + 1; fd <= max_fd; fd++) {if (FD_ISSET(fd, &readfds)) {char buffer[1024];ssize_t bytes = read(fd, buffer, sizeof(buffer));if (bytes > 0) {// 处理数据...} else {close(fd);FD_CLR(fd, &readfds);}}}}return 0;
}

6. select 的优缺点

优点
  • 跨平台:支持所有主流操作系统(Linux、Windows、macOS等)。

  • 简单易用:适合少量并发连接的管理。

缺点
  • 性能瓶颈

    • 文件描述符数量限制(通常为 FD_SETSIZE=1024)。

    • 每次调用需遍历所有描述符,时间复杂度为 O(n)

  • 重复初始化:每次调用需重新设置监听集合。

  • 内核-用户态拷贝:描述符集合需在用户态和内核态之间拷贝。


7. 生活例子

想象一个 客服中心 的场景:

  • select 的作用:一个客服人员(单线程)同时监听多个来电(文件描述符)。

  • 流程

    1. 将所有来电号码加入监听列表(FD_SET)。

    2. 每隔一段时间检查哪些电话已接通(select 返回就绪描述符)。

    3. 处理接通的电话(读/写数据),挂断后移除监听列表(FD_CLR)。

    4. 若有新来电,加入监听列表(FD_SET)。


8. select 的替代方案

  • poll:改进文件描述符数量限制,但仍有性能问题。

  • epoll(Linux):高效的事件驱动模型,支持海量并发连接。

  • kqueue(BSD/macOS):类似 epoll 的高性能机制。


总结

  • select 适用场景:小型服务器、跨平台程序或对并发要求不高的场景。

  • 核心思想:单线程通过轮询管理多个I/O任务,避免多线程资源竞争。

  • 学习意义:理解多路复用的基础原理,为掌握更高效的 epoll/kqueue 打下基础。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词