文件属性获取与目录IO操作详解
文件属性获取与目录IO操作详解
一、文件属性获取函数
1.1 函数原型及区别
在Linux系统中,我们可以使用以下三个函数来获取文件的属性信息:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>int stat(const char *path, struct stat *buf); // 通过文件名获取
int fstat(int fd, struct stat *buf); // 通过文件描述符获取
int lstat(const char *path, struct stat *buf); // 获取链接文件本身属性
三个函数的区别对比表:
| 函数 | 参数类型 | 特点 | 适用场景 |
|---|---|---|---|
stat() | 文件路径字符串 | 会追踪符号链接 | 获取符号链接指向文件的属性 |
fstat() | 文件描述符 | 通过已打开的文件获取属性 | 已打开文件的属性获取 |
lstat() | 文件路径字符串 | 不追踪符号链接 | 获取符号链接文件本身的属性 |
1.2 struct stat 结构体详解
struct stat 结构体包含了文件的完整属性信息:
struct stat {dev_t st_dev; // 文件所在设备的IDino_t st_ino; // inode编号(文件系统内唯一标识)mode_t st_mode; // 文件类型和权限nlink_t st_nlink; // 硬链接数量uid_t st_uid; // 文件所有者的用户IDgid_t st_gid; // 文件所有者的组IDdev_t st_rdev; // 特殊设备文件的设备IDoff_t st_size; // 文件大小(字节),特殊文件为0blksize_t st_blksize; // 文件系统I/O的块大小blkcnt_t st_blocks; // 分配的512字节块数量time_t st_atime; // 最后访问时间time_t st_mtime; // 最后修改时间time_t st_ctime; // 最后状态改变时间
};
时间字段说明表:
| 时间字段 | 含义 | 触发条件 |
|---|---|---|
st_atime | 最后访问时间 | 读取文件内容时更新 |
st_mtime | 最后修改时间 | 修改文件内容时更新 |
st_ctime | 最后状态改变时间 | 修改文件属性(如权限、所有者)时更新 |
1.3 文件类型判断宏
通过 st_mode 字段可以判断文件类型:
S_ISREG(m) // 是否为普通文件
S_ISDIR(m) // 是否为目录文件
S_ISCHR(m) // 是否为字符设备文件
S_ISBLK(m) // 是否为块设备文件
S_ISFIFO(m) // 是否为管道文件
S_ISLNK(m) // 是否为符号链接文件
S_ISSOCK(m) // 是否为套接字文件
文件类型判断示例表:
| 宏 | 返回值 | 文件类型 | 描述 |
|---|---|---|---|
S_ISREG(st_mode) | 1 | 普通文件 | 常规数据文件 |
S_ISDIR(st_mode) | 1 | 目录文件 | 包含其他文件的目录 |
S_ISCHR(st_mode) | 1 | 字符设备 | 如终端设备 |
S_ISBLK(st_mode) | 1 | 块设备 | 如磁盘分区 |
S_ISFIFO(st_mode) | 1 | 管道文件 | FIFO特殊文件 |
S_ISLNK(st_mode) | 1 | 符号链接 | 指向其他文件的链接 |
S_ISSOCK(st_mode) | 1 | 套接字文件 | 用于进程间通信 |
1.4 代码示例
示例1:获取文件大小
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>int main(int argc, char** argv)
{if(argc < 2){printf("请先传递文件名\n");return -1;}struct stat info;if(stat(argv[1], &info) == 0){printf("文件大小: %ld 字节\n", info.st_size);printf("文件占用的磁盘块数: %ld (每块512字节)\n", info.st_blocks);printf("实际占用空间: %ld 字节\n", info.st_blocks * 512);} else {perror("获取文件信息失败");}return 0;
}
示例2:判断文件类型
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>int main(int argc, char** argv)
{if(argc < 2){printf("请先传递文件名\n");return -1;}struct stat info;if(stat(argv[1], &info) != 0){perror("获取文件信息失败");return -1;}printf("文件信息:\n");printf("- 权限模式: %o\n", info.st_mode & 0777);if(S_ISREG(info.st_mode)){printf("- 类型: 普通文件\n");printf("- 大小: %ld 字节\n", info.st_size);}else if(S_ISDIR(info.st_mode)){printf("- 类型: 目录文件\n");}else if(S_ISCHR(info.st_mode)){printf("- 类型: 字符设备文件\n");}else if(S_ISBLK(info.st_mode)){printf("- 类型: 块设备文件\n");}else if(S_ISFIFO(info.st_mode)){printf("- 类型: 管道文件\n");}else if(S_ISLNK(info.st_mode)){printf("- 类型: 符号链接文件\n");}else if(S_ISSOCK(info.st_mode)){printf("- 类型: 套接字文件\n");}else{printf("- 类型: 未知文件类型\n");}return 0;
}
练习:在普通文件末尾追加内容
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>int main(int argc, char *argv[])
{struct stat buf = {0};int ret = 0;FILE *fp = NULL;if (argc < 2){printf("参数太少,请指定文件名\n");return -1;}// 使用lstat避免符号链接追踪ret = lstat(argv[1], &buf);if (ret < 0){printf("获取文件属性失败\n");return -1;}if (S_ISREG(buf.st_mode) != 1){printf("该文件不是普通文件,无法追加内容\n");return -1;}printf("该文件是普通文件,大小为 %ld 字节\n", buf.st_size);fp = fopen(argv[1], "a");if (NULL == fp){perror("打开文件失败");return -1;}fwrite("hehe", 4, 1, fp);fclose(fp);printf("成功在文件末尾追加 'hehe'\n");return 0;
}
二、目录IO操作
2.1 学习目录IO的意义
文件系统层次结构示意图:
根目录 /
├── home/
│ ├── user1/
│ │ ├── document.txt
│ │ └── photo.jpg
│ └── user2/
├── etc/
│ └── config.conf
└── var/└── log/
目录是组织和管理文件的容器,通过目录IO可以:
- 遍历目录内容
- 创建/删除目录
- 管理目录结构
- 实现文件搜索功能
2.2 访问文件与访问目录的区别
文件系统存储结构对比表:
| 组件 | 存储内容 | 访问方式 |
|---|---|---|
| 文件 | 实际数据内容 | 读写文件内容 |
| 目录 | 文件名和inode号的映射表 | 遍历目录项 |
| inode | 文件元数据(属性) | 通过系统调用获取 |
Linux目录结构示意图:
目录项表 (dentry)
+----------------+-----------------+
| 文件名 | inode编号 |
+----------------+-----------------+
| "file1.txt" | 12345 |
| "file2.c" | 12346 |
| "subdir" | 12347 |
+----------------+-----------------+↓
inode表 (存储文件属性)
+---------+-----------------------+
| inode# | struct stat 信息 |
+---------+-----------------------+
| 12345 | 大小、权限、时间等 |
| 12346 | 大小、权限、时间等 |
| 12347 | 大小、权限、时间等 |
+---------+-----------------------+
2.3 opendir() - 打开目录
#include <sys/types.h>
#include <dirent.h>DIR *opendir(const char *name);
参数说明:
name:要打开的目录路径
返回值:
- 成功:目录流指针 (
DIR *) - 失败:
NULL
验证opendir是否改变当前目录:
#include <sys/types.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>int main(int argc, char* argv[])
{if(argc < 2){printf("请指定目录路径\n");return -1;}printf("打开目录前的当前目录: ");system("pwd");DIR *dirfp = opendir(argv[1]);if(dirfp == NULL){perror("opendir error");return -1;}printf("打开目录后的当前目录: ");system("pwd");closedir(dirfp);return 0;
}
结论: opendir() 只打开目录进行读取,不会改变程序的当前工作目录。
2.4 chdir() - 切换工作目录
#include <unistd.h>
int chdir(const char *path);
参数:
path:目标路径
返回值:
- 成功:0
- 失败:-1
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>int main()
{FILE *fp = NULL;char buf[1024] = {0};printf("切换前的当前目录: ");system("pwd");if (chdir("/home/superman") < 0){perror("切换路径失败");return -1;}printf("切换后的当前目录: ");system("pwd");// 现在打开的是 /home/superman/1.txtfp = fopen("./1.txt", "r");if (NULL == fp){perror("打开文件失败");return -1;}fread(buf, 1024, 1, fp);printf("文件内容: %s\n", buf);fclose(fp);return 0;
}
2.5 readdir() - 读取目录内容
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
struct dirent 结构体:
struct dirent {ino_t d_ino; // 文件索引号off_t d_off; // 目录项偏移量unsigned short d_reclen; // 目录项大小unsigned char d_type; // 文件类型char d_name[256]; // 文件名
};
d_type 文件类型常量表:
| 常量 | 值 | 文件类型 | 描述 |
|---|---|---|---|
DT_REG | 8 | 普通文件 | 常规数据文件 |
DT_DIR | 4 | 目录 | 文件夹 |
DT_LNK | 10 | 符号链接 | 软链接文件 |
DT_CHR | 2 | 字符设备 | 终端等字符设备 |
DT_BLK | 6 | 块设备 | 磁盘等块设备 |
DT_FIFO | 1 | 管道 | FIFO特殊文件 |
DT_SOCK | 12 | 套接字 | 进程间通信 |
DT_UNKNOWN | 0 | 未知类型 | 无法确定类型 |
2.6 closedir() - 关闭目录
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
2.7 完整示例:遍历目录内容
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>const char* get_file_type(unsigned char d_type) {switch(d_type) {case DT_REG: return "普通文件";case DT_DIR: return "目录";case DT_LNK: return "符号链接";case DT_CHR: return "字符设备";case DT_BLK: return "块设备";case DT_FIFO: return "管道";case DT_SOCK: return "套接字";default: return "未知类型";}
}int main(int argc, char* argv[])
{const char* path = ".";if(argc > 1) {path = argv[1];}struct dirent *entry = NULL;DIR *dir_p = opendir(path);if (NULL == dir_p) {perror("打开目录失败");return -1;}printf("目录 '%s' 的内容:\n", path);printf("%-15s %-10s %-15s %s\n", "索引号", "类型", "大小", "文件名");printf("------------------------------------------------\n");while (1) {entry = readdir(dir_p);if (NULL == entry) {break;}printf("%-15lu %-10s %-15d %s\n",entry->d_ino,get_file_type(entry->d_type),entry->d_reclen,entry->d_name);}closedir(dir_p);return 0;
}
目录遍历流程图:
开始↓
opendir()打开目录↓
readdir()读取第一个目录项↓
┌─→ 是否为NULL? ──→ 是 ──→ closedir()关闭目录 ──→ 结束
│ 否
│ 处理当前目录项信息
│ ↓
└── readdir()读取下一个目录项
2.8 进阶示例:递归遍历目录树
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <string.h>void list_directory(const char* path, int depth) {DIR *dir;struct dirent *entry;struct stat statbuf;char fullpath[1024];if ((dir = opendir(path)) == NULL) {return;}while ((entry = readdir(dir)) != NULL) {// 跳过 . 和 .. 目录if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {continue;}// 构建完整路径snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);// 获取文件信息if (lstat(fullpath, &statbuf) == -1) {continue;}// 缩进显示层次结构for (int i = 0; i < depth; i++) {printf(" ");}if (S_ISDIR(statbuf.st_mode)) {printf("📁 %s/\n", entry->d_name);// 递归遍历子目录list_directory(fullpath, depth + 1);} else if (S_ISLNK(statbuf.st_mode)) {printf("🔗 %s -> 链接文件\n", entry->d_name);} else if (S_ISREG(statbuf.st_mode)) {printf("📄 %s (%ld 字节)\n", entry->d_name, statbuf.st_size);} else {printf("❓ %s\n", entry->d_name);}}closedir(dir);
}int main(int argc, char* argv[]) {const char* path = ".";if (argc > 1) {path = argv[1];}printf("目录树: %s\n", path);printf("====================\n");list_directory(path, 0);return 0;
}
三、总结
文件属性获取与目录IO对比表:
| 功能 | 文件操作 | 目录操作 |
|---|---|---|
| 打开 | open(), fopen() | opendir() |
| 读取 | read(), fread() | readdir() |
| 关闭 | close(), fclose() | closedir() |
| 定位 | lseek(), fseek() | 自动顺序读取 |
| 信息获取 | stat(), fstat() | readdir()返回dirent |
关键知识点:
- stat vs lstat:处理符号链接时的区别很重要
- 目录流:目录读取是顺序的,类似文件流但结构不同
- 文件类型判断:可以通过
st_mode宏或d_type字段 - 错误处理:所有系统调用都应检查返回值
- 资源释放:打开的目录必须用
closedir()关闭
。
