的,也就是说,函数 __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 操作完成,用户内存页面必须从页缓存中释放。在用户内存页被释放