文件系统操作
目录操作与文件属性
概述
文件系统操作是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;
}