前次深入的分析了FAT表的组织形式和linux内核在处理FAT表项时候的一些操作。任何一个文件系统都需要将文件的基本信息以某种形式存储在设备中(包括文件名,文件读写属性,文件的创建、访问、修改时间等等)。FAT文件系统也不能例外。
Linux内核中以结构体struct msdos_dir_entry来表示一个文件目录项。该文件目录项一共占32个字节的空间。
struct msdos_dir_entry {
//8字节的文件名,3字节的扩展名
__u8 name[8],ext[3]; /* name and extension */
// 1字节的文件属性
//00000000(读写)
//00000001(只读)
//00000010(隐藏)
//00000100(系统)
//00001000(卷标)
//00010000(子目录)
//00100000(归档)
__u8 attr; /* attribute bits */
//系统保留一字节
__u8 lcase; /* Case for base and extension */
//创建时间的10 毫秒位
__u8 ctime_cs; /* Creation time, centiseconds (0-199) */
//文件创建时间
__le16 ctime; /* Creation time */
//文件创建日期
__le16 cdate; /* Creation date */
//文件最后访问日期
__le16 adate; /* Last access date */
//文件起始簇号的高16 位
__le16 starthi; /* High 16 bits of cluster in FAT32 */
//文件的最近修改时间
//文件的最近修改日期
//文件起始簇号的低16 位
__le16 time,date,start;/* time, date and first cluster */
//表示文件的长度
__le32 size; /* file size (in bytes) */
};
这个结构体中的成员变量starthi和start回到了上次没有回答的一个问题,就是怎么知道文件是从哪个簇开始的。这儿给予了回答。同时因为文件的size是一个u32类型的变量,所以FAT32的单个文件最大只能支持到4G。
但是从上面成员变量name[8],ext[3]可以看出,文件目录项的名字的最大只能支持到8个字节以及3个字节的扩展文件名(什么是扩展文件名,就是我们平时所提到的doc、txt、pdf等等格式)。
问题来了,因为在关于FAT32的资料上说FAT32最大能支持到长度为255的文件名以及长度为244的目录名。
事实上,FAT32用采用另外一个形式来支持长文件目录项,具体形式为结构体struct msdos_dir_slot所定义,t同样该结构体占用32个字节的空间:
struct msdos_dir_slot {
//属性字节位意义
//7:保留未用
//6:1表示长文件最后一个目录项
//5:保留未用
//4~0 顺序号数值
__u8 id; /* sequence number for slot */
// 长文件名 unicode 码第一部分
__u8 name0_4[10]; /* first 5 characters in name */?
// 长文件名目录项标志,取值 0xF
__u8 attr; /* attribute byte */
//系统保留
__u8 reserved; /* always 0 */
//校验值(根据短文件名计算得出)
__u8 alias_checksum; /* checksum for 8.3 alias */
//长文件名unicode码第二部分
__u8 name5_10[12]; /* 6 more characters in name */
//文件起始簇号(目前常置0)
__le16 start; /* starting cluster number, 0 in long slots */
//长文件名unicode码第三部分
__u8 name11_12[4]; /* last 2 characters in name */
};
那么FAT32是如何实现长文件目录项的呢?
系统在存储长文件名时,总是先按倒序填充长文件名目录项,然后紧跟其对应的短文件名。从上面的结构体定义可以看出,长文件名中并不存储对应文件的文件开始簇、文件大小、各种时间和日期属性。文件的这些属性还是存放在短文件名目录项中,一个长文件名总是和其相应的短文件名一一对应,短文件名没有了长文件名还可以读,但长文件名如果没有对应的短文件名,不管什么系统都将忽略其存在。所以短文件名是至关重要的。在不支持长文件名的环境中对短文件名中的文件名和扩展名字段作更改(包括删除,因为删除是对首字符改写E5H),都会使长文件名形同虚设。 长文件名和短文件名之间的联系光靠他们之间的位置关系维系显然远远不够。其实,长文件名的0xD字节的校验和起很重要的作用,此校验和是用短文件名的11个字符通过一种运算方式来得到的。系统根据相应的算法来确定相应的长文件名和短文件名是否匹配。
FAT32将长文件名字符串转换成unicode编码,struct msdos_dir_slot结构体的成员中unicode码一共分为3部分,总共占用26个字节,可以存储13个unicode码。也就是说一个struct msdos_dir_slot可以存储13个unicode码。如果文件名太长,那么就分为多个slot,并按倒序存储。
当创建一个长文件名文件时,系统会自动加上对应的短文件名,其一般有
的原则:
(1)、取长文件名的前6个字符加上"~1"形成短文件名,扩展名不变。
(2)、如果已存在这个文件名,则符号"~"后的数字递增,直到5。
(3)、如果文件名中"~"后面的数字达到5,则短文件名只使用长文件名的前两个字母。通过数学操纵长文件名的剩余字母生成短文件名的后四个字母,然后加后缀"~1"直到最后(如果有必要,或是其他数字以避免重复的文件名)。
(4)、如果存在老OS或程序无法读取的字符,换以"_"
图一:上图是一个长文件目录项
上图一行为16个字节,这样一个slot或者dir占2行的大小。第一个字节为0x42,其中第6bit为1表示该slot是最后一个(因为是按倒序排列的,所以在物理位置上是第一个)。另外个2表示该长文件目录项一共有2个slots。其中attr(第12个字节)为0xF,是长文件目录项的标志。最后是一个短文件目录项,扩展属性的3个字节为0x53 0x48 0x20,其中0x20是因为字节不够补充上去的,0x53和0x48为"sh",这样就知道了,该文件是一个shell脚本。
通过查看start和starthi成员变量可以看出该文件在第二个簇上。
图二:FAT表的一部分。
再通过查看图二中的FAT表,我们可以看出该文件所占第二簇在FAT表中的入口为FF FF,表示该文件只占用一个簇,是个小文件。
下面我们就从内核源码中看看linux是怎么实现FAT文件系统的长短目录项的。
Linux内核中通过函数vfat_build_slots来创建一个文件的长短目录项。其中输入参数dir为父inode节点,name为需要创建的文件或者目录的名称(这样说是不太准确的,在FAT中,一切都是目录,而在linux中一切都是文件),cluster是要创建的文件的数据的第一个簇。指针slots和nr_slots用于返回生成的slot和slot的个数。(我个人很喜欢看代码、贴代码。虽说文字性的描述可以大体的阐述一下程序的思想,但是只有真实的代码才能看出作者的精细的构思和睿智)
static int vfat_build_slots(struct inode *dir, const unsigned char *name,
int len, int is_dir, int cluster,
struct timespec *ts,
struct msdos_dir_slot *slots, int *nr_slots)
{
struct msdos_sb_info *sbi = MSDOS_SB(dir->i_sb);
struct fat_mount_options *opts = &sbi->options;
struct msdos_dir_slot *ps;
struct msdos_dir_entry *de;
unsigned long page;
unsigned char cksum, lcase;
unsigned char msdos_name[MSDOS_NAME];
wchar_t *uname;
__le16 time, date;
int err, ulen, usize, i;
loff_t offset;
*nr_slots = 0;
err = vfat_valid_longname(name, len);
if (err)
return err;
page = __get_free_page(GFP_KERNEL);
if (!page)
return -ENOMEM;
uname = (wchar_t *)page;
//用于将名字转换成unicode码,usize为转换后的unicode码得长度,对于转换unicode,在此不详细分析
err = xlate_to_uni(name, len, (unsigned char *)uname, &ulen, &usize,
opts->unicode_xlate, opts->utf8, sbi->nls_io);
if (err)
goto out_free;
err = vfat_is_used_badchars(uname, ulen);
if (err)
goto out_free;
//根据长文件名来创建一个短文件名
// 当创建一个长文件名文件时,系统会自动加上对应的短文件名,其一般有
//的原则:
// (1)、取长文件名的前6个字符加上"~1"形成短文件名,扩展名不变。
// (2)、如果已存在这个文件名,则符号"~"后的数字递增,直到5。
// (3)、如果文件名中"~"后面的数字达到5,则短文件名只使用长文件名的
//前两个字母。通过数学操纵长文件名的剩余字母生成短文件名的后四个字母,
//然后加后缀"~1"直到最后(如果有必要,或是其他数字以避免重复的文件名)。
// (4)、如果存在老OS或程序无法读取的字符,换以"_"
err = vfat_create_shortname(dir, sbi->nls_disk, uname, ulen,
msdos_name, &lcase);
if (err < 0)
goto out_free;
else if (err == 1) {
de = (struct msdos_dir_entry *)slots;
err = 0;
goto shortname;
}
/* build the entry of long file name */
cksum = fat_checksum(msdos_name);
//每个长文件名目录项可以保持13个unicode码
*nr_slots = usize / 13;
//生成长文件slot,注意长文件目录项是按倒序排列的。i--
for (ps = slots, i = *nr_slots; i > 0; i--, ps++) {
ps->id = i;
ps->attr = ATTR_EXT;
ps->reserved = 0;
ps->alias_checksum = cksum;
ps->start = 0;
offset = (i - 1) * 13;
fatwchar_to16(ps->name0_4, uname + offset, 5);
fatwchar_to16(ps->name5_10, uname + offset + 5, 6);
fatwchar_to16(ps->name11_12, uname + offset + 11, 2);
}
//最后一个的id的第5置1(因为是按倒序排列的,所以也可以认为是
//第一个),表明这是长目录项的最后一个
slots[0].id |= 0x40;
de = (struct msdos_dir_entry *)ps;
shortname:
/* build the entry of 8.3 alias name */
(*nr_slots)++;
memcpy(de->name, msdos_name, MSDOS_NAME);
de->attr = is_dir ? ATTR_DIR : ATTR_ARCH;
de->lcase = lcase;
fat_date_unix2dos(ts->tv_sec, &time, &date);
de->time = de->ctime = time;
de->date = de->cdate = de->adate = date;
de->ctime_cs = 0;
de->start = cpu_to_le16(cluster);
de->starthi = cpu_to_le16(cluster >> 16);
de->size = 0;
out_free:
free_page(page);
return err;
}
上面的函数是用于创建文件的时候的生成长短文件目录项的。那么我们再看一个函数fat_parse_long,来看看linux是如何解析长文件目录项的。其中输入参数dir为父inode节点,指针pos用于返回该长目录项在dir中的相对位置(单位为字节),bh用于读出一个sector的数据,de用于指向第一个slot,unicode用于返回长文件目录项名字的unicode,nr_slots用于返回slot的个数。
static int fat_parse_long(struct inode *dir, loff_t *pos,
struct buffer_head **bh, struct msdos_dir_entry **de,
wchar_t **unicode, unsigned char *nr_slots)
{
struct msdos_dir_slot *ds;
unsigned char id, slot, slots, alias_checksum;
if (!*unicode) {
*unicode = (wchar_t *)__get_free_page(GFP_KERNEL);
if (!*unicode) {
brelse(*bh);
return -ENOMEM;
}
}
parse_long:
slots = 0;
ds = (struct msdos_dir_slot *)*de;
id = ds->id;
//查看是否是长文件目录项的最后一项(或者说是第一项)
//注意查看vfat_build_slots函数中的slot[0].id |= 0x40
if (!(id & 0x40))
return PARSE_INVALID;
//应为是倒序排列的,所以第一个slot的id号就是slot总个数。
slots = id & ~0x40;
if (slots > 20 || !slots) /* ceil(256 * 2 / 26) */
return PARSE_INVALID;
*nr_slots = slots;
alias_checksum = ds->alias_checksum;
//该长文件目录项占用slot个msdos_dir_slot
slot = slots;
while (1) {
int offset;
slot--;
offset = slot * 13;
//提取unicode码
fat16_towchar(*unicode + offset, ds->name0_4, 5);
fat16_towchar(*unicode + offset + 5, ds->name5_10, 6);
fat16_towchar(*unicode + offset + 11, ds->name11_12, 2);
if (ds->id & 0x40)
(*unicode)[offset + 13] = 0;
//通过函数fat_get_entry继续查找下一个slot或者dir
if (fat_get_entry(dir, pos, bh, de) < 0)
return PARSE_EOF;
if (slot == 0)
break;
ds = (struct msdos_dir_slot *)*de;
if (ds->attr != ATTR_EXT)
return PARSE_NOT_LONGNAME;
if ((ds->id & ~0x40) != slot)
goto parse_long;
if (ds->alias_checksum != alias_checksum)
goto parse_long;
}
//到这儿位置,unicode的编码以字符串的形式保存在unicode中,同时de指向了该长文件名目录项的相应的短文件名目录项。继续查看该项是否被删除了。
if ((*de)->name[0] == DELETED_FLAG)
return PARSE_INVALID;
//#define ATTR_EXT (ATTR_RO | ATTR_HIDDEN | ATTR_SYS | ATTR_VOLUME),表示该文件不可见,既然不可见,那么就无视掉,继续查看下一个目录项
if ((*de)->attr == ATTR_EXT)
goto parse_long;
if (IS_FREE((*de)->name) || ((*de)->attr & ATTR_VOLUME))
return PARSE_INVALID;
if (fat_checksum((*de)->name) != alias_checksum)
*nr_slots = 0;
return 0;
}
曾经对FAT的目录项组织很困惑,因为不太明白FAT是以一种什么样的树状形式来呈现目录之间的父子关系。后来发现,一个父文件的所有文件目录项都包含在该父目录的数据区中。这样就非常明了了。所有的中间目录(树的枝干)的数据就是其子目录的目录项。最后的叶子目录(不存在子目录了)包含具体的数据。