linux内核读写IO过程中的iovec及其相关的结构体

本文深入探讨了Linux内核中的iov_iter结构体,这是一种用于简化用户态缓存数据处理的原语。文章详细介绍了iov_iter的由来、结构定义、初始化及使用方法,包括与iovec、page页结构之间的数据拷贝接口,以及如何获取和映射用户空间缓冲区到内核。同时,文章还提到了iov_iter结构体的扩展应用,如在网络子系统和块设备层的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

接口如下:

ssize_t generic_perform_write(struct file *file, struct iov_iter *i, loff_t pos)

《存储技术原理分析》上讲到了结构体iovec的由来,概括起来就是我们通常用的系统调用read/write用于读取或写入文件,比如read用于读取数据到一个用户态缓冲区,readv读取数据到多个用户态缓冲区,那么为了兼容这两种syscall,引入了数据结构iovec,而iov_iter又是对iovec的迭代。故使用iov_iter结构体的本质是用于协助处理用户态缓冲区数据和页缓存之间的映射关系。

以下内容是翻译过来的,原文内容链接:

https://lwn.net/Articles/625077/

1. 结构体介绍

内核中最常见的任务之一就是处理用户态的缓存数据,所以,这种经常出错的内核代码通常会引起bug或者可能会导致安全漏洞。但是内核包含一个原语,称为“iov_iter”,就用于简化此任务。然而iov_iter结构经常被用于内核管理或者文件系统层。但是近几年来,这种数据结构也逐渐被用在了内核的其他模块。该结构体目前还没有输出文档,本文就起到了说明文档的作用,用于介绍该数据结构。

iov_iter的概念并不是最新出现的,它是由nick piggin在2007年发布的2.6.24版本内核中首次添加的。但在过去的一年里,人们一直在努力扩展这个api,并在内核更多的模块中使用它。例如,3.19发布的版本就可以看到它已经编译进网络子系统模块。

iov_iter结构实际上是一个迭代器,用于遍历iovec结构,而iovec定义在<uapi/linux/uio.h>文件中,如下图所示。

 
  1. struct iovec

  2. {

  3. void __user *iov_base; //用户空间缓存区地址

  4. __kernel_size_t iov_len; //缓冲区长度

  5. };

此结构与posix接口定义的用户空间iovec结构体相匹配,并与readv()系统调用一起使用。正如名称“vec”部分的描述,iovec结构倾向于以数组的形式出现;作为一个整体,iovec描述了一个缓冲区,这个缓冲区可能分散在物理和虚拟内存中。实际iov_iter结构体定义在<linux/uio.h>文件中,如下图所示:

 
  1. struct iov_iter {

  2. int type;

  3. size_t iov_offset;

  4. size_t count;

  5. const struct iovec *iov; /* SIMPLIFIED - see below */

  6. unsigned long nr_segs;

  7. };

● type:描述迭代器类型。它是一个bit位,包含读和写,读写取决于数据是读到迭代器还是从迭代器写入。因此,数据处理方向并不是指迭代器本身,而是数据处理的另外一个部分,即被读操作创建的iov_iter将被写入;

● iov_offset:描述的是由该结构体中iov指针指向的第一个iovec结构体中数据的第一个字节的偏移;

● count:iovec数组指向的数据个数存放在count中;

● nr_regs:iovec结构体的个数存放在nr_segs中;

注意:以上这些字段都会随代码在缓存中的更新而更新。

2. struct iov_iter的使用

在使用之前,必须初始化iov_iter结构体,用于包含一个(已填充的)iovec结构体:

 
  1. void iov_iter_init(struct iov_iter *i, int direction,

  2. const struct iovec *iov, unsigned long nr_segs,

  3. size_t count);

可以使用以下任一方法在iov_iter迭代器结构体和用户态空间之间拷贝数据:

 
  1. size_t copy_to_iter(void *addr, size_t bytes, struct iov_iter *i);

  2. size_t copy_from_iter(void *addr, size_t bytes, struct iov_iter *i);

这里的命名可能会有点混乱,直到你掌握它的窍门。调用copy_to_iter()将字节数据从addr地址指向的缓冲区复制字节个数为bytes的数据到迭代器指针指向的用户空间缓冲区地址。所以copy_to_iter()可以被认为是copy_to_user()的一个变体,只不过它接受的是一个迭代器结构体而不是一个单一的缓冲区地址。类似地,copy_from_iter()将数据从用户空间缓冲区复制到addr。函数执行返回值也和copy_to_user()保持一致,即未被拷贝的字节个数。

注意,这些调用通过对应传输数据的缓冲区的增加来递增iov_iter结构体的地址的变化。换句话说,迭代器的iov_offset、count、nr_segs和iov字段都将根据需要进行更改。因此,两次调用copy_from_iter()将从用户空间拷贝两个连续区域。另外,执行迭代器的指令必须记住iovec数组的基地址,因为iov-iter结构中的 struct iovec *iov地址值会发生变化。

数据在page页结构和迭代器iov_iter之间进行拷贝的接口如下所示:

 
  1. size_t copy_page_to_iter(struct page *page, size_t offset, size_t bytes,

  2. struct iov_iter *i);

  3. size_t copy_page_from_iter(struct page *page, size_t offset, size_t bytes,

  4. struct iov_iter *i);

注意:只有提供的单个page页将被拷贝进数据或从中拷贝出数据,因此不要求这些函数对跨越页面边界的数据进行处理。

在原子上下文中运行的代码可以尝试通过以下方式从用户空间获取数据:

size_t iov_iter_copy_from_user_atomic(struct page *page, struct iov_iter *i, unsigned long offset, size_t bytes);

由于此拷贝操作要在原子模式下完成,因此仅当数据已驻留在RAM中时才会成功;因此,调用者必须为较高失败几率做好准备。

如果需要将用户空间缓冲区映射到内核中,可以使用以下函数:

 
  1. ssize_t iov_iter_get_pages(struct iov_iter *i, struct page **pages,

  2. size_t maxsize, unsigned maxpages, size_t *start);

  3. ssize_t iov_iter_get_pages_alloc(struct iov_iter *i, struct page ***pages,

  4. size_t maxsize, size_t *start);

这两个函数都会先调用get_user_pages_fast,获取页面,并将这一页存储在页面缓存中。它们之间的区别在于iov_iter_get_pages()是页缓存数组由调用者自己分配,iov_iter_get_pages_alloc()由内核自己分配。在这种情况下,调用者自己通过kmalloc或vmalloc函数申请的页缓存数组在函数返回时,数组必须调用kvfree进行释放。

在迭代器中前进而不移动任何数据可以使用:

void iov_iter_advance(struct iov_iter *i, size_t size);

由迭代器(或其一部分)指向的缓冲区可以用以下方法清除:

size_t iov_iter_zero(size_t bytes, struct iov_iter *i);

有关迭代器结构体中相关信息可以从以下函数进行获取

 
  1. size_t iov_iter_single_seg_count(const struct iov_iter *i);

  2. int iov_iter_npages(const struct iov_iter *i, int maxpages);

  3. size_t iov_length(const struct iovec *iov, unsigned long nr_segs);

调用iov_iter_single_seg_count()返回缓冲区第一段中数据的长度。iov_iter_npages()返回迭代器中缓冲区占用的页数,而iov_length()返回总数据长度,另外注意这个函数必须小心使用,因为它信任iovec结构中的len字段。如果该数据来自用户空间,则可能导致内核中的整数溢出。

3. 扩展部分

上面显示的结构体iov_iter的定义与内核中实际找到的不完全匹配。真正的结构并不是只有iov数组单个字段,而是如下所示(在3.18中)

 
  1. union {

  2. const struct iovec *iov;

  3. const struct bio_vec *bvec;

  4. };

换句话说,iov_iter结构体中也包含了块设备层使用到的BIO结构体。所以在该迭代器结构体中,可以看到iter_bvec这样的结构体存在。一旦这个迭代器结构体被创建出来,上面提到的所有函数都可以调用它。目前,在内核中使用基于bio的迭代器很少,它们只能在swap和splice()代码中找到。

3.19版本

3.19内核可能会对iov_iter代码进行大量重写,目的是减少由上面函数实现的大量的冗余代码。重构后的代码更加短小精悍,但代价是引入了大量的宏定义。

在3.19内核源码中,迭代器由如下接口生成:

 
  1. void iov_iter_kvec(struct iov_iter *i, int direction,

  2. const struct kvec *iov, unsigned long nr_segs,

  3. size_t count);

对于这种情况,需要在上面union类型中添加一个新的kvec字段。

最后,可以添加一些函数来帮助处理网络问题的发生,比如可以拷贝一段缓冲区数据,并且生成一个checksum用于校验。

目前,iov_iter结构体正在逐渐一种方式,用于屏蔽处理用户态缓冲区数据带来的复杂性。这套逻辑目前已经存在了大约7年,那么可以预见iov_iter结构体会逐渐衍生出更多的函数接口供内核开发者使用

iov_iter接口。

### Linux Kernel `file_operations` 结构体内与 `iter` 相关的配置项 在Linux内核编程中,`file_operations` 是一个非常重要的结构体,用于定义文件操作函数集。对于涉及迭代器 (`iter`) 的功能,主要关注的是读写操作以及如何高效处理这些操作。 #### 1. 使用 iov_iter 处理 I/O 向量 为了支持复杂的I/O向量操作,在现代版本的Linux内核中引入了 `iov_iter` 类型[^1]。此类型可以表示多种类型的缓冲区序列,包括用户空间指针数组、内核空间指针数组等。通过这种方式,能够更灵活地实现数据传输逻辑而不必关心底层的具体细节。 ```c struct iovec { void *iov_base; /* Starting address */ size_t iov_len; /* Number of bytes to transfer */ }; struct iov_iter { ... }; ``` 当涉及到具体的数据复制时,会调用类似于 `_sw_accel_copy_iovs` 函数来进行实际的数据搬运工作,这里利用到了 `spdk_ioviter` 来遍历并获取源和目标地址以便执行 `memcpy` 操作。 #### 2. 文件操作接口中的 iter 方法 针对不同的应用场景,`file_operations` 提供了几种基于迭代器的方法: - **read_iter/write_iter**: 新式的读取/写入方法,接受 `kiocb` 和 `iov_iter` 参数,允许更加高效的零拷贝和其他优化技术的应用。 ```c ssize_t (*read_iter)(struct kiocb *, struct iov_iter *); ssize_t (*write_iter)(struct kiocb *, struct iov_iter *); ``` 这种方法相比传统的 read/write 接口提供了更好的性能表现,尤其是在面对大容量数据流的情况下[^2]。 #### 3. 配置选项的影响 某些情况下,编译内核时可以选择启用或禁用特定的功能模块,这可能会间接影响到 `file_operations` 中关于 `iter` 的行为。例如: - CONFIG_IO_URING: 支持 io_uring 子系统,该子系统依赖于高效的异步 I/O 实现方式,因此会对 `iter` 的使用有特殊需求。 - CONFIG_GENERIC_ITERATOR: 如果启用了通用迭代器的支持,则会影响整个系统的 I/O 路径设计。 然而需要注意的是,并不是所有的配置项都会直接影响到具体的 API 行为;更多的是决定了哪些特性可以在运行时被激活。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值