当前位置: 首页 > 科技观察

5分钟搞懂Linux直接I-O原理

时间:2023-03-13 16:08:55 科技观察

在介绍直接I/O之前,先介绍一下直接I/O的机制产生的原因。毕竟已经有了缓冲I/O(BufferedI/O),所以一定可以看出bufferedI/O有缺陷,所以按照这个思路。什么是缓冲I/O(BufferedI/O)缓冲I/O也称为标准I/O,大多数文件系统默认的I/O操作都是缓冲I/O。在Linux的缓存I/O机制中,操作系统会将I/O数据缓存在文件系统的页缓存(pagecache)中,即先将数据复制到操作系统内核的缓冲区中,然后从操作系统内核的缓冲区复制到应用程序的地址空间。写入过程是数据流的相反方向。缓存I/O具有以下优点:缓存I/O使用操作系统内核缓冲区,在某种程度上将应用程序空间与实际物理设备分开。缓存I/O通过减少磁盘读取次数来提高性能。对于读操作:当应用程序要读取某条数据时,如果该条数据已经在pagecache中,则返回。而不是通过硬盘读取操作。如果这段数据不在pagecache中,则需要从硬盘中读取数据到pagecache中。对于写操作:应用程序会先将数据写入pagecache,是否立即写入磁盘取决于所采用的写操作机制:同步机制,数据会立即写入磁盘,直到有数据写入完成后,写入接口返回;延时机制:写接口立即返回,操作系统会周期性的将pagecache中的数据刷新到硬盘中。所以这种机制会有丢失数据的风险。想象一下,当写接口返回时,pagecache中的数据还没有刷到硬盘,刚刚断电。对于应用程序来说,认为数据已经在硬盘上了。CachedI/OWriteOperationCachedI/ODadvantages在缓存I/O机制中,以写操作为例,数据先从用户态拷贝到内核态的pagecache中,再从page中拷贝cache写入磁盘时,这些复制操作造成的CPU和内存开销非常大。对于一些特殊的应用,能够绕过内核缓冲区可以获得更好的性能,这就是直接I/O的意义。DirectI/OWriteOperationDirectI/O简介当通过直接I/O传输数据时,数据直接从用户态地址空间写入磁盘,直接跳过内核缓冲区。对于某些应用程序,例如:数据库。他们更倾向于自己的缓存机制,可以提供更好的缓冲机制来提高数据库的读写性能。直接I/O写操作如上图所示。直接I/O的设计与实现要在块设备中进行直接I/O,进程在打开文件时必须将文件的访问模式设置为O_DIRECT,相当于告诉操作系统进程使用read()orwritenext()系统调用读写文件时,采用直接I/O方式,传输的数据不经过操作系统内核缓存空间。使用directI/O读写数据时,必须注意缓冲区对齐(bufferalignment)和缓冲区的大小,分别对应read()和write()系统调用的第二个和第三个参数.这里所说的对齐是指文件系统块大小的对齐,缓冲区的大小也必须是块大小的整数倍。下面主要介绍三个函数:open()、read()和write()。Linux中对文件的访问是多种多样的,所以这三个函数针对不同的文件访问方式定义了不同的处理方式。本文主要介绍直接I/O方式相关的函数和功能。首先,看一下open()系统调用。其函数原型如下:inopen(constchar*pathname,intoflag,.../*,mode_tmode*/);当应用程序需要不经过操作系统页面缓存而直接访问文件存储时,需要在打开文件时指定O_DIRECT标识符。操作系统内核中处理open()系统调用的内核函数是sys_open(),sys_open()会调用do_sys_open()来处理主要的打开操作。它主要做了三件事:调用getname()从进程地址空间中读取文件的路径名;do_sys_open()调用get_unused_fd()从进程的文件表中找到一个空闲的文件表指针,并将对应的新文件描述符存放在局部变量fd中;函数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操作相关的代码。如果(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_tread(intfeledes,void*buff,size_tnbytes);read()函数在操作系统中的入口函数是sys_read(),其主要调用函数图如下:sys_read()|-----vfs_read()|----generic_file_read()|----generic_file_aio_read()|------------generic_file_direct_IO()函数sys_read()从中获取文件描述符该过程和文件的当前操作位置之后,会调用vfs_read()函数执行具体的操作过程,vfs_read()函数最终调用文件结构中的相关操作完成文件读取操作,即即,调用了generic_file_read()函数,其代码如下:;ssize_tret;init_sync_kiocb(&kireadocb,filp);ret=__generic_file_aio_(&kiocb,&local_iov,1,ppos);if(-EIOCBQUEUED==ret)ret=wait_on_sync_kiocb(&kiocb);returnret;}函数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相关的代码如下:mapping->host;retval=0;if(!count)gotoout;size=i_size_read(inode);if(pos0&&!is_sync_kiocb(iocb))retval=-EIOCBQUEUED;if(retval>0)*ppos=pos+retval;}file_accessed(filp);gotoout;}以上代码段主要检查文件指针的值,文件指针的大小fileand请求读取的字节数等。之后函数调用generic_file_direct_io(),并指定操作类型READ、描述符iocb、描述符iovec、当前文件指针的值、用户地址空间buffer在描述符io_vec中将相当于区域数的值作为参数传递给它。当generic_file_direct_io()函数执行时,__generic_file_aio_read()函数会继续执行完成后续操作:更新文件指针,设置访问文件i节点的时间戳;执行完所有这些操作后,函数返回。函数generic_file_direct_IO()会用到五个参数,每个参数的含义如下:rw:操作类型,可以是READ或WRITEiocb:指向kiocb描述符的指针 iov:指向iovec描述符数组的指针offset:filestructureoffsetnr_segs:iov数组中iovec的个数函数generic_file_direct_IO()代码如下:structaddress_space*mapping=file->f_mapping;ssize_tretval;size_twrite_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_tend=(offset+write_len-1)>>PAGE_CACHE_SHIFT;interr=invalidate_inode_pages2_range(mapping,offset>>PAGE_CACHE_SHIFT,end);if(err)retval=err;}}returnretval;}函数generic_file_direct_IO()对类型有一些特殊的操作WRITE处理。此外,它主要调用direct_IO方法进行直接I/O读写操作。在执行直接I/O读操作之前,先将pagecache中的相关脏数据flush回磁盘,这样可以保证从磁盘读取到最新的数据。这里的direct_IO方法最终会对应到__blockdev_direct_IO()函数。__blockdev_direct_IO()函数的代码如下所示:ssize_t__blockdev_direct_IO(intrw,structkiocb*iocb,structinode*inode,structblock_device*bdev,conststructiovec*iov,loff_toffset,unsignedlongnr_segs,get_block_tget_block,dio_iodone_tend_io,intdio_lock_type){intseg;size_tsize;unsignedlongaddr;unsignedblkbits=inode->i_blkbits;unsignedbdev_blkbits=0;unsignedblocksize_mask=(1<lock_type=dio_lock_type;if(dio_lock_type!=DIO_NO_LOCKING){if(rw==READ&&end>offset){structaddress_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);gotoout;}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);elseif(acquire_i_mutex)mutex_lock(&inode->i_mutex);returnretval;}该函数对要读写的数据进行拆分,并检查缓冲区的对齐方式存储数据时需要注意缓冲区的对齐方式。从上面的代码可以看出,缓冲区的对齐是在__blockdev_direct_IO()函数中检查的。用户地址空间中的缓冲区可以通过iov数组中的iovec描述符来识别。直接I/O读或写操作是同步进行的,即函数__blockdev_direct_IO()会等到所有I/O操作完成后才返回,所以一旦应用read()系统调用返回后,应用就可以访问包含用户地址空间中相应数据的缓冲区。但是这种方式在应用读取操作完成之前是不能关闭应用的,会导致应用的关闭很慢。直接I/O的最大优点是减少了操作系统缓冲区和用户地址空间的副本数。减少CPU开销和内存带宽。对于某些应用程序来说,这将是一个福音,将大大提高性能。直接I/O缺点直接IO并不总是成功。直接IO的开销也很高。如果应用程序不控制读写,会导致磁盘读写效率低下。磁盘的读写是通过磁头的切换来读写不同磁道上的数据。如果要写入的数据在磁盘上相距较远,会大大增加寻道时间,读写效率会大大提高。减少。总结directIO方式确实可以降低CPU占用率和内存带宽占用率,但有时也会影响性能。所以,在使用directIO之前,一定要弄清楚它的原理,只有弄清楚了才考虑使用。我只是介绍了原理。如果想深入,建议参考相关的内核文档。