简介
目录流是一个抽象的概念,用于表示对目录的访问状态。它类似于文件指针,但专门用于目录操作。目录流通过一个指向 DIR 结构的指针来表示,该指针由 opendir() 函数返回。从OS层面理解过程如下所示:
-
打开目录文件:内核会查找目录的路径,找到对应的 inode,并打开这个目录文件。
-
创建目录流结构:内核会为目录流分配一个内部结构(如
DIR结构),并初始化一些状态信息,例如当前读取位置、目录文件的描述符等。 -
返回目录流指针:将目录流结构的指针返回给用户程序。


打开和关闭目录
opendir() 函数
opendir() 函数用于打开一个目录,并返回一个目录流指针。其原型如下:
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
-
参数
name是目录的路径。 -
返回值是一个指向
DIR结构的指针。如果目录成功打开,返回非空指针;如果失败,返回NULL。
其中DIR结构体如下:
struct __dirstream {
void *__fd; // 文件描述符,指向目录文件
char *__data; // 缓存区,存储从目录文件读取的数据
int __entry_data; // 缓存区中有效数据的条目数
char *__ptr; // 当前读取位置的指针
int __entry_ptr; // 当前读取位置的条目索引
size_t __allocation; // 缓存区分配的大小
size_t __size; // 缓存区中实际数据的大小
__libc_lock_define(, __lock) // 锁,用于多线程环境下的同步
};
typedef struct __dirstream DIR;
closedir() 函数
closedir() 函数用于关闭目录流。其原型如下:
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
-
参数
dirp是由opendir()返回的目录流指针。 -
返回值为
0表示成功,返回-1表示失败。
读取目录
当我们打开目录文件获取到目录指针时,就可以通过指针遍历到该目录下的所有目录项。

每个目录项存储了文件的一些基本信息,由 struct dirent 结构体维护,struct dirent 的定义如下:
struct dirent {
ino_t d_ino; // inode 号
off_t d_off; // 当前目录项在目录文件中的位置
unsigned short d_reclen; // 目录项记录长度
unsigned char d_type; // 文件类型(如普通文件、目录等)
char d_name[256]; // 文件名
};
d_type的可选值如下:
DT_BLK This is a block device.
DT_CHR This is a character device.
DT_DIR This is a directory.
DT_FIFO This is a named pipe (FIFO).
DT_LNK This is a symbolic link.
DT_REG This is a regular file.
DT_SOCK This is a UNIX domain socket.
DT_UNKNOWN The file type could not be determined.
readdir() 函数
readdir() 函数用于从目录流中读取目录项。其原型如下:
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
-
参数
dirp是由opendir()返回的目录流指针。 -
返回值是一个指向
struct dirent的指针,该结构包含目录项的信息,如文件名、文件类型等。 -
每次调用
readdir()会返回目录中的下一个目录项,直到返回NULL,表示目录已遍历完毕。
读取当前目录下所有文件信息示例:
#define ERROR_CHECK(ret,num,msg) {if(ret == num){perror(msg); return -1;}}
int main(int argc, char const *argv[])
{
DIR *dirp = opendir("/home/ubuntu/test"); //打开目录
ERROR_CHECK(dirp, NULL, "opendir"); //自定义宏函数,用于输出报错信息
struct dirent * pdirent;
while ((pdirent = readdir(dirp)) != NULL) //读取目录下所有文件信息
{
printf("inode = %ld, reclen = %d, type = %d, name = %s\n", pdirent->d_ino, pdirent->d_reclen, pdirent->d_type, pdirent->d_name);
}
closedir(dirp);
return 0;
}
输出如下结果:
inode = 45629913, reclen = 24, type = 4, name = .
inode = 45683356, reclen = 32, type = 8, name = test2.o
inode = 45673789, reclen = 32, type = 8, name = test3.cpp
inode = 45688999, reclen = 32, type = 8, name = test3
inode = 45648948, reclen = 32, type = 8, name = file1
inode = 45620828, reclen = 32, type = 8, name = a.out
inode = 45670364, reclen = 24, type = 8, name = test
inode = 45683251, reclen = 32, type = 8, name = test2
inode = 45659996, reclen = 32, type = 8, name = text.txt
inode = 45687412, reclen = 32, type = 8, name = test3.c
inode = 45683324, reclen = 32, type = 8, name = test2.c
inode = 45641583, reclen = 32, type = 8, name = test2.s
inode = 45658946, reclen = 32, type = 8, name = 54func.h
inode = 45744512, reclen = 32, type = 4, name = 20250531
inode = 45613058, reclen = 24, type = 4, name = ..
inode = 45663197, reclen = 32, type = 8, name = file.tar.gz
inode = 45633873, reclen = 32, type = 8, name = 1_fopen.c
inode = 45661888, reclen = 32, type = 8, name = 1_fopen
inode = 45655606, reclen = 32, type = 8, name = text1.txt
记录位置
telldir() 函数
telldir 函数是 Linux 系统中用于目录操作的一个函数,它用于获取当前目录流的读取位置。这个位置可以用来后续的目录定位操作,例如通过 seekdir 函数返回到该位置。原型如下:
#include <dirent.h>
long telldir(DIR *dirp);
dirp:指向DIR结构的指针,表示目录流。-
返回值是一个
long类型的值,表示当前目录流的读取位置。 -
如果发生错误,返回值是
-1。
当执行 telldir 获取当前目录流的读取位置。这个位置是一个内部的偏移量,用于标识目录流中当前读取的位置。这个位置可以用于后续的 seekdir 函数调用,从而可以返回到该位置继续读取目录项。
seekdir() 函数
用于设置目录流的读取位置。这个位置通常是通过 telldir 函数获取的偏移量。seekdir 函数允许程序在目录流中跳转到指定的位置,从而可以重新读取目录项或跳过某些目录项。原型如下:
#include <dirent.h>
void seekdir(DIR *dirp, long loc);
-
dirp:指向DIR结构的指针,表示目录流。这个目录流必须是通过opendir函数成功打开的。 -
loc:一个long类型的值,表示目录流中的位置偏移量。这个值通常是通过telldir函数获取的。
通过 seekdir,程序可以在目录流中自由跳转,从而实现以下功能:
-
重新读取某个位置的目录项。
-
跳过某些目录项,直接读取后续的目录项。
注意事项
-
偏移量的有效性:
seekdir函数的偏移量必须是通过telldir函数获取的有效值,注意:这里返回的偏移量LOC必须用 long 类型接收。如果使用无效的偏移量,可能导致未定义行为。 -
目录流的状态:
seekdir函数会更新目录流的内部状态,因此在调用seekdir之后,目录流的读取位置会改变。 -
线程安全性:telldir 和
seekdir函数在多线程环境下是线程安全的,因为每个目录流都有自己的状态信息。

在之前展示目录信息的基础上,我们记录 test2 文件的位置,并在遍历完目录后重新定位到该文件上并输出信息:
#define ERROR_CHECK(ret,num,msg) {if(ret == num){perror(msg); return -1;}}
int main(int argc, char const *argv[])
{
DIR *dirp = opendir("/home/ubuntu/test");
ERROR_CHECK(dirp, NULL, "opendir");
struct dirent * pdirent;
long loc = 0;
while ((pdirent = readdir(dirp)) != NULL)
{
printf("inode = %ld, reclen = %d, type = %d, name = %s\n", pdirent->d_ino, pdirent->d_reclen, pdirent->d_type, pdirent->d_name);
if(strcmp("test2", pdirent->d_name) == 0){
loc = telldir(dirp);
}
}
printf("-----------------\n");
seekdir(dirp, loc);
pdirent = readdir(dirp);
printf("inode = %ld, reclen = %d, type = %d, name = %s\n", pdirent->d_ino, pdirent->d_reclen, pdirent->d_type, pdirent->d_name);
closedir(dirp);
return 0;
}
结果如下:
inode = 45670364, reclen = 24, type = 8, name = test
inode = 45683251, reclen = 32, type = 8, name = test2
inode = 45659996, reclen = 32, type = 8, name = text.txt
inode = 45687412, reclen = 32, type = 8, name = test3.c
inode = 45683324, reclen = 32, type = 8, name = test2.c
inode = 45641583, reclen = 32, type = 8, name = test2.s
inode = 45658946, reclen = 32, type = 8, name = 54func.h
inode = 45744512, reclen = 32, type = 4, name = 20250531
inode = 45613058, reclen = 24, type = 4, name = ..
inode = 45663197, reclen = 32, type = 8, name = file.tar.gz
inode = 45633873, reclen = 32, type = 8, name = 1_fopen.c
inode = 45661888, reclen = 32, type = 8, name = 1_fopen
inode = 45655606, reclen = 32, type = 8, name = text1.txt
-----------------
inode = 45659996, reclen = 32, type = 8, name = text.txt
可以发现指针遍历到了目标文件的后一个文件,这是因为在调用readdir后,当前目录指针会自动向后偏移到下一个目录项,之后在调用telldir获取偏移量时也就指向了下一个目录的位置。所以在使用telldir记录位置时要注意这一点。
rewinddir() 函数
rewinddir 函数是 Linux 系统中用于目录操作的一个函数,它用于将目录流的读取位置重置到目录的开头。这个函数类似于文件操作中的 rewind 函数,但专门用于目录流。
#include <dirent.h>
void rewinddir(DIR *dirp);
-
dirp:指向DIR结构的指针,表示目录流。这个目录流必须是通过opendir函数成功打开的。
将目录流的读取位置重置到目录的开头。这意味着后续调用 readdir 函数时,将从目录的第一个目录项开始读取。
rewinddir 函数通常用于以下场景:
-
当需要重新遍历目录时,可以使用
rewinddir将目录流重置到开头。 -
在多次遍历目录时,避免关闭和重新打开目录流,从而提高效率。
注意事项
-
目录流的有效性:
rewinddir函数要求目录流是通过opendir成功打开的,并且在调用rewinddir时目录流仍然有效。 -
线程安全性:
rewinddir函数在多线程环境下是线程安全的,因为每个目录流都有自己的状态信息。 -
跨平台性:
rewinddir函数在 POSIX 系统中是标准的,但在非 POSIX 系统中可能不可用。
补充:文件状态信息
如果我们想进一步的获取文件的详细信息,我们可以调用 stat 类函数来实现
stat() 函数
stat 函数是一个非常重要的系统调用,用于获取文件的状态信息。它提供了关于文件的详细信息,例如文件类型、大小、权限、所有者等。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
-
pathname:指向一个以空字符结尾的字符串,表示要获取状态信息的文件路径。 -
buf:指向一个struct stat类型的指针,用于存储文件的状态信息。它是一个传入传出参数
stat 函数将文件的状态信息存储在 struct stat 结构体中。该结构体的定义如下:
struct stat {
mode_t st_mode; // 文件类型和权限
ino_t st_ino; // inode 编号
dev_t st_dev; // 设备标识符(文件所在的设备)
dev_t st_rdev; // 设备标识符(针对特殊文件)
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 文件所有者的用户ID
gid_t st_gid; // 文件所有者的组ID
off_t st_size; // 文件大小(以字节为单位)
blksize_t st_blksize; // 文件系统块大小
blkcnt_t st_blocks; // 分配的块数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态改变时间
};
-
st_mode:表示文件类型和权限。可以通过宏(如S_ISDIR、S_ISREG等)来判断文件类型,也可以通过位掩码(如S_IRUSR、S_IWUSR等)来查看权限。每个宏的具体含义如下所示:
S_IFBLK: block device
S_IFCHR: character device
S_IFDIR: directory
S_IFIFO: FIFO/pipe
S_IFLNK: symlink
S_IFREG: regular file
S_IFSOCK: socket
-
st_ino:inode 编号,是文件系统中唯一标识文件的数字。 -
st_dev和st_rdev:分别表示文件所在的设备和特殊文件的设备标识符。 -
st_nlink:硬链接数,表示指向该 inode 的硬链接数量。 -
st_uid和st_gid:分别表示文件所有者的用户ID和组ID。 -
st_size:文件大小,以字节为单位。 -
st_blksize和st_blocks:分别表示文件系统块大小和分配的块数。 -
st_atime``、st_mtime和st_ctime:分别表示文件的最后访问时间、最后修改时间和最后状态改变时间。
-
如果成功,
stat函数返回 0,并将文件的状态信息存储在buf指向的结构体中。 -
如果失败,返回 -1,并设置
errno以指示错误类型。常见的错误包括:-
ENOENT:文件或路径不存在。 -
EACCES:权限不足。 -
EFAULT:buf指针无效。
-
注意事项
-
stat函数获取的是文件的元数据,而不是文件内容本身。 -
如果需要获取符号链接的目标文件的状态信息,可以使用
lstat函数。 -
对于某些文件系统,某些字段(如
st_blksize或st_blocks)可能没有意义。
fstat() 函数
fstat 函数用于获取已打开文件的状态信息。它通过文件描述符来访问文件,而不是通过文件路径。
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, struct stat *buf);
参数说明
-
fd:文件描述符,是通过open函数或其他文件打开操作获得的。 -
buf:指向struct stat类型的指针,用于存储文件的状态信息。
返回值
-
如果成功,返回 0,并将文件的状态信息存储在
buf指向的结构体中。 -
如果失败,返回 -1,并设置
errno以指示错误类型。常见的错误包括:-
EBADF:无效的文件描述符。 -
EFAULT:buf指针无效。
-
注意事项
-
fstat函数只能用于已经打开的文件,因此需要先通过open函数获取文件描述符。 -
fstat不需要文件路径,因此在处理已打开的文件时效率更高。
lstat() 函数
lstat 函数用于获取符号链接的状态信息,而不是符号链接指向的目标文件的状态信息。如果需要获取目标文件的状态信息,应使用 stat 函数。
#include <sys/stat.h>
#include <unistd.h>
int lstat(const char *pathname, struct stat *buf);
参数说明
-
pathname:指向一个以空字符结尾的字符串,表示符号链接的路径。 -
buf:指向struct stat类型的指针,用于存储符号链接的状态信息。
返回值
-
如果成功,返回 0,并将符号链接的状态信息存储在
buf指向的结构体中。 -
如果失败,返回 -1,并设置
errno以指示错误类型。常见的错误包括:-
ENOENT:文件或路径不存在。 -
EACCES:权限不足。 -
EFAULT:buf指针无效。
-
获取文件信息示例:
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
struct stat file_stat;
const char *file_path = "test1.c";
if (stat(file_path, &file_stat) == -1) {
perror("stat");
return 1;
}
printf("File size: %ld bytes\n", file_stat.st_size);
printf("File permissions: %o\n", file_stat.st_mode & 0777);
printf("Number of hard links: %ld\n", file_stat.st_nlink);
printf("Owner UID: %d\n", file_stat.st_uid);
printf("Owner GID: %d\n", file_stat.st_gid);
return 0;
}
输出结果:
File size: 509 bytes
File permissions: 664
Number of hard links: 1
Owner UID: 1000
Owner GID: 1000
实战1:模拟 ls 命令实现遍历目录下所有文件相关信息
当键入 ls 命令时,可以输出当前目录或指定目录的文件信息:
drwxrwxr-x 3 ubuntu ubuntu 4096 7月 9 15:17 .
drwxrwxr-x 3 ubuntu ubuntu 4096 7月 8 22:03 ..
-rwxrwxr-x 1 ubuntu ubuntu 16312 7月 8 22:45 seekdir
-rw-rw-r-- 1 ubuntu ubuntu 875 7月 8 22:45 seekdir.c
-rwxrwxr-x 1 ubuntu ubuntu 16096 7月 9 15:17 stat
-rw-rw-r-- 1 ubuntu ubuntu 535 7月 9 15:17 stat.c
-rwxrwxr-x 1 ubuntu ubuntu 16136 7月 9 15:15 test1
-rw-rw-r-- 1 ubuntu ubuntu 509 7月 8 22:15 test1.c
drwxrwxr-x 2 ubuntu ubuntu 4096 7月 8 22:04 .vscode
在实现过程中,对于文件权限部分建议逐部分按位处理,用户名和组名可以调用getpwuid和getprgid获取,时间戳可以条用time.h中的函数进行转换
实现:
int main(int argc, char const *argv[])
{
ARGS_CHECK(argc, 2);
DIR * dirp = opendir(argv[1]);
ERROR_CHECK(dirp, NULL, "opendir");
struct dirent * pdirent;
char buf[256];
struct stat file_stat;
while ((pdirent = readdir(dirp)) != NULL)
{
sprintf(buf, "%s%s%s", argv[1], "/", pdirent->d_name);
int ret = stat(buf, &file_stat);
ERROR_CHECK(ret, -1, "stat");
switch (file_stat.st_mode & S_IFMT) {
case S_IFBLK: printf("b"); break;
case S_IFCHR: printf("c"); break;
case S_IFDIR: printf("d"); break;
case S_IFIFO: printf("p"); break;
case S_IFLNK: printf("l"); break;
case S_IFREG: printf("-"); break;
case S_IFSOCK: printf("s"); break;
default: printf("?"); break;
}
for(int i = 2; i >= 0; i--){
switch((file_stat.st_mode & (7 << (i * 3))) >> (i * 3)){
case 0: printf("---"); break;
case 1: printf("--x"); break;
case 2: printf("-w-"); break;
case 3: printf("-wx"); break;
case 4: printf("r--"); break;
case 5: printf("r-x"); break;
case 6: printf("rw-"); break;
case 7: printf("rwx"); break;
default: break;
}
}
struct passwd * pwd = getpwuid(file_stat.st_uid);
struct group * grp = getgrgid(file_stat.st_gid);
struct tm *local_time = localtime(&file_stat.st_mtime);
char time_str[80];
strftime(time_str, sizeof(time_str), "%b %d %H:%m", local_time);
printf(" %ld %s %s %ld %s %s\n",file_stat.st_nlink
,pwd->pw_name
,grp->gr_name
,file_stat.st_size
,time_str
,pdirent->d_name);
}
closedir(dirp);
return 0;
}
实现效果:

实战2:模拟tree命令列出指定目录的层次结构
int tree(const char *path, int level){
DIR * dirp = opendir(path);
ERROR_CHECK(dirp, NULL, "opendir");
struct dirent * pdirent;
while((pdirent = readdir(dirp)) != NULL){
for(int i = 0; i < level; i++) printf(" ");
printf("%s\n",pdirent->d_name);
if(pdirent->d_type == DT_DIR && strcmp(pdirent->d_name,".") != 0 && strcmp(pdirent->d_name,"..") != 0){
char newpath[1024];
sprintf(newpath,"%s%s%s",path,"/",pdirent->d_name);
tree(newpath, level+1);
}
}
closedir(dirp);
}
int main(int argc, char const *argv[])
{
ARGS_CHECK(argc, 2);
tree(argv[1], 0);
return 0;
}
实现效果:
(base) ubuntu@ubuntu:~/MyProject/Linux/dir$ ./tree .
.
stat
ls.c
stat.c
testdir
.
hello
..
.vscode
.
settings.json
..
test1.c
test1
..
tree
ls
seekdir.c
tree.c
seekdir
484

被折叠的 条评论
为什么被折叠?



