前置知识: C

文件系统操作

3 minIntermediate2026/6/14

目录操作与文件属性

概述

文件系统操作是C语言与操作系统交互的重要部分,包括目录遍历、文件属性查询、文件权限管理和路径操作等。C标准库提供了基本的文件操作(如 fopen/fclose),而 POSIX 标准则提供了更丰富的文件系统 API,如目录遍历、文件状态查询和文件系统操作。

基础概念

文件系统层次

文件系统以目录树的形式组织,根目录为 /(Linux/macOS)或盘符(Windows)。每个文件和目录都有以下属性:

  • 名称、大小、
  • 权限(读、写、执行)
  • 所有者(用户、组)
  • 时间戳(创建、修改、访问)

相关头文件

头文件说明
<stdio.h>标准文件I/O
<dirent.h>目录遍历(POSIX)
<sys/stat.h>文件属性(POSIX)
<unistd.h>文件操作(POSIX)
<fcntl.h>文件控制
<sys/types.h>系统数据

快速上手

遍历目录

#include <stdio.h>
#include <dirent.h>

int main(void) {
    // 打开当前目录
    DIR *dir = opendir(".");
    if (!dir) {
        perror("打开目录失败");
        return 1;
    }

    // 逐个读取目录项
    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        // 跳过 . 和 .. 目录
        if (entry->d_name[0] == '.' &&
            (entry->d_name[1] == '\0' ||
             (entry->d_name[1] == '.' && entry->d_name[2] == '\0'))) {
            continue;
        }
        printf("%s\n", entry->d_name);
    }

    closedir(dir);
    return 0;
}

获取文件属性

#include <stdio.h>
#include <sys/stat.h>
#include <time.h>

int main(void) {
    struct stat st;

    // 获取文件属性
    if (stat("file.txt", &st) != 0) {
        perror("获取文件属性失败");
        return 1;
    }

    printf("文件大小: %ld 字节\n", st.st_size);
    printf("权限: %o\n", st.st_mode & 0777);
    printf("最后修改: %s", ctime(&st.st_mtime));

    // 判断文件类型
    if (S_ISREG(st.st_mode))  printf("类型: 普通文件\n");
    if (S_ISDIR(st.st_mode))  printf("类型: 目录\n");
    if (S_ISLNK(st.st_mode))  printf("类型: 符号链接\n");

    return 0;
}

详细用法

目录操作

#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main(void) {
    // 创建目录
    if (mkdir("test_dir", 0755) != 0) {
        perror("创建目录失败");
    }

    // 检查目录是否存在
    struct stat st;
    if (stat("test_dir", &st) == 0 && S_ISDIR(st.st_mode)) {
        printf("目录存在\n");
    }

    // 切换工作目录
    if (chdir("test_dir") == 0) {
        printf("已切换到 test_dir\n");

        // 获取当前工作目录
        char cwd[256];
        if (getcwd(cwd, sizeof(cwd))) {
            printf("当前目录: %s\n", cwd);
        }

        chdir(".."); // 返回上级目录
    }

    // 删除空目录
    rmdir("test_dir");
    printf("目录已删除\n");

    return 0;
}

文件权限操作

#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main(void) {
    // 修改文件权限
    chmod("file.txt", 0644); // 用户读写,组读,其他读

    // 修改文件所有者
    // chown("file.txt", uid, gid); // 需要 root 权限

    // 检查文件是否可访问
    if (access("file.txt", R_OK) == 0) {
        printf("文件可读\n");
    }
    if (access("file.txt", W_OK) == 0) {
        printf("文件可写\n");
    }
    if (access("file.txt", X_OK) == 0) {
        printf("文件可执行\n");
    }
    if (access("file.txt", F_OK) == 0) {
        printf("文件存在\n");
    }

    return 0;
}

递归遍历目录

#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>

// 递归遍历目录
void list_dir(const char *path, int depth) {
    DIR *dir = opendir(path);
    if (!dir) {
        perror(path);
        return;
    }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        // 跳过 . 和 ..
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        // 构造完整路径
        char full_path[512];
        snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);

        // 获取文件属性
        struct stat st;
        if (lstat(full_path, &st) != 0) continue;

        // 打印缩进和文件名
        for (int i = 0; i < depth; i++) printf("  ");
        printf("%s", entry->d_name);

        if (S_ISDIR(st.st_mode)) {
            printf("/\n");
            list_dir(full_path, depth + 1); // 递归进入子目录
        } else {
            printf(" (%ld bytes)\n", st.st_size);
        }
    }

    closedir(dir);
}

int main(void) {
    list_dir(".", 0);
    return 0;
}

文件重命名与删除

#include <stdio.h>
#include <unistd.h>

int main(void) {
    // 重命名文件
    if (rename("old.txt", "new.txt") == 0) {
        printf("重命名成功\n");
    } else {
        perror("重命名失败");
    }

    // 删除文件
    if (unlink("new.txt") == 0) {
        printf("删除成功\n");
    } else {
        perror("删除失败");
    }

    // 创建临时文件
    char tmpname[] = "/tmp/myapp_XXXXXX";
    int fd = mkstemp(tmpname);
    if (fd != -1) {
        printf("临时文件: %s\n", tmpname);
        close(fd);
        unlink(tmpname); // 使用后删除
    }

    return 0;
}

常见场景

场景一:文件搜索工具

#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>

// 在目录树中搜索文件名包含关键字的文件
void search_files(const char *dir_path, const char *keyword) {
    DIR *dir = opendir(dir_path);
    if (!dir) return;

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        char path[512];
        snprintf(path, sizeof(path), "%s/%s", dir_path, entry->d_name);

        struct stat st;
        if (lstat(path, &st) != 0) continue;

        if (S_ISDIR(st.st_mode)) {
            search_files(path, keyword); // 递归搜索子目录
        } else if (strstr(entry->d_name, keyword)) {
            printf("找到: %s\n", path);
        }
    }

    closedir(dir);
}

int main(int argc, char *argv[]) {
    const char *dir = argc > 1 ? argv[1] : ".";
    const char *keyword = argc > 2 ? argv[2] : ".c";

    printf("在 %s 中搜索包含 '%s' 的文件:\n", dir, keyword);
    search_files(dir, keyword);
    return 0;
}

场景二:目录大小统计

#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>

// 递归计算目录总大小
long long calc_dir_size(const char *path) {
    DIR *dir = opendir(path);
    if (!dir) return 0;

    long long total = 0;
    struct dirent *entry;

    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        char full_path[512];
        snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);

        struct stat st;
        if (lstat(full_path, &st) != 0) continue;

        if (S_ISDIR(st.st_mode)) {
            total += calc_dir_size(full_path);
        } else {
            total += st.st_size;
        }
    }

    closedir(dir);
    return total;
}

int main(void) {
    long long size = calc_dir_size(".");
    printf("当前目录总大小: %.2f MB\n", size / (1024.0 * 1024.0));
    return 0;
}

注意事项

路径长度限制

不同系统对路径长度有不同的限制,应使用 PATH_MAX 常量:

#include <limits.h>

char path[PATH_MAX]; // 通常为4096字节

符号链接处理

stat 会跟随符号链接获取目标文件属性,lstat 则获取链接本身属性:

struct stat st;
stat("link", &st);   // 获取链接目标的属性
lstat("link", &st);  // 获取链接本身的属性

竞态条件

在检查文件属性和操作文件之间,文件可能被其他进程修改(TOCTOU问题)。对于安全敏感的操作,应直接尝试操作并检查返回值,而不是先检查再操作。

进阶用法

使用 nftw 遍历目录树

#include <stdio.h>
#include <ftw.h>
#include <sys/stat.h>

static long long total_size = 0;

// nftw 回调函数
static int walk_callback(const char *fpath, const struct stat *sb,
                         int typeflag, struct FTW *ftwbuf) {
    if (typeflag == FTW_F) {
        total_size += sb->st_size;
        printf("%s (%ld bytes)\n", fpath, sb->st_size);
    }
    return 0; // 继续遍历
}

int main(void) {
    // 使用 nftw 遍历目录树
    // FTW_DEPTH: 先访问子目录再访问父目录(深度优先)
    // FTW_PHYS: 不跟随符号链接
    nftw(".", walk_callback, 10, FTW_DEPTH | FTW_PHYS);

    printf("总大小: %lld 字节\n", total_size);
    return 0;
}

监控文件变化(inotify)

#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>

#define EVENT_SIZE (sizeof(struct inotify_event))
#define BUF_LEN (1024 * (EVENT_SIZE + 16))

int main(void) {
    // 初始化 inotify
    int fd = inotify_init();
    if (fd < 0) {
        perror("inotify_init 失败");
        return 1;
    }

    // 添加监控:监听当前目录的创建、删除和修改事件
    int wd = inotify_add_watch(fd, ".", IN_CREATE | IN_DELETE | IN_MODIFY);

    printf("监控当前目录变化(按 Ctrl+C 退出)\n");

    char buf[BUF_LEN];
    while (1) {
        int len = read(fd, buf, BUF_LEN);
        if (len < 0) {
            perror("read 失败");
            break;
        }

        int i = 0;
        while (i < len) {
            struct inotify_event *event = (struct inotify_event *)&buf[i];

            if (event->mask & IN_CREATE) {
                printf("创建: %s\n", event->name);
            }
            if (event->mask & IN_DELETE) {
                printf("删除: %s\n", event->name);
            }
            if (event->mask & IN_MODIFY) {
                printf("修改: %s\n", event->name);
            }

            i += EVENT_SIZE + event->len;
        }
    }

    inotify_rm_watch(fd, wd);
    close(fd);
    return 0;
}