文件属性获取与目录IO操作详解
一、文件属性获取函数
1.1 函数原型及区别
在Linux系统中,我们可以使用以下三个函数来获取文件的属性信息:
c
#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 结构体包含了文件的完整属性信息:
c
struct stat {
dev_t st_dev; // 文件所在设备的ID
ino_t st_ino; // inode编号(文件系统内唯一标识)
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数量
uid_t st_uid; // 文件所有者的用户ID
gid_t st_gid; // 文件所有者的组ID
dev_t st_rdev; // 特殊设备文件的设备ID
off_t st_size; // 文件大小(字节),特殊文件为0
blksize_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 字段可以判断文件类型:
c
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:获取文件大小
c
#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:判断文件类型
c
#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;
}
练习:在普通文件末尾追加内容
c
#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() - 打开目录
c
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
参数说明:
name:要打开的目录路径
返回值:
- 成功:目录流指针 (
DIR *) - 失败:
NULL
验证opendir是否改变当前目录:
c
#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() - 切换工作目录
c
#include <unistd.h>
int chdir(const char *path);
参数:
path:目标路径
返回值:
- 成功:0
- 失败:-1
c
#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.txt
fp = 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() - 读取目录内容
c
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
struct dirent 结构体:
c
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() - 关闭目录
c
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
2.7 完整示例:遍历目录内容
c
#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 进阶示例:递归遍历目录树
c
#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()关闭
。