为什么VFAT“跳过”写操作如此缓慢?
除非VFAT文件系统驱动程序在这方面进行了“欺骗”,否则在FAT类型的文件系统上创建大文件将始终需要很长时间。驱动程序需要遵守FAT规范,分配所有数据块并对其进行零初始化,即使您“跳过”写操作也是如此。这是由于“簇链接”FAT所做的。
这种行为的原因是FAT无法支持以下任何一种:
- UNIX风格的文件中的“空洞”(也称为“稀疏文件”)
这就是您在ext3上使用测试用例创建的内容 - 一个没有分配到前1GB-1MB的数据块的文件,并且实际提交、零初始化块的单个1MB块)在文件末尾。
- NTFS样式的“有效数据长度”信息。
在NTFS上,文件可以有未初始化的块分配给它,但是文件的元数据将保留两个大小字段 - 一个用于文件的总大小,另一个用于实际写入到它的字节数(从文件开头开始)。
如果没有支持任何一种技术的规范,则文件系统始终需要分配和填充所有“中间”数据块,即使您跳过了一定范围。
还要记住,在ext3上,您使用的技术实际上并没有分配块给文件(除了最后1MB)。如果您需要预分配块(而不仅仅是设置文件大小),则还需要在那里执行完整的写操作。
如何修改VFAT驱动程序以处理此问题?
目前,驱动程序使用Linux内核函数cont_write_begin()
来启动甚至是异步写入文件;该函数如下:
int cont_write_begin(struct file *file, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata,
get_block_t *get_block, loff_t *bytes)
{
struct inode *inode = mapping->host;
unsigned blocksize = 1 << inode->i_blkbits;
unsigned zerofrom;
int err;
err = cont_expand_zero(file, mapping, pos, bytes);
if (err)
return err;
zerofrom = *bytes & ~PAGE_CACHE_MASK;
if (pos+len > *bytes && zerofrom & (blocksize-1)) {
*bytes |= (blocksize-1);
(*bytes)++;
}
return block_write_begin(mapping, pos, len, flags, pagep, get_block);
}
那是一种简单的策略,但也会导致页面缓存垃圾(你的日志信息是调用cont_expand_zero()的结果,它完成了所有工作,而且不是异步的)。如果文件系统将这两个操作分开——一个任务来执行“真正”的写入,另一个任务来执行零填充,它看起来会更加迅速。
在仍然使用默认的Linux文件系统实用程序接口的情况下,可以通过内部创建两个“虚拟”文件来实现这一点——一个用于待填零区域,另一个用于实际要写入的数据。只有在后台任务实际完成时,通过将其最后一个簇与“零填充文件”的第一个簇以及该文件的最后一个簇与“实际写入文件”的第一个簇相连接,才会更新真实文件的目录项和FAT簇链。为了避免页面缓存垃圾,还需要进行直接写入以进行零填充。
注意:虽然所有这些从技术上来说肯定是可行的,但问题是做出这样的改变是否值得?谁一直需要这个操作?会有什么副作用?现有的(简单)代码对于较小的跳过写入是完全可接受的,如果您创建一个1MB的文件并在末尾写入一个字节,您几乎不会注意到它的存在。只有当您需要达到FAT文件系统允许的极限文件大小时,它才会对您造成影响。
其他选择...在某些情况下,手头的任务涉及两个(或多个)步骤:
- 使用FAT格式对SD卡进行新的格式化
- 将一个或多个大文件放入其中以“预填充”该卡
- (与应用程序相关,可选)
预填充文件,或
将回环文件系统映像放入其中
我曾经处理过的一个案例中,我们折叠了前两个步骤 - 即修改mkdosfs
以在创建(FAT32)文件系统时预分配/预创建文件。这相当简单,在写入FAT表时,只需创建已分配的簇链,而不是填充了“空闲”标记的簇。它还具有数据块保证是连续的优点,以防您的应用从中受益。并且您可以决定使mkdosfs
不清除数据块的先前内容。如果您知道,例如,您的准备步骤之一涉及无论如何都要写入整个数据或在FAT上执行ext3-in-file(非常常见的事情-用于与Windows应用程序/ GUI交换数据的Linux设备,sd卡),则不需要清零任何内容/双重写入(一次为零,一次为其他内容)。如果您的用例符合此条件(即格式化卡是“初始化为使用”过程中有用/正常的步骤),则请尝试一下;经过适当修改的mkdosfs
是TomTom的dosfsutils源代码,查看mkdosfs.c
搜索-N
命令行选项处理的一部分。
谈到预分配,如前所述,还有posix_fallocate()
。目前在Linux上使用FAT时,这将与手动dd ...
基本相同,即等待零填充。但是,该函数的规范并不要求它是同步的。块分配(FAT集群链生成)必须同步完成,但VFAT磁盘上的dirent大小更新和数据块零填充可以在后台/延迟执行(即仅在低优先级后台执行或仅在明确请求通过fdsync()
/ sync()
执行以便应用程序可以例如自己分配块,写入非零内容...)。这是技术/设计问题;我不知道是否有人已经对内核进行了修改,即使只是为了实验。