TLPI读书笔记第18章-目录与链接1
在文件系统中,目录的存储方式类似于普通文件。目录与普通文件的区别有二。 1.在其 i-node 条目中,会将目录标记为一种不同的文件类型(参见 14.4 节)。
2.目录是经特殊组织而成的文件。本质上说就是一个表格,包含文件名和 i-node 编号。
在大多数原生 Linux 文件系统上,文件名长度可达 255 个字符。图 18-1 所示为针对示例文件( /etc/passwd)所维护的文件系统 i-node 表以及相关目录文件的部分内容,展示了目录与i-node 之间的关系
回顾文件 i-node( 14.4 节)中存储的信息列表,会发现其中并未包含文件名,而仅通过目录列表内的一个映射来定义文件名称。其妙用在于,能够在相同或者不同目录中创建多个名称,每个均指向相同的 i-node 节点。也将这些名称称为链接,有时也称之为硬链接(稍后介绍),以示与符号链接有所区别。
可在 shell 中利用 ln 命令为一个业已存在的文件创建新的硬链接,正如下面 shell 会话日志所示。
echo "something" >abc
ls -li abc
ln abc xyz
ls -li abc xyz
经过 ls–li 命令所示 i-node 编码(即第一列)得到了进一步证实。名称 abc 和 xyz 指向相同的 i-node 条目,因此均指向相同文件。 ls–li 命令所示内容的第三列为对 i-node 链接的计数。执行 ln abc xyz 命令后, abc 所指向 i-node 的链接计数升至 2 因为现在指向该文件的有两个名字。 若移除其中一个文件名,另一文件名以及文件本身将继续存在。
仅当 i-node 的链接计数降为 0 时,也就是移除了文件的所有名字时,才会删除(释放)文件的 i-node 记录和数据块。
总结如下: rm 命令从目录列表中删除一文件名,将相应 i-node的链接计数减一,若链接计数因此而降为 0,则还将释放该文件名所指代的 i-node 和数据块。 同一文件的所有名字(链接)地位平等—没有一个名字(比如,第一个)会优于其他名字。正如上例所示,在移除与文件相关的第一个名称后,物理文件继续存在,但只能通过另一文件名来访问其内容。
对硬链接的限制有二,均可用符号链接来加以规避。 1.因为目录条目(硬链接)对文件的指代采用了 i-node 编号,而 i-node 编号的唯一性仅在一个文件系统之内才能得到保障,所以硬链接必须与其指代的文件驻留在同一文件系统中。 2.不能为目录创建硬链接,从而避免出现令诸多系统程序陷于混乱的链接环路。
18.2 符号(软)链接
符号链接,有时也称为软链接,是一种特殊的文件类型,其数据是另一文件的名称。 图 18-2 展示的情况是:两个硬链接—/home/erena/this 和/home/allyn/that—指向同一个文件,而符号链接/home/kiran/other,则指代文件名/home/erena/this。
在 shell 中,符号链接是由 ln–s 命令创建的。 ls–F 命令的输出结果中会在符号链接的尾部标记@。 符号链接的内容既可以是绝对路径,也可以是相对路径。解释相对符号链接时以链接本身的位置作为参照点。 符号链接的地位不如硬链接。尤其是,文件的链接计数中并未将符号链接计算在内。)因此,如果移除了符号链接所指向的文件名,符号链接本身还将继续存在,尽管无法再对其进行解引用(下溯)操作,也将此类链接称之为悬空链接。更有甚者,还可以为并不存在的文件名创建一个符号链接
因为符号链接指代一个文件名,而非 i-node 编号,所以可以用其来链接不同文件系统中的一个文件。对硬链接的那些制约也就不会困扰到符号链接,可以为目录创建符号链接。诸如 find 和 tar 之类的工具命令有能力识别硬链接和符号链接之间的差异,要么会在默认情况下不对符号链接进行解引用,要么会避免因使用符号链接而陷入引用环路。 符号链接之间可能会形成链路(例如, a 是指向 b 的符号链接, 而 b 是指向 c 的符号链接)。当在各个文件相关的系统调用中指定了符号链接时,内核会对一系列链接层层解去引用,直抵最终文件。 SUSv3 规定,针对路径名中的每个符号链接部件,系统实现应允许对其实施至少_POSIX_SYMLOOP_MAX 次解除引用操作。 _POSIX_SYMLOOP_MAX 的规定值为 8。然而,在内核 2.6.18 之前, Linux 将解析符号链接链路时的解引用操作次数限制为 5 次。始于版本2.6.18, Linux 内核实现了 SUSv3 所规定的最小解引用次数: 8 次。 Linux 还将对一个完整路径名的解引用总数限制为 40 次。施加这些限制,意在应对超长符号链接链路以及符号链接环路,从而使内核代码在解析符号链接时免于引发堆栈溢出
系统调用对符号链接的解释
许多系统调用都会对符号链接进行解引用处理(即下溯 follow),从而对链接所指向的文件展开操作。还有一些系统调用对符号链接则不作处理,直接操作于链接文件本身。书中会在论及每个系统调用的同时,描述其针对符号链接的行为。表 18-1 对此作了总结
少数情况下,符号链接本身及其所指向的文件会需要类似的功能,系统这时就会提供两套系统调用:一套会对链接解除引用,另一套则反之,后者在命名时会冠以字母 l。 stat()和 lstat()就是一例。 有一点是约定俗成的:总是会对路径名中目录部分(即最后一个斜线字符前的所有组成部分)的符号链接进行解除引用操作。因此,在路径/somedir/somesubdir/file 中,若 somedir 和 somesubdir 属于符号链接,则一定会解除对这两个目录的引用,而针对 file 是否进行解引用与否,则取决于路径名所传入的系统调用。
符号链接的文件权限和所有权
大部分操作会无视符号链接的所有权和权限(创建符号链接时会为其赋予所有权限)。是否允许操作反而是由符号链接所指代文件的所有权和权限来决定。仅当在带有粘性权限位( 15.4.5 节)的目录中对符号链接进行移除或改名操作时,才会考虑符号链接自身的所有权。
18.3 创建和移除(硬)链接: link()和 unlink()
link()和 unlink()系统调用分别创建和移除硬链接
若 oldpath 中提供的是一个已存在文件的路径名,则系统调用 link()将以 newpath 参数所指定的路径名创建一个新链接。若 newpath 指定的路径名已然存在,则不会将其覆盖;相反,将产生一个错误( EEXIST)。 在 Linux 中, link()系统调用不会对符号链接进行解引用操作。若 oldpath 属于符号链接,则会将 newpath 创建为指向相同符号链接文件的全新硬链接。
这一行为有悖于 SUSv3 规范。 SUSv3 要求,除非另行规定( link()系统调用不在此列),否则所有执行路径名解析操作的函数都应对符号链接进行解引用。大多数其他 UNIX 实现的行事方式都与 SUSv3 相符。值得注意的是, Solaris 是个例外,默认情况下的行为与 Linux 相同。但若采用适当的编译器选项,又可提供符合 SUSv3 规范的 行为。鉴于系统实现间的这种差异,应避免将 oldpath 参数指定为符号链接,以保障程序的可移植性。
unlink()系统调用移除一个链接(删除一个文件名),且如果此链接是指向文件的最后一个链接,那么还将移除文件本身。若 pathname 中指定的链接不存在,则 unlink()调用失败,并将errno 置为 ENOENT。 unlink()不能移除一个目录,完成这一任务需要使用 rmdir()或 remove(),将于 18.6 节进行介绍
unlink()系统调用不会对符号链接进行解引用操作,若 pathname 为符号链接,则移除链接本身,而非链接指向的名称。
仅当关闭所有文件描述符时,方可删除一个已打开的文件
内核除了为每个 i-node 维护链接计数之外,还对文件的打开文件描述(参见图 5-2)计数。
当移除指向文件的最后一个链接时,如果仍有进程持有指代该文件的打开文件描述符,那么在关闭所有此类描述符之前,系统实际上将不会删除该文件。
这一特性的妙用在于允许取消对文件的链接,而无需担心是否有其他进程已将其打开。(然而,对于链接数已降为 0 的打开文件,就无法将文件名与其重新关联起来。 )此外,基于上述事实,还可以玩点小技巧:先创建并打开一个临时文件, 随即取消对文件的链接( unlink), 然后在程序中继续使用该文件。( tmpfile()函数 ) 程序清单 18-1 对此现象做了展示
程序清单 18-1 中程序接受两个命令行参数。第一个参数标识程序应该创建的文件名称。程序打开此文件后随即取消与文件名的链接。虽然文件名已消失,但是文件本身依然存在。 然后程序向文件随机写入一些数据块,数据块数量由程序的第二个命令行参数(可选项)指定。这时,程序会利用 df(1)命令显示文件系统的空间使用情况。程序接着会关闭文件描述符,系统因之而将文件移除, 程序会再次使用 df(1)命令来显示有所下降的磁盘使用情况。
如下 shell会话演示了运行程序清单 18-1 程序的情况:
18.4 更改文件名: rename()
借助于 rename()系统调用,既可以重命名文件,又可以将文件移至同一文件系统中的另一目录。
调用会将现有的一个路径名 oldpath 重命名为 newpath 参数所指定的路径名。 rename()调用仅操作目录条目,而不移动文件数据。改名既不影响指向该文件的其他硬链接,也不影响持有该文件打开描述符的任何进程,因为这些文件描述符指向的是打开文件描述, (在调用 open()之后)与文件名并无瓜葛。 以下规则适用与对 rename()的调用:
1.若 newpath 已经存在,则将其覆盖。
2.若 newpath 与 oldpath 指向同一文件,则不发生变化(且调用成功)。这很不合常理。顺着上一条规则的思路,通常的推断是:如果两个文件名 x 和 y 都存在,那么调用rename("x","y")时应当把 x 移除才是。但如果 x 和 y链接的是同一文件,事实却并非如此
3.rename()系统调用对其两个参数中的符号链接均不进行解引用。如果 oldpath 是一符号链接,那么将重命名该符号链接。如果 newpath 是一符号链接,那么会将其视为由oldpath 重命名而成的普通路径名(即移除已有的符号链接 newpath)。
4.如果 oldpath 指代文件,而非目录,那么就不能将 newpath 指定为一个目录的路径名(否则将 errno 置为 EISDIR)。要想重命名一个文件到某一目录中(亦即将文件移到另一目录), newpath 必须包含新的文件名。如下调用既将一个文件移动到另一目录中,同时又将其改名:rename('sub1/x','sub2/y')
5.若将 oldpath 指定为目录名,则意在重命名该目录。这种情况下,必须保证 newpth 要么不存在,要么是一个空目录的名称。无论 newpath 是一个已有文件还是一个非空目录,调用都将出错(分别将 errno 置为 ENOTDIR 和 ENOTEMPTY)。
6.若 oldpath 是一目录,则 newpath 不能包含 oldpath 作为其目录前缀。例如,不能将/home/mtk 重命名为/home/mtk/bin(错误为 EINVAL)。
7.oldpath 和 newpath 所指代的文件必须位于同一文件系统。之所以如此,是因为目录内容由硬链接列表组成, 且硬链接所指向的 i-node 与目录位于同一文件系统。 如前所述,rename()仅限于操作目录列表的内容。试图将一文件重命名至不同的文件系统将返回错误 EXDEV。(非要如此, 必须从一个文件系统中将其文件内容复制到另一文件系统,然后再删除老文件。这正是 mv 命令的功能所在。 )
18.5 使用符号链接: symlink()和 readlink()
现在来看看用于创建符号链接,以及检查其内容的系统调用。 symlink()系统调用会针对由 filepath 所指定的路径名创建一个新的符号链接—linkpath。(想移除符号链接,需使用 unlink()调用。 )
若 linkpath 中给定的路径名已然存在,则调用失败(且将 errno 置为 EEXIST)。由 filepath 指定的路径名可以是绝对路径,也可以是相对路径。 由 filepath 所命名的文件或目录在调用时无需存在。即便当时存在,也无法阻止后来将其删除。这时, linkpath 成为“悬空链接”,其他系统调用试图对其进行解引用操作都将出错(通常错误号为 ENOENT)。 如果指定一符号链接作为 open()调用的 pathname 参数,那么将打开链接指向的文件。有时,倒宁愿获取链接本身的内容,即其所指向的路径名。这正是 readlink()系统调用的本职工作,将符号链接字符串的一份副本置于buffer 指向的字符数组中。 bufsiz 是一个整型参数,用以告知 readlink()调用 buffer 中的可用字节数。 如果一切顺利, readlink()将返回实际放入 buffer 中的字节数。若链接长度超过 bufsiz,则置于 buffer 中的是经截断处理的字符串(并返回字符串大小,亦即 bufsiz)。 由于 buffer 尾部并未放置终止空字符,故而也无法分辨 readlink()所返回的字符串到底是经过截断处理,还是恰巧将 buffer 填满。验证后者的方法之一是重新分配一块更大的 buffer,并再次调用 readlink()。另外,还可以将 pathname 的长度定义为常量 PATH_MAX(参见 11.1节),该常量定义了程序可拥有的最长路径名长度。 程序清单 18-4 演示了 readlink()的用法
18.6 创建和移除目录: mkdir()和 rmdir()
mkdir()系统调用创建一个新目录。
pathname 参数指定了新目录的路径名。该路径名可以是相对路径,也可以是绝对路径。 若具有该路径名的文件已经存在,则调用失败并将 errno 置为 EEXIST。 对新目录所有权的设置遵循 15.3.1 节所述规则。 mode 参数指定了新目录的权限。 对该位掩码值的指定方式既可以与 open()调用相同—对表 15-4 所列各常量进行或(|)操作,也可直接赋予八进制数值。既定的 mode 值还将于进程掩码相与( &)(参见 15.4.6 节)。另外,set-user-ID 位始终处于关闭状态,因为该位对于目录而言毫无意义。 如果在 mode 中设置了粘滞位( S_ISVTX),那么将对新目录设置该权限。 调用还会忽略在 mode 中设置的 set-group-ID 位( S_ISGID)。相反,若对其父目录设置了set-group-ID 位, 则同样会对新建目录设置该权限。 15.3.1 节曾指出, 对目录设置 set-group-ID权限位将导致目录中新建文件的组 ID 取自目录组 ID,而非进程有效组 ID。
mkdir()系统调用按此处描述的方式来传播 set-group-ID 权限位,以保证目录下所有子目录的行为均保持一致。 SUSv3 明文规定, mkdir()对 set-user-ID、 set-group-ID 以及粘滞位的处理方式由实现定义。 某些 UNIX 实现在新建目录时总是关闭这 3 个权限位。
新建目录包括两个条目: .(点),即指向目录自身的链接; ..(点点),即指向父目录的链接。
mkdir()系统调用所创建的仅仅是路径名中的最后一部分。换言之, mkdir("aaa/bbb/ccc", mode) 仅当目录 aaa 和 aaa/bbb 已经存在的情况下才会成功。(这相当于 mkdir(1)命令的默认行为,但 mkdir(1)同时也提供-p 选项,可将不存在的中间目录一一创建。 )
rmdir()系统调用移除由 pathname 指定的目录,该目录可以是绝对路径名,也可以是相对路径名
要使 rmdir()调用成功,则要删除的目录必须为空。如果 pathname 的最后一部分为符号链接,那么 rmdir()调用将不对其进行解引用操作,并返回错误,同时将 errno 置为 ENOTDIR。
18.7 移除一个文件或目录: remove()
remove()库函数移除一个文件或一个空目录。
如果 pathname 是一文件,那么 remove()去调用unlink();如果 pathname 为一目录,那么remove()去调用rmdir()。 与 unlink()、 rmdir()一样, remove()不对符号链接进行解引用操作。若 pathname 是一符号链接,则 remove()会移除链接本身,而非链接所指向的文件。 如果移除一个文件只是为创建同名新文件做准备,那么编码时使用 remove()函数会更加简单,无需再去检查目录名所指是文件还是目录,然后再决定是调用 unlink()还是 rmdir()。