如何在嵌入式Linux中高效地在VFAT分区上创建大文件

Auf*_*ben 3 linux filesystems file-io embedded-linux

我试图在嵌入式Linux框中使用`dd'命令在VFAT分区上创建一个大的空文件:

dd if=/dev/zero of=/mnt/flash/file bs=1M count=1 seek=1023
Run Code Online (Sandbox Code Playgroud)

目的是跳过前1023个块并在文件末尾只写一个块,这在原生EXT3分区上应该非常快,而且确实如此.但是,此操作在VFAT分区上变得非常慢,并伴有以下消息:

lowmem_shrink:: nr_to_scan=128, gfp_mask=d0, other_free=6971, min_adj=16
// ... more `lowmem_shrink' messages
Run Code Online (Sandbox Code Playgroud)

另一种尝试是在VFAT分区上fopen()一个文件然后fseek()到最后写入数据,这也被证明是慢的,以及来自内核的相同消息.

基本上,有没有一种快速的方法在VFAT分区上创建文件(不遍历前1023块)?

谢谢.

Fra*_*kH. 10

为什么VFAT"跳过"写得这么慢?

除非在这方面使VFAT文件系统驱动程序"作弊",否则在FAT类型文件系统上创建大文件总是需要很长时间.符合FAT规范的驱动程序必须分配所有数据块并对它们进行零初始化,即使您"跳过"写入也是如此.这是因为"群集链"FAT确实如此.

这种行为的原因是FAT无法支持:

  • 文件中的UN*X风格的"漏洞"(又名"稀疏文件")
    就是你在ext3上用你的测试包创建的 - 一个没有分配给它的第一个1GB-1MB的数据块的文件,以及一个1MB的块最后实际提交的,零初始化的块)
  • NTFS样式的"有效数据长度"信息.
    在NTFS上,文件可以分配给它的未初始化块,但文件的元数据将保留两个大小字段 - 一个用于文件的总大小,另一个用于实际写入它的字节数(从文件的开头) .

如果没有支持这两种技术的规范,如果跳过一个范围,文件系统将始终必须分配和零填充所有"中间"数据块.

还记得在ext3上,你使用的技术实际上并没有为文件分配块(除了最后1MB).如果您需要预先分配的块(不仅仅是文件集的大小),那么您还必须在那里执行完整的写入.

如何修改VFAT驱动程序来处理这个?

目前,驱动程序使用Linux内核函数cont_write_begin()启动甚至异步写入文件; 这个功能看起来像:

/*
 * For moronic filesystems that do not allow holes in file.
 * We may have to extend the file.
 */
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);
}
Run Code Online (Sandbox Code Playgroud)

这是一个简单的策略,但也是一个pagecache trasher(你的日志消息是cont_expand_zero()所有工作调用的结果,而不是异步).如果文件系统将两个操作分开 - 一个任务执行"真实"写操作,另一个任务执行零填充操作,则看起来更加快捷.

在仍然使用默认的linux文件系统实用程序接口的同时实现这一目的的方法是在内部创建两个"虚拟"文件 - 一个用于待零填充区域,另一个用于实际要写入的数据.真实文件的目录条目和FAT群集链只有在后台任务实际完成后才会更新,方法是将其最后一个群集与"zerofill文件"的第一个群集以及该群集中的第一个群集连接到第一个群集.实际写文件".人们还希望使用directio编写来执行zerofilling,以避免破坏页面缓存.

注意:虽然所有这一切在技术上都是可能的,但问题是这样做是否值得进行这样的改变?谁一直需要这个操作?副作用会是什么?
对于较小的跳过写入,现有的(简单)代码是完全可以接受的,如果创建1MB文件并在末尾写入单个字节,则不会真正注意到它的存在.只有当您按照FAT文件系统允许的限制顺序查找文件大小时,它才会咬你.

其他选择......

在某些情况下,手头的任务涉及两个(或更多)步骤:

  1. 新格式化(例如)带有FAT的SD卡
  2. 将一个或多个大文件放在其上以"预填"卡
  3. (依赖于应用程序,可选)
    预先填充文件,或
    将回送文件系统映像放入其中

我工作过的其中一个案例我们已经折叠了前两个 - 即mkdosfs在制作(FAT32)文件系统时修改为预先分配/预创建文件.这很简单,在编写FAT表时只需创建分配的簇链而不是填充"free"标记的簇.如果您的应用程序从中受益,它还具有保证数据块是连续的优势.你可以决定mkdosfs 不做清除数据块的先前内容.例如,如果你知道你的一个准备步骤涉及到编写整个数据或者在文件中执行ext3-in-FAT(非常常见的事情 - 用于与windows app/gui进行数据交换的linux设备,sd卡),那么就没有必要将任何东西归零/双写(一次用零,一次用其他东西).如果您的用例适合这种情况(即格式化卡片无论如何都是"初始化它"的有用/正常步骤)然后尝试一下; 经过适当修改的mkdosfsTomTom的dosfsutils源代码的一部分,请参阅mkdosfs.c搜索-N命令行选项处理.

在谈到预分配时,如上所述,也有posix_fallocate().目前在Linux上使用FAT时,这将基本上与手册相同dd ...,即等待zerofill.但是函数的规范并不要求它是同步的.块分配(FAT簇链生成)必须同步完成,但VFAT磁盘上的直接大小更新和数据块zerofill可以后台/延迟(即在后台以低prio完成或仅在明确时完成通过fdsync()/ 请求sync()应用程序可以例如分配块,用非零本身写内容......).那是技术/设计; 如果仅用于实验,我还没有意识到有人已经完成了内核修改.