这一节开始我们来学习文件系统。是的,你没看错,熟悉一点的读者一看目录就知道,本文的出处了。本文摘自陈智育,温彦军,陈琪等主编,人民邮电出版社出版的书籍《VxWorks程序开发实践》一书。当然了,还会有一些出自网络和其他文本资料的信息,如WindRiver官方出的Tornado Training Workshop和官方给出的BSP开发手册等,我们不能为了写而学,为了写而写,现在要先学再写,先学理解再总结。
从某种意义上说,文件系统是IO系统的一部分,或者说是IO系统的扩展。先来看一个VxWorks的系统组件图
可以看到,I/O系统直接对上应用程序提供接口,对下管理文件系统并可以直接调用BSP的相关程序(应用通过IO系统的标准接口访问文件系统;文件系统和字符设备一样挂接在IO系统上;块设备通过文件系统被访问,而不是直接归IO系统管理;同样块设备可以选择不同的文件系统来管理,如dosFs或rawFs)。文件系统管理SCSI驱动程序。文件系统和字符设备驱动一样是一个驱动程序,用于辅助块设备的管理。
这个图更加清晰的说明了应用程序通过IO系统访问文件系统,通过文件系统管理块设备。下图显示了应用代码访问块设备的一个函数流程:
该图来自WindRiver的Workshop。可以看到文件系统在其中大概的位置。接下来的两个表可以列出了VxWorks中实际的文件系统相关函数库的层次关系。
表1: 简单RAM文件系统相关库层次表
层次 | 向下接口 |
---|---|
应用模块 | read、write、ioctl 等 |
VxWorks IO 系统 | 挂接入IO系统的文件系统接口 |
文件系统 (dosFs/rawFs) | CBIO API (cbioBlkRw、 cbioIoctl 等) |
CBIO API 设备驱动模块(ramDiskCbio.c) | |
RAM 硬件 |
表2: 本地存储盘上文件系统相关库层次表
层次 | 向下接口 |
---|---|
应用模块 | read、write、ioctl 等 |
VxWorks IO 系统 | 挂接入IO系统的文件系统接口 |
文件系统 (dosFs/rawFs) | CBIO API 接口(cbioLib.h) |
CBIO 到 CBIO 设备(dpartCbio) | CBIO API 接口 |
CBIO 到 CBIO 设备(dcacheCbio) | CBIO API 接口 |
基本CBIO 到 BLK_DEV 封装设备(cbioLib) | BLK_DEV (blkIo.h) |
BLK_DEV 设备驱动(fdDrv、 ataDrv 和 tffsDrv 等) | |
存储设备硬件 |
看到这两个表,不知道你是否对上一节SD卡驱动程序中最后一个接口函数中关于 CBIO 等操作的印象。新版的dosFs 和 rawFs 都需要一个新抽象层 CBIO的支持, 通过 CBIO再同块设备连接, CBIO[Cached Block Input Outpout]用于块设备缓冲管理 。对于不同的情况,VxWorks提供多种CBIO库支持,但都遵循类似的接口标准。对于RAM设备,缓存没有必要, ramDiskCbio 用于仿真实现CBIO接口,所以和普通存储设备的实现层次有所区别 。如果设备需要多分区管理,可以使用 dpartCbio 库,对下层的CBIO再次抽象。
ioLib
IO系统提供7个标准接口用于文件系统操作(分别是 create, remove, open, close, read, write, ioctl–对文件系统执行特殊的控制功能)。实际中,这些函数多用文件描述符来对文件进行操作,除了标准的输入、输出、错误输出,VxWorks的文件描述符是全局的,可以被任何一个任务存取。具体文件得到的描述符都是大于2的。
ioctl是文件系统的灵活接口,用于标准IO接口规定外的功能实现。ioctl的功能实现是分层的,如FIOGETNAME在IO系统实现,而FIORENAME在文件系统实现。上层没有实现再向下层传递,若各层都没有该功能的实现,则返回错误。功能代码统一在ioLib.h中定义。
除此之外,ioLib还提供的与文件系统相关的接口有:
// 第3参数 mode 值在 ioLib.h 中定义
int fd = open(name, O_RDWR| O_CREAT, FSTAT_DIR | DEFAULT_DIR_PERM);
remove(name);
dirLib
VxWorks提供dirLib库来实现目录文件列表读取的功能。 dirLib建立在ioLib之上,接口如下:
接口 | 说明 |
---|---|
DIR *opendir(char *name) | 打开目录 |
struct dirent *readdir(DIR *pDir) | 读取目录 |
void rewinddir(DIR *pDir) | 使目录读取位置从头开始,和刚打开目录时一样 |
STATUS closedir(DIR *pDir) | 关闭目录 |
// 两个相关结构在 中定义
struct dirent // 目录条目 dirent
{
/* NAME_MAX = _PARM_NAME_MAX = 99, 在 limits.h和vxParams.h 中定义 */
char d_name[NAME_MAX + 1];
};
typedef struct
{
int dd_fd; // 打开目录的文件描述符
int dd_cookie; // 具体文件系统相关的目录位置标志
struct dirent dd_dirent; // 取得的目录条目
} DIR; // 目录描述符DIR
dirLib还提供用于获取文件或文件系统状态信息的接口,如表:
接口 | 说明 |
---|---|
fstat | 读取文件状态信息(用文件描述符) |
stat | 读取文件状态信息(用文件名) |
fstatfs | 读取文件系统状态信息(用文件描述符) |
statfs | 读取文件系统状态信息(用文件名) |
utime | 更新文件时标(用文件名) |
// stat, statfs在中定义, utimbuf在utime.h中定义
typedef unsigned long time_t;
struct utimbuf
{
time_t actime; // 设置访问时间
time_t modtime; // 设置修改时间
};
上面几个函数的用法可以参考 “\target\src\usr\usrFsLib.c”文件。
usrFsLib
VxWorks在“\target\src\usr\”目录下提供usrFsLib库在ioLib和dirLib之上做了更实用的抽象,usrFsLib函数接口多为用户熟悉命令在Shell下直接使用。
接口 | 说明 |
---|---|
ioHelp | 显示详细的命令帮助列表 |
cd | 切换当前工作目录 |
pwd | 显示当前的工作目录 |
mkdir | 建立目录 |
rmdir | 删除目录 |
rm | 删除文件 |
chkdsk | 文件系统一致性检查 |
ls | 显示文件列表 |
ll | 显示详细文件列表 |
lsr | 显示文件列表,包含所有子目录 |
copy | 文件复制 |
cp | 文件复制到其他文件或目录 |
rename | 文件重命名 |
mv | 文件更名或移到其他目录 |
xcopy | 文件复制,包括子目录 |
xdelete | 文件删除,包括子目录 |
attrib | 修改文件或目录的属性 |
xattrib | 修改文件或目录的属性,包括子目录,支持通配符 |
diskFormat | 格式化,ioctl的FIODISKFORMAT和 FIODISKINIT调用 |
ansiStdio
ansiStdio库提供与ANSI C兼容的标准IO函数接口,各函数使用缓存机制,可提高大块数据操作性能。
其中函数大多以字母“f”开关,与IO系统接口名称差别也多在这个首字母。如 fopen对open. 各函数不再用文件描述符索引文件或设备,而用FILE指针,或称做流[stream]. FILE指针指向FILE结构,该结构封装一些与缓存操作相关的成员,在stdio.h中定义。 FILE结构成员 “_file” 指向IO系统中的文件描述符,FILE指针可以直接用fopen得到,或者用 fdopen关联已打开文件描述符。FILE指针不是全局的。,而归属于某个任务。VxWorks允许同一文件可以同时被打开多次,不同任务访问同一文件时,需要用户自己负责保证同步。
fioLib
fioLib提供一些与ANS C兼容的串格式处理函数,如printf 等。
库中一些函数输入输出定向在标准IO文件描述符上,可以用 ioGlobalStdSet 或 ioTaskStdSet 重定向标准IO文件描述符到具体的文件描述符上,则可用prinf等向文件格式化写入;或者使用 fdprintf 等直接文件描述符传入函数;或者使用变参数支持函数封装自己的IO函数;或者用 sprintf 等先在缓存格式化完成后,再用IO系统接口操作文。
在讲IO系统之前,先放两张WindRiver给的图:
第一张图可以看到标准的IO接口函数在整个操作系统中的位置,向上为应用程序提供接口,向下调用驱动程序进行读写。
文件系统代表块设备和IO系统接口,块设备(包括CBIO设备)都不直接挂入IO系统。
简单地看一下dosFs。在dosFsLib库中首先实现了IO系统规定的7个标准函数。在dosFsLibInit中调用iosDrvInstall将7个函数指针填入驱动表中,并得到一个驱动号。驱动表可以通过命令 iosDrvShow 在shell中查看。
文件系统接口在驱动表注册后,dosFsDevCreate 中调用 iosDevAdd,传入设备结构指针、设备名称和驱动号作为参数,将设备结构添加到设备链表中。这样dosFs 就加入了IO系统中。可以在shell中使用devs或iosDevShow 查看设备表。
看上面第二张图,IO系统Overview,文件系统安装到IO系统中后,就可以用上层库提供的接口函数进行操作了。如,使用设备名称作为设备链表索引的open, remove;使用文件描述符为索引的 read, write 等。
CBIO
为了提高文件系统的块输入输出效率,新版本的VxWorks文件系统中引入了CBIO机制。CBIO是文件系统的向下接口,原来的块设备接口都必须转换为CBIO接口,才能和文件系统挂接。
CBIO机制——一种为块设备数据的输入输出提供缓存的机制,主要由VxWorks文件系统中的cbioLib库提供的API来实现。
cbioLib 库即 块设备缓存IO库 [Cached Block I/O ]用于提供缓存块输入输出编程接口。VxWorks的各文件系统(dosFs、rawFs、tffs等)使用CBIO API 针对底层存储设备进行输入输出操作。
cbioLib 还提供了一个基本的CBIO模块,用它将块I/O驱动数据结构 BLK_DEV 接口转变为 CBIO API 接口。因此这个模块叫做 BLK_DEV设备到CBIO设备的基本封装模块。封装后的设备可以直接挂接在文件系统上,而只有极小的内存消耗。cbioLib 也为其它CBIO模块(如 dpartCbio、dcacheCbio、ramDiskCbio)提供支持。
cbioLib 包含的代码用来支持CBIO设备,一个CBIO设备本质上是一个软件层,它为其从属设备提供基本的I/O控制。CBIO设备层逻辑上位于文件系统之下,存储设备之上。它向其上层文件系统提供统一的CBIO API。
上层软件调用CBIO模块必须包含< cbioLib.h >头文件。
dcacheCbio模块通过CBIO API 实现存储盘cache机制,主要针对VxWorks的DOS文件系统使用,将经常用到的磁盘块存储在内存中以提高效率, 系统运行时,cache并不知道在存储盘上的具体文件系统格式,而是将存储盘作为特定大小的块的集合进行操作。
文件系统利用该模块可以以块为单位访问块设备中的数据,也可以以字节为单位访问块设备。该模块提供的缓存机制允许文件系统利用在内存上开辟的磁盘缓存存储最近经常使用到的文件数据、目录以及FAT表,这样避免了在磁盘上进行频繁 的数据查寻操作,进而提高了文件系统的数据读写效率。
这个dpartCbio模块利用CBIO API实现一般的分区管理,它支持在其每一个分区设备上创建一个独立的文件系统。
VxWorks提供ramDiskCbio模块用于实现CBIO接口的RAM盘驱动。ramDiskCbio替代dcacheCbio的位置,创建的设备可以直接用文件系统加载,这样可以减少内存的使用。该模块的实现会使用CBIO基本接口。
CBIO关键技术
CBIO提供的磁盘缓存机制由 dcacheCbio.c 模块具体实现,引入磁盘缓存目的是为了减少磁盘访问的次数并提高磁盘读写效率,它将最近经常使用的块存储于内存中开辟的cache中避免经常访问磁盘驱动带来的长时间消耗。
提前读
当所需块数据不在磁盘cache中时,从磁盘中一次性读取与所需块相邻的一组连续的块数据的技术。
由于访问块驱动是比较消耗时间的,为了提高读效率,一次性读取多个连续的块的方法提高了读操作的吞吐量,从而提高了读操作的性能。这些提前读出的一组块数据不直接放入磁盘Cache,而先存放在一个被称为“大缓存”的缓冲器——内存中为磁盘cache开辟的缓存器,可以理解为一个二级缓存,它的大小可达到整个磁盘Cache的1/4, 提前读出的一组数据块被预先放入“大缓存 ”中,然后再将它们转换成正常的磁盘cache块大小放入磁盘cache中。
用户可以通过dcacheDevTune函数来调节提前读的大小,以获取更好的性能 。
大块请求跳过
用户通过函数 dcacheDevTune 来调节参数bypassCount,当读写请求的数据块大小大于 bypassCount 的值时,数据的传输将绕过磁盘cache 直接通过块设备驱动进行内存与磁盘之间的数据传输,类似于DMA方式。用户应注意防止因 bypassCount 值设置过小,而失去使用磁盘cache的意义。
后台更新
由于在缓存中块可能会被修改,为了保持磁盘和缓存的一致性,采用了“后台更新”技术,该技术通过一个特定的任务以实现磁盘和缓存中数据的一致性,该任务以块为单位每隔一定的时间将磁盘更新一次,更新的时间间隔由可调参数 syncInterval 来控制。该任务的优先级应比CPU级任务的要高,以确保该伤能被实时的激活以保持磁盘和缓存数据的一致性。
所有的缓存设备都是由这个任务来更新的。在VxWorks中它被命名为 tDcacheUpd. 针对可移动设备,这个更新任务在当可移动设备有2秒或更长时间没被使用时,负责将磁盘缓存置于无效。
CBIO设备分区实现
(1) 定义块设备指针变量 * blkDevId
BLK_DEV *blkDevId;
const char* cbiodevNames[] = {"/deva", "/devb", "/devc"};
CBIO_DEV_ID cbioCache;
CBIO_DEV_ID cbioParts;
(2)在整个BLK_DEV设备上建立 Cache:
cbioCache = dcacheDevCreate(blkDevId, NULL, 0, "/dev");
(3)创建3个cbio设备分区并同时使用FDISK格式解析器:
cbioParts = dpartDevCreate(cbioCache, 3, usrFdiskPartRead);
(4)在前两个cbio设备分区设备上建立DOS文件系统,在最后一个cbio设备分区设备上建立 raw 文件系统:
dosFsDevCreate(cbiodevNames[0], dpartPartGet(cbioParts, 0), 0x10, NONE);
dosFsDevCreate(cbiodevNames[1], dpartPartGet(dbioParts, 1), 0x10, NONE);
rawFsDevInit(cbiodevNames[2], dpartPartGet(cbioParts, 2));
当dpartCbio模块和dcacheCbio模块堆叠使用时,应将其放在 dcacheCbio之上, 作为主CBIO设备,调用dpartDevCreate()时应先调用 dcacheDevCreate(). 这样 dcacheCbio 层就可以为整个存储盘服务,而不是只对单分区服务 。
文件时标
文件系统中使用ansiTime库中time函数来获取系统时间,确定文件或目录的时标。缺省时标使用1980.1.1,比这更早的时间是无效的。time用 clockLib中的 clock_gettime实现,取系统软件时钟“_clockRealtime”的值,软时钟由系统周期时钟中断按 tick 单位进位。 在系统初始化时需调用 clock_settime 确定软件时钟初值,初值来源可以为硬件 RTC。另外,如果系统中有比系统周期时钟中断更精确的时钟源,也可调用 clock+settime 不断同步系统软时钟。
连续文件
为了提高文件操作性能,可以使用连续文件。连续文件由连续的存储块组成,也可为目录分配连续存储区。
为了给文件分配连续的存储区,需要先按正常方式创建文件,再使用 ioctl 的 FIOCONFIG 功能为新文件分配要求大小的连续存储区,若存储盘无足够大的连续区, iotcl 返回错误。
#include "vxWorks.h"
#include "ioLib.h"
#include "fcntl.h"
STATUS fileConfigTest(void)
{
int fd;
STATUS status;
if((fd = create("file", O_RDWR)) == ERROR) // 创建文件
return (ERROR);
status = ioctl(fd, FIOCONTIG, 0x10000/*or CONFIG_MAX*/);
if(OK != status)
printf("ERROR");
// TODO: 使用文件
close(fd); // 关闭文件
}
也可以先取得存储盘可用的最大连续区大小,再判断是否能创建想要的连续文件。
#include "vxWorks.h"
#include "fcntl.h"
#include "ioLib.h"
STATUS configTest(void)
{
int count;
int fd;
if((fd=open("/DEV1/", O_RDONLY, 0)) == ERROR)
return (ERROR);
ioctl(fd, FIONCONTIG, &count);
// TODO: 创建连续文件
close(fd);
}
ioctl选项
ioctl中各功能由各层结构分别实现。dosFs支持的功能码在 ioLib.h 中定义,如下表:
功能 | 说明 | 其他 |
---|---|---|
FIOATTRIBSET | 设置文件属性 | |
FIOCONTIG | 为连续文件或目录分配存储区 | FIOCONTIG64 |
FIONCONTIG | 取得存储盘最大连续区大小 | FIONCONTIG64 |
FIODISKCHANGE | 声明存储盘改变 | 由启动CBIO实现 |
FIODISKINIT | dosFs 卷格式化,类似 dosFsVolFormat | |
FIOFLUSH | 写文件缓存到存储盘上 | |
FIOSYNC | 同上,并重读缓存的文件数据 | |
FIOFSTATGET | 取文件状态信息和目录条目数据 | |
FIOFSTATFSGET | 取文件系统信息 | |
FIOGETNAME | 根据文件描述符取文件名 | |
FIOLABELSET | 设置卷标 | |
FIOLABELGET | 读取卷标 | |
FIOMKDIR | 创建目录 | |
FIORMDIR | 删除目录 | |
FIONFREE | 读取空闲字节数 | FIONFREE64 |
FIONREAD | 读取文件未读字节数 | FIONREAD64 |
FIOREADDIR | 读取下一个目录条目 | |
FIORENAME | 为文件或目录更名 | |
FIOSEEK | 设置文件的字节偏移 | FIOSEEK64 |
FIOWHERE | 读取文件的字节偏移 | FIOWHERE64 |
FIOTRUNC | 剪裁文件 | FIOTRUNC64 |
FIOUNMOUNT | 卸载文件系统 | |
FIOTIMESET | 设置文件时标 | |
FIOCHKDSK | 文件系统一致性检查 |
rawFs为一个简单的小文件系统,为系统实现最基本的存储盘IO操作。VxWorks提供rawFsLib库来实现rawFs. rawFs将整个存储盘作为一个大文件来管理,可以为简单文件管理提供较好的性能 。
块设备前面一节讲SD卡的时候已经介绍了一部分。这里面我们仅对TFFS再做近一步的补充。
关于TFFS这里摘录了关于
1. Flash存储器
2. 算法分析
3. 结构分析
4. 共享Flash
5. 初始化
等五个部分,其中前三个部分可以在上一篇关于SD卡的章节看到。此节我们只看第4和第5两个部分。
如果程序映象实际在RAM中运行,程序映象和TFFS可以共享同一Flash存储器。比如只有一片Flash存储器,可以将BootRom和TFFS同时存于其中,VxWorks可以存放在TFFS中。但如果程序映象是从Flash存储器上运行的(XIP),则TFFS只能读,因为Flash在擦除过程中不能读,而写会引起垃圾回收的擦除操作。
TFFS库提供一种共享机制,在tffsDevFormat 中可以指定保留一段存储区,用于启动程序映象的存放。可以使用 tffsBootImagePut 函数来操作这块保留区。
在使用TFFS前,首先需要调用tffsDrv。该函数用于建立TFFS管理需要的互斥信号量、全局变量和数据结构,并完成Socket注册和启动Socket查询任务 tTffsPTask。tffsDrv一般都能调用成功,主要需要Socket层的接口函数,看一张图。
tTffsPTask任务是周期性循环任务,不是必须的,通过设置 flcustom.h 中的POLLING_INTERVAL 为0来禁止,缺省值是100ms。tTffsPTask任务主要用于探测Flash存储器插拔, Vpp 和 Vcc 电源延时关闭等。
tffsDrv调用成功后,在tffsDevCreate之前,需要先调用tffsDevFormat格式化Flash。
STATUS tffsDevFormat(int tffsDriveNo, int arg);