4. 目录操作与文件属性.md 9.7 KB

4 目录操作与文件属性

4.1 基本概念

Linux中目录也是一种文件,目录文件的内容是一组"文件名→ inode编号"的映射。目录操作和文件属性获取是嵌入式Linux应用开发中的常见需求,比如扫描设备节点、读取 /proc/sys 文件系统获取系统信息。

核心函数族:

  • 目录操作:opendir / readdir / closedir(类似C库的fopen/fread/fclose)
  • 文件属性:stat / fstat / lstat(获取文件元数据)
  • 文件类型判断:S_ISREG() / S_ISDIR() / S_ISCHR() 等宏

4.2 核心API详解

4.2.1 目录操作

#include <dirent.h>

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);

DIR:目录流对象,类似FILE*

struct dirent:

struct dirent {
    ino_t          d_ino;       // inode编号
    off_t          d_off;       // 到下一个dirent的偏移(不常用)
    unsigned short d_reclen;    // 本条记录长度
    unsigned char  d_type;      // 文件类型
    char           d_name[256]; // 文件名(不含路径前缀)
};

d_type文件类型:

含义
DT_REG 普通文件
DT_DIR 目录
DT_LNK 符号链接
DT_CHR 字符设备 嵌入式常用
DT_BLK 块设备 嵌入式常用
DT_FIFO 管道
DT_SOCK socket
DT_UNKNOWN 未知 某些文件系统不支持d_type

注意: d_type 不是所有文件系统都支持(如ext2/ext3),此时返回DT_UNKNOWN,需要调用 stat 来判断文件类型。

4.2.2 文件属性 stat

#include <sys/stat.h>

int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);

区别:

  • stat:通过路径获取属性,跟随符号链接
  • fstat:通过已打开的fd获取属性
  • lstat:通过路径获取属性,不跟随符号链接(获取链接文件本身的属性)

struct stat(核心字段):

struct stat {
    dev_t     st_dev;      // 文件所在设备
    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;     // 特殊设备文件的设备号
    off_t     st_size;     // 文件大小(字节)
    blksize_t st_blksize;  // 文件系统IO块大小
    blkcnt_t  st_blocks;   // 占用的磁盘块数
    time_t    st_atime;    // 最后访问时间
    time_t    st_mtime;    // 最后修改时间
    time_t    st_ctime;    // 最后状态变更时间
};

文件类型判断宏(从st_mode中提取):

含义
S_ISREG(m) 普通文件
S_ISDIR(m) 目录
S_ISCHR(m) 字符设备
S_ISBLK(m) 块设备
S_ISFIFO(m) 管道/FIFO
S_ISSOCK(m) socket
S_ISLNK(m) 符号链接

权限判断宏:

含义
S_IRUSR 所有者读
S_IWUSR 所有者写
S_IXUSR 所有者执行
S_IRGRP 组读
S_IWGRP 组写
S_IXGRP 组执行
S_IROTH 其他读
S_IWOTH 其他写
S_IXOTH 其他执行

用法示例:

struct stat st;
if (stat("/dev/fb0", &st) == 0) {
    if (S_ISCHR(st.st_mode)) {
        printf("是字符设备, 设备号: %d.%d\n",
               major(st.st_rdev), minor(st.st_rdev));
    }
    printf("权限: %o\n", st.st_mode & 0777);
}

4.3 完整代码示例

示例1:扫描目录并分类统计

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

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

    DIR *dp = opendir(path);
    if (!dp) {
        perror("opendir");
        return 1;
    }

    int reg_count = 0, dir_count = 0, dev_count = 0, other_count = 0;
    struct dirent *entry;

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

        // 优先使用d_type
        if (entry->d_type != DT_UNKNOWN) {
            switch (entry->d_type) {
                case DT_REG: reg_count++; break;
                case DT_DIR: dir_count++; break;
                case DT_CHR:
                case DT_BLK: dev_count++; break;
                default: other_count++; break;
            }
        } else {
            // d_type不支持时用stat
            char fullpath[1024];
            snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);
            struct stat st;
            if (stat(fullpath, &st) == 0) {
                if (S_ISREG(st.st_mode)) reg_count++;
                else if (S_ISDIR(st.st_mode)) dir_count++;
                else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) dev_count++;
                else other_count++;
            }
        }
    }

    closedir(dp);
    printf("目录 %s:\n", path);
    printf("  普通文件: %d\n", reg_count);
    printf("  目录: %d\n", dir_count);
    printf("  设备文件: %d\n", dev_count);
    printf("  其他: %d\n", other_count);
    return 0;
}

编译:gcc -o scandir scandir.c

示例2:递归遍历目录树

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

void traverse(const char *base, int depth) {
    DIR *dp = opendir(base);
    if (!dp) return;

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

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

        // 缩进显示
        for (int i = 0; i < depth; i++) printf("  ");

        if (entry->d_type == DT_DIR) {
            printf("[DIR]  %s\n", entry->d_name);
            traverse(path, depth + 1);  // 递归进入子目录
        } else if (entry->d_type == DT_REG) {
            struct stat st;
            if (stat(path, &st) == 0)
                printf("[FILE] %s (%ld bytes)\n", entry->d_name, (long)st.st_size);
            else
                printf("[FILE] %s\n", entry->d_name);
        } else {
            printf("[?]    %s\n", entry->d_name);
        }
    }
    closedir(dp);
}

int main(int argc, char *argv[]) {
    traverse(argc > 1 ? argv[1] : ".", 0);
    return 0;
}

编译:gcc -o tree tree.c

示例3:检查设备文件是否存在

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

int device_exists(const char *path) {
    struct stat st;
    if (stat(path, &st) < 0) return 0;
    return S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode);
}

int main() {
    const char *devices[] = {
        "/dev/fb0", "/dev/input/event0", "/dev/ttyS0",
        "/dev/i2c-0", "/dev/spidev0.0", NULL
    };

    for (int i = 0; devices[i]; i++) {
        if (device_exists(devices[i]))
            printf("  [√] %s\n", devices[i]);
        else
            printf("  [×] %s\n", devices[i]);
    }
    return 0;
}

编译:gcc -o checkdev checkdev.c

4.4 注意事项与易错点

序号 坑点 说明
1 readdir不是线程安全的 多线程中应该用 readdir_r(旧)或 readdir(glibc2.19+已线程安全)
2 d_type不保证支持 某些文件系统(ext2/ext3)d_type总是DT_UNKNOWN,必须用stat兜底
3 opendir失败返回NULL 必须检查返回值,常见原因:路径不存在、权限不足
4 stat跟随符号链接 如果要获取链接文件本身属性,必须用 lstat
5 权限用八进制表示 st_mode & 0777 获取权限位,printf("%o", mode) 以八进制输出
6 时间字段是time_t ctime(&st.st_mtime) 转为可读字符串,或用 localtime + strftime
7 递归遍历深度 目录嵌套太深可能导致栈溢出,嵌入式环境尤其注意
8 /proc和/sys是虚拟文件系统 文件属性可能不完整(如文件大小为0),实际内容由内核动态生成

4.5 面试要点

Q: stat和lstat的区别? A: stat跟随符号链接,返回目标文件的属性;lstat不跟随符号链接,返回链接文件本身的属性。比如 stat("link_to_file", &st) 返回的是目标文件的属性,lstat("link_to_file", &st) 返回的是符号链接本身的属性。

Q: 如何判断一个文件是普通文件还是目录? A: 用 stat 获取 struct stat,然后用 S_ISREG(st.st_mode) 判断是否为普通文件,S_ISDIR(st.st_mode) 判断是否为目录。

Q: readdir的d_type字段有什么限制? A: d_type不是所有文件系统都支持,ext2/ext3下可能返回DT_UNKNOWN。不可移植到所有Unix系统。需要兜底时用stat判断文件类型。

Q: 嵌入式中哪些场景会用到目录操作? A: 扫描 /dev/ 目录查找设备节点、读取 /proc//sys/ 获取系统信息、遍历配置文件目录加载配置。