C语言K&R圣经笔记 8.6 样例 - 列出目录内容

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 条目中包含的其他信息。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值