8.6 样例 - 列出目录内容
除了读写文件之外,与文件系统之间还有另一种类型的交互,有时被称作——确定文件的相关信息,而不是文件的内容。像 UNIX 程序 ls 这样列出目录内容的程序就是一个例子——它打印出目录中的文件名,及其他可选信息,如文件大小、权限等等。MS-DOS 有类似的 dir 程序。
由于 UNIX 目录仅仅是一个文件,ls 只需要读它就能获取它所包含的文件名。但有必要使用系统调用来访问文件的其他信息,如文件的大小。在其他系统上,甚至连访问文件名都需要系统调用;例如 MS-DOS 就是如此。我们想要提供一种相对独立于系统的信息访问方式,尽管具体的实现可能是高度依赖系统的。
我们写个 fsize 程序来说明其中部分内容。fsize 是 ls 的一种特殊形式,它打印命令行参数列表中给出的所有文件的大小。如果某些文件是目录,则 fsize 把自己递归应用到这些目录上。如果没有任何参数,则处理当前目录。
首先简单回顾下 UNIX 文件系统结构。“目录”是一个文件,其中包含了一系列文件名,以及这些文件所在位置的标识。“位置” 是另一个叫做 “inode 列表” 的表格的索引。一个文件的 inode,是一个文件保存除文件名之外的所有相关信息的地方。目录的一个条目通常只包含两项,文件名和 inode编号。
遗憾的是,目录的格式和确切内容并非在所有版本的 UNIX 系统中都相同。因此我们决定把任务分成两部分,以隔离不可移植的部分。外层定义一个称为 Dirent 的结构和三个例程 opendir,readdir 和 closedir ,它们提供对目录条目中的文件名和 inode 的访问,不依赖具体的系统。我们将使用这个接口来写 fsize。然后我们说明如何在目录结构相同的系统,如 Version 7 和 System V UNIX 上实现这些;其他 UNIX 系统的实现作为练习。
Dirent 结构包含 inode 编号和文件名称。文件名部分的最大长度是 NAME_MAX,这是个依赖系统的值。opendir 返回一个类似 FILE,名为 DIR 结构的指针,供 readdir 和 closedir 使用。这些信息收集到名为 dirent.h 头文件中。
#define NAME_MAX 14 /* 最长的文件名部分,依赖系统 */
typedef struct { /* 可移植的目录条目 */
long ino; /* inode 编号 */
char name[NAME_MAX+1]; /* 名字 + '\0'结束符 */
} Dirent;
typedef struct { /* 尽可能最小实现的DIR:无缓存等 */
int fd; /* 目录的文件描述符 */
Dirent d; /* 目录的条目 */
} DIR;
DIR *opendir(char *dirname);
Dirent *readdir(DIR *dfd);
void closedir(DIR *dfd);
系统调用 stat 拿到一个文件名,并返回该文件 inode 中的所有信息,如果遇到错误,则返回 -1。因此下面的代码
char *name;
struct stat stbuf;
int stat(char *, struct stat *);
stat(name, &stbuf);
用文件 name 的 inode 信息填充结构体 stbuf 。描述 stat 返回值的结构体在 <sys/stat.h> 中,通常如下
struct stat /* stat 返回的 inode 信息*/
{
dev_t st_dev; /* inode的设备 */
ino_t st_ino; /* inode编号 */
short st_mode; /* 模式的多个bit位 */
short st_nlink; /* 文件的链接数 */
short st_uid; /* 属主的用户id */
short st_gid; /* 属主的组id */
dev_t st_rdev; /* 供特殊文件使用 */
off_t st_size; /* 文件大小,单位字节 */
time_t st_atime; /* 最近访问时间 */
time_t st_mtime; /* 最近修改时间 */
time_t st_ctime; /* inode最近修改时间 */
};
大部分值的含义都在注释里说明了。如 dev_t 和 ino_t 这样的类型定义在 <sys/type.h> 中,这个头文件也必须包含。
st_mode 项包含了一系列描述文件的标志位。标志位定义也包含在 <sys/stat.h> 中;我们只需要其中处理文件类型的那部分:
#define S_IFMT 0160000 /* 文件的类型: */
#define S_IFDIR 0040000 /* 目录 */
#define S_IFCHR 0020000 /* 特殊的字符设备 */
#define S_IFBLK 0060000 /* 特殊的块设备 */
#define S_IFREG 0100000 /* 常规 */
/* ... */
现在我们就可以开始写 fsize 程序。如果从 stat 获取到的模式表明该文件不是一个目录,则立马就能拿到它的大小并直接打印出来。然而如果文件是目录,我们必须每次处理目录中的一个文件;这个文件也可能是子目录,因此这个过程是递归的。
主例程处理命令行参数;它将每个参数传给 fsize 函数。
#include <stdio.h>
#include <string.h>
#include "syscalls.h"
#include <fcntl.h> /* 读写标志位 */
#include <sys/types.h> /* typdefs */
#include <sys/stat.h> /* stat返回的结构 */
#include "dirent.h"
void fsize(char *);
/* 打印文件大小 */
main(int argc, char **argv)
{
if (argc == 1) /* 默认:当前目录 */
fsize(".");
else
while (--argc > 0)
fsize(*++argv);
return 0;
}
fsize 函数打印文件的大小。然而如果文件是目录,则 fsize 调用 dirwalk 来处理其该目录下的所有文件。注意代码如何使用 <sys/stat.h> 中定义的标志位名称 S_IFMT 和 S_IFDIR 来判断一个文件是否为目录。括号很重要,因为 & 的优先级低于 ==。
int stat(char *, struct stat *);
void dirwalk(char *, void (*fcn)(char *));
/* fsize:打印文件“name”的大小 */
void fsize(char *name)
{
struct stat stbuf;
if (stat(name, &stbuf) == -1) {
fprintf(stderr, "fsize: cannot access %s\n", name);
return;
}
if ((stbuf.stmode & S_IFMT) == S_IFDIR)
dirwalk(name, fsize);
printf("%8ld %s\n", stbuf.st_size, name);
}
dirwalk 函数是将一个函数应用到目录中每个文件的通用例程。它打开目录,遍历其中的文件,对每一项都调用函数,然后关闭目录并返回。由于 fsize 读每个目录时都要调用 dirwalk,因此这两个函数互相递归调用。
#define MAX_PATH 1024
/* void dirwalk:对 dir 内的所有文件应用 fcn */
void dirwalk(char *dir, void (*fcn)(char *))
{
char name[MAX_PATH];
Dirent *dp;
DIR *dfd;
if ((dfd = opendir(dir)) == NULL) {
fprintf(stderr, "dirwalk: can't open %s\n", dir);
return;
}
while ((dp = readdir(dfd)) != NULL) {
if (strcmp(dp->name, ".") == 0
|| strcmp(dp->name, "..") == 0)
continue; /* 跳过当前目录和父目录 */
if (strlen(dir)+strlen(dp->name)+2 > sizeof(name))
fprintf(stderr, "dirwalk: name %s/%s too long\n",
dir, dp->name);
else {
sprintf(name, "%s/%s", dir, dp->name);
(*fcn)(name);
}
}
closedir(dfd);
}
每次调用 readdir 都返回指向下一个文件的信息的指针,若没有文件时则返回 NULL。每个目录总是包含了代表自身的项 “.”,以及上一级目录的项 “..”;它们必须要跳过,否则程序就死循环了。
在这一层为止,代码是不依赖目录格式的。下一步是要给出在特定系统上实现 opendir,readdir 和 closedir 的最小版本了。下面的例程用于 Version 7 和 System V UNIX 系统;它们使用了 <sys/dir.h> 头文件中的目录信息,如下:
#ifndef DIRSIZ
#define DIRSIZ 14
#endif
struct direct /* 目录条目 */
{
ino_t d_ion; /* inode编号 */
char d_name[DIRSIZE]; /* 长名字不含 '\0' */
}
某些版本的系统允许更长的文件名,并且有更复杂的目录结构。
ino_t 类型是 typedef 得到的类型,用作 inode 列表的下标 。在我们常用的系统中它正好是 unsigned short,但这类信息不应该嵌入到程序中;在不同的系统上可能会不同,因此用 typedef 更好。 “系统”类型的完整集合在 <sys/types.h> 中。
opendir 做的是打开目录,验证文件是目录(这里使用系统调用 fstat,类似于与 stat ,区别是用在文件描述符上),分配目录结构,并记录信息:
int fstat(int fd; struct stat *);
/* opendir:打开一个目录,供 readdir 调用 */
DIR *opendir(char *dirname)
{
int fd;
struct stat stbuf;
DIR *dp;
if ((fd = open(dirname, O_RDONLY, 0)) == -1
|| fstat(fd, &stbuf) == -1
|| (stbuf.st_mode & S_IFMT) != S_IFDIR
|| (dp = (DIR *) malloc(sizeof(DIR))) == NULL)
return NULL;
dp->fd = fd;
return dp;
}
closedir 关闭目录文件并释放内存空间:
/* closedir: 关闭由opendir打开的目录 */
void closedir(DIR *dp)
{
if (dp) {
close(dp->fd);
free(dp);
}
}
最后,readdir 使用 read 来读取每个目录条目。如果目录槽当前未被使用(由于文件已被删除),则 inode 编号为0,就会跳过这个位置。否则,inode 编号和名称会被放到一个 static 的结构中,而指向它的指针被返回给用户。每次调用会覆盖上次调用返回的信息。
#include <sys/dir.h> /* 本地目录结构 */
/* readir:按顺序读取目录条目 */
Dirent *readdir(DIR *dp)
{
struct direct dirbuf; /* 本地目录结构 */
static Dirent d; /* 返回的可移植结构 */
while (read(dp->fd, (char *) &dirbuf, sizeof(dirbuf))
== sizeof(dirbuf)) {
if (dirbuf.d_ino == 0) /* 槽位未使用 */
continue;
d.ino = dirbuf.d_ino;
strncpy(d.name, dirbuf.d_name, DIRSIZE);
d.name[DIRSIZE] = '\0'; /* 保证\0结尾 */
return &d;
}
return null;
}
尽管 fsize 程序是非常特殊的,但它的确说明了几个重要的思想。首先,很多程序并不是“系统程序”;它们仅仅是使用了操作系统维护的信息。对这样的程序,非常重要的一点是,只让这些信息的展示出现在标准头文件中,然后程序包含这些头文件,而不是把这些声明内嵌在程序内部。第二个观察到的点是,如果足够用心,就能够为依赖系统的对象创建一个相对不那么依赖系统的接口。标准库中的函数就是很好的例子。
练习8-5、修改 fsize 程序,使其打印 inode 条目中包含的其他信息。