译者 | 奇伢白眉林|李德姝黎
简述
Linux 下有 3 种“复本”,依次是 ln,cp,mv,这 3 个指示虽说都能 copy 出两个捷伊文档出。
细致入微的爸爸妈妈看见我给 “复本” 挂上了单引号?即使 Linux 的这 3 个指示有很大的差别,尽管采用者看上去是复本出了新文档。
你与否曾碰到过下列难题,圣埃蒂安德其原因了吗?:
ln 建立镜像文档,软镜像能跨文档系统,硬镜像跨文档系统会收起,为何?;
mv 好似有这时候快,有这时候十分慢,很多这时候还会残余废弃物,为何?;
cp 复本统计数据有这时候快,有这时候十分慢,源文档和最终目标文档所占力学内部空间居然不完全一致?
第一集该文看完,期望你以内难题无须有疑点,淡然采用 ln,mv,cp 指示。
温情提示信息:
下列他们只探讨文档的单纯操作方式,有关产品目录操作方式或是繁杂模块的操作方式无此他们此次主轴以内,他们忽视;
coreutils 库的标识符版用的是 8.3;
他们上看下单纯的 3 个指示操作方式。具体来说在继续执行下列指示以后,预备两个非常大的 test 的一般文档(比如说 1G )。
“复本”指示一:ln
# 建立两个软镜像文档
ln -s ./test ./test_soft_link
# 建立两个硬连接文档
ln ./test ./test_hard_link
你会发现当前产品目录出现了两个新文档 test_soft_link ,test_hard_link 。并且你会发现复本速度好快?为何呢?
“复本”指示二:mv
把 test 文档”复本”到 ./backup/ 产品目录
mv ./test ./backup/
更神奇的是,好似 copy 两个 1 G 的文档,速度也贼快?
“复本”指示三:cp
把 test 文档”复本”到 ./backup/ 产品目录
cp ./test ./backup/
上面他们看见,好似 ln,mv,cp 这 3 个指示都是“复本”?好似都进行了统计数据复制出了捷伊文档?
答案:当然不是。这 3 个看上去都是复制出了新文档,但其实天壤之别。他们两个个来揭秘。
在揭秘这 3 个指示以后,他们必须先复习文档的基础知识点,Linux 的文档和产品目录的关系。
Linux的文档和产品目录
在广度探究 Linux cp 的秘密一文中,他们详细探究了文档系统的形态。有几个关键知识点:
文档系统内有 3 个关键区域:超级块区域,inode 区域,统计数据 block 区域;
其中两个 inode 和两个文档对应,包含了文档的元统计数据信息;
一个 inode 有唯一的编号,能理解成就是单调递增的整数。比如说 1,2,3,4,5,6,,,,;
有关上面,他们注意到 inode 其实标识的是两个平坦的结构,inode 索引到统计数据 data 区域,每个 inode 都有唯一编号。
难题来了:Linux 的产品目录是两个倒挂的树形结构呀,为何上面说 inode 是平坦的结构?如下:
Linux 的文档确实是树形结构,inode 也确实是平坦的结构。你会感觉到即使是即使以后故意忽视了两个几个东西:产品目录文档和 dentry 项。这是两个十分重要的概念,他们逐个解释下。
文档系统中其实有两种文档类型,分为:
一般文档(这里把镜像文档包含在一般文档以内)
产品目录文件
能通过 inode->i_mode 字段,采用 S_ISREG,S_ISDIR 这两个宏来判断是哪个类型。一般文档很容易理解,就是一般的统计数据文档,inode 里面存储元统计数据,inode 能索引到 block,block 里面存储采用者的统计数据。产品目录文档 inode 存储元统计数据,block 里面存储的是产品目录条目。产品目录条目是什么样子的东西?
举个形象的例子:在当前 testdir 产品目录下,有 dir1,dir2,dir3 这三个文档。假设 dir1 的 inode 编号是 1024,dir2 是 1025,dir3 是 1026。
那么现实是这样的:
testdir 那个产品目录具体来说会对应有两个 inode,inode->i_mode 的类型是产品目录,并且还会有 block 块,通过 inode->i_blocks 能索引到这些 block;
block 里面存储的内容很单纯,是两个个产品目录条目,内核的名字缩写为 dirent,每两个 dirent 本质就是两个 文档名字 到 inode 编号的映射,所以,testdir 那个产品目录文档的 block 里存了 3 条记录 [dir1, 1024],[dir2, 1025],[dir3, 1026];
所以,产品目录到底是什么呢?就存储形态而已,产品目录也是文档,存储的是 名字 到 inode number 的映射表。dirent 其实就是 directory entry 的缩写。
好似还没讲到树形结构?
其实已经讲了一半了,树形结构的统计数据结构基础已经有了,就是产品目录文档和 dirent 的实现。
假设叶子结点的为一般文档
针对开篇的图,其实磁盘上存储了 3 个产品目录文档
那个这时候,读者朋友你是不是都能用笔画出两个树形结构了,内存的树形结构也是这么来的。通过磁盘的映射统计数据构造出。在内存中,那个树形结构的节点用 dentry 来表示(通常翻译成产品目录项,但是笔者认为那个翻译很容易让人误解)。
下列是笔者从内核精简出的 dentry 结构体,通过那个总结到几个信息:
dentry 绑定到唯一两个 inode 结构体;
dentry 有父,子,兄弟的索引路径,有那个就足够在内存中构建两个树了,并且事实也确实如此;
structdentry {
// …
structdentry *d_parent;/* 父节点 */
structqstrd_name;// 名字
structinode *d_inode;// inode 结构体
structlist_headd_child;/* 兄弟节点 */
structlist_headd_subdirs;/* 子节点 */
};
所以,看见现在理解了吗?父、子 指针,这就是经典的树形结构需要的字段呀。产品目录文档类型为树形结构提供了存储到磁盘持久化的一种形态,是一种 map 表项的形态,每两个表项他们叫做 dirent 。文档树的结构在内存中以 dentry 结构体体现。
划重点:仔细理解下 dirent 和 dentry 的概念和形态,仔细理解磁盘的统计数据形态和内存的统计数据结构形态,后面要考的。
ln指示
ln 是 Linux 的基础命令之一,是 link 的缩写,顾名思义就是跟镜像文档相关的两个指示。一般语法如下:
ln [OPTION]… TARGET LINK_NAME
ln 能用来建立两个镜像文档,有趣的是,镜像文档有两个不同的类别:
软镜像文档
硬镜像文档
1 什么是软镜像文档?
无论是软镜像还是硬镜像都是“镜像”文档,也就是说,通过那个镜像文档都能找到背后的那个“源文档”。具体来说说结论:
软镜像文档是两个全捷伊文档,有独立的 inode,有自己的 block ,而那个文档类型是“镜像文档”的类型而已;
那个软镜像文档的内容是一段 path 路径,那个路径直接指向源文档;
所以,你明白了吗?软镜像文档就是两个文档而已,文档里面存储的是两个路径字符串。所以软镜像文档能十分灵活,镜像文档本身和源解耦,只通过一段路径字符串寻路。
所以,软镜像文档是能跨文档系统建立的。
有兴趣的爸爸妈妈能去看源码实现,在coreutils库里,调用栈如下:
main -> do_link -> force_symlinkat -> symlinkat
也就是说最终调用的是系统调用 symlinkat 来完成建立,而那个 symlinkat 系统调用在内核由不同的文档系统实现。举个例子,如果是 minix 文档系统,那么对应的函数就是 minix_symlink。minix_symlink 那个函数上来就是新建两个 inode ,然后在对应的产品目录文档中添加两个 dirent 。来来来,他们看一眼 minix_symlink 的主干标识符:
staticintminix_symlink(struct inode * dir, struct dentry *dentry,
constchar * symname)
{
// …
// 新建两个 inode,inode 类型为 S_IFLNK 镜像类型
inode = minix_new_inode(dir, S_IFLNK | 0777, &err);
if (!inode)
goto out;
// 填充镜像文档内容
minix_set_inode(inode, );
err = page_symlink(inode, symname, i);
if (err)
goto out_fail;
// 绑定 dentry 和 inode
err = add_nondir(dentry, inode);
//…
}
划重点:软镜像文档是新建了两个文档,文档类型是镜像文档,文档内容就是一段字符串路径。分配捷伊 inode,内存对应捷伊 dentry ,当然了,也新增了两个 dirent 。软链文档能跨越不同的文件系统。
2 什么是硬镜像文档?
现在他们知道了,软镜像文档怎么找到源文档的?通过路径找到的,路径就存储在软镜像文档中。硬镜像文档又怎么办到的呢?
硬镜像很神奇,硬镜像其实是新建了两个 dirent 而已。下面是重点:
硬镜像文档其实并没有新建文档(也就是说,没有消耗 inode 和 文档所需的 block 块);
硬镜像其实是修改了当前产品目录所在的产品目录文档,加了两个 dirent 而已,那个 dirent 用两个捷伊 name 名字指向原来的 inode number;
重点来了,由于新旧两个 dirent 都是指向同两个 inode,那么就导致了两个限制:不能跨文档系统。即使,不同文档系统的 inode 管理都是独立的。
感兴趣的同学能试下,跨文档系统建立硬镜像就会报告如下错误:Invalid cross-device link
sh-4.4# ln /dev/shm/source.txt ./dest.txt
ln: failed to create hard link ./dest.txt => /dev/shm/source.txt: Invalid cross-device link
有兴趣的爸爸妈妈能去看源码实现,在 coreutils 库里,调用栈如下:
main -> do_link -> force_linkat -> linkat
也就是说最终调用的是系统调用 linkat 来完成建立,而那个 linkat 系统调用在内核由不同的文档系统实现。举个例子,如果是 minix 文档系统,那么对应的函数就是 minix_link。那个函数从内存上来讲是把两个 dentry 和 inode 关联起来。从磁盘统计数据结构上来讲,会在对应产品目录文档中增加两个 dirent 项。
划重点:硬镜像只增加了两个 dirent 项,只修改了产品目录文档而已。不涉及到 inode 数量的变化。捷伊 name 指向原来的 inode。
mv指示
mv 是 move 的缩写,从效果上上看,是把源文档搬移到另两个位置。
你与否思考过 mv 指示内部是怎么实现的呢?
是把源文档复本到最终目标位置,然后删除源文档吗?所以,说 mv 虽说也是“复本”?
其实,并不是,准确的说不完全是。
对于 mv 的探讨,要拆分成源和目的文档与否在同两个文档系统。
1 源 和 目的 在同两个文档系统
mv 指示的核心操作方式是系统调用 rename ,rename 从内核实现来说只涉及到元统计数据的操作方式,只涉及到 dirent 的增删(当然不同的文档系统可能略有不同,但是大致如是)。通常操作方式是删除源文档所在产品目录文档中的 dirent,在最终目标产品目录文档中添加两个捷伊 dirent 项。
划重点:inode number 不变,inode 不变,不增不减,还是原来的 inode 结构体,所以统计数据完全没有复本。
mv 的调用栈如下,感兴趣的能自己调试。
main -> renameat2
main -> movefile -> do_move -> copy -> copy_internal -> renameat2
他们用例子来直观看下,具体来说预备好两个 source.txt 文档,用 stat 指示看下元统计数据信息:
sh-4.4# stat source.txt
File: source.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 78h/120d Inode: 3156362 Links: 1
Access: (0644/-rw-r–r–) Uid: ( 0/ root) Gid: ( 0/ root)
他们看见 inode 编号是:3156362 。然后继续执行 mv 指示:
sh-4.4# mv source.txt dest.txt
然后 stat 看下 dest.txt 文档的信息:
sh-4.4# stat dest.txt
File: dest.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 78h/120d Inode: 3156362 Links: 1
Access: (0644/-rw-r–r–) Uid: ( 0/ root) Gid: ( 0/ root)
发现没?inode 编号还是 3156362 。
2 源和目的在不同的文档系统
还记得以后他们提过,由于硬镜像是直接在产品目录文档中添加两个 dirent,名字直接指向源文档的 inode ,不同文档系统都是独立的一套 inode 管理系统,所以硬镜像不能跨文档系统。
那么难题来了,mv 碰到跨文档系统的场景呢,怎么处理?与否还是 rename ?
举个例子,如下指示,源和目的是不同的文档系统。我虚拟机的挂载点如下:
sh-4.4# df -h
Filesystem Size Used Avail Use% Mounted on
overlay 59G 3.5G 52G 7% /
tmpfs 64M 0 64M 0% /dev
shm 64M 0 64M 0% /dev/shm
我故意挑选 /home/qiya/testdir 和 /dev/shm/ ,这两个产品目录依次对应了 “/” 和 “/dev/shm/” 的挂载点的文档系统,分属两个不同的文档系统。他们先提前看下源文档的信息(主要是 inode 信息):
sh-4.4# stat /dev/shm/source.txt
File: /dev/shm/source.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 7fh/127d Inode: 163990 Links: 1
Access: (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# stat dest.txt
File: dest.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 78h/120d Inode: 3155414 Links: 1
Access: (0644/-rw-r–r–) Uid: ( 0/ root) Gid: ( 0/ root)
对比有没有发现,inode 的信息是不一样的,inode number 是不一样的(是不是跟上面同一文档系统下的 mv 现象不完全一致)什么其原因呢?我下面一一道来,从原理出探究。
当系统调用 rename 的这时候,如果源和目的无此同一文档系统时,会报告 EXDEV 的错误码,提示信息该调用不能跨文档系统。
#define EXDEV 18 /* Cross-device link */
所以,rename 是不能用于跨文档系统的,那个这时候怎么办?
划重点:那个这时候操作方式分成两步走,先 copy ,后 remove 。
第一步:走不了 rename ,那么就退化成 copy ,也就是真正的复本。读取源文档,写入最终目标位置,生成两个全捷伊最终目标文档副本;
这里调用的 copy_reg 的函数封装(要知道那个函数是 cp 指示的核心函数,在 广度探究 Linux cp 的秘密 有深入探究过 );
ln,mv,cp 是在 coreutils 库里的指示,公用函数本身就是能复用的;
第二步:删除源文档,采用 rm 函数删除;
思考难题:mv 跨文档系统的这时候,如果第一步成功了,第二步失败了(比如说没有删除权限)会怎么样?
会导致废弃物。也就是说,最终目标处建立了两个新文档,源文档并没有删除。那个小实验有兴趣的能试下。
cp指示
cp 指示才是真正的统计数据复本指示,即复本元统计数据,也会复本统计数据。cp 指示也是我以后花了万字篇幅分析的指示,详细可见:广度探究 Linux cp 的秘密。这里就无须赘述,下面提炼出有关复本的 3 种模式。
涉及到统计数据复本的,关键有个 –sparse 模块,能控制复本统计数据的 IO 次数。
1 auto 模式
重点:跳过文档空洞。是 cp 默认的模式
cp src.txt dest.txt
2 always 模式重点:跳过文档空洞,还会跳过全 0 统计数据,是内部空间最省的模式。
cp –sparse=always src.txt dest.txt
3 never 模式重点:无脑复本,从头复本到尾,不识别力学空洞和全 0 统计数据,是速度最慢的一种模式。
cp –sparse=never src.txt dest.txt
复用以后画的这3张图,很形象的体现了cp的行为。
总结
产品目录文档是一种特殊的文档,能理解成存储的是 dirent 列表。dirent 只是名字到 inode 的映射,那个是树形结构的基础;
常说产品目录树在内存中确实是两个树的结构,每个节点由 dentry 结构体表示;
ln -s 建立软镜像文档,软镜像文档是两个独立的新文档,有两个捷伊 inode ,有捷伊 dentry,文档类型为 link,文档内容就是一条指向源的路径,所以软链的建立能无视文档系统,跨越山河;
ln 默认建立硬连接,硬镜像文档只在产品目录文档里添加了两个新 dirent 项 ,文档 inode 还是和原文档同两个,所以硬镜像不能跨文档系统(即使不同的文档系统是独立的一套 inode 管理方式,不同的文档系统实例对 inode number 的解释各有不同);
ln 指示虽说建立出了新文档,但其实不然,ln 只跟元统计数据相关,涉及到 dirent 的变动,不涉及到统计数据的复本,起不到统计数据备份的目的;
mv 其实是调用 rename 调用,在同两个文档系统中不涉及到统计数据复本,只涉及到元统计数据变更( dirent 的增删 ),所以速度也很快。但如果 mv 的源和目的在不同的文档系统,那么就会退化成真正的 copy ,会涉及到统计数据复本,那个这时候速度相对慢一些,慢成什么样子?就跟 cp 指示一样;
cp 指示才是真正的统计数据复本指示,速度可能相对慢一些,但是 cp 指示有 –spare 能优化复本速度,针对空洞和全 0 统计数据,能跳过,从而针对稀疏文档能节省大量磁盘 IO;