用 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 |