块设备 CACHE 管理
由于磁盘属于低速设备,磁盘的读写速度远远低于 CPU,所以为了解决这种速度不匹配的问题,SylixOS 提供了对应块设备的缓冲器。它是一个特殊的块设备,与物理设备相对应(多个逻辑分区共享一个 CACHE),介于文件系统和磁盘之间,可以极大地减少磁盘 I/O 的访问率,同时提高系统性能。当然引入磁盘缓冲器的最大问题在于磁盘数据与缓冲数据的不同步性,这个问题可以通过调用 sync 等函数来解决。
SylixOS 对块设备 CACHE 管理的代码在”libsylixos/SylixOS/fs/diskCache”目录下,主要包含磁盘缓冲器的创建、删除和同步等操作。
块设备 CACHE 的创建
创建磁盘 CACHE 块设备的函数原型如下:
#include <SylixOS.h>
ULONG API_DiskCacheCreateEx2(PLW_BLK_DEV pblkdDisk,
PLW_DISKCACHE_ATTR pdcattrl,
LW_BLK_DEV *ppblkDiskCache);
函数 API_DiskCacheCreateEx2 原型分析:
- 此函数成功返回 ERROR_NONE ,失败返回错误码。
- 参数 pblkDisk 是需要 CACHE 的块设备。
- 参数 pdcattrl 是磁盘 CACHE 描述信息。
- 参数 ppblkDiskCache 是创建出的CACHE块设备。
使用 API_DiskCacheCreateEx2 函数创建CACHE块设备时,上层可通过 LW_DISKCACHE_ATTR 结构体传递磁盘 CACHE 的描述信息,该结构体的详细描述如下:
typedef struct {
PVOID DCATTR_pvCacheMem; /* 扇区缓存地址 */
size_t DCATTR_stMemSize; /* 扇区缓存大小 */
BOOL DCATTR_bCacheCoherence; /* 缓冲区需要 CACHE 一致性 */
INT DCATTR_iMaxRBurstSector; /* 磁盘猝发读的最大扇区数 */
INT DCATTR_iMaxWBurstSector; /* 磁盘猝发写的最大扇区数 */
INT DCATTR_iMsgCount; /* 管线消息队列缓存个数 */
INT DCATTR_iPipeline; /* 处理管线线程数量 */
BOOL DCATTR_bParallel; /* 是否支持并行读写 */
ULONG DCATTR_ulReserved[8]; /* 保留 */
} LW_DISKCACHE_ATTR;
typedef LW_DISKCACHE_ATTR *PLW_DISKCACHE_ATTR;
SylixOS 要求处理管线线程数量最小为 0,即不使用管线并发技术,最大为 LW_CFG_DISKCACHE_MAX_PIPELINE ,该值目前定义为 4,即最多支持 4 个处理管线线程。而管线消息队列缓存的数量不能小于处理管线线程数量,一般为其 2~8 倍。
除了函数 API_DiskCacheCreateEx2 之外,SylixOS 还提供了另外两个 CACHE 块设备创建函数,即 API_DiskCacheCreateEx 函数和 API_DiskCacheCreate 函数。API_DiskCacheCreateEx 函数是在 API_DiskCacheCreateEx2 之上默认配置了缓冲区不需要 CACHE 一致性保障、不支持并行读写、4 个管线消息队列缓存、1 个处理管线线程。而 API_DiskCacheCreate 函数则是在 API_DiskCacheCreateEx 之上默认配置了磁盘读猝发长度比写少一半。
每个磁盘 CACHE 块设备都由一个控制块管理,该控制块包含了各种控制信息,其详细描述如下:
typedef struct {
LW_BLK_DEV DISKC_blkdCache; /* DISK CACHE的控制块 */
PLW_BLK_DEV DISKC_pblkdDisk; /* 被缓冲 BLK IO 控制块地址 */
LW_LIST_LINE DISKC_lineManage; /* 背景线程管理链表 */
LW_OBJECT_HANDLE DISKC_hDiskCacheLock; /* DISK CACHE 操作锁 */
INT DISKC_iCacheOpt; /* CACHE 工作选项 */
ULONG DISKC_ulEndStector; /* 最后一个扇区的编号 */
ULONG DISKC_ulBytesPerSector; /* 每一扇区字节数量 */
ULONG DISKC_ulValidCounter; /* 有效的扇区数量 */
ULONG DISKC_ulDirtyCounter; /* 需要回写的扇区数量 */
INT DISKC_iMaxRBurstSector; /* 最大猝发读扇区数量 */
INT DISKC_iMaxWBurstSector; /* 最大猝发写扇区数量 */
LW_DISKCACHE_WP DISKC_wpWrite; /* 并发写管线 */
PLW_LIST_RING DISKC_pringLruHeader; /* LRU 表头 */
PLW_LIST_LINE *DISKC_pplineHash; /* HASH 表池 */
INT DISKC_iHashSize; /* HASH 表大小 */
ULONG DISKC_ulNCacheNode; /* CACHE 缓冲的节点数 */
caddr_t DISKC_pcCacheNodeMem; /* CACHE 节点链表首地址 */
caddr_t DISKC_pcCacheMem; /* CACHE 缓冲区 */
PLW_DISKCACHE_NODE DISKC_disknLuck; /* 幸运扇区节点 */
VOIDFUNCPTR DISKC_pfuncFsCallback; /* 文件系统回调函数 */
PVOID DISKC_pvFsArg; /* 文件系统回调参数 */
} LW_DISKCACHE_CB;
typedef LW_DISKCACHE_CB *PLW_DISKCACHE_CB;
块设备 CACHE 创建的过程主要包括创建背景回写线程、初始化磁盘、创建并发写队列、开辟 HASH 表和缓冲内存池、开辟初始化控制块等。
- 背景回写线程
磁盘高速缓冲控制器背景回写线程会定期检查所有加入背景线程的磁盘缓冲,随机回写一定数量的磁盘缓冲,目前系统默认配置为 128 个扇区。
- 并发写队列
如果磁盘 CACHE 块设备支持管线并发技术,则会创建多个写线程,线程中循环等待写事务消息,执行具体的写盘操作,详细内容将在下一小节讲解。
- HASH 表和缓冲内存池
SyilxOS 以 NODE 节点的方式管理 CACHE 缓存,每一个 NODE 节点都对应着一个扇区缓存,NODE 节点则以 LRU(Least Recently Used)的方式组成循环链表,方便查找空闲节点和刷新脏节点。对于已经缓存的扇区,则通过 HASH 表与链表结合的方式进行快速索引。
- 开辟初始化控制块
控制块中包含了CACHE块设备的各种控制信息,也包括 BLKIO控制块。所有文件系统、逻辑磁盘的读、写、I/O控制操作都会调用 CACHE 块设备的相应操作函数。
管理CACHE缓存的 NODE 节点详细信息如下:
typedef struct {
LW_LIST_RING DISKN_ringLru; /* LRU 表节点 */
LW_LIST_LINE DISKN_lineHash; /* HASH 表节点 */
ULONG DISKN_ulSectorNo; /* 缓冲的扇区号 */
INT DISKN_iStatus; /* 节点状态 */
caddr_t DISKN_pcData; /* 扇区数据缓冲 */
UINT64 DISKN_u64FsKey; /* 文件系统自定义数据 */
} LW_DISKCACHE_NODE;
typedef LW_DISKCACHE_NODE *PLW_DISKCACHE_NODE;
关于 NODE 节点以及 HASH 表的具体操作方法,将在后续的 CACHE 块设备的同步、读写操作中进行详细的介绍。
块设备 CACHE 的同步
磁盘 CACHE 块设备的同步即回写缓存中的脏页到存储介质。SylixOS 的 CACHE 管理机制存在多种回写方式,包括指定扇区范围回写( diskCacheFlushInvRange 函数),指定关键区回写(diskCacheFlushInvMeta 函数)和指定扇区数量回写(__diskCacheFlushInvCnt 函数)。
SylixOS 以 LRU 管理 CACHE 缓存,当对某一页缓存进行了读写操作后,需要重新确定 LRU 表位置,将 NODE 节点插入链表头部,脏页回写后的空闲页则插入 LRU 表的尾部。这样最近使用的缓存放在最前面,不常使用的逐渐移到最后,而空闲的节点放在了链表最尾部。每次需要查找空闲节点或回写脏页时,只需要从链表尾部向前查找即可。
若需要回写指定扇区数量的缓存,只需要从 LRU 表的尾部向前查找指定数量的节点,将其中的脏节点加入临时链表,然后回写该链表上的所有节点即可。在插入临时链表时,以缓冲扇区号升序排列,方便后续回写时合并连续扇区。关于具体的回写方式将在后续章节的磁盘高速传输中详细介绍,本节不做过多介绍。完成回写后,空闲节点需要重新插入 LRU 表尾部,方便空闲节点申请。
块设备 CACHE 的读写操作
当文件系统需要进行读写操作时,最终会调用到磁盘对应的 CACHE 块设备读写操作。此时,需要先获取一个经过指定处理的 CACHE 节点。首先根据扇区号从有效的 HASH 表中查找对应节点,若找到则直接命中,说明之前对该扇区进行过缓存,否则需要开辟新的节点。开辟新节点时,从 LRU 表的结尾向前查找,将找到的节点置为有效,并加入到相关的 HASH 表中。若所有 CACHE 全部为脏节点,或无法找到合适的控制块,则需要做 FLUSH 操作,回写一些扇区,之后重新查找。
查找到有效节点后,读与写将有不同的操作。读操作时,若直接从 HASH 表中命中,则只需将 CACHE 缓存中的数据拷贝到上层缓冲即可。若不是直接命中,系统不仅会从存储介质中读出上层申请的扇区数据,还会根据设置的猝发读扇区数量和当前空闲节点数量进行预读,这样可以大大提高后续读操作的命中率。
若为写操作,在查找到有效节点后,需要将该节点置为脏节点,并将上层缓冲数据拷贝到 CACHE 缓存中。
除了以上的操作之外,无论是读操作还是写操作,都需要将操作后的节点设置为幸运节点,并重新确定 LRU 表位置,将该节点插入到链表头。
块设备 CACHE 的删除
磁盘 CACHE 块设备删除的函数原型如下:
#include <SylixOS.h>
INT API_DiskCacheDelete (PLW_BLK_DEV pblkdDiskCache);
函数 API_DiskCacheDelete 原型分析:
- 此函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。
- 参数 pblkDiskCache 是磁盘CACHE的块设备。
调用函数 API_DiskCacheDelete 删除磁盘之前首先要使用 remove 函数卸载卷,删除时需要等待所有写操作完成,然后释放所有申请的资源。