在下边这一小节中,我们会重点介绍 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() 系统调用,其函数原型如下所示:

  1. int open(const char *pathname, int oflag, … /*, mode_t mode * / ) ;  
  2. |-------10--------20--------30--------40--------50--------60--------70--------80--------9|  
  3. |-------- XML error:  The previous line is longer than the max of 90 characters ---------|  

以下列出了 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. 主要调用函数关系图

  1. sys_open()  
  2.   |-----do_sys_open()  
  3.   |---------getname()  
  4.   |---------get_unused_fd()  
  5.   |---------do_filp_open()  
  6.   |--------nameidata_to_filp()  
  7.   |----------__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 相关的代码

  1. if (f->f_flags & O_DIRECT) {  
  2.  
  3.          if (!f->f_mapping->a_ops ||  
  4.  
  5.             ((!f->f_mapping->a_ops->direct_IO) &&  
  6.  
  7.             (!f->f_mapping->a_ops->get_xip_page))) {  
  8.  
  9.                   fput(f);  
  10.  
  11.                   f = ERR_PTR(-EINVAL);  
  12.  
  13.           }  
  14.  
  15. }  
  16.  

当文件打开时指定了 O_DIRECT 标识符,那么操作系统就会知道接下来对文件的读或者写操作都是要使用直接 I/O 方式的。

下边我们来看一下当进程通过 read() 系统调用读取一个已经设置了 O_DIRECT 标识符的文件的时候,系统都做了哪些处理。 函数 read() 的原型如下所示:

ssize_t read(int feledes, void *buff, size_t nbytes) ;

操作系统中处理 read() 函数的入口函数是 sys_read(),其主要的调用函数关系图如下清单 3 所示:

清单 3. 主调用函数关系图

  1. sys_read()  
  2.     |-----vfs_read()  
  3.           |----generic_file_read()  
  4.                |----generic_file_aio_read()  
  5.                     |--------- generic_file_direct_IO()  
  6.  

函数 sys_read() 从进程中获取文件描述符以及文件当前的操作位置后会调用 vfs_read() 函数去执行具体的操作过程,而 vfs_read() 函数最终是调用了 file 结构中的相关操作去完成文件的读操作,即调用了 generic_file_read() 函数,其代码如下所示:

清单 4. 函数 generic_file_read()

  1. ssize_t  
  2. generic_file_read(struct file *filp,  
  3. char __user *buf, size_t count, loff_t *ppos)  
  4. {  
  5.        struct iovec local_iov = { .iov_base = buf, .iov_len = count };  
  6.        struct kiocb kiocb;  
  7.        ssize_t ret;  
  8.        init_sync_kiocb(&kiocb, filp);  
  9.        ret = __generic_file_aio_read(&kiocb, &local_iov, 1, ppos);  
  10.        if (-EIOCBQUEUED == ret)  
  11.        ret = wait_on_sync_kiocb(&kiocb);  
  12.        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 有关的代码

  1. if (filp->f_flags & O_DIRECT) {  
  2.           loff_t pos = *ppos, size;  
  3.           struct address_space *mapping;  
  4.           struct inode *inode;  
  5.           mapping = filp->f_mapping;  
  6.           inode = mapping->host;   
  7.           retval = 0;  
  8.           if (!count)  
  9.              goto out;  
  10.           size = i_size_read(inode);  
  11.           if (pos < size) {  
  12.              retval = generic_file_direct_IO(READ, iocb,  
  13. iov, pos, nr_segs);  
  14.           if (retval > 0 && !is_sync_kiocb(iocb))  
  15.              retval = -EIOCBQUEUED;  
  16.           if (retval > 0)  
  17.              *ppos = pos + retval;  
  18.           }  
  19. file_accessed(filp);  
  20. 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()

  1. static ssize_t  
  2. generic_file_direct_IO(int rw, struct kiocb *iocb, const struct iovec *iov,loff_t offset, unsigned long nr_segs)  
  3. {  
  4.          struct file *file = iocb->ki_filp;  
  5.          struct address_space *mapping = file->f_mapping;  
  6.          ssize_t retval;  
  7.          size_t write_len = 0;  
  8.          if (rw == WRITE) {  
  9.                 write_len = iov_length(iov, nr_segs);  
  10.                 if (mapping_mapped(mapping))  
  11.                            unmap_mapping_range(mapping, offset, write_len, 0);  
  12.          }  
  13.          retval = filemap_write_and_wait(mapping);  
  14.          if (retval == 0) {  
  15.                     retval = mapping->a_ops->direct_IO(rw, iocb, iov,offset, nr_segs);  
  16.                     if (rw == WRITE && mapping->nrpages) {  
  17.                             pgoff_t end = (offset + write_len - 1)  
  18.                                         >> PAGE_CACHE_SHIFT;  
  19.                             int err = invalidate_inode_pages2_range(mapping,  
  20.                                 offset >> PAGE_CACHE_SHIFT, end);  
  21.                             if (err)  
  22.                                 retval = err;  
  23.                        }  
  24.  
  25.           }  
  26.           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()

  1. ssize_t  
  2. __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)  
  3. {  
  4.            int seg;  
  5.            size_t size;  
  6.            unsigned long addr;  
  7.            unsigned blkbits = inode->i_blkbits;  
  8.            unsigned bdev_blkbits = 0;  
  9.            unsigned blocksize_mask = (1 << blkbits) - 1;  
  10.            ssize_t retval = -EINVAL;  
  11.            loff_t end = offset;  
  12.            struct dio *dio;  
  13.            int release_i_mutex = 0;  
  14.            int acquire_i_mutex = 0;  
  15.            if (rw & WRITE)  
  16.                   rw = WRITE_SYNC;  
  17.            if (bdev)  
  18.                   bdev_blkbits = blksize_bits(bdev_hardsect_size(bdev));  
  19.            if (offset & blocksize_mask) {  
  20.                       if (bdev)  
  21.                           blkbits = bdev_blkbits;  
  22.                           blocksize_mask = (1 << blkbits) - 1;  
  23.                       if (offset & blocksize_mask)  
  24.                                  goto out;  
  25.  
  26.             }  
  27.            for (seg = 0; seg < nr_segs; seg++) {  
  28.                     addr = (unsigned long)iov[seg].iov_base;  
  29.                     size = iov[seg].iov_len;  
  30.                     end += size;  
  31.                     if ((addr & blocksize_mask) || (size & blocksize_mask)) {  
  32.                                if (bdev)  
  33.                                   blkbits = bdev_blkbits;  
  34.                                   blocksize_mask = (1 << blkbits) - 1;  
  35.                                if ((addr & blocksize_mask) || (size & blocksize_mask))  
  36.                                   goto out;  
  37.  
  38.                                  }  
  39.  
  40.                      }  
  41.             dio = kmalloc(sizeof(*dio), GFP_KERNEL);  
  42.             retval = -ENOMEM;  
  43.             if (!dio)  
  44.             goto out;  
  45.             dio->lock_type = dio_lock_type;  
  46.             if (dio_lock_type != DIO_NO_LOCKING) {  
  47.                    if (rw == READ && end > offset) {  
  48.                           struct address_space *mapping;  
  49.                           mapping = iocb->ki_filp->f_mapping;  
  50.                           if (dio_lock_type != DIO_OWN_LOCKING) {  
  51.                                  mutex_lock(&inode->i_mutex);  
  52.                                  release_i_mutex = 1;  
  53.                   }  
  54.  
  55.                   retval = filemap_write_and_wait_range(mapping, offset,end - 1);  
  56.                   if (retval) {  
  57.                         kfree(dio);  
  58.                         goto out;  
  59.  
  60.                   }  
  61.  
  62.                   if (dio_lock_type == DIO_OWN_LOCKING){        
  63.                   mutex_unlock(&inode->i_mutex);  
  64.                   acquire_i_mutex = 1;  
  65.                   }  
  66.             }  
  67.                   if (dio_lock_type == DIO_LOCKING)  
  68.                          down_read_non_owner(&inode->i_alloc_sem);  
  69.                   }  
  70.                   dio->is_async = !is_sync_kiocb(iocb) && !((rw & WRITE) &&(end > i_size_read(inode)));  
  71.                   retval = direct_io_worker(rw, iocb, inode, iov, offset,nr_segs, blkbits, get_block, end_io, dio);  
  72.                   if (rw == READ && dio_lock_type == DIO_LOCKING)  
  73.                   release_i_mutex = 0;  
  74. out:  
  75.  
  76.             if (release_i_mutex)  
  77.                       mutex_unlock(&inode->i_mutex);  
  78.             else if (acquire_i_mutex)  
  79.                       mutex_lock(&inode->i_mutex);  
  80.             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. 主调用函数关系图

  1. sys_write()  
  2.           |-----vfs_write()  
  3.                |----generic_file_write()  
  4.                     |----generic_file_aio_read()  
  5.                          |---- __generic_file_write_nolock()  
  6.                                 |-- __generic_file_aio_write_nolock  
  7.                                      |-- generic_file_direct_write()  
  8.                                          |-- generic_file_direct_IO()  
  9.  

函数 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 相关的代码

  1. if (unlikely(file->f_flags & O_DIRECT)) {  
  2.             written = generic_file_direct_write(iocb, iov,&nr_segs, pos, ppos, count, ocount);  
  3.             if (written < 0 || written == count)  
  4.                        goto out;  
  5.                        pos += written;  
  6.                        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  写操作完成之后,在页缓存中相关的脏数据就都已经失效了,磁盘与页缓存中的数据内容必须保持同步。


相关内容