新闻资讯  快讯  焦点  财经  政策  社会
互 联 网   电商  金融  数据  计算  技巧
生活百科  科技  职场  健康  法律  汽车
手机百科  知识  软件  修理  测评  微信
软件技术  应用  系统  图像  视频  经验
硬件技术  知识  技术  测评  选购  维修
网络技术  硬件  软件  设置  安全  技术
程序开发  语言  移动  数据  开源  百科
安全防护  资讯  黑客  木马  病毒  移动
站长技术  搜索  SEO  推广  媒体  移动
财经百科  股票  知识  理财  财务  金融
教育考试  育儿  小学  高考  考研  留学
您当前的位置:首页 > IT百科 > 软件技术 > 操作系统 > 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   点击:()  评论:()
声明:本站部分内容来自互联网,内容观点仅代表作者本人,如有任何版权侵犯请与我们联系,我们将立即删除。
▌相关评论
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表
▌相关推荐
1、反斜杠的使用反斜线可以将紧随其后的单个字符视为字面意义上的字符,如*在 Shell中代表任意字符,在查找时经常会使用*来查找多个匹配的文件,但是有时我们需要找的就是*字...【详细内容】
2019-11-27   Linux  点击:(0)  评论:(0)  加入收藏
1. 前言本文主要讲解如何使用Linux命令行测试网站连接速度。网站速度可以对用户体验有很大的影响,如果你是一个web开发人员,你当然希望开发一个能让用户迅速打开的网站。怎...【详细内容】
2019-11-27   Linux  点击:(0)  评论:(0)  加入收藏
第一步:创建脚本#!/bin/bash# func:自动监控tomcat脚本并且执行重启操作# 获取tomcat进程ID(其中[grep -w &#39;tomcat&#39;]代码中的tomcat需要替换为你的tomcat文件夹名)Tomc...【详细内容】
2019-11-27   Linux  点击:(0)  评论:(0)  加入收藏
0.新建操作:mkdir abc #新建一个文件夹touch abc.sh #新建一个文件1.查看操作查看目录:ll #显示目录文件详细信息du -h 文件/目录 #查看大小pwd #显示路径查看文件内容:cat|hea...【详细内容】
2019-11-27   Linux  点击:(0)  评论:(0)  加入收藏
本文记录的是在CentOS 7下安装与配置jdk-8u162的过程。一、下载jdk-8u162版本链接地址:官方地址二、上传jdk到centos下三、检查当前linux系统上是否有jdk,linux命令:rpm -qa |...【详细内容】
2019-11-27   Linux  点击:(0)  评论:(0)  加入收藏
中断处理 - 上半部(硬中断)由于 APIC中断控制器 有点小复杂,所以本文主要通过 8259A中断控制器 来介绍Linux对中断的处理过程。中断处理相关结构前面说过,8259A中断控制器 由两...【详细内容】
2019-11-27   Linux  点击:(0)  评论:(0)  加入收藏
我们都知道没有插电的计算机就是一堆废铁,那么插了电的计算机其实也就是带了电的废铁,哈哈,没有软件的运作,计算机的功能也无从发挥。就好像行尸走肉,所以我们要了解一下软件是什...【详细内容】
2019-11-27   Linux  点击:(0)  评论:(0)  加入收藏
作为一个Java开发人员,有些常用的Linux命令必须掌握。即使平时开发过程中不使用Linux(Unix)或者mac系统,也需要熟练掌握Linux命令。因为很多服务器上都是Linux系统。所以,要和服...【详细内容】
2019-11-26   Linux  点击:(4)  评论:(0)  加入收藏
1、把/home目录下面的mydata目录压缩为mydata.zipzip -r mydata.zip mydata #压缩mydata目录2、把/home目录下面的mydata.zip解压到mydatabak目录里面unzip mydata.zip -d m...【详细内容】
2019-11-26   Linux  点击:(2)  评论:(0)  加入收藏
Linux 的优秀之处自然不必多说。如果将操作系统比作一辆汽车,那 Linux 就是一辆性能出色的多功能越野车,上山下海飞天无所不能。如果你拥有了它,一定不会只满足于驾驶它上下班,...【详细内容】
2019-11-26   Linux  点击:(2)  评论:(0)  加入收藏
硬盘分区并格式化好以后,就能在Linux系统上使用了,使用之前需要挂载到对应的目录上面去。...【详细内容】
2019-11-26   Linux  点击:(4)  评论:(0)  加入收藏
如果这篇文章对您有帮助,请关注并点赞,感谢您的支持,如果还有其他问题,请私信给我今天来介绍linux下如何添加账户及修改密码的命令首先来介绍如何添加账户命令格式 useradd...【详细内容】
2019-11-26   Linux  点击:(1)  评论:(0)  加入收藏
1. 基本用法最简单的用法就是不带参数,仅输入 ssh 再加上主机地址,比如:ssh 192.168.0.112这种形式登陆主机,会默认使用当前用户进行登录。第一次连接的时候,SSH 会确认目标主机...【详细内容】
2019-11-25   Linux  点击:(3)  评论:(0)  加入收藏
最近在用freeswitch软交换进行语音通话,通话的时候需要调用音频文件进行播放,当并发量上来的时候,freeswitch监听服务的端口会不断的down掉,查看日志发现报了一个错如下: 这个问...【详细内容】
2019-11-25   Linux  点击:(1)  评论:(0)  加入收藏
概述运维工作中,常常需要下载文件,因为网站下载速度限制或者网络等原因导致下载让人无法忍受,所以今天推荐这款多线程下载工具--axel,下载文件时可以替代curl、wget。Axel tries...【详细内容】
2019-11-25   Linux  点击:(2)  评论:(0)  加入收藏
每个人喜欢的版本因人而异。可以给你推几荐个常用的版本,优缺点对比,然后你根据自己的需求选择:linux常见发行版本(一):deepin 这是中国的操作系统中排名最高的一个,基于debian,以易...【详细内容】
2019-11-22   Linux  点击:(8)  评论:(0)  加入收藏
基本环境准备jdk安装配置。安装目录例如:/usr/java/jdk1.8.0_05tomcat下载放到约定目录。例如:/usr/local/dmstomcat tomcat做成系统服务把tomcat做成系统服务,就可以使用servi...【详细内容】
2019-11-20   Linux  点击:(6)  评论:(0)  加入收藏
在工作中经常有需要将linux虚拟机配置成固定的IP地址。首先要关闭VMware的DHCP编辑->虚拟机网络配置 选择VMnet8,取消勾选"使用本地DHCP服务将IP地址分配给虚拟机"选项。 点...【详细内容】
2019-11-20   Linux  点击:(7)  评论:(0)  加入收藏
应朋友们要求,介绍上Linux系统下的实时监控平台,在上次提到了glances,它提供了较多的监控指标,那如果我们要看历史数据呢?某一时间段的回放呢?显然glances是做不到的。因此,实时监...【详细内容】
2019-11-20   Linux  点击:(6)  评论:(0)  加入收藏
想玩linux,电脑不想装双系统,性能不够跑不了虚拟机,树莓派买不起,怎么办?安卓手机啊本文就以Nexus 5X(PureNexus 7.1.2)为例,介绍如何在手机上跑Linux,这是一种我认为最简便 最好看的...【详细内容】
2019-11-20   Linux  点击:(10)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条