8.6 实例(目录列表)
我们常常还需要对文件系统执行另一种操作,以获得文件的有关信息,而不是读取文件的具体内容
目录列表程序便是其中的一个例子,比如 UNIX 命令 ls
,它打印一个目录中的文件名以及其它一些可选信息,如文件长度、访问权限等等
MS-DOS 操作系统中的 dir
命令也有类似的功能
由于 UNIX 中的目录就是一种文件,因此,ls
只需要读此文件就可获得所有的文件名
但是,如果需要获取文件的其它信息,比如长度等,就需要使用系统调用
在其它一些系统中,甚至获取文件名也需要使用系统调用,例如在 MS-DOS 系统中即如此
无论实现方式是否同具体的系统有关,我们需要提供一种与系统无关的访问文件信息的途径
以下将通过程序 fsize
说明这一点
fsize
程序是 ls
命令的一个特殊形式,它打印命令行参数表中指定的所有文件的长度
如果其中一个文件是目录,则 fsize
程序将对此目录递归调用自身
如果命令行中没有任何参数,则 fsize
程序处理当前目录
我们首先回顾 UNIX 文件系统的结构
在 UNIX 系统中,目录就是一种文件,这种文件包含了一个文件名列表以及指示这些文件位置的信息
“ 位置 ” 是一个指向其它表(即 inode
表)的索引
文件的 inode
是存放除文件名以外的所有文件信息的地方
目录项(directory entry)通常仅包含两个条目:文件名和 inode
编号
在不同版本的系统中,目录的格式和确切的内容是不一样的
因此,为了分离出不可移植的部分,我们把任务分成两部分
外层定义了一个称为 Dirent
的结构和 3 个函数 opendir
、readdir
和 closedir
它们提供与系统无关的对目录项中的名字和 inode
编号的访问
我们将利用此接口编写 fsize
程序,然后说明如何在与 Version 7 和 System V UNIX 系统的目录结构相同的系统上实现这些函数
其它情况留作练习
结构 Dirent
包含 inode
编号和文件名
文件名的最大长度由 NAME_MAX
设定,NAME_MAX
的值由系统决定
opendir
返回一个指向称为 DIR
的结构的指针,该结构与结构 FILE
类似,它将被 readdir
和 closedir
使用
所有这些信息存放在头文件 dirent.h
中
#define NAME_MAX 14 /* longest filename component; */
/* system-dependent */
typedef struct { /* portable directory entry */
long ino; /* inode number */
char name[NAME_MAX+1]; /* name + '\0' terminator */
} Dirent;
typedef struct { /* minimal DIR: no buffering, etc. */
int fd; /* file descriptor for the directory */
Dirent d; /* the directory entry */
} 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 /* inode information returned by stat */
{
dev_t st_dev; /* device of inode */
ino_t st_ino; /* inode number */
short st_mode; /* mode bits */
short st_nlink; /* number of links to file */
short st_uid; /* owners user id */
short st_gid; /* owners group id */
dev_t st_rdev; /* for special files */
off_t st_size; /* file size in characters */
time_t st_atime; /* time last accessed */
time_t st_mtime; /* time last modified */
time_t st_ctime; /* time originally created */
};
dev_t
和 ino_t
等类型在头文件 <sys/types.h>
中定义,程序中必须包含此文件
st_mode
项包含了描述文件的一系列标志,这些标志在 <sys/stat.h>
中定义
我们只需要处理文件类型的有关部分:
#define S_IFMT 0160000 /* type of file: */
#define S_IFDIR 0040000 /* directory */
#define S_IFCHR 0020000 /* character special */
#define S_IFBLK 0060000 /* block special */
#define S_IFREG 0010000 /* regular */
/* ... */
下面我们来着手编写程序 fsize
如果由 stat
调用获得的模式说明某文件不是一个目录,就很容易获得该文件的长度,并直接输出
但是,如果文件是一个目录,则必须逐个处理目录中的文件
由于该目录可能包含子目录,因此该过程是递归的
主程序 main
处理命令行参数,并将每个参数传递给函数 fsize
#include <stdio.h>
#include <string.h>
#include "syscalls.h"
#include <fcntl.h> /* flags for read and write */
#include <sys/types.h> /* typedefs */
#include <sys/stat.h> /* structure returned by stat */
#include "dirent.h"
void fsize(char *)
/* print file name */
main(int argc, char **argv)
{
if (argc == 1) /* default: current directory */
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: print the name of file "name" */
void fsize(char *name)
{
struct stat stbuf;
if (stat(name, &stbuf) == -1) {
fprintf(stderr, "fsize: can't access %s\n", name);
return;
}
if ((stbuf.st_mode & S_IFMT) == S_IFDIR)
dirwalk(name, fsize);
printf("%8ld %s\n", stbuf.st_size, name);
}
函数 dirwalk
是一个通用的函数,它对目录中的每个文件都调用函数 fcn
一次
它首先打开目录,循环遍历其中的每个文件,并对每个文件调用该函数,然后关闭目录返回
因为 fsize
函数对每个目录都要调用 dirwalk
函数,所以这两个函数是相互递归调用的
#define MAX_PATH 1024
/* dirwalk: apply fcn to all files in dir */
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, ".."))
continue; /* skip self and parent */
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 { /* directory entry */
ino_t d_ino; /* inode number */
char d_name[DIRSIZ]; /* long name does not have '\0' */
};
某些版本的系统支持更长的文件名和更复杂的目录结构
类型 ino_t
是使用 typedef
定义的类型,它用于描述 inode
表的索引
在我们通常使用的系统中,此类型为 unsigned short
,但是这种信息不应在程序中使用
因为不同的系统中该类型可能不同,所以使用 typedef
定义要好一些
所有的 “ 系统 ” 类型可以在文件 <sys/types.h>
中找到
opendir
函数首先打开目录,验证此文件是一个目录(调用系统调用 fstat
,它与 stat
类似,但它以文件描述符作为参数)
然后分配一个目录结构,并保存信息:
int fstat(int fd, struct stat *);
/* opendir: open a directory for readdir calls */
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: close directory opened by opendir */
void closedir(DIR *dp)
{
if (dp) {
close(dp->fd);
free(dp);
}
}
最后,函数 readdir
使用 read
系统调用读取每个目录项
如果某个目录位置当前没有使用(因为删除了一个文件),则它的 inode
编号为 0
,并跳过该位置
否则,将 inode
编号和目录名放在一个 static
类型的结构中,并给用户返回一个指向此结构的指针
每次调用 readdir
函数将覆盖前一次调用获得的信息
#include <sys/dir.h> /* local directory structure */
/* readdir: read directory entries in sequence */
Dirent *readdir(DIR *dp)
{
struct direct dirbuf; /* local directory structure */
static Dirent d; /* return: portable structure */
while (read(dp->fd, (char *) &dirbuf, sizeof(dirbuf)) == sizeof(dirbuf)) {
if (dirbuf.d_ino == 0) /* slot not in use */
continue;
d.ino = dirbuf.d_ino;
strncpy(d.name, dirbuf.d_name, DIRSIZ);
d.name[DIRSIZ] = '\0'; /* ensure termination */
return &d;
}
return NULL;
}
尽管 fsize
程序非常特殊,但是它的确说明了一些重要的思想
首先,许多程序并不是 “ 系统程序 ”,它们仅仅使用由操作系统维护的信息
对于这样的程序,很重要的一点是,信息的表示仅出现在标准头文件中,使用它们的程序只需要在文件中包含这些头文件即可,而不需要包含相应的声明
其次,有可能为与系统相关的对象创建一个与系统无关的接口
标准库中的函数就是很好的例子