fatfs文件系统详解之f_open函数分析

前序

前面分析了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的信息:
fatfs文件系统详解之f_open函数分析_第1张图片
主要设置文件描述符fp的当前扇区,文件的打开模式mode等信息。

核心思想总结
通过f_open()函数的过程,核心思想:
(1)使用一个_FDID结构体记录要解析的簇信息。
(2)用DIR结构体把_FDID结构体对应的簇,解析出对应的扇区。
(3)把扇区上面的每个目录项进行分析和目标名字是否匹配。
(4)如果最终匹配到文件目录项,就使用DIR结构体里面对应的信息,将FILE结构体初始化,返回到用户空间。

后记
f_open函数并没有分析的非常详尽,在分析的过程中发现,中间涉及非常多的其他函数,希望“后来人”能把它分析的更详细一些吧,略表遗憾。

你可能感兴趣的:(文件系统,fatfs文件系统,f_open函数,fatfs,文件系统,fat文件系统)