一个IO的传奇一生Word格式文档下载.docx
《一个IO的传奇一生Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《一个IO的传奇一生Word格式文档下载.docx(22页珍藏版)》请在冰豆网上搜索。
如果你想保存你的Word资料至本地硬盘,你就会触发一个文件系统写操作。
如果你想将一个文件从本地电脑拷贝到U盘时,你会触发一次文件系统的读写过程。
大家知道,为了简化用户对文件的管理,操作系统提供了文件系统对数据资料进行了管理,文件系统是操作系统最为重要的组成部分。
一旦你想往文件系统写入数据时,一个新的IO请求就会在用户态诞生,但是,其绝大部分的人生旅程都会在内核空间。
对于不同的应用类型,IO请求的属性会大相径庭。
除了文件本身应该具备的基本属性(读写权限等)之外,我们还需要考虑文件的访问模式:
异步IO还是同步IO?
对文件系统的Cache是如何控制的?
应用程序和内核程序之间是如何交互的?
所以,在创建一个IO时,我们需要考虑很多这样的因素。
我们知道,当我们需要进行文件操作的时候,5个API函数是必不可少的。
Create,Open,Close,Write和Read函数实现了对文件的所有操作。
Create函数用来打开一个文件,如果该文件不存在,那么需要在磁盘上创建该文件。
Open函数用于打开一个指定的文件。
如果在Open函数中指定O_CREATE标记,那么Open函数同样可以实现Create函数的功能。
Close函数用于释放文件句柄。
Write和Read函数用于实现文件的读写过程。
举个例子,如果用户需要对一个文件进行写操作,那么首先调用Open函数打开想要操作的文件,函数完成之后获取所要操作文件的句柄;
然后调用Write函数将数据写入文件;
最后采用Close函数释放文件句柄,结束文件写入过程。
上述过程大家应该都非常的熟悉,在上述过程中,整个系统到底发生了哪些操作呢?
打开文件
众所周知,用户态的API函数通过系统调用陷入内核。
对于Open函数对应了sys_open函数例程。
该函数的主要职责是查找指定文件的inode,然后在内核中生成对应的文件对象。
在Linux中,Sys_open函数调用do_sys_open完成具体功能。
在do_sys_open中通过do_filp_open函数完成文件名解析、inode对象查找,然后创建file对象,最后执行特定文件对应的file->
open函数。
Do_filp_open过程中的核心处理函数是link_path_walk。
该函数完成了基本的文件路径的解析功能,是名字字符串解析处理实现的核心。
该函数的实现基于分级解析处理的思想。
例如,当需要解析“/dev/mapper/map0”字符串时,其首先需要判断从何处开始解析,根目录还是当前目录?
这个例子是从根目录开始解析的,那么首先获取根目录的dentry对象并开始分析后继字符串。
处理过程是以‘/’字符为界按序提取字符串。
根据规则,首先我们可以提取“dev”字符串,并且计算该字符串的Hash值,通过该Hash值查找dentry下的inodeHash表,就可以很快的找到/dev/目录下的inode对象。
Hash值的计算是比较简单的,把所有字符对应的值累加起来就可以得到一个Hash值。
根据规则,依此类推,最后解析得到”/dev/mapper/”目录的inode对象以及文件名字符串“map0”。
到这一步为止,link_path_walk函数的使命完成,最后可以通过do_last函数获取或者创建文件inode。
如果用户态程序设置了O_CREATE标记,那么系统如果找不到用户指定的inode,do_last会创建一个新的文件inode,并且把这些信息以元数据的形式写入磁盘。
当指定文件的inode找到之后,另一件很重要的事情就是初始化file文件对象。
初始化文件对象通过__dentry_open函数来实现。
文件对象通过inode参数进行初始化,并且把inode的操作方法函数集告诉给file对象。
一旦file对象初始化成功之后,调用文件对象的open函数执行进一步的初始化工作。
通过上述分析,整个过程看似比较复杂,涉及到dentry,inode以及file对象。
其实这个模型还是很简单的。
Dentry用来描述文件目录,在磁盘上会采用元数据的方式存储在一个block中,文件目录本身在Linux中也是一个文件。
Inode描述一个具体的文件,也通过元数据的方式在磁盘上保存。
如果对一个文件系统从根目录开始往下看,整个文件系统是一颗庞大的inode树:
在打开一个文件的过程中,文件系统所要做的事情就是找到指定文件的inode,所以在这个过程中会有磁盘元数据读操作。
一旦文件所属的inode被找到,那么需要在内存中初始化一个描述被打开文件的对象,这个对象就是file。
所以,dentry,inode之类的信息在磁盘上是永久存储的,file对象是在内存中是临时存在的,它会随着文件的创建而生成,随着文件的关闭而消亡。
在Linux系统中文件类型是多种多样的,一个USB设备也是一个文件,一个普通的Word文档也是一个文件,一个RAID设备也是一个文件。
虽然他们在系统中都是文件,但是,他们的操作方式是截然不同的。
USB设备可能需要采用字符设备的方式和设备驱动交互;
RAID设备可能需要采用块设备的方式和设备驱动进行交互;
普通Word文件需要通过cache机制进行性能优化。
所以,虽然都是文件,但是,文件表面下的这些设备是不相同的,需要采用的操作方法显然是截然不同的。
作为一个通用的文件系统,如何封装不同的底层设备是需要考虑的问题。
在Linux中,为了达到这个目的,推出了VFS概念。
在VFS层次对用户接口进行了统一封装,并且实现了通用的文件操作功能。
例如打开一个文件和关闭一个文件的操作都是相同的。
在VFS下面会有针对不同需求的具体文件系统,例如针对Word文档可以采用EXT3文件系统进行操作,对于磁盘设备可以采用bdev块设备文件系统进行操作。
在打开一个文件,对文件对象file进行初始化的时候,会将具体的文件系统操作方法关联到file->
f_op和file->
f_mapping对象。
在后面的读写过程中,我们将会看到针对不同的文件类型,会采用不同的f_op和f_mapping方法。
读写文件
当一个文件被打开之后,用户态程序就可以得到一个文件对象,即文件句柄。
一旦获取文件句柄之后就可以对其进行读写了。
用户态的读写函数write对应内核空间的sys_write例程。
通过系统调用可以陷入sys_write。
Sys_write函数在VFS层做的工作及其有限,其会调用文件对象中指定的操作函数file->
f_op->
write。
对于不同的文件系统,file->
write指向的操作函数是不同的。
对于EXT3文件系统而言,在文件inode初始化的时候会指定ext3_file_operations操作方法集。
该方法集说明了EXT3文件系统的读写操作方法,说明如下:
如果文件设备是一个USB设备,并且采用的是字符设备的接口,那么在初始化文件inode的时候会调用init_special_inode初始化这些特殊的设备文件。
对于字符设备会采用默认的def_chr_fops方法集,对于块设备会采用def_blk_fops方法集。
不同的文件类型会调用各自的方法集。
下面章节会对EXT3文件写和块设备文件写进行详细阐述。
由于字符设备类型比较简单,在此进行简单说明。
Def_chr_fops方法集其实就定义了open方法,其它的方法都没有定义。
其实字符设备的操作方法都需要字符设备驱动程序自己定义,每个设备驱动程序都需要定义自己的write、read、open和close方法,这些方法保存在字符设备对象中。
当用户调用文件系统接口open函数打开指定字符设备文件时,VFS会通过上述讲述的sys_open函数找到设备文件inode中保存的def_chr_fops方法,并且执行该方法中的open函数(chrdev_open),chrdev_open函数完成的一个重要功能就是将文件对象file中采用的方法替换成驱动程序设定的设备操作方法。
完成这个偷梁换柱的代码是:
一旦这个过程完成,后继用户程序通过文件系统的write方法都将会调用字符设备驱动程序设定的write方法。
即对于字符设备文件而言,在VFS的sys_write函数将直接调用字符设备驱动程序的write方法。
所以,对于字符设备驱动程序而言,整个过程很简单,用户态程序可以直接通过系统调用执行字符设备驱动程序的代码。
而对于块设备和普通文件,这个过程将会复杂的多。
在用户程序发起写请求的时候,通常会考虑如下三个问题:
第一个问题是用户态数据如何高效传递给内核?
第二个问题是采用同步或者异步的方式执行IO请求。
第三个问题是如果执行普通文件操作,需不需要文件Cache?
第一个问题是数据拷贝的问题。
对于普通文件,如果采用了pagecache机制,那么这种拷贝合并在很大程度上是避免不了的。
但是对于网卡之类的设备,我们在读写数据的时候,需要避免这样的数据拷贝,否则数据传输效率将会变的很低。
我第一次关注这个问题是在做本科毕业设计的时候,那时候我设计了一块PCI数据采集卡。
在PCI采集卡上集成了4KB的FIFO,数据采集电路会将数据不断的压入FIFO,当FIFO半满的时候会对PCI主控芯片产生一个中断信号,通知PCI主控制器将FIFO中的2KB数据DMA至主机内存。
CPU接收到这个中断信号之后,分配DMA内存,初始化DMA控制器,并且启动DMA操作,将2KB数据传输至内存。
并且当DMA完成操作之后,会对CPU产生一个中断。
板卡的设备驱动程序在接收到这个中断请求之后,面临一个重要的问题:
如何将内核空间DMA过来的数据传输给用户空间?
通常有两种方法:
一种是直接将内核内存映射给用户程序;
另一种是进行数据拷贝的方式。
对于PCI数据采集卡而言,一个很重要的特性是实时数据采集,在板卡硬件FIFO很小的情况下,如果主机端的数据传输、处理耗费太多的时间,那么整条IO流水线将无法运转,导致FIFO溢出,数据采集出现漏点的情况。
所以,为了避免这样的情况,在这些很严格应用的场合只能采用内存映射的方法,从而实现数据在操作系统层面的零拷贝。
在Linux中,我们可以采用memorymap的方法将内核空间内存映射给用户程序,从而实现用户程序对内核内存的直接访问。
在Windows操作系统中,这种内核空间和用户空间的数据交互方式定义成两种:
MapIO和DirectIO。
MapIO就是采用内存拷贝的方式,DirectIO就是采用MDL内存映射的方式。
在编写WDMWindows设备驱动程序的时候经常会用到这两种数据传输模式。
值得注意的是,Windows中的DirectIO和Linux中的DirectIO是完全不同的两个概念。
在Linux中DirectIO是指写穿pagecache的一种IO方法。
第二个问题是异步IO和同步IO的问题。
对于普通文件而言,为了提高效率,通常会采用pagecache对文件数据在内存进行缓