APUE第四章 文件和目录

第四章 文件和目录

4.1 引言

上一章我们说明了执行I/O操作的基本函数,其中的讨论是围绕普 通文件I/O进行的—打开文件、读文件或写文件。本章将描述文件系统 的其他特征和文件的性质。我们将从stat函数开始,逐个说明stat结构的 每一个成员以了解文件的所有属性。在此过程中,我们将说明修改这些 属性的各个函数(更改所有者、更改权限等),还将更详细地说明 UNIX文件系统的结构以及符号链接。本章最后介绍对目录进行操作的 各个函数,并且开发了一个以降序遍历目录层次结构的函数。

4.2 函数stat、fstat、fstatat和lstat

本章主要讨论4个stat函数以及它们的返回信息。

#include 
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);

所有4个函数的返回值:若成功;返回0;若出错,返回-1

一旦给出pathname,stat函数将返回与此命名文件有关的信息结构。
fstat函数获得已在描述符fd上打开文件的有关信息。lstat函数类似于 stat,但是当命名的文件是一个符号链接时,lstat返回该符号链接的有关 信息,而不是由该符号链接引用的文件的信息。(在4.22节中,当以降 序遍历目录层次结构时,需要用到lstat。4.17节将更详细地说明符号链 接。)

fstatat函数为一个相对于当前打开目录(由fd参数指向)的路径名返 回文件统计信息。flag参数控制着是否跟随着一个符号链接。当 AT_SYMLINK_NOFOLLOW标志被设置时,fstatat不会跟随符号链接, 而是返回符号链接本身的信息。否则,在默认情况下,返回的是符号链 接所指向的实际文件的信息。如果fd参数的值是AT_FDCWD,并且 pathname参数是一个相对路径名, fstatat会计算相对于当前目录的pathname参数。如果pathname是一个绝对路径,fd参数就会被忽略。这 两种情况下,根据flag的取值,fstatat的作用就跟stat或lstat一样

第2个参数buf是一个指针,它指向一个我们必须提供的结构。函数 来填充由buf指向的结构。结构的实际定义可能随具体实现有所不同, 但其基本形式是:

struct stat {
mode_t        st_mode;   /* file type & mode (permissions) */
ino_t        st_ino;    /* i-node number (serial number) */
dev_t        st_dev;    /* device number (file system) */
dev_t        st_rdev;   /* device number for special files */
nlink_t       st_nlink;  /* number of links */ uid_t        st_uid;    /* user ID of owner*/
gid_t        st_gid;    /* group ID of owner*/
off_t        st_size;   /* size in bytes, for regular files */
struct timespec   st_atime;  /* time of last access */
struct timespec   st_mtime;  /* time of last modification */
struct timespec   st_ctime;  /* time of last file status change */
blksize_t      st_blksize; /* best I/O block size */
blkcnt_t       st_blocks;  /* number of disk blocks allocated */
};

POSIX.1未要求st_rdev、st_blksize和st_blocks字段。Single UNIX Specification XSI扩展定义了这些字段。
timespec结构类型按照秒和纳秒定义了时间,至少包括下面两个字 段:
time_t tv_sec;
long tv_nsec; 在2008年版以前的标准中,时间字段定义成st_atime、st_mtime以
及st_ctime,它们都是time_t类型的(以秒来表示)。timespec结构提 供了更高精度的时间戳。为了保持兼容性,旧的名字可以定义成tv_sec 成员。例如,st_atime可以定义成st_atim.tv_sec。

注意,stat结构中的大多数成员都是基本系统数据类型(见2.8 节)。我们将说明此结构的每个成员以了解文件属性。
使用 stat 函数最多的地方可能就是 ls -l 命令,用其可以获得有关一 个文件的所有信息。

4.3 文件类型

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

(1)普通文件(regular file)。这是最常用的文件类型,这种文件 包含了某种形式的数据。至于这种数据是文本还是二进制数据,对于 UNIX内核而言并无区别。对普通文件内容的解释由处理该文件的应用 程序进行。
一个值得注意的例外是二进制可执行文件。为了执行程序,内核必 须理解其格式。所有二进制可执行文件都遵循一种标准化的格式,这种 格式使内核能够确定程序文本和数据的加载位置。

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

(3)块特殊文件(block special file)。这种类型的文件提供对设备 (如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。
注意,FreeBSD不再支持块特殊文件。对设备的所有访问需要通过 字符特殊文件进行。

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

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

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

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

文件类型信息包含在stat结构的st_mode成员中。可以用图4-1中的宏 确定文件类型。这些宏的参数都是stat结构中的st_mode成员。
image.png

允许实现将进程间通信(IPC)对象(如消息队列和信号量 等)说明为文件。图4-2 中的宏可用来从 stat 结构中确定 IPC 对象的类 型。这些宏与图 4-1 中的不同,它们的参数并非st_mode,而是指向stat 结构的指针。

image.png

消息队列、信号量以及共享存储对象等将在第 15 章中讨论。

我们写一个简单的代码查看一下文件类型 和文件ino

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "../all.h"


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

    for (i = 0; i < argc; ++i) {
        printf("file name : %s ", argv[i]);
        if (lstat(argv[i], &buf) < 0) {
            printf("lsata error\n");
            continue;
        }

        if (S_ISREG(buf.st_mode))
        {
            ptr = "regular";
        }
        else if (S_ISDIR(buf.st_mode))
        {
            ptr = "dirent";
        }
        else if (S_ISCHR(buf.st_mode))
        {
            ptr = "charfile";
        }
        else if (S_ISBLK(buf.st_mode))
        {
            ptr = "blockfile";
        }
        else if (S_ISFIFO(buf.st_mode))
        {
            ptr = "fifo file";
        }
        else if (S_ISLNK(buf.st_mode))
        {
            ptr = "islink";
        }
        else if (S_ISSOCK(buf.st_mode))
        {
            ptr = "socket";
        }
        else
        {
            ptr = "other file";
        }
        
        printf("file_type : %s, file_ino: %lld\n", ptr, buf.st_ino);
   }

    return 0;
}
image.png

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

早期的UNIX版本并不提供S_ISxxx宏,于是就需要将st_mode与屏蔽 字S_IFMT进行逻辑“与”运算,然后与名为S_IFxxx的常量相比较。大 多数系统在文件中定义了此屏蔽字和相关的常量。如若查看 此文件,则可找到S_ISDIR宏定义为:

define S_ISDIR (mode) (((mode) & S_IFMT) == S_IFDIR)

我们说过,普通文件是最主要的文件类型,但是观察一下在一个给 定的系统中各种文件的比例是很有意思的。图4-4显示了在一个单用户 工作站Linux系统中的统计值和百分比。这些数据是由4.22节中的程序得 到的。


image.png

4.4 用户id和组id

与一个进程相关联的ID有6个或更多

file

•实际用户ID和实际组ID 标识我们究竟是谁。这两个字段在登录时 取自口令文件中的登录项。通常,在一个登录会话期间这些值并不改 变,但是超级用户进程有方法改变它们, 8.11节将说明这些方法。
•有效用户ID、有效组ID以及附属组ID决定了我们的文件访问权 限,下一节将对此进行说明(我们已在1.8节中说明了附属组ID)。
•保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有 效用户ID和有效组ID的副本,在8.11节中说明setuid函数时,将说明这两 个保存值的作用。

每个文件有一个所有者和组所有者,所有者由stat结构中的st_uid指 定,组所有者则由st_gid指定。

例如,若文件所有者是超级用户,而且设置了该文件的设置用户 ID 位,那么当该程序文件由一个进程执行时,该进程具有超级用户权 限。不管执行此文件的进程的实际用户 ID 是什么,都会是这样。例 如,UNIX 系统程序passwd(1)允许任一用户改变其口令,该程序是一个 设置用户 ID 程序。因为该程序应能将用户的新口令写入口令文件中 (一般是/etc/passwd 或/etc/shadow),而只有超级用户才具有对该文 件的写权限,所以需要使用设置用户 ID 功能。因为运行设置用户ID 程 序的进程通常会得到额外的权限,所以编写这种程序时要特别谨慎。第 8 章将更详细地讨论这种类型的程序。

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

4.5 文件访问权限

st_mode值也包含了对文件的访问权限位。当提及文件时,指的是 前面所提到的任何类型的文件。所有文件类型(目录、字符特别文件 等)都有访问权限(access permission)。很多人认为只有普通文件有访 问权限,这是一种误解。

每个文件有9个访问权限位,可将它们分成3类

image.png

图4-6中的3类访问权限(即读、写及执行)以各种方式由不同的函 数使用。我们将这些不同的使用方式汇总在下面。当说明相关函数时, 再进一步讨论。
•第一个规则是,我们用名字打开任一类型的文件时,对该名字中 包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权 限。这就是为什么对于目录其执行权限位常被称为搜索位的原因。
例如,为了打开文件/usr/include/stdio.h,需要对目录/、/usr 和/usr/include具有执行权限。然后,需要具有对文件本身的适当权限, 这取决于以何种模式打开它(只读、读-写等)。

如果当前目录是/usr/include,那么为了打开文件stdio.h,需要对当 前目录有执行权限。这是隐含当前目录的一个示例。打开stdio.h文件与 打开./stdio.h作用相同。注意,对于目录的读权限和执行权限的意义是 不相同的。读权限允许我们读目录,获得在该目录中所有文件名的列 表。当一个目录是我们要访问文件的路径名的一个组成部分时,对该目 录的执行权限使我们可通过该目录(也就是搜索该目录,寻找一个特定 的文件名)。引用隐含目录的另一个例子是,如果PATH环境变量 (8.10节将对其进行说明)指定了一个我们不具有执行权限的目录,那 么shell绝不会在该目录下找到可执行文件。

•对于一个文件的读权限决定了我们是否能够打开现有文件进行读 操作。这与open函数的O_RDONLY和O_RDWR标志相关。
•对于一个文件的写权限决定了我们是否能够打开现有文件进行写 操作。这与open函数的O_WRONLY和O_RDWR标志相关。
•为了在open函数中对一个文件指定O_TRUNC标志,必须对该文件 具有写权限。
•为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。

•为了删除一个现有文件,必须对包含该文件的目录具有写权限和
执行权限。对该文件本身则不需要有读、写权限。 •如果用7个exec函数(见8.10节)中的任何一个执行某个文件,都必
须对该文件具有执行权限。该文件还必须是一个普通文件。

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

1)若进程的有效用户ID是0(超级用户),则允许访问。这给予 了超级用户对整个文件系统进行处理的最充分的自由。
(2)若进程的有效用户ID等于文件的所有者ID(也就是进程拥有 此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否 则拒绝访问。适当的访问权限位指的是,若进程为读而打开该文件,则 用户读位应为1;若进程为写而打开该文件,则用户写位应为1;若进程 将执行该文件,则用户执行位应为1。
(3)若进程的有效组ID或进程的附属组ID之一等于文件的组ID, 那么如果组适当的访问权限位被设置,则允许访问;否则拒绝访问。
(4)若其他用户适当的访问权限位被设置,则允许访问;否则拒 绝访问。
按顺序执行这 4 步。注意,如果进程拥有此文件(第 2 步),则按 用户访问权限批准或拒绝该进程对文件的访问—不查看组访问权限。类 似地,若进程并不拥有该文件。但进程属于某个适当的组,则按组访问 权限批准或拒绝该进程对文件的访问—不查看其他用户的访问权限。

4.6 新文件和目录的所有权

在第3章中讲述用open或creat创建新文件时,我们并没有说明赋予 新文件的用户ID和组ID是什么。4.21节将说明mkdir函数,此时就会了解 如何创建一个新目录。关于新目录的所有权规则与本节将说明的新文件 所有权规则相同。
新文件的用户ID设置为进程的有效用户ID。关于组ID,POSIX.1允 许实现选择下列之一作为新文件的组ID。

(1)新文件的组ID可以是进程的有效组ID。
(2)新文件的组ID可以是它所在目录的组ID。

FreeBSD 8.0和Mac OS X 10.6.8总是使用目录的组ID作为新文件的
组ID。有些Linux文件系统使用 mount(1)命令选项允许在 POSIX.1 提 出的两种选项中进行选择。对于 Linux 3.2.0 和Solaris 10,默认情 况下,新文件的组ID取决于它所在的目录的设置组ID位是否被设置。如 果该目录的这一位已经被设置,则新文件的组ID设置为目录的组ID;否 则新文件的组ID设置为进程的有效组ID。

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

4.7 函数access和faccessat

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

#include 
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);

两个函数的返回值:若成功,返回0;若出错,返回-1 其中,如果测试文件是否已经存在,mode就为F_OK;否则mode是
图4-7中所列常量的按位或。


image.png

faccessat函数与access函数在下面两种情况下是相同的:一种是 pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而 pathname参数为相对路径。否则,faccessat计算相对于打开目录(由fd参 数指向)的pathname。
flag参数可以用于改变faccessat的行为,如果flag设置为 AT_EACCESS,访问检查用的是调用进程的有效用户ID和有效组ID, 而不是实际用户ID和实际组ID。

4.8 umask

umask(mode_t cmask)
umask(S_IRGRP | S_IWGRP | S_IROTH)

4.11 chown、fcbown 、fchownat 、lchown

int chown(const char * pathname, uid_t owner, gid_t group)
int fchown(int fd , uid_t owner, gid_t group)
int fchown(int fd, const char * pathname, uid_t owner, gid_t group, int flag);

4.13文件截断

#include 
int truncate(const char * pathname, off_t length);
int ftruncate(int fd, off_t length);

4.14 文件系统

link、linkat、unlink、unlinkat 和remove

#include 
int link(const char * existpath, const char * newpath);
int linkat(int fd, const char *existinpath, int nfd, const char * newpath, int flag);
#include 
int unlink(const char * pathname);
int unlinkat(int fd, const char *pathname, int flag);
int remove(char name

4.17符号链接

image.png

4.18创建和读取符号链接

可以用symlink或symlinkat函数创建一个符号链接。

#include 
int symlink(const char *actualpath, const char *sympath); 
int symlinkat(const char *actualpath, int fd, const char *sympath);

两个函数的返回值:若成功,返回0;若出错,返回-1 函数创建了一个指向actualpath的新目录项sympath。在创建此符号链接时,并不要求actualpath已经存在(在上一节结束部分的例子中我们 已经看到了这一点)。并且,actualpath和sympath并不需要位于同一文 件系统中。

因为open函数跟随符号链接,所以需要有一种方法打开该链接本 身,并读该链接中的名字。readlink和readlinkat函数提供了这种功能。
#include 
ssize_t readlink(const char *restrict pathname, char *restrict buf,
size_t bufsize);
ssize_t readlinkat(int fd, const char* restrict pathname,
char *restrict buf, size_t bufsize);

futimens、utimensat、utimes

#include 
int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const structtimespec times[2], int flag);
#include 
int utimes(const char *pathname, const struct timeval times[2]);

utimes函数对路径名进行操作。times参数是指向包含两个时间戳 (访问时间和修改时间)元素的数组的指针,两个时间戳是用秒和微妙 表示的。
struct timeval {
time_t tv_sec; /* seconds / long tv_usec; / microseconds */
};

4.23 chdir 、fcdir 、getcwd

#include 
int chdir(const char *pathname); 
int fchdir(int fd);
include 
char *getcwd(char *buf, s i z e_t size);

个函数的返回值:若成功,返回0;若出错,返回-1 在这两个函数中,分别用pathname或打开文件描述符来指定新的当前工作目录。 实例因为当前工作目录是进程的一个属性,所以它只影响调用 chdir 的 进程本身,而不影响其他进程(我们将在第8章更详细地说明进程之间 的关系)。这就意味着图4-23的程序并不会产生我们可能希望得到的结果。

getcwd
返回值:若成功,返回buf;若出错,返回NULL 必须向此函数传递两个参数,一个是缓冲区地址buf,另一个是缓 冲区的长度size(以字节为单位)。该缓冲区必须有足够的长度以容纳
绝对路径名再加上一个终止 null 字节,否则返回出错(请回忆2.5.5节中 有关为最大长度路径名分配空间的讨论)。
某些getcwd的早期实现允许第一个参数buf为NULL。在这种情况 下,此函数调用malloc动态地分配size字节数的空间。这不是POSIX.1 或Single UNIX Specification的所属部分,应当避免使用。

4.26 特殊设备文件

st_dev和st_rdev这两个字段经常引起混淆,在18.9节,我们编写 ttyname函数时,需要使用这两个字段。有关规则很简单:

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

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

#include 
#include 
#include 
#include 
#include "../all.h"
#include 
#include 

int main(int argc, char *argv[]) {
    printf("hello world\n");
    
    struct stat buf;
    stat(argv[1],&buf);
    
    printf("dev = %d/%d\n", major(buf.st_dev), minor(buf.st_dev));
    return 0;
}

4.25 文件访问权限小结

我们已经说明了所有文件访问权限位,其中某些位有多种用途。图 4-26列出了所有这些权限位,以及它们对普通文件和目录文件的作用。
最后9个常量还可以分成如下3组:
S_IRWXU = S_IRUSR|S_IWUSR|S_IXUSR S_IRWXG = S_IRGRP|S_IWGRP|S_IXGRP S_IRWXO = S_IROTH|S_IWOTH|S_IXOTH


image.png

你可能感兴趣的:(APUE第四章 文件和目录)