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

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

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

目录

前言

        什么是IO

        如何设计高效的IO

        五种IO模型        

        阻塞IO

        非阻塞IO

        信号驱动IO

        IO多路复用

        异步IO

        fcntl接口

        作用

        函数表达式(C 语法)

        参数(针对非阻塞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 并设 errno 为 EAGAIN 或 EWOULDBLOCKwrite/send 若写缓冲区满同样立即返回错误。

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

        函数表达式(C 语法)

#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_NONBLOCK 或 flags & ~O_NONBLOCK)。

常用标志:O_RDONLYO_WRONLYO_RDWRO_NONBLOCKO_ASYNCO_CLOEXEC 等。

        返回值

  • 成功

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

    • 对于 F_SETFL:返回 0。

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

        示例代码

#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 的可写事件,而不是固定延时。

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

        代码为:

#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;
}

        结果为:

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

封面图自取:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐