如果是你,会怎么比较操作系统上的一个文件是否发生了变化呢?这是我在上一个周的工作里遇到的问题。我想,肯定会有人说,这不是很简单嘛,用 md5sum
去对文件进行校验和计算,如果前后两次的校验和不一致,那么文件就是被修改过的。
诚然,使用 md5sum
对文件求和是一种观察文件是否发生过修改的方式。但倘若使用场景稍微发生一些变化,我们需要观察的是一批文件呢,这些文件或大或小,大的文件近似100G,小的文件或者1M,文件中大小分布不均衡。那么在这样的使用场景中你还会觉得使用 md5sum
对文件求和是一种好办法吗?
或许对于大多数人的使用场景中,绝大部分情况下使用 md5sum
求和是的文件是一些小文件,而且也是单次求和,这样的场景下固然没有什么问题。但倘若连续对10个100G大小的文件进行求和,使用 md5sum
则会出现许多问题。
例如,计算校验和时硬盘的带宽会被消耗殆尽,导致操作系统上出现大量的D进程(不可中断进程),阻塞其他进程的执行。
盖因在操作系统上,文件通常存储在硬盘上,而使用 md5sum
计算校验和时,需要读取文件内容。而这个读取操作首先需要到硬盘上读取文件的位置,然后不断将文件的内容读取出来进行计算。这其中硬盘的读写性能至关重要,差一点的硬盘会在读取上消耗掉大量的时间。而D进程,也往往和磁盘的读写相关。
我刚刚说的是大文件与小文件共存的场景,别以为若是只有小文件就没有上面的问题了。(这里不考虑文件数量比较少的情况),大量的小文件读取难道不会产生大量的磁盘读吗?
实际上,硬盘读在操作系统上上一种非常消耗资源的事,尤其是大量的读,大量占用磁盘带宽并不是一个好的做法。因此,在操作系统设计中包含了 缓存(Cache)
,缓存的出现不正是为了更好的解决磁盘读的问题吗。
不过,这与我今天要说的并没多大关系,实际上我们也用不到 缓存
。
还是回到最初的问题上吧,如果是你,会怎么比较操作系统上的一个文件是否发生了变化呢?
。
既然和文件相关,那么文件自身的属性能否表明文件发生了修改呢?注意,这里我说的是文件内容有没有发生修改。
那么,文件的属性都有什么呢。通过 stat
来看一下吧
#stat testlog
File: ‘testlog’
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: fd00h/64768d Inode: 188836 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2022-04-17 11:44:27.361232934 +0800
Modify: 2022-04-17 11:44:27.361232934 +0800
Change: 2022-04-17 11:44:27.361232934 +0800
Birth: -
如上所示,文件自身所携带的属性众多。包含 File(文件名)、Size(文件大小)、Blocks(文件存储所消耗的硬盘块数量)、IO Block(操作系统发生一次IO可以查找的硬盘字节数)、regular empty file(表明当前文件是一个空文件)、Device、Inode(操作系统中的inode编号)、Links(操作系统中的引用计数)、Access(访问文件的时间)、Modify(文件被定义的时间)、Change(文件被修改的时间)、Birth。
看到这些文件属性,或许会觉得直接找到 Change
属性不就说明文件被修改过了吗?但是这真的正确吗?
在说下一步之前,有必要提一下可以修改文件属性的一些命令。touch
、mv
、cat
、grep
、less
、more
、vim
、echo
、chmod
、chown
等等。这部分命令部分修改文件内容,部分修改文件属性,这里不做区分
刚刚说到,直接查找文件的 Change
属性是不是可以证实文件被修改过?让我们重新 touch
一下文件,再看看文件属性的变化吧。
#touch testlog
#stat testlog
File: ‘testlog’
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: fd00h/64768d Inode: 188836 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2022-04-17 12:02:38.903708993 +0800
Modify: 2022-04-17 12:02:38.903708993 +0800
Change: 2022-04-17 12:02:38.903708993 +0800
Birth: -
可以看到,文件的三个和时间相关的属性通通发生了变化。但文件的内容并没有发生过修改。类似的场景包含使用 vim
打开一个文件、(未做修改的情况下)保存退出。文件的 Change
属性也会发生变化。
由此可见,我们认为的文件发生变化与操作系统对文件修改事件的定义是不同的。那么,单纯使用文件的 Change
和 Modify
属性也就无法说明文件是真的被修改过的。
那么,文件的 Change
和 Modify
两个属性对我们来说就没有用吗?不是的,正如我刚刚使用了 单纯 两个字。
单一的 Change
和 Modify
属性无法说明文件内容被修改过,那么 Change
+ Modify
+ md5sum
能否说明文件被修改过了。当然是可以的。
现在再回到最初的使用场景中,如果存在大量大小不均的大文件和小文件,需要找到其中内容发生修改过的文件呢。
通过记录文件的属性,从中查找前一次文件属性 Change
和 Modify
的时间戳,和当前的文件时间戳。如果文件的时间戳没有发生变化,那么文件是没有被修改过的。如果前后两次的时间戳不一致,在通过 md5sum
进行校验求和,从而判断文件是否发生过修改。
不可避免的,在一定程度上使用 md5sum
还是会在硬盘上产生大量读,但是通过时间戳的筛选过后,或许并不需要每个文件都去从硬盘上读一次。
或许,在绝大部分场景下,通过 Change
+ Modify
+ md5sum
去判断文件是否发生了内容修改已经可以大大降低操作系统的负载了。
当然,最后还是要提一下灵感的来源,inotify
一个针对系统内核事件的监听工具。网上可以看到许多基于 inotify + rsync
实现的远端节点文件同步。