当前位置: 首页 > 科技观察

深入解析Linux中的三个“复制”命令

时间:2023-03-17 10:30:07 科技观察

概述Linux下存在三个“复制”命令,分别是ln、cp、mv。这三个命令似乎可以复制一个新文件。细心的朋友看到??我在“copy”上加了双引号?因为Linux的这三个命令差别很大,虽然用户好像复制了一个新的文件。您是否遇到过以下问题并弄清楚原因?:ln创建链接文件,软链接可以跨文件系统,硬链接跨文件系统会报错,为什么呢?;mv好像有时候很快,有时候很慢,有时候垃圾,为什么?;cp拷贝数据有时候很快,有时候很慢,源文件和目标文件占用的物理空间居然不一致?看完本文,希望你对以上问题不再有疑惑,从容使用ln、mv、cp命令。提示:以下只讨论文件的简单操作。目录操作或者复杂参数的操作不在我们这次的题目中,我们忽略;coreutils库的代码版本为8.3;让我们来看看三个简单的命令操作。首先准备一个小的测试文件(比如1G),然后再执行下面的命令。“复制”命令一:ln#创建一个软链接文件ln-s./test./test_soft_link#创建一个硬链接文件ln./test./test_hard_link你会发现当前有两个新文件test_soft_link和test_hard_link目录。而且你会发现复制速度这么快?为什么?“复制”命令2:mv把测试文件“复制”到./backup/目录下mv./test./backup/更神奇的是,它好像复制了一个1G的文件,而且速度真快?“复制”命令三:cp将测试文件“复制”到./backup/目录下cp./test./backup/上面我们看到,好像ln、mv、cp这三个命令都是“复制”?好像是复制了数据,产生了新的文件?答:当然不是。这3个看似都复制了新的文件,其实差别很大。让我们一一揭秘。在揭秘这3条命令之前,我们首先要回顾一下文件的基础知识,Linux文件与目录的关系。Linux文件与目录在《深入剖析Linuxcp的秘密》一文中,我们详细分析了文件系统的形态。有几个关键知识点:文件系统中有3个关键区域:超级块区、inode区、数据块区;其中一个inode对应一个文件,包含该文件的元数据信息;一个inode有一个唯一的编号,可以理解为成就是单调递增的整数。例如,1、2、3、4、5、6、、、、;关于上面,我们注意到inode其实标识的是一个扁平结构,inode是索引到数据区的,每个inode都有一个唯一的编号。问题来了:linux目录是倒树结构,为什么说inode是扁平结构呢?如下:Linux文件确实是树状结构,inode确实是扁平结构。你会觉得是因为之前故意忽略了一些东西:目录文件和dentry项。这是两个很重要的概念,下面一一解释。文件系统中其实有两种文件,分为:普通文件(链接文件在这里包含在普通文件中)目录文件可以使用S_ISREG和S_ISDIR这两个宏通过inode->来判断自己是哪种类型i_mode字段。普通文件很好理解,就是普通的数据文件,inode存放元数据,inode可以索引成block,block存放用户数据。目录文件inode存储元数据,块存储目录条目。目录条目是什么样的?举个形象的例子:在当前testdir目录下,有dir1、dir2、dir3三个文件。假设dir1的inode号为1024,dir2为1025,dir3为1026。那么实际情况是这样的:testdir目录首先会对应一个inode,inode->i_mode的类型是一个目录,还会有blocks,可以通过inode->i_blocks进行索引;block中存储的内容很简单,就是一个目录项,内核名称缩写为dirent,每个dirent本质上是一个文件名到inode号的映射,所以,block中存储了3条记录目录文件testdir[dir1,1024],[dir2,1025],[dir3,1026];那么,究竟什么是目录?就存储形式而言,目录也是一个文件,存储的是从名字到inode号的映射表。dirent其实就是directoryentry的缩写。树结构好像还没说?其实说到一半了。树结构的数据结构基础已经有了,就是目录文件和dirent的实现。假设叶节点是普通文件。对于开局图,其实磁盘上存放了3个目录文件。这时候各位读者朋友们,能不能用笔画出一个树状结构,内存的树状结构也是这样的。.从磁盘上的映射数据构建。在内存中,这个树结构的节点用dentry表示(通常翻译成目录项,但笔者认为这种翻译容易误导)。下面是作者从内核中简化出来的dentry结构。通过这个总结,得到几个信息:dentry绑定了唯一的inode结构;dentry有parent,child,sibling索引路径,足以构建一棵树了,事实也确实如此;structdentry{//...structdentry*d_parent;/*父节点*/structqstrd_name;//namestructinode*d_inode;//inode结构structlist_headd_child;/*兄弟节点*/structlist_headd_subdirs;/*子节点*/};那么,你现在明白了吗?父子指针,这是经典树结构需要的字段。目录文件类型为树结构提供了一种存储到磁盘持久性的形式。它是映射条目的一种形式,每个条目称为dirent。文件树的结构体现在内存中的dentry结构中。要点:认真理解dirent和dentry的概念和形式,认真理解磁盘的数据形式和内存的数据结构形式,后面会考到。ln命令ln是Linux的基本命令之一,是link的缩写。顾名思义,就是与链接文件相关的命令。一般语法如下:ln[OPTION]...TARGETLINK_NAMEEln可用于创建链接文件。有趣的是,有两种不同类型的链接文件:软链接文件硬链接文件1什么是软链接文件?无论是软链接还是硬链接,都是一个“链接”文件,也就是说可以通过这个链接文件找到其背后的“源文件”。先说结论:软链接文件是一个全新的文件,有独立的inode和自己的block,这种文件类型只是一种“链接文件”类型;这个软链接文件的内容是一个路径,直接指向源文件;所以,你明白了吗?软链接文件只是一个文件,文件中存储了一个路径字符串。因此,软链接文件可以非常灵活。链接文件本身与源解耦,路径只能通过路径字符串找到。因此,可以跨文件系统创建软链接文件。感兴趣的朋友可以看看源码实现。在coreutils库中,调用栈如下:main->do_link->force_symlinkat->symlinkat,也就是说最终调用系统调用symlinkat完成创建,而这个symlinkat系统调用在内核中由不同的文件系统实现。例如,如果是minix文件系统,那么对应的函数就是minix_symlink。函数minix_symlink上来就是创建一个新的inode,然后在对应的目录文件中添加一个dirent。来,我们看一下minix_symlink的主要代码:staticintminix_symlink(structinode*dir,structdentry*dentry,constchar*symname){//...//创建一个新的inode,inode类型为S_IFLNKlinktypeinode=minix_new_inode(dir,S_IFLNK|0777,&err);if(!inode)gotoout;//填充链接文件内容minix_set_inode(inode,0);err=page_symlink(inode,symname,i);if(err)gotoout_fail;//binddentryandinodeerr=add_nondir(dentry,inode);//...}重点:软链接文件是新创建的文件,文件类型是链接文件,文件内容是字符串路径。分配一个新的inode,内存对应一个新的dentry。当然,也增加了一个新的目录。软链接文件可以跨越不同的文件系统。2什么是硬链接文件?现在我们知道了,软链接文件是如何找到源文件的呢?通过路径找到,路径存放在软链接文件中。硬链接文件呢?硬链接是惊人的。硬链接实际上是一种新的目录。下面是重点:硬链接文件实际上并不创建新文件(即不消耗inode和文件所需的block块);硬链接实际上修改了当前目录所在的目录文件,增加了一个dirent,这个dirent使用一个新的名字指向原来的inode号;关键是,由于新旧目录都指向同一个inode,这导致了一个限制:它们不能跨文件系统。因为,不同文件系统的inode管理是独立的。有兴趣的同学可以试试。创建跨文件系统的硬链接会报如下错误:Invalidcross-devicelinksh-4.4#ln/dev/shm/source.txt./dest.txtln:failedtocreatehardlink'./dest.txt'=>'/dev/shm/source.txt':Invalidcross-devicelink有兴趣的朋友可以去看看源码实现。在coreutils库中,调用栈如下:main->do_link->force_linkat->linkat也就是说最后会调用系统调用linkat完成创建,而这个linkat系统调用是由不同的文件系统实现的在内核中。例如,如果是minix文件系统,那么对应的函数就是minix_link。此函数在内存方面将dentry与inode相关联。在磁盘数据结构上,会在对应的目录文件中增加一个dirent项。重点:硬链接只是增加了一个dirent项,只是修改了目录文件。不涉及inode数量的变化。新名称指向原来的inode。mv命令mv是move的缩写。从效果上看,就是将源文件移动到另一个位置。你有没有想过mv命令在内部是如何实现的?是将源文件复制到目标位置,然后删除源文件吗?那么,好像mv也是“copy”的?实际上,不,不完全是。对于mv的讨论,应该分为源文件和目标文件是否在同一个文件系统。1源和目标在同一个文件系统中。mv命令的核心操作是系统调用重命名。从内核实现来看,rename只涉及到对元数据的操作,只涉及dirent的增删改查(当然不同的文件系统可能略有不同。但大致如此)。通常的操作是删除源文件所在目录文件中的dirent,在目标目录文件中添加新的dirent项。重点:inode号不变,inode不变,既不增加也不减少,还是原来的inode结构,所以根本没有复制数据。mv的调用栈如下,有兴趣的可以自行调试。main->renameat2main->movefile->do_move->copy->copy_internal->renameat2举个例子直接看,先准备一个source.txt文件,使用stat命令查看元数据信息:sh-4.4#statsource.txtFile:source.txtSize:0Blocks:0IOBlock:4096regularemptyfileDevice:78h/120dInode:3156362Links:1Access:(0644/-rw-r--r--)Uid:(0/root)Gid:(0/root)让我们看到inode号是:3156362然后执行mv命令:sh-4.4#mvsource.txtdest.txt然后stat查看dest.txt文件的信息:sh-4.4#statdest.txtFile:dest.txtSize:0Blocks:0IOBlock:4096regularemptyfileDevice:78h/120dInode:3156362Links:1Access:(0644/-rw-r--r--)Uid:(0/root)Gid:(0/root)你找到了吗?inode编号仍然是3156362。2源和目标在不同的文件系统中。还记得我们之前提到过的吗,因为硬链接就是直接在目录文件中添加一个dirent,名字直接指向源文件的inode。不同的文件系统是独立的inode管理系统。所以硬链接不能跨越文件系统。那么问题来了,mv遇到跨文件系统的场景怎么处理呢?还是重命名?例如,在以下命令中,源和目标是不同的文件系统。我的虚拟机挂载点如下:sh-4.4#df-hFilesystemSizeUsedAvailUse%Mountedonoverlay59G3.5G52G7%/tmpfs64M064M0%/devshm64M064M0%/dev/shm我特意选择了/home/qiya/testdir和/dev/shm/,这些这两个目录分别对应“/”和“/dev/shm/”挂载点的文件系统,属于两个不同的文件系统。先看一下源文件信息(主要是inode信息):sh-4.4#stat/dev/shm/source.txtFile:/dev/shm/source.txtSize:0Blocks:0IOBlock:4096regularemptyfileDevice:7fh/127dInode:163990Links:1Access:(0644/-rw-r--r--)Uid:(0/root)Gid:(0/root)我们执行如下mv命令:sh-4.4#mv/dev/shm/source.txt/home/qiya/testdir/dest.txt然后查看目标文件信息:sh-4.4#statdest.txtFile:dest.txtSize:0Blocks:0IOBlock:4096regularemptyfileDevice:78h/120dInode:3155414Links:1Access:(0644/-rw-r--r--)Uid:(0/root)Gid:(0/root)有没有发现inode信息不一样,inode号不一样(是不是和同一个文件里的mv一样systemabove?现象不一致)请问是什么原因?下面我就一一来,从原理上分析一下。系统调用rename时,如果源和目的不在同一个文件系统,会报EXDEV错误码,表示调用不能跨文件系统。#defineEXDEV18/*Cross-devicelink*/所以rename不能跨文件系统使用,这时候怎么办?要点:此时操作分为两步,先复制再删除。1.第一步:rename不行,就退化成copy,也就是真正的copy。读取源文件,写入目标位置,生成目标文件的全新副本;这里调用的copy_reg的函数包(要知道这个函数是cp命令的核心函数,在深入剖析linuxcp的秘籍中已经深入剖析);ln、mv、cp是coreutils库中的命令,公共函数本身可以复用;2、第二步:删除源文件,使用rm函数删除;思考问题:mv跨文件系统时,如果第一步成功,但第二步失败(比如没有删除权限)会怎样?会导致垃圾。也就是说,在目的地创建了一个新文件,而源文件没有被删除。对这个小实验感兴趣的可以试试。cp命令cp命令是真正的数据拷贝命令,即拷贝元数据,也拷贝数据。cp命令也是我之前花了几万字写的命令,详细可以看:深入剖析linuxcp的秘密。这里就不细说了,下面摘录三种复制方式。说到数据拷贝,关键是--sparse参数,可以控制拷贝数据的IO数。1自动模式对焦:跳过文件孔。就是cp默认模式cpsrc.txtdest.txt2alwaysmodefocus:skipfileholes,也跳过全0数据,是最省空间的模式。cp--sparse=alwaysrc.txtdest.txt3Never模式重点:无脑复制,从头到尾复制,不识别物理孔和全0数据,是最慢的模式cp--sparse=neversrc.txtdest.txt复用三者之前画的图,很形象的反映了cp的行为。总结1、目录文件是一种特殊的文件,可以理解为存放一个目录列表。dirent就是name到inode的映射,是树结构的基础;2、人们常说目录树在内存中确实是一个树结构,每个节点都用dentry结构表示;3.ln-s创建一个软链接文件,软链接文件是一个独立的新文件,有一个新的inode和一个新的dentry,文件类型是link,文件内容是一个指向源的路径,所以创建软链接的实现可以无视文件系统,跨越千山万水;4.ln默认创建硬链接。硬链接文件只是在目录文件中增加了一个新的dirent项。文件inode仍然和原文件一样,所以硬链接不能跨文件系统(因为不同的文件系统是一套独立的inode管理方式,不同的文件系统实例对inode号的解释不同);5.ln命令看似是新建一个文件,其实不然,ln只是和元数据有关,涉及到dirent6.mv其实是调用rename,不涉及同一个文件系统中的数据拷贝,但只涉及元数据的变化(dirent的增删改),所以速度也很快。但是如果mv的source和destination在不同的文件系统,那么就会退化成真正的副本,这就涉及到数据的拷贝。这个时候速度比较慢。有多慢?就像cp命令一样;7、cp命令是真正的数据拷贝命令,速度可能会比较慢,但是cp命令有--spare优化拷贝速度。文件可以节省大量的磁盘IO;