Linux_driver

Reading time ~1 minute

一直想将在计算所的工作总结一下,但是一直给自己借口说没时间,最近一直在搞 Mesos/Mesosphere, 突然发现之前的工作快被我忘光了,终于不能再给自己任何借口,下了决心将自己在计算所的工作总结一下。

首先从Driver开始,其中可能涉及到Linux其他的方面,随性而写吧。

##Linux Driver##

要说Linux Driver的话,Linux IO 是必不可少的,我们首先来看一下Linux IO stack(Kernel version 3.3),如下图所示:

Linux_IO_Stack.png

这里将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 IODirect IOMap 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

Linux_generic_block_layer

## 块设备层分析 ##IO无论是经过EXT3文件系统还是块设备文件,最终都要通过writeback机制将数据刷新到磁盘,除非用户在对文件进行读写的时候采用了`Direct IO`的方式。为了提高性能,文件系统或者是裸设备都会采用Linux的cache机制对数据读写性能进行...… Continue reading

Linux Direct I/O

Published on January 12, 2017

ARM Interrupt Process

Published on December 29, 2016