第4章:文件和目录(Files and Directories)
一、导读
1.1 本章学习目标
第4章深入讲解UNIX文件系统的核心概念。文件系统是操作系统管理持久存储的核心机制,理解其工作原理对于编写文件系统相关的程序(如备份工具、文件管理器、搜索工具)至关重要。本章将揭示文件名与inode的关系、目录的组织方式、链接的本质、文件权限的控制机制。
本章的核心学习目标包括:
- 理解inode的结构:inode存储哪些元数据?文件名和inode是什么关系?
- 掌握目录的组织方式:目录是特殊的文件,其内容是文件名到inode号的映射
- 区分硬链接和符号链接:两者的本质区别是什么?各自适用于什么场景?
- 掌握文件权限机制:rwx权限、set-uid/set-gid/sticky bit的含义和作用
- 学会使用stat系列函数:
stat、fstat、lstat获取文件属性信息 - 理解文件时间戳:atime、mtime、ctime分别记录什么?何时更新?
1.2 为什么本章如此重要
文件系统是UNIX系统的核心组件。几乎所有的系统编程任务都与文件操作有关:配置文件读写、日志记录、数据处理、程序分发等。理解文件系统的底层机制,可以帮助你:
- 编写更高效的文件操作代码
- 正确设置文件权限,保证安全性
- 理解链接的行为,避免误删数据
- 编写跨文件系统的工具(如
mv、cp) - 实现守护进程、日志轮转等系统管理功能
此外,文件系统的设计思想(如inode、目录树、权限模型)深刻影响了后世的操作系统设计。理解这些概念,是成为系统级程序员的必经之路。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
文件名和inode是什么关系?为什么可以有多个文件名指向同一个inode?
硬链接和符号链接在底层有什么区别?删除一个有硬链接的文件会发生什么?
为什么ls -l显示的文件大小和du显示的磁盘占用可能不同?
set-uid位有什么作用?为什么/usr/bin/passwd需要set-uid权限?
目录的execute权限(x)有什么含义?没有x权限的目录能否列出文件?
为什么mv在同一文件系统上是原子操作,跨文件系统时不是?
二、核心概念详解
2.1 inode
inode(索引节点)是UNIX文件系统的核心数据结构。每个文件(包括目录、设备、管道等)都对应一个inode。inode存储了文件的所有元数据,但不包含文件名。
2.1.1 inode的内容
// inode存储的信息(简化)
struct inode {
dev_t st_dev; // 文件所在设备的设备号
ino_t st_ino; // inode号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 所有者UID
gid_t st_gid; // 所属组GID
dev_t st_rdev; // 设备文件的设备号
off_t st_size; // 文件大小(字节)
blksize_t st_blksize; // 文件系统块大小
blkcnt_t st_blocks; // 分配的块数(512字节为单位)
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态改变时间
};三个时间戳的含义:
| 时间戳 | 含义 | 更新时机 |
|---|---|---|
| st_atime | 最后访问时间 | read操作 |
| st_mtime | 最后修改时间 | write操作 |
| st_ctime | 最后状态改变时间 | 修改inode信息(权限、所有者、链接数等) |
注意:现代Linux系统默认使用relatime挂载选项,只在atime早于mtime/ctime时才更新atime,以减少磁盘I/O。
2.1.2 文件名与inode的关系
文件名存储在目录中,目录是一种特殊的文件,其内容是"文件名→inode号"的映射表。
目录 /home/user/
┌──────────────────────────────┐
│ "." → inode 100 (自身) │
│ ".." → inode 50 (父目录) │
│ "file.txt" → inode 200 │
│ "dir1" → inode 300 │
└──────────────────────────────┘
inode 200 (file.txt)
┌──────────────────────────────┐
│ 权限: rw-r--r-- │
│ 所有者: uid=1000, gid=1000 │
│ 大小: 1024 bytes │
│ 链接数: 1 │
│ 数据块: [block 5000, ...] │
└──────────────────────────────┘当访问一个文件时,内核的工作流程是:
从根目录开始,逐级查找路径中的每个目录
在每个目录中查找对应的文件名,获取inode号
读取inode,获取文件的权限、大小、数据块位置等信息
通过数据块位置读取实际数据
2.2 stat系列函数
#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);stat:返回命名文件的信息fstat:返回已打开文件的信息(通过文件描述符)lstat:对于符号链接,返回链接本身的信息,而不是链接指向的文件
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <file>\n", argv[0]);
return 1;
}
struct stat st;
if (stat(argv[1], &st) == -1) {
perror("stat");
return 1;
}
printf("File: %s\n", argv[1]);
printf("Size: %ld bytes\n", st.st_size);
printf("Blocks: %ld (disk usage: %ld bytes)\n",
st.st_blocks, st.st_blocks * 512);
printf("Inode: %ld\n", st.st_ino);
printf("Links: %ld\n", st.st_nlink);
printf("UID: %d, GID: %d\n", st.st_uid, st.st_gid);
printf("Modify: %s", ctime(&st.st_mtime));
return 0;
}2.3 文件类型
st_mode字段包含了文件类型和权限信息。文件类型可以通过以下宏判断:
| 宏 | 文件类型 |
|---|---|
| S_ISREG() | 普通文件 |
| S_ISDIR() | 目录 |
| S_ISCHR() | 字符设备 |
| S_ISBLK() | 块设备 |
| S_ISFIFO() | 管道/FIFO |
| S_ISLNK() | 符号链接 |
| S_ISSOCK() | 套接字 |
#include <stdio.h>
#include <sys/stat.h>
void print_file_type(const char *path) {
struct stat st;
stat(path, &st);
if (S_ISREG(st.st_mode)) printf("Regular file\n");
else if (S_ISDIR(st.st_mode)) printf("Directory\n");
else if (S_ISCHR(st.st_mode)) printf("Character device\n");
else if (S_ISBLK(st.st_mode)) printf("Block device\n");
else if (S_ISFIFO(st.st_mode)) printf("FIFO/Pipe\n");
else if (S_ISLNK(st.st_mode)) printf("Symbolic link\n");
else if (S_ISSOCK(st.st_mode)) printf("Socket\n");
}2.4 目录操作
#include <dirent.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
// 遍历目录
void list_dir(const char *path) {
DIR *dir = opendir(path);
if (dir == NULL) {
perror("opendir");
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
char fullpath[1024];
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);
struct stat st;
lstat(fullpath, &st);
printf("%s", entry->d_name);
if (S_ISDIR(st.st_mode)) printf("/");
else if (S_ISLNK(st.st_mode)) printf("@");
printf("\n");
}
closedir(dir);
}
// 递归遍历目录树
void walk_dir(const char *path, int depth) {
DIR *dir = opendir(path);
if (dir == NULL) return;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
char fullpath[1024];
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);
struct stat st;
lstat(fullpath, &st);
printf("%*s%s%s\n", depth * 2, "", entry->d_name,
S_ISDIR(st.st_mode) ? "/" : "");
if (S_ISDIR(st.st_mode)) {
walk_dir(fullpath, depth + 1);
}
}
closedir(dir);
}注意:readdir返回的dirent结构中的d_name字段只包含文件名(如file.txt),不包含路径。要获取完整路径,需要拼接目录路径和文件名。
2.5 链接
UNIX支持两种链接:硬链接和符号链接。
2.5.1 硬链接
硬链接是多个文件名指向同一个inode。创建硬链接时,inode的链接计数(st_nlink)加1。
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
int unlink(const char *pathname); // 删除文件名// 创建硬链接
link("original.txt", "hardlink.txt");
// 现在original.txt和hardlink.txt指向同一个inode
// 删除文件名
unlink("original.txt");
// 只删除了original.txt这个文件名,inode和链接计数减1
// 文件数据仍然存在,因为hardlink.txt仍然指向该inode硬链接的限制:
- 不能跨文件系统(因为inode号只在同一文件系统中有意义)
- 不能链接目录(防止目录树中出现环)
- 只有普通文件可以创建硬链接
2.5.2 符号链接
符号链接是一个独立的文件,包含指向目标文件的路径。符号链接有自己的inode,与目标文件的inode不同。
#include <unistd.h>
int symlink(const char *target, const char *linkpath);
int readlink(const char *pathname, char *buf, size_t bufsiz);// 创建符号链接
symlink("/usr/bin/python3", "python");
// python是一个独立的文件,内容是"/usr/bin/python3"
// 读取符号链接的内容
char buf[256];
ssize_t len = readlink("python", buf, sizeof(buf) - 1);
if (len != -1) {
buf[len] = '\0';
printf("python -> %s\n", buf);
}
// 删除符号链接
unlink("python");
// 只删除符号链接本身,不影响目标文件符号链接 vs 硬链接:
| 特性 | 硬链接 | 符号链接 |
|---|---|---|
| inode | 与目标相同 | 独立的inode |
| 跨文件系统 | 不支持 | 支持 |
| 链接目录 | 不支持 | 支持 |
| 目标删除后 | 仍然有效 | 变成悬空链接 |
| 文件大小 | 与目标相同 | 存储路径字符串的长度 |
2.6 文件权限
文件权限由st_mode的低12位表示:
特殊位 所有者 组 其他
┌───────┬──────┬──────┬──────┐
│ 4 2 1 │ r w x│ r w x│ r w x│
│ s g t │ 7 │ 7 │ 5 │
└───────┴──────┴──────┴──────┘2.6.1 基本权限
| 权限 | 对文件的含义 | 对目录的含义 |
|---|---|---|
| r(读) | 读取文件内容 | 列出目录内容(ls) |
| w(写) | 修改文件内容 | 在目录中创建/删除文件 |
| x(执行) | 执行文件 | 进入目录(cd) |
目录权限的特殊性:
# 目录权限为rwx--x--x (711)
# 其他用户可以进入目录,但不能列出内容
cd /some/dir # 可以,有x权限
ls /some/dir # 失败,没有r权限
cat /some/dir/file.txt # 可以,如果知道文件名且有x权限2.6.2 特殊权限位
set-uid(s位,所有者执行位):当设置在一个可执行文件上时,进程运行时拥有文件所有者的权限,而不是运行者的权限。
# passwd命令需要修改/etc/shadow文件(只有root可写)
ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root ... /usr/bin/passwd
# s位使得普通用户运行passwd时,进程拥有root权限set-gid(s位,组执行位):进程运行时拥有文件所属组的权限。对目录设置set-gid时,在该目录下创建的文件继承目录的组。
sticky bit(t位,其他执行位):对目录设置sticky bit时,只有文件所有者、目录所有者或root可以删除/重命名该目录中的文件。/tmp目录通常设置了sticky bit。
ls -ld /tmp
# drwxrwxrwt 1 root root ... /tmp
# t位使得用户只能删除自己的文件2.6.3 umask
umask设置创建文件时的权限掩码。新文件的权限为mode & ~umask。
#include <sys/stat.h>
#include <unistd.h>
// 查看当前umask
mode_t old_mask = umask(0);
umask(old_mask); // 恢复
printf("Current umask: %o\n", old_mask);
// 设置umask
umask(022); // 新文件默认权限:666 & ~022 = 644 (rw-r--r--)
umask(077); // 新文件默认权限:666 & ~077 = 600 (rw-------)2.7 文件时间戳与utimes
#include <sys/time.h>
int utimes(const char *pathname, const struct timeval times[2]);utimes修改文件的访问时间和修改时间:
struct timeval times[2];
times[0].tv_sec = 1000000; // atime
times[0].tv_usec = 0;
times[1].tv_sec = 1000000; // mtime
times[1].tv_usec = 0;
utimes("file.txt", times);三、实战案例
3.1 类似ls -l的程序
#include <stdio.h>
#include <sys/stat.h>
#include <dirent.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <string.h>
void print_perms(mode_t mode) {
const char *perms[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"};
if (S_ISDIR(mode)) printf("d");
else if (S_ISLNK(mode)) printf("l");
else printf("-");
printf("%s", perms[(mode >> 6) & 7]);
printf("%s", perms[(mode >> 3) & 7]);
printf("%s", perms[mode & 7]);
}
void list_file(const char *path) {
struct stat st;
if (lstat(path, &st) == -1) {
perror("lstat");
return;
}
print_perms(st.st_mode);
printf(" %ld", st.st_nlink);
struct passwd *pw = getpwuid(st.st_uid);
printf(" %-8s", pw ? pw->pw_name : "unknown");
struct group *gr = getgrgid(st.st_gid);
printf(" %-8s", gr ? gr->gr_name : "unknown");
printf(" %8ld", st.st_size);
char timebuf[64];
strftime(timebuf, sizeof(timebuf), "%b %d %H:%M", localtime(&st.st_mtime));
printf(" %s", timebuf);
printf(" %s", path);
if (S_ISLNK(st.st_mode)) {
char link[256];
ssize_t len = readlink(path, link, sizeof(link) - 1);
if (len != -1) {
link[len] = '\0';
printf(" -> %s", link);
}
}
printf("\n");
}3.2 递归删除目录(类似rm -rf)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
int remove_recursive(const char *path) {
struct stat st;
if (lstat(path, &st) == -1) {
perror("lstat");
return -1;
}
if (S_ISDIR(st.st_mode)) {
DIR *dir = opendir(path);
if (dir == NULL) {
perror("opendir");
return -1;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
char child[1024];
snprintf(child, sizeof(child), "%s/%s", path, entry->d_name);
remove_recursive(child);
}
closedir(dir);
if (rmdir(path) == -1) {
perror("rmdir");
return -1;
}
} else {
if (unlink(path) == -1) {
perror("unlink");
return -1;
}
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <path>\n", argv[0]);
exit(1);
}
return remove_recursive(argv[1]);
}3.3 查找硬链接
#include <stdio.h>
#include <sys/stat.h>
#include <dirent.h>
#include <string.h>
void find_hardlinks(const char *dir, ino_t target_ino) {
DIR *d = opendir(dir);
if (d == NULL) return;
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
char path[1024];
snprintf(path, sizeof(path), "%s/%s", dir, entry->d_name);
struct stat st;
lstat(path, &st);
if (st.st_ino == target_ino && !S_ISDIR(st.st_mode)) {
printf("Found hardlink: %s\n", path);
}
if (S_ISDIR(st.st_mode)) {
find_hardlinks(path, target_ino);
}
}
closedir(d);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <file> <search_dir>\n", argv[0]);
return 1;
}
struct stat st;
stat(argv[1], &st);
printf("Looking for hardlinks to inode %ld...\n", st.st_ino);
find_hardlinks(argv[2], st.st_ino);
return 0;
}四、本章小结
第4章深入讲解了UNIX文件系统的核心概念。核心知识点包括:
inode:存储文件元数据(权限、所有者、大小、时间戳、数据块指针),不包含文件名。文件名存储在目录中。
目录:特殊的文件,内容是"文件名→inode号"的映射表。每个目录包含.(自身)和..(父目录)两个特殊条目。
硬链接:多个文件名指向同一个inode。链接计数为0时文件才被删除。不能跨文件系统,不能链接目录。
符号链接:独立的文件,包含目标路径。可以跨文件系统,可以链接目录。目标删除后变成悬空链接。
文件权限:rwx权限、set-uid/set-gid/sticky bit。目录权限与文件权限的含义不同(r=列出、w=创建删除、x=进入)。
stat系列函数:stat、fstat、lstat获取文件属性。lstat对符号链接返回链接本身的信息。
五、思考与练习
思考题
为什么UNIX不允许创建目录的硬链接?如果允许,会导致什么问题?
一个文件的链接计数为3,这意味着什么?删除其中一个文件名后,文件数据是否立即被释放?
为什么/tmp目录需要设置sticky bit?如果不设置,会有什么安全隐患?
为什么mv在同一文件系统上是原子操作?跨文件系统时为什么不是?
编程练习
编写一个程序,统计指定目录下各类文件(普通文件、目录、符号链接等)的数量和总大小。
实现一个find命令的简化版,支持按文件名、文件类型、大小查找文件。
编写一个程序,检测指定目录中是否存在悬空的符号链接(目标文件不存在)。
实现一个文件去重工具:扫描目录,找出内容完全相同的文件,用硬链接替换重复文件以节省空间。