第四章 文件和目录

Posted yangxinrui

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第四章 文件和目录相关的知识,希望对你有一定的参考价值。

4.1 引言

  上一章我们说明了执行 I/O 操作的基本函数,其中的讨论是围绕普通文件 I/O 进行的——打开一个文件,读或写一个文件。本章将描述文件系统的其他特征和文件的性质。

  我们将从 stat 函数开始,逐个说明 stat 结构的每一个成员以了解文件的所有属性。在此过程中,我们将说明修改这些属性的各个函数(更改所有者、更改权限等),还将更详细地查看UNIX文件系统地结构以及符号链接。

  本章最后介绍了对目录进行操作地各个函数,并且开发了一个以降序遍历目录层次结构的函数。

4.2 stat、fstat和 lstat 函数

  本章讨论的中心是三个 stat 函数以及它们所返回的信息。

#include <sys/stat.h>

int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);

  lstat 类似于 stat,但是当命名的文件是一个符号链接时, lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件信息。

  linux 中 struct stast 结构如下:

技术分享图片

4.3 文件类型

  至今我们已介绍了两种不同的文件类型——普通文件和目录。UNIX系统的大多数文件是普通文件或目录,但是也有另一些文件类型。文件类型包括如下几种:

  (1)普通文件(regular file)。这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据对于UNIX内核而言并无区别。对普通文件内容的解释由处理该文件的应用程序进行。

  一个值得注意的例外是二进制可执行文件。为了执行程序,内核必须理解其格式。所有二进制可执行文件都遵循一种格式,这种格式使内核能够确定程序文本和数据加载位置。

  (2)目录文件(directory file)。这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。进程必须使用本章说明的函数才能更改目录。

  (3)块特殊文件(block special file)。这种文件类型提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。

  (4)字符特殊文件(character special file)。这种文件类型提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。

  (5)FIFO。这种类型文件用于进程间通信,有时也将其称为命令管道(named pipe)。15.5节将对其进行了说明。

  (6)套接字(socket)。这种文件类型用于进程间网络通信。套接字也可用于在一台宿主机上进程之间的非网络通信。16章将用套接字进行进程间的通信。

  (7)符号链接(symbolic link)。这种文件类型指向另一个文件。4.16将更多地述及符号链接。

  文件类型信息包含在stat结构地 st_mode 成员中。可以用下表中的宏确定文件类型。这些宏的参数都是stat 结构中的 st_mode 成员。

技术分享图片

 

 

// 对每个命令行参数打印文件类型

#include "apue.h"

int main(int argc, char **argv)
{
   int i;
   struct stat buf;
   char *ptr;

   for (i = 1; i < argc; i++) {
      printf("%s: ", argv[i]);
      if (lstat(argv[i], &buf) < 0) {
         err_ret("lstat error");
         continue;
      }
      if (S_ISREG(buf.st_mode))
         ptr = "regular";
      else if (S_ISDIR(buf.st_mode))
         ptr = "directory";
      else if (S_ISCHR(buf.st_mode))
         ptr = "character special";
      else if (S_ISBLK(buf.st_mode))
         ptr = "block special";
      else if (S_ISLNK(buf.st_mode))
         ptr = "symbolic link";
      else if (S_ISFIFO(buf.st_mode))
         ptr = "fifo";
      else if (S_ISSOCK(buf.st_mode))
         ptr = "socket";
      else 
         ptr = "** unknown mode **";
      printf("%s
", ptr);
   }
   exit(0)
}

  我们特地使用了 lstat 函数而不是 stat 函数以便检测符号链接。如若使用 stat 函数,则不会观察到符号链接。

  为了在 Linux 系统上编译该程序,必须定义 _GNU_SOURCE,这样就能包括 S_ISSOCK 宏的定义。

4.4 设置用户ID和设置组ID

  与一个进程相关联的ID有6个或更多,它们于下表:

技术分享图片

  (1)实际用户ID和实际组ID标识我们究竟是谁。这两个字段在登陆时取自口令文件中的登陆项。通常在一个登陆会话间这些值并不改变,但是超级用户进程有方法改变它们。

  (2)有效用户ID,有效组ID以及附加组ID决定了我们的文件访问权限。

  (3)保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效组ID的副本。

  通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。

  每个文件都有一个所有者和组所有者,所有者由stat结构中的 st_uid 成员表示,组所有者则由st_gid 成员表示。

  当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID通常是实际组ID。但是可以在文件模式字(st_mode)中设置一个特殊标志,其含义是“当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID(st_uid)”。与此相类似,在文件模式字中可以设置另一位,它使得将执行此文件的进程的有效组ID设置为文件的组所有者ID(st_gid)。在文件模式字中的这两位被称为设置用户ID(set-user-ID)位和设置用户组ID(set-group-ID)位。

  例如,若文件所有者是超级用户,而且设置了该文件的设置用户ID位,然后当该程序由一个进程执行时,则该进程具有超级用户特权。不管执行此文件的进程的实际用户ID 是什么,都进行这种处理。

  再返回到 stat 函数,设置用户 ID位及设置组 ID位都包含再 st_mode 值中。这两位可用常量 S_ISUID 和 S_ISGID 测试。

4.5 文件访问权限

  st_mode值也包含了针对文件的访问权限位。当提及文件时,指的是前面所提到的任何类型的文件。所有文件类型(目录文件、字符特别文件等)都有访问权限。

  每个文件有9个访问权限位,可将它们分成三类,如下图:

技术分享图片

  术语用户指的是文件所有者(owner)。chmod(1)命令用于修改这9个权限位。该命令允许我们用u表示用户(所有者),用g表示组,用o表示其他,o不包括所有者。

  我们用名字打开任一类型的 文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。这就是为什么对于目录其执行权限位常被称为搜索位的原因。

  例如,为了打开文件 /usr/include/stdio.h,需要对目录 /,/usr,/usr/include 具有执行权限。然后,需要具有对该文件本身的适当权限,这取决于以何种模式打开它(只读、写等)。

  注意,对于目录的读权限和执行权限的意义是不同的。读权限允许我们读目录获得在该目录中所有文件名的列表。当一个目录是我们要访问的路径名的一个组成部分时,对该目录的执行权限使我们可通过该目录(也就是搜索该目录,寻找一个特定的文件名)。

  引用隐含目录的另一个例子是,如果 PATH 环境遍历(8.10节将对其进行说明)指定了一个我们不具有执行权限的目录,那么shell决不会在该目录下找到可执行文件。

  (a)为了在一个目录中创建一个新文件,必须对该目录有写权限和执行权限。

  (b)为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身则不需要有读、写权限。

  (6)如果用6个exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限,该文件还必须是一个普通文件。

  进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试,而这种测试可能涉及文件所有者(st_uid 和 st_gid)、进程的有效ID(有效用户ID和有效组ID)以及进程的附加组(若支持的话)。两个所有者ID是文件的性质,而两个有效ID和附加ID则是进程的性质。内核进行的测试是:

  (1)若进程的有效用户ID是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分的自由。

  (2)若进程的有效用户ID等于文件的所有者ID(也就是该进程拥有此文件),那么:若所有者适当的访问权限位被设置,则允许访问,否则拒绝访问。适当的访问权限位指的是,若进程为读而打开该文件,则用户读位应为1;若进程为写而打开该文件,则用户写位应为1;若进程将执行该文件,则用户执行位应为1。

  (3)若进程的有效组ID或进程的附加组ID之一等于文件的组ID,那么:若组适当的访问权限位被设置,则允许访问,否则拒绝访问。

  (4)若其他用户适当的访问权限位被设置,则允许访问,否则拒绝访问。

  按顺序执行这四步。注意:如若进程拥有此文件(第2步),则按用户访问权限批准或拒绝该进程对文件的访问——不查看组访问权限。类似地,若进程并不拥有该文件,但进程属于某个适当地组,则按组访问权限批准或拒绝该进程对文件地访问——不查看其他用户的访问权限。

4.6 新文件和目录的所有权

  在第3章,当说明用 open 或 creat 创建新文件时,我们并没有说明赋予新文件的用户ID和组ID是什么。4.20节将说明 mkdir 函数,此时就会了解如何创建一个新目录。关于新目录的所有权规则与本节将说明的新文件所有权规则相同。

  新文件的用户 ID 设置为进程的有效用户ID。关于组ID,POSIX.1 允许实现选择下列之一作为新文件的组ID。

  (1)新文件的组 ID 可以是进程的有效组 ID。

  (2)新文件的组 ID 可以是它所在目录的组 ID。

  Linux ext2 和 ext3 文件系统允许基于文件系统在 POSIX.1 所允许的两种选项中选择一种,为此在 mount(1)命令中使用了一个特殊标志。新文件的组ID 取决于它所在目录的设置组 ID 位是否设置。如果该目录的这一位以及设置,则将新文件的组 ID 设置为 目录的组ID,否则将新文件的组 ID设置为进程的有效组 ID。

  使用 POSIX.1所允许的第二个选项(继承目录的组ID)使得在某个目录下创建的文件和目录都具有该目录的组ID。于是文件和目录的组所有权从该点向下传递。例如,在Linux的/var/spool/mail目录中就使用这种方法。

4.7 access函数

  如前所述,当用 open函数打开一个文件时,内核以进程的有效用户 ID 和有效组ID为基础执行访问权限测试。有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。例如当一个进程使用设置用户ID或设置组ID特征作为另一个用户(或组)运行时,就可能会有这种需求。即使一个进程可能已经因设置用户ID以超级用户权限运行,它仍可能想验证其实际用户能否访问一个给定的文件。 access函数是按照实际用户 ID 和实际组ID进行访问权限测试的(该测试也分为四步,这与4.5节所述的一样,但将有效改为实际。)

#include <unistd.h>

int access(const char *pathname, int mode);

  其中,mode是按下表所列常量的按位或。

技术分享图片

#include "apue.h"
#include <fcntl.h>

int main(int argc, char **argv)
{
   if (argc != 2)
      err_quit("usage: a.out <pathname>");
   if (access(argv[1], R_OK) < 0)
      err_ret("access error for %s", argv[1]);
   else 
      printf("read access OK
");
   if (open(argv[1], O_RDONLY) < 0)
      err_ret("open error for %s", argv[1]);
   else 
      printf("open for reading OK
");
   exit(0);    
}

技术分享图片

 

技术分享图片

  在本例中,设置用户ID程序可以确定实际用户不能读 某个指定文件,而open函数却能打开该文件。

4.8 umask函数

  至此我们已说明了每个文件相关的9个访问权限位,在此基础上我们可以说明与每个进程相关联的文件模式创建屏蔽字。

  umask函数为进程设置文件模式创建屏蔽字,并返回以前的值(这是少数几个没有出错返回函数的一个)

#include <sys/stat.h>

mode_t umask(mode_t cmask);

  其中,参数cmask是上面表中9个常量(S_IRUSR、S_IWUSR等)中的若干按位或构成的。

  在进程中创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字(回忆3.3,3.4节,在那里我们说明了open和creat函数,这两个函数都有一个参数mode,它指定了新文件的访问权限)。我们将在4.20节说明如何创建一个新文件。对于任何在文件模式创建屏蔽字中为1的位,在文件mode中的相应位则一定被关闭。

// 程序创建两个文件,创建第一个时,umask值为0,创建第二个时,umask值禁止所有组和所有其他用户的访问权限

#include "apue.h"
#include <fcntl.h>

#define RWRWRW (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)

int main(void)
{
   umask(0);
   if (creat("foo", RWRWRW) < 0)
      err_sys("creat error for foo");
   umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
   if (creat("bar", RWRWRW) < 0)
      err_sys("creat error for bar");
   exit(0);
}

技术分享图片

  UNIX系统的大多数用户从不处理它们的umask值。通常在登陆时,由shell的启动文件设置一次,然后从不改变。尽管如此,当编写创建新文件的程序时,如果我们像确保指定的访问权限为已激活,那么必须在进程运行时修改umask值。例如,如果我们想确保任何用户都能读文件,则应将umask设置为0。否则,当我们的进程运行时,有效的umask值可能关闭该权限位。

  (上面的程序还可以看出,子进程会继承父进程的 umask,修改子进程的umask,不会影响父进程的umask)。

  用户可以用 umask命令设置shell的umask值,该值表示成八进制数,一位代表一种要屏蔽的权限。

技术分享图片

技术分享图片

4.9 chmod 和 fchmod 函数

  这两个函数使我们可以更改现有文件的访问权限

#include <sys/stat.h>

int chmod(const char *pathname, mode_t mode);
int fchmod(int filedes, mode_t mode);

  为了改变一个文件的权限位,进程的有效用户ID必须等于文件所有者ID,或者该进程必须具有超级用户权限。

  参数mode是表4-8中所列常量的某种按位或运算构成的。

技术分享图片

 

// chmod 函数示例

int main(void)
{
   struct stat statbuf;
   /*  turn on set-group-ID and turn off group-execute */
   if (stat("foo", &statbuf) < 0)
      err_sys("stat error for foo");
   if (chmod("foo", (statbuf.st_mod & ~S_IXGRP) | S_ISGID) < 0)
      err_sys("chmod error for foo");
   
   /*  set absolute mode to "rw-r--r--" */
   if (chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0)
      err_sys("chmod error for bar");

   exit(0);
}

  本例中,对于bar,不论当前权限为什么,我们都将其权限设置为一绝对值,对于foo,我们相对于其当前状态设置设置。

 

4.11 chown、fchown 和 lchown

  下面几个chown函数用于更改文件的用户ID和组ID

int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int filedes, uid_t owner, gid_t group);
int lchown(const char *pathname, uid_t owner, gid_t group);

  lchown更改符号链接本身的所有者,而不是符号链接指向的文件。

  若两个参数 owner 或 group 中的任意一个是-1,则对应ID不变。

  基于Linux的系统规定只有超级用户才能更改一个文件所有者。这样做的原因是防止用户改变其文件的所有者从而摆脱磁盘空间对他们的限额。

4.12 文件长度

  stat结构成员st_size表示以字节为单位的文件长度。此字段只对普通文件、符号链接、目录文件有意义。 

  对于普通文件,其长度可以是0,在读这种文件时,将得到文件结束(end-of-file)指示。

  对于目录,长度通常是一个数(例如16或512)。

  对于符号链接,长度是文件名中的实际字节数。例如:

技术分享图片

  文件长度7就是路径名usr/lib的长度(注意,因为符号链接文件长度总是由st_size指示,所以它并不包含通常C语言用作名字结尾null字符)。

  大多数UNIX系统提供字段 st_blksize 和 st_blocks。其中,第一个是对文件I/O较合适的块长度,第二个是所分配的实际512字节块数量。回忆前面的程序,使用 st_blksize 读写文件,所用的时间最少,由于效率的缘故,标准IO也试图一次读写 st_blksize 个字节。

  应该了解的是,不同的UNIX版本其 st_blocks所用的单位可能不是 512 字节块。使用此值并不是可移植的。

  文件中的空洞

 我们前面提到,普通文件可以包含空洞。空洞是由于所设置的偏移量超过了文件尾端,并写了某些数据后造成的。作为一个例子,考虑下面情况:

技术分享图片

  文件 core 的长度刚好超过 8 MB 字节,而 du 命令则报告文件占用272个512字节块(139 264字节)。很明显,此文件中有很多空洞。

  对于没有写过的字节位置,read 函数读到的字节是0。如果执行:

技术分享图片

  由此可见,正常的 I/O 操作读整个文件长度。

  如果使用实用程序(如 cat(1))复制这种文件,那么所有空洞会被填满,实际数据将为 0.

技术分享图片

技术分享图片

4.13 文件截短

  有时我们需要在文件尾端处截去一些数据以缩短文件。将一个文件清空为0是一个特例,在打开文件时使用 O_TRUNC 标志可以做到这一点。

int truncate(const char *pathname, off_t length);
int ftruncate(int filedes, off_t length);

  这两个函数将把现有的文件长度截短为 length,如果文件以前长度大于 length,则超过部分不能访问,如果文件以前长度小于 length,则其效果于系统有关。Linux 下增加该文件的长度,新增部分将读作0(可能在文件中创建了一个空洞)。

4.14 文件系统

  为了说明文件链接的概念,先要介绍UNIX文件系统的基本结构。同时,了解 i 节点和指向节点的目录项之间的区别也是很有益的。

  目前,UNIX文件系统有很多种实现。UFS是以BSD快速文件系统为基础的。本节讨论该文件系统。

  我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统,如图:

技术分享图片

  i 节点是固定长度的记录项,它包含有关文件的大部分信息。

  如果更仔细地观察一个柱面组的 i 节点和数据块部分,则可以看到如下图所示的情况。

技术分享图片

注意图4-2中的下列各点:

  (1)在图中有两个目录项指向同一个 i 节点。每个 i 节点中都有一个链接计数,其值是指向该 i 节点的目录项数。只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)。这就是为什么“解除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”的原因。这也是为什么删除一个目录项函数被称为 unlink 而不是 delete 的原因。在 stat 结构中,链接计数包含在 st_nlink 成员中,其基本系统数据结构是 nlink_t。这种链接类型称为硬链接。POSIX.1常量 LINK_MAX 指定一个文件链接数的最大值。

  (2)另外一种链接类型称为符号链接(symbolic link)。对于这种链接,该文件的实际内容(在数据块中)包含了该符号链接所指向的文件名字。在下例中:

技术分享图片

  该目录项的名字是3字符的字符串 lib,而在该文件中包含了7个数据字节 usr/lib。该 i 节点中的文件类型是 S_IFLINK,于是系统知道这是一个符号链接。

  i 节点包含了大多数与文件有关的信息:文件类型、文件访问权限位、文件长度和指向该文件所占用的数据块的指针等等。 stat 结构中的大多数信息都取自 i 节点。只有两项数据存放在目录项中: 文件名和 i 节点编号。 i 节点编号的数据结构类型是 ino_t 。

  每个文件系统各自对它们的 i 节点进行编号,因此目录项中的 i 节点编号数指向同一个文件系统中的相应 i 节点,不能使一个目录项指向另一个文件系统的 i 节点。这就是为什么 ln(1) 命令(构造一个指向一个现有文件的新目录项)不能跨越文件系统的原因。我们将在下一节说明 link 函数。

  当在不更换文件系统情况下为一个文件更名时,该文件的实际内容并未移动,只需构造一个指向现有 i 节点的新目录项,并解除与旧目录项的链接。例如,将文件/usr/lib/foo更名为/usr/foo,如果目录 /usr/lib 和 /usr 在同一文件系统中,则文件 foo 的内容无需移动,这就是 mv(1) 命令的通常操作方式。

  我们说明了普通文件的链接计数概念,但是对于目录文件的链接计数字段又如何呢?假定我们在工作目录中构造了一个新目录:

  

mkdir testdir

  下图显示了其结果。注意,该图显示地显示了 . 和 .. 目录项。

技术分享图片

对于编号 2549 的 i 节点,其类型字段表示它是一个目录,而链接计数为2。任何一个叶目录(不包含任何其他目录的目录)的链接计数总是 2,数值2来自命令该目录(testdir)的目录项以及在该目录中的 . 项。对于编号为 1267 的 i 节点,其类型字段表示它是一个目录,而其链接计数则大于或等于3.它大于或等于 3 的原因是,至少有三个目录项指向它:一个命令它的目录项(在图中没有表示出来),第二个是在该目录中的 . 项,第三个是在其子目录 testdir 中的 .. 项。注意,父目录中的每一个子目录都会使该父目录的链接计数增 1.

4.15 link、unlink、remove和rename函数

  如上节所述,任何一个文件可以有多个目录项指向其 i 节点。创建一个指向现有文件的链接的方法是使用 link 函数。

int link(const char *exitingpath, const char *newpath);

此函数创建一个新目录项 newpath,它引用现有的文件 existingpath。如若 newpath 已经存在,则返回出错。只创建 newpath 中的最后一个分量,路径中的其他部分应当已经存在。

  创建新目录项以及增加链接计数应当是个原子操作。虽然 POSIX.1 允许实现支持跨文件系统的链接,但是大多数实现要求这两个路径名在同一个文件系统中。如果实现支持创建指向一个目录的硬链接,那么也仅限于超级用户才可以这样做。其理由是这样做可能在文件系统中形成循环,大多数处理文件系统的实用程序都不能处理这种情况。因此很多文件系统实现了不允许对于目录的硬链接。

  为了删除一个现有的目录项,可以调用 unlink函数

int unlink(const char *pathname);

  此函数删除目录项,并将 pathname 所引用文件的链接计数减 1。如果还有指向该文件的其他链接,则仍可通过其他链接访问该文件的数据。如果出错,则不对该文件做任何更改。

  我们在前面已经提及,为了解除对文件的链接,必须对包含该目录项的目录具有写和执行权限。

  只有当链接计数达到0时,该文件的内容才可被删除。另一个条件也会阻止删除文件内容——只要进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程数。如果该数达到0,然后内核检查其链接数,如果这个数也是0,那么就删除该文件的内容。

// 打开一个文件,然后解除对它的锁定。执行该程序的进程然后休眠15秒钟,接着就终止。

int main(void)
{
   if (open("tempfile", O_RDWR) < 0)
      err_sys("open error");
   if (unlink("tempfile") < 0)
      err_sys("unlink error");
   sleep(15);
   printf("done
");
   exit(0);
}

  运行结果:

技术分享图片

  unlink的这种性质经常被程序用来确保即使是在该程序崩溃时,它所创建的临时文件也不会遗留下来。进程 open 或 creat 创建一个文件,然后立即调用 unlink。因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭文件或终止时(在这种情况下,内核会关闭该进程打开的全部文件),该文件的内容才会被删除。

  如果 pathname 是符号链接,那么 unlink 删除该符号链接,而不会删除由该链接所引用的文件。给出符号连接名情况下,没有一个函数能删除由该链接所引用的文件。

  超级用户可以调用 unlink,其参数 pathname 指定一个目录,但是通常应当使用 rmdir 函数,而不使用这种方式。

  我们也可以用 remove 函数解除对一个文件或目录的链接。对于文件,remove的功能与 unlink 相同。对于目录, remove 的功能与 rmdir 相同。

int remove(cosnt char *pathname);

  文件或目录用 rename 函数 更名。

int rename(const char *oldname, const char *newname);

  根据 oldname 是指文件还是目录,有几种情况要加以说明。我们也应说明如果 newname 已经存在将会发生什么。

  (1)如果 oldname 指的是一个文件而不是目录,那么为该文件或符号链接更名。在这种情况下,如果 newname 已经存在,则它不能引用一个目录。如果 newname 已经存在,而且不是一个目录,则先将该目录项删除然后将 oldname 更名为 newname。对包含 oldname 的目录已经包含 newname 的目录,调用进程必须具有写权限,因为将更改这两个目录。

  (2)如若 oldname 指的是一个目录,那么为该目录更名。如果 newname 已经存在,则它必须引用一个目录,而且该目录应该是空目录(空目录指的是该目录中只有 . 和 .. 项)。如果 newname 存在(而且是一个空目录),则先将其删除,然后将 oldname 更名为 newname。另外,当为一个目录更名时, newname 不能包含 oldname 作为其路径前缀。例如,不能将 /usr/foo 更名为 /usr/foo/testdir,因为旧名字(/usr/foo)是新名字的路径前缀,因而不能将其删除。

  (3)如若 oldname 或 newname 引用符号链接,则处理的是符号链接本身,而不是它所引用的文件。

  (4)作为一个特例,如果 oldname 和 newname 引用同一个文件,则函数不做任何更改而成功返回。

  如若 newname 已经存在,则调用进程对它需要有写权限(如同删除情况一样)。另外,调用进程将删除 oldname 目录项,并可能创建 newname 目录项,所以它需要对包含 oldname 及包含 newname的目录具有写和执行权限。

 4.16 符号链接

  符号链接是指向一个文件的间接指针,它与上一节所述的硬链接有所不同,硬链接直接指向文件的 i 节点。引入符号链接的原因是为了避开硬链接的一些限制:

  (1)硬链接通常要求链接和文件位于同一文件系统中。

  (2)只有超级用户才能创建指向目录的硬链接。

  对于符号链接以及它指向何种对象并无任何文件系统限制,任何用户都可创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构移到系统中的另一个位置。

  对符号链接以及它指向何种对象并无任何文件系统限制,任何用户都可创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构移到系统中的另一个位置。

  当使用以名字引用文件的函数时,应当了解函数是否处理符号链接。也就是该函数是否跟随符号链接达到它所链接的文件。如若该函数具有处理符号链接的功能,则其路径参数引用由符号链接指向的文件。否则,路径名参数将引用链接本身,而不是该链接指向的文件。

  下图列出了个函数对符号链接的处理:

技术分享图片

  表中没有列出 mkdir,mkinfo,mknod和rmdir函数,原因是,当路径名是符号链接时,它们都出错返回。

  表中也没有列出以文件描述符作为参数的一些函数(如 fstat、fchmod等),原因是,对符号链接的处理是由返回文件描述符的函数(通常是 open)进行的。

  chown 是否跟随符号链接取决于实现, Linux 2.1.81开始,chown跟随符号链接。

  表中一个例外是,同时用 O_CREAT 和 O_EXCL 调用 open 函数,若路径名引用符号链接, open 将出错返回,并将 errno 设置为 EEXIST。 这种处理方式的意图是堵塞一个安全性漏洞,使其有特权的进程不会被诱骗对不适当的文件进行写操作。

4.17 symlink 和 readlink 函数

  symlink 函数创建一个符号链接

int symlink(const char *actualpath, const char *sympath);

  该函数创建一个指向 actualpath的新目录项 sympath,在创建此符号链接时,并不要求 actualpath以及存在。并且,actualpath和 sympath 并不需要位于同一文件系统中。

  因为 open 函数跟随符号链接,所以需要一种方法打开链接本身,并读该链接中的名字。 readlink 函数提供了这种功能。

ssize_t readlink(const char *restrict pathname, char *restrict buf, 
size_t bufsize);

  此函数组合了 open、read、close的所有操作,如果函数成功执行,则它返回读入buf的字节数。在buf中返回的符号链接的内容不以 null 字符终止。

4.18 文件时间

  对每个文件保持有三个时间字段,它们的意义如下表:

技术分享图片

  注意修改时间(st_mtime)和更改状态时间(st_ctime)之间的区别。修改时间是文件内容最后一次被修改的时间。更改状态时间是该文件的 i 节点最后一次被修改的时间。在本章中我们已经说明了很多影响到 i 节点的操作,例如,更改文件的访问权限、用户ID、链接数等,但它们并没有更改文件的实际内容。因为 i 节点中的所有信息都是于文件实际内容分开存放的,所以,除了文件数据修改时间以外,还需要更改状态的时间。

  注意,系统并不保持对一个 i 节点的最后一次访问时间,所以 access 和 stat 函数并不更改这三个时间中的任一个。

4.19 utime函数

  一个文件的访问和修改时间可以用 utime 函数更改。

int utime(const char *pathname, const struct utimbuf *times);

4.20 mkdir 和 rmdir 函数

  mkdir 创建目录,rmdir 删除目录

int mkdir(const char *pathname, mode_t mode);

  此函数创建一个新的空目录。其中, . 和 .. 目录项是自动创建。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。

  常见错误是指定与文件相同的mode(只指定读、写权限)。但是,对于目录通常至少要设置1个执行权限位,以允许访问该目录中的文件名。

int rmdir(const char *pathname);

  用 rmdir 函数删除一个空目录,空目录是只包含 . 和 .. 这两项的目录。

  如果调用此函数使目录的链接计数成为0,并且也没有其他进程打开此目录,则释放由此目录占用的空间。如果在链接计数达到0时,有一个或几个进程打开了此目录,则在此函数返回前删除最后一个链接及 . 和 .. 项。另外,此目录中不能再创建新文件。但是再最后一个进程关闭它之前并不释放此目录。(即使另一个进程打开该目录,它们在此目录下也不能执行其他操作。这样处理的原因是,为了使 rmdir 函数成功执行,该目录必须是空的。)

4.21 读目录

  对于某个目录具有访问权限的任一用户都可读该目录,但是为了防止文件系统产生混乱,只有内核才能写目录。

4.22 chdir、fchdir和getcwd函数

  每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜杠开始的路径名位相对路径名)。当用户登陆到UNIX系统时,其当前工作目录通常是口令文件(/etc/passwd)中该用户登陆项的第6个字段——用户的起始目录(home directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。

  进程通过调用 chdir 或 fchdir 函数可以更改当前工作目录。

int chdir(const char *pathname);
int fchidr(int filedes);

4.23 设备特殊文件

  st_dev和 st_rdev。相关规则:

  (1)每个文件系统所在的存储设备都由其主次设备号表示。设备号所用的数据类型是基本系统数据类型 dev_t。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。磁盘驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件系统通常具有相同的主设备号,但它们的次设备号却不同。

  (2)我们通常使用两个宏即 major 和 minor 来访问 主、次设备号,大多数实现都定义了这两个宏。这就意味着我们无需关系这两个数是如何存放在 dev_t 对象中的。

  (3)系统中每个文件名关联的 st_dev 值 是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的 i 节点。

  (4)只有字符特殊文件和块特殊文件才有 st_rdev 值。此值包含实际设备的设备号。

4.24 文件权限位小结

技术分享图片

 



以上是关于第四章 文件和目录的主要内容,如果未能解决你的问题,请参考以下文章

第四章——续3.1

第四章:文件和目录

《python编程》第四章——文件和目录工具

软件工程第四次作业

第四章 文件和目录

第四篇 函数