后端面试之Linux-cp和mv命令的区别
本篇博客的视频教程首发于 Youtube:科技小飞哥,加入 电报粉丝群 获得最新视频更新和问题解答。
背景
如果你突然被面试官问:cp和mv这两个linux的命令有什么区别?
你会不会一脸懵逼,cp不就是复制,mv不就是移动吗,还能有什么区别?
如果你也是这么想,那么这篇文章适合你。
inode
了解文件操作命令例如cp
、mv
、rm
的底层原理时,需要先了解 linux 中文件系统的基本原理。
在linux系统中,文件系统对文件的存储和访问是通过一种被称为inode
即i节点的机制来实现的。
为什么需要inode呢?
文件数据存储在硬盘上,硬盘的最小存储单位叫做"扇区"(512Bytes)。OS读取硬盘的时候,为了提高效率会一次性读取一个"块"(8*扇区=4K)。
所以一个大文件的数据内容在磁盘上可能不是连续空间的,就需要inode来把各个Block
串联起来。
每个文件都对应一个 i 节点,i 节点存储了除文件名
和文件内容
之外的所有信息。
inode(index node)表中包含文件系统所有文件列表,一个节点 (索引节点)是在一个表项,包含有关文件的信息( 元数据 ),包括:文件类型,权限,UID,GID、链接数(指向这个文件名路径名称个数)、该文件的大小和不同的时间戳、指向磁盘上文件的数据块指针、有关文件的其他数据。
了解inode的基本信息之后,我们再看看cp
, mv
有什么区别。
cp
目标文件不存在时
当dest.txt
不存在时,执行cp src.txt dest.txt
。
可以发现dest.txt
和src.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.txt
和src.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.txt
和src.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系统调用,只有当所有的进程都不占用此文件的时候,才会真正的从磁盘删除。
<全文完>