您当前的位置:首页 > 电脑百科 > 软件技术 > 操作系统 > linux

Linux中直接I/O原理

时间:2019-06-13 09:39:15  来源:  作者:

在介绍直接 I/O 之前,先来介绍下直接I/O这种机制产生的原因。毕竟已经有了缓存I/O(Buffered I/O),那肯定能够像到缓存I/O有缺陷吧,就按照这个思路来。

5分钟搞懂Linux中直接I/O原理

 

 

什么是缓存 I/O (Buffered I/O)

 

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间写的过程就是数据流反方向。缓存 I/O 有以下这些优点:

  1. 缓存 I/O 使用了操作系统内核缓冲区,在一定程度上分离了应用程序空间和实际的物理设备。
  2. 缓存 I/O 可以减少读盘的次数,从而提高性能。

对于读操作:当应用程序要去读取某块数据的时候,如果这块数据已经在页缓存中,那就返回之。而不需要经过硬盘的读取操作了。如果这块数据不在页缓存中,就需要从硬盘中读取数据到页缓存。

对于写操作:应用程序会将数据先写到页缓存中,数据是否会被立即写到磁盘,这取决于所采用的写操作机制:

  • 同步机制,数据会立即被写到磁盘中,直到数据写完,写接口才返回;
  • 延迟机制:写接口立即返回,操作系统会定期地将页缓存中的数据刷到硬盘。所以这个机制会存在丢失数据的风险。想象下写接口返回的时候,页缓存的数据还没刷到硬盘,正好断电。对于应用程序来说,认为数据已经在硬盘中。

 

5分钟搞懂Linux中直接I/O原理

缓存I/O的写操作

 

缓存 I/O 的缺点

在缓存I/O的机制中,以写操作为例,数据先从用户态拷贝到内核态中的页缓存中,然后又会从页缓存中写到磁盘中,这些拷贝操作带来的CPU以及内存的开销是非常大的。

对于某些特殊的应用程序来说,能够绕开内核缓冲区能够获取更好的性能,这就是直接I/O出现的意义。

 

5分钟搞懂Linux中直接I/O原理

直接I/O写操作

 

 

直接I/O 介绍

凡是通过直接I/O方式进行数据传输,数据直接从用户态地址空间写入到磁盘中,直接跳过内核缓冲区。对于一些应用程序,例如:数据库。他们更倾向于自己的缓存机制,这样可以提供更好的缓冲机制提高数据库的读写性能。直接I/O写操作如上图所示。

直接I/O 设计与实现

要在块设备中执行直接 I/O,进程必须在打开文件的时候设置对文件的访问模式为 O_DIRECT,这样就等于告诉操作系统进程在接下来使用 read() 或者 write() 系统调用去读写文件的时候使用的是直接 I/O 方式,所传输的数据均不经过操作系统内核缓存空间。使用直接 I/O 读写数据必须要注意缓冲区对齐( buffer alignment )以及缓冲区的大小的问题,即对应 read() 以及 write() 系统调用的第二个和第三个参数。这里边说的对齐指的是文件系统块大小的对齐,缓冲区的大小也必须是该块大小的整数倍。

下面主要介绍三个函数:open(),read() 以及 write()。Linux 中访问文件具有多样性,所以这三个函数对于处理不同的文件访问方式定义了不同的处理方法,本文主要介绍其与直接 I/O 方式相关的函数与功能.首先,先来看 open() 系统调用,其函数原型如下所示:

int open(const char *pathname, int oflag, … /*, mode_t mode * / ) ;

 

当应用程序需要直接访问文件而不经过操作系统页高速缓冲存储器的时候,它打开文件的时候需要指定 O_DIRECT 标识符。

操作系统内核中处理 open() 系统调用的内核函数是 sys_open(),sys_open() 会调用 do_sys_open() 去处理主要的打开操作。它主要做了三件事情:

  1. 调用 getname() 从进程地址空间中读取文件的路径名;
  2. do_sys_open() 调用 get_unused_fd() 从进程的文件表中找到一个空闲的文件表指针,相应的新文件描述符就存放在本地变量 fd 中;
  3. 函数 do_filp_open() 会根据传入的参数去执行相应的打开操作。

下面列出了操作系统内核中处理 open() 系统调用的一个主要函数关系图。

sys_open() 
 |-----do_sys_open() 
 |---------getname() 
 |---------get_unused_fd() 
 |---------do_filp_open() 
 |--------nameidata_to_filp() 
 |----------__dentry_open()

函数 do_flip_open() 在执行的过程中会调用函数 nameidata_to_filp(),而 nameidata_to_filp() 最终会调用 __dentry_open() 函数,若进程指定了 O_DIRECT 标识符,则该函数会检查直接 I./O 操作是否可以作用于该文件。下面列出了 __dentry_open() 函数中与直接 I/O 操作相关的代码。

if (f->f_flags & O_DIRECT) { 
 if (!f->f_mApping->a_ops || 
 ((!f->f_mapping->a_ops->direct_IO) && 
 (!f->f_mapping->a_ops->get_xip_page))) { 
 fput(f); 
 f = ERR_PTR(-EINVAL); 
 } 
}

当文件打开时指定了 O_DIRECT 标识符,那么操作系统就会知道接下来对文件的读或者写操作都是要使用直接 I/O 方式的。

下边我们来看一下当进程通过 read() 系统调用读取一个已经设置了 O_DIRECT 标识符的文件的时候,系统都做了哪些处理。 函数 read() 的原型如下所示:

ssize_t read(int feledes, void *buff, size_t nbytes) ;

操作系统中处理 read() 函数的入口函数是 sys_read(),其主要的调用函数关系图如下:

sys_read() 
 |-----vfs_read() 
 |----generic_file_read() 
 |----generic_file_aio_read() 
 |--------- generic_file_direct_IO()
​

函数 sys_read() 从进程中获取文件描述符以及文件当前的操作位置后会调用 vfs_read() 函数去执行具体的操作过程,而 vfs_read() 函数最终是调用了 file 结构中的相关操作去完成文件的读操作,即调用了 generic_file_read() 函数,其代码如下所示:

ssize_t 
generic_file_read(struct file *filp, 
char __user *buf, size_t count, loff_t *ppos) 
{ 
 struct iovec local_iov = { .iov_base = buf, .iov_len = count }; 
 struct kiocb kiocb; 
 ssize_t ret; 
 
 init_sync_kiocb(&kiocb, filp); 
 ret = __generic_file_aio_read(&kiocb, &local_iov, 1, ppos); 
 if (-EIOCBQUEUED == ret) 
 ret = wait_on_sync_kiocb(&kiocb); 
 return ret; 
}

函数 generic_file_read() 初始化了 iovec 以及 kiocb 描述符。描述符 iovec 主要是用于存放两个内容:用来接收所读取数据的用户地址空间缓冲区的地址和缓冲区的大小;描述符 kiocb 用来跟踪 I/O 操作的完成状态。之后,函数 generic_file_read() 凋用函数 __generic_file_aio_read()。该函数检查 iovec 中描述的用户地址空间缓冲区是否可用,接着检查访问模式,若访问模式描述符设置了 O_DIRECT,则执行与直接 I/O 相关的代码。函数 __generic_file_aio_read() 中与直接 I/O 有关的代码如下所示:

if (filp->f_flags & O_DIRECT) { 
 loff_t pos = *ppos, size; 
 struct address_space *mapping; 
 struct inode *inode; 
 
 mapping = filp->f_mapping; 
 inode = mapping->host; 
 retval = 0; 
 if (!count) 
 goto out; 
 size = i_size_read(inode); 
 if (pos < size) { 
 retval = generic_file_direct_IO(READ, iocb, 
 iov, pos, nr_segs); 
 if (retval > 0 && !is_sync_kiocb(iocb)) 
 retval = -EIOCBQUEUED; 
 if (retval > 0) 
 *ppos = pos + retval; 
 } 
 file_accessed(filp); 
 goto out; 
}

上边的代码段主要是检查了文件指针的值,文件的大小以及所请求读取的字节数目等,之后,该函数调用 generic_file_direct_io(),并将操作类型 READ,描述符 iocb,描述符 iovec,当前文件指针的值以及在描述符 io_vec 中指定的用户地址空间缓冲区的个数等值作为参数传给它。当 generic_file_direct_io() 函数执行完成,函数 __generic_file_aio_read()会继续执行去完成后续操作:更新文件指针,设置访问文件 i 节点的时间戳;这些操作全部执行完成以后,函数返回。 函数 generic_file_direct_IO() 会用到五个参数,各参数的含义如下所示:

  1. rw:操作类型,可以是 READ 或者 WRITE
  2. iocb:指针,指向 kiocb 描述符 
  3. iov:指针,指向 iovec 描述符数组
  4. offset:file 结构偏移量
  5. nr_segs:iov 数组中 iovec 的个数

函数 generic_file_direct_IO() 代码如下所示:

static ssize_t 
generic_file_direct_IO(int rw, struct kiocb *iocb, const struct iovec *iov, 
 loff_t offset, unsigned long nr_segs) 
{ 
 struct file *file = iocb->ki_filp; 
 struct address_space *mapping = file->f_mapping; 
 ssize_t retval; 
 size_t write_len = 0; 
 
 if (rw == WRITE) { 
 write_len = iov_length(iov, nr_segs); 
 if (mapping_mapped(mapping)) 
 unmap_mapping_range(mapping, offset, write_len, 0); 
 } 
 
 retval = filemap_write_and_wait(mapping); 
 if (retval == 0) { 
 retval = mapping->a_ops->direct_IO(rw, iocb, iov, 
 offset, nr_segs); 
 if (rw == WRITE && mapping->nrpages) { 
 pgoff_t end = (offset + write_len - 1) 
 >> PAGE_CACHE_SHIFT; 
 int err = invalidate_inode_pages2_range(mapping, 
 offset >> PAGE_CACHE_SHIFT, end); 
 if (err) 
 retval = err; 
 } 
 } 
 return retval; 
}

函数 generic_file_direct_IO() 对 WRITE 操作类型进行了一些特殊处理。除此之外,它主要是调用了 direct_IO 方法去执行直接 I/O 的读或者写操作。在进行直接 I/O 读操作之前,先将页缓存中的相关脏数据刷回到磁盘上去,这样做可以确保从磁盘上读到的是最新的数据。这里的 direct_IO 方法最终会对应到 __blockdev_direct_IO() 函数上去。__blockdev_direct_IO() 函数的代码如下所示:

ssize_t 
__blockdev_direct_IO(int rw, struct kiocb *iocb, struct inode *inode, 
 struct block_device *bdev, const struct iovec *iov, loff_t offset, 
 unsigned long nr_segs, get_block_t get_block, dio_iodone_t end_io, 
 int dio_lock_type) 
{ 
 int seg; 
 size_t size; 
 unsigned long addr; 
 unsigned blkbits = inode->i_blkbits; 
 unsigned bdev_blkbits = 0; 
 unsigned blocksize_mask = (1 << blkbits) - 1; 
 ssize_t retval = -EINVAL; 
 loff_t end = offset; 
 struct dio *dio; 
 int release_i_mutex = 0; 
 int acquire_i_mutex = 0; 
 
 if (rw & WRITE) 
 rw = WRITE_SYNC; 
 
 if (bdev) 
 bdev_blkbits = blksize_bits(bdev_hardsect_size(bdev)); 
 
 if (offset & blocksize_mask) { 
 if (bdev) 
 blkbits = bdev_blkbits; 
 blocksize_mask = (1 << blkbits) - 1; 
 if (offset & blocksize_mask) 
 goto out; 
 } 
 
 for (seg = 0; seg < nr_segs; seg++) { 
 addr = (unsigned long)iov[seg].iov_base; 
 size = iov[seg].iov_len; 
 end += size; 
 if ((addr & blocksize_mask) || (size & blocksize_mask)) { 
 if (bdev) 
 blkbits = bdev_blkbits; 
 blocksize_mask = (1 << blkbits) - 1; 
 if ((addr & blocksize_mask) || (size & blocksize_mask)) 
 goto out; 
 } 
 } 
 
 dio = kmalloc(sizeof(*dio), GFP_KERNEL); 
 retval = -ENOMEM; 
 if (!dio) 
 goto out; 
 dio->lock_type = dio_lock_type; 
 if (dio_lock_type != DIO_NO_LOCKING) { 
 if (rw == READ && end > offset) { 
 struct address_space *mapping; 
 
 mapping = iocb->ki_filp->f_mapping; 
 if (dio_lock_type != DIO_OWN_LOCKING) { 
 mutex_lock(&inode->i_mutex); 
 release_i_mutex = 1; 
 } 
 
 retval = filemap_write_and_wait_range(mapping, offset, 
 end - 1); 
 if (retval) { 
 kfree(dio); 
 goto out; 
 } 
 
 if (dio_lock_type == DIO_OWN_LOCKING) { 
 mutex_unlock(&inode->i_mutex); 
 acquire_i_mutex = 1; 
 } 
 } 
 
 if (dio_lock_type == DIO_LOCKING) 
 down_read_non_owner(&inode->i_alloc_sem); 
 } 
 
 dio->is_async = !is_sync_kiocb(iocb) && !((rw & WRITE) && 
 (end > i_size_read(inode))); 
 
 retval = direct_io_worker(rw, iocb, inode, iov, offset, 
 nr_segs, blkbits, get_block, end_io, dio); 
 
 if (rw == READ && dio_lock_type == DIO_LOCKING) 
 release_i_mutex = 0; 
 
out: 
 if (release_i_mutex) 
 mutex_unlock(&inode->i_mutex); 
 else if (acquire_i_mutex) 
 mutex_lock(&inode->i_mutex); 
 return retval; 
}

该函数将要读或者要写的数据进行拆分,并检查缓冲区对齐的情况。本文在前边介绍 open() 函数的时候指出,使用直接 I/O 读写数据的时候必须要注意缓冲区对齐的问题,从上边的代码可以看出,缓冲区对齐的检查是在 __blockdev_direct_IO() 函数里边进行的。用户地址空间的缓冲区可以通过 iov 数组中的 iovec 描述符确定。直接 I/O 的读操作或者写操作都是同步进行的,也就是说,函数 __blockdev_direct_IO() 会一直等到所有的 I/O 操作都结束才会返回,因此,一旦应用程序 read() 系统调用返回,应用程序就可以访问用户地址空间中含有相应数据的缓冲区。但是,这种方法在应用程序读操作完成之前不能关闭应用程序,这将会导致关闭应用程序缓慢。

直接I/O 优点

最大的优点就是减少操作系统缓冲区和用户地址空间的拷贝次数。降低了CPU的开销,和内存带宽。对于某些应用程序来说简直是福音,将会大大提高性能。

直接I/O 缺点

直接IO并不总能让人如意。直接IO的开销也很大,应用程序没有控制好读写,将会导致磁盘读写的效率低下。磁盘的读写是通过磁头的切换到不同的磁道上读取和写入数据,如果需要写入数据在磁盘位置相隔比较远,就会导致寻道的时间大大增加,写入读取的效率大大降低。

总结

直接IO方式确实能够减少CPU的使用率以及内存带宽的占用,但是有时候也会造成性能的影响。所以在使用直接IO之前一定要清楚它的原理,只有在各项都清晰的情况下,才考虑使用。本人只是介绍了原理,如想深入,建议参考内核相关文档。
 



Tags:Linux   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
一、背景介绍作为一名渗透测试工作人员(或者小白),在我们的日常工作或者学习中,我们不可能时时刻刻将自己的个人电脑(安装好Kali Linux的个人主机)带在身边,当我们没有带自己的个人...【详细内容】
2021-12-27  Tags: Linux  点击:(3)  评论:(0)  加入收藏
作用显示文件或目录所占用的磁盘空间使用命令格式du [option] 文件/目录命令功能显示文件或目录所占用的磁盘空间一些写法的区别du -sh xxx 显示总目录的大小,但是不会列出...【详细内容】
2021-12-23  Tags: Linux  点击:(12)  评论:(0)  加入收藏
什么是linux内核linux就像是一个哲学的最佳实践。如果非要对它评价,我真的不知道该怎么赞叹,我只能自豪地说着:“linux的美丽简直让人沉醉。”我只能说是我处在linux学习的修炼...【详细内容】
2021-12-23  Tags: Linux  点击:(15)  评论:(0)  加入收藏
本文将比较 Linux 中 service 和 systemctl 命令,先分别简单介绍这两个命令的基础用法,然后进行比较。从 CentOS 7.x 开始,CentOS 开始使用 systemd 服务来代替 service服务(dae...【详细内容】
2021-12-23  Tags: Linux  点击:(14)  评论:(0)  加入收藏
以下是服务器安全加固的步骤,本文以腾讯云的CentOS7.7版本为例来介绍,如果你使用的是秘钥登录服务器1-5步骤可以跳过。1、设置复杂密码服务器设置大写、小写、特殊字符、数字...【详细内容】
2021-12-20  Tags: Linux  点击:(7)  评论:(0)  加入收藏
mv是move的缩写,可以用来移动文件或者重命名文件名,经常用来备份文件或者目录。命令格式mv [选项] 源文件或者目录 目标文件或者目录命令功能mv命令中第二个参数类型的不同(...【详细内容】
2021-12-17  Tags: Linux  点击:(23)  评论:(0)  加入收藏
大数据技术AI Flink/Spark/Hadoop/数仓,数据分析、面试,源码解读等干货学习资料 98篇原创内容 -->公众号 Linux sed 命令是利用脚本来处理文本文件。sed 可依照脚本的指令来处...【详细内容】
2021-12-17  Tags: Linux  点击:(22)  评论:(0)  加入收藏
概述我们知道SQL Server是微软公司推出的重要的数据库产品,通常情况下只支持部署在windows平台上。不过令人感到兴奋的是,从SQL Server 2017开始支持 linux系统。此 SQL Serve...【详细内容】
2021-12-17  Tags: Linux  点击:(13)  评论:(0)  加入收藏
Node是个啥?  写个东西还是尽量面面俱到吧,所以有关基本概念的东西我也从网上选择性地拿了下来,有些地方针对自己的理解有所改动,对这些概念性的东西有过了解的可选择跳过这段...【详细内容】
2021-12-15  Tags: Linux  点击:(25)  评论:(0)  加入收藏
简介Netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。输出信息含义执行net...【详细内容】
2021-12-13  Tags: Linux  点击:(28)  评论:(0)  加入收藏
▌简易百科推荐
作用显示文件或目录所占用的磁盘空间使用命令格式du [option] 文件/目录命令功能显示文件或目录所占用的磁盘空间一些写法的区别du -sh xxx 显示总目录的大小,但是不会列出...【详细内容】
2021-12-23  mitsuhide1992    Tags:du命令   点击:(12)  评论:(0)  加入收藏
什么是linux内核linux就像是一个哲学的最佳实践。如果非要对它评价,我真的不知道该怎么赞叹,我只能自豪地说着:“linux的美丽简直让人沉醉。”我只能说是我处在linux学习的修炼...【详细内容】
2021-12-23  linux上的码农    Tags:linux内核   点击:(15)  评论:(0)  加入收藏
本文将比较 Linux 中 service 和 systemctl 命令,先分别简单介绍这两个命令的基础用法,然后进行比较。从 CentOS 7.x 开始,CentOS 开始使用 systemd 服务来代替 service服务(dae...【详细内容】
2021-12-23  软件架构    Tags:systemctl   点击:(14)  评论:(0)  加入收藏
mv是move的缩写,可以用来移动文件或者重命名文件名,经常用来备份文件或者目录。命令格式mv [选项] 源文件或者目录 目标文件或者目录命令功能mv命令中第二个参数类型的不同(...【详细内容】
2021-12-17  入门小站    Tags:mv命令   点击:(23)  评论:(0)  加入收藏
大数据技术AI Flink/Spark/Hadoop/数仓,数据分析、面试,源码解读等干货学习资料 98篇原创内容 -->公众号 Linux sed 命令是利用脚本来处理文本文件。sed 可依照脚本的指令来处...【详细内容】
2021-12-17  仙风道骨的宝石骑士    Tags:sed命令   点击:(22)  评论:(0)  加入收藏
Node是个啥?  写个东西还是尽量面面俱到吧,所以有关基本概念的东西我也从网上选择性地拿了下来,有些地方针对自己的理解有所改动,对这些概念性的东西有过了解的可选择跳过这段...【详细内容】
2021-12-15  linux上的码农    Tags:node   点击:(25)  评论:(0)  加入收藏
难道只有我一个人觉得Ubuntu的unity桌面非常好用吗?最近把台式机上面的Ubuntu 16.04格式化了,装了黑苹果用了一周,不得不说,MacOS确实很精美,软件生态比Linux丰富很多,比Windows简...【详细内容】
2021-12-14  地球末日村    Tags:ubuntu   点击:(41)  评论:(0)  加入收藏
简介Netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。输出信息含义执行net...【详细内容】
2021-12-13  窥镜天    Tags:Linux netstat   点击:(28)  评论:(0)  加入收藏
对于较多数量的文件描述符的监听无论是select还是poll系统调用都显得捉襟见肘,poll每次都需要将所有的文件描述符复制到内核,内核本身不会对这些文件描述符加以保存,这样的设计...【详细内容】
2021-12-13  深度Linux    Tags:Linux   点击:(19)  评论:(0)  加入收藏
今天,我们来了解下 Linux 系统的革命性通用执行引擎-eBPF,之所以聊着玩意,因为它确实牛逼,作为一项底层技术,在现在的云原生生态领域中起着举足轻重的作用。截至目前,业界使用范...【详细内容】
2021-12-10  架构驿站    Tags:eBPF   点击:(29)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条