Skip to content

IO 模型 (I/O Models)

一、IO 概述

什么是 IO

IO (Input/Output):数据在 CPU/内存 与外部设备(磁盘、网络、终端)之间的传输。

IO 类型:

类型 设备 速度
磁盘 IO 硬盘/SSD 慢
网络 IO 网卡 很慢
终端 IO 键盘/鼠标 中
内存 IO 内存 很快
GPU IO 显卡 中-很快

IO 的核心问题

  • 慢:IO 设备比 CPU 慢几个数量级
  • 阻塞:IO 期间 CPU 不能干别的
  • 并发:一个进程同时处理多个 IO
  • 多路复用:同时监控多个 IO

IO 性能指标

  • 延迟 (Latency):单次操作的响应时间 (μs 或 ms)
  • 吞吐量 (Throughput):单位时间传输量 (MB/s、IOPS)
  • 带宽 (Bandwidth):理论最大传输率

IO 的层次

应用层: read()/write() / send()/recv()
   ↓
标准库: fread/fwrite / fgets / ...
   ↓
系统调用层: read/write/sendmsg/recvmsg
   ↓
VFS / Socket 层
   ↓
具体驱动: Ext4 / TCP/IP / USB / ...
   ↓
设备: 磁盘 / 网卡 / ...

二、五大 IO 模型

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

关注点:消息通知机制

  • 同步:调用方主动等待结果
  • 异步:调用方不等结果,通过回调/事件通知

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

关注点:调用时进程状态

  • 阻塞:调用时进程挂起
  • 非阻塞:调用时立即返回(可能带 EAGAIN/EWOULDBLOCK)

3. POSIX 的 5 大 IO 模型 (Unix Network Programming)

模型 同步/异步 阻塞/非阻塞 关键
阻塞 IO 同步 阻塞 等待数据 + 等待内核
非阻塞 IO 同步 非阻塞 轮询
IO 多路复用 同步 阻塞(等 select)/可优化 select/poll/epoll
信号驱动 IO 异步 异步 SIGIO 通知
异步 IO (AIO) 异步 异步 内核全程处理

注意: - "同步/异步" 在 IO 上下文中:同步 = 等数据完整就绪,异步 = 数据就绪后通知 - "阻塞/非阻塞" 在调用上下文:阻塞 = 挂起,非阻塞 = 立即返回

4. 经典图示

        ┌──────────┐
        │  用户态  │
        └────┬─────┘
             │ 1. recvfrom (发起调用)
             ↓
        ┌──────────┐
        │  内核态  │
        └────┬─────┘
             │ 2. 等待数据 (网络包到达)
             │ 3. 复制数据到用户缓冲
             ↓
        ┌──────────┐
        │  用户态  │
        └──────────┘
             │ 4. 处理数据
  • 阻塞 IO:1-2-3-4 全程等待
  • 非阻塞 IO:2 立即返回 EAGAIN,轮询直到数据到
  • IO 复用:多个 IO 同时监控(单线程)
  • 信号驱动:内核通过 SIGIO 通知
  • 异步 IO:1 后立即返回,内核完成所有事,完成后通知

三、阻塞 IO (Blocking I/O)

1. 原理

// 最简单的阻塞读
char buf[1024];
int n = read(fd, buf, sizeof(buf));   // 阻塞直到数据到达
process(buf);
  • 进程调用 read,内核等待数据
  • 数据到达后,内核复制到用户缓冲
  • 复制完成后,read 返回
  • 整个过程进程阻塞

2. 特点

  • 优点:简单,易理解
  • 缺点:等待期间 CPU 闲置
  • 适合:连接少、业务简单

3. 阻塞 accept

// 经典阻塞式服务器
int listen_fd = socket(...);
bind(listen_fd, ...);
listen(listen_fd, 128);

while (1) {
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    int conn_fd = accept(listen_fd, (struct sockaddr *)&client, &len);
    // 阻塞,直到有客户端连接
    handle_client(conn_fd);
}

致命问题:每来一个客户端,就阻塞等待,无法处理多个并发。


四、非阻塞 IO (Non-Blocking I/O)

1. 原理

把 fd 设为非阻塞 (O_NONBLOCK),read 不再阻塞,返回 EAGAIN/EWOULDBLOCK

// 设置非阻塞
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

// 读
char buf[1024];
while (1) {
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        process(buf, n);
    } else if (n == 0) {
        // EOF
        break;
    } else if (errno == EAGAIN) {
        // 数据未就绪,稍后再试
        usleep(1000);
    } else {
        // 错误
        break;
    }
}

2. 特点

  • 优点:不阻塞,可以做别的事
  • 缺点:轮询 (busy polling) 浪费 CPU

3. 应用场景

  • 与多路复用结合 (select/poll/epoll 配合非阻塞)
  • 单独使用通常不好(浪费 CPU)

五、IO 多路复用 (I/O Multiplexing)

1. 原理

用一个进程/线程监控多个 IO,当某个 IO 就绪时通知程序处理

核心系统调用: - select() (1983,古老) - poll() (1986,简单) - epoll() (Linux 2.6,2002,高性能) - kqueue() (BSD/macOS,等效 epoll)

2. select 详解

接口:

#include <sys/select.h>

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

特点: - fd_set 是位图:每一位代表一个 fd - 限制:fd_set 1024 位 → 最多 1024 个 fd - 每次调用都要: - 从用户态拷贝 fd_set 到内核态 - 内核轮询所有 fd - 从内核态拷贝回用户态 - O(n) 复杂度

使用:

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);

struct timeval tv = {5, 0};   // 5 秒超时
int n = select(maxfd+1, &readfds, NULL, NULL, &tv);

if (n > 0) {
    if (FD_ISSET(fd1, &readfds)) {
        // fd1 可读
    }
    if (FD_ISSET(fd2, &readfds)) {
        // fd2 可读
    }
}

缺点: - fd 数量有限 - 每次都要遍历 - 大量 fd 性能差

3. poll 详解

接口:

#include <poll.h>

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

struct pollfd {
    int   fd;         // 文件描述符
    short events;     // 关注的事件
    short revents;    // 实际发生的事件
};

改进: - 无 fd 数量限制 - 每次要重新设置 pollfd 数组 - 仍是 O(n)

4. epoll 详解 (Linux 高性能)

为什么 epoll 高效: - 不限制 fd 数量 - O(1) 时间复杂度 (用红黑树 + 就绪链表) - mmap 共享内存 (避免拷贝) - 边缘触发 (ET) 支持

(1) 三个核心 API

#include <sys/epoll.h>

// 1. 创建 epoll 实例
int epoll_create(int size);   // size 提示,实际不限
int epoll_create1(int flags);  // 推荐

// 2. 注册/修改/删除 fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// op: EPOLL_CTL_ADD / MOD / DEL

struct epoll_event {
    uint32_t events;   // EPOLLIN, EPOLLOUT, EPOLLET, ...
    epoll_data_t data; // 用户数据
};

// 3. 等待事件
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

(2) 工作模式

模式 触发条件
水平触发 (LT, default) fd 有数据就触发(默认)
边缘触发 (ET) fd 状态变化时触发(必须用非阻塞 I/O)

ET 优势: - 事件少,系统调用少 - 性能高

ET 注意事项: - 一次必须读完(read 返回 < buffer,或 EAGAIN) - 必须用非阻塞 fd

(3) 经典 ET 例子

// epoll + ET + 非阻塞 + 完整读取
void et_read(int fd) {
    char buf[1024];
    while (1) {
        ssize_t n = read(fd, buf, sizeof(buf));
        if (n > 0) {
            process(buf, n);
        } else if (n == 0) {
            // 客户端关闭
            close(fd);
            break;
        } else {
            if (errno == EAGAIN) {
                // 全部读完
                break;
            } else {
                // 错误
                close(fd);
                break;
            }
        }
    }
}

(4) epoll 完整示例 (回声服务器)

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>

#define MAX_EVENTS 64
#define PORT 8080

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int flags = fcntl(listen_fd, F_GETFL);
    fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);

    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listen_fd, 128);

    int epfd = epoll_create1(0);
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            if (fd == listen_fd) {
                // 接受新连接
                int client = accept(listen_fd, NULL, NULL);
                int cflags = fcntl(client, F_GETFL);
                fcntl(client, F_SETFL, cflags | O_NONBLOCK);
                ev.events = EPOLLIN | EPOLLET;  // ET 模式
                ev.data.fd = client;
                epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev);
            } else if (events[i].events & EPOLLIN) {
                // 读客户端
                char buf[1024];
                ssize_t len = read(fd, buf, sizeof(buf));
                if (len <= 0) {
                    close(fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                } else {
                    write(fd, buf, len);  // 回声
                }
            }
        }
    }
}

5. 三种多路复用对比

维度 select poll epoll
时间复杂度 O(n) O(n) O(1)
fd 数量 1024 (FD_SETSIZE) 无限制 无限制
触发模式 LT LT LT / ET
数据拷贝 每次双向 每次双向 mmap 共享
跨平台 几乎所有 Unix 几乎所有 Unix 仅 Linux
内核实现 位图 数组轮询 红黑树 + 就绪链表
适用 fd 少 / 老代码 fd 少 高并发

6. kqueue (BSD/macOS)

  • 等效 epoll 的 BSD 版本
  • kqueue() 创建,kevent() 等待
  • 还能监控信号、子进程、文件等
  • 性能与 epoll 相当

7. IOCP (Windows)

  • I/O Completion Port:Windows 专用
  • 真正的异步 I/O
  • 性能好,但 API 复杂

六、信号驱动 IO (Signal-Driven I/O)

1. 原理

让内核在数据就绪时发信号给进程

// 设置信号处理
signal(SIGIO, sigio_handler);

// 设置 fd 为信号驱动 + 属主进程
fcntl(fd, F_SETFL, O_ASYNC);
fcntl(fd, F_SETOWN, getpid());

// 进程继续执行
// 当 fd 有数据,SIGIO 信号到达,触发 sigio_handler

2. 优缺点

  • 优点:不用轮询,数据到达时被通知
  • 缺点:
  • 信号处理复杂
  • 全局 SIGIO 只能一个 fd
  • 实际很少用,被 epoll 取代

七、异步 IO (AIO / Async I/O)

1. POSIX AIO

#include <aio.h>

struct aiocb cb;
cb.aio_fildes = fd;
cb.aio_buf = buf;
cb.aio_nbytes = count;
cb.aio_offset = offset;

aio_read(&cb);     // 立即返回

// 做别的事
do_other_things();

// 等待完成
struct timespec ts = {0, 0};
while (aio_error(&cb) == EINPROGRESS) {
    nanosleep(&ts, NULL);   // 或做别的事
}

ssize_t n = aio_return(&cb);

2. Linux io_uring (现代高性能异步 IO)

io_uring (Linux 5.1+, 2019):Linux 现代化的异步 I/O 接口

特点: - 共享 ring buffer (mmap,零拷贝) - 支持 sqe (Submission Queue Entry) 提交 - cqe (Completion Queue Entry) 完成 - 极高性能,比 epoll 还快 - 适合高并发服务器、数据库

使用:

#include <liburing.h>

struct io_uring ring;
io_uring_queue_init(64, &ring, 0);

// 准备 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, count, 0);

// 提交
io_uring_submit(&ring);

// 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);

// 处理结果
int res = cqe->res;
io_uring_cqe_seen(&ring, cqe);

io_uring_queue_exit(&ring);

高级特性: - 固定文件 (Fixed Files) - 注册 buffer (Registered Buffers) - 轮询模式 (Polled IO) - io_uring 的 io_uring,网络 IO 支持,等等

性能: - 比 epoll + 4 次 copy 提升显著 - 微秒级响应

3. mmap + epoll 模式 (高性能服务器常用)

// 接收数据
recv(fd, ..., MSG_PEEK | MSG_WAITALL);  // 看数据大小
// mmap 一段 DMA 内存
// 直接处理,免去内存拷贝

4. sendfile 零拷贝

#include <sys/sendfile.h>

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • 适合静态文件服务(Nginx)
  • 2 次上下文切换 → 0 次(直接 DMA 复制)
  • 性能大幅提升

5. splice / tee

ssize_t splice(int fd_in, off64_t *off_in, int fd_out,
               off64_t *off_out, size_t len, unsigned int flags);
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
  • splice:两个 fd 间零拷贝移动数据
  • tee:两个 fd 间零拷贝复制数据
  • 适合管道(pipe)

6. io_uring vs epoll

维度 epoll io_uring
同步/异步 同步(等待就绪) 异步(等待完成)
系统调用 1 次(epoll_wait) 2 次(submit + wait_cqe)
上下文切换 提交和等待各 1 次 mmap 共享,几乎 0 切换
适用 网络 IO 事件 通用 IO(网络 + 磁盘)
性能 极好 更好(零拷贝)
复杂度 简单 复杂
内核版本 2.6+ 5.1+

八、IO 设计模式 (Reactor / Proactor)

1. Reactor 模式 (同步)

Reactor = 反应器,事件驱动 + 单线程/多线程

Event Loop:
  while (true) {
      events = epoll_wait()
      for (event in events) {
          dispatch(event)
          // 根据事件类型,调用 handler
      }
  }

特点: - 同步 IO (read/write 仍由程序调用) - 多个 IO 复用单线程 - 性能高

代表: - Redis (单线程 Reactor) - Netty (Java) - libuv (Node.js) - libevent / libev (C) - Nginx (多 Reactor)

Nginx 多 Reactor 模式:

Master 进程
  ├─ Worker 1 (Reactor + Worker pool)
  ├─ Worker 2
  └─ Worker N
每个 Worker:
  - Accept 事件
  - Read/Write
  - 子请求交给线程池

2. Proactor 模式 (异步)

Proactor = 主动器,全程异步

1. 发起异步 IO
2. 内核完成所有 IO
3. 内核(或通过回调)通知结果
4. 程序处理结果

特点: - 真正的异步 IO - 程序不调用 read/write - 实现复杂

代表: - Windows IOCP - Boost.Asio (Proactor 模拟器) - 一些 AIO 实现

3. Reactor vs Proactor

维度 Reactor Proactor
同步性 同步 IO 异步 IO
实现 epoll / select IOCP / io_uring
复杂度 中 高
性能 高 更高
主流程度 主流 较少
代表 Netty、Redis、Nginx IOCP、Boost.Asio

九、磁盘 IO 调度

1. IO 调度器

Linux 多队列块设备 IO 调度器 (现代):

调度器 特点 适用
none 不调度,直通 NVMe SSD
mq-deadline 截止时间调度 SSD / 一般场景
bfq Budget Fair Queueing 桌面/混合
kyber 令牌桶,简单低延迟 NVMe SSD

老式单队列调度器(已废弃): - cfq - deadline - noop

# 查看当前调度器
cat /sys/block/sda/queue/scheduler

# 设置调度器
echo mq-deadline > /sys/block/sda/queue/scheduler

2. 关键 IO 参数

# 块大小
blockdev --getbsz /dev/sda
fdisk -l /dev/sda

# 预读 (readahead)
blockdev --getra /dev/sda
echo 256 > /sys/block/sda/queue/read_ahead_kb

# 队列深度
cat /sys/block/sda/queue/queue_depth
echo 1024 > /sys/block/sda/queue/queue_depth

3. 磁盘 IO 性能

# 顺序读写测试
dd if=/dev/zero of=/tmp/test bs=1M count=1024 oflag=direct
dd if=/tmp/test of=/dev/null bs=1M iflag=direct

# 随机 IO 测试
fio --name=randread --ioengine=libaio --direct=1 \
    --filename=/dev/sda --bs=4k --size=1G \
    --rw=randread --iodepth=32 --runtime=60

4. 优化建议

  • SSD/NVMe 用 none 或 mq-deadline
  • HDD 用 bfq 或 mq-deadline
  • 数据库 用 none 调度 + 直接 IO (O_DIRECT)
  • 挂载选项:noatime,discard
  • RAID 卡:开启写缓存(配 BBU)
  • NUMA 优化:绑本地 NUMA 节点

十、网络 IO

1. 网络 IO 性能

  • 延迟:同机房 0.1-0.5 ms,跨城 5-30 ms,跨洲 100-200 ms
  • 吞吐:1 Gbps / 10 Gbps / 25 Gbps / 100 Gbps / 200 Gbps / 400 Gbps
  • PPS (包每秒):千兆网 ~ 1.5 Mpps,万兆 ~ 15 Mpps

2. 网络优化

# 网卡多队列 (RSS)
ethtool -l eth0
ethtool -L eth0 combined 8

# 中断亲和
echo 0-7 > /proc/irq/24/smp_affinity_list

# 网卡缓冲区
ethtool -G eth0 rx 4096 tx 4096

# TCP 缓冲区
sysctl -w net.ipv4.tcp_rmem="4096 87380 6291456"
sysctl -w net.ipv4.tcp_wmem="4096 65536 6291456"

# 各种 TCP 调优
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.ipv4.tcp_tw_reuse=1

3. 网络 IO 模型

  • Nginx:多 Reactor + epoll
  • HAProxy:单 Reactor + epoll
  • Envoy:多线程异步 + iouring/IO
  • DPDK:用户态驱动
  • RDMA:远程直接内存访问

十一、零拷贝 (Zero Copy)

1. 为什么需要

  • 传统 IO:磁盘 → 内核缓冲 → 用户缓冲 → socket
  • 多次拷贝,浪费 CPU
  • 零拷贝:减少甚至消除数据拷贝

2. 零拷贝技术

技术 拷贝次数 系统调用 适用
传统 read+write 4 次 read + write -
mmap + write 3 次 mmap + write 通用
sendfile 2 次 sendfile 静态文件
splice 2 次 splice 管道/网络
SG-DMA + sendfile 1 次 sendfile 网卡支持 SG
DPDK 0 次 用户态驱动 极致性能
io_uring 0~1 次 共享 ring 现代

3. sendfile 详解

// 0 拷贝发送文件内容
int sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • in_fd:文件描述符
  • out_fd:目标文件描述符(通常是 socket)
  • 数据直接在内核的 file cache → socket buffer
  • 不经过用户空间

4. 静态文件服务的零拷贝

// 简单回声 + 零拷贝
int server = socket(...);
while (1) {
    int client = accept(server, ...);
    // 客户端请求 GET /bigfile.mp4
    int file = open("/bigfile.mp4", O_RDONLY);
    struct stat st;
    fstat(file, &st);
    sendfile(client, file, NULL, st.st_size);
    close(file);
    close(client);
}

十二、IO 命令与工具

1. 监控 IO

# 整体
iostat -x 1
iotop
vmstat 1
dstat

# 进程级
iotop -p PID
cat /proc/PID/io

# 详细
perf stat -e block:* command
bpftrace -e 'tracepoint:block:block_rq_issue { @[comm] = count(); }'

2. 压测工具

# 磁盘
fio
dd
hdparm -tT /dev/sda

# 网络
iperf3
netperf
wrk
ab (Apache Bench)

3. 调优

# 内核参数
sysctl -a | grep vm
sysctl -a | grep net
sysctl -a | grep fs

# /proc/sys/vm
vm.dirty_ratio
vm.dirty_background_ratio
vm.swappiness
vm.vfs_cache_pressure

十三、IO 性能调优实战

1. Web 服务器

# Nginx
worker_processes auto;
worker_rlimit_nofile 1000000;
events {
    worker_connections 4096;
    multi_accept on;
    use epoll;
}
http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    open_file_cache max=1000;
}

2. 数据库 (MySQL/PG)

# 文件系统挂载
mount -o noatime,discard,data=writeback /dev/sda1 /var/lib/mysql

# IO 调度器
echo none > /sys/block/sda/queue/scheduler

# 关键参数
innodb_flush_method=O_DIRECT
innodb_io_capacity=2000
innodb_read_io_threads=8
innodb_write_io_threads=8

3. 高性能服务器

  • 单 Reactor + 多线程
  • 多 Reactor + 线程池
  • io_uring + coroutine
  • DPDK 旁路内核

十四、核心要点速记

  • 5 大 IO 模型:阻塞、非阻塞、多路复用、信号驱动、AIO
  • epoll = Linux 高性能 IO 多路复用,O(1) 复杂度
  • LT (水平触发) = 默认,有数据就触发
  • ET (边缘触发) = 状态变化才触发,必须用非阻塞 + 完整读
  • Reactor = 事件驱动 + 多路复用 (主流)
  • Proactor = 真异步,需系统支持 (Windows IOCP / io_uring)
  • io_uring = Linux 5.1+ 现代化异步 IO,零拷贝
  • 零拷贝 = sendfile / splice / mmap / io_uring
  • IO 调度器:NVMe=none/mq-deadline,HDD=bfq
  • select 限制 1024 fd,poll/epoll 不限
  • epoll 三 API:epoll_create / epoll_ctl / epoll_wait
  • mmap = 高效内存映射
  • sendfile = 静态文件服务最佳
  • 直接 IO O_DIRECT = 绕过 Page Cache
  • iostat -x 1 = 看磁盘 IO
  • iotop = 实时进程 IO
  • fio = 磁盘压测神器
  • net.ipv4.tcp_rmem/wmem = TCP 缓冲区
  • Reactor 单线程 = Redis,Nginx
  • 多 Reactor = Netty、Nginx worker
  • ASLR = 内存地址随机化(安全)
  • DPDK = 用户态驱动,极致性能
  • RDMA = 远程直接内存访问
  • ASLR / PIE = 缓解缓冲区溢出攻击