高级I/O函数:readv()、writev()、sendfile()、mmap()共享内存

各位同学,今天我们来聊聊网络编程中的几个高级I/O函数。说实话,这些函数在日常开发中用得不算特别频繁,但一旦用上,往往能解决一些棘手的问题。我个人习惯把它们称为「效率三剑客」——readv、writev、sendfile,再加上一个mmap共享内存。

你想想看,传统的read/write每次只能操作一个缓冲区。如果你要发送多个数据块,就得循环调用多次。这就像搬家,一次只能搬一个箱子,效率可想而知。而高级I/O函数,说白了就是让你一次搬多个箱子,甚至直接把箱子从仓库搬到车上,省去中间环节。

1. readv() 和 writev():分散读与集中写

这两个函数是成对出现的。readv用于从文件描述符读取数据到多个缓冲区,writev则把多个缓冲区的数据一次性写入文件描述符。我当年做高性能日志系统时,就靠它们把日志头、时间戳、消息体拼在一起发送,省去了多次系统调用。

核心优势:减少系统调用次数,降低上下文切换开销。

函数原型

#include <sys/uio.h>

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

这里的iovec结构体是关键:

struct iovec {
    void  *iov_base;  // 缓冲区起始地址
    size_t iov_len;   // 缓冲区长度
};

实战示例:用writev发送HTTP响应

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/uio.h>
#include <unistd.h>
#include <sys/socket.h>

void send_http_response(int client_fd) {
    // 准备三个数据块:状态行、头部、正文
    char *status_line = "HTTP/1.1 200 OK\r\n";
    char *headers = "Content-Type: text/html\r\nContent-Length: 13\r\n\r\n";
    char *body = "<h1>Hello</h1>";

    struct iovec iov[3];
    iov[0].iov_base = status_line;
    iov[0].iov_len = strlen(status_line);
    iov[1].iov_base = headers;
    iov[1].iov_len = strlen(headers);
    iov[2].iov_base = body;
    iov[2].iov_len = strlen(body);

    // 一次系统调用,发送所有数据
    ssize_t sent = writev(client_fd, iov, 3);
    if (sent == -1) {
        perror("writev failed");
    } else {
        printf("Sent %zd bytes\n", sent);
    }
}

我的经验:iovcnt参数不要太大,一般不超过16。我在项目中遇到过iovcnt设置成100的情况,结果内核需要分配大量内存来拷贝这些指针,反而得不偿失。

2. sendfile():零拷贝的利器

sendfile是我最喜欢的函数之一。它可以直接把文件内容发送到socket,不需要经过用户空间缓冲区。我曾经做过一个文件下载服务器,用sendfile之后,CPU占用率从30%降到了5%以下。

函数原型

#include <sys/sendfile.h>

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

参数说明:

  • out_fd:目标文件描述符,通常是socket
  • in_fd:源文件描述符,必须是支持mmap的文件
  • offset:从文件哪个位置开始读,NULL表示当前位置
  • count:要传输的字节数

零拷贝原理

传统方式发送文件需要4次上下文切换和4次数据拷贝:

  1. read:文件→内核缓冲区→用户缓冲区
  2. write:用户缓冲区→内核缓冲区→socket

而sendfile只需要2次上下文切换和3次数据拷贝(甚至2次,如果硬件支持DMA):

  1. 文件→内核缓冲区(DMA)
  2. 内核缓冲区→socket缓冲区(CPU拷贝)
  3. socket缓冲区→网卡(DMA)

注意:sendfile的in_fd必须指向真实文件,不能是socket或管道。我曾经犯过这个错误,调试了半天才发现。

3. mmap():共享内存的基石

mmap可以把文件或设备映射到进程的地址空间。这样一来,读写文件就像读写内存一样简单。我参与过一个数据库项目,就是用mmap来管理数据文件的,性能提升非常明显。

函数原型

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

关键参数:

  • prot:保护标志,常用组合:
    • PROT_READ | PROT_WRITE(读写)
    • PROT_READ(只读)
  • flags:映射类型:
    • MAP_SHARED:修改会写回文件,对其他进程可见
    • MAP_PRIVATE:修改不写回文件,类似写时拷贝
    • MAP_ANONYMOUS:匿名映射,不依赖文件

实战:用mmap实现进程间通信

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define SHM_SIZE 4096

int main() {
    // 创建共享内存文件
    int fd = open("/dev/shm/my_shm", O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    // 设置文件大小
    ftruncate(fd, SHM_SIZE);

    // 映射到进程地址空间
    void *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, 
                     MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(1);
    }

    // 写入数据
    strcpy((char *)ptr, "Hello from process A!");

    printf("Data written: %s\n", (char *)ptr);

    // 清理
    munmap(ptr, SHM_SIZE);
    close(fd);
    return 0;
}

避坑指南:我曾经在mmap后忘记调用ftruncate设置文件大小,结果访问超出文件大小的内存区域时,直接触发了SIGBUS信号,进程崩溃。记住:文件大小必须大于等于映射长度。

4. 知识体系总览

下面这张图展示了这四个高级I/O函数的核心关系和应用场景:

高级I/O函数知识体系 readv() 分散读 writev() 集中写 sendfile() 零拷贝 mmap() 共享内存 核心目标:减少系统调用,提升I/O效率 典型应用场景 HTTP服务器 文件下载服务 进程间通信 数据库引擎 选择依据:数据量大小、是否需要零拷贝、是否跨进程

5. 性能对比

函数 系统调用次数 数据拷贝次数 适用场景
read/write N次 4N次 小数据量,简单场景
readv/writev 1次 4次 多缓冲区拼接/拆分
sendfile 1次 2-3次 文件到socket传输
mmap 1次(映射时) 1次(缺页时) 大文件、共享内存

总结一下:

  • 如果你需要发送多个不连续的内存块,用writev,别傻傻地循环write
  • 如果你要从文件发送数据到socket,用sendfile,零拷贝真香
  • 如果你需要多个进程共享数据,或者处理大文件,用mmap,但记得处理SIGBUS
  • readv用得相对少一些,但在解析协议头时特别好用

嗯,这些高级I/O函数就讲到这里。说实话,刚开始学的时候我也觉得它们可有可无,但真正遇到性能瓶颈时,才发现这些函数才是解决问题的关键。记住:系统调用是昂贵的,能合并就合并,能跳过就跳过。

公众号:蓝海资料掘金营,微信 deep3321