一个IO的传奇一生.docx

上传人:b****3 文档编号:2994955 上传时间:2022-11-17 格式:DOCX 页数:22 大小:912.23KB
下载 相关 举报
一个IO的传奇一生.docx_第1页
第1页 / 共22页
一个IO的传奇一生.docx_第2页
第2页 / 共22页
一个IO的传奇一生.docx_第3页
第3页 / 共22页
一个IO的传奇一生.docx_第4页
第4页 / 共22页
一个IO的传奇一生.docx_第5页
第5页 / 共22页
点击查看更多>>
下载资源
资源描述

一个IO的传奇一生.docx

《一个IO的传奇一生.docx》由会员分享,可在线阅读,更多相关《一个IO的传奇一生.docx(22页珍藏版)》请在冰豆网上搜索。

一个IO的传奇一生.docx

一个IO的传奇一生

一个IO的传奇一生

前言

前几天同事提议写一篇文章来仔细分析一下一个IO从创建到消亡的整个过程,我觉得这个想法很好,一个IO从创建到消亡经历了千山万水,从软件到硬件涉及到很多很多的技术。

一个看似简单的IO读写操作,其实汇集了从计算机软件技术、硬件技术、电子技术、信号处理等各个方面的内容。

所以,我想把IO的一生通过自己的认识把他描述一下,让世人看清在执行一个简单的IO操作究竟汇集了多少的智慧!

汇集了工程师、科学家多少的心血!

在此,将此系列文章定名为《一个IO的传奇一生》,与大家一起分享。

针对不同的操作系统,IO历程是有所差别的,但是很多基本思想是相同的。

在此,我想以Linux操作系统为样本,对整个IO历程进行深入分析,最主要的是设计思想方面的考虑。

上图描述了IO操作中所涉及到的软硬件模块,从这张图中我们可以一窥整个系统还是很庞大的,主要涉及了文件系统、块设备层、SCSI层、PCI层、SAS/Ethernet网络以及磁盘/U盘。

本文会根据作者的理解对IO在上述各个层次的游历过程进行详细阐述。

文件基本操作

如果你想保存你的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->f_op->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对文件数据在内存进行缓

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 法律文书 > 调解书

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1