使用 iovec 和 writev
 
简介
在 Unix 系统编程中,iovec 和 writev 提供了一种高效的方法来进行 I/O 操作,特别是在需要处理多个非连续缓冲区时。通过减少系统调用的次数,它们提高了 I/O 的性能。
iovec 结构体
 
iovec 是一个描述内存缓冲区的数据结构,定义在 <sys/uio.h> 头文件中。它的结构如下:
struct iovec {
    void  *iov_base;    /* Pointer to data */
    size_t iov_len;     /* Length of data */
};
 
- 
iov_base是指向数据的指针。 - 
iov_len是数据的长度。 
writev 系统调用
 
writev 是一种向文件描述符写入数据的系统调用,它允许从多个缓冲区一次性写入数据。其原型如下:
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
 
- 
fd:文件描述符,表示要写入的目标。 - 
iov:指向iovec结构体数组的指针。 - 
iovcnt:iovec结构体数组中的元素数量。 
writev 的实现原理
 
- 
参数校验:检查 
fd、iov和iovcnt是否有效。 - 
锁定内存:锁定用户态的 
iovec数组。 - 分配内核缓冲区:分配内存用于存储数据。
 - 数据拷贝:将数据从用户态拷贝到内核缓冲区。
 - 实际写操作:调用文件系统或设备驱动的写操作。
 - 清理和返回:释放内存并返回写入的字节数。
 
使用示例
以下是一个使用 writev 的示例:
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
int main() {
    int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    struct iovec iov[2];
    char *buf1 = "Hello, ";
    char *buf2 = "world!\n";
    iov[0].iov_base = buf1;
    iov[0].iov_len = strlen(buf1);
    iov[1].iov_base = buf2;
    iov[1].iov_len = strlen(buf2);
    ssize_t nwritten = writev(fd, iov, 2);
    if (nwritten == -1) {
        perror("writev");
        close(fd);
        return -1;
    }
    printf("Wrote %zd bytes\n", nwritten);
    close(fd);
    return 0;
}
 
优化技术
分批处理
当需要处理大量数据时,可以将数据分成多个批次,以避免 iovec 数量超过系统限制。例如:
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#define MAX_IOVEC 1024
int main() {
    int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    struct iovec iov[MAX_IOVEC];
    int total_buffers = 5000;
    char *buffers[5000];
    for (int i = 0; i < total_buffers; ++i) {
        buffers[i] = "data\n";
    }
    for (int i = 0; i < total_buffers; i += MAX_IOVEC) {
        int batch_size = (total_buffers - i > MAX_IOVEC) ? MAX_IOVEC : (total_buffers - i);
        for (int j = 0; j < batch_size; ++j) {
            iov[j].iov_base = buffers[i + j];
            iov[j].iov_len = strlen(buffers[i + j]);
        }
        ssize_t nwritten = writev(fd, iov, batch_size);
        if (nwritten == -1) {
            perror("writev");
            close(fd);
            return -1;
        }
    }
    close(fd);
    return 0;
}
 
资源管理
确保应用程序在使用系统资源时进行适当管理,避免一次性占用过多资源,导致系统资源不足或性能下降。
结论
通过合理使用 iovec 和 writev,可以有效提高 I/O 操作的性能,特别是在需要处理多个非连续缓冲区时。理解其实现原理和限制,有助于更好地优化应用程序的 I/O 性能。
Linux Kernel 中 writev 的实现
 
在 Linux 内核中,writev 的实现涉及多个层次,包括系统调用的入口、文件操作接口(file_operations)以及具体文件系统或设备驱动的实现。
系统调用入口
writev 系统调用的入口函数在内核中通常是 sys_writev。该函数首先会对传入的参数进行校验,然后调用内核内部的通用写入函数。
asmlinkage ssize_t sys_writev(unsigned long fd, const struct iovec __user *vec, unsigned long vlen)
{
    ssize_t ret;
    struct file *file;
    struct iovec *iov = NULL;
    struct iov_iter iter;
    if (vlen > UIO_MAXIOV)
        return -EINVAL;
    ret = import_iovec(WRITE, vec, vlen, UIO_FASTIOV, &iov, &iter);
    if (ret < 0)
        return ret;
    file = fget(fd);
    if (file) {
        ret = vfs_writev(file, &iter, &pos, 0);
        fput(file);
    } else {
        ret = -EBADF;
    }
    kfree(iov);
    return ret;
}
 
文件操作接口
sys_writev 最终调用的是 vfs_writev,后者是内核虚拟文件系统(VFS)层的函数。vfs_writev 会根据文件描述符对应的文件类型,调用具体文件系统或设备驱动的 write_iter 方法。
ssize_t vfs_writev(struct file *file, const struct iov_iter *iter, loff_t *pos, rwf_t flags)
{
    if (file->f_op->write_iter)
        return file->f_op->write_iter(file, iter);
    return do_iter_write(file, iter, pos);
}
 
file_operations 的 write_iter 方法
 
具体文件系统或设备驱动需要实现 file_operations 结构体中的 write_iter 方法。例如,对于 ext4 文件系统,这个方法如下:
const struct file_operations ext4_file_operations = {
    .read_iter      = generic_file_read_iter,
    .write_iter     = ext4_file_write_iter,
    ...
};
 
ext4_file_write_iter 的实现如下:
ssize_t ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    return generic_file_write_iter(iocb, from);
}
 
generic_file_write_iter 是一个通用的写入函数,它处理大部分通用逻辑,然后调用底层文件系统的具体写入操作。
底层实现
底层文件系统或设备驱动需要实现具体的写入操作,这通常涉及到将数据写入磁盘或其他存储介质。例如,对于块设备驱动,这个操作可能会涉及到直接与硬件交互。
小结
writev 系统调用从用户态到内核态的实现过程涉及多个层次:
- 用户态调用 
writev系统调用。 - 内核态的 
sys_writev函数处理参数并调用vfs_writev。 - 
vfs_writev调用具体文件系统或设备驱动的write_iter方法。 - 具体文件系统或设备驱动的 
write_iter方法实现数据的实际写入。 
通过这种分层结构,Linux 内核实现了文件操作的高度可扩展性和可移植性。