一、npm
1、在npm1
和npm2
的时候。依赖结构是这样的
go
node_modules
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
└─ package.json
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
└─ package.json
-
上面这种结构会导致哪些问题呢?让我们来分析下:
(1)依赖包会被重复安装
A
依赖B
,C
也依赖B
。也就是说B
同时是A
和C
的依赖。这种情况下。B
会被下载两次。npm1
和npm2
的运行逻辑是,某一个包被其他包依赖N
次,就需要被下载N次。也就是我们所说的重复安装。
(2)
React
实例无法共享- 举个例子,同一node_modules目录下A包中引入了 React (import React from 'react'),C包中也引入了 React (import React from 'react')虽然同为React,但这两个React其实是两个实例,有各自的存储空间。这样就意味着。存储在A包的React实例内容,无法通过C包的React实例引用到。
(3)依赖层级太多问题
-
假设我们有这么一个依赖
gonode_modules └─ 依赖A ├─ index.js ├─ package.json └─ node_modules └─ 依赖B ├─ index.js ├─ package.json └─ node_modules └─ 依赖C ├─ index.js ├─ package.json └─ node_modules └─ 依赖D ├─ index.js └─ package.json
-
上方的代码示例就有四层。如果我们有某一个依赖了10层呢?他也一样会一层一层依赖下去。像是"依赖地狱"。可读性不高。
2、npm3
和yarn
中平铺的结构依然存在的问题
- 上述
npm1
和npm2
的这些问题在npm3+
和yarn
中得到了解决,从npm3
开始。npm3
和yarn
都采用了"扁平化依赖"来解决上述问题。 - 采用了扁平化管理之后,代码示例(一) 就从嵌套结构变成了扁平化结构
go
node_modules
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
├─ package.json
└─ node_modules
-
所有的依赖都会被平铺到同一层面。这样,因为require寻找包的机制。如果A和C都依赖了B。那么A和C在自己的node_modules中未找到依赖B的时候会向上寻找,并最终在与他们同级的node_modules中找到依赖包B。 这样,就不会出现重复下载的情况。而且依赖层级嵌套也不会太深。因为没有重复的下载,所有的A和C都会寻找并依赖于同一个B包。自然也就解决了实例无法共享数据的问题
-
这里需要提一嘴require寻找依赖包的机制,require/import在引入一个包的时候(比如require("express"),不考虑使用路径引入的情况);require、import会从自己当前的node_modules的一级文件(或文件夹)中寻找这个依赖,如果当前依赖没有,则会去上一级的node_modules中寻找该依赖,以此类推一直到根节点。到跟节点都没找到依赖的话则会报错。
-
这种平铺的结构看似完美,但其实依然存在一些细节上的问题。比如
(1)依赖结构的不确定性
- 上面我们已经说过,
npm3
和yarn
已经开始采用扁平化的依赖结构。也就是说会将所有的依赖包不分几级依赖的全部都平铺到同一个层级中。然后通过包require
引入的特性去访问。那么问题来了,如果有这么一个依赖形式呢?A
包和C
包依赖于B
包的不同版本(也就是两个不同的包依赖于同一个包的不同版本)。
go
node_modules
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B 1.0
├─ index.js
└─ package.json
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B 2.0
├─ index.js
└─ package.json
-
因为同一目录下不能出现两个同名的文件,所以如果依赖于同一个包的不同版本,那么有一个版本注定还是要被嵌套依赖。在这种情况下,你认为扁平化的依赖会是哪种形式,答案是:都有可能。
-
这完全取决于
A
包和C
包在package.json
中的位置。在前面先处理的依赖会被扁平化到A
,C
两包的同级目录中。后处理的包当npm3/yarn
打算把依赖包平铺的时候会发现目录下已经有该包的另一个版本。所以后处理的包不会扁平化处理,保持原来的位置。 -
npm install
时,首先将package.json
里的依赖按照首字母(@排最前)进行排序,然后将排序后的依赖包按照广度优先遍历的算法进行安装,最先被安装到的模块将会被优先安装在一级node_modules
目录下。
所谓广度优先遍历的安装方式,就是优先将同一层级的模块包及其依赖安装好,而不是优先将一个模块及其所有的子模块安装好。
(2)扁平化算法的复杂度比较高,相对的比较耗时。
- 这也是一个问题。要知道,对文件的操作可不像我们给数组和对象进行排序。这种深度嵌套的结构。
npm3/yarn
在处理过程中相对的麻烦。所需要运算的时间会比较长。尤其是大项目,依赖了很多很多包的时候,我们会明显的感觉到,npm
的依赖安装变慢了。
(3)项目中还是存在可以非法访问的问题
-
什么叫非法访问?举个例子,项目依赖
B
包,B
包依赖C
包,然后你就会惊讶的发现,你在项目中,居然可以访问到C
包里的内容。 -
原理解释:
- 因为扁平化的处理机制,身为B包依赖的
C
包,也会被放到和B包同级别的node_modules
下,而我们在项目中使用require/import
去引入C
包的时候。require
的机制会去当前的node_modules
下(也就是B
包所在的那个目录下)寻找C
包。然后,它就找到了,我们就拿来用了,这就是非法访问,也就是所谓的幽灵依赖。
- 因为扁平化的处理机制,身为B包依赖的
-
这会造成什么问题呢?
- 如果你在项目中使用了
C
包@1.0,那么有一天,B
包升级了,他的依赖C
包也跟着升级成了@2.0
并遗弃了C@1.0
在项目中使用的某个api
。那么这个时候,项目就会报错。而这种错误,一旦出现,处理起来就会非常棘手,尤其是当我们有大量使用的时候,那就很难办了。
- 如果你在项目中使用了
3、package-lock.json
-
从
npm 5.x
开始,执行npm install
时会自动生成一个package-lock.json
文件。npm
为了让开发者在安全的前提下使用最新的依赖包,在package.json
中通常做了锁定大版本的操作,这样在每次npm install的时候都会拉取依赖包大版本下的最新的版本。这种机制最大的一个缺点就是当有依赖包有小版本更新时,可能会出现协同开发者的依赖包不一致的问题。``package-lock.json``文件精确描述了
node_modules` 目录下所有的包的树状依赖结构,每个包的版本号都是完全精确的。 -
**
package-lock.json
文件和node_modules
目录结构是一一对应的,即项目目录下存在package-lock.json
可以让每次安装生成的依赖目录结构保持相同。**在开发一个应用时,建议把package-lock.json
文件提交到代码版本仓库,从而让你的团队成员、运维部署人员或CI
系统可以在执行npm install
时安装的依赖版本都是一致的。但是在开发一个库时,则不应把package-lock.json
文件发布到仓库中。实际上,npm
也默认不会把package-lock.json
文件发布出去。之所以这么做,是因为库项目一般是被其他项目依赖的,在不写死的情况下,就可以复用主项目已经加载过的包,而一旦库依赖的是精确的版本号那么可能会造成包的冗余。 -
版本说明
-
固定版本: 比如
5.38.1
,安装时**只安装指定版本。 ** -
**波浪号: 比如
~5.38.1
, 表示安装5.38.x
的最新版本(不低于5.38.1),但是不安装5.39.x
,也就是说安装时不改变大版本号和次要版本号。 ** -
插入号: 比如
ˆ5.38.1
, ,表示安装5.x.x
的最新版本(不低于5.38.1),但是不安装6.x.x
,也就是说安装时不改变大版本号。
需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
latest
: 安装最新版本。 -
4、npm
中的依赖包
4.1、依赖包分类
-
在
node
中其实总共有5种依赖:-
dependencies
- 业务依赖 -
devDependencies
- 开发依赖 -
peerDependencies
- 同伴依赖 -
bundledDependencies
/bundleDependencies
- 打包依赖 -
optionalDependencies
- 可选依赖
作为
npm
的使用者,我们常用的依赖是dependencies
和devDependencies
,剩下三种依赖则是作为包的发布者才会使用到的字段。 -
4.2、dependencies
-
这种依赖在项目最终上线或者发布
npm
包时所需要,即其中的依赖项应该属于线上代码的一部分。比如框架vue
,第三方的组件库element-ui
等,这些依赖包都是必须装在这个选项里供生产环境使用。 -
通过命令
npm install/i packageName -S/--save
把包装在此依赖项里。如果没有指定版本,直接写一个包的名字,则安装当前npm
仓库中这个包的最新版本。如果要指定版本的,可以把版本号写在包名后面,比如npm i vue@3.0.1 -S
。 -
从
npm 5.x
开始,可以不用手动添加-S/--save
指令,直接执行npm i packageName
把依赖包添加到dependencies
中去。
4.3、devDependencies
-
这种依赖只在项目开发时所需要,即其中的依赖项不应该属于线上代码的一部分。比如构建工具
webpack
、gulp
,预处理器babel-loader
、scss-loader
,测试工具e2e
、chai
等,这些都是辅助开发的工具包,无须在生产环境使用。 -
通过命令
npm install/i -D/--save-dev
把包安装成开发依赖。如果想缩减安装包,可以使用命令npm i --production
忽略开发依赖,只安装基本依赖,这通常在线上机器(或者QA
环境)上使用。 -
千万别以为只有在
dependencies
中的模块才会被一起打包,而在devDependencies
中的不会!模块能否被打包,取决于项目里是否被引入了该模块! 在业务项目中dependencies
和devDependencies
没有什么本质区别,只是单纯的一个规范作用,在执行npm i
时两个依赖下的模块都会被下载;而在发布npm
包的时候,包中的dependencies
依赖项在安装该包的时候会被一起下载,devDependencies
依赖项则不会。 (详情参考前端package.json.md
)
4.4、peerDependencies
-
这种依赖的作用是提示宿主环境去安装插件在
peerDependencies
中所指定依赖的包,然后插件所依赖的包永远都是宿主环境统一安装的npm
包,最终解决插件与所依赖包不一致的问题。 -
这句话听起来可能有点拗口,举个例子来给大家说明下。
element-ui@2.6.3
只是提供一套基于vue
的ui
组件库,但它要求宿主环境需要安装指定的vue
版本,所以你可以看到element
项目中的package.json
中具有一项配置:json"peerDependencies": { "vue": "^2.5.16" }
-
它要求宿主环境安装
3.0.0 > vue@ >= 2.5.16
的版本,也就是element-ui
的运行依赖宿主环境提供的该版本范围的vue
依赖包。 -
在安装插件的时候,
peerDependencies
在npm 2.x
和npm 3.x
中表现不一样: -
在
npm 2.x
中,安装包中peerDependencies
所指定的依赖会随着npm install packageName
一起被强制安装,并且peerDependencies
中指定的依赖会安装在宿主环境中,所以不需要在宿主环境的package.json
文件中指定对所安装包中peerDependencies
内容的依赖。 -
在
npm 3.x
中,不会再要求peerDependencies
所指定的依赖包被强制安装,npm 3.x
只会在安装结束后检查本次安装是否正确,如果不正确会给用户打印警告提示,比如提示用户有的包必须安装或者有的包版本不对等。 -
大白话:如果你安装我,那么你最好也要按照我的要求安装
A
、B
和C
。
5、package.json
中的 bin
字段
-
package.json
中的字段bin
表示的是一个可执行文件到指定文件源的映射。通过npm bin
指令显示当前项目的bin
目录的路径。例如在@vue/cli的package.json
中:json"bin": { "vue": "bin/vue.js" }
-
如果全局安装
@vue/cli
的话,@vue/cli
源文件会被安装在全局源文件安装目录(/user/local/lib/node_modules
)下,而npm
会在全局可执行bin
文件安装目录(/usr/local/bin
)下创建一个指向/usr/local/lib/node_modules/@vue/cli/bin/vue.js
文件的名为vue
的软链接,这样就可以直接在终端输入vue
来执行相关命令。 -
如果局部安装
@vue/cli
的话,npm
则会在本地项目./node_modules/.bin
目录下创建一个指向./node_moudles/@vue/cli/bin/vue.js
名为vue
的软链接,这个时候需要在终端中输入./node_modules/.bin/vue
来执行。
6、PATH 环境变量
-
在
terminal
中执行命令时,命令会在PATH
环境变量里包含的路径中去寻找相同名字的可执行文件。局部安装的包只在./node_modules/.bin
中注册了它们的可执行文件,不会被包含在PATH
环境变量中,这个时候在terminal
中输入命令将会报无法找到的错误。 -
那为什么通过
npm run
可以执行局部安装的命令行包呢?- 因为每当执行
npm run
时,会自动新建一个Shell
,这个Shell
会将当前项目的node_modules/.bin
的绝对路径加入到环境变量PATH
中,执行结束后,再将环境变量PATH
恢复原样。
- 因为每当执行
二、yarn
-
2016年
yarn
的出现主要解决了npm
的两个主要问题,- 语义控制导致
npm
安装不确定的问题。 npm
安装包依赖嵌套的的问题(虽然npm3
及时的解决了这个问题)。
- 语义控制导致
-
yarn
和npm
的主要区别介绍:yarn
虽然和npm
一样是本地缓存,但是yarn
无需互联网链接就能安装本地缓存的依赖项,提供了离线模式,而这个是npm
实现不了的。yarn
解决了由于语义版本控制而导致的npm
的不确定性问题,通过安装时创建的默认文件,确保使用的库的版本相同。yarn
增加了一些能让开发人员并行化处理所有必需的操作,且通过添加了一些改进,使得运行速度有了显著的提升,整个安装时间也变得更少。npm
的输出信息比较冗长,相比之下,yarn
简洁很多。默认情况下,直观且直接地打印出必要的信息。yarn
的语义相对于npm
更加清晰,主要是因为yarn
改变了一些npm
的命令名称,看上去就更清晰了。
三、pnpm
-
和
npm
,yarn一样,pnpm
是一个包管理工具。不一样的是,pnpm
解决了npm
和yarn
一直都没有解决的痛点。在许多方面比npm
和yarn
更优秀。 -
pnpm
对比npm/yarn
的优点:- (1)更快速的依赖下载
- (2)更高效的利用磁盘空间
- (3)更优秀的依赖管理
-
为什么说
pnpm
会比npm
和yarn
更高效的利用磁盘空间?-
pnpm
有一个store
的概念(是一块存储文件的空间) -
store
是pnpm
在硬盘上的公共存储空间,pnpm
的store
在Mac/linux
中默认会设置到{home dir}>/.pnpm-store/v3;windows
下会设置到当前盘符的根目录下。使用名为.pnpm-store
的文件夹名称。项目中所有.pnpm/依赖名@版本号/node_modules/
下的软连接都会连接到pnpm
的store
中去。 -
内部使用"基于内容寻址"的文件系统来存储磁盘上所有的文件 ,基于内容寻址是一种文件系统的设计原则,以文件内容的哈希值作为文件的唯一标识符,并将文件存储在以哈希值命名的目录中。这样的设计使得相同内容的文件只会存储一次,避免了重复存储相同文件的问题。在
pnpm
的store
目录中,每个文件的名称都是其内容的哈希值,因此同一个文件无论在多个项目中使用,都只会被存储一次。当安装或更新依赖项时,pnpm
会检查store
目录中是否已经存在所需的文件,如果存在则直接复用,避免了重复的下载和存储。
-
-
这一套系统的优点是:
(1)更快速的依赖下载
- 由于文件已经存在于
store
中,所以安装依赖项时可以直接使用已有的文件,而无需下载和解压,从而提高了安装速度。
(2)不会重复下载依赖
-
4个项目 都依赖了
express.js
(第三方插件)。如果是npm/yarn
的话,express.js
就会被安装4个在你的磁盘空间当中。但是**pnpm
得益于"基于内容寻址"的文件系统,使用pnpm
下载的文件不论被多少项目所依赖,都只会在磁盘中写入一次。后面再有项目使用的时候会采用硬链接的方式去索引那一块磁盘空间。** -
如果有一天你所依赖的版本提升了。假设从
express@2.0
升级到了express@3.0
。而express@3.0
比express@2.0
多了20个文件。这个时候pnpm
并不会删除express@2.0再去重新下载express@3.0
。而是复用express@2.0
原本的磁盘内容。再在express@2.0
的基础上增加20个文件形成express@3.0
。
(3)更优秀的依赖管理
-
使用
pnpm
下载的所有依赖都会以.pnpm/依赖名@版本号/node_modules/
这种形式被存储。乍一看,好像又从扁平化管理回到嵌套结构了。性能不是倒退了吗?不是的。一开始从npm1
和npm2
的嵌套结构变成扁平化结构是为了解决:- 包被重复安装
- 依赖示例无法共享
- 依赖层级太深
-
这三个问题。而
.pnpm/依赖名@版本号/node_modules/
下面的依赖也全部都是软连接。这些软连接指向存储在store
中的文件。 -
发现这种设计的巧妙之处了吗。因为
.pnpm/依赖名@版本号/node_modules/
下面都是软连接,他们指向同一块存储空间。所以也就不存了包会重复安装和依赖实例无法共享的问题。而express
所有的依赖都会在.pnpm/依赖名@版本号/node_modules/
这个目录下被扁平化处理,同样解决了依赖结构太深的问题。还有将包本身和依赖放到同一目录下,这样,利用require
的特性也能够找到所有的依赖包。再将包本身的软连接放到外层的node_modules
中。这样,node_modules
中的包在结构上就几乎和package.json
中的内容保持一致。为什么说几乎一致而不是完全一致?因为有些包有变量提升,会被提升到外层node_modules
中。但是大体上还是一样的。到这里,pnpm
就又解决了npm3/yarn
当时没有解决的依赖结构的不确定性。
- 由于文件已经存在于
-
关于
npm3/yarn
未解决的非法访问问题-
**当我们在项目中引用的时候,他会去
node_modules
中去寻找。**由上图可知,只会在node_modules
下面的第一层去寻找!!! 而pnpm
的机制会让node_moduels
下只有一级依赖包的软链接(也就是说如果你下载一个express
,那么项目级别的node_modules
下就只有express
的软连接而没有express
的依赖包的软链接)。所以如果你在自己的项目中直接去引用二级依赖包的话,会报错,直接找不到 -
举个例子,我们下载了一个
A
包。A
包的软连接在项目级别的node_modules
中,但是A
包的所有依赖包都在.pnpm/依赖名@版本号/node_modules/
下面。假设C
包是A
包的依赖包。我们直接在项目中使用require/import
引用C
包的话。require/import
会去项目级别的node_modules
中寻找。然后就会发现项目级别的node_modules
下面只有一个A
包的软连接,没有A
包的依赖C
包,找不到,就会报错。这样,只要C
包没有在项目的package.json
中声明,就无法访问(避免了非法访问)。所以,到这里,pnpm
也解决了npm3
和yarn
中非法访问的问题。而且,非法访问的这个问题以当前的npm
和yarn
的版本,使用在项目级别的node_modules
下进行扁平化的管理的机制,几乎无法避免。pnpm
却完美的解决了。
-
四、软链接和硬链接
1、inode
(1)理解inode
,要从文件储存说起。
- 文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于
0.5KB
)。 - 操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(
block
)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB
,即连续八个 扇区组成一个 块。 - 文件数据都储存在"块"中 ,那么很显然,我们还必须找到一个地方储存文件的元信息 ,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做
inode
,中文译名为"索引节点"。 - 每一个文件都有对应的
inode
,里面包含了与该文件有关的一些信息。
(2)inode
的内容
-
inode
包含文件的元信息,具体来说有以下内容:- 文件的字节数
- 文件拥有者的
User ID
- 文件的
Group ID
- 文件的读、写、执行权限
- 文件的时间戳,共有三个:
ctime
指inode
上一次变动的时间,mtime
指文件内容上一次变动的时间,atime
指文件上一次打开的时间。 - 链接数,即有多少文件名指向这个**
inode
** - 文件数据
block
的位置
-
可以用
stat
命令,查看某个文件的inode
信息:bashstat example.txt
-
总之,除了文件名以外的所有文件信息,都存在
inode
之中。至于为什么没有文件名,下文会有详细解释。
(3)inode
的大小
inode
也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode
区(inode table
),存放inode
所包含的信息。- 每个
inode
节点的大小,一般是128字节或256字节。inode
节点的总数,在格式化时就给定,一般是每1KB
或每2KB
就设置一个inode
。假定在一块1GB
的硬盘中,每个inode
节点的大小为128字节,每1KB
就设置一个inode
,那么inode table
的大小就会达到128MB
,占整块硬盘的12.8%。 - 由于每个文件都必须有一个
inode
,因此有可能发生inode
已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。
(4)inode
号码
- 每个
inode
都有一个号码,操作系统用inode
号码来识别不同的文件。 - 这里值得重复一遍,
Unix/Linux
系统内部不使用文件名,而使用inode
号码来识别文件。对于系统来说,文件名只是inode
号码便于识别的别称或者绰号。 - 表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的
inode
号码;其次,通过inode
号码,获取inode
信息;最后,根据inode
信息,找到文件数据所在的block
,读出数据。
(5)目录文件
Unix/Linux
系统中,目录(directory
)也是一种文件。打开目录,实际上就是打开目录文件。- 目录文件的结构非常简单,就是一系列目录项(
dirent
)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode
号码。 - 举例:文件系统查找文件
/var/log/message
的过程- 首先确定/目录文件的数据块,事实上/目录是由系统决定在哪里存储的,我们不用关心太多(在操作系统启动的时候,就知道根目录,用户的家目录在哪);读取/目录文件,找到
var
的文件名,找到对应的inode
号,根据var
的inode
号查找到inode
节点,从inode
节点中读取var
的block
,得知var
是个目录文件;打开var
目录文件,查找到名称为log
的文件名,找出log
对应的inode
号,查询inode
对应的inode
节点,通过inode
节点找到log
的block
,读取log
的block
,发现log
是个目录文件;打开log
目录文件,查找到名称message
文件名,找出message
对应的inode
号,查询inode
对应的inode
节点,通过inode
节点找到message
的block
,即找到了message
文件;由此可以看到操作系统经过了哪些曲折的步骤。 - 理解了上面这些知识,就能理解目录的权限。目录文件的读权限(
r
)和写权限(w
),都是针对目录文件本身。由于目录文件内只有文件名和inode
号码,所以如果只有读权限,只能获取文件名,无法获取其他信息,因为其他信息都储存在inode
节点中,而读取inode
节点内的信息需要目录文件的执行权限(x
)。
- 首先确定/目录文件的数据块,事实上/目录是由系统决定在哪里存储的,我们不用关心太多(在操作系统启动的时候,就知道根目录,用户的家目录在哪);读取/目录文件,找到
2、硬链接
-
硬链接是指多个文件名指向同一个物理文件的链接关系。它们(这些文件名)在文件系统中具有相同的
inode
号(索引节点号),但可以位于不同的目录中。当创建硬链接时,实际上是为文件增加了一个新的路径入口,他们都享有同一个inode X
和一个数据块(data block
)。但硬链接本身并不占用实际存储空间。 -
共享
inode
:硬链接直接指向文件的数据所在的位置,而不是文件名。多个硬链接实际上是共享同一存储空间的文件名,它们具有相同的inode
号。 -
文件系统限制:硬链接只能在同一个文件系统中创建,不能跨文件系统。
-
一般情况下,文件名和
inode
号码是"一一对应"关系,每个inode
号码对应一个文件名。但是,Unix/Linux
系统允许,多个文件名指向同一个inode
号码。 -
这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(
hard link
)。 -
硬链接的工作原理:
- 在创建硬链接时,操作系统会为新创建的链接分配相同的
inode
号,并在文件系统中的目录项中添加对应的链接关系。因此,无论通过哪个文件名访问该文件,都指向同一个inode
,即同一个文件内容。 ln
命令可以创建硬链接,运行上面这条命令以后,源文件与目标文件的inode
号码相同,都指向同一个inode
。inode
信息中有一项叫做"链接数",记录指向该inode
的文件名总数,这时就会增加1。反过来,删除一个文件名,就会使得inode
节点中的"链接数"减1。当这个值减到0,表明没有文件名指向这个inode
,系统就会回收这个inode
号码,以及其所对应block区域。
- 在创建硬链接时,操作系统会为新创建的链接分配相同的
-
为什么目录文件的连接数通常为2?
-
一个空目录里并不是真的空,他有很多隐藏文件,至少有一个"."或者"..","."是当前路径,".."是上级路径,"."文件也是个文件名,他有
inode
,且和dir
是同一个inode
,所以这一个"."为什么能代表当前路径,是因为一个"."代表的就是dir
的硬链接,就会发现dir
目录文件的硬链接数是2了。 -
在Linux文件系统中,每个文件都有一个唯一的索引节点(
inode
),用于存储文件的元数据。硬链接是指向同一个inode
的多个文件名。当一个目录被创建时,它会自动创建两个硬链接:一个是目录本身的名字,另一个是"."(当前目录),它指向目录的inode
。此外,目录的父目录也会创建一个指向该目录的".."(上级目录)链接。因此,一个新创建的空目录会有两个硬链接:一个是指向目录本身的"."链接,另一个是指向其父目录的".."链接。 -
这就是为什么创建一个目录,一开始硬链接数为2( 创建的目录名和目录中的
.
,并且新创建一个目录,父目录的硬链接会加1 (子目录中的..
)) -
由于硬链接是直接将文件名与索引节点号(即
inode
号)链接,因此硬链接存在以下几个特点:- 文件有相同的
inode
号及data block
,这使得修改其中一个硬链接文件属性或文件数据时,其他硬链接文件都会发生相应修改; - 只能对已存在的文件进行创建,硬链接与原始文件之间没有区别,它们是完全平等的。
- 不能跨文件系统(即分区)进行创建;
- 不能对目录文件进行创建;
- 删除其中一个硬链接文件时,不会对其他硬链接文件产生影响。
- 文件有相同的
-
用途:
- **共享文件:**多个用户或进程可以使用硬链接来共享同一个文件,节省存储空间。
- **备份文件:**硬链接可以用于文件备份,因为它们不会占用额外的磁盘空间(除了链接文件本身的
inode
和文件名信息)。 - **系统文件管理:**一些系统文件经常需要在不同位置进行引用,通过创建硬链接可以简化管理和维护。
- **防止误删:**通过创建多个硬链接,可以确保即使删除了一个链接,文件本身也不会被删除,从而防止误删。
3、软链接
-
软链接是指一个文件名指向另一个文件或目录的符号链接。与硬链接不同,软链接实际上是一个特殊类型的文件,其中包含指向目标文件或目录的路径信息。
-
软连接是一种特殊类型的文件,它包含了另一个文件或目录的路径名。当访问软连接时,系统会通过该路径名找到并访问目标文件或目录。
-
软链接类似于
Windows
系统中的快捷方式,它存储的是目标文件的路径,而不是文件本身的数据。这意味着如果目标文件被移动、重命名或删除,软链接将会失效。 -
文件
A
和文件B
的inode
号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A
时,系统会自动将访问者导向文件B
。因此,无论打开哪一个文件,最终读取的都是文件B
。这时,文件A就称为文件B
的"软链接"(soft link
)或者"符号链接(symbolic link
)。 -
这意味着,文件
A
依赖于文件B
而存在,如果删除了文件B,打开文件A就会报错:"No such file or directory
"。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B
的inode
号码,文件B
的inode
"链接数"不会因此发生变化。 -
软链接类似于
Windows
的快捷方式。它实际上是一个特殊的文件,有着自己的索引节点号(即inode
号)以及用户数据块(data block
),但用户数据块(data block
)中包含的是另一个文件的位置信息。 -
软链接的工作原理:
-
创建软链接时,操作系统会为其分配一个新的
inode
,并在文件系统中的目录项中添加软链接的信息,指向目标文件或目录的路径。当访问软链接时,操作系统会通过路径信息找到目标文件或目录。 -
由于软链接有着自己的索引节点号(即
inode
号)以及用户数据块(data block
),因此没有硬链接的诸多限制,它的特性如下: - (软链接有自己的文件属性、inode
号和data block
,但是编辑文件其实就是编辑源文件; - 可以对不存在的文件或目录进行创建; - 可以跨文件系统(即分区)进行创建,使用ln
命令跨文件系统创建时,源文件必须是绝对路径,否则为死链接; - 可以对文件或目录文件进行创 建; - 删除软链接并不影响源文件,但源文件被删除,则相关软链接文件变为死链接(dangling link
),若源文件(原地址原文件名)重新被创建,则死链接恢复为正常软链接。 -
软链接的应用场景:
- **快捷方式:**软链接可以创建桌面或文件夹中的快捷方式,方便用户快速访问目标文件或目录。
- **软件安装:**在某些操作系统中,软链接常用于指向已安装软件的可执行文件,简化软件的升级和管理。
- **跨文件系统引用:**软链接可以跨越不同的文件系统,将一个文件或目录链接到另一个文件系统中。
4、inode
的特殊作用
-
由于
inode
号码与文件名分离,这种机制导致了一些Unix/Linux
系统特有的现象。 -
有时,文件名包含特殊字符,无法正常删除。这时,直接删除
inode
节点,就能起到删除文件的作用。 -
移动文件或重命名文件,只是改变文件名,不影响
inode
号码。 -
打开一个文件以后,系统就以
inode
号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode
号码得知文件名。 -
第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过
inode
号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode
,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode
则被回收。