Linux内核:一文读懂文件系统、缓冲区高速缓存和块设备、超级块

目录

前言 第一部分 Linux文件系统堆栈

VFS数据结构

文件系统初始化顺序

Dentries

打开文件-说起来容易做起来难!

虚拟文件系统

前言 第二部分 Linux文件系统堆栈

当我们键入“ cd / bm / celtic”时会发生什么?

如何写入新文件?

前言 第三部分 Linux缓冲区高速缓存和块设备

流式写入如何?

Linux如何实现这种聚集?

架构实现的算法

前言 第四部分 SD卡闪存转换层

启动码

传输模式

翻译层

Flash翻译层算法

a.连续写入

b.跨AU的随机写入

关联行业翻译

前言 第五部分 SD Card Speed Class

前言 第六部分 Linux 文件系统写操作

一些细节问题

要旨

第一部分:文件系统模块模板

5组系统调用

硬编码的SFS超级块和根索引节点

Bare bone SFS模块正在运行

第二部分:块设备上的文件系统

块设备上的真实SFS

第三部分:运行中的文件系统

实际的SFS运作中

运行背后的实现

笔记


 

前言 第一部分 Linux文件系统堆栈

https://msreekan.com/2012/06/07/file-system-loadable-kernel-module-lkm/


为什么将文件系统作为可加载的内核模块(LKM)?

如您所见,用户空间的想法并没有很好地展现出来:http : //tekrants.me/2012/05/22/fuse-file-system-port-for-embedded-linux/

VFS数据结构

索引节点可能是定义VFS文件条目的最关键的抽象—它代表文件系统中的每个文件/目录/链接。如果您的文件系统类似于FAT,并且缺少清晰的“ inode”,则需要转换层。最终,它是关于从文件系统特定的数据结构中提取与Linux inode相关的文件信息。

 

文件系统初始化顺序


  • 1.向VFS注册文件系统的挂载和卸载回调。
  • 2.安装调用负责创建和注册根目录 inode  。
  • 3.根目录 inode 本质上是卷的入口点。它提供了特定的函数指针,以后由VFS调用,用于与inode相关的操作(如create)和文件操作(如open,read)和目录操作(如readdir)。

以上三个步骤以及您的文件系统模块已全部设置好,这意味着Linux将有足够的信息将“打开”调用从应用程序转换为文件系统特定的内部打开调用,这要归功于根inode内的函数指针 。

 

Dentries


Dentry是每个文件和目录都存在的另一个内核结构。例如,访问路径“ / mnt / ramfs”将导致在内存dentry结构中创建两个。“ mnt”和“ ramfs”各一个。请注意,“ ramfs” dentry将具有指向“ mnt” dentry的父指针和指向其自己的VFS索引节点的指针。

Dentry实际上包含文件系统条目的父目录的名称和句柄之类的属性。将Inode与这些属性分开的原因之一是文件链接的存在,其中单个Inode在多个Dentries之间共享。

 

打开文件-说起来容易做起来难!


  1. 考虑打开路径为“ /mnt/ramfs/dir1/dir2/foo.txt”的文件
  2. 上面路径中的dentry元素是“ mnt”,“ ramfs”,“ dir1”,“ dir2”和“ foo.txt”
  3. “ mnt” dentry将成为Linux根文件系统的一部分,所有Dentries信息都属于哈希表,字符串“ mnt”将映射到哈希表条目,并提供其dentry指针。VFS从此dentry获取inode指针,并且每个目录inode都有一个回调函数,用于在其文件/目录条目上列出查找操作。
  4. 在“ mnt” inode上查找的查询将返回“ ramfs”的inode以及其dentry。
  5. 这是一个反复的过程,最终VFS将找出文件路径中所有元素的inode和dentries。
  6. 请注意,与“ foo.txt”关联的Inode将提供打开函数指针以调用特定于文件系统驱动程序的打开调用。

 

虚拟文件系统


移植到Linux的文件系统有望填充VFS数据结构的字段,例如Inode和Dentries,以便Linux可以理解并将文件属性和内容传达给用户。跨文件系统(例如ext4,UBIFS,JFFS2等)的明显区别在于它们各自的算法,它们还定义了内部数据结构和设备访问模式。

如何从存储中表示和访问Dentries / Inodes是文件系统特有的,这从本质上定义了它们的优缺点。简而言之,Linux中的文件系统包括一组用于管理通用VFS数据结构的回调,这些回调基本上是Inode,Dentries,文件处理程序等。因此,我们具有inode数据结构和相应的关联inode操作,我们具有文件指针数据结构和文件操作,dentry数据结构和dentry操作等。

Linux文件系统的关键在于它能够使用Inodes和Dentries的Linux内核语言进行交谈。另外,除非是只读卷,否则这种解释也需要相反。当用户对文件进行更改时,文件系统需要理解Linux,并将这些更改转换为它可能在存储中具有的表示形式。毫无疑问,理解Linux VFS要求对内核数据结构有深入的了解,这可能意味着文件系统编写者需要在文件系统代码中具有内核特定的层,可以通过使用内核库函数来极大地减少这种不希望的复杂性。

通常以“ generic_”开头的函数可以解释为这样的帮助函数,该函数从内核模块中提取内核细节,该函数广泛用于文件系统操作,例如“读取”,“写入”甚至卸载。在研究代码时,内核模块中通用帮助程序功能的使用可能会造成混淆,因为它们倾向于模糊内核和模块的特定边界,这种重叠是令人费解的,但却是避免内核依赖的极其有效的方法。

图片来源 :http :  //wiki.osdev.org/images/e/e5/Vfs_diagram.png

Linux VFS背后​​的设计思想似乎是“选择”,除了最低限度的要求外,始终向内核黑客提供有关接口实现的选择。他或她可以使用通用函数,创建自定义函数或将其简单设置为NULL。这是支持大量不同设备和文件系统的理想方法,当我们查看ext4的代码时很容易看到,其中ext4的页面缓存上使用了缓存缓存抽象,而UBIFS的页面缓存没有sans缓存,而JFFS2的直接方法。要适应所有这些各种各样的设计,就需要一个灵活的法治 驱动框架,每个人都可以找到自己的定位,而又不会影响另一个内核模块的功能。

 

前言 第二部分 Linux文件系统堆栈

https://msreekan.com/2012/10/05/a-linux-file-system/


Linux文件系统有望处理两种类型的数据结构-Dentries和Inodes。它们确实是Linux内核中运行的文件系统的定义特征。例如,路径“ / bm / celtic”包含三个元素“ /”,“ bm”和“ celtic”,因此每个元素都有自己的dentry和inode。在许多其他信息中,一个dentry封装了名称,一个指向父dentry的指针以及一个指向相应inode的指针。

 

当我们键入“ cd / bm / celtic”时会发生什么?


设置当前工作目录涉及将进程“ task_struct”指向与“ celtic”相关联的树,查找该特定条目涉及以下步骤。

  1. 字符串开头的“ /”表示根
  2. 在文件系统安装期间会提供根目录树,因此VFS可以在其中开始搜索文件或目录。
  3. 文件系统模块有望在其提供父级Dentry时搜索子级。因此,VFS将通过提供其父dentry(根)来请求denty“ bm”。
  4. 由文件系统模块使用父Dentry查找子条目。请注意,父Dentry也有一个指向其自己的inode的指针,该inode可能持有密钥。

上述步骤序列将递归重复。这次,父级将是“ bm”,而“celtic”将是子级,以这种方式,VFS将生成与路径关联的dentry列表。

Linux适用于在相对较大的DRAM存储器支持的缓慢硬盘上运行。这可能意味着Dentries和Inode的海洋被缓存在RAM中,每当遇到缓存未命中时,VFS都会通过调用特定于文件系统模块的“ look_up”函数来尝试使用上述步骤对其进行访问。

从根本上说,文件系统模块只能在inode上运行,Linux将请求诸如inode的创建和删除,inode的查找,inode的链接,为inode分配存储块等操作。

路径解析,控制缓存管理都作为VFS的一部分在内核中抽象,而作为块驱动程序框架的一部分的缓冲区管理则抽象为内核。

 

如何写入新文件?


  1. 用户空间使用“ write”系统调用传达要写入的缓冲区
  2. 然后,VFS分配一个内核页面,并将该页面与该inode的“ address_space”中的写偏移量相关联,每个inode都有其自己的address_space(由文件偏移量索引)。
  3. 每次写入最终都需要在存储设备中结束,因此RAM缓存中的新页面将必须映射到存储设备中的块。为此,VFS调用文件系统模块的“ get_block”接口,该接口建立此映射。
  4. copy_from_user_space例程将用户内容移至该内核页面并将其标记为脏
  5. 最后,控件返回到应用程序。

文件的覆盖内容在两个方面有所不同–一个是写入偏移量可能已经在缓存中分配了页,另一个是已将其映射到存储中的块。因此,这只是从用户空间到内核空间缓冲区的一个简单问题。当内核刷新程序线程插入时,所有脏页都将被写入,这时已经建立的存储映射将帮助内核识别页面必须进入哪个存储块。

读取新文件的步骤与之类似,只是需要将内容从设备读取到页面中,然后再读取到用户空间缓冲区中。如果遇到更新的页面,则当然避免从存储设备读取。

 

前言 第三部分 Linux缓冲区高速缓存和块设备

https://msreekan.com/2012/11/04/linux-buffer-cache/


在Linux内核的Flash文件系统端口上工作需要对它的缓冲区管理有一个很好的了解,有时对内部结构的挖掘可能会有些麻烦,但是执行集成良好的端口是必需的。希望本文有助于对内核缓冲区/页面高速缓存有良好设计概述的人。

VFS在页面顶部工作,它以页面大小的最小粒度(通常为4K)分配内存,但是存储设备的I / O大小要比该大小小得多(通常为512字节)。将设备写入粒度设置为页面大小的情况可能导致冗余写入,例如,如果应用程序修改了文件的256字节,则高速缓存中的相应页面被标记为“脏”,并且刷新该文件会导致4K传输字节,因为缓存没有机制来识别此脏PAGE中的脏扇区。

缓冲区高速缓存 桥接页面和块Universe,与文件关联的页面将被划分为缓冲区;当修改页面时,仅将相应的缓冲区设置为脏缓冲区,因此在刷新时仅写入脏缓冲区。换句话说,缓冲区高速缓存是在页面顶部完成的抽象,它与同一RAM存储器只是一个不同的方面。称为“ buffer_head”的数据结构用于管理这些缓冲区,下图是对该系统的描述。

 

缓冲区高速缓存可以通过“ buffer_head”链接列表遍历与页面关联的这些缓冲区的列表,并确定其状态;请注意,上图假定缓冲区大小为1K。

 

流式写入如何?


当处理大量数据时,小扇区的顺序传输方法会导致明显的整体读写头定位延迟,并且由于传输的建立和拆除周期多而导致额外的开销。

让我们考虑一个由四个4K页面组成的16K缓冲区写操作的示例,下表中给出了这4个页面的内存映射。

例如,文件的0至3扇区位于RAM地址0x1000至0x1FFF中,并且已映射到存储块10、11、12和13。

将16K盲目拆分为16个缓冲区并一一写入将导致许多传输设置和拆卸,以及同等数量的读写头位置延迟。

我们如何以以下方式对页面的编写进行重新排序?

第1页>>>第3页>>>第2页>>>第4页

上面的序列是按块映射的升序对页面进行排序的结果,即:从10到25,仔细查看还将发现相邻传输的地址从0x1000到0x4FFF,因此在RAM存储器中是连续的。这样就可以不间断地传输16K字节,这似乎是最佳的优化方案。

 

Linux如何实现这种聚集?


让我们尝试理解博克驱动程序子系统如何扭曲应用程序的写入顺序以实现上述优化。

架构实现的算法

步骤1: 将缓冲区封装在BIO结构中。
文件系统缓冲区封装在BIO结构中(submit_bh函数可以提供详细信息)。在这个阶段,我们示例中的所有16个缓冲区都包装在其相应的BIO结构中。我们可以在下表中看到映射。

步骤2: 将BIO插入请求中。
BIO依次提交到下一层,而映射到相邻块的BIO则合并为一个“请求”。添加了请求结构后,我们得到了以下更新的映射表,如您所见,“请求0”结合了BIO 0、1、2和3。

在将请求添加到全局块驱动程序队列之前,存在一个中间task_struct队列,这是一个显而易见的优化,旨在将相邻的BIO合并为一个请求,并最大程度地减少对块驱动程序队列的争用。是的,全局排队操作不能与其他排队或去排队同时进行。

步骤3:将 请求合并到全局队列中。
将一个请求合并到全局队列中将重新排序,并将相邻映射的“请求”合并为一个,在我们的情况下,我们最终将只有一个单个请求,它将所有BIO包裹起来。

步骤4:最后,我们需要确定连续的内存段
块设备驱动程序将识别相邻BIO上的连续内存段,并在启动传输之前合并它们,这将减少I / O设置-拆卸周期。因此,我们将在下面的最终表中添加一个segment属性。

块设备驱动程序还可以具有一个请求调度程序,该调度程序算法将对优先级进行排序,并从全局队列中选择要排队的请求以进行I / O处理。请注意,读取请求的步骤也遵循上述框架。

推理:以上提到的钟声与闪存无关紧要,因此避免所有此类行李可能很有意义。

 

前言 第四部分 SD卡闪存转换层

https://msreekan.com/2014/01/15/sdcard/


SD CARD协议本身提供了内部设计的粗略蓝图,但是有时您可以根据规范提供的提示来实现特定的用例,以揭示卡机制的某些限制和优势。众所周知,SD卡通常在NAND MLC或SLC闪存上运行,但从表面上看,它的行为不像闪存,用户无需担心块擦除,位翻转或损耗均衡。闪光的所有复杂性似乎都在黑匣子中抽象出来,但是我们仍然可以尝试发掘这种混乱的某些方面。

 

启动码


一旦提供了电压,就可以通过对卡检测引脚(引脚1)进行采样来在内部确定卡是用于SPI还是SD总线,仅在SD模式下才将其拉高。为这些接口实现的协议差异很大,因此它们的软件堆栈也不同,并且当然也存在一个小型微型控制器来驱动此SD固件。

引导代码检查引脚1,标识接口并跳转到相应的软件接口堆栈,现在让我们考虑该卡是通过SD总线连接的。引导代码通过使用CMD8和ACMD41来确保主机提供的电压是适当的,该引导过程由主机驱动,我们可以想象只有在软件进入规范所定义的条件时,卡的启动才能完成。 转移状态

两种模式下都共享硬件引脚,因此必须有一个内部多路复用器,它将配置控制器以使用SDSPI物理层协议来驱动引脚。我们可以想象使用的芯片组将包括相应的硬件IP或SD,SPI和多路复用器逻辑的至少某种形式的软件实现。

 

传输模式


一旦进入传输模式,启动就完成了,这里SD状态机主要用于服务I / O请求,例如扇区读取,写入和擦除。DMA引擎最有可能用于确保内部闪存传输与SD I / O并行发生,换句话说,卡可能正在将NAND页传输到闪存,同时它通过SD接口从主机接收更多数据。读和写操作通过流水线和缓冲进行流水线处理,以确保最大的吞吐量,尤其是对于更高等级的10类卡。

翻译层


如果不进行中间擦除,则无法覆盖Flash内容。因此,对于任何SDCARD I / O操作,都存在从虚拟地址到物理地址的“转换”。例如,假设我们已写入SDCARD上的扇区1024,并且此地址最初在闪存上已映射到物理扇区1024本身。后来我们覆盖了内容,但是这次写操作无法到达相同的物理地址,因为在再次编程之前需要擦除它。现在,转换层将必须将虚拟地址1024重新映射到另一个已擦除且干净的物理地址,例如2048。因此,现在,虚拟扇区1024转换为另一个物理地址。同样,对虚拟地址的每次写入都将映射到物理地址,并且随后对该位置的每次读取都转换为相同的地址。这些映射由固件单独存储,这样,转换层将从卡用户那里提取闪存的复杂性,而卡用户始终不理会实际的物理地址位置。显然,在重新使用脏页之前,需要先进行擦除。

每个翻译层还需要为其地图指定大小,规格确认,符合速度等级规范(2、4、6和10级)的卡将实现与其“分配单位”(AU)大小相等的内部地图大小,这可以从卡寄存器之一读取。这意味着虚拟到物理的地图将具有AU大小,通常为4MB。

Flash翻译层算法

读取非常简单,所有SDCARD所需要做的就是访问其映射信息并读取相应的物理AU。根据使用情况,写入可能会比较棘手,让我们考虑两种类型。

a.连续写入

写入是连续的-仅在完全写入先前的物理AU之后才跨越AU边界。此处,卡转换层将为写入选择完全擦除的物理AU,然后连续刷新内容。在每次写入AU之后,较旧的物理AU被标记为脏并排队等待擦除,同时将转换重新映射到新写入的AU。下面给出一个例子。

考虑在写入之前将虚拟AU 10映射到物理AU 30(PAU 30),并且在IO启动时,映射算法选择(Physical)AU40作为虚拟AU10(VAU 10)的新物理映射。

写入从PAU40开始。4MB写操作结束后,当前映射到PAU 30的VAU 10将重新映射到PAU 40。

PAU 30被标记为脏并添加到擦除队列中。

最简单,最快的用例很容易。

b.跨AU的随机写入

这里,我们在位于不同AU的寻址块处进行写入,这与在文件写入之间需要更新FAT表和目录条目内容的用例非常相似。请考虑以下插图:

  • 步骤1:将文件内容以1MB的形式写入VAU10。
  • 步骤2:以512字节的FAT表形式写入VAU1。
  • 步骤3:以512字节的目录条目形式写入VAU 8。

第一步将在VAU10上写入1 MB,然后文件系统继续写入位于VAU1上的FAT表,请考虑在写入之前VAU10已被完全使用,并且包含3MB的有效数据和1MB的脏数据(3 + 1)。 = 4MB)。

下面说明了有关上述文件系统操作的FTL内部执行的步骤。

VAU10内部的文件簇最初被映射到PAU20,当写入开始时,一个完全擦除的PAU 35被选作新的映射(写操作不能在脏闪存块上进行!)。

1MB的文件内容已写入PAU35。现在3MB的VAU 10内容驻留在PAU 20中,其余新写入的1MB内容位于PAU35中。

一个VAU不能被映射到两个物理AU的,所以我们有两个选择:
a.将3MB内容从PAU20移到PAU35,然后将VAU10重新映射到PAU35。


b.进行PAU 20的部分擦除,并将新的1 MB从PAU35移至该位置,并保持相同的旧地图。

SDCARD可能使用上述两个选项之一,在第一种情况下,将更新地图信息(将VAU10-> PAU20修改为VAU10-> PAU35),而在第二种情况下,地图信息将保持不变(VAU10-> PAU20)。两种情况都会产生开销,这严重影响了SDCARD的写入性能。

上面的细节暗示着一个事实,即SDCARD倾向于选择一个我们要写入的AU作为“活动的AU”,因此对该“活动AU”的任何写入都将具有最小的开销,但是只要我们将写入位置切换到外部即可。这个“主动AU”地图维护开始了-本质上就是垃圾收集。一些类10卡可容纳两个活性的AU,因此开销仅在上述情况发生第三步 以上给出的FAT文件系统的写说明的,而切换到第二步发生无故障。

Y轴:以秒为单位的时间,X轴:写入计数

Y轴:以秒为单位的时间,X轴:写入计数

上图说明了以下三个用例的写入时间:

  • 1.交错式写入连续在VAU之间切换,第一个4K字节写入VAU1上的地址,第二个4K字节写入VAU2上的地址,依此类推。
  • 2.随机写入每五次写入切换一次VAU,因此在VAU1中的四个位置移动到VAU2之前发生4K写入,依此类推。在这里,我们切换VAU的次数少于第一种情况。
  • 3.流式 4K写入是连续的,只有在完全写入前一个之后才能进行AU切换,因此没有合并开销,并且这里切换AU的次数最少。

关联行业翻译

引用局部性对于实现最佳写入吞吐量至关重要。从图上可以看出,连续时写入速度更快,而跨AU边界完成写入时,写入速度则远非最佳。SD CARD最有可能应采用块关联扇区转换的变体 。

有关此主题的更多信息– SDCARD Speed Class

 

前言 第五部分 SD Card Speed Class

https://msreekan.com/2014/02/15/sdcard-speed-classes/


 

前言 第六部分 Linux 文件系统写操作

https://msreekan.com/2014/07/14/linux-file-system-write/


Linux文件写入方法特定于特定文件系统。

例如,JFFS2将在用户进程“上下文”自身内部(在write_end调用内)同步数据,而诸如UBIFS,LogFS,Ext和FAT之类的文件系统往往会创建脏页,并让后台冲洗程序任务管理同步。

这两种方法各有优点,但是使用第二种方法会使Linux内核表现出某些怪异现象,这可能会严重影响写入吞吐量的测量。

上图确实给出了设计概述,实际上,此体系结构有两个主要属性:

COPY:将用户空间数据复制到与该文件关联的内核页面中,并通过“ write_begin”“ write_end”文件系统回调将其标记为“脏,实际上它们是 由用户进程进行的每个“写入”系统调用所调用的。因此,尽管它们确实是内核模式函数,但它们是代表 用户空间进程执行的,而用户空间进程反过来只是在等待系统调用。序列说明如下:

    write_begin()  /* Informs FS about the number of bytes being written */
    mem_copy()    /* Copies data to kernel page cache.*/
    write_end()     /* Copied kernel page marked as dirty and FS internal */
/*  meta information is updated */

回写:Linux内核将产生一个冲洗程序任务,以将以上创建的这些脏页写入存储中。为此,这个新产生的任务通常会调用文件系统特定的 “ writepage”“ writepages”回调。

一些细节问题

上面提到的回调是特定于文件系统的,并且实际上是在其使用内核初始化时注册的。您可能已经注意到,以上设计几乎类似于经典的生产者消费者方案,但是有一些细微差别:

较低阈值:仅当一定百分比的页面缓存变脏时才产生 刷新任务(使用者)。因此,确实有一个较低的阈值(在/ proc / sys / vm / dirty_background_ratio中配置)可以生成此任务。请注意,内部超时后也可以调用它。因此,即使脏页数量很少,它们也不会长时间保存在易失性内存中。

更高的阈值: 一旦用户进程(生产者)弄脏了一定比例的页面高速缓存,该进程将被迫进入休眠状态,以避免内核耗尽内存。直到刷新任务赶上来并将脏页级别降低到可接受的极限之前,这样的用户进程才不会从这种非自愿睡眠中解脱出来。这样,内核趋向于推迟具有大量数据写入能力的进程。可以通过写入/ proc / sys / vm / dirty_ratio来指定此更高的阈值。

因此,在上述下限和上限之间,系统倾向于同时执行两个任务,一个任务将页面内容弄脏,另一个任务通过写回清除它们。如果创建了太多脏页,那么Linux可能还会产生多个刷新程序任务。似乎这种设计可能在具有多个存储磁盘路径的SMP体系结构上很好地扩展。

我们可以通过代码级详细信息来使内容更加清晰。上述机制的关键方面是在下面提到的balance_dirty_pages(page-writeback.c)中实现的 ,它 在内核内部的“ write_end”调用之后被调用。

该功能审核系统中脏页的数量,然后确定是否应停止用户进程。

static void balance_dirty_pages (struct address_space *mapping, unsigned long write_chunk)
{
    int pause = 1;
    …
    ……
    ………
    for (;;)
    {
        ….
        ……         /* Code checks for dirty page status and breaks out of the loop only if the dirty page ratio has gone down */
        ……..
        io_schedule_timeout(pause);
        /*
        * Increase the delay for each loop, up to our previous
        * default of taking a 100ms nap.
        */
        pause <<= 1; if (pause > HZ / 10)
        pause = HZ / 10;
    }
}

函数io_schedule_timeout的参数“ pause”以抖动为单位指定睡眠时间,而大的 for(;;)循环在每个睡眠周期后反复检查脏页比率,如果没有足够的减少,则睡眠时间为翻倍,直到达到100mS,这实际上是“生产者”过程可以被强制进入睡眠状态的最长时间。在文件写入过程中执行该算法,以确保无赖进程不会占用整个页面高速缓存内存。

要旨

对于上述上限和下限阈值,我们可以赋予相当大的意义,如果未针对系统特定用例对它们进行微调,则RAM利用率将远非最佳。这些配置的详细信息将取决于RAM大小以及同时启动文件系统写入的进程数,这些写入的大小,正在写入的文件大小,连续两次文件写入之间的典型时间间隔等。

安装了关闭同步的文件系统旨在利用页面缓存进行缓冲。最佳写入速度将要求上限必须大到足以缓冲整个文件,同时允许无缝并发回写,否则用户进程写入的内核响应时间充其量是不稳定的。

另一个关键方面是, 写页 对write_beginwrite_end的干扰应最小, 只有这样,在不断调用后两个功能的用户流程方面,冲洗程序任务的写回操作才能以最小的摩擦发生。如果它们共享一些资源,则最终用户进程和刷新程序任务将最终彼此等待,并且异步缓冲页面高速缓存写入的预期快速响应时间将无法实现。

 

第一部分:文件系统模块模板

这是第二十二篇文章,是Linux设备驱动程序系列的一部分,介绍了一个基本的文件系统模块。

https://sysplay.in/blog/linux-device-drivers/2014/11/the-semester-project-part-v-file-system-module-template/


通过笔式驱动器的格式化,文件系统全部设置在硬件空间中。现在,轮到使用内核空间中的相应文件系统模块进行解码,并相应地提供用户空间文件系统接口,以便像其他文件系统一样对其进行浏览。

5组系统调用

与字符或块驱动程序不同,文件系统驱动程序不仅涉及文件系统提供的各种接口的功能指针的一种结构,还涉及5种功能指针的结构。这些是:

  • struct file_system_type –包含对超级块进行操作的函数
  • struct super_operations –包含在inode上操作的函数
  • struct inode_operations –包含对目录条目进行操作的函数
  • struct file_operations –包含对文件数据进行操作的功能(通过页面缓存)
  • struct address_space_operations –包含文件数据的页面缓存操作

有了这些,有了许多新名词。他参考了以下词汇表,以了解文件系统模块开发中上面和以后使用的各种术语:

  • 页面高速缓存或缓冲区高速缓存:RAM缓冲区池,每个页面大小(通常为4096字节)。这些缓冲区用作从底层硬件读取的文件数据的缓存,从而提高了文件操作的性能
  • Inode:包含文件元数据/信息的结构,例如权限,所有者等。尽管文件名是文件的元数据,但是为了更好地利用空间,在典型的Linux文件系统中,它不保存在inode中。在所谓的目录条目中。索引节点的集合,称为索引节点表
  • 目录条目:包含文件或目录的名称和索引节点号的结构。在典型的基于Linux的文件系统中,目录D的数据块中存储着目录D的直接文件和目录的目录条目的集合。
  • 超级块:包含有关文件系统各种数据结构信息的结构,例如inode表,…基本上是元元数据,即元数据的元数据
  • 虚拟文件系统(VFS):概念文件系统层以抽象方式将内核空间与用户空间连接,将“所有内容”显示为文件,并将其操作从用户转换为内核空间中的适当实体

上面五个结构中的每个结构都包含一个函数指针列表,需要根据文件系统(模块)中存在的所有功能或所支持的功能来进行填充。例如,

  1. struct file_system_type可能包含用于挂载和卸载文件系统的系统调用,基本上在其超级块上运行;

  2. struct super_operations可能包含inode读/写系统调用;

  3. struct inode_operations可能包含查找目录项的功能;

  4. struct file_operations通常可以对页面缓存的文件数据进行操作,进而可以调用在struct address_space_operations定义的页面缓存操作。

对于这些各种操作,这些功能中的大多数将与相应的基础块设备驱动程序对接,以最终在硬件空间中与格式化的文件系统一起运行。

首先,Pugs展示了他真正的SFS模块的完整框架,但是功能最少,足以编译,加载并且不会使内核崩溃。他只填充了这五个结构中的第一个-struct file_system_type ; 剩下的都空着了 这是结构定义的确切代码:

#include <linux/fs.h> /* For system calls, structures, ... */

static struct file_system_type sfs;
static struct super_operations sfs_sops;
static struct inode_operations sfs_iops;
static struct file_operations sfs_fops;
static struct address_space_operations sfs_aops;
#include <linux/version.h> /* For LINUX_VERSION_CODE & KERNEL_VERSION */

static struct file_system_type sfs =
{
	name: "sfs", /* Name of our file system */
#if (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,38))
	get_sb:  sfs_get_sb,
#else
	mount:  sfs_mount,
#endif
	kill_sb: kill_block_super,
	owner: THIS_MODULE
};

请注意,在Linux内核版本2.6.38之前,安装函数指针称为get_sb,而且它的参数曾经略有不同。因此,以上#if使其至少在2.6.3x以及可能与3.x内核版本之间兼容-不能保证其他版本。因此,相应的函数sfs_get_sb()sfs_mount()也被#if'd如下所示:

#include <linux/kernel.h> /* For printk, ... */

if (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,38))
static int sfs_get_sb(struct file_system_type *fs_type, int flags,
			const char *devname, void *data, struct vfsmount *vm)
{
	printk(KERN_INFO "sfs: devname = %s\n", devname);

	/* sfs_fill_super this will be called to fill the super block */
	return get_sb_bdev(fs_type, flags, devname, data, &sfs_fill_super, vm);
}
#else
static struct dentry *sfs_mount(struct file_system_type *fs_type,
					int flags, const char *devname, void *data)
{
	printk(KERN_INFO "sfs: devname = %s\n", devname);

	/* sfs_fill_super this will be called to fill the super block */
	return mount_bdev(fs_type, flags, devname, data, &sfs_fill_super);
}
#endif

以上两个功能的唯一区别在于,在后面的功能中,已删除了与VFS安装点相关的结构。其中的printk()将显示用户将要挂载的基础分区的设备文件,基本上是笔驱动器的SFS格式化分区。get_sb_bdev()mount_bdev()是相应内核版本的通用块设备安装函数,它们在fs / super.c定义并在<linux / fs.h>中进行原型化。像大多数其他文件系统编写者一样,Pugs也使用它们。您是否想知道:所有文件系统都以相同的方式挂载块设备吗?除安装操作需要填充VFS的超级块结构的部分(struct super_block),根据基础文件系统的超级块-显然,很可能会有所不同。但是,那怎么办呢?请仔细观察,在上述函数中,除了按原样传递所有参数外,还有一个附加参数sfs_fill_super,这是Pugs的自定义函数,用于根据SFS文件系统填充VFS的super块。

与mount函数指针不同,对于某些内核版本,unmount函数指针是相同的(kill_sb)。在卸载时,甚至没有跨不同文件系统的最小区别。因此,通用块设备卸载函数kill_block_super()已直接用作函数指针。

sfs_fill_super(),理想情况下,Pugs应该从底层硬件空间SFS中读取超级块,然后相应地将其转换并填充到VFS的超级块中,以使VFS能够提供用户空间文件系统接口。但是他还没有弄清楚如何在内核空间中读取底层块设备。从用户发出mount命令获得的信息中,已经使用的块设备信息已经嵌入到super_block结构本身中。但是当Pugs决定建立真正的SFS时,首先,他继续编写此sfs_super_fill()也可以用作硬编码填充功能。就此而言,他向VFS注册了Simula文件系统。与其他任何Linux驱动程序一样,这是文件系统驱动程序的构造函数和析构函数:

#include <linux/module.h> /* For module related macros, ... */

static int __init sfs_init(void)
{
	int err;

	err = register_filesystem(&sfs);
	return err;
}

static void __exit sfs_exit(void)
{
	unregister_filesystem(&sfs);
}

module_init(sfs_init);
module_exit(sfs_exit);

register_filesystem代码()unregister_filesystem()取指针的结构file_system_type SFS(实心以上),因为它们的参数,以分别注册和注销由它描述的文件系统。

 

硬编码的SFS超级块和根索引节点

是的,这是硬编码的sfs_fill_super()函数:

#include "real_sfs_ds.h" /* For SFS related defines, data structures, ... */

static int sfs_fill_super(struct super_block *sb, void *data, int silent)
{
	printk(KERN_INFO "sfs: sfs_fill_super\n");

	sb->s_blocksize = SIMULA_FS_BLOCK_SIZE;
	sb->s_blocksize_bits = SIMULA_FS_BLOCK_SIZE_BITS;
	sb->s_magic = SIMULA_FS_TYPE;
	sb->s_type = &sfs; // file_system_type
	sb->s_op = &sfs_sops; // super block operations

	sfs_root_inode = iget_locked(sb, 1); // obtain an inode from VFS
	if (!sfs_root_inode)
	{
		return -EACCES;
	}
	if (sfs_root_inode->i_state & I_NEW) // allocated fresh now
	{
		printk(KERN_INFO "sfs: Got new root inode, let's fill in\n");
		sfs_root_inode->i_op = &sfs_iops; // inode operations
		sfs_root_inode->i_mode = S_IFDIR | S_IRWXU |
			S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
		sfs_root_inode->i_fop = &sfs_fops; // file operations
		sfs_root_inode->i_mapping->a_ops = &sfs_aops; // address operations
		unlock_new_inode(sfs_root_inode);
	}
	else
	{
		printk(KERN_INFO "sfs: Got root inode from inode cache\n");
	}

#if (LINUX_VERSION_CODE < KERNEL_VERSION(3,4,0))
	sb->s_root = d_alloc_root(sfs_root_inode);
#else
	sb->s_root = d_make_root(sfs_root_inode);
#endif
	if (!sb->s_root)
	{
		iget_failed(sfs_root_inode);
		return -ENOMEM;
	}

	return 0;
}

如前所述,该函数基本上应该读取底层的SFS超级块,并相应地转换和填充结构super_block,该结构由其第一个参数sb指向。因此,了解它与了解struct super_block的最小字段已被填充一样。前三个是块大小,以2为底的对数以及Simula文件系统的类型/魔术代码。随着Pugs进一步编码,我们将看到,一旦他从硬件空间获得了超级块,他将取而代之的是从那个超级块中获取这些值,更重要的是验证它们,以确保安装了正确的分区。

之后,将各种结构指针指向其对应的功能指针结构。最后但并非最不重要的一点是,根索引节点的指针s_root指向从VFS的索引节点缓存中获取的struct索引结构,它基于根节点的索引节点号(目前已被硬编码为1),它可能会更改。如果是首次获取inode结构,即是第一次获取,则根据基础SFS根inode的内容对其进行填充。同样,模式字段被硬编码为“ drwxr-xr-x ”。除此之外,通常的结构指针由相应的结构地址初始化。最后,使用以下命令将根的inode附加到超级块:d_alloc_root()d_make_root(),根据内核版本。

所有上面的代码段放在一起作为裸骨real_sfs_bb.c,与沿real_sfs_ds.h(基于相同的文件先前创建),以及支撑生成文件可从rsfsbb_code.tbz2

 

Bare bone SFS模块正在运行

使用make编译并获得real_sfs_bb.ko驱动程序后,Pugs进行了他通常的不寻常的实验,如图38所示。

Figure 38: Bare-bone real SFS experiments

Pugs的实验(图38的说明):

  • 在内核窗口/ proc / filesystems中检查了内核支持的文件系统
  • 加载了real_sfs_bb.ko驱动程序
  • 重新检查内核窗口/ proc / filesystems以获取内核支持的文件系统。现在,它显示在末尾列出的sfs
  • 一个没有安装他的笔式驱动器分区/ dev / sdb1到/ mnt使用SFS文件系统。检查相邻窗口上的dmesg日志。(请记住,现在,sfs_fill_super()并未真正读取分区,因此未进行任何检查。因此,/ dev / sdb1的格式实际上并不重要。)但是,是的,所述安装件,它是使用安装输出显示SFS文件系统

糟糕!但是df输出显示“未实现功能”,cd显示“不是目录”。啊哈!Pugs尚未在其他四个功能指针结构中的任何一个中实现任何其他功能。因此,这是预期的。

注意:以上实验均使用“ sudo”。取而代之的是,可以进入root shell并在没有“ sudo”的情况下执行相同的操作

好的,因此没有内核崩溃,也没有运行裸机文件系统– Yippee。!!哈巴狗知道dfcd,…尚未起作用。为此,他需要开始在其他(四个)函数指针结构中添加各种系统调用,以便能够使用各种shell命令像使用所有其他文件系统一样进行冷酷浏览。是的,Pugs已经开始执行他的任务了-毕竟他需要为他的最后一个学期项目制作一个怪异的演示。

 

 

第二部分:块设备上的文件系统

这第二十三篇文章是Linux设备驱动程序系列的一部分,它增强了先前编写的裸机文件系统模块,以与真实的硬件分区连接

https://sysplay.in/blog/linux-device-drivers/2014/12/the-semester-project-part-vi-file-system-on-block-device/


自从上一个裸露的文件系统以来,Pugs弄清楚的第一件事就是如何从底层的块设备读取数据。以下是执行此操作的典型方法:

struct buffer_head *bh;

bh = sb_bread(sb, block); /* sb is the struct super_block pointer */
// bh->b_data contains the data
// Once done, bh should be released using:
brelse(bh);

为了完成上述操作以及其他各种实际的SFS(Simula文件系统)操作,Pugs认为需要将自己的句柄作为关键参数,他在以下内容中添加了此内容(在先前的real_sfs_ds.h中):

typedef struct sfs_info
{
	struct super_block *vfs_sb; /* Super block structure from VFS for this fs */
	sfs_super_block_t sb; /* Our fs super block */
	byte1_t *used_blocks; /* Used blocks tracker */
} sfs_info_t;

其背后的主要思想是将所有必需的静态全局变量放在单个结构中,并通过文件系统的私有数据指针(即结构super_block结构中的s_fs_info指针)来指出。这样,fill_super_block()(在先前的real_sfs_bb.c文件中)的密钥更改变为:

  • 使用kzalloc()为句柄分配结构
  • 初始化句柄的结构(通过init_browsing()
  • 读取物理超级块,验证并将其信息转换为VFS超级块(通过init_browsing()
  • s_fs_info指向它(通过init_browsing()
  • 相应地更新VFS超级块

因此,错误处理代码将需要执行shut_browsing(info)kfree(info)。并且,还需要与在umount期间调用的kill_block_super在先前的real_sfs_bb.c中定义的kill_sb函数指针相对应的函数一起使用。

以下是各种代码段:

#include <linux/slab.h> /* For kzalloc, ... */
...
static int sfs_fill_super(struct super_block *sb, void *data, int silent)
{
	sfs_info_t *info;

	if (!(info = (sfs_info_t *)(kzalloc(sizeof(sfs_info_t), GFP_KERNEL))))
		return -ENOMEM;
	info->vfs_sb = sb;
	if (init_browsing(info) < 0)
	{
		kfree(info);
		return -EIO;
	}
	/* Updating the VFS super_block */
	sb->s_magic = info->sb.type;
	sb->s_blocksize = info->sb.block_size;
	sb->s_blocksize_bits = get_bit_pos(info->sb.block_size);

	...
}

static void sfs_kill_sb(struct super_block *sb)
{
	sfs_info_t *info = (sfs_info_t *)(sb->s_fs_info);

	kill_block_super(sb);
	if (info)
	{
		shut_browsing(info);
		kfree(info);
	}
}

为了完成上述操作以及其他各种实际的SFS(Simula文件系统)操作,Pugs认为需要将自己的句柄作为关键参数,他在以下内容中添加了此内容(在先前的real_sfs_ds.h中):

static int get_bit_pos(unsigned int val)
{
	int i;

	for (i = 0; val; i++)
	{
		val >>= 1;
	}
	return (i - 1);
}

init_browsing() shut_browsing()基本上都是早期用户空间的功能转换browse_real_sfs.c到内核空间代码real_sfs_ops.c,原型在real_sfs_ops.h。这基本上涉及以下转换:

  • 将“ int sfs_handle ”转换为“ sfs_info_t * info ”
  • lseek()read()使用sb_bread()从块设备读取
  • 将calloc()转换为vmalloc(),然后通过零进行适当的初始化。
  • 将free()转换为vfree()

这里的转化init_browsing()shut_browsing()real_sfs_ops.c

#include <linux/fs.h> /* For struct super_block */
#include <linux/errno.h> /* For error codes */
#include <linux/vmalloc.h> /* For vmalloc, ... */

#include "real_sfs_ds.h"
#include "real_sfs_ops.h"

int init_browsing(sfs_info_t *info)
{
	byte1_t *used_blocks;
	int i, j;
	sfs_file_entry_t fe;
	int retval;

	if ((retval = read_sb_from_real_sfs(info, &info->sb)) < 0)
	{
		return retval;
	}
	if (info->sb.type != SIMULA_FS_TYPE)
	{
		printk(KERN_ERR "Invalid SFS detected. Giving up.\n");
		return -EINVAL;
	}

	/* Mark used blocks */
	used_blocks = (byte1_t *)(vmalloc(info->sb.partition_size));
	if (!used_blocks)
	{
		return -ENOMEM;
	}
	for (i = 0; i < info->sb.data_block_start; i++)
	{
		used_blocks[i] = 1;
	}
	for (; i < info->sb.partition_size; i++)
	{
		used_blocks[i] = 0;
	}

	for (i = 0; i < info->sb.entry_count; i++)
	{
		if ((retval = read_from_real_sfs(info,
					info->sb.entry_table_block_start,
					i * sizeof(sfs_file_entry_t),
					&fe, sizeof(sfs_file_entry_t))) < 0)
		{
			vfree(used_blocks);
			return retval;
		}
		if (!fe.name[0]) continue;
		for (j = 0; j < SIMULA_FS_DATA_BLOCK_CNT; j++)
		{
			if (fe.blocks[j] == 0) break;
			used_blocks[fe.blocks[j]] = 1;
		}
	}

	info->used_blocks = used_blocks;
	info->vfs_sb->s_fs_info = info;
	return 0;
}
void shut_browsing(sfs_info_t *info)
{
	if (info->used_blocks)
		vfree(info->used_blocks);
}

同样,browse_real_sfs.c中的所有其他函数也必须一一转换。另外,请注意,来自底层块设备的读取被两个函数(即read_sb_from_real_sfs()read_from_real_sfs())捕获,它们的编码如下:

#include <linux/buffer_head.h> /* struct buffer_head, sb_bread, ... */
#include <linux/string.h> /* For memcpy */

#include "real_sfs_ds.h"

static int read_sb_from_real_sfs(sfs_info_t *info, sfs_super_block_t *sb)
{
	struct buffer_head *bh;

	if (!(bh = sb_bread(info->vfs_sb, 0 /* Super block is the 0th block */)))
	{
		return -EIO;
	}
	memcpy(sb, bh->b_data, SIMULA_FS_BLOCK_SIZE);
	brelse(bh);
	return 0;
}
static int read_from_real_sfs(sfs_info_t *info, byte4_t block,
				byte4_t offset, void *buf, byte4_t len)
{
	byte4_t block_size = info->sb.block_size;
	byte4_t bd_block_size = info->vfs_sb->s_bdev->bd_block_size;
	byte4_t abs;
	struct buffer_head *bh;

	/*
	 * Translating the real SFS block numbering to underlying block device
	 * block numbering, for sb_bread()
	 */
	abs = block * block_size + offset;
	block = abs / bd_block_size;
	offset = abs % bd_block_size;
	if (offset + len > bd_block_size) // Should never happen
	{
		return -EINVAL;
	}
	if (!(bh = sb_bread(info->vfs_sb, block)))
	{
		return -EIO;
	}
	memcpy(buf, bh->b_data + offset, len);
	brelse(bh);
	return 0;
}

以上所有代码段都作为real_sfs_minimal.c(基于先前创建的文件real_sfs_bb.c),real_sfs_ops.creal_sfs_ds.h(基于先前创建的同一文件),real_sfs_ops.h和支持的Makefile放在一起,以及以前创建的format_real_sfs.c应用程序,可从rsfs_on_block_device_code.tbz2获得

 

块设备上的真实SFS

一旦使用make编译并获得real_sfs_first.ko驱动程序,Pugs就不会期望它与以前的real_sfs_bb.ko驱动程序有所不同,但至少现在它应该正在读取并验证基础分区。为此,他首先尝试安装笔式驱动器的常规分区,以便在dmesg输出中获得“检测到无效的SFS”消息。然后格式化。请注意,存在与上一篇文章相同的“ Not a directory”错误,但仍然存在–无论如何它仍与以前的裸机驱动程序非常相似–尚未实现的核心功能–只是现在已经存在阻止设备分区。图39显示了所有这些步骤的确切命令。

注意: “ ./ format_real_sfs”和“ mount”命令可能会花费很多时间(可能以分钟为单位),具体取决于分区大小。因此,最好使用小于1MB的分区。

通过使文件系统模块与基础块设备进行交互这一重要步骤,Pugs的最后一步将是执行来自browser_real_sfs.c的其他转换,并相应地在SFS模块中使用它们。

 

第三部分:运行中的文件系统

这是第二十四篇文章,是Linux设备驱动程序系列的一部分,它使完整的真实SIMULA文件系统模块发挥作用,并且在笔式驱动器上具有真实的硬件分区。

https://sysplay.in/blog/linux-device-drivers/2015/01/the-semester-project-part-vii-file-system-in-action/


实际的SFS运作中

rsfs_in_action_code.tbz2中提供的代码到达Pugs&Shweta的最后一个学期项目的最终测试实现。其中包含以下内容:

  • real_sfs.c –包含早期real_sfs_minimal.c的代码以及其余的实际SIMULA文件系统功能。注意文件系统的名称变更从SFSreal_sfs
  • real_sfs_ops.creal_sfs_ops.h –包含早期代码以及增强的real_sfs.c实现所需的其他操作
  • real_sfs_ds.h(与上一篇文章几乎相同的文件,并在真实SFS信息结构中添加了自旋锁,用于防止在访问相同结构中的used_blocks数组时出现竞争状况)
  • format_real_sfs.c(与先前文章中的文件相同)–真正的SFS格式化程序
  • Makefile –包含使用real_sfs _ *。*文件构建驱动程序real_sfs_final.ko的规则,以及使用format_real_sfs.c构建format_real_sfs应用程序的规则

利用所有这些以及更早的细节,Shweta完成了他们的项目文档。因此,最后,Shweta&Pugs都准备好进行其最后一个学期的项目演示,演示和现场直播。

其演示的重点(在root shell上)如下:

  • 加载real_sfs_final驱动程序:insmod real_sfs_final.ko
  • 使用先前格式化的笔驱动器分区/ dev / sdb1或使用format_real_sfs应用程序重新格式化它:./format_real_sfs / dev / sdb1警告: 在实际格式化之前,请查看上一篇文章的完整详细步骤。
  • 挂载真正的SFS格式化分区:mount -t real_sfs / dev / sdb1 / mnt
  • 然后……又是什么?浏览安装文件系统。使用通常的shell命令在文件系统上进行操作:lscdtouchvirmchmod …

图40显示了实际的SIMULA文件系统

图40:实际的SIMULA文件系统模块正在运行

 

运行背后的实现

而且,如果您真的想知道,Pugs对前一章的代码进行了哪些额外的增强,使其达到了这一水平,那么基本上是以下核心系统调用,它们是功能指针结构的5组中其余4组的一部分(在real_sfs.c中):

  1. write_inode(在struct super_operations下)– sfs_write_inode()基本获取指向VFS的inode缓存中的inode的指针,并有望使其与物理硬件空间文件系统中的inode同步。这可以通过调用经过适当修改的sfs_update()(在real_sfs_ops.c中定义)来实现(改编自先前的browser_real_sfs应用程序)。关键参数的变化是通过传递索引节点号而不是文件名和实际时间戳而不是其更新状态的标志。因此,对基于文件名的sfs_lookup()的调用将替换为基于inode编号的sfs_get_file_entry()(在real_sfs_ops.c中定义),此外,如果文件大小减小,则现在还将释放数据块(使用sfs_put_data_block()(在real_sfs_ops.c中定义))。请注意,sfs_put_data_block()(在real_sfs_ops.c中定义)是对browser_real_sfs应用程序中的put_data_block()转换。另外,还要注意的有趣映射/从VFS inode编号从/到我们从零开始的文件条目索引,使用宏S2V_INODE_NUM() / V2S_INODE_NUM()real_sfs_ops.h
    最后,使用write_to_real_sfs()实现底层写入,该函数添加在real_sfs_ops.c中,与read_from_real_sfs()(已经在real_sfs_ops.c中),除了数据传输的方向反转和将缓冲区标记为脏以与物理内容同步。同时,在real_sfs_ops.c中编写了两个包装函数read_entry_from_real_sfs()(使用read_from_real_sfs())和write_entry_to_real_sfs()(使用write_to_real_sfs()),分别用于增加读写文件条目的特定要求,代码可读性。sfs_write_inode()sfs_update()显示在下面的代码片段中。sfs_write_inode()已写入文件real_sfs.c中。对于其他文件,请检查文件real_sfs_ops.c
#if (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,34))
static int sfs_write_inode(struct inode *inode, int do_sync)
#else
static int sfs_write_inode(struct inode *inode, struct writeback_control *wbc)
#endif
{
	sfs_info_t *info = (sfs_info_t *)(inode->i_sb->s_fs_info);
	int size, timestamp, perms;

	printk(KERN_INFO "sfs: sfs_write_inode (i_ino = %ld)\n", inode->i_ino);

	if (!(S_ISREG(inode->i_mode))) // Real SFS deals only with regular files
		return 0;

	size = i_size_read(inode);
	timestamp = inode->i_mtime.tv_sec > inode->i_ctime.tv_sec ?
			inode->i_mtime.tv_sec : inode->i_ctime.tv_sec;
	perms = 0;
	perms |= (inode->i_mode & (S_IRUSR | S_IRGRP | S_IROTH)) ? 4 : 0;
	perms |= (inode->i_mode & (S_IWUSR | S_IWGRP | S_IWOTH)) ? 2 : 0;
	perms |= (inode->i_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) ? 1 : 0;

	printk(KERN_INFO "sfs: sfs_write_inode with %d bytes @ %d secs w/ %o\n",
		size, timestamp, perms);

	return sfs_update(info, inode->i_ino, &size, &timestamp, &perms);
}

int sfs_update(sfs_info_t *info, int vfs_ino, int *size, int *timestamp, int *perms)
{
	sfs_file_entry_t fe; 
	int i;
	int retval;

	if ((retval = sfs_get_file_entry(info, vfs_ino, &fe)) < 0) 
	{   
		return retval; 
	}   
	if (size) fe.size = *size;
	if (timestamp) fe.timestamp = *timestamp;
	if (perms && (*perms <= 07)) fe.perms = *perms;

	for (i = (fe.size + info->sb.block_size - 1) / info->sb.block_size;
		i < SIMULA_FS_DATA_BLOCK_CNT; i++)
	{   
		if (fe.blocks[i])
		{   
			sfs_put_data_block(info, fe.blocks[i]);
			fe.blocks[i] = 0;
		}   
	}   

	return write_entry_to_real_sfs(info, V2S_INODE_NUM(vfs_ino), &fe);
}
  1. createunlinklookup(在struct inode_operations下)–所有3个函数sfs_inode_create()sfs_inode_unlink()sfs_inode_lookup()都有2个通用参数(父级的inode指针和所考虑文件的目录条目指针),它们分别创建,删除和查找与由其参数(例如dentry)指向的目录条目相对应的索引节点。
    sfs_inode_lookup()基本上使用经过适当修改的sfs_lookup()(在real_sfs_ops.c中定义)来搜索文件名是否存在。)(改编自早期的browser_real_sfs应用程序-采用的做法是将用户空间lseek() + read()组合替换为read_entry_from_real_sfs())。如果找不到filename,则将其调用通用内核函数d_splice_alias()在基础文件系统中创建一个新的inode条目,然后将其附加到dentry指向的目录条目。否则,它只是从VFS的索引节点缓存附加索引节点(使用通用内核函数d_add())。如果获得,则此inode(I_NEW),则需要使用实际的SFS查找文件属性来填充。在上述所有实现以及以后的实现中,已经做出了一些基本假设,即:

     

    • Real SFS仅针对用户维护模式,该模式映射到用户,组,其他VFS索引节点的所有3个
    • Real SFS仅维护一个时间戳,该时间戳映射到VFS索引节点的所有3个创建,修改,访问的时间。

    sfs_inode_create()sfs_inode_unlink()分别调用转换后的sfs_create()sfs_remove()(在real_sfs_ops.c中定义)(改编自早期的browser_real_sfs应用程序),分别在底层硬件空间文件中创建和清除inode条目除了通常的inode缓存操作之外,还可以使用new_inode() + insert_inode_locked()d_instantiate()inode_dec_link_count(),而不是之前学习过的iget_locked()和d_add()。除了权限和文件输入参数外,并替换lseek() +阅读()通过组合read_entry_from_real_sfs() sfs_create()具有从用户空间到内核空间一个有趣的转变:时间(NULL)get_seconds() 。在sfs_create()sfs_remove()中,用户空间lseek() + write()组合已由明显的write_entry_to_real_sfs()取代。如上所述,在文件real_sfs.creal_sfs_ops.c中检出上述所有代码段。

  2. readpagewrite_beginwritepagewrite_end(在struct address_space_operations下)–所有这些地址空间操作基本上都是在底层文件系统上读写块,并使用相应的通用内核函数mpage_readpage()block_write_begin()block_write_full_page()来实现。generic_write_end()。第一个在<linux / mpage.h>中原型化,其余3个在<linux / buffer_head.h>中原型化。。现在,尽管这些功能已经足够通用了,但稍加思考,就会发现其中的前三个最终将必须使用相应的块层API与基础块设备(硬件分区)进行真正的SFS特定事务。而这正是通过真正的SFS特定功能sfs_get_block()来实现的,该功能已被上述前三个功能传递并使用。
    调用sfs_get_block()(在real_sfs.c中定义)以将文件(由inode表示)的特定块号(iblock)读入缓冲区头(bh_result)),也可以选择提取(分配)新块。因此,为此,将查找对应的实际SFS inode的块数组,然后使用内核API map_bh()获取物理分区的对应块。再次注意,要获取一个新块,我们调用sfs_get_data_block()(在real_sfs_ops.c中定义),这也是对browser_real_sfs应用程序get_data_block()转换。同样,在分配新块的情况下,还使用sfs_update_file_entry()real_sfs_ops.c中的一个线性实现在下面更新了真实的SFS inode 。下面的代码段显示了sfs_get_block() 实施。
static int sfs_get_block(struct inode *inode, sector_t iblock,
				struct buffer_head *bh_result, int create)
{
	struct super_block *sb = inode->i_sb;
	sfs_info_t *info = (sfs_info_t *)(sb->s_fs_info);
	sfs_file_entry_t fe;
	sector_t phys;
	int retval;

	printk(KERN_INFO "sfs: sfs_get_block called for I: %ld, B: %llu, C: %d\n",
		inode->i_ino, (unsigned long long)(iblock), create);

	if (iblock >= SIMULA_FS_DATA_BLOCK_CNT)
	{
		return -ENOSPC;
	}
	if ((retval = sfs_get_file_entry(info, inode->i_ino, &fe)) < 0)
	{
		return retval;
	}
	if (!fe.blocks[iblock])
	{
		if (!create)
		{
			return -EIO;
		}
		else
		{
			if ((fe.blocks[iblock] = sfs_get_data_block(info)) ==
				INV_BLOCK)
			{
				return -ENOSPC;
			}
			if ((retval = sfs_update_file_entry(info, inode->i_ino, &fe))
				< 0) 
			{   
				return retval;
			}
		}
	}
	phys = fe.blocks[iblock];
	map_bh(bh_result, sb, phys);

	return 0;
}
  1. openreleasereadwriteaio_read / read_iter(从内核v3.16开始),aio_write / write_iter(从内核v3.16开始),fsync(在常规文件的struct file_operations下)–所有这些操作基本上应该经过缓冲区高速缓存,即应使用地址空间操作来实现。这是一个常见的要求,内核提供了一组通用的内核API,即generic_file_open()do_sync_read() / new_sync_read()(自内核v3.16起),do_sync_write() /new_sync_write()(自内核v3.16起),generic_file_aio_read() / generic_file_read_iter()(自内核v3.16起),generic_file_aio_write() / generic_file_write_iter()(自内核v3.16起),simple_sync_file () / noop_fsync()(自起内核v2.6.35)。此外,地址空间操作读取写入不再需要因为内核V4.1给予。请注意,没有用于发布的API,因为它是一个“ return 0 ” API。real_sfs.c文件以获取struct file_operations sfs_fops的确切定义。
  2. readdir / iterate(从内核v3.11开始)(在目录的struct file_operations下)– sfs_readdir() / sfs_iterate()主要读取基础目录的目录条目(由文件表示),并将其填充到VFS目录条目缓存中(由dirent参数指向)(使用参数函数filldir)或进入目录上下文(由ctx参数指向)(自内核v3.11起)。
    由于实际的SFS只有一个目录,而顶层只有一个目录,因此此函数基本上使用转换后的sfs_list()将目录文件高速缓存用基础文件系统中所有文件的目录条目填充(在real_sfs_ops.c中定义),改编自browser_real_sfs应用程序。请检查real_sfs.c文件以获取完整的sfs_readdir() / sfs_iterate()实现,该实现以填充目录条目开头“(当前目录)和” .. “使用参数的函数(父目录)filldir() ,或通用内核函数dir_emit_dots() (因为内核V3.11),然后为真正SFS的所有文件,使用sfs_list( )
  3. put_super(下结构super_operations) -以前的自定义实现sfs_kill_sb() (由指向kill_sb)已得到增强,通过它分离成定制部分被放入sfs_put_super() (现在被指出put_super),以及标准kill_block_super()的存在由kill_sb直接指向。出文件real_sfs.c进行所有这些更改。

完成所有这些操作后,就可以看到Pugs进行的惊人演示,如图40所示。不要忘了使用' tail -f / var / log /来观看/ var / log / messages的实时日志。messages ',将其与您在已安装的实际SFS文件系统上发出的每个命令相匹配。这将使您最好地了解何时调用哪个系统调用。或者,换句话说,哪个应用程序从文件系统前端调用哪个系统调用。为了跟踪由应用程序/命令调用的所有系统调用,请在命令中使用strace,例如,键入“ strace ls ”而不是“ ls ”。

 

笔记

  1. 在像Ubuntu这样的发行版中,您可能会在/ var / log / syslog下找到日志,而不是/ var / log / messages下

 

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页