在Linux上快速合并多个文件

15

我正在使用Python多进程来生成每个进程的临时输出文件。它们可以达到几GB大小,我会生成好几十个这样的文件。需要将这些临时文件连接在一起形成所需的输出,但是这一步正在成为瓶颈(也是并行性杀手)。是否有Linux工具可以通过修改文件系统元数据而不实际复制内容来创建连接文件?只要它在任何Linux系统上都能正常工作,我就可以接受。但特定于文件系统的解决方案对我帮助不大。

我没有经过操作系统或计算机科学培训,但理论上似乎应该可以创建一个新的inode,并从我要复制的文件的inode中复制inode指针结构,然后取消链接这些inode。是否有任何实用程序可以做到这一点?鉴于UNIX实用程序很多,我完全希望有这样的实用程序,但是找不到任何东西。因此,我在SO上提出了我的问题。该文件系统位于块设备上,实际上是硬盘,如果这些信息有用的话。由于我以前从未进行过系统级编程,因此我没有信心自己编写代码,因此任何指向C / Python代码片段的指针都将非常有帮助。


@san:作为背景,您能否简单说明一下为什么最终输出必须是一个单一的文件。 - NPE
@aix 这是传递给我无法控制的另一段代码的输入。 - san
@san:你事先知道每个临时文件的大小吗? - NPE
@aix 不,我不知道。每个进程的固定数量也不一样。虽然我可能愿意猜一个上限,但如果有一种无需猜测的方法那就更好了。 - san
@mmutz 它是否比在文件上使用系统调用 cat 更快。那是我现在所做的,而不是从 Python 中复制/连接文件的内容。该程序不会从 stdin 读取,但我认为我可以创建一个命名管道来处理它。 - san
显示剩余3条评论
6个回答

15
即使有这样的工具,它也只能在除了最后一个文件以外的文件保证大小是文件系统块大小的倍数的情况下才能起作用。
如果您控制如何将数据写入临时文件,并且知道每个文件的大小,可以采取以下步骤:
1. 在启动多进程之前,创建最终输出文件并通过fseek()到达末尾,将其增长到最终大小,这将创建稀疏文件。 2. 启动多进程,将FD和偏移量分配给每个特定文件切片的进程。 这样,进程将协作填充单个输出文件,无需稍后将它们连接在一起。
编辑
如果无法预测各个文件的大小,但最终文件的使用者可以使用顺序(而不是随机访问)输入,则可以将cat tmpfile1..tmpfileN提供给使用者,也可以通过stdin提供。
cat tmpfile1 ... tmpfileN | consumer

或者通过命名管道(使用bash的进程替换):

consumer <(cat tmpfile1 ... tmpfileN)

+1 好久没见到有人提到稀疏文件了。 - dietbuddha
刚才添加了一条关于此事的评论。我的主要问题是:(i)我无法保证每个进程将写入多少数据,(ii)也不知道文件的使用者将如何处理这些空洞。如果有一种方法可以检测和删除这些空洞,那么仍然值得一试。 - san

5
您表示您事先不知道每个临时文件的大小。考虑到这一点,我认为您最好编写一个FUSE文件系统,将这些块呈现为单个大文件,同时将它们作为底层文件系统上的单独文件保留。
在这种解决方案中,您的生产和消费应用程序保持不变。生产者编写一堆文件,FUSE层使其看起来像是单个文件。然后将此虚拟文件呈现给使用者。
FUSE有许多语言的绑定,包括Python。如果您查看一些示例(在此处)(在此处)(这些是不同绑定的示例),则需要的代码 surprisingly 很少。

谢谢。我之前不知道fuse。希望开销不会太高。如果它可以在大约3分钟内读取200GB,我会很高兴。 - san
1
@san:无论使用FUSE与否,我都预计读取将受到磁盘限制,因此我怀疑您在读取性能上不会看到任何差异。 - NPE
谢谢(实际上我正在做的连接是I/O绑定)。希望有一种方法可以接受两个答案,因为我喜欢FUSE和命名管道解决方案。我已经给两个答案点赞了,但只能接受一个。 - san

3

对于4个文件; xaa、xab、xac、xad,在bash中进行快速连接(作为root):

losetup -v -f xaa; losetup -v -f xab; losetup -v -f xac; losetup -v -f xad

假设loop0、loop1、loop2、loop3是新设备文件的名称。
http://pastebin.com/PtEDQH7G放入“join_us”脚本文件中。然后,您可以像这样使用它:
./join_us /dev/loop{0..3}

如果这个大文件是电影,你可以将其所有权交给普通用户(chown itsme /dev/mapper/joined),然后他/她可以通过以下方式播放它:mplayer /dev/mapper/joined。
这些操作之后需要进行清理(以root身份):
dmsetup remove joined; losetup -d /dev/loop[0123]

2
我认为不太可能,因为inode可能会对齐,所以只有在你可以在一个文件的页脚和另一个文件的页眉之间留下一些零(或未知字节)时才可能实现。
与其连接这些文件,我建议重新设计分析工具,以支持从多个文件中获取数据。以日志文件为例,许多日志分析器支持每天读取一个日志文件。
编辑:
@san:如你所说,你无法控制正在使用的代码,但是你可以通过使用命名管道来动态连接单独的文件。
$ mkfifo /tmp/cat
$ cat file1 file2 ... >/tmp/cat &
$ user_program /tmp/cat
...
$ rm /tmp/cat

你能详细说明一下inode对齐问题吗? - san
1
一个文件可能有12345字节的长度,如果inode的大小(可以通过sudo tune2fs -l /dev/sda1获取)为4096字节,则该文件占用4个inode。总共4个inode是16384字节,因此最后一个inode没有填满,因此在末尾留下了4096*4 - 12345 = 4039未知字节。 - Lenik
2
命名管道似乎是我们正在趋向的最方便的解决方案。@mmutz在这个线程上提出了建议。但感谢您对inode的解释。但是您指的是块,对吗?因为如果我没记错(其中r=read),每个文件对应一个单独的inode,但inode指针结构引用磁盘上的多个块。 - san
是的,我的错误,我是指块,而不是inode!O.O - Lenik

0

没有这样的工具或系统调用。

你可以探究一下每个进程是否可以直接写入最终文件。比如说,进程1写入0-X字节,进程2写入X-2X字节,以此类推。


是的,我考虑过这个问题,也就是我打开一个单一的文件并在不同的偏移量处进行写入。但我不确定文件的使用者将如何处理所创建的空洞。此外,我无法保证每个进程将写入多少数据。 - san

0
一个潜在的替代方案是将所有临时文件合并到一个命名管道中,然后将该命名管道用作单输入程序的输入。只要单输入程序按顺序读取输入而不寻找位置即可。

是的!我们刚在问题的评论部分讨论过这个问题。 - san

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接