一直想将在计算所的工作总结一下,但是一直给自己借口说没时间,最近一直在搞 Mesos/Mesosphere
, 突然发现之前的工作快被我忘光了,终于不能再给自己任何借口,下了决心将自己在计算所的工作总结一下。
首先从Driver开始,其中可能涉及到Linux其他的方面,随性而写吧。
##Linux Driver##
要说Linux Driver的话,Linux IO 是必不可少的,我们首先来看一下Linux IO stack(Kernel version 3.3),如下图所示:
这里将SSD相关的内容也添加进来了,因为在之后的章节中我们也会详细介绍SSD FTL(Flash Translation Layer)。
OK,Here we going!
文件的基本操作
如果你想保存word资料到本地磁盘,就会触发一个文件系统的写操作,如果你想将一个文件从本地磁盘拷贝到U盘时,就会触发一个文件系统的读写过程。众所周知,为了简化用户对文件的管理,Linux提供了文件系统对数据进行管理,文件系统是操作系统最为重要的组成部分。一旦你想往文件系统写入数据时,一个新的IO请求就会才用户态诞生,但是其绝大部分的处理流程都发生的内核空间。对于不同的应用类型,IO请求的属性会大相径庭。除了文件本身应该具备的基本属性(读写权限等)之外,还需要考虑文件的访问模式:一步IO还是同步IO?对文件系统的Cache是如何控制的?应用程序和内核程序之间是如何交互的?所以,在创建一个IO时,需要考虑很多这样的因素。
在Linux中,当进行文件操作的时候,5个API是必不可少的:create()
open()
close()
write()
read()
实现了对文件的所有操作。create()
函数用来打开一个文件,如果该文件不存在就在磁盘上创建该文件;open()
函数用来打开一个指定的文件,如果在open()
中指定了O_CREATE
标记,那么open()
函数同样可以实现create()
函数的功能;close()
函数用于释放文件句柄;write()
read()
函数用于实现文件的读写过程。
举个例子:如果用户需要对一个文件进行写操作,那么首先调用open()
函数打开想要操作的文件,该函数返回要操作文件的句柄,然后调用write()
将数据写入文件,最后采用close()
释放文件句柄,结束文件写入过程。但是在这个过程中,整个系统到底发生了哪些事情呢?
打开操作
用户态的API通过系统调用,触发INT80中断陷入内核。
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
下的inode hash表,就可以很快找到/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()
函数执行进一步的初始化工作。
写操作
当一个文件被打开之后,用户态程序就可以得到一个文件对象,即文件句柄。一旦获取文件句柄之后就可以对其进行读写操作了。用户态的读写函数write()
对应内核空间的sys_write()
例程,通过系统调用陷入sys_write()
。sys_write()
函数在VFS层做的工作有限,其会调用文件对象中指定的操作函数file->f_op_write()
。对于不同的文件系统,file->f_op_write()
指向的操作函数是不同的。
如果文件设备是一个USB设备,并且采用的是字符设备的接口,那么在初始化文件inode的时候会调用init_special_inode()
初始化这些特殊的文件。对于字符设备会采用默认的def_chr_fops()
方法集,对于块设备会采用def_blk_fops()
方法集,不同的文件类型会调用各自的方法集。
def_chr_fops()
方法集其实就只定义了open()
方法,其他的方法都没有定义。其实字符设备的操作方法都需要字符设备驱动程序自己定义,每个设备驱动程序都需要定义自己的write()
read()
open()
close()
方法,这些方法保存在字符设备对象中。当用户调用文件系统接口open()
函数打开指定字符设备文件时,VFS会通过上述的sys_open()
函数找到设备文件inode中保存的def_chr_fopens()
方法,并执行该方法中open()
函数(chrde_open()
),chrdev_open()
函数完成一个重要的功能就是将文件对象file中采用的方法替换成驱动程序设定的设备操作方法。完成这个偷梁换柱的代码是:
file->f_op = fops_get(p->ops)
一旦完成这个过程,后继用户程序通过文件系统的write方法都会调用字符设备驱动程序设定的write方法。即对于字符设备而言,在VFS的sys_write()
函数将直接调用字符设备程序的write()
方法。所以,对字符设备驱动程序而言,整个过程很简单,用户态可以直接通过系统调用执行字符设备驱动程序的代码。而对于块设备和普通文件,这个过程将会复杂的多。
在用户程序发起写请求的时候,通常会考虑如下三个问题:
- 第一个问题是用户态数据如何高效传递给内核?
- 第二个问题是采用同步或者异步的方式执行IO请求?
- 第三个问题是如何执行普通文件操作,需不需要文件cache?
第一个问题是数据拷贝的问题。对于普通文件,如果采用了page cache
机制,那么这种拷贝合并的开销在很大程度上是 避免不了的。但是对于网卡设备之类的设备,在读写数据的时候,就需要避免这样的数据拷贝,否则数据传输效率将会变得很低。例如,一块PCI数据采集卡。在PCI采集卡上集成了4KB的FIFO,数据采集电路会将数据不断的压入FIFO,当FIFO半满的时候会对PCI主控芯片产生一个中断信号,通知PCI主控器将FIFO中的2KB数据DMA到主机内存。CPU接收到这个中断信号之后,分配DMA内存,初始化DMA控制器,并启动DMA操作,将2KB数据传输到Host内存。并且当DMA完成操作之后,会对CPU产生一个中断。板卡的设备驱动程序在接收到这个中断请求之后,面临一个重要的问题:如何将内核空间DMA过来的数据传输给用户空间?通常有两种方案:
- 一种是直接将内核内存映射给用户程序
- 另一种是进行数据拷贝的方式
对于PCI数据采集卡而言,一个很重要的特性是实时数据采集,在板卡硬件FIFO很小的情况下,如果主机端的数据传输,处理耗费太多的时间,那么整条IO流水线将无法运转,导致FIFO溢出,数据采集就会出现漏点的情况。所以,为了避免这样的情况,在这些很严格应用的场合只能采用内存映射的方法,从而实现数据在操作系统层面的零拷贝,在Linux中,可以采用memory map
的方法将内核空间内存映射给用户程序,从而实现用户对内核内存的直接访问。在Windows操作系统中,这种内核空间的和用户空间的数据交互方式定义成两种:Map IO
和 Direct IO
。Map IO
就是采用内存拷贝的方式,Direct IO
就是采用MDL内存映射的方式。在编写WDM Windows设备驱动程序的时候经常会用到这两种数据传输模式。值得注意的是,Windows中的Direct IO
和 Linux 中的Direct IO
是完全不同的两个概念。在Linux中Direct IO
是指写穿page cache
的一种IO方法。
第二个问题是异步IO和同步IO的问题。对于普通文件而言,为了提高效率,Linux通常会采用page cache
对文件数据在内存进行缓存。cache 虽然提高了效率,但是对有些应用来说,一旦发出写请求并且执行完之后,其期望的是将数据写入磁盘,而不是内存(page cache
)。例如:有些应用会有一些元数据操作,在元数据操作的过程中,通常期望将数据写入磁盘,而不是cache在内存中。这就提出了同步IO的需求。为了达到这个效果,可以在打开文件的时候设置O_SYNV
标记。当数据在page cache
中聚合之后,如果发现O_SYNC
标记被设置,那么就会将page cache
中的数据强制刷新到磁盘。对于EXT3文件系统,该过程在ext3_file_write()
函数中实现的。
第三个问题是普通文件的cache问题。对于普通文件,由于磁盘性能比较低,为了提高读写性能,通常会采用内存作为磁盘的cache。文件系统会采用预读等机制对文件读写性能进行优化,避免磁盘随机IO性能过低对文件读写性能造成影响。
但是,page cache
虽然提高了性能,但是也会对文件系统的可靠性造成一定影响。例如:当数据已经被写入内存之后,系统Crash,内存中的磁盘数据将会遭到破会。为了避免这种情况,Linux文件系统提供了Direct IO
方式,该方式就是让一次IO过程绕过page cache
机制,直接将文件内容刷新到磁盘。与上面的同步IO相比,Direct IO
达到的效果有点类似。其实,同步IO是一种write through
的cache机制,而Direct IO
是完全把cache抛弃了,同步IO的数据在内存还是有镜像的,而Direct IO
是没有的,这是两者的区别。在Linux中的__generic_file_aio_nolock()
函数中,会判断O_DIRECT
标记是否被设置,如果该标记被设置,那么调用generic_file_direct_write()
函数完成数据磁盘写入过程,如果该标记不存在,那么调用generic_file_buffered_write()
函数将数据写入page chache
。