分散 / 聚集 IO

网络编程中常用的分散 / 聚集 IO 技术

1. readvwritev:分散 / 聚集 I/O

readv(分散读)和 writev(聚集写)是用于处理分散缓冲区的 I/O 函数,核心作用是通过一次系统调用完成对多个不连续缓冲区的读写操作,减少系统调用次数(系统调用存在上下文切换开销)。

函数原型

1
2
3
4
5
6
7
#include <sys/uio.h>

// 从fd读取数据,分散存储到iov指向的多个缓冲区
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

// 将iov指向的多个缓冲区数据聚集后写入fd
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

关键参数

  • struct iovec描述一个缓冲区的结构,定义为:

    1
    2
    3
    4
    struct iovec {
    void *iov_base; // 缓冲区起始地址
    size_t iov_len; // 缓冲区长度(字节数)
    };
  • iovcnt:缓冲区数量,上限由系统限制(通常IOV_MAX,如 Linux 为 1024)。

功能与适用场景

  • readv:从文件描述符fd读取数据,按顺序填充iov数组中的缓冲区(前一个填满后再填下一个),返回实际读取的字节数。

  • writev:将iov数组中所有缓冲区的数据按顺序拼接后写入fd,返回实际写入的字节数。

网络编程场景:在构造协议报文时(如 HTTP 响应包含状态行、头部字段、正文等多个独立部分),可通过writev一次发送所有部分,避免多次write调用的开销;同理,接收报文时若需拆分到不同缓冲区(如头部和正文分离),可使用readv

注意事项

  • 若读取 / 写入中途发生错误,仍会返回已成功处理的字节数(部分成功)。

  • 对于非阻塞fd,行为与read/write一致(可能返回EAGAINEWOULDBLOCK)。

2. dupdup2:文件描述符复制

dupdup2 用于复制文件描述符,使新的描述符与原描述符指向同一个文件表项(共享文件偏移量、文件状态标志等),核心作用是实现 I/O 重定向。

函数原型

1
2
3
4
5
6
7
#include <unistd.h>

// 复制oldfd,返回最小的未使用描述符
int dup(int oldfd);

// 将newfd重定向为oldfd的副本(若newfd已打开则先关闭)
int dup2(int oldfd, int newfd);

功能差异

  • dup:自动选择一个最小的未被使用的描述符作为新描述符,与oldfd指向同一文件。

  • dup2:显式指定新描述符newfd,若newfd已打开,则先关闭它(若newfd == oldfd,则直接返回newfd,不关闭)。

网络编程场景

  • 重定向标准 I/O:例如在网络服务中,将客户端socket的描述符通过dup2(client_fd, STDOUT_FILENO)重定向为标准输出,此时printf的内容会直接发送给客户端。

  • 简化接口适配:某些库函数仅支持固定描述符(如标准输出),通过dup2可将其适配到 socket 描述符。

注意事项

  • oldfd无效(未打开),两函数均返回-1并设置errnoEBADF

  • dup2关闭newfd时若失败(如权限问题),会返回-1,但此时newfd可能已处于关闭状态,需注意错误处理。

3. sendfile:零拷贝文件传输

sendfile 用于在两个文件描述符之间直接传输数据,数据无需经过用户空间,全程在内核空间处理,属于零拷贝(zero-copy) 技术,可大幅提升大文件传输效率。

函数原型(Linux)

1
2
3
4
#include <sys/sendfile.h>

// 将in_fd的数据传输到out_fd,返回实际传输的字节数
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

关键参数与限制

  • in_fd:源文件描述符,必须是支持mmap的常规文件(不能是 socket 或管道)。

  • out_fd:目标文件描述符,在 Linux 中必须是 socket(其他系统可能支持更多类型)。

  • offset:指定in_fd的起始读取位置,若不为NULL,传输后会更新为实际结束位置(原子操作,避免多线程竞争)。

  • count:计划传输的字节数。

功能与优势

传统文件传输流程(read + write)需要 4 次数据拷贝(磁盘→内核缓冲区→用户缓冲区→socket 缓冲区→网卡)和 2 次系统调用;而sendfile通过内核直接将文件数据从页缓存传输到 socket 缓冲区,仅需 2 次拷贝(磁盘→内核缓冲区→socket 缓冲区)和 1 次系统调用,显著减少开销。

网络编程场景:在文件服务器中(如 HTTP 服务器传输静态资源),sendfile是高效传输文件的首选方式,比read + write组合性能提升明显。

注意事项

  • 跨平台差异大:Linux 的sendfile与 BSD 的sendfile参数和行为不同,移植时需注意。

  • 不支持非阻塞 I/O:若out_fd为非阻塞 socket 且暂时无法写入,sendfile可能返回-1并设置errnoEAGAIN,但部分实现可能不支持。

  • in_fd必须是常规文件:不能用于 socket 之间的数据转发(需用splice等其他函数)。

总结

  • readv/writev:优化多缓冲区 I/O,减少系统调用次数,适合协议报文的拼接 / 拆分。

  • dup/dup2:实现文件描述符重定向,简化 I/O 接口适配。

  • sendfile:利用零拷贝技术高效传输文件,是大文件网络传输的核心优化手段。