解码Linux文件IO目录检索与文件属性

目录检索的核心需求

当需要批量访问某个路径下的多个文件时,手动调用open函数逐个处理效率极低。Linux 系统将目录视为特殊文件,提供了一套专门的目录操作接口,可高效实现目录的创建、删除、打开、读取,以及文件属性获取,解决批量文件访问问题。

Linux 目录与文件系统基础

目录的本质:索引而非容器

  • Linux 中目录是特殊文件,存储的最小单位是 "目录项"(而非普通文件的字符)。
  • 目录项的作用:记录 "文件名" 与 "inode 编号" 的映射关系(类似门牌号,只记位置,不存内容)。
  • 文件夹 vs 目录:文件夹是 "容器"(直观存储文件的概念),目录是 "索引"(底层记录位置的机制),日常使用中可混用,但底层逻辑不同。
    目录与文件夹
对比维度 目录(Directory,Linux 下) 文件夹(Folder)
底层本质 文件系统中标识为 "d" 类型的特殊文件(本质是逻辑索引表),是 Linux 底层文件系统的核心组成单元 图形界面(GUI)中的 可视化容器形象,仅为用户层抽象概念(无独立底层实体,依赖目录存在)
数据存储逻辑 不存储文件本体,仅保存"目录项"(每条含 文件名 + inode 编号),本质是文件定位的索引表 无实际底层存储逻辑/数据结构,仅通过 GUI 视觉效果(如"打开显示内部文件")传递"存放"感知
与文件的关联 间接关联:通过目录项的 inode 编号,映射文件在磁盘数据区的存储块(依赖 inode 与数据块的底层关联) 视觉关联:依赖 GUI 操作(双击打开、拖拽文件)让用户"感知"文件在其中,不涉及底层 inode/数据块
技术核心作用 支撑 Linux "/" 为顶层的唯一绝对路径体系,构建树状文件结构,实现文件的精准索引与定位 降低操作门槛:将抽象"目录索引"转化为可交互容器(如桌面文件夹、Nautilus/Finder 中的文件夹)
底层操作关联 可通过系统调用直接操作(如 mkdir 创建、opendir 打开、readdir 读取目录项),是底层操作对象 无法通过系统调用直接操作,本质是 GUI 对"目录"的封装------操作文件夹即间接调用目录接口
类比对应 类比"街道门牌号":仅记录文件的精准位置(路径),不承载任何文件内容 类比"实体房子":仅提供"文件在其中"的直观承载感知,无实际位置定位的底层逻辑

倒置树状文件系统

  • 所有文件 / 目录以根目录/ 为顶层,构成倒置树状结构。
  • 根目录下常见系统目录(必须掌握):
    • /bin:存放系统基础命令(如lscd
    • /dev:存放设备文件(如硬盘/dev/sda、终端/dev/tty
    • /etc:存放系统配置文件(如/etc/passwd
    • /home:普通用户的主目录(如/home/gec
    • /root:管理员(root 用户)的主目录
    • /usr:存放用户程序与资源(如/usr/bin/usr/lib
    • /tmp:临时文件目录(重启后内容清空)

磁盘存储原理:文件数据存哪里?

磁盘的最小存储单位

  • 扇区(Sector):硬盘物理最小存储单位,默认 512 字节(不可更改)。
  • 块(Block):操作系统访问磁盘的最小单位,由 8 个扇区组成(即 4KB)。理由:CPU 一次读取 4KB 比多次读取 512 字节更高效,减少 IO 次数。

inode:文件属性的 "身份证"

inode 区与数据区

硬盘格式化时会自动分成两个区域:

  • inode 区(inode table) :存储 "inode 结构体",每个文件对应一个 inode。
    • inode 结构体记录文件属性:大小、读写权限、时间戳(访问 / 修改 / 状态变更时间)、所属用户 / 组、指向数据块的指针。
    • inode 区以数组存储,数组下标即 inode 编号(唯一标识一个文件)。
  • 数据区(Block Area):存储文件的实际内容,inode 中的指针直接指向数据区的块。

查看文件的 inode 编号

通过ls命令的-i选项(-l显示详细信息,可组合使用):

bash 复制代码
# 格式:ls -li [路径]
gec@ubuntu:~/project$ ls -li
total 600
939233 drwxrwxr-x 4 gec gec 4096 12月 21 09:54 libjpeg  # inode编号:939233(目录)
939249 -rwxrwxr-x 1 gec gec 598784 12月 21 09:55 project_gif  # inode编号:939249(可执行文件)
939236 -rw-rw-r-- 1 gec gec 6146 12月 21 09:54 project_gif.c  # inode编号:939236(普通文件)

核心目录操作接口

所有接口需包含对应头文件,使用前建议通过man命令查看细节(如man 2 mkdir)。

创建目录:mkdir

功能

创建指定路径的新目录。

函数

c 复制代码
#include <sys/stat.h>
#include <sys/types.h>
/**
 * 创建新目录
 * @param pathname 要创建的目录路径(绝对路径如"/home/gec/test",相对路径如"test")
 * @param mode 目录的权限模式(八进制,如0755表示所有者rwx、组rx、其他rx)
 * @return 成功返回0;失败返回-1,且设置errno(如EEXIST表示目录已存在)
 * @note 实际权限 = mode & ~umask(umask是系统默认权限掩码,默认0022)
 *       需确保父目录存在(如创建"/a/b"需先有"/a",否则失败)
 */
int mkdir(const char *pathname, mode_t mode);

删除目录:rmdir

功能

删除指定的空目录(非空目录无法删除)。

函数

c 复制代码
#include <unistd.h>
/**
 * 删除空目录
 * @param pathname 要删除的目录路径(绝对/相对路径均可)
 * @return 成功返回0;失败返回-1,且设置errno(如ENOTEMPTY表示目录非空)
 * @note 只能删除空目录(需先删除目录内所有文件/子目录)
 *       不能删除根目录`/`或当前工作目录的父目录(避免系统崩溃)
 */
int rmdir(const char *pathname);

打开目录:opendir

功能

打开指定目录,返回 "目录流指针"(后续读取目录需用此指针)。

函数

c 复制代码
#include <sys/types.h>
#include <dirent.h>
/**
 * 打开目录并返回目录流指针
 * @param name 要打开的目录路径(如"/home/gec")
 * @return 成功返回指向DIR结构体的指针(目录流);失败返回NULL,且设置errno
 * @note 目录流初始指向目录的第一个目录项(`.`表示当前目录)
 *       打开目录后需用closedir()关闭,避免资源泄漏
 */
DIR *opendir(const char *name);

关闭目录:closedir

功能

关闭指定目录。

函数

c 复制代码
/**
 * 关闭目录流(必须配对opendir使用)
 * @param dirp opendir返回的目录流指针
 * @return 成功返回0;失败返回-1,且设置errno
 */
int closedir(DIR *dirp);

切换工作目录:chdir

功能

将当前进程的 "工作目录" 切换到指定路径(类似终端的cd命令)。

关键说明

  • 打开目录(opendir)≠ 进入目录(chdir):只有切换到目标目录,才能正确读取目录内文件的属性(如用 stat 获取信息)。

函数

c 复制代码
#include <unistd.h>

/**
 * 切换当前工作目录
 * @param path 目标工作目录路径(绝对/相对路径均可)
 * @return 成功返回0;失败返回-1,且设置errno(如ENOENT表示路径不存在)
 * @example 如当前目录是"/home",chdir("gec")后,工作目录变为"/home/gec"
 */
int chdir(const char *path);

/**
 * 获取当前工作目录(辅助函数)
 * @param buf 存储当前目录路径的缓冲区
 * @param size 缓冲区大小(需足够大,避免路径截断)
 * @return 成功返回buf(缓冲区地址);失败返回NULL,且设置errno
 */
char *getcwd(char *buf, size_t size);

读取目录项:readdir

功能

从目录流中读取 "下一个目录项"(每个目录项对应一个文件 / 子目录)。

核心结构体:struct dirent

存储目录项信息,关键成员如下:

c 复制代码
struct dirent {
    ino_t  d_ino;       // 目录项对应的inode编号
    char   d_name[256]; // 文件名(以'\0'结尾,最长255字符)
    unsigned char d_type; // 文件类型(部分文件系统支持,如ext4)
    // d_type的常用取值:
    // DT_REG:普通文件   DT_DIR:目录   DT_LNK:符号链接   DT_UNKNOWN:未知类型
};
d_type宏定义 对应文件类型 说明与典型场景(含编程用途)
DT_REG 普通文件 最基础的文件类型,存储文本、二进制等实际数据(如.c源码、.txt文档、编译后的可执行文件),遍历目录时常用其筛选普通文件
DT_DIR 目录 用于索引子文件/子目录的特殊文件(如/home用户目录、./test_dir当前子目录),编程中通过它判断是否为目录以递归遍历
DT_LNK 符号链接(软链接) 指向其他文件/目录的"路径指针"(如ln -s src_file link_file创建的link_file),需注意链接可能指向不存在的路径(断链)
DT_BLK 块设备文件 以"固定大小块"为单位读写的硬件设备接口(如硬盘/dev/sda、分区/dev/sda1),常用于驱动或磁盘管理程序中识别块设备
DT_CHR 字符设备文件 以"字符流"为单位实时读写的硬件设备接口(如终端/dev/tty、键盘/dev/input/event0),适合处理键盘输入、串口通信等实时数据
DT_FIFO 命名管道(FIFO) 用于无亲缘关系进程间通信的特殊文件(如mkfifo my_pipe创建的my_pipe),可通过read/write函数实现进程间数据传递
DT_SOCK 套接字文件 支持本地或网络进程通信的文件(如/var/run/docker.sock),是本地套接字(AF_UNIX)的载体,常用于进程间跨网络或本地通信
DT_UNKNOWN 未知类型 当文件系统不支持d_type字段时返回(如部分老版本文件系统),需通过stat函数读取st_mode进一步判断实际类型

函数

c 复制代码
#include <dirent.h>
/**
 * 读取目录流中的下一个目录项
 * @param dirp opendir返回的目录流指针
 * @return 成功返回指向struct dirent的指针;失败/到目录末尾返回NULL
 * @note 目录末尾返回NULL时,errno不变;错误返回NULL时,errno被设置
 *       需过滤`.`(当前目录)和`..`(父目录),避免无限循环
 */
struct dirent *readdir(DIR *dirp);

删除文件 / 空目录:remove

功能

删除指定路径的普通文件,或删除空目录(功能覆盖 rmdir,且支持文件删除,更灵活)。

函数

c 复制代码
#include <stdio.h>
/**
 * 删除普通文件或空目录
 * @param pathname 要删除的文件/空目录路径(绝对/相对路径均可)
 * @return 成功返回0;失败返回-1,且设置errno(如ENOTEMPTY表示目录非空,ENOENT表示路径不存在)
 * @note 删除文件:直接删除普通文件、符号链接(仅删链接本身,不删目标文件)
 *       删除目录:仅能删除空目录,功能与rmdir一致
 *       无法删除非空目录(需先递归删除目录内内容)
 */
int remove(const char *pathname);

重置目录流:rewinddir

功能

将已打开的目录流指针重置到目录的 "第一个目录项"(即.的位置),支持目录内容的重复读取。

函数

c 复制代码
#include <sys/types.h>
#include <dirent.h>
/**
 * 重置目录流到起始位置
 * @param dirp opendir返回的目录流指针(必须是已打开的有效指针)
 * @return 无返回值(无失败状态,直接操作目录流)
 * @note 配合readdir使用:读完目录后调用rewinddir,再调用readdir可重新遍历目录
 *       重置后,下一次readdir会从第一个目录项(`.`)开始读取
 */
void rewinddir(DIR *dirp);

获取目录流当前位置:telldir

功能

获取目录流当前的读取位置,返回值为 "位置标识",可用于后续seekdir定位。

函数

c 复制代码
#include <sys/types.h>
#include <dirent.h>
/**
 * 获取目录流当前的读取位置
 * @param dirp opendir返回的目录流指针(有效且已打开)
 * @return 成功返回当前位置的标识(off_t类型);失败返回-1(部分系统可能不设置errno)
 * @note 返回的off_t值是" opaque值"(透明值),不要假设其具体含义(如不是字节偏移)
 *       仅用于传递给seekdir,实现目录流的定位,不可手动修改或计算
 *       目录流被修改(如rewinddir、readdir)后,之前的位置标识可能失效
 */
off_t telldir(DIR *dirp);

定位目录流:seekdir

功能

将目录流移动到telldir返回的指定位置,实现目录的 "随机读取"(而非只能顺序读取)。

函数

c 复制代码
#include <sys/types.h>
#include <dirent.h>
/**
 * 将目录流定位到指定位置
 * @param dirp opendir返回的目录流指针(有效且已打开)
 * @param offset 要定位的位置标识(必须是同一目录流通过telldir获取的值)
 * @return 无返回值(无失败状态,直接操作目录流)
 * @note offset必须来自同一目录流的telldir返回值,不可使用其他目录流的offset
 *       定位后,下一次readdir会从offset对应的目录项开始读取
 *       目录内容被修改(如新增/删除文件),定位结果可能不准确
 */
void seekdir(DIR *dirp, off_t offset);

示例

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
int main() {
    DIR *dirp = opendir(".");
    if (dirp == NULL) {
        perror("opendir failed");
        return -1;
    }

    struct dirent *entry;
    off_t save_pos; // 存储目录流位置

    // 遍历目录,找到"test.c"后保存位置
    printf("遍历目录,寻找test.c:\n");
    while ((entry = readdir(dirp)) != NULL) {
        printf("%s ", entry->d_name);
        if (strcmp(entry->d_name, "test.c") == 0) {
            save_pos = telldir(dirp); // 保存当前位置(下一个要读的目录项)
            printf("\n找到test.c,保存位置\n");
            break;
        }
    }

    // 继续读完剩余目录项
    printf("继续读取剩余目录项:\n");
    while ((entry = readdir(dirp)) != NULL) {
        if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) {
            printf("%s ", entry->d_name);
        }
    }
    printf("\n");

    // 定位到之前保存的位置,重新读取
    seekdir(dirp, save_pos);
    printf("定位到test.c之后,读取的目录项:\n");
    while ((entry = readdir(dirp)) != NULL) {
        if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) {
            printf("%s ", entry->d_name);
        }
    }
    printf("\n");

    closedir(dirp);
    return 0;
}

文件属性获取:stat 函数

功能

获取指定文件的详细属性(大小、权限、时间戳等),存储到struct stat结构体中。

核心结构体:struct stat

记录文件的完整属性,关键成员解析:

c 复制代码
struct stat {
    dev_t     st_dev;     // 文件所在设备的设备号
    ino_t     st_ino;     // 文件的inode编号
    mode_t    st_mode;    // 文件类型 + 文件权限(核心成员)
    nlink_t   st_nlink;   // 硬链接数量(默认1,创建硬链接时增加)
    uid_t     st_uid;     // 文件所有者的用户ID(如gec的UID可能是1000)
    gid_t     st_gid;     // 文件所属组的组ID
    off_t     st_size;    // 文件大小(字节数,普通文件有效)
    blksize_t st_blksize; // 系统IO块大小(通常4KB)
    blkcnt_t  st_blocks;  // 文件占用的512字节块数量
    // 时间戳(精确到纳秒)
    struct timespec st_atim; // 最后访问时间(如cat文件)
    struct timespec st_mtim; // 最后修改时间(如vim编辑保存)
    struct timespec st_ctim; // 最后状态变更时间(如chmod修改权限)
};

// 时间戳结构体:秒 + 纳秒
struct timespec {
    long tv_sec;  // 秒
    long tv_nsec; // 纳秒(1秒=10^9纳秒)
};

函数

c 复制代码
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
/**
 * 获取文件的属性(跟随符号链接)
 * @param path 目标文件路径(绝对/相对路径)
 * @param buf 指向struct stat的指针,用于存储文件属性
 * @return 成功返回0;失败返回-1,且设置errno
 * @note 若path是符号链接,stat()获取的是链接指向的文件属性
 */
int stat(const char *path, struct stat *buf);

/**
 * 获取文件的属性(不跟随符号链接)
 * @param path 目标文件路径(可是符号链接)
 * @param buf 指向struct stat的指针
 * @return 成功返回0;失败返回-1,且设置errno
 * @note 若path是符号链接,lstat()获取的是符号链接本身的属性
 */
int lstat(const char *path, struct stat *buf);

st_mode 解析:文件类型与权限

st_mode是 32 位整数,高 4 位表示文件类型 ,低 9 位表示文件权限

文件类型判断(用系统宏)

宏定义 功能说明(含对应 d_type、典型场景与编程细节)
S_ISREG(st_mode) 判断是否为普通文件(对应 DT_REG);典型如 .c 源码、.txt 文档、编译后的可执行程序;编程中用于筛选需读写的实际数据文件,需结合 stat/fstat 获取的 st_mode 使用
S_ISDIR(st_mode) 判断是否为目录(对应 DT_DIR);典型如 /home 用户目录、./test_subdir 子目录;核心用于目录递归遍历(如遍历文件夹时,先判断是否为目录再调用 opendir 进入)
S_ISLNK(st_mode) 判断是否为符号链接(对应 DT_LNK);典型如 ln -s src_file link_file 创建的软链接;注意 :默认 stat 会跟随链接获取目标文件属性,需用 lstat 才能获取链接本身的 st_mode
S_ISBLK(st_mode) 判断是否为块设备(对应 DT_BLK);典型如硬盘 /dev/sda、分区 /dev/sda1、U盘 /dev/sdb;常用于磁盘管理工具、驱动程序中识别块设备,以便进行分区、格式化操作
S_ISCHR(st_mode) 判断是否为字符设备(对应 DT_CHR);典型如终端 /dev/tty、键盘 /dev/input/event0、串口 /dev/ttyUSB0;适合在串口通信、终端交互程序中判断字符设备,处理实时字符流数据
S_ISFIFO(st_mode) 判断是否为命名管道(对应 DT_FIFO);典型如 mkfifo my_pipe 创建的管道文件;用于无亲缘关系进程间通信(如 shell 脚本与 C 程序间传递数据),编程中需通过 read/write 操作管道
S_ISSOCK(st_mode) 判断是否为套接字文件(对应 DT_SOCK);典型如 socket 函数创建的本地套接字(如 /var/run/docker.sock)、网络套接字;核心用于网络编程或本地进程间通信(如 AF_UNIX 域套接字场景)

文件权限判断(用系统宏)

权限分为三类:所有者(u)、所属组(g)、其他用户(o),每类 3 位(r 读、w 写、x 执行)。

宏定义 权限说明(主体+操作) 八进制值 核心用途(编程场景) 注意事项(权限依赖/风险)
S_IRUSR 文件所有者(User)拥有读取文件内容的权限 0400 单独使用或与其他宏组合,设置文件所有者的读权限(如 `S_IRUSR S_IWUSR` 表示所有者读写)
S_IWUSR 文件所有者(User)拥有修改文件内容的权限 0200 常用于创建可修改的文件(如配置文件),避免所有者无写入权限导致操作失败 给"其他用户"时风险极高(如 S_IWOTH),需严格控制
S_IXUSR 文件所有者(User)拥有执行文件的权限(仅对程序有效) 0100 编译可执行程序后必设(如 `gcc 编译后,用 S_IRUSR S_IXUSR` 让所有者能读且执行)
S_IRGRP 文件所属组(Group)拥有读取文件内容的权限 0040 多用户协作场景(如团队共享文档),让同组成员可读取但不修改 同"所有者读权限",是组"执行权限"的前提
S_IWGRP 文件所属组(Group)拥有修改文件内容的权限 0020 需同组多人编辑的文件(如项目源码),需谨慎使用,避免误修改 非协作场景建议关闭(如系统程序的组权限)
S_IXGRP 文件所属组(Group)拥有执行文件的权限(仅对程序有效) 0010 同组用户需共同执行的程序(如团队内部工具),需组合 `S_IRGRP S_IXGRP`(读+执行)
S_IROTH 其他用户(Others)拥有读取文件内容的权限 0004 公开可读的文件(如系统配置文件 /etc/profile),或作为"其他用户执行"的前提 常规程序给"其他用户执行"时,必须搭配此权限
S_IWOTH 其他用户(Others)拥有修改文件内容的权限 0002 严禁常规场景使用,仅特殊共享场景(如临时协作文件夹)临时设置,且需配合严格的文件监控 给所有用户写入权限会导致文件被随意篡改、删除,风险极高
S_IXOTH 其他用户(Others)拥有执行文件的权限(仅对程序有效) 0001 系统公共工具(如 /bin/ls),需组合 `S_IROTH S_IXOTH`(读+执行),实现"可执行不可修改"

综合案例:读取目录并输出文件信息

功能:通过命令行参数传入目录路径,读取目录下所有文件,输出文件名、inode 编号、文件类型。

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[]) {
    // 检查命令行参数(需传入目录路径)
    if (argc != 2) {
        fprintf(stderr, "用法:%s <目录路径>\n", argv[0]);
        return -1;
    }
    const char *dir_path = argv[1];

    // 打开目录
    DIR *dirp = opendir(dir_path);
    if (dirp == NULL) {
        perror("opendir failed");
        return -1;
    }

    // 切换到目标目录(确保stat能正确获取属性)
    if (chdir(dir_path) == -1) {
        perror("chdir failed");
        closedir(dirp); // 失败前关闭目录流
        return -1;
    }

    // 循环读取目录项
    struct dirent *dir_entry;
    struct stat file_stat;
    while ((dir_entry = readdir(dirp)) != NULL) {
        // 过滤`.`(当前目录)和`..`(父目录)
        if (strcmp(dir_entry->d_name, ".") == 0 || strcmp(dir_entry->d_name, "..") == 0) {
            continue;
        }

        // 获取文件属性
        if (stat(dir_entry->d_name, &file_stat) == -1) {
            perror("stat failed");
            continue; // 跳过获取失败的文件
        }

        // 解析文件类型
        const char *file_type;
        if (S_ISREG(file_stat.st_mode)) {
            file_type = "普通文件";
        } else if (S_ISDIR(file_stat.st_mode)) {
            file_type = "目录";
        } else if (S_ISLNK(file_stat.st_mode)) {
            file_type = "符号链接";
        } else if (S_ISBLK(file_stat.st_mode)) {
            file_type = "块设备";
        } else if (S_ISCHR(file_stat.st_mode)) {
            file_type = "字符设备";
        } else {
            file_type = "未知类型";
        }

        // 输出文件信息
        printf("inode: %-8ld  类型: %-8s  文件名: %s\n", 
               file_stat.st_ino, file_type, dir_entry->d_name);
    }

    // 检查readdir是否因错误返回NULL
    if (errno != 0) {
        perror("readdir failed");
        closedir(dirp);
        return -1;
    }

    // 关闭目录流(资源释放)
    closedir(dirp);
    return 0;
}

编译与运行

bash 复制代码
# 编译
gcc dir_scan.c -o dir_scan
# 运行(读取当前目录)
./dir_scan .
# 运行(读取/home/gec目录)
./dir_scan /home/gec
相关推荐
大聪明-PLUS6 小时前
关于新的 Linux 内核接口 gpio uapi 的说明
linux·嵌入式·arm·smarc
玉树临风江流儿6 小时前
Linux驱动开发总结速记
linux·运维·驱动开发
cccyi76 小时前
Linux 进程信号机制详解
linux·signal·volatile
gd63213747 小时前
银河麒麟 aarch64 linux 里面的 qt 怎么安装kit
linux·服务器·qt
A-花开堪折7 小时前
Qemu 嵌入式Linux驱动开发
linux·运维·驱动开发
磊灬泽7 小时前
【Linux驱动开发】PWM子系统-servo
linux·运维·算法
郝学胜-神的一滴8 小时前
Linux系统函数stat和lstat详解
linux·运维·服务器·开发语言·c++·程序人生·软件工程
Mr.亮先生9 小时前
常用、高效、实用的 Linux 服务器监控与运维工具清单
linux·运维·服务器
poemyang9 小时前
单线程如何撑起百万连接?I/O多路复用:现代网络架构的基石
linux·rpc·i/o 模式