当前位置: 首页 > news >正文

解码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 对“目录”的封装——操作文件夹即间接调用目录接口
类比对应 类比“街道门牌号”:仅记录文件的精准位置(路径),不承载任何文件内容 类比“实体房子”:仅提供“文件在其中”的直观承载感知,无实际位置定位的底层逻辑

倒置树状文件系统

  • 所有文件 / 目录以根目录/ 为顶层,构成倒置树状结构。

image

  • 根目录下常见系统目录(必须掌握):
    • /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 中的指针直接指向数据区的块。

image

查看文件的 inode 编号

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

# 格式: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

功能

创建指定路径的新目录。

函数

#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

功能

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

函数

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

打开目录:opendir

功能

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

函数

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

关闭目录:closedir

功能

关闭指定目录。

函数

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

切换工作目录:chdir

功能

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

关键说明

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

函数

#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

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

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进一步判断实际类型

函数

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

删除文件 / 空目录:remove

功能

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

函数

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

重置目录流:rewinddir

功能

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

函数

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

获取目录流当前位置:telldir

功能

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

函数

#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返回的指定位置,实现目录的 “随机读取”(而非只能顺序读取)。

函数

#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);

示例

#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

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

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;     // 文件所属组的组IDoff_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纳秒)
};

函数

#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 位表示文件权限

image

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

宏定义 功能说明(含对应 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_IRUSR),单独设置无效
S_IRGRP 文件所属组(Group)拥有读取文件内容的权限 0040 多用户协作场景(如团队共享文档),让同组成员可读取但不修改 同“所有者读权限”,是组“执行权限”的前提
S_IWGRP 文件所属组(Group)拥有修改文件内容的权限 0020 需同组多人编辑的文件(如项目源码),需谨慎使用,避免误修改 非协作场景建议关闭(如系统程序的组权限)
S_IXGRP 文件所属组(Group)拥有执行文件的权限(仅对程序有效) 0010 同组用户需共同执行的程序(如团队内部工具),需组合 S_IRGRP | S_IXGRP(读+执行) 单独设置 S_IXGRP 无意义,会因缺少读权限执行失败
S_IROTH 其他用户(Others)拥有读取文件内容的权限 0004 公开可读的文件(如系统配置文件 /etc/profile),或作为“其他用户执行”的前提 常规程序给“其他用户执行”时,必须搭配此权限
S_IWOTH 其他用户(Others)拥有修改文件内容的权限 0002 严禁常规场景使用,仅特殊共享场景(如临时协作文件夹)临时设置,且需配合严格的文件监控 给所有用户写入权限会导致文件被随意篡改、删除,风险极高
S_IXOTH 其他用户(Others)拥有执行文件的权限(仅对程序有效) 0001 系统公共工具(如 /bin/ls),需组合 S_IROTH | S_IXOTH(读+执行),实现“可执行不可修改” 单独设置无效(缺读权限);
需确保程序本身无安全漏洞

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

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

#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是否因错误返回NULLif (errno != 0) {perror("readdir failed");closedir(dirp);return -1;}// 关闭目录流(资源释放)closedir(dirp);return 0;
}

编译与运行

# 编译
gcc dir_scan.c -o dir_scan
# 运行(读取当前目录)
./dir_scan .
# 运行(读取/home/gec目录)
./dir_scan /home/gec
http://www.hskmm.com/?act=detail&tid=32589

相关文章:

  • p66实验题
  • 【比赛记录】2025NOIP 冲刺模拟赛合集I
  • 20251016
  • C# - 串口助手
  • 使用SpringBoot+MyBatisPlus实现增删改查
  • P4168 [Violet] 蒲公英题解
  • Java了解
  • VGG使用块的网络
  • 使用SpringBoot + Thymeleaf + MyBatisPlus实现一个简单的书籍管理系统
  • Linux 基础
  • P2605 [ZJOI2010] 基站选址
  • NVIDIA Jetson AGX Xavier刷机教程
  • 洛谷p1462-通往奥格瑞码道路
  • 详细介绍:VR 太阳光参数与快速渲染
  • 位运算中没用的小技巧
  • 第六周
  • 超越基础:SightAI 智能路由与多模型选择实战 - sight
  • [Vulhub靶机]JARBAS靶机渗透
  • 10月16号
  • CF622D 题解
  • vue学习的总结
  • 最小二乘问题详解5:非线性最小二乘求解实例
  • AlexNet
  • 【28】C# WinForm入门到精通 ——多文档窗体MDI【属性、强大的方法、实例、源码】【多窗口重叠、水平平铺、垂直平铺、窗体传值】
  • 第五周预习
  • 2025 非标门/铸铝门/别墅大门厂家推荐榜:聚焦品质与服务的实力之选
  • 工业数字化未来:IT与OT融合实践
  • AI安全新威胁:提示注入与模型中毒攻击深度解析
  • 神经网络入门研读报告
  • 阅读《记录一类分治方法》笔记