参考引用
- UNIX 环境高级编程 (第3版)
- 黑马程序员-Linux 系统编程
1. 文件存储
- 一个文件主要由两部分组成,dentry (目录项) 和 Inode
- 所谓的删除文件,就是删除 Inode,但数据其实还是在硬盘上,以后会覆盖掉
1.1 Inode
- 其本质为结构体,存储文件的属性信息。如:权限、类型、大小、时间、用户、盘块位置
- Inode 也叫作文件属性管理结构,大多数的 Inode 都存储在磁盘上
- 少量常用、近期使用的 Inode 会被缓存到内存中
1.2 目录项 (dentry)
- 目录项,其本质依然是结构体,重要成员变量有两个 {文件名,Inode,…},而文件内容 (data) 保存在磁盘盘块中
2. 文件系统
2.1 函数 stat、fstat、fstatat 和 lstat
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
int fstatat(int fd, const char *pathname, struct stat *buf, int flag);
-
函数返回值
- 成功返回 0
- 出错返回 -1
-
一旦给出 pathname
- stat 函数将返回与此命名文件有关的信息结构
- fstat 函数获得已在描述符 fd 上打开文件的有关信息
- lstat 函数类似于 stat,但是当命名的文件是一个符号链接时,lstat 返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息
- stat 会拿到符号链接指向的那个文件或目录的属性,不想穿透符号就用 lstat
- fstatat 函数为一个相对于当前打开目录 (由 fd 参数指向) 的路径名返回文件统计信息
- flag 参数控制着是否跟随着一个符号链接。当 AT_SYMLINK_NOFOLLOW 标志被设置时,fstatat 不会跟随符号链接,而是返回符号链接本身的信息;否则,默认返回的是符号链接所指向的实际文件的信息
- 如果 fd 参数的值是 AT_FDCWD,并且 pathname 参数是一个相对路径名,fstatat 会计算相对于当前目录的 pathname 参数;如果 pathname 是一个绝对路径,fd 参数就会被忽略
-
第 2 个参数 buf 是一个指针,存放文件属性,是一个 Inode 结构体指针。其基本形式如下
struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* Inode number */ mode_t st_mode; /* File type and mode */ nlink_t st_nlink; /* Number of hard links */ uid_t st_uid; /* User ID of owner */ gid_t st_gid; /* Group ID of owner */ dev_t st_rdev; /* Device ID (if special file) */ off_t st_size; /* Total size, in bytes */ blksize_t st_blksize; /* Block size for filesystem I/O */ blkcnt_t st_blocks; /* Number of 512B blocks allocated */ struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ };
案例
-
获取文件大小:st_size
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> int main(int argc, char* argv[]) { struct stat sbuf; int ret = stat(argv[1], &sbuf); if (ret == -1) { perror("stat error"); exit(1); } printf("file size: %ld\n", sbuf.st_size); return 0; }
-
文件权限位
2.2 文件类型
- UNIX 系统的大多数文件是普通文件或目录,但是也有另外一些文件类型。文件类型包括如下几种
- 普通文件(regular file)
- 包含了某种形式的数据。至于这种数据是文本还是二进制数据,对于 UNIX 内核而言并无区别
- 目录文件(directory file)
- 包含了其他文件的名字以及指向与这些文件有关信息的指针
- 对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件
- 块特殊文件(block special file)
- 提供对设备(如磁盘)带缓冲的访问,每次访问长度固定
- 字符特殊文件(character special file)
- 提供对设备不带缓冲的访问,每次访问长度可变
- 系统中的所有设备要么是字符特殊文件,要么是块特殊文件
- 管道 FIFO
- 用于进程间通信,有时也称为命名管道
- 套接字(socket)
- 用于进程间的网络通信,也可用于在一台宿主机上进程之间的非网络通信
- 符号链接(symbolic link)
- 这种类型的文件指向另一个文件
- 普通文件(regular file)
案例
- 获取文件类型/权限:st_mode
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> int main(int argc, char* argv[]) { struct stat sbuf; // stat 会穿透符号链接,导致无法判断符号链接 // int ret = stat(argv[1], &sbuf); int ret = lstat(argv[1], &sbuf); if (ret == -1) { perror("stat error"); exit(1); } if (S_ISREG(sbuf.st_mode)) { printf("It's a regular\n"); } else if (S_ISDIR(sbuf.st_mode)) { printf("It's a dir\n"); } else if (S_ISFIFO(sbuf.st_mode)) { printf("It's a pipe\n"); } else if (S_ISLNK(sbuf.st_mode)) { printf("It's a sym link\n"); } return 0; }
ls -l 命令不会穿透符号链接;cat 和 vim 命令则会穿透符号链接
2.3 设置用户 ID 和设置组 ID
- 与一个进程相关联的 ID 有 6 个或更多
- 实际用户 ID 和实际组 ID 标识我们是谁。这两个字段在登录时取自口令文件中的登录项。通常,在一个登录会话期间这些值并不改变,但是超级用户进程有方法改变它们
- 有效用户 ID、有效组 ID 以及附属组 ID 决定文件访问权限
- 保存的设置用户 ID 和保存的设置组 ID 在执行一个程序时包含了有效用户 ID 和有效组 ID 的副本
- 通常,有效用户 ID 等于实际用户 ID,有效组 ID 等于实际组 ID
- 每个文件有一个所有者和组所有者
- 所有者由 stat 结构中的 st_uid 指定
- 组所有者则由 st_gid 指定
2.4 文件访问权限
-
st_mode 值也包含了对文件的访问权限位。所有文件类型 (目录、字符特别文件等) 都有访问权限
-
每个文件有 9 个访问权限位
- 用 u 表示用户 (所有者),用 g 表示组,用 o 表示其他
- 文件访问权限规则
- 用名字打开任一类型的文件时,对该名字中包含的每一个目录(包括隐含的当前工作目录)都应具有执行权限
- 例如,为了打开文件 /usr/include/stdio.h,需要对目录 /、/usr 和 /usr/include 都具有执行权限
- 对于一个文件的读权限决定了是否能够打开现有文件进行读操作
- 这与 open 函数的 O_RDONLY 和 O_RDWR 标志相关
- 对于一个文件的写权限决定了是否能够打开现有文件进行写操作
- 这与 open 函数的 O_WRONLY 和 O_RDWR 标志相关
- 为了在 open 函数中对一个文件指定 O_TRUNC 标志,必须对该文件具有写权限
- 为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限
- 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限
- 对该文件本身则不需要有读、写权限
- 用名字打开任一类型的文件时,对该名字中包含的每一个目录(包括隐含的当前工作目录)都应具有执行权限
进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试,而这种测试可能涉及文件的所有者 (st_uid 和 st_gid)、进程的有效 ID(有效用户 ID 和有效组 ID) 以及进程的附属组 ID(若支持的话)。两个所有者 ID 是文件的性质,而两个有效 ID 和附属组 ID 则是进程的性质
2.5 新文件和目录的所有权
- 新文件的用户 ID 设置为进程的有效用户 ID。关于组 ID,POSIX.1 允许实现选择下列之一作为新文件的组 ID
- 新文件的组 ID 可以是进程的有效组 ID
- 新文件的组 ID 可以是它所在目录的组 ID
2.6 函数 access 和 faccessat
#include <fcntl.h>
#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
-
即使一个进程可能已经通过设置用户 ID 以超级用户权限运行,它仍可能想验证其实际用户能否访问一个给定的文件
-
函数返回值
-
若成功,返回 0
-
若出错,返回 -1
-
access 和 faccessat 函数是按实际用户 ID 和实际组 ID 进行访问权限测试的
-
-
如果测试文件是否已经存在,mode 就为 F_OK,否则 mode 是下图所列常量的按位或
-
faccessat 函数与 access 函数在下面两种情况下是相同的,否则,faccessat 计算相对于打开目录 (由 fd 参数指向) 的 pathname
- 一种是 pathname 参数为绝对路径
- 另一种是 fd 参数取值为 AT_FDCWD 而 pathname 参数为相对路径
-
flag 参数可以用于改变 faccessat 的行为
- 如果 flag 设置为 AT_EACCESS,访问检查用的是调用进程的有效用户 ID 和有效组 ID,而不是实际用户 ID 和实际组 ID
案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char* argv[]) {
if (argc != 2) {
perror("usage: access <pathname>");
exit(1);
}
if (access(argv[1], R_OK) < 0) {
perror("access error");
exit(1);
} else {
printf("read access OK\n");
}
if (open(argv[1], O_RDONLY) < 0) {
perror("open error");
exit(1);
} else {
printf("open for reading OK\n");
}
return 0;
}
$ gcc access.c -o access
$ ./access fcntl.c
read access OK
open for reading OK
2.7 函数 umask
#include <sys/types.h>
#include <sys/stat.h>
// mask 取值见 2.4 节图
mode_t umask(mode_t mask);
-
umask 函数为进程设置文件模式创建屏蔽字,并返回之前的值,这是少数几个没有出错返函数中的一个
-
函数返回值
- 之前的文件模式创建屏蔽字
-
在进程创建一个新文件或新目录时,就一定会使用文模式创建屏字
- open 和 creat 函数都有一个参数 mode,它指定了新文件的访问权限位
案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define RWRWRW (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)
int main(int argc, char* argv[]) {
umask(0);
if (creat("foo", RWRWRW) < 0) {
perror("creat error for foo");
}
umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (creat("bar", RWRWRW) < 0) {
perror("creat error for bar");
}
return 0;
}
$ umask # 先打印当前文件模式创建屏蔽字
0002
$ gcc umask.c -o umask
$ ./umask
$ ls -l foo bar
-rw------- 1 yxd yxd 0 9月 14 08:53 bar
-rw-rw-rw- 1 yxd yxd 0 9月 14 08:53 foo
$ umask
0002
$ umask -S # 观察文件模式创建屏蔽字是否更改
u=rwx,g=rwx,o=rx
$ umask 027 # 更改文件模式创建屏蔽字
$ umask -S
u=rwx,g=rx,o=
- 当编写创建新文件的程序时,如果想确保指定的访问权限位已经激活,那么必须在进程运行时修改 umask 值。例如,如果想确保任何用户都能读文件,则应将 umask 设置为 0。否则,当进程运行时,有效的 umask 值可能关闭该权限位
- 更改进程的文件模式创建屏蔽字并不影响其父进程 (常常是 shell) 的屏蔽字
- 用户可以设置 umask 值以控制所创建文件的默认权限。该值表示成八进制数,一位代表一种要屏蔽的权限,下图所示,设置了相应位后,它所对应的权限就会被拒绝
- 常用的几种 umask 值是 002、022 和 027
- 002 阻止其他用户写入你的文件
- 022 阻止同组成员和其他用户写入你的文件
- 027 阻止同组成员写你的文件以及其他用户读、写或执行你的文件
- 常用的几种 umask 值是 002、022 和 027
2.8 函数 chmod、fchmod 和 fchmodat(更改现有文件访问权限)
#include <fcntl.h>
#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);
-
函数返回值
- 若成功,返回 0
- 若出错,返回 -1
-
chmod 函数在指定的文件上进行操作,而 fchmod 函数则对已打开的文件进行操作
-
为了改变一个文件的权限位
- 进程的有效用户 ID 必须等于文件的所有者 ID
- 或者该进程必须具有超级用户权限
-
参数 mode 是下图中所示常量的按位或
案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define RWRWRW (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)
int main(int argc, char* argv[]) {
struct stat statbuf;
// 对于其当前状态设置权限:先调用 stat 获得其当前权限,然后修改它
// 显式地打开设置组 ID 位、关闭了组执行位
if (stat("foo", &statbuf) < 0) {
perror("stat error for foo");
exit(1);
}
if (chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0) {
perror("chmod error for foo");
exit(1);
}
// 不管文件 bar 的当前权限位如何,都将其权限设置为一个绝对值 rw-r--r--
if (chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0) {
perror("chmod error for bar");
}
return 0;
}
$ gcc chmod.c -o chmod
$ ./chmod
$ ls -l foo bar
-rw-r--r-- 1 yxd yxd 0 9月 14 08:53 bar
-rw-rwSrw- 1 yxd yxd 0 9月 14 08:53 foo
2.9 粘着位
- S_ISVTX 被称为粘着位
- 如果一个可执行程序文件的这一位被设置了,那么当该程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区 (程序的正文部分是机器指令),这使得下次执行该程序时能较快地将其装载入内存
- 目录 /tmp 和 /var/tmp 是设置粘着位的典型候选者:任何用户都可在这两个目录中创建文件。任一用户 (用户、组和其他) 对这两个目录的权限通常都是读、写和执行。但是用户不应能删除或重命名属于其他人的文件,为此在这两个目录的文件模式中都设置了粘着位
2.10 函数 chown、fchown、fchownat 和 lchown
-
下面几个 chown 函数可用于更改文件的用户 ID 和组 ID
#include <fcntl.h> #include <unistd.h> // 如果两个参数 owner 或 group 中的任意一个是 -1,则对应的 ID 不变 int chown(const char *pathname, uid_t owner, gid_t group); int fchown(int fd, uid_t owner, gid_t group); int lchown(const char *pathname, uid_t owner, gid_t group); int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);
-
函数返回值
- 若成功,返回 0
- 若出错,返回 -1
-
除了所引用的文件是符号链接以外,这 4 个函数的操作类似
- 在符号链接情况下,lchown 和 fchownat (设置了 AT_SYMLINK NOFOLLOW 标志) 更改符号链接本身的所有者,而不是该符号链接所指向的文件的所有者
- fchown 函数改变 fd 参数指向的打开文件的所有者,既然它在一个已打开的文件上操作,就不能用于改变符号链接的所有者
2.11 文件长度
-
stat 结构成员 st_size 表示以字节为单位的文件的长度
- 此字段只对普通文件、目录文件和符号链接有意义
-
对于普通文件,其文件长度可以是 0,在开始读这种文件时,将得到文件结束指示
-
对于目录,文件长度通常是一个数 (如 16 或 512) 的整倍数
-
对于符号链接,文件长度是在文件名中的实际字节数
- 因为符号链接文件长度总是由 st_size 指示,所以它并不包含通常 C 语言用作名字结尾的 null 字节
-
文件空洞
- 空洞是由所设置的偏移量超过文件尾端,并写入了某些数据后造成的
2.12 文件截断
- 有时需要在文件尾端处截去一些数据以缩短文件
- 将一个文件的长度截断为 0 是一个特例,在打开文件时使用 O_TRUNC 标志可以做到这一点
- 为了截断文件可以调用函数 truncate 和 ftruncate
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
-
返回值
- 若成功,返回 0
- 若出错,返回 -1
-
这两个函数将一个现有文件长度截断为 length
- 如果该文件以前的长度大于 length,则超过 length 以外的数据就不能访问
- 如果以前的长度小于 length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作 0 (也就是可能在文件中创建了一个空洞)
2.13 文件系统
- 可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统,i 节点是固定长度的记录项,它包含有关文件的大部分信息
- 一个柱面组的 i 节点和数据块部分
- 每个 i 节点中都有一个链接计数,其值是指向该 i 节点的目录项数。只有当链接计数减少至 0 时,才可删除该文件 (也就是可以释放该文件占用的数据块)
- 这就是为什么 “解除对一个文件的链接” 操作并不总是意味着 “释放该文件占用的磁盘块” 的原因
- 这也是为什么删除一个目录项的函数被称之为 unlink 而不是 delete 的原因
- 另外一种链接类型称为符号链接。符号链接文件的实际内容 (在数据块中) 包含了该符号链接所指向的文件的名字
- i 节点包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度和指向文件数据块的指针等。stat 结构中的大多数信息都取自 i 节点。只有两项重要数据存放在目录项中:文件名和 i 节点编号
- 因为目录项中的 i 节点编号指向同一文件系统中的相应 i 节点,一个目录项不能指向另一个文件系统的 i 节点
- 当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有 i 节点的新目录项,并删除老的目录项。链接计数不会改变
- 例:将文件 /usr/lib/foo 重命名 /usr/foo,若 /usr/lib 和 /usr 在同一文件系统,则文件 foo 内容无需移动
- 每个 i 节点中都有一个链接计数,其值是指向该 i 节点的目录项数。只有当链接计数减少至 0 时,才可删除该文件 (也就是可以释放该文件占用的数据块)
2.14 函数 link、linkat、unlink、unlinkat 和 remove
为什么目录项要游离于 inode 之外并将文件名单独存储呢?这样的存储方式有什么样的好处?
- 其目的是为了实现文件共享。Linux 允许多个目录项共享一个 inode,即共享盘块(data)
- 不同文件名,在人类眼中将它理解成两个文件,但是在内核眼里是同一个文件
2.14.1 函数 link、linkat
- 创建一个指向现有文件的链接的方法是使用 link 函数或 linkat 函数
#include <fcntl.h> #include <unistd.h> int link(const char *oldpath, const char *newpath); int linkat(int oldfd, const char *oldpath, int newfd, const char *newpath, int flag);
- 这两个函数创建一个新目录项 newpath,它引用现有文件 oldpath
- 如果 newpath 已经存则返回出错。只创建 newpath 中的最后一个分量,路径中的其他部分应当已经存在
- 返回值
- 若成功,返回 0
- 若出错,返回 -1
- 当现有文件是符号链接时,由 flag 参数来控制 linkat 函数是创建指向现有符号链接的链接还是创建指向现有符号链接所指向的文件的链接
案例
- 实现 mv 命令
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> int main(int argc, char* argv[]) { link (argv[1], argv[2]); unlink(argv[1]); return 0; }
2.14.2 函数 unlink 和 unlinkat
- 为了删除一个现有的目录项,可以调用 unlink 函数
#include <fcntl.h>
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
- 这两个函数删除目录项,并将由 pathname 所引用文件的链接计数减 1
- 如对该文件还有其他链接,则仍可通过其他链接访问该文件的数据
- 如果出错,则不对该文件做任何更改
- 返回值
- 若成功,返回 0
- 若出错,返回 -1
- flag 参数给出了一种方法使调用进程可以改变 unlinkat 函数的默认行为
- 当 AT_REMOVEDIR 标志被设置时,unlinkat 函数可以类似于 rmdir 一样删除目录
- 如果这个标志被清除,unlinkat 与 unlink 执行同样的操作
只有当链接计数达到 0 时,该文件的内容才可被删除。另一个条件也会阻止删除文件的内容:只要有进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程个数,如果这个计数达到 0,内核再去检查其链接计数,如果计数也是 0,那么就删除该文件的内容
- unlink 的这种特性经常被程序用来确保即使是在程序溃时,它所创建的临时文件也不会遗留下来
- 进程用 open 或 creat 创建一个文件,然后立即调用 unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时(在这种情况下,内核关闭该进程所打开的全部文件),该文件的内容才被删除
- 删除文件,从某种意义上说,只是让文件具备了被释放的条件
- unlink 函数的特征:清除文件时,如果文件的硬链接数到 0 了,没有 dentry 对应,但该文件仍不会马上被释放。要等到所有打开该文件的进程关闭该文件,系统才会挑时间将该文件释放掉
隐式回收:当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统的这一特性称之为隐式回收系统资源
案例
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char* argv[]) {
int fd, ret;
char* p = "test of unlink\n";
char* p2 = "after write something.\n";
fd = open("lseek.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open temp error");
exit(1);
}
ret = unlink("lseek.txt");
if (ret < 0) {
perror("unlink error");
exit(1);
}
// 此处的 write 实际是把内容写到了缓冲区而非磁盘区
ret = write(fd, p, strlen(p));
if (ret == -1) {
perror("-----write error");
}
printf("hi! I'm printf\n");
ret = write(fd, p2, strlen(p2));
if (ret == -1) {
perror("-----write error");
}
printf("Enter anykey continue\n");
getchar();
p[3] = 'H';
close(fd);
return 0;
}
2.14.3 函数 remove
#include <stdio.h>
int remove(const char* pathname);
- 可以用 remove 函数解除对一个文件或目录的链接
- 对于文件,remove 的功能与 unlink 相同
- 对于目录,remove 的功能与 rmdir 相同
2.15 函数 rename 和 renameat
- 文件或目录可以用 rename 函数或者 renameat 函数进行重命名
#include <fcntl.h>
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
int renameat(int oldfd, const char *oldpath, int newfd, const char *newpath);
-
如果 oldname 指的是一个文件,那么为该文件或符号链接重命名
- 如果 newname 已存在,则它不能引用一个目录
- 如果 newname 已存在,而且不是一个目录,则先将该目录项删除然后将 oldname 重命名为 newname
- 对包含 oldname 的目录以及包含 newname 的目录,调用进程必须具有写权限,因为将更改这两个目录
-
如果 oldname 指的是一个目录,那么为该目录重命名
- 如果 newname 已存在,则它必须引用一个目录,而且该目录应当是空目录 (空目录指的是该目录中只有 . 和 … 项)
- 如果 newname 存在 (而且是一个空目录),则先将其删除,然后将 oldname 重命名为 newname
- 当为一个目录重命名时,newname 不能包含 oldname 作为其路径前缀
- 例:不能将 /usr/foo 重命名为 /usr/foo/testdir,因为旧名字 (/usr/foo) 是新名字的路径前缀,因而不能将其删除
-
不能对 . 和 … 重命名。更确切地说 . 和 … 都不能出现在 oldname 和 newname 的最后部分
-
作为一个特例,如果 oldname 和 newname 引用同一文件,则函数不做任何更改而成功返回
2.16 符号链接
-
符号链接是对一个文件的间接指针,它与上一节所述的硬链接有所不同,硬链接直接指向文件的 i 节点
-
引入符号链接的原因是为了避开硬链接的一些限制
- 硬链接通常要求链接和文件位于同一文件系统中
- 只有超级用户才能创建指向目录的硬链接 (在底层文件系统支持的情况下)
-
对符号链接以及它指向何种对象并无任何文件系统限制,任何用户都可以创建指向目录的符号链接,符号链接一般用于将一个文件或整个目录结构移到系统中另一个位置
案例
- 使用符号链接可能在文件系统中引入循环。大多数查找路径名的函数在这种情况发生时都将出错返回,errno 值为 ELOOP。考虑下列命令序列:
$ mkdir foo # 创建一个新目录 $ touch foo/a # 创建一个 0 长度的文件 $ ln -s ../foo foo/testdir # 创建一个符号链接 $ ls -l foo total 0 -rw-rw-r-- 1 yxd yxd 0 9月 14 15:28 a lrwxrwxrwx 1 yxd yxd 6 9月 14 15:28 testdir -> ../foo
- 以上命令创建了一个目录 foo,它包含了一个名为 a 的文件以及一个指向 foo 的符号链接
- 构成循环的符号链接 testdir
- 这样一个循环是很容易消除的
- 因为 unlink 并不跟随符号链接,所以可以 unlink 文件 foo/testdir
- 但是如果创建了一个构成这种循环的硬链接,那么就很难消除它
- 这就是为什么 link 函数不允许构造指向目录的硬链接的原因 (除非进程具有超级用户权限)
- 用 open 打开文件时,如果传递给 open 函数的路径名指定了一个符号链接,那么 open 跟随此链接到达所指定的文件。此符号链接所指向的文件并不存在,则 open 返回出错,表示它不能打开该文件
$ ln -s /no/such/file myfile # 创建一个符号链接 $ ls myfile myfile $ cat myfile # 试图查看文件 cat: myfile: No such file or directory $ ls -l myfile lrwxrwxrwx 1 yxd yxd 13 9月 14 15:37 myfile -> /no/such/file
- 文件 myfile 存在,但 cat 却称没有这一文件,其原因是 myfile 是个符号链接,由该符号链接所指向的文件并不存在
- ls 命令的 -l 选项有两个提示
- 第一个字符是 l,它表示这是一个符号链接,而->也表明这是一个符号链接
- ls 命令还有另一个选项 -F
- 它会在符号链接的文件名后加一个 @ 符号,在未使用 -l 选项时,这可以帮助识别出符号链接
2.17 创建和读取符号链接
- 可以用 symlink 或 symlinkat 函数创建一个符号链接
#include <fcntl.h> #include <unistd.h> int symlink(const char *target, const char *linkpath); int symlinkat(const char *target, int newdirfd, const char *linkpath);
- 返回值
- 若成功,返回 0
- 若出错,返回 -1
- 因为 open 函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字,readlink 和 readlinkat 函数提供了这种功能
#include <fcntl.h> #include <unistd.h> ssize_t readlink(const char *pathname, char *buf, size_t bufsiz); ssize_t readlinkat(int fd, const char *pathname, char *buf, size_t bufsiz);
2.18 函数 mkdir、mkdirat 和 rmdir
2.18.1 函数 mkdir、mkdirat(创建目录)
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
- 返回值
- 若成功,返回 0
- 若出错,返回 -1
- 这两个函数创建一个新的空目录
- 其中 . 和 … 目录项是自动创建的。所指定的文件访问权限 mode 由进程的文件模式创建屏蔽字修改
- 常见的错误是指定与文件相同的 mode (只指定读、写权限)。但是,对于目录通常至少要设置一个执行权限位,以允许访问该目录中的文件名
2.18.2 函数 rmdir(删除目录)
#include <unistd.h>
int rmdir(const char *pathname);
-
返回值
- 若成功,返回 0
- 若出错,返回 -1
-
用 rmdir 函数可以删除一个空目录
- 空目录是只包含 .和 … 这两项的目录
-
如果调用此函数使目录的链接计数成为 0,并且也没有其他进程打开此目录,则释放由此目录占用的空间
-
如果在链接计数达到 0 时,有一个或多个进程打开此目录,则在此函数返回前删除最后一个链接及 . 和 … 项
-
另外,在此目录中不能再创建新文件。但是在最后一个进程关闭它之前并不释放此目录
2.18.3 文件、目录权限
- 目录文件也是 “文件”,其文件内容是该目录下所有子文件的目录项 dentry。可以尝试用 vim 打开一个目录
2.19 读目录
- 对某个目录具有访问权限的任一用户都可以读该目录,但为了防止文件系统产生混乱,只有内核才能写目录
#include <sys/types.h> #include <dirent.h> // 1、打开目录 // 若成功,返回目录结构体指针;若出错,返回 NULL // DIR* 类似于 FILE* DIR *opendir(const char *name); DIR *fdopendir(int fd); // 2、读目录 // 若成功,返回目录项结构体指针;若在目录尾或出错,返回 NULL,设置 errno 为相应值 struct dirent *readdir(DIR *dirp); // 3、关闭目录 // 若成功,返回 0;若出错,返回 -1,设置 errno 为相应值 int closedir(DIR *dirp);
- 定义在头文件 <dirent.h> 中的 dirent 结构与实现有关。实现对此结构所做的定义至少包含下列两个成员
ino_t d_ino; // inode 编号 char d_name[256] // 文件名
案例
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <dirent.h>
int main(int argc, char* argv[]) {
DIR* dp;
struct dirent* sdp;
dp = opendir(argv[1]);
if (dp == NULL) {
perror("opendir error");
exit(1);
}
while ((sdp = readdir(dp)) != NULL) {
if ((strcmp(sdp->d_name, ".") == 0)) {
continue;
}
printf("%s\t", sdp->d_name);
}
printf("\n");
closedir(dp);
return 0;
}
$ gcc myls.c -o myls
$ ./myls .. # 与 ls .. 命令等价
2.20 函数 chdir、fchdir 和 getcwd
2.20.1 函数 chdir、fchdir
-
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点 (不以斜线开始的路径名为相对路径名)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性
-
进程调用 chdir 或 fchdir 函数可以更改当前工作目录
#include <unistd.h> int chdir(const char *path); int fchdir(int fd);
-
返回值
- 若成功,返回 0
- 若出错,返回 -1
-
因为当前工作目录是进程的一个属性,所以它只影响调用 chdir 的进程本身,而不影响其他进程
2.20.2 函数 getcwd
-
函数 getcwd 功能
- 从当前工作目录 (.) 开始,用 … 找到其上一级目录,然后读其目录项,直到该目录项中的 i 节点编号与工作目录 i 节点编号相同,这样就找到了其对应的文件名
- 按照这种方法,逐层上移,直到遇到根,这样就得到了当前工作目录完整的绝对路径名
#include <unistd.h> char* getcwd(char* buf, size_t size);
-
返回值
- 若成功,返回 buf
- 若出错,返回 NULL
-
必须向此函数传递两个参数,一个是缓冲区地址 buf,另一个是缓冲区的长度 size (以字节为单位)。该缓冲区必须有足够的长度以容纳绝对路径名再加上一个终止 null 字节,否则返回出错
-
当一个应用程序需要在文件系统中返回到它工作的出发点时,getcwd 函数是有用的
- 在更换工作目录之前,可以调用 getcwd 函数先将其保存起来。在完成了处理后,就可将所保存的原工作目录路径名作为调用参数传送给 chdir,这样就返回到了文件系统中的出发点
2.21 设备特殊文件
- st_dev 和 st_rdev 这两个字段经常引起混淆
- 每个文件系统所在的存储设备都由其主、次设备号表示
- 设备号所用的数据类型是基本系统数据类型 dev_t
- 主设备号标识设备驱动程序
- 次设备号标识特定的子设备
- 通常可以使用两个宏:major 和 minor 来访问主、次设备号
- 系统中与每个文件名关联的 st_dev 值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的 i 节点
- 只有字符特殊文件和块特殊文件才有 st_rdev 值,此值包含实际设备的设备号
- 每个文件系统所在的存储设备都由其主、次设备号表示
递归遍历目录案例
1. 思路分析
2. 代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <dirent.h>
void isFile(char* name);
// 打开目录读取,处理目录
void read_dir(char* dir, void (*func)(char*)) {
char path[256];
DIR* dp;
struct dirent *sdp;
dp = opendir(dir);
if (dp == NULL) {
perror("opendir error");
return;
}
// 读取目录项
while ((sdp = readdir(dp)) != NULL) {
if (strcmp(sdp->d_name, ".") == 0 || strcmp(sdp->d_name, "..") == 0) {
continue;
}
// 目录项本身不可访问, 拼接 目录/目录项
sprintf(path, "%s/%s", dir, sdp->d_name);
// 判断文件类型,目录递归进入,文件显示名字/大小
(*func)(path);
}
closedir(dp);
return;
}
void isFile(char* name) {
int ret = 0;
struct stat sub;
// 获取文件属性, 判断文件类型
ret = stat(name, &sub);
if (ret == -1) {
perror("stat error");
return;
}
// 是目录文件
if (S_ISDIR(sub.st_mode)) {
read_dir(name, isFile);
}
// 是普通文件, 直接打印名字/大小
printf("%10s\t\t%ld\n", name, sub.st_size);
return;
}
int main(int argc, char* argv[]) {
// 命令行参数个数 argc = ---→ ./ls-R
// 命令行参数列表 argv[1]---→ ./ls-R /home/test
if (argc == 1) { // 判断命令行参数
isFile(".");
} else {
isFile(argv[1]);
}
return 0;
}
$ gcc ls-R.c -o ls-R
$ ./ls-R
./fcntl 8384
./mycat.c 262
./ls-R.c 943
./fcntl2 8432
./ls-R 8768
. 4096