4UNIX环境高级编程(第3版)

文件和目录

文件系统

inode目录链接文件权限

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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的内容

c
// 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系列函数

c
#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:对于符号链接,返回链接本身的信息,而不是链接指向的文件
c
#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()套接字
c
#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 目录操作

c
#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。

c
#include <unistd.h>

int link(const char *oldpath, const char *newpath);
int unlink(const char *pathname);  // 删除文件名
c
// 创建硬链接
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不同。

c
#include <unistd.h>

int symlink(const char *target, const char *linkpath);
int readlink(const char *pathname, char *buf, size_t bufsiz);
c
// 创建符号链接
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)

目录权限的特殊性:

bash
# 目录权限为rwx--x--x (711)
# 其他用户可以进入目录,但不能列出内容
cd /some/dir     # 可以,有x权限
ls /some/dir     # 失败,没有r权限
cat /some/dir/file.txt  # 可以,如果知道文件名且有x权限

2.6.2 特殊权限位

set-uid(s位,所有者执行位):当设置在一个可执行文件上时,进程运行时拥有文件所有者的权限,而不是运行者的权限。

bash
# 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。

bash
ls -ld /tmp
# drwxrwxrwt 1 root root ... /tmp
# t位使得用户只能删除自己的文件

2.6.3 umask

umask设置创建文件时的权限掩码。新文件的权限为mode & ~umask。

c
#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

c
#include <sys/time.h>

int utimes(const char *pathname, const struct timeval times[2]);

utimes修改文件的访问时间和修改时间:

c
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的程序

c
#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)

c
#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 查找硬链接

c
#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命令的简化版,支持按文件名、文件类型、大小查找文件。

编写一个程序,检测指定目录中是否存在悬空的符号链接(目标文件不存在)。

实现一个文件去重工具:扫描目录,找出内容完全相同的文件,用硬链接替换重复文件以节省空间。


4/12文件和目
0%