本篇博客的视频教程首发于 Youtube:科技小飞哥,加入 电报粉丝群 获得最新视频更新和问题解答。

背景

如果你突然被面试官问:cp和mv这两个linux的命令有什么区别?
你会不会一脸懵逼,cp不就是复制,mv不就是移动吗,还能有什么区别?
如果你也是这么想,那么这篇文章适合你。

inode

了解文件操作命令例如cpmvrm的底层原理时,需要先了解 linux 中文件系统的基本原理。

在linux系统中,文件系统对文件的存储和访问是通过一种被称为inode即i节点的机制来实现的。

为什么需要inode呢?
文件数据存储在硬盘上,硬盘的最小存储单位叫做"扇区"(512Bytes)。OS读取硬盘的时候,为了提高效率会一次性读取一个"块"(8*扇区=4K)。
所以一个大文件的数据内容在磁盘上可能不是连续空间的,就需要inode来把各个Block串联起来。

每个文件都对应一个 i 节点,i 节点存储了除文件名文件内容之外的所有信息。

inode(index node)表中包含文件系统所有文件列表,一个节点 (索引节点)是在一个表项,包含有关文件的信息( 元数据 ),包括:文件类型,权限,UID,GID、链接数(指向这个文件名路径名称个数)、该文件的大小和不同的时间戳、指向磁盘上文件的数据块指针、有关文件的其他数据。

inode

了解inode的基本信息之后,我们再看看cp, mv有什么区别。

cp

目标文件不存在时

dest.txt不存在时,执行cp src.txt dest.txt
可以发现dest.txtsrc.txt的inode不一样,也就是用open()新建一个文件dest.txt,然后读取src.txt的数据再写入dest.txt

cp前:

  • src.txt: Inode: 34643179

cp后:

  • dest.txt: Inode: 34257722
[root@instance-1 blog]# strace cp source.txt destination.txt 2>&1 | egrep 'source.txt|destination.txt'
execve("/bin/cp", ["cp", "source.txt", "destination.txt"], 0x7ffd8f1f3ea0 /* 23 vars */) = 0
stat("destination.txt", 0x7fff021b0040) = -1 ENOENT (No such file or directory)
stat("source.txt", {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
stat("destination.txt", 0x7fff021afda0) = -1 ENOENT (No such file or directory)
open("source.txt", O_RDONLY)            = 3
open("destination.txt", O_WRONLY|O_CREAT|O_EXCL, 0644) = 4

目标文件存在时

此时dest.txt已经存在,再次执行cp src.txt dest.txt
可以发现dest.txt跟上次执行的dest.txt的inode没有变化,同时看open()的参数也可以看出:先清空了dest.txt的内容,再把新的内容写入目标文件。没有文件的删除和创建,所以inode没有变化。

cp前:

  • src.txt: Inode: 34643179
  • dest.txt: Inode: 34257722

cp后:

  • dest.txt: Inode: 34257722
[root@instance-1 blog]# strace cp source.txt destination.txt 2>&1 | egrep 'source.txt|destination.txt'
execve("/bin/cp", ["cp", "source.txt", "destination.txt"], 0x7ffd4bfd93e0 /* 23 vars */) = 0
stat("destination.txt", {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
stat("source.txt", {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
stat("destination.txt", {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
open("source.txt", O_RDONLY)            = 3
open("destination.txt", O_WRONLY|O_TRUNC) = 4

结论

  • cp调用open系统函数,只会复制文件数据,不会复制inode索引节点的元数据。

mv

目标文件不存在时

当目标文件dest.txt不存在时,执行mv src.txt dest.txt
可以发现dest.txtsrc.txt的inode一样,底层调用了rename(),inode信息与src.txt的索引节点保持一致。
注:centos 7.5以上的版本,调用renameat2函数,7.5及以下的函数,依旧调用rename函数,没有本质的区别。

mv前:

  • src.txt: Inode: 34643179

mv后:

  • dest.txt: Inode: 34643179
[root@instance-1 blog]# strace mv src.txt dest.txt 2>&1 | egrep 'src.txt|dest.txt'
execve("/bin/mv", ["mv", "src.txt", "dest.txt"], 0x7ffd71529810 /* 23 vars */) = 0
stat("dest.txt", 0x7ffdb24b0fe0)        = -1 ENOENT (No such file or directory)
lstat("src.txt", {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
lstat("dest.txt", 0x7ffdb24b0c90)       = -1 ENOENT (No such file or directory)
renameat2(AT_FDCWD, "src.txt", AT_FDCWD, "dest.txt", 0) = 0

目标文件存在时

当目标文件dest.txt存在时,执行mv src.txt dest.txt
可以发现dest.txtsrc.txt的inode一样(之前的dest.txt的inode不见了),底层调用了rename(),inode变为src.txt的索引节点。

mv前:

  • src.txt: Inode: 34643179
  • dest.txt: Inode: 34257722

mv后:

  • dest.txt: Inode: 34643179
[root@instance-1 blog]# strace mv src.txt dest.txt 2>&1 | egrep 'src.txt|dest.txt'
execve("/bin/mv", ["mv", "src.txt", "dest.txt"], 0x7ffeef1ed240 /* 23 vars */) = 0
stat("dest.txt", {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
lstat("src.txt", {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
lstat("dest.txt", {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
renameat2(AT_FDCWD, "src.txt", AT_FDCWD, "dest.txt", 0) = 0

结论

  • mv调用rename系统调用,把src.txt重命名为目标文件,会将存储于inode索引节点上的文件元信息也移动到新文件中。

rm

在Linux中,要真正删除一个文件,需要满足两个条件:

  • 链接数为0
  • 没有进程打开该文件

系统调用unlink()是移除目标文件的一个链接。可以发现rm底层调用的其实就是unlink()

[root@instance-1 blog]# strace rm src.txt 2>&1 | egrep 'src.txt'
execve("/bin/rm", ["rm", "src.txt"], 0x7fffbf900e58 /* 23 vars */) = 0
newfstatat(AT_FDCWD, "src.txt", {st_mode=S_IFREG|0644, st_size=13, ...}, AT_SYMLINK_NOFOLLOW) = 0
unlinkat(AT_FDCWD, "src.txt", 0)        = 0

unlink系统调用

从文件系统中删除一个名称。如果名称是文件的最后一个连接,并且没有其它进程将文件打开,名称对应的文件会实际被删除。
如果文件仍旧是打开的,或者是被进程占用,其内容不会被删除。只有当进程关闭该文件或终止时(这种情况下,内核关闭该进程所打开的全部文件),该文件的内容才会被删除。

所以你可能会遇到,一个进程在读写文件时,你发现磁盘空间不足,使用rm删除文件,却发现磁盘空间却没有释放的情况。 使用lsof | grep deleted可以查看占用的进程。kill进程之后,文件才能真正的被删除。

结论

  • rm调用unlink系统调用,只有当所有的进程都不占用此文件的时候,才会真正的从磁盘删除。

替换可执行程序

不知道你还记不记得你是怎么替换可执行文件的,一般来说:

可当你使用

cp new_backend_server backend_server

的时候,提示Text file busy。 为什么呢,因为你这个文件正在被使用,当你清空并写入的时候,它能感知到修改,修改文件内容很可能导致程序逻辑错误甚至崩溃。所以禁止你对正在使用的文件执行cp替换。

当你执行:

rm backend_server
mv new_backend_server backend_server
service restart backend_server

就可以成功替换新的文件。
这又是为什么呢,因为当你使用rm&mv的时候是直接unlink旧的文件,由于文件被进程占用,实际上并没有删除,当你把新的文件mv到当前文件的时候,直接进行rename。并不会影响当前被进程占用的那个文件(新旧的inode不同,只是名字一样)。当你重启的时候,才会释放旧的文件,使用新的文件。

总结

最后总结一下:

  • cp调用open系统函数,只会复制文件数据,不会复制inode索引节点的元数据。(不改变inode)
  • mv调用rename系统调用,把src.txt重命名为目标文件,会将存储于inode索引节点上的文件元信息也移动到新文件中。(改变inode)
  • rm调用unlink系统调用,只有当所有的进程都不占用此文件的时候,才会真正的从磁盘删除。

<全文完>