Linux网络:网络多路IO模型详解

我们知道网络本身就是IO,但是如果按照系统IO和语言的IO,对于网络而言很慢。本期我们就来学习网络IO。

而我们的前辈总结了五种IO模型,本期我们就来学习什么是IO。

相关代码:LinuxNet/IO · 楼田莉子/Linux学习 - 码云 - 开源中国

目录

前言

什么是IO

如何设计高效的IO

五种IO模型

阻塞IO

非阻塞IO

信号驱动IO

IO多路复用

异步IO

fcntl接口

作用

[函数表达式(C 语法)](#函数表达式(C 语法))

参数(针对非阻塞IO)

返回值

[非阻塞 IO 常见误区](#非阻塞 IO 常见误区)

以轮询的方式读取标准输入


前言

什么是IO

之前我们学习的系统IO主要是外设与内存的通信。而网络通信本质上依然是IO的一种。

IO分为两个部分------等待数据就绪+数据拷贝。

任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.

如何设计高效的IO

根据上述内容,我们很容易就发现设计出高级IO------少调用系统IO,且使用零拷贝技术优化

五种IO模型

IO模型 核心机制 数据从内核到用户态的流程 是否阻塞在系统调用上 是否需轮询 执行特点 典型函数/技术 优点 缺点
阻塞IO 用户进程发起read/recv后,一直等待内核数据准备完成并复制到用户空间,期间进程挂起 等待数据 → 拷贝数据,两次阶段都阻塞 进程主动放弃CPU,进入睡眠状态,直到IO完成被内核唤醒;适合单任务顺序处理 read, recv, accept 简单直观,适合单连接或低并发 并发能力差,连接阻塞会浪费CPU
非阻塞IO 调用立即返回,若数据未就绪返回错误(如EWOULDBLOCK),用户需循环调用直到成功 等待阶段不阻塞(立即返回),拷贝阶段阻塞 仅在拷贝阶段阻塞 是(用户主动轮询) 进程反复执行系统调用,忙等待(busy waiting)消耗大量CPU时间;适合延迟不敏感但需尝试多个fd的场景 fcntl(O_NONBLOCK) + read/recv 一个线程可处理多个连接 轮询浪费CPU,延迟较高
IO多路复用 使用select/poll/epoll等同时监听多个fd,任一fd就绪后通知进程,进程再调用read/recv 等待阶段阻塞在select/epoll上(而非每个fd),就绪后拷贝数据时阻塞 阻塞在select/epoll上,数据拷贝阶段阻塞 否(由内核通知) 单线程或少量线程通过事件循环同时监听数百上千个fd;epoll采用回调机制,无线性扫描 select, poll, epoll 支持高并发,单线程可管理成千上万连接 两次系统调用(epoll_wait + recv),编程相对复杂
信号驱动IO 注册信号处理函数,内核在数据就绪时发送SIGIO信号,进程在信号处理中调用read/recv 等待阶段不阻塞(立即返回),数据就绪后信号通知,拷贝时阻塞 仅在拷贝阶段阻塞 异步通知机制,但信号处理函数中只能执行异步安全函数;TCP下SIGIO无法区分read/write/accept事件,实际使用极少 sigaction, fcntl(F_SETFL, O_ASYNC) 无轮询,不阻塞等待,通知及时 信号队列有限,TCP场景下SIGIO不区分多种事件,实际使用较少
异步IO 发起aio_read后立即返回,内核完成数据准备及拷贝后,通过信号或回调通知进程 等待和拷贝阶段都由内核完成,完全不阻塞进程 全程无阻塞 用户态只需提交请求并绑定回调,内核完成全部工作后主动通知;真正的异步非阻塞,可大幅度提高IO密集型任务效率 aio_read, aio_write, io_uring 真正的非阻塞,系统调用次数少,性能最高 实现复杂,需要内核原生支持(io_uring在Linux 5.1+成熟)

阻塞IO

在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.

非阻塞IO

如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

信号驱动IO

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

IO多路复用

虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.

异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

fcntl接口

作用

  • 改变已打开文件描述符的状态标志 (如 O_NONBLOCKO_ASYNCO_DIRECT 等)。

  • 对于 socket,非阻塞模式下调用 read/recv 若无数据立即返回 -1 并设 errnoEAGAINEWOULDBLOCKwrite/send 若写缓冲区满同样立即返回错误。

  • 常用于配合 select/epoll 实现高并发网络服务。

函数表达式(C 语法)

cpp 复制代码
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

参数(针对非阻塞IO)

  • fd:要操作的文件描述符(如 socket、普通文件、管道等)。

  • cmd:操作命令。设置非阻塞时常用的两个命令:

    • F_GETFL:获取文件描述符的当前状态标志。

    • F_SETFL:设置文件描述符的状态标志(可组合多个标志)。

  • arg :根据 cmd 的可选参数。

    • cmd = F_GETFL 时,arg 忽略,返回值是当前标志。

    • cmd = F_SETFL 时,arg 是要设置的标志值(通常是 flags | O_NONBLOCKflags & ~O_NONBLOCK)。

常用标志:O_RDONLYO_WRONLYO_RDWRO_NONBLOCKO_ASYNCO_CLOEXEC 等。

返回值

  • 成功

    • 对于 F_GETFL:返回当前文件描述符的标志(int 类型)。

    • 对于 F_SETFL:返回 0。

  • 失败 :返回 -1,并设置 errno 指示错误类型(如 EBADF:fd 无效;EINVAL:cmd 无效等)

示例代码

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

// 设置 fd 为非阻塞模式,成功返回 true,失败返回 false
bool setNonBlocking(int fd) 
{
    // 获取当前标志
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) 
    {
        perror("fcntl F_GETFL failed");
        return false;
    }
    // 设置 O_NONBLOCK 标志
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) 
    {
        perror("fcntl F_SETFL O_NONBLOCK failed");
        return false;
    }
    return true;
}

int main() 
{
    // 1. 创建 socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket");
        return 1;
    }

    // 2. 设置为非阻塞
    if (!setNonBlocking(sock)) {
        close(sock);
        return 1;
    }

    // 3. 准备连接地址(例如本机 8080 端口)
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

    // 4. 非阻塞 connect:立即返回,正常会返回 -1 且 errno == EINPROGRESS
    int ret = connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    if (ret < 0 && errno != EINPROGRESS) {
        perror("connect");
        close(sock);
        return 1;
    }

    std::cout << "Non-blocking connect issued, waiting for connection establishment..." << std::endl;
    
    // 生产环境这里应该使用 select/epoll 等待可写事件,此处简单 sleep 模拟
    sleep(1);

    // 5. 尝试非阻塞 recv(假设连接已建立且服务端发送了数据)
    char buffer[1024];
    while (true) {
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (n == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 没有数据可读,这是非阻塞模式的正常情况
                std::cout << "No data available now (EAGAIN/EWOULDBLOCK), would retry later." << std::endl;
                // 实际应加入 epoll 等待下次可读事件,这里简单跳出循环演示
                break;
            } else {
                perror("recv error");
                break;
            }
        } else if (n == 0) {
            std::cout << "Connection closed by peer." << std::endl;
            break;
        } else {
            buffer[n] = '\0';
            std::cout << "Received: " << buffer << std::endl;
        }
    }

    close(sock);
    return 0;
}

结果为:

非阻塞 IO 常见误区

误区 正确理解
fcntl 设置了非阻塞后,所有操作都立即返回 只有那些可能阻塞的操作(如 readwriteconnectaccept)才会立即返回错误;close 等仍可能阻塞(但通常忽略)。
非阻塞模式下 connect 返回 EINPROGRESS 就是失败 这是正常现象,后续必须通过 select/epoll 检查是否可写,并调用 getsockopt(SO_ERROR) 确认连接成功。
非阻塞 send 返回 EAGAIN 就是缓冲区满,可以稍等一会再发 正确,但等待应基于 epoll 的可写事件,而不是固定延时。

以轮询的方式读取标准输入

代码为:

cpp 复制代码
#include <iostream>
#include <poll.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>

int main()
{
    struct pollfd fds[1];
    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;

    std::cout << "Polling stdin (type something and press Enter, or wait 5s for timeout)..." << std::endl;

    while (true) {
        int ret = poll(fds, 1, 5000);  // 5秒超时,每轮重新计时
        if (ret == -1) {
            perror("poll");
            return 1;
        }
        if (ret == 0) {
            std::cout << "Timeout: no data within 5s, polling again..." << std::endl;
            continue;
        }

        if (fds[0].revents & POLLIN) {
            char buf[1024];
            ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
            if (n == -1) {
                perror("read");
                return 1;
            }
            if (n == 0) {
                std::cout << "EOF reached, exiting." << std::endl;
                break;
            }
            buf[n] = '\0';
            std::cout << "Read: " << buf;
        }

        if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) {
            std::cerr << "poll error/hangup on stdin" << std::endl;
            break;
        }
    }

    return 0;
}

结果为:

本期内容就到这里了,喜欢请点个赞谢谢

封面图自取:

相关推荐
wen_zhufeng1 小时前
python-dotenv 使用文档
数据库·python·oracle
嵌入式小能手1 小时前
飞凌嵌入式ElfBoard-进程间的通信之信号处理signal
linux·服务器·信号处理
phltxy1 小时前
Redis Java 集成到 Spring Boot
数据库·redis·git
振浩微433射频芯片2 小时前
433射频方案在远距离工业遥控中的应用解析:从TM-03到RM521的成熟之道
网络·单片机·嵌入式硬件·物联网·智能家居
dadaobusi2 小时前
PCIe的ATS和PRS
java·网络·数据库
汽车仪器仪表相关领域2 小时前
HORIBA MEXA-584L 全功能汽车排放废气分析仪:便携精准排放检测 + 多参数同步测量 + 国六 / 欧 7 合规适配,汽车检测与调校的黄金标准
服务器·数据库·人工智能·功能测试·汽车·压力测试·可用性测试
Irene19912 小时前
Linux 中换行符 = 命令结束,xargs 防止意外执行的机制,不支持标准输入的命令,-i 在各个命令中的真实含义
linux
TechWayfarer2 小时前
账号安全实战:基于IP归属地基线的三原则异地登录风控模型
服务器·网络·python·安全·网络安全
Edward111111112 小时前
SSL/TSL配置 集群节点间通信加密还有客户端
linux·服务器·ssl