使用 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 内核实现了文件操作的高度可扩展性和可移植性。