新闻资讯  快讯  焦点  财经  政策  社会
互 联 网   电商  金融  数据  计算  技巧
生活百科  科技  职场  健康  法律  汽车
手机百科  知识  软件  修理  测评  微信
软件技术  应用  系统  图像  视频  经验
硬件技术  知识  技术  测评  选购  维修
网络技术  硬件  软件  设置  安全  技术
程序开发  语言  移动  数据  开源  百科
安全防护  资讯  黑客  木马  病毒  移动
站长技术  搜索  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. 直接操作设备文件描述符我们知道/dev目录下存放的是设备的文件描述符。直接往设备描述符中写入数据,将破坏整个设备,如:对于硬盘设备映射到/dev/目录下的文件描述符写入数据...【详细内容】
2019-06-14 Linux  点击:(0)  评论:(0)  加入收藏
scp是secure copy的简写,用于在Linux下进行远程拷贝文件的命令,和它类似的命令有cp,不过cp只是在本机进行拷贝不能跨服务器,而且scp传输是加密的。可能会稍微影响一下速度。当你...【详细内容】
2019-06-14 Linux  点击:(0)  评论:(0)  加入收藏
当我们使用top命令查看系统的资源使用情况时会看到load average,如下图所示,它表示系统在1,5,15分钟的平均工作负载。那么什么是负载(load)呢?它和CPU的利用率又有什么关系呢? l...【详细内容】
2019-06-14 Linux  点击:(1)  评论:(0)  加入收藏
对于初学者,只需要一台安装有Linux操作系统(比如Ubuntu)的PC即可,或者在windows下安装一个虚拟机软件(Vmware),虚拟一个Linux系统的主机,但是我建议还是安装真机,这样会迫使你必须在L...【详细内容】
2019-06-14 开发  点击:(2)  评论:(0)  加入收藏
周一早上刚到办公室,就听到同事说有一台服务器登陆不上了,我也没放在心上,继续边吃早点,边看币价是不是又跌了。 不一会运维的同事也到了,气喘吁吁的说:我们有台服务器被阿里云...【详细内容】
2019-06-14 入侵  点击:(3)  评论:(0)  加入收藏
在介绍直接 I/O 之前,先来介绍下直接I/O这种机制产生的原因。毕竟已经有了缓存I/O(Buffered I/O),那肯定能够像到缓存I/O有缺陷吧,就按照这个思路来。 什么是缓存 I/O (Buffered...【详细内容】
2019-06-13 Linux  点击:(9)  评论:(0)  加入收藏
文件 & 目录操作(16 个)ls● ls -a 查看所有文件,包含隐藏文件● ls -l 简写 ll,查看详细信息● ls -h 文件大小以易读的方式显示cd● cd ../ 返回上级目录● cd ~ 前往家...【详细内容】
2019-06-12 Linux  点击:(2)  评论:(0)  加入收藏
概述在用linux命令时候,我们可以一行执行多条命令或者有条件的执行下一条命令,今天主要介绍一下linux命令分号&&和&,|和||的用法。01“;”分号用法方式:command1 ; command2用;...【详细内容】
2019-06-11 Linux  点击:(1)  评论:(0)  加入收藏
有时文件副本相当于对硬盘空间的巨大浪费,并会在你想要更新文件时造成困扰。以下是用来识别这些文件的六个命令。-- Sandra Henry-stocker(作者)在最近的帖子中,我们看了 如何...【详细内容】
2019-06-11 Linux  点击:(1)  评论:(0)  加入收藏
对于每一个Linux学习者来说,了解Linux文件系统的目录结构,是学好Linux的至关重要的一步.,深入了解linux文件目录结构的标准和每个目录的详细功能,对于我们用好linux系统只管重要...【详细内容】
2019-06-10 Linux  点击:(13)  评论:(0)  加入收藏
Linux 爱好者们分享了他们犯下的一些最大错误。-- Jen Wike Huger(作者)终身学习是明智的 &mdash;&mdash; 它可以让你的思维敏捷,让你在就业市场上更具竞争力。但是有些技能比...【详细内容】
2019-06-10 Linux  点击:(2)  评论:(0)  加入收藏
Linux中利用netstat命令查看网络状态补充:IP地址是服务器在互联网中唯一的地址标识。假设有一台阿里云服务器,我们通过IP地址可以找到它。服务器中启动了Tomcat、FTP服务...,当...【详细内容】
2019-06-06 netstat  点击:(8)  评论:(0)  加入收藏
概述 在Linux系统当中,对于可执行文件或者说是可执行命令,通常可以存放在/bin,/sbin,/usr/bin,/usr/local/bin,usr/sbin等目录,而这些目录存放的可执行命令存在哪些区别呢?或者说某...【详细内容】
2019-06-05 Linux,bin  点击:(9)  评论:(0)  加入收藏
1. Linux export命令简介Linux export命令用于设置或显示环境变量。在shell中执行程序时,shell会提供一组环境变量。export可新增,修改或删除环境变量,供后续执行的程序使用。e...【详细内容】
2019-06-05 Linux  点击:(5)  评论:(0)  加入收藏
想要使用 Linux 命令,但又不想离开 Windows ?以下是在 Windows 中运行 Linux bash 命令的几种方法。-- Abhishek Prakash(作者)如果你正在课程中正在学习 shell 脚本,那么需要使...【详细内容】
2019-06-05 Windows,Linux  点击:(7)  评论:(0)  加入收藏
在使用linux时(虚拟机),经常会发现使用一段时间后,linux时间和我的宿主机(真实机)的时间不一致,而宿主机的时间确实是internet时间,安装linux时选择的时区也是Asia/Shanghai,那么今天...【详细内容】
2019-06-05 Linux,时间  点击:(11)  评论:(0)  加入收藏
Linux挂载详解参考篇:浅谈Linux中一切皆文件1、概念Linux系统中"一切皆文件",所有文件都放置在以根目录为树根的树形目录结构中。在Linux看来,任何硬件设备也都是文件,它们各有...【详细内容】
2019-05-21 Linux  点击:(13)  评论:(0)  加入收藏
Linux是每个后端程序员必须要掌握的系统,今天小编就给你分享一篇Linux基础知识点大全,看看你知道多少?(私信我python,获得万元python大礼包!) 一、 从认识操作系统开始1.1 操作系统...【详细内容】
2019-05-20 Linux  点击:(18)  评论:(0)  加入收藏
在这篇快速指南中,你将学到如何在 Ubuntu 桌面和其他使用 GNOME 桌面的发行版中添加应用图标。-- Abhishek Prakash(作者)一个经典的桌面操作系统在“桌面屏”上总是有图标的...【详细内容】
2019-05-20 Ubuntu,,Linux  点击:(10)  评论:(0)  加入收藏
概述今天主要分享一些常见的Linux重启,查看、重启、禁用网卡以及修改IP和操作防火墙的命令,温故而知新。以下针对redhat6.8操作系统。 重启命令1、reboot2、shutdown -r now...【详细内容】
2019-05-20 Linux  点击:(8)  评论:(0)  加入收藏
推荐资讯
相关文章
栏目更新
栏目热门
'); })();