目录检索的核心需求
当需要批量访问某个路径下的多个文件时,手动调用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
:存放系统基础命令(如ls
、cd
)/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
显示详细信息,可组合使用):
# 格式: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 位表示文件权限。
文件类型判断(用系统宏)
宏定义 | 功能说明(含对应 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