Linux 中直接 I/O 机制的介绍

Linux 中直接 I/O 机制的介绍

黄 晓晨, 软件工程师, IBM

简介: 对于传统的操做系统来讲,普通的 I/O 操做通常会被内核缓存,这种 I/O 被称做缓存 I/O。本文所介绍的文件访问机制不通过操做系统内核的缓存,数据直接在磁盘和应用程序地址空间进行传输,因此该文件访问的机制称做为直接 I/O。Linux 中就提供了这样一种文件访问机制,对于那种将 I/O 缓存存放在用户地址空间的应用程序来讲,直接 I/O 是一种很是高效的手段。本文将基于 2.6.18 版本的内核来讨论 Linux 中直接 I/O 的技术的设计与实现。html

本文的标签:  dio, linux, 之后看, 内核, 须要好好理解的node


直接 I/O 的动机linux

在介绍直接 I/O 以前,这一小节先介绍一下为何会出现直接 I/O 这种机制,即传统的 I/O 操做存在哪些缺点。算法

什么是缓存 I/O (Buffered I/O)数据库

缓存 I/O 又被称做标准 I/O,大多数文件系统的默认 I/O 操做都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操做系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。缓存 I/O 有如下这些优势:数组

  • 缓存 I/O 使用了操做系统内核缓冲区,在必定程度上分离了应用程序空间和实际的物理设备。
  • 缓存 I/O 能够减小读盘的次数,从而提升性能。

当应用程序尝试读取某块数据的时候,若是这块数据已经存放在了页缓存中,那么这块数据就能够当即返回给应用程序,而不须要通过实际的物理读盘 操做。固然,若是数据在应用程序读取以前并未被存放在页缓存中,那么就须要先将数据从磁盘读到页缓存中去。对于写操做来讲,应用程序也会将数据先写到页缓 存中去,数据是否被当即写到磁盘上去取决于应用程序所采用的写操做机制:若是用户采用的是同步写机制( synchronous writes ), 那么数据会当即被写回到磁盘上,应用程序会一直等到数据被写完为止;若是用户采用的是延迟写机制( deferred writes ),那么应用程序就彻底不须要等到数据所有被写回到磁盘,数据只要被写到页缓存中去就能够了。在延迟写机制的状况下,操做系统会按期地将放在页缓存中的数 据刷到磁盘上。与异步写机制( asynchronous writes )不一样的是,延迟写机制在数据彻底写到磁盘上的时候不会通知应用程序,而异步写机制在数据彻底写到磁盘上的时候是会返回给应用程序的。因此延迟写机制自己 是存在数据丢失的风险的,而异步写机制则不会有这方面的担忧。缓存

缓存 I/O 的缺点网络

在缓存 I/O 机制中,DMA 方式能够将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样的话,数据在传 输过程当中须要在应用程序地址空间和页缓存之间进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的。app

对于某些特殊的应用程序来讲,避开操做系统内核缓冲区而直接在应用程序地址空间和磁盘之间传输数据会比使用操做系统内核缓冲区获取更好的性能,下边这一小节中提到的自缓存应用程序就是其中的一种。异步

自缓存应用程序( self-caching applications)

对于某些应用程序来讲,它会有它本身的数据缓存机制,好比,它会将数据缓存在应用程序地址空间,这类应用程序彻底不须要使用操做系统内核中的 高速缓冲存储器,这类应用程序就被称做是自缓存应用程序( self-caching applications )。数据库管理系统是这类应用程序的一个表明。自缓存应用程序倾向于使用数据的逻辑表达方式,而非物理表达方式;当系统内存较低的时候,自缓存应用程序会 让这种数据的逻辑缓存被换出,而并不是是磁盘上实际的数据被换出。自缓存应用程序对要操做的数据的语义了如指掌,因此它能够采用更加高效的缓存替换算法。自 缓存应用程序有可能会在多台主机之间共享一块内存,那么自缓存应用程序就须要提供一种可以有效地将用户地址空间的缓存数据置为无效的机制,从而确保应用程 序地址空间缓存数据的一致性。

对于自缓存应用程序来讲,缓存 I/O 明显不是一个好的选择。由此引出咱们这篇文章着重要介绍的 Linux 中的直接 I/O 技术。Linux 中的直接 I/O 技术很是适用于自缓存这类应用程序,该技术省略掉缓存 I/O 技术中操做系统内核缓冲区的使用,数据直接在应用程序地址空间和磁盘之间进行传输,从而使得自缓存应用程序能够省略掉复杂的系统级别的缓存结构,而执行程 序本身定义的数据读写管理,从而下降系统级别的管理对应用程序访问数据的影响。在下面一节中,咱们会着重介绍 Linux 中提供的直接 I/O 机制的设计与实现,该机制为自缓存应用程序提供了很好的支持。

回页首

Linux 2.6 中的直接 I/O 技术

Linux 2.6 中提供的几种文件访问方式

全部的 I/O 操做都是经过读文件或者写文件来完成的。在这里,咱们把全部的外围设备,包括键盘和显示器,都当作是文件系统中的文件。访问文件的方法多种多样,这里列出下边这几种 Linux 2.6 中支持的文件访问方式。

标准访问文件的方式

在 Linux 中,这种访问文件的方式是经过两个系统调用实现的:read() 和 write()。当应用程序调用 read() 系统调用读取一块数据的时候,若是该块数据已经在内存中了,那么就直接从内存中读出该数据并返回给应用程序;若是该块数据不在内存中,那么数据会被从磁盘 上读到页高缓存中去,而后再从页缓存中拷贝到用户地址空间中去。若是一个进程读取某个文件,那么其余进程就都不能够读取或者更改该文件;对于写数据操做来 说,当一个进程调用了 write() 系统调用往某个文件中写数据的时候,数据会先从用户地址空间拷贝到操做系统内核地址空间的页缓存中去,而后才被写到磁盘上。可是对于这种标准的访问文件的 方式来讲,在数据被写到页缓存中的时候,write() 系统调用就算执行完成,并不会等数据彻底写入到磁盘上。Linux 在这里采用的是咱们前边提到的延迟写机制( deferred writes )。

图 1. 以标准的方式对文件进行读写

同步访问文件的方式

同步访问文件的方式与上边这种标准的访问文件的方式比较相似,这两种方法一个很关键的区别就是:同步访问文件的时候,写数据的操做是在数据彻底被写回磁盘上才算完成的;而标准访问文件方式的写数据操做是在数据被写到页高速缓冲存储器中的时候就算执行完成了。


图 2. 数据同步写回磁盘

内存映射方式

在不少操做系统包括 Linux 中,内存区域( memory region )是能够跟一个普通的文件或者块设备文件的某一个部分关联起来的,若进程要访问内存页中某个字节的数据,操做系统就会将访问该内存区域的操做转换为相应的 访问文件的某个字节的操做。Linux 中提供了系统调用 mmap() 来实现这种文件访问方式。与标准的访问文件的方式相比,内存映射方式能够减小标准访问文件方式中 read() 系统调用所带来的数据拷贝操做,即减小数据在用户地址空间和操做系统内核地址空间之间的拷贝操做。映射一般适用于较大范围,对于相同长度的数据来说,映射 所带来的开销远远低于 CPU 拷贝所带来的开销。当大量数据须要传输的时候,采用内存映射方式去访问文件会得到比较好的效率。


图 3. 利用 mmap 代替 read

直接 I/O 方式

凡是经过直接 I/O 方式进行数据传输,数据均直接在用户地址空间的缓冲区和磁盘之间直接进行传输,彻底不须要页缓存的支持。操做系统层提供的缓存每每会使应用程序在读写数据 的时候得到更好的性能,可是对于某些特殊的应用程序,好比说数据库管理系统这类应用,他们更倾向于选择他们本身的缓存机制,由于数据库管理系统每每比操做 系统更了解数据库中存放的数据,数据库管理系统能够提供一种更加有效的缓存机制来提升数据库中数据的存取性能。


图 4. 数据传输不通过操做系统内核缓冲区

异步访问文件的方式

Linux 异步 I/O 是 Linux 2.6 中的一个标准特性,其本质思想就是进程发出数据传输请求以后,进程不会被阻塞,也不用等待任何操做完成,进程能够在数据传输的时候继续执行其余的操做。相 对于同步访问文件的方式来讲,异步访问文件的方式能够提升应用程序的效率,而且提升系统资源利用率。直接 I/O 常常会和异步访问文件的方式结合在一块儿使用。


图 5.CPU 处理其余任务和 I/O 操做能够重叠执行

在下边这一小节中,咱们会重点介绍 Linux 2.6 内核中直接 I/O 的设计与实现。

Linux 2.6 中直接 I/O 的设计与实现

在块设备或者网络设备中执行直接 I/O 彻底不用担忧实现直接 I/O 的问题,Linux 2.6 操做系统内核中高层代码已经设置和使用了直接 I/O,驱动程序级别的代码甚至不须要知道已经执行了直接 I/O;可是对于字符设备来讲,执行直接 I/O 是不可行的,Linux 2.6 提供了函数 get_user_pages() 用于实现直接 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 * / ) ;

如下列出了 Linux 2.6 内核定义的系统调用 open() 所使用的标识符宏定义:


表 1. open() 系统调用提供的标识符
标识符名 标识符描述
O_RDONLY 以只读的方式打开文件
O_WRONLY 以只写的方式打开文件
O_RDWR 以读写的方式打开文件
O_CREAT 若文件不存在,则建立该文件
O_EXCL 以独占模式打开文件;若同时设置 O_EXCL 和 O_CREATE, 那么若文件已经存在,则打开操做会失败
O_NOCTTY 若设置该描述符,则该文件不能够被当成终端处理
O_TRUNC 截断文件,若文件存在,则删除该文件
O_APPEND 若设置了该描述符,则在写文件以前,文件指针会被设置到文件的底部
O_NONBLOCK 以非阻塞的方式打开文件
O_NELAY 同 O_NELAY,若同时设置 O_NELAY 和 O_NONBLOCK,O_NONBLOCK 优先起做用
O_SYNC 该描述符会对普通文件的写操做产生影响,若设置了该描述符,则对该文件的写操做会等到数据被写到磁盘上才算结束
FASYNC 若设置该描述符,则 I/O 事件通知是经过信号发出的
O_DIRECT 该描述符提供对直接 I/O 的支持
O_LARGEFILE 该描述符提供对超过 2GB 大文件的支持
O_DIRECTORY 该描述符代表所打开的文件必须是目录,不然打开操做失败
O_NOFOLLOW 若设置该描述符,则不解析路径名尾部的符号连接

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

操做系统内核中处理 open() 系统调用的内核函数是 sys_open(),sys_open() 会调用 do_sys_open() 去处理主要的打开操做。它主要作了三件事情:首先, 它调用 getname() 从进程地址空间中读取文件的路径名;接着,do_sys_open() 调用 get_unused_fd() 从进程的文件表中找到一个空闲的文件表指针,相应的新文件描述符就存放在本地变量 fd 中;以后,函数 do_filp_open() 会根据传入的参数去执行相应的打开操做。清单 1 列出了操做系统内核中处理 open() 系统调用的一个主要函数关系图。


清单 1. 主要调用函数关系图
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 操做是否能够做用于该文件。清单 2 列出了 __dentry_open() 函数中与直接 I/O 操做相关的代码。


清单 2. 函数 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(),其主要的调用函数关系图以下清单 3 所示:


清单 3. 主调用函数关系图
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() 函数,其代码以下所示:


清单 4. 函数 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 有关的代码以下所示:


清单 5. 函数 __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() 会用到五个参数,各参数的含义以下所示:

  • rw:操做类型,能够是 READ 或者 WRITE
  • iocb:指针,指向 kiocb 描述符 
  • iov:指针,指向 iovec 描述符数组
  • offset:file 结构偏移量
  • nr_segs:iov 数组中 iovec 的个数
函数 generic_file_direct_IO() 代码以下所示:


清单 6. 函数 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 操做类型进行了一些特殊处理,这在下边介绍 write() 系统调用的时候再作说明。除此以外,它主要是调用了 direct_IO 方法去执行直接 I/O 的读或者写操做。在进行直接  I/O  读操做以前,先将页缓存中的相关脏数据刷回到磁盘上去,这样作能够确保从磁盘上读到的是最新的数据。这里的 direct_IO 方法最终会对应到 __blockdev_direct_IO() 函数上去。__blockdev_direct_IO() 函数的代码以下所示:


清单 7. 函数 __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() 系统调用返回,应用程序就能够访问用户地址空间中含有相应数据的缓冲区。可是,这种方法在应用程序读操做完成以前不能关闭应用程序,这将会致使关闭应用程 序缓慢。

 
	接下来咱们看一下 write() 系统调用中与直接 I/O 相关的处理实现过程。函数 write() 的原型以下所示:
	
	 ssize_t write(int filedes, const void * buff, size_t nbytes) ; 
	
	操做系统中处理 write() 系统调用的入口函数是 sys_write()。其主要的调用函数关系以下所示:


清单 8. 主调用函数关系图
sys_write() 
	   |-----vfs_write() 
	      |----generic_file_write() 
	            |----generic_file_aio_read() 
	                  |---- __generic_file_write_nolock() 
	                        |-- __generic_file_aio_write_nolock 
	                            |-- generic_file_direct_write() 
	                                |-- generic_file_direct_IO()

函数 sys_write() 几乎与 sys_read() 执行相同的步骤,它从进程中获取文件描述符以及文件当前的操做位置后即调用 vfs_write() 函数去执行具体的操做过程,而 vfs_write() 函数最终是调用了 file 结构中的相关操做完成文件的写操做,即调用了 generic_file_write() 函数。在函数 generic_file_write() 中, 函数 generic_file_write_nolock() 最终调用 generic_file_aio_write_nolock() 函数去检查 O_DIRECT 的设置,而且调用  generic_file_direct_write() 函数去执行直接 I/O 写操做。

函数 generic_file_aio_write_nolock() 中与直接 I/O 相关的代码以下所示:


清单 9. 函数 generic_file_aio_write_nolock() 中与直接 I/O 相关的代码
if (unlikely(file->f_flags & O_DIRECT)) { 
			 written = generic_file_direct_write(iocb, iov, 
					 &nr_segs, pos, ppos, count, ocount); 
			 if (written < 0 || written == count) 
				 goto out; 
			
			 pos += written; 
			 count -= written; 
		 }

从上边代码能够看出, generic_file_aio_write_nolock() 调用了 generic_file_direct_write() 函数去执行直接 I/O 操做;而在 generic_file_direct_write() 函数中,跟读操做过程相似,它最终也是调用了 generic_file_direct_IO() 函数去执行直接 I/O 写操做。与直接 I/O 读操做不一样的是,此次须要将操做类型 WRITE 做为参数传给函数 generic_file_direct_IO()。

前边介绍了 generic_file_direct_IO() 的主体 direct_IO 方法:__blockdev_direct_IO()。函数 generic_file_direct_IO() 对 WRITE 操做类型进行了一些额外的处理。当操做类型是 WRITE 的时候,若发现该使用直接 I/O 的文件已经与其余一个或者多个进程存在关联的内存映射,那么就调用 unmap_mapping_range() 函数去取消创建在该文件上的全部的内存映射,并将页缓存中相关的全部 dirty 位被置位的脏页面刷回到磁盘上去。对于直接  I/O  写操做来讲,这样作能够保证写到磁盘上的数据是最新的,不然,即将用直接  I/O  方式写入到磁盘上的数据极可能会由于页缓存中已经存在的脏数据而失效。在直接  I/O  写操做完成以后,在页缓存中相关的脏数据就都已经失效了,磁盘与页缓存中的数据内容必须保持同步。

如何在字符设备中执行直接 I/O

在字符设备中执行直接 I/O 多是有害的,只有在肯定了设置缓冲 I/O 的开销很是巨大的时候才建议使用直接 I/O。在 Linux 2.6 的内核中,实现直接 I/O 的关键是函数 get_user_pages() 函数。其函数原型以下所示:

int get_user_pages(struct task_struct *tsk, 
	 struct mm_struct *mm, 
	 unsigned long start, 
		 int len, 
	 int write, 
	 int force, 
	 struct page **pages, 
	 struct vm_area_struct **vmas);

该函数的参数含义以下所示:

  • tsk:指向执行映射的进程的指针;该参数的主要用途是用来告诉操做系统内核,映射页面所产生的页错误由谁来负责,该参数几乎老是 current。
  • mm:指向被映射的用户地址空间的内存管理结构的指针,该参数一般是 current->mm 。
  • start: 须要映射的用户地址空间的地址。
  • len:页内缓冲区的长度。
  • write:若是须要对所映射的页面有写权限,该参数的设置得是非零。
  • force:该参数的设置通知 get_user_pages() 函数无需考虑对指定内存页的保护,直接提供所请求的读或者写访问。
  • page:输出参数。调用成功后,该参数中包含一个描述用户空间页面的 page 结构的指针列表。
  • vmas:输出参数。若该参数非空,则该参数包含一个指向 vm_area_struct 结构的指针,该 vm_area_struct 结构包含了每个所映射的页面。

在使用 get_user_pages() 函数的时候,每每还须要配合使用如下这些函数:

void down_read(struct rw_semaphore *sem); 
	 void up_read(struct rw_semaphore *sem); 
	 void SetPageDirty(struct page *page); 
	 void page_cache_release(struct page *page);

首先,在使用 get_user_pages() 函数以前,须要先调用 down_read() 函数将 mmap 为得到用户地址空间的读取者 / 写入者信号量设置为读模式;在调用完 get_user_pages() 函数以后,再调用配对函数 up_read() 释放信号量 sem。若 get_user_pages() 调用失败,则返回错误代码;若调用成功,则返回实际被映射的页面数,该数目有可能比请求的数量少。调用成功后所映射的用户页面被锁在内存中,调用者能够通 过 page 结构的指针去访问这些用户页面。

直接 I/O 的调用者必须进行善后工做,一旦直接 I/O 操做完成,用户内存页面必须从页缓存中释放。在用户内存页被释放以前,若是这些页面中的内容改变了,那么调用者必需要通知操做系统内核,不然虚拟存储子系 统会认为这些页面是干净的,从而致使这些数据被修改了的页面在被释放以前没法被写回到永久存储中去。所以,若是改变了页中的数据,那么就必须使用 SetPageDirty() 函数标记出每一个被改变的页。对于 Linux 2.6.18.1,该宏定义在 /include/linux/page_flags.h 中。执行该操做的代码通常须要先检查页,以确保该页不在内存映射的保留区域内,由于这个区的页是不会被交换出去的,其代码以下所示:

if (!PageReserved(page)) 
	        SetPageDirty(page);

可是,因为用户空间所映射的页面一般不会被标记为保留,因此上述代码中的检查并非严格要求的。

最终,在直接 I/O 操做完成以后,无论页面是否被改变,它们都必须从页缓存中释放,不然那些页面永远都会存在在那里。函数 page_cache_release() 就是用于释放这些页的。页面被释放以后,调用者就不能再次访问它们。

关于如何在字符设备驱动程序中加入对直接 I/O 的支持,Linux 2.6.18.1 源代码中 /drivers/scsi/st.c 给出了一个完整的例子。其中,函数 sgl_map_user_pages()和 sgl_map_user_pages()几乎涵盖了本节中介绍的全部内容。

回页首

直接 I/O 技术的特色

直接 I/O 的优势

直接 I/O 最主要的优势就是经过减小操做系统内核缓冲区和应用程序地址空间的数据拷贝次数,下降了对文件读取和写入时所带来的 CPU 的使用以及内存带宽的占用。这对于某些特殊的应用程序,好比自缓存应用程序来讲,不失为一种好的选择。若是要传输的数据量很大,使用直接 I/O 的方式进行数据传输,而不须要操做系统内核地址空间拷贝数据操做的参与,这将会大大提升性能。

直接 I/O 潜在可能存在的问题

直接 I/O 并不必定总能提供使人满意的性能上的飞跃。设置直接 I/O 的开销很是大,而直接 I/O 又不能提供缓存 I/O 的优点。缓存 I/O 的读操做能够从高速缓冲存储器中获取数据,而直接 I/O 的读数据操做会形成磁盘的同步读,这会带来性能上的差别 , 而且致使进程须要较长的时间才能执行完;对于写数据操做来讲,使用直接 I/O 须要 write() 系统调用同步执行,不然应用程序将会不知道何时才可以再次使用它的 I/O 缓冲区。与直接 I/O 读操做相似的是,直接 I/O 写操做也会致使应用程序关闭缓慢。因此,应用程序使用直接 I/O 进行数据传输的时候一般会和使用异步 I/O 结合使用。

回页首

总结

Linux 中的直接 I/O 访问文件方式能够减小 CPU 的使用率以及内存带宽的占用,可是直接 I/O 有时候也会对性能产生负面影响。因此在使用直接 I/O 以前必定要对应用程序有一个很清醒的认识,只有在肯定了设置缓冲 I/O 的开销很是巨大的状况下,才考虑使用直接 I/O。直接 I/O 常常须要跟异步 I/O 结合起来使用,本文对异步 I/O 没有做详细介绍,有兴趣的读者能够参看 Linux 2.6 中相关的文档介绍。


参考资料

学习

讨论

欢迎加入 My developerWorks 中文社区
相关文章
相关标签/搜索