Linux I/O 接口的类型及处理流程

linux i/o 接口linux i/o 接口可以分为以下几种类型:
文件 i/o 接口:用于对文件进行读写操作的接口,包括 open()、read()、write()、close()、lseek() 等。
网络 i/o 接口:用于网络通信的接口,包括 socket()、connect()、bind()、listen()、accept() 等。
设备 i/o 接口:用于对设备(e.g. 字符设备、块设备)进行读写操作的接口,包括 ioctl()、mmap()、select()、poll()、epoll() 等。
其他 i/o 接口:如管道接口、共享内存接口、信号量接口等。
linux i/o 处理流程下面以最常用的 read() 和 write() 函数来介绍 linux 的 i/o 处理流程。
read() 和 write()read() 和 write() 函数,是最基本的文件 i/o 接口,也可用于在 tcp socket 中进行数据读写,属于阻塞式 i/o(blocking i/o),即:如果没有可读数据或者对端的接收缓冲区已满,则函数将一直等待直到有数据可读或者对端缓冲区可写。
函数原型:
fd 参数:指示 fd 文件描述符。
buf 参数:指示 read/write 缓冲区的入口地址。
count 参数:指示 read/write 数据的大小,单位为 byte。
函数返回值:
返回实际 read/write 的字节数。返回 0,表示已到达文件末尾。返回 -1,表示操作失败,可以通过 errno 全局变量来获取具体的错误码。#include
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
处理流程下面以同时涉及了 storage i/o 和 network i/o 的一次网络文件下载操作来展开 read() 和 write() 的处理流程。
read() 的处理流程:
application 调用 read(),cpu 模式从用户态切换到内核态。kernel 根据 file fd 查表(进程文件符表),找到对应的 file 结构体(普通文件),从而找到此文件的 inode 编号。kernel 将 buf 和 count 参数、以及文件指针位置等信息传递给 device driver(磁盘驱动程序)。driver 将请求的数据从 disk device 中 dma copy 到 kernel pagecache buffer 中。kernel 将数据从 kernel pagecache buffer 中 cpu copy 到 userspace buffer 中(application 不能直接访问 kernel space)。read() 最终返回读取的字节数或错误代码给 application,cpu 模式从内核态切换到用户态。write() 的处理流程:
application 调用 write(),cpu 模式从用户态切换到内核态。kernel 根据 socket fd 查表,找到对应的 file 结构体(套接字文件),从而找到该 socket 的 sock 结构体。kernel 将 buf 和 count 参数、以及文件指针位置等信息传递给 device driver(网卡驱动程序)。driver 将请求的数据从 userspace buffer 中 cpu copy 到 kernel socket buffer 中。kernel 将数据从 kernel socket buffer 中 dma copy 到 nic device。write() 最终返回写入的字节数或错误代码给 application,cpu 模式从内核态切换到用户态。可见,在一次常规的 i/o(read/write)操作流程中 处理流程中,总共需要涉及到:
4 次 cpu 模式切换:当 application 调用 sci 时,cpu 从用户态切换到内核态;当 sci 返回时,cpu 从内核态切换回用户态。2 次 cpu copy:cpu 执行进程数据拷贝指令,将数据从 user process 虚拟地址空间 copy 到 kernel 虚拟地址空间。2 次 dma copy:cpu 向 dma 控制器下达设备数据拷贝指令,将数据从 dma 物理内存空间 copy 到 kernel 虚拟地址空间。
i/o 性能优化机制i/o buff/cachelinux kernel 为了提高 i/o 性能,划分了一部分物理内存空间作为 i/o buff/cache,也就是内核缓冲区。当 kernel 接收到 read() / write() 等读写请求时,首先会到 buff/cache 查找,如果找到,则立即返回。如果没有则通过驱动程序访问 i/o 外设。
查看 linux 的 buff/cache:
$ free -mh
total used free shared buff/cache available
mem: 7.6g 4.2g 2.9g 10m 547m 3.1g
swap: 4.0g 0b 4.0g
实际上,cache(缓存)和 buffer(缓冲)从严格意义上讲是 2 个不同的概念,cache 侧重加速 “读”,而 buffer 侧重缓冲 “写”。但在很多场景中,由于读写总是成对存在的,所以并没有严格区分两者,而是使用 buff/cache 来统一描述。
page cache
page cache(页缓存)是最常用的 i/o cache 技术,以页为单位的,内容就是磁盘上的物理块,用于减少 application 对 storage 的 i/o 操作,能够令 application 对文件进行顺序读写的速度接近于对内存的读写速度。
页缓存读策略:当 application 发起一个 read() 操作,kernel 首先会检查需要的数据是否在 page cache 中:
如果在,则直接从 page cache 中读取。如果不在,则按照原 i/o 路径从磁盘中读取。同时,还会根据局部性原理,进行文件预读,即:将已读数据随后的少数几个页面(通常是三个)一同缓存到 page cache 中。页缓存写策略:当 application 发起一个 write() 操作,kernel 首先会将数据写到 page cache,然后方法返回,即:write back(写回)机制,区别于 write through(写穿)。此时数据还没有真正的写入到文件中去,kernel 仅仅将已写入到 page cache 的这一个页面标记为 “脏页(dirty page)”,并加入到脏页链表中。然后,由 flusher(pdflush,page dirty flush)kernel thread(回写内核线程)周期性地将脏页链表中的页写到磁盘,并清理 “脏页” 标识。在以下 3 种情况下,脏页会被写回磁盘:
当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘。当 application 主动调用 sync、fsync、fdatasync 等 sci 时,内核会执行相应的写回操作。flusher 刷新策略由以下几个内核参数决定(数值单位均为 1/100 秒):
# flush 每隔 5 秒执行一次
$ sysctl vm.dirty_writeback_centisecs
vm.dirty_writeback_centisecs = 500
# 内存中驻留 30 秒以上的脏数据将由 flush 在下一次执行时写入磁盘
$ sysctl vm.dirty_expire_centisecs
vm.dirty_expire_centisecs = 3000
# 若脏页占总物理内存 10% 以上,则触发 flush 把脏数据写回磁盘
$ sysctl vm.dirty_background_ratio
vm.dirty_background_ratio = 10
综上可见,page cache 技术在理想的情况下,可以在一次 storage i/o 的流程中,减少 2 次 dma copy 操作(不直接访问磁盘)。
buffered i/o下图展示了一个 c 程序通过 stdio 库中的 printf() 或 fputc() 等输出函数来执行数据写入的操作处理流程。过程中涉及到了多处 i/o buffer 的实现:
stdio buffer:在 userspace 实现的 buffer,因为 sci 的成本昂贵,所以,userspace buffer 用于 “积累“ 到更多的待写入数据,然后再通过一次 sci 来完成真正的写入。另外,stdio 也支持 fflush() 强制刷新函数。kernel buffer cache:处理包括上文以及提到的 page cache 技术之外,磁盘设备驱动程序也提供块级别的 buffer 技术,用于 “积累“ 更多的文件系统元数据和磁盘块数据,然后在合适的时机完成真正的写入。
零拷贝技术(zero-copy)零拷贝技术(zero-copy),是通过尽量避免在 i/o 处理流程中使用 cpu copy 和 dma copy 的技术。实际上,零拷贝并非真正做到了没有任何拷贝动作,它更多是一种优化的思想。
下列表格从 cpu copy 次数、dma copy 次数以及 sci 次数这 3 个方面来对比了几种常见的零拷贝技术。可以看见,2 次 dma copy 是不可避免的,因为 dma 是外设 i/o 的基本行为。零拷贝技术主要从减少 cpu copy 和 cpu 模式切换这 2 个方面展开。
1、userspace direct i/ouserspace direct i/o(用户态直接 i/o)技术的底层原理由 kernel space 中的 zone_dma 支持。zone_dma 是一块 kernel 和 user process 都可以直接访问的 i/o 外设 dma 物理内存空间。基于此, application 可以直接读写 i/o 外设,而 kernel 只会辅助执行必要的虚拟存储配置工作,不直接参与数据传输。因此,该技术可以减少 2 次 cpu copy。
userspace direct i/o 的缺点:
由于旁路了 要求 kernel buffer cache 优化,就需要 application 自身实现 buffer cache 机制,称为自缓存应用程序,例如:数据库管理系统。由于 application 直接访问 i/o 外设,会导致 cpu 阻塞,浪费 cpu 资源,这个问题需要结合异步 i/o 技术来规避。
具体流程看下图:using direct i/o with dma
2、mmap() + write()mmap() sci 用于将 i/o 外设(e.g. 磁盘)中的一个文件、或一段内存空间(e.g. kernel buffer cache)直接映射到 user process 虚拟地址空间中的 memory mapping segment,然后 user process 就可以通过指针的方式来直接访问这一段内存,而不必再调用传统的 read() / write() sci。
申请空间函数原型:
addr 参数:分配 mms 映射区的入口地址,由 kernel 指定,调用时传入 null。length 参数:指示 mms 映射区的大小。prot 参数:指示 mms 映射区的权限,可选:prot_read、prot_write、prot_read|prot_write 类型。flags 参数:标志位参数,可选:map_shared:映射区所做的修改会反映到物理设备(磁盘)上。map_private:映射区所做的修改不会反映到物理设备上。fd 参数:指示 mms 映射区的文件描述符。offset 参数:指示映射文件的偏移量,为 4k 的整数倍,可以映射整个文件,也可以只映射一部分内容。函数返回值:成功:更新 addr 入口地址。失败:更新 map_failed 宏。void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
释放空间函数原型:
addr 参数:分配 mms 映射区的入口地址,由 kernel 指定,调用时传入 null。length 参数:指示 mms 映射区的大小。函数返回值:成功:返回 0。失败:返回 -1。int munmap(void *addr, size_t length)
可见,mmap() 是一种高效的 i/o 方式。通过 mmap() 和 write() 结合的方式,可以实现一定程度的零拷贝优化。
// 读
buf = mmap(diskfd, len);
// 写
write(sockfd, buf, len);
mmap() + write() 的 i/o 处理流程如下。
mmap() 映射:
application 发起 mmap() 调用,进行文件操作,cpu 模式从用户态切换到内核态。mmap() 将指定的 kernel buffer cache 空间映射到 application 虚拟地址空间。mmap() 返回,cpu 模式从内核态切换到用户态。在 application 后续的文件访问中,如果出现 page cache miss,则触发缺页异常,并执行 page cache 机制。通过已经建立好的映射关系,只使用一次 dma copy 就将文件数据从磁盘拷贝到 application user buffer 中。write() 写入:
application 发起 write() 调用,cpu 模式从用户态切换到内核态。由于此时 application user buffer 和 kernel buffer cache 的数据是一致的,所以直接从 kernel buffer cache 中 cpu copy 到 kernel socket buffer,并最终从 nic 发出。write() 返回,cpu 模式从内核态切换到用户态。可见,mmap() + write() 的 i/o 处理流程减少了一次 cpu copy,但没有减少 cpu 模式切换的次数。另外,由于 mmap() 的进程间共享特性,非常适用于共享大文件的 i/o 场景。
mmap() + write() 的缺点:当 mmap 映射一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被 sigbus 信号终止,sigbus 默认会杀死进程并产生一个 coredump。解决这个问题通常需要使用文件租借锁实现。在 mmap 之前加锁,操作完之后解锁。即:首先为文件申请一个租借锁,当其他进程想要截断这个文件时,内核会发送一个实时的 rt_signal_lease 信号,告诉当前进程有进程在试图破坏文件,这样 write 在被 sigbus 杀死之前,会被中断,返回已经写入的字节数,并设置 errno 为 success。
3、sendfile()linux kernel 从 v2.1 开始引入了 sendfile(),用于在 kernel space 中将一个 in_fd 的内容复制到另一个 out_fd 中,数据无需经过 userspace,所以应用在 i/o 流程中,可以减少一次 cpu copy。同时,sendfile() 比 mmap() 方式更具安全性。
函数原型:
out_fd 参数:目标文件描述符,数据输入文件。in_fd 参数:源文件描述符,数据输出文件。该文件必须是可以 mmap 的。offset 参数:指定从源文件的哪个位置开始读取数据,若不需要指定,传递一个 null。count 参数:指定要发送的数据字节数。函数返回值:成功:返回复制的字节数。失败:返回 -1,并设置 errno 全局变量来指示错误类型。#include
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
sendfile() 处理流程:
application 调用 sendfile(),cpu 从用户态切换到内核态。kernel 将数据通过 dma copy 从磁盘设备写入 kernel buffer cache。kernel 将数据从 kernel buffer cache 中 cpu copy 到 kernel socket buffer。kernel 将数据从 kernel socket buffer 中 dma copy 到 i/o 网卡设备。sendfile() 返回,cpu 从内核态切换到用户态。
4、sendfile() + dma gather copy上文知道 sendfile() 还具有一次 cpu copy,通过结合 dma gather copy 技术,可以进一步优化它。
dma gather copy 技术,底层有 i/o 外设的 dma controller 提供的 gather 功能支撑,所以又称为 “dma 硬件辅助的 sendfile()“。借助硬件设备的帮助,在数据从 kernel buffer cache 到 kernel socket buffer 之间,并不会真正的数据拷贝,而是仅拷贝了缓冲区描述符(fd + size)。待完成后,dma controller,可以根据这些缓冲区描述符找到依旧存储在 kernel buffer cache 中的数据,并进行 dma copy。
显然,dma gather copy 技术依旧是 zone_dma 物理内存空间共享性的一个应用场景。
sendfile() + dma gather copy 的处理流程:
application 调用 sendfile(),cpu 从用户态切换到内核态模式。kernel 将数据通过 dma copy 从磁盘设备写入 kernel buffer cache。kernel 将数据的缓冲区描述符从 kernel buffer cache 中 cpu copy 到 kernel socket buffer(几乎不费资源)。基于缓冲区描述符,cpu 利用 dma controller 的 gather / scatter 操作直接批量地将数据从 kernel buffer cache 中 dma copy 到网卡设备。sendfile() 返回,cpu 从内核态切换到用户态。
5、splice()splice() 与 sendfile() 的处理流程类似,但数据传输方式有本质不同。
sendfile() 的传输方式是 cpu copy,且具有数据大小限制;splice() 的传输方式是 pipeline,打破了数据范围的限制。但也要求 2 个 fd 中至少有一个必须是管道设备类型。函数原型:
fd_in 参数:源文件描述符,数据输出文件。off_in 参数:输出偏移量指针,表示从源文件描述符的哪个位置开始读取数据。fd_out 参数:目标文件描述符,数据输入文件。off_out 参数:输入偏移量指针,表示从目标文件描述符的哪个位置开始写入数据。len 参数:指示要传输的数据长度。flags:控制数据传输的行为的标志位。#define _gnu_source /* see feature_test_macros(7) */
#include
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
splice() 的处理流程如下:
application 调用 splice(),cpu 从用户态切换到内核态。kernel 将数据通过 dma copy 从磁盘设备写入 kernel buffer cache。kernel 在 kernel buffer cache 和 kernel socket buffer 之间建立 pipeline 传输。kernel 将数据从 kernel socket buffer 中 dma copy 到 i/o 网卡设备。splice() 返回,cpu 从内核态切换到用户态。
6、缓冲区共享技术缓冲区共享技术,是对 linux i/o 的一种颠覆,所以往往需要由 application 和设备来共同实现。
其核心思想是:每个 applications 都维护着一个 buffer pool,并且这个 buffer pool 可以同时映射到 kernel 虚拟地址空间,这样 userspace 和 kernel space 就拥有了一块共享的空间。以此来规避掉 cpu copy 的行为。

因无人机高通大疆联芯强强联合
集创新新力量,探生态新应用 | 毫米波雷达“居家智能”应用大赛火热进行时
英国在是否允许华为提供5G技术问题上面临艰难抉择
虹科分享 | 带您了解太赫兹成像技术及系统方案(下)
万用表的基本原理,The principles of multimeter
Linux I/O 接口的类型及处理流程
石墨烯研发接连突破 未来或将成为理想产氢平台
ARM:从未断供华为 与海思会保持长期合作
被英特尔抢代工订单 台积电与富士通联盟共抗敌
雷达技术的进步和舱内传感的发展
一个按键的多次击键组合应该如何判别详细技巧程序概述
明朔科技成为智慧路灯引领者,已应用于国内30个省级区域
手术机器人面临的伦理风险及应对措施
华为全新折叠屏产品Mate V
浅析时序数据库的流计算支持
什么是弱电工程 弱电系统由哪几部分组成
哪种PC品类更值得买
智能锁的种类有哪些,它们的用途是什么
CMOS RF技术迎来新一轮发展
拉索生物:从下游产品端企业向全产业链布局,核心能力是技术