前面分析了f_mkfs、f_mount函数,在格式化和挂载完成之后,当然是要创建文件的操作,本节分析f_open函数,分析一下创建和打开一个文件时,发生了什么。
(1)假设一个磁盘就一个分区。
(2)只分析FAT32文件系统相关的代码。
(3)函数的大部分分析,都写入代码注释中。
(4)重要的注释都回加入很多星号以及数学标号。例如,
/****************** 1.把字符存入lfn的buffer中 *******************/
(1)FAT文件系统描述一个文件夹的结构体。虽然是一个文件夹结构体,但是再打开一个文件的开始,先创建这个结构体,然后先把文件的信息填充到这个结构体,最终使用这个结构体填充FIL结构图的一些重要信息。
typedef struct {
_FDID obj; /* 存放下一个目标的信息 */
DWORD dptr; /* 当前目录项的起始地址(单位:字节) */
DWORD clust; /* 当前簇 */
DWORD sect; /* 当前簇对应的扇区位置(通过目录项的位置确定) */
BYTE* dir; /* 指向当前扇区win[]中的偏移(单位:字节) */
BYTE fn[12]; /* 存放8.3文件名,最后1个字节存放标志:是否有长目录项 */
#if _USE_LFN != 0
DWORD blk_ofs; /* 指向第1个长目录项的起始位置(字节:单位) (0xFFFFFFFF:Invalid) */
#endif
#if _USE_FIND
const TCHAR* pat; /* Pointer to the name matching pattern */
#endif
} DIR;
(2)FAT文件系统描述一个文件的结构图。
typedef struct {
_FDID obj; /* 存放下一个目标的信息 */
BYTE flag; /* 文件状态标志 */
BYTE err; /* 文件错误码 */
FSIZE_t fptr; /* 文件的读写指针位置 */
DWORD clust; /* fptr所在的当前簇 */
DWORD sect; /* Sector number appearing in buf[] (0:invalid) */
#if !_FS_READONLY
DWORD dir_sect; /* Sector number containing the directory entry */
BYTE* dir_ptr; /* Pointer to the directory entry in the win[] */
#endif
#if _USE_FASTSEEK
DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open, set by application) */
#endif
#if !_FS_TINY
BYTE buf[_MAX_SS]; /* File private data read/write window */
#endif
} FIL;
(3)_FDID结构体。
在(1)和(2)的结构体中,都有_FDID结构体,那么分析一下这个结构体的内容:
注:在FATFS中有个全局的指针数组FATFS[],在f_mount的时候可知,这个数组会被填充,填充的内容是MBR的内容,之后,我们把这个数组指针称作为**“超级快”**。
typedef struct {
FATFS* fs; /* 指向全局的在挂载的时候磁盘对应的超级块 */
WORD id; /* Owner file system mount ID */
BYTE attr; /* 目标属性,根据目录项中的属性进行填充 */
BYTE stat; /* Object chain status (b1-0: =0:not contiguous, =2:contiguous (no data on FAT), =3:got flagmented, b2:sub-directory stretched) */
DWORD sclust; /* 目标的起始簇号 */
FSIZE_t objsize; /* 文件或者文件夹的大小 */
#if _FS_LOCK != 0
UINT lockid; /* File lock ID origin from 1 (index of file semaphore table Files[]) */
#endif
} _FDID;
f_open函数分析
下面是f_open的源代码:
FRESULT f_open (
FIL* fp, /* 返回的文件描述符 */
const TCHAR* path, /* 打开文件的路径 */
BYTE mode /* 文件打开的模式 */
)
{
FRESULT res;
DIR dj; /* 分配一个DIR结构体 */
FATFS *fs;
#if !_FS_READONLY
DWORD dw, cl, bcs, clst, sc;
FSIZE_t ofs;
#endif
if (!fp) return FR_INVALID_OBJECT;
/* Get logical drive */
mode &= _FS_READONLY ? FA_READ : FA_READ | FA_WRITE | FA_CREATE_ALWAYS | FA_CREATE_NEW | FA_OPEN_ALWAYS | FA_OPEN_APPEND | FA_SEEKEND;
res = find_volume(&path, &fs, mode);
if (res == FR_OK) {
dj.obj.fs = fs;
/* 根据不同的配置来分配fs->lfn,用于存放path指向的路径 */
INIT_NAMBUF(fs);
res = follow_path(&dj, path); /* Follow the file path */
#if !_FS_READONLY /* R/W configuration */
if (res == FR_OK) {
if (dj.fn[NSFLAG] & NS_NONAME) {
/* Origin directory itself? */
res = FR_INVALID_NAME;
}
#if _FS_LOCK != 0
else {
res = chk_lock(&dj, (mode & ~FA_READ) ? 1 : 0);
}
#endif
}
/* Create or Open a file */
if (mode & (FA_CREATE_ALWAYS | FA_OPEN_ALWAYS | FA_CREATE_NEW)) {
if (res != FR_OK) {
/* 文件不存在 创建文件,省略不少代码 */
}
else {
/* 文件存在 但是不允许写入 */
if (dj.obj.attr & (AM_RDO | AM_DIR)) {
/* Cannot overwrite it (R/O or DIR) */
res = FR_DENIED;
} else {
if (mode & FA_CREATE_NEW) res = FR_EXIST; /* Cannot create as new file */
}
}
if (res == FR_OK && (mode & FA_CREATE_ALWAYS)) {
/* 覆盖文件,省略不少代码 */
}
}
else {
/* 成功打开一个文件 */
if (res == FR_OK) {
/* Following succeeded */
if (dj.obj.attr & AM_DIR) {
/* It is a directory */
res = FR_NO_FILE;
} else {
if ((mode & FA_WRITE) && (dj.obj.attr & AM_RDO)) {
/* R/O violation */
res = FR_DENIED;
}
}
}
}
if (res == FR_OK) {
if (mode & FA_CREATE_ALWAYS) /* Set file change flag if created or overwritten */
mode |= FA_MODIFIED;
fp->dir_sect = fs->winsect; /* Pointer to the directory entry */
fp->dir_ptr = dj.dir;
}
#else
#endif
if (res == FR_OK)
{
/* 使用dj结构体对文件描述符fp填充 */
/* 具体填充的代码,参考源码,最终会进行一些总结概括 */
}
FREE_NAMBUF();
}
if (res != FR_OK) fp->obj.fs = 0; /* Invalidate file object on error */
LEAVE_FF(fs, res);
}
(1)在注释1的地方,先调用INIT_NAMBUF(fs)宏,初始化fs->lfn,这个lfn之后会被用来存放path中以’/'为分隔符的各个段的名字。
(2)接着在注释2的地方调用follow_path(&dj, path),追踪path的各个段,下面是follow_path()函数的源码:
/*-----------------------------------------------------------------------*/
/* 跟踪文件路径 */
/*-----------------------------------------------------------------------*/
static
FRESULT follow_path ( /* FR_OK(0): successful, !=0: error code */
DIR* dp, /* Directory object to return last directory and found object */
const TCHAR* path /* Full-path string to find a file or directory */
)
{
FRESULT res;
BYTE ns;
_FDID *obj = &dp->obj;
FATFS *fs = obj->fs;
{
/* With heading separator */
while (*path == '/' || *path == '\\') path++; /* Strip heading separator */
obj->sclust = 0; /* Start from the root directory */
}
if ((UINT)*path < ' ') {
/* Null path name is the origin directory itself */
dp->fn[NSFLAG] = NS_NONAME;
res = dir_sdi(dp, 0);
} else {
/* Follow path */
for (;;) {
/***************** 1.获取path的每一段名字 *****************/
res = create_name(dp, &path);
if (res != FR_OK) break;
/***************** 2.判断磁盘上面是否存在提取出来的目录项 *****************/
res = dir_find(dp); /* Find an object with the segment name */
/***************** 3.获取当前文件名获取的状态 *****************/
ns = dp->fn[NSFLAG];
if (res != FR_OK) {
/* Failed to find the object */
if (res == FR_NO_FILE) {
/* 不存在此目录项 */
if (_FS_RPATH && (ns & NS_DOT)) {
/* If dot entry is not exist, stay there */
if (!(ns & NS_LAST)) continue; /* Continue to follow if not last segment */
dp->fn[NSFLAG] = NS_NONAME;
res = FR_OK;
} else {
/* Could not find the object */
if (!(ns & NS_LAST)) res = FR_NO_PATH; /* Adjust error code if not last segment */
}
}
break;
}
if (ns & NS_LAST) break; /* 是最后一段 */
/* Get into the sub-directory */
if (!(obj->attr & AM_DIR)) {
/* 是子目录,不需要再追踪 */
res = FR_NO_PATH; break;
}
{
/********* 4.设置下一个簇的位置*********/
obj->sclust = ld_clust(fs, fs->win + dp->dptr % SS(fs)); /* Open next directory */
}
}
}
return res;
}
在这个函数中又调用create_name()和dir_find(dp)两个函数,这两个函数是追踪的核心代码,也是f_open的核心代码。
(2.1)下面是create_name()的源码:
/*-----------------------------------------------------------------------*/
/* 使用传入的Path,然后用'/'进行分段,提取每一段的名字 ,同时修改path指向的位置*/
/*-----------------------------------------------------------------------*/
static
FRESULT create_name ( /* FR_OK: successful, FR_INVALID_NAME: could not create */
DIR* dp, /* Pointer to the directory object */
const TCHAR** path /* Pointer to pointer to the segment in the path string */
)
{
//#if _USE_LFN != 0 /* LFN configuration */
BYTE b, cf;
WCHAR w, *lfn;
UINT i, ni, si, di;
const TCHAR *p;
/* Create LFN in Unicode */
p = *path; lfn = dp->obj.fs->lfnbuf; si = di = 0;
for (;;) {
w = p[si++]; /* 从path中获取一个字符 */
if (w < ' ') break; /* 如果字符的值<' ',那么就判定path没有字符了,因为文件的命名字符不能<' ' */
if (w == '/' || w == '\\') {
/* 如果字符是/或者\字符,就跳出来,说明一段已经结束了 */
while (p[si] == '/' || p[si] == '\\') si++;
break;
}
/* 一段的名字太长了,报错,
这个判断写再下面才容易阅读,尴尬中。。。
*/
if (di >= _MAX_LFN) return FR_INVALID_NAME;
/* 如果字符不是UNICODE编码的话,就进行转码 */
#if !_LFN_UNICODE
w &= 0xFF;
if (IsDBCS1(w)) {
/* Check if it is a DBC 1st byte (always false on SBCS cfg) */
b = (BYTE)p[si++]; /* Get 2nd byte */
w = (w << 8) + b; /* Create a DBC */
if (!IsDBCS2(b)) return FR_INVALID_NAME; /* Reject invalid sequence */
}
w = ff_convert(w, 1); /* Convert ANSI/OEM to Unicode */
if (!w) return FR_INVALID_NAME; /* Reject invalid code */
#endif
/* 非法字符检测 */
if (w < 0x80 && chk_chr("\"*:<>\?|\x7F", w)) return FR_INVALID_NAME; /* Reject illegal characters for LFN */
/******************* 1.把字符存入lfn的buffer中 *******************/
lfn[di++] = w;
}
/******************* 2.让path指向下一段*******************/
*path = &p[si];
cf = (w < ' ') ? NS_LAST : 0; /******************** 3.判断是否是最后一段 ********************/
/* 在lfn中,从后向前找,找到第1个并不是空格也不是点的位置 */
while (di) {
w = lfn[di - 1];
if (w != ' ' && w != '.') break;
di--;
}
lfn[di] = 0; /* 在不是空格也不是点的后面设置字符串结束标志 */
if (di == 0) return FR_INVALID_NAME; /* 判断名字是否为空 */
/* Create SFN in directory form */
mem_set(dp->fn, ' ', 11);
/*从lfn的头开始找到不是空格也不是点的位置*/
for (si = 0; lfn[si] == ' ' || lfn[si] == '.'; si++) ;
if (si) cf |= NS_LOSS | NS_LFN; /* 如果lfn的头部存在空格和点,设置标志 */
while (di && lfn[di - 1] != '.') di--; /* 从后向前找到点的位置,作为扩展名的起始位置 */
/* 此时si指向文件名的起始位置,di指向文件名的结尾 */
i = b = 0; ni = 8;
for (;;) {
w = lfn[si++]; /* Get an LFN character */
if (!w) break; /* Break on end of the LFN */
if (w == ' ' || (w == '.' && si != di)) {
/* Remove spaces and dots */
cf |= NS_LOSS | NS_LFN; continue;
}
/* 判断是否有长文件名 */
if (i >= ni || si == di) {
/* 如果文件名有8个字符或者到了文件名的结束字符 */
if (ni == 11) {
/* 如果ni==11,说明需要设置长文件名 */
cf |= NS_LOSS | NS_LFN; break;
}
if (si != di) cf |= NS_LOSS | NS_LFN; /* 文件名的长度肯定是大于8个字符,需要设置长文件名 */
if (si > di) break; /* di指向的是文件名的结尾,说明不存在扩展名 */
si = di; i = 8; ni = 11; /* 直接让si=di,下一次循环就直接会跳出,不会再给dp->fn赋值了 */
b <<= 2; continue;
}
/* 字符编码转换 */
if (w >= 0x80) {
/* Non ASCII character */
#ifdef _EXCVT
w = ff_convert(w, 0); /* Unicode -> OEM code */
if (w) w = ExCvt[w - 0x80]; /* Convert extended character to upper (SBCS) */
#else
w = ff_convert(ff_wtoupper(w), 0); /* Upper converted Unicode -> OEM code */
#endif
cf |= NS_LFN; /* Force create LFN entry */
}
/* 下面把小写转换成大写存入dp->fn中 */
if (_DF1S && w >= 0x100) {
/* Is this DBC? (always false at SBCS cfg) */
if (i >= ni - 1) {
cf |= NS_LOSS | NS_LFN; i = ni; continue;
}
dp->fn[i++] = (BYTE)(w >> 8);
} else {
/* SBC */
if (!w || chk_chr("+,;=[]", w)) {
/* Replace illegal characters for SFN */
w = '_'; cf |= NS_LOSS | NS_LFN;/* Lossy conversion */
} else {
if (IsUpper(w)) {
/* ASCII large capital */
b |= 2;
} else {
if (IsLower(w)) {
/* ASCII small capital */
b |= 1; w -= 0x20;
}
}
}
}
/******************* 4.把短文件名放入dp->fn[]中 *******************/
dp->fn[i++] = (BYTE)w;
}
/* 如果 dp->fn[0]== 0XE5那么就替换成0x05,因为E5代表的是被删除的目录项*/
if (dp->fn[0] == DDEM) dp->fn[0] = RDDEM;
if (ni == 8) b <<= 2;
if ((b & 0x0C) == 0x0C || (b & 0x03) == 0x03) cf |= NS_LFN; /* 进一步保证需要有长文件名 */
/* 文件名和扩展名不存在长文件名标志 */
if (!(cf & NS_LFN)) {
/* When LFN is in 8.3 format without extended character, NT flags are created */
if ((b & 0x03) == 0x01) cf |= NS_EXT; /* NT flag (Extension has only small capital) */
if ((b & 0x0C) == 0x04) cf |= NS_BODY; /* NT flag (Filename has only small capital) */
}
/******************* 5.在dp->fn[]最后一个字节写入文件名的标志:是否有长文件名、是否是最后一段 *******************/
dp->fn[NSFLAG] = cf; /* 把CF放入fn的最后一个字节 */
return FR_OK;
}
create_name ()函数的主要作用是根据path中的“/”进行分割每一段的名字,并且把短文件名存入dp->fn[]中,具体操作步骤:
a.跟进path找出一段文件名,然后存入lfn[]缓存中。
b.修改path指向下一段文件名的起始位置。
c.把lfn[]制作成短文件名放入dp->fn[]中。
d.把当前的状态放入dp->fn[]的最后一个字节,用来指示:是否有长文件名,是否是最后一段。
(2.2)dir_find()函数的源码:
/*-----------------------------------------------------------------------*/
/*根据dp->sect、dp->fn找到相应的目录项, */
/*-----------------------------------------------------------------------*/
static
FRESULT dir_find ( /* FR_OK(0):succeeded, !=0:error */
DIR* dp /* Pointer to the directory object with the file name */
)
{
FRESULT res;
FATFS *fs = dp->obj.fs;
BYTE c;
#if _USE_LFN != 0
BYTE a, ord, sum;
#endif
/*********** 1.根据dp->obj.sclust,设置dp的当前信息 ************/
res = dir_sdi(dp, 0); /* Rewind directory object */
if (res != FR_OK) return res;
/* On the FAT12/16/32 volume */
#if _USE_LFN != 0
ord = sum = 0xFF; dp->blk_ofs = 0xFFFFFFFF; /* Reset LFN sequence */
#endif
do {
/********* 2.如果当前扇区的内容不在win[]缓冲,就从磁盘读出来 *************/
res = move_window(fs, dp->sect);
if (res != FR_OK) break;
c = dp->dir[DIR_Name];
if (c == 0) {
res = FR_NO_FILE; break; } /* 判断目录项的第1个字节是否合法 */
//#if _USE_LFN != 0 /* LFN configuration */
dp->obj.attr = a = dp->dir[DIR_Attr] & AM_MASK;
/* 如果第1个字节是E5或者是卷标,就判定也不是合适的目录项 */
if (c == DDEM || ((a & AM_VOL) && a != AM_LFN)) {
ord = 0xFF; dp->blk_ofs = 0xFFFFFFFF; /* Reset LFN sequence */
} else {
if (a == AM_LFN) {
/* 是长目录项 */
if (!(dp->fn[NSFLAG] & NS_NOLFN)) {
if (c & LLEF) {
/* 如果c==0x40那么就是最后一个长目录项 */
sum = dp->dir[LDIR_Chksum];
c &= (BYTE)~LLEF; ord = c; /* LFN start order */
dp->blk_ofs = dp->dptr; /* Start offset of LFN */
}
/* Check validity of the LFN entry and compare it with given name */
ord = (c == ord && sum == dp->dir[LDIR_Chksum] && cmp_lfn(fs->lfnbuf, dp->dir)) ? ord - 1 : 0xFF;
}
} else {
/* An SFN entry is found */
if (!ord && sum == sum_sfn(dp->dir)) break; /* LFN matched? */
/********* 3.比较短目录名是否匹配 **************/
if (!(dp->fn[NSFLAG] & NS_LOSS) && !mem_cmp(dp->dir, dp->fn, 11)) break;
ord = 0xFF; dp->blk_ofs = 0xFFFFFFFF; /* Reset LFN sequence */
}
}
/********** 4.寻找下一个目录项:会修改dp->dptr,dp->dir的值,也可能修改当前簇,当前扇区等信息
如果此时在当前目录下没有目录项了,就会返回错误***************
*/
res = dir_next(dp, 0);
} while (res == FR_OK);
return res;
}
dir_find ()函数的作用是在扇区中找到每一个目录项,取出目录项的文件名与dp->fn[]中的文件名对比,用来确定是否找到对应的目录项,主要步骤:
a.dir_sdi(dp, 0)函数用来设置dp的当前扇区dp->sect等信息。
b.读出当前扇区的信息到win[]中。
c.然后取出win[]中的每一个目录项。
d.判断目录项中的文件名是否与dp->fn[]中的文件名一致。
(3)通过follow_path()函数的返回值,就可以最终判定出path的目标文件是否存在。如果文件存在,在f_open()的最后会设置文件描述符fp的信息:
主要设置文件描述符fp的当前扇区,文件的打开模式mode等信息。
核心思想总结
通过f_open()函数的过程,核心思想:
(1)使用一个_FDID结构体记录要解析的簇信息。
(2)用DIR结构体把_FDID结构体对应的簇,解析出对应的扇区。
(3)把扇区上面的每个目录项进行分析和目标名字是否匹配。
(4)如果最终匹配到文件目录项,就使用DIR结构体里面对应的信息,将FILE结构体初始化,返回到用户空间。
后记
f_open函数并没有分析的非常详尽,在分析的过程中发现,中间涉及非常多的其他函数,希望“后来人”能把它分析的更详细一些吧,略表遗憾。