APUE - 文件和目录

stat、fstat 和 lstat 函数

#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 *restruct pathname, struct stat *restrict buf);

一旦给出 pathname,stat 函数就返回与命名文件有关的信息结构。fstat 函数获取已在描述符 filedes 上打开文件的有关信息。lstat 函数类似于 stat,但是当命名的文件是一个符号链接时,lstat 返回该符号链接的有关信息,而不是由该符号链接引用文件的信息。

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 */
    time_t      st_atime;   /* time of last access */
    time_t      st_mtime;   /* time of last modification */
    time_t      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 */
};

文件类型

文件类型包括如下几种:

  1. 普通文件。这是最常见的文件类型,这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据对于 UNIX 内核而言并无区别。对普通文件内容的解释由处理该文件的应用程序进行。一个值得注意的例外是二进制可执行文件。为了执行文件,内核必须理解其格式。所有二进制可执行文件都遵循一种格式,这种格式使得内核能够确定程序文本和数据的加载位置。
  2. 目录文件。这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。
  3. 块特殊文件。这种文件类型提供对设备(例如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。
  4. 字符特殊文件。这种文件类型提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
  5. FIFO。这种类型文件用于进程间通信,有时也将其称为命名管道。
  6. 套接字。这种文件类型用于进程间的网络通信。套接字也可用于在一台宿主机上进程之间的非网络通信。
  7. 符号链接。这种文件类型指向另一个文件。

文件类型信息包含在 stat 结构的 st_mode 成员中。

设置用户 ID 和设置组 ID

  • 实际用户 ID 和实际组 ID 标识我们究竟是谁。这两个字段在登录时取自口令文件中的登录项。
  • 有效用户 ID,有效组 ID 以及附加组 ID 决定了我们的文件访问权限。
  • 保存的设置用户 ID 和保存的设置组 ID 在执行一个程序时包含了有效用户 ID 和有效组 ID 的副本。

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

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

当执行一个程序时,进程的有效用户 ID 通常就是实际用户 ID,有效组 ID 通常是实际组 ID。但是可以在文件模式子中设置一个特殊标志,其含义是“当执行此文件时,将进程的有效用户 ID 设置为文件所有者的用户 ID”。与此相类似,在文件模式字中可以设置另一位,它使得将执行此文件的进程的有效组 ID 设置为文件的组所有者 ID。

文件访问权限

st_mode 值也包含了针对文件的访问权限位。

  • 我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。
  • 对于一个文件的读权限决定了我们是否能够打开该文件进行读操作。这与 open 函数的 O_RDONLY 和 O_RDWR 标志相关。
  • 对于一个文件的写权限决定了我们是否能够打开该文件进行写操作。这与 open 函数的 O_WRONLY 和 O_RDWR 标志相关。
  • 为了在 open 函数中对一个文件指定 O_TRUNC 标志,必须对该文件具有写权限。
  • 为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。
  • 为了删除一个现有的文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身则不需要有读、写权限。
  • 如果用 exec 函数执行某一个文件,必须对该文件具有执行权限。该文件还必须是一个普通文件。

进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试:

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

新文件和目录的所有权

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

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

新目录的所有权规则与新文件所有权规则相同。

access 函数

access 函数是按实际用户 ID 和实际组 ID 进行访问权限测试的。

#include <unistd.h>
int access(const char *pathname, int mode);

umask 函数

umask 函数为进程设置文件模式创建屏蔽字,并返回以前的值。

#include <sys/stat.h>
mode_t umask(mode_t cmask);

chmod 和 fchmod 函数

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

#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int filedes, mode_t mode);

chmod 函数在指定的文件上进行操作,而 fchmod 函数则对已打开的文件进行操作。

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

chown、fchown 和 lchown 函数

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

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchmod(int filedes, uid_t owner, gid_t group);
int lchown(const char *pathname, uid_t owner, gid_t group);

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

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

若 _POSIX_CHOWN_RESTRICTED 对指定的文件起作用,则

  1. 只有超级用户进程能更改该文件的用户 ID。
  2. 若满足下列条件,一个非超级用户进程就可以更改该文件的组 ID:
    1. 进程拥有此文件(其有效用户 ID 等于该文件的用户 ID)。
    2. 参数 owner 等于 -1 或者文件的用户 ID,并且参数 group 等于进程的有效组 ID 或进程的附加组 ID 之一。

文件长度

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

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

对于目录,文件长度通常是一个数的倍数。

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

文件截短

#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int filedes, off_t length);

这两个函数将把现有的文件长度截短为 length 字节。

link、unlink、remove 和 rename 函数

创建一个指向现有文件的链接的方法是使用 link 函数。

#include <unistd.h>
int link(const char *existingpath, const char *newpath);

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

#include <unistd.h>
int unlink(const char *pathname);

我们也可以用 remove 函数解除对一个文件或目录的链接。

#include <stdio.h>
int remove(const char *pathname);

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

#include <stdio.h>
int rename(const char *oldname, const char *newname);

symlink 和 readlink 函数

symlink 函数创建一个符号链接。

#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);

readlink 函数打开该链接本身,并读该链接中的名字。

#include <unistd.h>
readlink(const char *restrict pathname, char *restrict buf, size_t bufsize);

文件的时间

  • st_atime -- 文件数据的最后访问时间
  • st_mtime -- 文件数据的最后修改时间
  • st_ctime -- i-node 状态的最后更改时间

utime 函数

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

#include <utime.h>
int utime(const char *pathname, const struct utimbuf *times);

此函数所使用的数据结构是:

struct utimbuf {
    time_t  actime;  /* access time */
    time_t  modtime; /* modification time */
};

此结构中的两个时间值是日历时间,这是自 1970 年 1 月 1 日 00:00:00 以来国际标准时间所经过的秒数。

此函数的操作以及执行它所要求的特权取决于 times 参数是否是 NULL。

  • 如果 times 是一个空指针,则访问时间和修改时间两者都设置为当前时间。为了执行此操作必须满足下列两条件之一:进程的有效用户 ID 必须等于该文件的所有者 ID;或者进程对该文件必须具有写权限。
  • 如果 times 是非空指针,则访问时间和修改时间被设置为 times 所指向结构中的值。此时,进程的有效用户 ID 必须等于该文件的所有者 ID,或者进程必须是一个超级用户进程。对文件只具有写权限是不够的。

注意,我们不能对更改状态时间 st_ctime 指定一个值,当调用 utime 函数时,此字段将被自动更新。

mkdir 和 rmdir 函数

用 mkdir 函数创建目录,用 rmdir 函数删除目录。

#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);

#include <unistd.h>
int rmdir(const char *pathname);

读目录

对某个目录具有访问权限的任一用户都可以读该目录,只有内核才能写目录。一个目录的写权限位和执行权限位决定了在该目录中能否创建新文件以及删除文件,它们并不表示能否写目录本身。

#include <dirent.h>
DIR *opendir(const char *pathname);
struct dirent *readdir(DIR *dp);
void rewinddir(DIR *dp);
int closedir(DIR *dp);
long telldir(DIR *dp);
void seekdir(DIR *dp, long loc);

头文件 中定义的 dirent 结构与实现有关。几种典型的 UNIX 实现对此结构所作的定义至少包含下列两个成员:

struct dirent {
    ino_t   d_ino;                  /* i-node number */
    char    d_name[NAME_MAX + 1];   /* null-terminated filename */
};

chdir、fchdir 和 getcwd 函数

每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。

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

#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int filedes);

调用 getcwd 函数获得当前工作目录的绝对路径名。

#include <unistd.h>
char *getcwd(char *buf, size_t size);

当一个应用程序需要在文件系统中返回到其工作的起点时,getcwd 函数是有用的。在更换工作目录之前,我们可以调用 getcwd 函数先将其保存起来。在完成了处理后,就可将从 getcwd 获得的路径名作为调用参数传送给 chdir,这样就返回到了文件系统中的起点。

fchdir 函数向我们提供了一种完成此任务的便捷方法。在更换到文件系统中的不同位置前,无需调用 getcwd 函数,而是使用 open 打开当前工作目录,然后保存文件描述符。当希望回到原工作目录时,只要简单地将文件描述符传递给 fchdir。