APUE 文件和目录

《UNIX环境高级编程》第4章 文件和目录 笔记

函数stat、fstat、fstatat和lstat

返回文件信息的主要有4个函数

1
2
3
4
5
6
7
8
#include <sys/stat.h>
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);
// All four return: 0 if OK, −1 on error

一旦给出pathname:

  • stat将返回与此命名文件有关的信息结构:
  • fstat函数获得已在描述符fd上打开文件的有关信息
  • 1stat函数类似于stat,但是当命名的文件是一个符号链接时,1stat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息。
  • fstatat函数为一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息。

第二个参数buf是一个指针,其指向一个我们必须提供的结构。函数来填充由buf指向的结构。其基本格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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_atim; /* time of last access */
struct timespec st_mtim; /* time of last modification */
struct timespec st_ctim; /* time of last file status change */
blksize_t st_blksize; /* best I/O block size */
blkcnt_t st_blocks; /* number of disk blocks allocated */
};

文件类型

UNIX系统的大多数文件是普通文件或目录,但是也有另外一些文件类型。文件类型包括如下几种:

  1. 普通文件(regular file)文本文件或者二进制文件
  2. 目录文件(directory file),对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。
  3. 块特殊文件(block special file),这种类型的文件提供对设备(如磁盘)带缓冲的访问,
  4. 字符特殊文件(character special file)。这种类型的文件提供对设备不带缓冲的访问
  5. FIFO,用于进程间通信的命名管道
  6. 套接字(socket),用于网络间进程通信
  7. 符号链接(symbol link),这种类型的文件指向另一个文件

文件类型信息包含在stat结构的 st_mode成员中。也可以使用自带的宏确定的文件类型

设置用户ID和设置组ID

和一个进程相关联的ID有6个:

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

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

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

文件访问权限

st_mode值也包含了对文件的访问权限位。当提及文件时,指的是前面所提到的任何类型的文件。所有文件类型(目录、字符特别文件等)都有访问权限( access permission)。

3类访问权限的使用方式如下:

  • 我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。这就是为什么对于目录其执行权限位常被称为搜索位的原因
  • 对于一个文件的读权限决定了我们是否能够打开现有文件进行读操作
  • 对于一个文件的写权限决定了我们是否能够打开现有文件进行写操作
  • 为了在open函数中对一个文件指定O_TRUNC标志,必须对该文件具有写权限
  • 为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限
  • 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。

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

内核测试的具体内容:

  1. 若进程的有效用户ID是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分的自由
  2. 若进程的有效用户ID等于文件的所有者ID(也就是进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否则拒绝访问。
  3. 若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问;否则拒绝访问。
  4. 若其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问

新文件和目录的所有权

新文件的用户ID设置为进程的有效用户ID。

关于组ID,POSIX实现有两种方案:

  • 进程有效组ID
  • 所在目录的组ID

函数access和faccessat

正如前面所说,当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础执行其访问权限测试。有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。

accessfaccessat函数是按实际用户ID和实际组ID进行访问权限测试的。(该测试也分成4步)

1
2
3
4
5
6
#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
// Both return: 0 if OK, −1 on error

flag参数可以用于改变faccessat的行为,如果fag设置为AT_EACCESS,访问检查用的是调用进程的有效用户ID和有效组D,而不是实际用户ID和实际组ID

函数umask

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

1
2
3
4
5
#include <sys/stat.h>
mode_t umask(mode_t cmask);
// Returns: previous file mode creation mask

cmask是9个权限访问为常量(S_IRUSR、S_IWUSR)的若干个或组成的

在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字

UNX系统的大多数用户从不处理他们的 umask值。通常在登录时,由 shell的启动文件设置一次,然后,再不改变。

用户可以设置 umask值以控制他们所创建文件的默认权限。该值表示成八进制数,一位代表一种要屏蔽的权限,这示于图4-10中。设置了相应位后,它所对应的权限就会被拒绝常用的几种 umask值是002、022和027。002阻止其他用户写入你的文件,022阻止同组成员和其他用户写入你的文件,027阻止同组成员写你的文件以及其他用户读、写或执行你的文件。

函数chmod、fchmod和fchmodat

chmod、fchmod和fchmodat这3个函数使我们可以更改现有文件的访问权限

1
2
3
4
5
6
7
8
9
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
// All three return: 0 if OK, −1 on error

chmod函数在指定的文件上进行操作,而fchmod函数则对已打开的文件进行操作。fchmodat函数与chmod函数在下面两种情况下是相同的:一种是 pathname参数为绝对路径另一种是/参数取值为 AT_FDCWD而 pathname参数为相对路径。

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

粘着位

在UNIX尚未使用请求分页式技术的早期版本中, S_ISVTX位被称为粘着位( sticky bit)。如果一个可执行程序文件的这一位被设置了,那么当该程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区(程序的正文部分是机器指令)。这使得下次执行该程序时能较快地将其装载入内存。

后来的UNIX版本称它为保存正文位( saved-text bit),因此也就有了常量S_ISVTX。现今较新的UNX系统大多数都配置了虚拟存储系统以及快速文件系统,所以不再需要使用这种技术

现在粘着位的使用方法是:
如果对一个目录设置了粘着位,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件

  • 拥有此文件
  • 拥有此目录
  • 是超级用户

函数chown、fchown、fchownat和lchown

下面几个chown函数可用于更改文件的用户ID和组ID。如果两个参数 owner或 group中的任意一个是-1,则对应的ID不变

1
2
3
4
5
6
7
8
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);
int lchown(const char *pathname, uid_t owner, gid_t group); All four return:
// 0 if OK, −1 on error

基于BSD的系统一直规定只有超级用户才能更改一个文件的所有者这样做的原因是防止用户改变其文件的所有者从而摆脱磁盘空间限额对他们的限制。 System V则允许任一用户更改他们所拥有的文件的所有者。

文件长度

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

对于普通文件,其文件长度可以是0;对于目录,文件长度通常是一个数(如16或512)的整倍数;对于符号链接,文件长度是在文件名中的实际字节数。

现今,大多数现代的UNIⅨ系统提供字段stb1ks1ze和st_b1ocks。其中,第一个是对文件I/O较合适的块长度,第二个是所分配的实际512字节块块数。

文件空洞

空洞是由所设置的文件偏移量超过文件尾端,并写入了某些数据造成的。

文件截断

有时我们需要在文件尾端处截去一些数据以缩短文件。将一个文件的长度截断为0是一个特例,在打开文件时使用 O_TRUNC标志可以做到这一点。

1
2
3
4
5
6
7
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
// Both return: 0 if OK, −1 on error

这两个函数将一个现有文件长度截断为 length。如果该文件以前的长度大于 length,则超过length以外的数据就不再能访问。如果以前的长度小于 length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)

文件系统

目前,正在使用的UNIX文件系统有多种实现。例如, Solaris支持多种不同类型的磁盘文件系统:传统的基于BSD的UNIX文件系统(称为UFS),读、写DOS格式软盘的文件系统(称为PCFS),以及读CD的文件系统(称为HSFS)。

我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统,i节点是固定长度的记录项,它包含有关文件的大部分信息。

更仔细地观察一个柱面组的i节点和数据块部分:

在图中有两个目录项指向同一个i节点。每个i节点中都有一个链接计数,其值是指向该i节点的目录项数。只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)。这就是为什么“解除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”的原因。这也是为什么删除一个目录项的函数被称之为un1ink

另外一种链接类型称为符号链接( symbolic link)。符号链接文件的实际内容(在数据块中)包含了该符号链接所指向的文件的名字。

节点包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度和指向文件数据块的指针等。stat结构中的大多数信息都取自i节点。只有两项重要数据存放在目录项中:文件名和节点编号。

因为目录项中的i节点编号指向同一文件系统中的相应i节点,一个目录项不能指向另个文件系统的i节点。这就是为什么1n(1)命令不能跨越文件系统的原因

当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并删除老的目录项。链接计数不会改变。

我们说明了普通文件链接计数的概念,对于目录的链接计数相关方法如下:

任何一个叶目录(不包含任何其他目录的目录)的链接计数总是2,数值2来自于命名该目录( testdir)的目录项以及在该目录中的.项。在父目录中的每一个子目录都使该父目录的链接计数增加1

函数link、linkat、unlink、unlinkat和remove

创建一个指向现有文件的链接的方法是使用linke或者linkat函数:

1
2
3
4
5
6
7
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
// Both return: 0 if OK, −1 on error

这两个函数创建一个新目录项newpath,它引用现有文件 existingpath

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

1
2
3
4
5
6
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
// Both return: 0 if OK, −1 on error

这两个函数删除目录项,并将由pathname所引用文件的链接计数减1。如果对该文件还有其他链接,则仍可通过其他链接访问该文件的数据。如果出错,则不对该文件做任何更改。

我们在前面已经提及,为了解除对文件的链接,必须对包含该目录项的目录具有写和执行权限。如果对该目录设置了粘着位,则对该目录必须具有写权限,并且具备下面三个条件之一:拥有该文件、拥有该目录或者具有超级用户权限

只有当链接计数达到0时,该文件的内容才可被删除。另一个条件也会阻止删除文件的内容——只要有进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程个数;如果这个计数达到0,内核再去检查其链接计数;如果计数也是0,那么就删除该文件的内容。

ulink的这种特性经常被程序用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用open或 creat创建一个文件,然后立即调用 unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时,该文件的内容才被删除。

我们也可以用remove函数解除对一个文件或目录的链接。对于文件,remove的功能与unlink相同。对于目录,remove的功能与rmdir相同

1
2
3
4
5
#include <stdio.h>
int remove(const char *pathname);
// Returns: 0 if OK, −1 on error

函数rename和renameat

文件或目录可以用 rename函数或者 renameat函数进行重命名

1
2
3
4
5
6
#include <stdio.h>
int rename(const char *oldname, const char *newname);
int renameat(int oldfd, const char *oldname, int newfd,const char *newname);
// Both return: 0 if OK, −1 on error

符号链接

符号链接是对一个文件的间接指针,它与上一节所述的硬链接有所不同,硬链接直接指向文件的i节点。引入符号链接的原因是为了避开硬链接的一些限制:

  • 硬链接通常要求链接和文件位于同一文件系统中。
  • 只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)。

对符号链接以及它指向何种对象并无任何文件系统限制,任何用户都可以创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构移到系统中另一个位置

创建和读取符号链接

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

1
2
3
4
5
6
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
// Both return: 0 if OK, −1 on error

因为open函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字read1ink和 readlinkat函数提供了这种功能。

1
2
3
4
5
6
#include <unistd.h>
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);
// Both return: number of bytes read if OK, −1 on error

文件的时间

每个文件属性所保存的实际精度依赖于文件系统的实现。对于把时间戳记录在秒级的文件系统来说,纳秒这个字段就会被填充为0。对于时间戳的记录精度高于秒级的文件系统来说,不足秒的值被转换成纳秒并记录在纳秒这个字段中。

每个文件维护3个时间字段:

注意修改时间( st_mtim)和状态更改时间( st_ctim)之间的区别。修改时间是文件内容最后一次被修改的时间。状态更改时间是该文件的i节点最后一次被修改的时间。系统并不维护对一个i节点的最后一次访问时间,所以 access和stat函数并不更改这3个时间中的任一个。

函数futimens、utimensat和utimes

一个文件的访问和修改时间可以用以下几个函数更改。futimensutimensat函数可以指定纳秒级精度的时间戳。用到的数据结构是与stat函数族相同的timespec结构。

1
2
3
4
5
6
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
// Both return: 0 if OK, −1 on error

函数mkdir、mkdirat和rmdir

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

1
2
3
4
5
6
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
// Both return: 0 if OK, −1 on error

这两个函数创建一个新的空目录。其中,.和..目录项是自动创建的。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。

rmdir函数可以删除一个空目录。空目录是只包含.和..这两项的目录

1
2
3
4
5
#include <unistd.h>
int rmdir(const char *pathname);
// Returns: 0 if OK, −1 on error

读目录

对某个目录具有访问权限的任一用户都可以读该目录,但是,为了防止文件系统产生混乱只有内核才能写目录。

读目录相关的系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
// Both return: pointer if OK, NULL on error
struct dirent *readdir(DIR *dp);
// Returns: pointer if OK, NULL at end of directory or error
void rewinddir(DIR *dp);
int closedir(DIR *dp);
// Returns: 0 if OK, −1 on error
long telldir(DIR *dp);
// Returns: current location in directory associated with dp
void seekdir(DIR *dp, long loc);

函数chdir、fchdir和getcwd

每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)。当用户登录到UNIX系统时,其当前工作目录通常是口令文件(/etc/ passwd)中该用户登录项的第6个字段——用户的起始目录( home directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性

进程调用chdir和fchdir函数可以改变当前工作目录

1
2
3
4
5
6
7
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
// Both return: 0 if OK, −1 on error

我们需要一个函数,它从当前工作目录(.)开始,用.找到其上一级目录,然后读其目录项,直到该目录项中的i节点编号与工作目录i节点编号相同,这样地就找到了其对应的文件名按照这种方法,逐层上移,直到遇到根,这样就得到了当前工作目录完整的绝对路径名。很幸运,函数 getcwd就提供了这种功能

1
2
3
4
5
#include <unistd.h>
char *getcwd(char *buf, size_t size);
// Returns: buf if OK, NULL on error

文件访问权限位小结