解码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
相关推荐
AZ996ZA8 小时前
自学linux第十八天:【Linux运维实战】系统性能优化与安全加固精要
linux·运维·安全·性能优化
大虾别跑9 小时前
OpenClaw已上线:我的电脑开始自己打工了
linux·ai·openclaw
weixin_4370446410 小时前
Netbox批量添加设备——堆叠设备
linux·网络·python
hhy_smile10 小时前
Ubuntu24.04 环境配置自动脚本
linux·ubuntu·自动化·bash
宴之敖者、10 小时前
Linux——\r,\n和缓冲区
linux·运维·服务器
LuDvei10 小时前
LINUX错误提示函数
linux·运维·服务器
未来可期LJ10 小时前
【Linux 系统】进程间的通信方式
linux·服务器
Abona10 小时前
C语言嵌入式全栈Demo
linux·c语言·面试
Lenyiin11 小时前
Linux 基础IO
java·linux·服务器
The Chosen One98511 小时前
【Linux】深入理解Linux进程(一):PCB结构、Fork创建与状态切换详解
linux·运维·服务器