Linux C 目录流基本操作
简介
目录流是一个抽象的概念,用于表示对目录的访问状态。它类似于文件指针,但专门用于目录操作。目录流通过一个指向 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; // 文件所有者的用户IDgid_t st_gid; // 文件所有者的组IDoff_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