V4.0
王道C++班级参考资料<br />——<br />Linux部分卷3文件系统编程<br/>节2目录流<br/><br/>最新版本V4.0
<br>王道C++团队<br/>COPYRIGHT ⓒ 2021-2024. 王道版权所有系统编程简介从文件系统编程开始如何学习呢?目录相关系统调用改变文件的权限扩展:如何查看类型别名的具体定义获取当前工作目录改变当前工作目录创建目录删除目录目录相关POSIX库函数/目录流打开目录流关闭目录流读目录流dirent结构体扩展:如何查询结构体类型的具体定义破产版ls指令移动目录流指针的位置seekdir和telldir函数倒带目录流实现无排序的ls -al指令stat系统调用函数path参数的问题buf参数的问题stat结构体(重点)实现青春版ls -al指令处理文件类型和权限处理拥有者名和组名处理最后修改时间戳最终实现扩展:实现排序功能实现青春版tree指令The End
Gn!
我们仍然回到Linux课程的开端,在那里我们详细讲解了Linux系统的结构设计,如下图所示:
我们已经学习了利用shell命令来和内核进行交互,在C阶段我们也学习了大量的标准库函数,也可以实现与内核的交互。
那么从这节课开始,我们就学习一种直接与内核交互的方式——Linux系统调用。
稍微要注意的是:
标准库函数遵循C标准规范(ISO-C),任何使用标准C语言的平台都遵循这一规范,虽然不同编译器和平台的实现细节可能有所不同,但整体上是能够保证跨平台性的。
Linux的系统调用一般遵循POSIX规范,这意味着Linux系统调用最多在类Unix平台上实现跨平台性(实际上还是有些兼容性问题)。总之基本可以认为Linux系统调用就只能在Linux内核上使用。关于这个问题,我们下面还会进一步详细讲解。
Gn!
Linux系统编程主要可以分成三大部分:文件系统编程,进程和线程管理以及网络系统编程。
我们在学习的时候,学习的顺序也是这样的, 那么为什么要这么学习呢?
类Unix系统的设计哲学中,"一切皆文件" 是一个核心概念。
Linux系统下,把一切外部设备、进程线程资源以及网络通信等都视为文件,都可以使用类似文件操作的方式来进行访问和管理。
先学习文件系统编程有利于后续的学习,可以作为后续系统编程学习的基础。
文件系统编程直接操作各自文件,最简单且最直观。
实用性强,文件系统编程的操作,如文件的创建、读写、权限修改等,是系统编程中最常见的操作。
总之,我们就从现在开始来学习Linux文件系统编程。
Gn!
很多人可能听到系统编程四个字,就下意识的觉得会很难,实际上这是一个错误的认知。
虽然学习系统的调用的过程,会有很多模型、概念需要理解记忆,但最核心,展现在代码上的还是——函数,C语言的系统调用函数。
Linux的系统调用虽然是在内核中实现的,但还是会通过C语言风格的函数,将接口暴露给用户空间的程序去使用。
所以在学习系统调用的过程中,我们会涉及大量的C语言系统调用函数,那么应该如何学习这么多的系统调用函数呢?
答:记住系统调用的函数名,大致记住功能,在忘记的时候查询
man
手册寻找自己想要的答案。系统调用的C语言函数,都放置在man手册的2号手册中,我们可以通过指令查询函数的使用方式,比如:
xxxxxxxxxx
11man 2 chmod
如何通过man手册查看一个系统调用函数如何使用呢?遵循以下步骤:
第一步:学习系统调用函数以及标准库函数,总是需要先记住函数的名字,否则就无法打开man手册了
第二步:使用指令打开man手册,在很多时候这需要你指定man手册的卷数。系统调用函数在第2卷,库函数在第3卷。
第三步:当你打开一个函数的man手册时,内容查看的顺序是:
先看NAME,NAME块描述了函数的基本信息,描述了函数的基本作用。
再看概述(SYNOPSIS)信息:
先看头文件,要知道使用此函数要包含什么头文件
接下来看函数的声明
怎么看函数的声明呢?
应该先看函数的名字,这是基本的
然后看函数的返回值:
因为C语言缺乏错误的检查机制,所以函数执行出错都需要依赖返回值来确定,返回值是什么决定了函数应该如何进行错误处理,这是非常重要的。比如:
返回值类型若是int或者
ssize_t(跨平台的有符号整数)
,普遍来说如果此函数返回-1,就表示发生了错误。(尤其返回值是ssize_t时)返回值类型若是一个指针类型,普遍上如果此函数返回NULL,就表示发生了错误。
在一众返回值中,我们尤其要特别注意指针类型的返回值:
返回值是指针类型,绝不可能返回此函数栈区的数据,因为这样的指针是一个悬空指针,不可能作为返回值。
返回值是指针类型代表申请了栈区外的额外内存空间或者资源,这往往意味着需要手动管理/关闭这些额外的内存空间或资源(但也不是必然)。
看完函数的返回值,可以紧跟着看函数的形参列表,在一众形参列表中我们也需要特别重视指针类型的形参,尤其关注它们有没有被
const
修饰:
被
const
修饰的指针类型参数,意味着指针指向的内容,绝不会在函数体内部被修改。我们把这种指针类型参数称之为"传入参数",单纯的传入,不会修改指向内容。没有被
const
修饰的指针类型参数,一般就意味着函数要通过传入的指针修改指向的内容,并且往往会在修改内容后再将原指针作为返回值返回,这种指针类型参数称之为"传入传出参数"。传入传出参数在传参时要特别小心空指针、野指针问题!!这里举一个例子:
xxxxxxxxxx
21int chmod(const char *pathname, mode_t mode); // 传入参数pathname
2char *getcwd(char *buf, size_t size); // 传入传出参数buf
比如我们这样调用getcwd函数:
xxxxxxxxxx
21char *p;
2getcwd(p, 128);
这样的调用显然会导致未定义行为:
p是一个野指针,指向随机内存位置。若这个随机位置是不可访问区域,将导致程序报错崩溃,这其实是一件好事。
如果随机指向了一个可以访问修改的位置,发生了错误的数据修改,甚至程序还能短暂正常运行,那就太糟糕了。(这在C语言中其实稀松平常,所以C语言很坑)
C语言是一种非常不安全的语言,要想避免这种错误就需要程序自己的细心细致了,要确保指针指向的区域是已分配的。比如:
xxxxxxxxxx
21char p[100]; // 栈上分配数组
2char *p = malloc(100); // 堆上分配空间
总之,要想写好C语言代码,成为优秀的C语言开发,需要注意的事情非常多,要仔细一些。
Gn!
在C语言阶段,我们已经学习过文件处理相关的库函数了,现在我们就把目录处理相关的操作补上。注意:此小节我们将学习目录相关的系统调用,目录相关的库函数将在下一小节学习。
关于C语言的系统调用函数、标准库函数以及POSIX库函数的关系
操作系统内核为了实现对外开放系统功能,会主动对外暴露系统调用接口,这对于任何操作系统内核来说都是一样的。
而类Unix操作系统的设计与实现都深植根于 C 语言(C语言诞生的初衷就是为了写Unix系统),类 Unix 系统的系统调用都是以C语言风格暴露的,而且这些系统调用函数一般都遵循POSIX标准。
实际上,C语言由于其接近硬件的特性和高效率,在操作系统开发中被广泛使用,包括Windows和MacOS在内的系统内核在对外暴露系统功能时都会选择C语言风格。
总之:
C语言的系统调用函数显然是不具有跨平台性,它植根于特定的系统内核,不同的系统内核会有不同的暴露方式
C语言的系统调用函数也不是C语言标准的一部分,不属于标准库函数。
理论上来说,所有的类Unix系统都遵循POSIX标准,那么它们的系统调用函数应该是一致的。但实际上不同的类Unix系统内核,在系统调用上还是会有一些细微的差异。
一般来说,我们基于Linux内核学习系统调用,就意味着开发出来的系统调用代码仅能运行于Linux平台上。
但:
C语言的标准库函数显然不是这样的,标准库函数意味着遵循特定C语言规范(ISO-C)。由于标准库函数很多是对系统调用的封装,这使得标准库函数在不同平台上的实现方式是不同的,也就是实现的细节不同。
但从程序员的角度看,它们提供了一致的接口和行为。
所以:
标准库函数具有较好跨平台性,基本保证在任何遵循标准C语言规范的平台上,它们的调用方式和行为是一致的。
标准库函数可以在任何支持 C 标准的编译器和环境中使用,不受底层操作系统的限制。
那么最后,什么是POSIX库函数呢?
标准库函数指的是符合ISO-C官方C语言标准的库函数,这意味着它们在使用标准C语言的任意平台下使用起来都是一样的,具有最好的跨平台性。Windows、Linux、mac等平台下基本都是一样,使用起来没什么差异。
但POSIX库函数,指的是符合POSIX标准的C语言库函数。
POSIX标准一般所有的类Unix系统都遵守,所以你可以认为POSIX标准的库函数可以在类Unix系统平台中实现跨平台性。但非类Unix系统,比如Windows,那一般就不能直接移植代码了。
但POSIX和ISO-C库函数仍然具有相同点:同为库函数,它们都不是系统调用,但都有可能利用系统调用来实现功能。
Gn!
注意,从这里开始每学习一个新的函数,你都需要将此函数的头文件包含(此前没有写过的)加到公共头文件中!
在以往我们已经学过了shell指令当中,用于改变文件权限的一个shell指令:chmod
在C语言代码也是一样的,也可以使用同名函数chmod来改变文件的权限。需要注意的是:在Linux环境下,
chmod
既是一个系统调用的函数名,也是一个POSIX库函数名。当我们在C语言代码中调用
chmod
函数时,调用的是库函数chmod,当然此库函数的底层实现依赖于系统调用函数chmod。所以chmod这个字符序列,在man手册的1、2、3号三个手册中都存在。
它的函数声明如下:
xxxxxxxxxx
21// 使用该函数需要包含的头文件
2int chmod(const char *pathname, mode_t mode);
形式参数:
pathname
:文件或目录的路径字符串。
mode
:要设置的新权限,这里要使用权限数字表示法,即八进制数(C语言中八进制整数需要以"0"开头)。返回值:
成功时,
chmod
返回 0。失败时,返回 -1,并设置 errno 以指示错误原因。
一个使用示例:
xxxxxxxxxx
101/* Useage: ./01_chmod pathname mode */
2int main(int argc, char *argv[]){
3ARGS_CHECK(argc, 3);
4mode_t mode;
5sscanf(argv[2], "%o", &mode ); // %o表示八进制无符号数
6
7int ret = chmod(argv[1], mode);
8ERROR_CHECK(ret, -1, "chmod");
9return 0;
10}
需要注意的是:设置权限时需要传参权限的"数字表示法",而且设定的权限就是文件的最终权限,掩码只会影响新建文件,不会影响chmod函数。
Gn!
在上面的chmod函数中,我们看到了一个类型别名:
mode_t
,那么如何确定这个类型别名的具体类型呢?有以下两种办法:
查阅预处理后的.i文件:
类型别名的具体定义肯定是包含在头文件里的,如果直接查看头文件中此别名的定义,可能会受到条件编译等预处理指令的干扰,所以最好的办法是查看预处理文件。
第一步可以随意创建一个.c源文件,只要确保包含目标类型别名的头文件即可,比如要查看类型别名
mode_t
,就需要确保包含:xxxxxxxxxx
11然后直接利用指令:
xxxxxxxxxx
11gcc -E main.c -o main.i
生成对应的预处理.i文件。
然后可以选择使用vim编辑器进入这个.i文件,然后使用vim编辑器查询即可,比如我们查到
mode_t
类型别名的定义如下:或者也可以使用
grep
指令搜索这个.i文件,指令如下:xxxxxxxxxx
11grep -nE "typedef.*mode_t" main.i
于是我们就知道,在当前机器下,mode_t类型别名实际上就是无符号的int类型。
第二种方式,通过代码计算推导:
对于类型别名而言,大多数情况下,别名的具体类型都是有符号整数或无符号整数,我们可以通过下列代码来确定它们的长度以及有无符号:
xxxxxxxxxx
131int main(void){
2size_t size = sizeof(mode_t);
3printf("size of mode_t = %zu bytes\n", size); // %zu是专门提供给size_t类型进行格式化的转换说明
4
5if((mode_t)-1 > 0){
6// 无符号数的-1会溢出成为一个正数
7printf("mode_t是无符号的!\n");
8}else{
9// 有符号数的-1就是一个负数
10printf("mode_t是有符号的!\n");
11}
12return 0;
13}
以上
Gn!
getcwd
函数 即"get current working directory",它是一个POSIX标准的C语言库函数,用于获取当前工作目录的绝对路径名称。这个库函数的作用,很类似于
pwd
这个shell指令。其声明如下:
xxxxxxxxxx
21// 需要包含此头文件调用函数
2char *getcwd(char *buf, size_t size);
形式参数:
buf: 指向存放当前工作目录字符串的字符数组
size: 这个数组的大小
返回值:
成功时,
getcwd
返回一个指向buf
的指针,buf
中包含了当前工作目录的绝对路径。失败时,返回
NULL
,并设置errno
以指示错误的原因。出错的原因普遍是buf数组过小,无法容纳整个工作目录绝对路径字符串。一个演示示例如下:
xxxxxxxxxx
131/* Usage: ./02_getcwd */
2int main(void){
3// 方式1:直接用栈数组作为getcwd返回值数组
4char path[1024] = {0};
5char *p = getcwd(path,sizeof(path));
6ERROR_CHECK(p,NULL,"getcwd");
7printf("cwd = %s\n", path);
8
9// 方式2:动态分配一个堆数组
10// char *path = (char *)malloc(1024);
11// char *p = getcwd(path,1024);
12// free(path);
13}
注意:
getcwd
不负责管理传入函数的buf空间的生命周期,buf生命周期的管理是程序员的职责。分配空间不要过于抠门,内存空间在当前的开发环境下一般不是稀缺资源。如果分配空间较少,那么会导致报错。
getcwd
函数还有一种调用方式,如下:xxxxxxxxxx
91int main(void) {
2char* cwd = getcwd(NULL, 0);
3ERROR_CHECK(cwd, NULL, "getcwd");
4
5puts(cwd);
6// 由调用者负责释放申请的内存空间
7free(cwd);
8return 0;
9}
注意:
这种调用方式意味着,由getcwd函数来动态分配堆空间,存储当前工作目录字符串,然后返回。
在这种情况下,getcwp仍然不负责管理它申请的空间,所以需要你自己手动free释放,否则会导致内存泄漏。
Gn!
chdir
函数是一个Linux系统调用函数,它的作用是改变当前工作目录。"chdir" 代表 "change directory",即改变(当前工作)目录。该函数的声明如下:
xxxxxxxxxx
212int chdir(const char *path);
形式参数:
path
: 是一个指向字符数组的指针,这个数组包含了新工作目录的路径。路径可以是绝对的(从根目录开始,例如 "/usr/local/bin")或相对的(相对于当前工作目录,例如 "../docs")。
返回值:
成功时,
chdir
返回 0。失败时,返回 -1,并设置全局变量
errno
以指示错误的原因。比较常见的错误,比如目标目录不存在、权限问题、目标不是目录等。一个使用的示例如下:
xxxxxxxxxx
231/* Usage: ./03_chdir pathname */
2int main(int argc, char* argv[]) {
3ARGS_CHECK(argc, 2); // 命令行参数必须2个,第二个是要切换的目标目录
4
5// 先获取当前工作目录,然后打印
6char buf[1024] = {0};
7char *ret = getcwd(buf, sizeof(buf));
8ERROR_CHECK(ret, NULL, "getcwd");
9printf("一开始的工作目录是: ");
10puts(buf);
11
12// 改变当前工作目录
13int ret2 = chdir(argv[1]);
14ERROR_CHECK(ret2, -1, "chdir");
15
16// 再次打印当前工作目录
17char *ret3 = getcwd(buf, sizeof(buf));
18ERROR_CHECK(ret3, NULL, "getcwd");
19printf("chdir后的工作目录是: ");
20puts(buf);
21
22return 0;
23}
注意:
当前工作目录是进程的属性,也就是说每一个进程都有自己的当前工作目录。而且父进程创建子进程的时候,子进程会继承父进程的当前工作目录。
这里,我们有一个执行的进程是
shell/bash
进程,它是一个父进程,执行可执行程序就会创建一个子进程,子进程在一开始继承父进程的当前工作目录。然后在这个子进程中将工作目录修改了,并不会影响父进程的工作目录。这个过程有点类似于值传递,如下图所示:
所以很明显,
chdir
函数并不等同于cd
指令,cd这个shell指令可以修改shell/bash进程的当前工作目录。那么cd指令是什么原理呢?
这是因为cd并不是一个独立于bash程序的外部可执行程序,它是bash这个程序的一部分,是bash/shell的内部指令。这个cd指令的作用就是在进程内部,自己修改自己进程的当前工作目录。
这也是
which cd
找不到可执行程序cd的原因,因为它不是一个独立的可执行程序,只是bash这个可执行程序的一部分。这里涉及到了一些进程的知识,大家先了解一下,后续进程课程会详细了解。
Gn!
mkdir
(make directory)系统调用函数用于创建一个新目录。函数声明如下:
xxxxxxxxxx
3123int mkdir(const char *pathname, mode_t mode);
形式参数:
pathname
: 要创建的目录的路径名。可以是绝对路径也可以是相对路径名。
mode
: 设置新目录的权限。这是一个八进制数,类似于chmod
的权限设置(例如,0775
)。注意,最终的权限还会受到进程的 umask 设置的影响。返回值:
成功时,
mkdir
返回 0。失败时,返回 -1,并设置
errno
以指示错误的原因。常见错误比如目录已存在等。一个演示的代码如下:
xxxxxxxxxx
91/* Usage: ./04_mkdir pathname mode */
2int main(int argc, char* argv[]) {
3ARGS_CHECK(argc, 3); // 需要三个命令行参数,允许传入一个三位八进制数表示权限
4mode_t mode;
5sscanf(argv[2], "%o", &mode); // 将第三个命令行参数字符串转换成八进制无符号整数
6int ret = mkdir(argv[1], mode);
7ERROR_CHECK(ret, -1, "mkdir");
8return 0;
9}
注意:
新建一个目录设定权限为
777
,你可能会发现最终生成的目录权限为775
这是因为文件掩码
umask
的影响文件掩码
umask
是一个三位的八进制数,用于在创建新文件或者目录时移除一些权限。比如umask若是
0002
,指定权限为777
,最终结果就会是775
,相当于"新权限 - 掩码"才得到新权限可以通过指令
umask
来查看当前掩码值,也可以用umask n
来设定一个新的掩码值。掩码的存在是为了系统安全,避免新文件目录拥有过多的权限。较低的
umask
值会导致更开放的权限,而较高的umask
值会导致更严格的权限。更加详细的关于umask文件权限掩码的概念和计算,可以参考之前的文档:文件权限掩码
Gn!
rmdir
(remove directory)系统调用函数用于删除一个空目录,注意只能删除空目录。函数声明如下:
xxxxxxxxxx
212int rmdir(const char *pathname);
参数:
pathname
: 要删除的空目录的路径名。返回值:
成功时,
rmdir
返回 0。失败时,返回 -1,并设置
errno
以指示错误的原因。常见的错误原因有目录非空,不存在或无权限等。一个演示示例如下:
xxxxxxxxxx
81/* Usage: ./05_rmdir pathname */
2int main(int argc, char* argv[]) {
3ARGS_CHECK(argc, 2);
4
5int ret = rmdir(argv[1]); // 注意:rmdir 只能删除空目录
6ERROR_CHECK(ret, -1, "rmdir");
7return 0;
8}
该函数比较简单,没有什么值得注意的。
Gn!
在上面我们已经学会了目录的初级操作:创建、删除等。那么这一小节,我们将使用目录流,来查看目录中的内容。
在具体讲解之前,我们还是要解释一下标题:
我们这里使用的目录流是符合POSIX标准的C语言库函数,这意味着它不能直接用于Windows系统,只能类Unix系统平台上使用。
首先,既然是目录流,那我们就需要回忆一下流的模型,类似水流或者流水线:
而且我们已经知道,目录中存储的是子目录或文件的目录项,它们近似以链表的形式链接。
那么一个目录流就可以用下图来理解:
目录流指针,完全可以和文件流指针做类似的理解,它一开始指向目录文件的第一个目录项,并且可以继续向后移动。
目录流和文件流的API对比如下,你可以发现它们非常的类型:
目录流 VS 文件流
文件流 目录流 fopen opendir fclose closedir fread readdir fwrite × ftell telldir fseek seekdir rewind rewinddir 但还是要再强调一下,Linux目录流是符合POSIX标准的API,一般仅兼容类Unix平台,在Windows平台下都是不可以直接使用的。
注意:目录流是没有写操作的,只能读。这是因为如果开放写权限非常容易破坏文件系统的目录结构,只提供读是为了保障文件系统的安全。
Gn!
opendir函数,用于打开一个目录流以读取其中的目录项。
当你使用
opendir
打开一个目录时,系统会创建一个目录流,并返回一个指向该流的DIR
指针。该目录流指针一开始指向目录项第一个,然后程序可以通过函数来移动这个目录项指针,从而实现对目录的遍历和管理。
从使用上来看,它的特点非常类似于文件流的
fopen
函数以及FILE
指针,只不过文件流是"ISO-C"官方标准C语言的库函数,而我们今天讲的目录流是POSIX标准的库函数,不是官方C标准库函数。该函数的声明如下:
xxxxxxxxxx
31// dirent是directory entry的简写,就是目录项的意思
23DIR *opendir(const char *name);
形式参数:
name
: 字符串,代表要打开的目录的路径。返回值:
成功时,返回指向
DIR
类型的指针,即目录流的指针失败时,返回
NULL
并设置errno
以指示错误的原因。常见的失败原因,比如目录没有权限打开、目录不存在等。
Gn!
既然可以打开目录流,那么就可以关闭目录流。这样做可以释放分配给目录流的资源,避免内存泄漏。
函数声明如下:
xxxxxxxxxx
3123int closedir(DIR *dirp);
形式参数:
dirp
: 由opendir
返回的目录流指针。返回值:
成功:返回0
失败时,返回
-1
并设置errno
以指示错误的原因。常见的错误原因就是乱传指针,传入的指针不是打开的目录流指针。综合打开目录流和关闭目录流,我们可以写出一个目录流操作的标准格式写法:
xxxxxxxxxx
121/* Usage: ./06_dirent pathname */
2int main(int argc, char *argv[]){
3// 命令行参数校验
4ARGS_CHECK(argc,2);
5DIR *dirp = opendir(argv[1]);
6// 打开目录流的错误处理
7ERROR_CHECK(dirp,NULL,"opendir");
8// 读目录流的操作
9// 及时释放资源,关闭目录流
10closedir(dirp);
11return 0;
12}
这一整套先打开,最后关闭,中间进行目录流操作的流程完全类似于我们之前学过的文件流,可以视为目录流操作的一套惯用法。
Gn!
当你使用
opendir
打开一个目录流后,可以使用readdir
依次读取目录中的每个条目,直到目录中没有更多条目为止。其函数的声明如下:
xxxxxxxxxx
212struct dirent *readdir(DIR *dirp);
形式参数:
dirp
: 由opendir
返回的目录流指针。返回值:
返回值: 成功时,返回指向
struct dirent
结构体对象的指针,这个结构体当中包含了目录下文件和子目录的信息(如文件名等)。当目录中没有更多条目时返回
NULL
。当函数出错时该函数也会返回NULL。但只要传给函数的指针是一个打开的目录流指针,该函数一般不会出错,所以可以把返回NULL作为读完目录流的标记,而不需要做错误处理。
聪明的同学,已经在思考一个问题了:
该函数返回一个指向结构体对象的指针,说明函数内部肯定分配了额外的内存空间,那么这片空间需不需要程序员来手动释放呢?
不需要,在man手册中,官方开发者明确提出了:返回值指针指向的结构体对象是静态分配的内存,这意味着这个结构体对象的存储方式是由系统管理的,而不需要由程序员手动管理。
man手册的原文如下:
On success, readdir() returns a pointer to a dirent struc‐ture. (This structure may be statically allocated; do not attempt to free(3) it.)
实际上只需要使用closedir函数关闭目录流,目录项相应的结构体对象就会被释放销毁,不需要再手动free这个结构体了。
Gn!
要想真正理解和掌握
opendir
函数,最需要理解的就是dirent结构体,这个结构体到底是什么呢?首先dirent不是一个单词,它是词组
directory entry
的缩写,也就是目录项的意思。所以dirent结构体,就是当前目录下的某个文件/子目录的目录项结构体,这个结构体中会存储当前目录下的某个文件/子目录的信息。通过查询
man
手册,我们知道dirent结构体的定义如下:xxxxxxxxxx
81// dirent是directory entry的简写,就是目录项的意思
2struct dirent {
3ino_t d_ino; // 此目录项的inode编号,目录项中会存储文件的inode编号。一般是一个64位无符号整数(64位平台)
4off_t d_off; // 到下一个目录项的偏移量。可以视为指向下一个目录项的指针(近似可以看成链表),一般是一个64位有符号整数
5unsigned short d_reclen; // 此目录项的实际大小长度,以字节为单位(注意不是目录项所表示文件的大小,也不是目录项结构体的大小)
6unsigned char d_type; // 目录项所表示文件的类型,用不同的整数来表示不同的文件类型
7char d_name[256]; // 目录项所表示文件的名字,该字段一般决定了目录项的实际大小。也就是说文件名越长,目录项就越大
8};
其中文件类型
d_type
的可选值如下(使用宏常量定义的整数):xxxxxxxxxx
81DT_BLK // 块设备文件,对应整数值6
2DT_CHR // 字符设备文件,对应整数值2
3DT_DIR // 目录文件,对应整数值4
4DT_FIFO // 有名管道文件,对应整数值1
5DT_LNK // 符号链接文件,对应整数值10
6DT_REG // 普通文件,对应整数值8
7DT_SOCK // 套接字文件,对应整数值12
8DT_UNKNOWN // 未知类型文件,对应整数值0
所以我们这么理解
readdir
函数:xxxxxxxxxx
11struct dirent *readdir(DIR *dirp);
readdir
函数需要传入一个目录流指针,用于从打开的目录流中读取下一个目录项,并返回一个存储该目录项数据的struct dirent
结构体指针。这个结构体对象由
readdir
函数在内部申请空间创建,也由目录流库自动管理生命周期,程序不需要考虑它的内存管理问题。
Gn!
上述代码中出现了一个结构体类型,那么如何查询结构体类型的具体定义呢?
除了查阅man手册外,我们也可以直接自己搜索预处理后的.i文件,如下图所示:
当然你也可以通过grep指令来查询:
xxxxxxxxxx
11grep -nEC10 "struct dirent" main.i
其中
-C10
选项表示显示匹配行及其周围的10行。包括这些类型的别名,也都可以用这种方式来查询:
以上。
Gn!
以上,我们就可以写出一个用于打印目录下所有文件的:inode编号、目录项大小、文件类型、文件名信息的类似
ls
的工具(纯破产版,完全不一样好吧)。参考代码如下:
xxxxxxxxxx
191/* Usage: ./06_dirent pathname */
2int main(int argc, char *argv[]){
3ARGS_CHECK(argc,2);
4DIR * dirp = opendir(argv[1]);
5// 打开目录流需要进行错误处理
6ERROR_CHECK(dirp,NULL,"opendir");
7struct dirent * pdirent;
8// 循环读目录项并打印目录信息,循环结束的条件是返回值为NULL
9while((pdirent = readdir(dirp)) != NULL){
10printf("inode num = %lu, reclen = %hu, type = %u, name = %s\n",
11pdirent->d_ino, // 64位平台这个inode编号一般是一个64位无符号整数
12pdirent->d_reclen, // 无符号短整型
13pdirent->d_type, // 无符号整型
14pdirent->d_name); // 以字符串类型打印
15}
16// 及时释放资源,关闭目录流
17closedir(dirp);
18return 0;
19}
这段代码你应该非常熟悉,因为我们在使用文件流读文件内容时,也是一模一样的操作!实际上它们的原理也就是差不多的!
Gn!
和文件流FILE指针视为指示文件当前读写位置的文件指针一样,目录流的DIR指针也用于指示当前读写目录项的位置。
在上面的代码中,DIR指针遍历完了所有的目录项,也就是该指针指向了目录文件的末尾,那么有没有办法将这个指针再移动回去或者重置它么?
当然可以,和文件流类似,这里需要涉及三个函数:
seekdir
、telldir
、rewinddir
下面我们先来看一下这三个函数的作用,再来具体讲解一下其中的细节问题。
Gn!
seekdir
函数用于设置目录流指针的当前位置,但和文件流的fseek
函数不同的是:该函数没有参照点的概念,只能通过telldir
函数提供的返回值来进行指针的移动。即:必须先用
telldir
函数记录指针的位置,然后才能够使用seekdir
返回记录的位置!
telldir
函数的声明如下:xxxxxxxxxx
212long telldir(DIR *dirp);
形式参数:
dirp
是由opendir
返回的目录流指针。返回值:
返回值是目录流指针当前读取位置的标记,是一个长整型值。该返回值需要给
seekdir
函数使用,一般不作其它用途。该函数如果出错,会返回-1。但只要正确传递已打开的目录流指针,该函数一般不会出错,所以一般不做错误处理。
seekdir
函数的声明如下:xxxxxxxxxx
212void seekdir(DIR *dirp, long loc);
形式参数:
dirp
是由opendir
返回的目录流指针。
loc
是由telldir
返回的位置标记。返回值:
该函数没有返回值,这说明函数的设计者认为该函数不需要错误处理。
该函数必须传入由telldir函数获取的位置,在移动指针的过程中是纯粹的内存操作,也不涉及内存数据的读写操作,本身是非常安全的,不会出错,也确实不需要错误处理。
演示代码如下:
xxxxxxxxxx
301/* Usage: ./06_dirent pathname */
2int main(int argc, char *argv[]){
3ARGS_CHECK(argc,2);
4DIR * dirp = opendir(argv[1]);
5// 检查打开目录流是否成功
6ERROR_CHECK(dirp,NULL,"opendir");
7struct dirent * pdirent;
8// 用于记录目录流指针的位置
9long loc;
10while((pdirent = readdir(dirp)) != NULL){
11printf("inode num = %lu, reclen = %hu, type = %u, name = %s\n\n",
12pdirent->d_ino, pdirent->d_reclen,
13pdirent->d_type,pdirent->d_name);
14if(strcmp("file1",pdirent->d_name) == 0){
15// 如果当前的目录项表示文件的名字是file1,那么记录文件指针的位置
16loc = telldir(dirp);
17}
18}
19printf("--------------------------\n"); // 一个代码分隔线
20
21seekdir(dirp,loc); // 目录流指针回到记录位置
22// 获取目录流指针指向的目录项结构体, 并打印信息
23pdirent = readdir(dirp);
24printf("inode num = %lu, reclen = %hu, type = %u, name = %s\n\n",
25pdirent->d_ino, pdirent->d_reclen,
26pdirent->d_type,pdirent->d_name);
27// 不要忘记关闭流
28closedir(dirp);
29return 0;
30}
按照上面代码的思路,我们记录了
file1
目录项的位置,然后在下面回到这个位置,那么打印的结果是什么呢?打印的结果却不是
file1
,而是.
,这是为什么呢?这其实是由目录流指针
DIR*
的行为引起的:readdir 函数在读取当前目录项并返回dirent结构体指针后,会自动移动到目录流中的下一个条目。这意味着每次调用 readdir 后,目录流的内部指针都会向前移动到下一个条目。
你可以参考下图来理解这个原理:
所以,对于上图的目录项来说:
如果希望记录
.
这个目录项以待返回,需要在readdir函数的返回值为file1
时记录。如果希望记录
file1
这个目录项以待返回,需要在readdir函数的返回值为..
时记录。如果希望记录
..
这个目录项以待返回,需要在readdir函数的返回值为dir2
时记录。也就是说,需要在readdir函数的返回值为上一个目录项时,记录下一个目录项的位置。
那我们就有一个问题了:
如果我希望记录第一个目录项
dir2
的位置,咋办呢?很简单,不用记录,因为有专门的函数来实现这个功能,也就是
rewinddir
,倒带目录流,回到第一个目录项。
Gn!
rewinddir
函数重置目录流的位置到开始处,类似于文件流中的rewind
函数。其函数的声明如下:
xxxxxxxxxx
212void rewinddir(DIR *dirp);
形式参数:
dirp
是由opendir
返回的目录流指针。返回值:没有返回值。说明函数的设计者认为,该函数不会出错,也不需要进行错误的处理。
使用
rewinddir
可以使得目录流回到初始位置,这样下一次调用readdir
将返回目录中的第一个条目。
Gn!
以上,我们就已经学完了目录流的内容,那么下面我们将做一个练习,自己来手动实现一个简化版本的
ls - al
指令。简化,主要是简化了将目录项结果排序的过程,但-l选项所有的信息都要求大家打印出来。
在上面我们已经实现了打印一个目录下,所有文件子目录的:inode编号、目录项大小、文件类型以及文件名。
这和
ls -al
还有哪些差距呢?差距太大了:
文件类型应该用
d
、b
、-
等形式来代表,而不是一个整数值。权限信息,完全没有
硬链接数,完全没有
拥有者名和拥有组名,完全没有
文件大小,没有(目录项大小不是文件大小)
最后修改时间,没有
看起来我们现在只有一个文件名,所以为了打印这些信息,我们还需要一个学习一个非常重要的函数——stat系统调用函数。
Gn!
stat是一个系统调用函数,
stat
用于获取指定文件的相关信息。它的函数声明如下:
xxxxxxxxxx
212int stat(const char *path, struct stat *buf);
形式参数:
path
:这是一个指向字符数组的指针,代表要获取信息的文件的路径名。
buf
:这是一个指向struct stat
结构的指针,用于存储获取到的path文件的信息。返回值:
成功:返回
0
。失败:返回
-1
,并且errno
被设置以指示错误的原因。常见的出错原因有文件不存在、没有权限等。关于这个函数的调用,我们需要注意以下几个细节问题。
Gn!
该函数的调用,首先就有一个小细节需要注意:
利用
dirent
结构体我们可以获取某个目录下所有文件的文件名,但这里的文件名就是stat
系统调用函数的第一个参数吗?并不一定是:
如果进程的当前工作目录就是
dirent
结构体所表示的目录(就是opendir打开的目录),那么文件名就是路径名(相对路径).如果进程的当前工作目录不是
dirent
结构体所表示的目录(不是opendir打开的目录),那么"dirent
结构体目录路径/文件名"才是路径名(绝对路径)。此时基于
dirent
结构体,来获取stat
函数形参的文件路径名就有了两种办法:
在子l进程中使用
chdir
函数切换进程的当前工作目录为dirent
结构体所表示的目录,此时文件名就是路径名。利用
dirent
结构体中获取的文件名,结合dirent
结构体目录路径名,拼接得到一个绝对路径。两种方式都可以,具体用哪个看自己的需求。
Gn!
函数的第二个形参是
*buf
指针,需要传入一个指向stat结构体对象的指针。该形参没有被const修饰,传入stat函数后,显然函数内部会修改指向的内容,也就是在函数内部会修改stat结构体对象。实际上这个函数的主要作用就是给stat结构体对象赋值,这非常类似
scanf
函数,*buf
是一个非常典型的传入传出参数。你可能非常好奇,
stat
结构体到底是个什么东西呢?确实,理解这个结构体,是理解stat系统调用函数最重要的部分。但不着急,我们先来思考一个问题:
这个传入函数的结构体对象,它的内存由谁进行管理呢?
当然是由程序员自己管理了,推荐传入函数一个局部变量结构体指针,这样内存管理可以交给栈自动管理。
Gn!
通过
man 2 stat
查看stat函数的说明手册,我们可以找到stat结构体类型的定义,这里我们仅列出对我们实现ls -l
功能有用的字段:xxxxxxxxxx
91struct stat {
2mode_t st_mode; // 包含文件的类型以及权限信息
3nlink_t st_nlink; // 文件的硬链接数量
4uid_t st_uid; // 文件所有者的用户ID
5gid_t st_gid; // 文件所有者组的组ID
6off_t st_size; // 文件的实际大小, 以字节为单位
7
8struct timespec st_mtim; /* 包含文件最后修改时间的结构体对象 */
9};
这些字段的类型都是使用别名来定义的,在64位Linux操作系统上,这些别名的类型一般是:
mode_t
:一般是一个32位无符号整数。
nlink_t
:一般是一个64位无符号整数。
uid_t
和uid_t
:一般是一个32位无符号整数。
off_t
:一般是一个64位无符号整数。这些字段中,最后修改时间
st_mtim
字段的类型稍微麻烦一些,是一个结构体timespec
类型,代码如下:xxxxxxxxxx
41struct timespec {
2__time_t tv_sec; // 时间戳,秒为单位。此类型别名一般就是long类型
3__syscall_slong_t tv_nsec; // 纳秒 - 存储时间戳当中不足秒的部分,用于精准表示时间。此类型别名一般就是long类型
4};
综上所述,这里给出一个stat函数的简单调用:
xxxxxxxxxx
61// 错误处理都省略了
2DIR *dirp = opendir(argv[1]);
3chdir(dir_name); // 切换当前进程的工作目录为dir_name目录
4struct dirent* pdirent = readdir(dirp)); // 目录项结构体, dirp是打开的dir_name目录流指针
5struct stat stat_buf; // 预先申请一个stat结构体对象
6int ret = stat(pdirent->d_name, &stat_buf); // 获取文件的相关信息,相当于给stat_buf结构体对象初始化
可以把这段代码视为一个惯用方式。
关于stat的成员"st_mtim"以及结构体"timespec"有一个细节需要注意:
如果想要获取文件的最后修改时间戳的秒数,可以使用下面代码:
xxxxxxxxxx
11stat_buf.st_mtim.tv_sec; // stat_buf是调用stat函数完成赋值的stat结构体对象
这样做是可以的,但是两个成员运算符"."的连用未免有些麻烦,于是在stat结构体的类型定义外,还有一个宏定义:
xxxxxxxxxx
11这就是man手册中显示的内容:
基于这个宏定义,下面两条代码就是完全等价的:
xxxxxxxxxx
21stat_buf.st_mtim.tv_sec; // stat_buf是调用stat函数完成赋值的stat结构体对象
2stat_buf.st_mtime; // 等价于上一条代码
注意:
一个是"st_mtim",这是一个结构体类型,它是stat结构体的成员。
一个是宏定义"st_time",它是一个"long int"类型的表示文件最后修改时间的时间戳秒数。
Gn!
以上内容学完后,我们就可以利用
stat
函数实现一个青春版的ls -al指令。参考代码如下所示:
xxxxxxxxxx
411/* Usage: ./07_myls pathname */
2int main(int argc, char *argv[]){
3ARGS_CHECK(argc, 2);
4DIR *dirp = opendir(argv[1]);
5ERROR_CHECK(dirp, NULL, "opendir");
6
7// 切换工作目录为参数传入的目录
8int ret = chdir(argv[1]);
9ERROR_CHECK(ret, -1, "chdir");
10
11// dirent结构体指针,用于存放目录项信息
12struct dirent *pdirent;
13// 读取目录中的每个目录项
14while ((pdirent = readdir(dirp)) != NULL) {
15// 拼接文件路径,如果上面没有chdir切换目录,这里就需要做绝对路径的拼接操作
16// char path[1024] = {0};
17// sprintf(path, "%s%s%s", argv[1], "/", pdirent->d_name);
18// 或者也可以用strcpy以及strcat函数进行复制拼接获取最终目录
19// strcpy(path, argv[1]);
20// strcat(path, "/");
21// strcat(path, pdirent->d_name);
22
23// 获取文件相关的信息
24struct stat stat_buf;
25int ret = stat(pdirent->d_name, &stat_buf); // 由于切换了工作目录,所以文件名就是路径名。如果没有切换,那么这里需要传参path
26ERROR_CHECK(ret, -1, "stat");
27
28// 输出文件相关的信息
29printf("%o %lu %u %u %lu %lu %s\n",
30stat_buf.st_mode, // 以八进制无符号输出,可以看到权限的数字表示法
31stat_buf.st_nlink, // 无符号长整型输出
32stat_buf.st_uid, // 无符号int输出
33stat_buf.st_gid, // 无符号int输出
34stat_buf.st_size, // 无符号长整型输出
35statbuf.st_mtim.tv_sec, // 时间戳打印秒数,以无符号长整型打印。这里还可以写statbuf.st_mtime
36pdirent->d_name); // 字符串打印
37}
38// 关闭目录流
39closedir(dirp);
40return 0;
41}
只需要注意
stat
函数调用时需要传入的两个参数就可以了:
第一个参数必须是文件的路径名,而文件名并不一定直接就是路径名。
第二个参数必须是一个已申请内存的
stat
结构体指针,整个stat函数相当于给stat
结构体对象初始化。
Gn!
首先,我们要把
st_mode
成员的数字表示转换成"文件类型 + rwx权限"字符串,这要如何去实现呢?首先,我们还是要理解
st_mode
这个成员是如何存储文件类型和权限信息的。上面已经讲过了,
st_mode
在64位Ubuntu下一般是一个32位无符号整数,但实际上它也并不需要这么多位。它实际利用上的位只有:
前4个(或前5个)二进制位,用于存储数据表示该文件的类型。
最后9个二进制位,用于存储数据表示该文件的权限。
既然涉及到位,那我们应该立刻想要位运算,实际上要想通过
st_mode
处理转换文件类型和权限,整个过程都需要依赖位运算。文件类型转换的实现是比较简单的,因为man手册关于这个操作,给出了一个示例,如下:
xxxxxxxxxx
101switch (sb.st_mode & S_IFMT) {
2case S_IFBLK: printf("block device\n"); break;
3case S_IFCHR: printf("character device\n"); break;
4case S_IFDIR: printf("directory\n"); break;
5case S_IFIFO: printf("FIFO/pipe\n"); break;
6case S_IFLNK: printf("symlink\n"); break;
7case S_IFREG: printf("regular file\n"); break;
8case S_IFSOCK: printf("socket\n"); break;
9default: printf("unknown?\n"); break;
10}
其中:
sb.st_mode
就是结构体的st_mode成员
S_IFMT
是一个位掩码,将st_mode
成员与S_IFMT
进行位与操作,就能够得到代表文件类型的值。随后再用这个代表文件类型的值,进行switch选择判断,从而实现根据
st_mode
来获取对应文件类型。于是我们就完美的获取了文件类型。
那么接下来,如何转换权限数字,变为一个字符串呢?
st_mode
成员的低9位用于存储权限信息,我们只需要从低9位开始,向后一直判断到最低位,判断每一位数字是否是1,从而决定该位置的权限字符。整个判断的过程,仍然要使用按位与
&
运算符,举例:
st_mode
&100 000 000
(0400) ,若结果不为0说明拥有者有读权限,若为0表示拥有者没有读权限。
st_mode
&010 000 000
(0200) ,若结果不为0说明拥有者有写权限,若为0表示拥有者没有写权限。
st_mode
&001 000 000
(0100) ,若结果不为0说明拥有者有执行权限,若为0表示拥有者没有执行权限。...
于是我们可以写出这样的代码:
xxxxxxxxxx
191void mode_to_string(mode_t mode, char str[10]) {
2// 所有者权限
3str[0] = (mode & 0400) ? 'r' : '-';
4str[1] = (mode & 0200) ? 'w' : '-';
5str[2] = (mode & 0100) ? 'x' : '-';
6
7// 组权限
8str[3] = (mode & 0040) ? 'r' : '-';
9str[4] = (mode & 0020) ? 'w' : '-';
10str[5] = (mode & 0010) ? 'x' : '-';
11
12// 其他用户权限
13str[6] = (mode & 0004) ? 'r' : '-';
14str[7] = (mode & 0002) ? 'w' : '-';
15str[8] = (mode & 0001) ? 'x' : '-';
16
17// 设置字符串结束符
18str[9] = '\0';
19}
这样,我们就完成了文件类型和文件权限的处理。
Gn!
现在我们已经有用户UID以及组ID了,可以直接通过以下两个POSIX标准库函数将uid和gid成员转换成对应字符串。
xxxxxxxxxx
51234struct passwd *getpwuid(uid_t uid);
5struct group *getgrgid(gid_t gid);
其中passwd和group这两个结构体类型,如下所示:
xxxxxxxxxx
161struct passwd {
2char *pw_name; // 用户名
3char *pw_passwd; // 密码(通常是加密后的密码,但在现代系统中通常是 'x' 或 '*',真正的密码存储在安全文件中)
4uid_t pw_uid; // 用户ID
5gid_t pw_gid; // 组ID
6char *pw_gecos; // GECOS 字段,包含其他用户信息
7char *pw_dir; // 用户主目录
8char *pw_shell; // 用户登录 shell
9};
10
11struct group {
12char *gr_name; // 组名
13char *gr_passwd; // 组密码(在现代系统中通常是 'x' 或 '*',真正的密码存储在安全文件中)
14gid_t gr_gid; // 组ID
15char **gr_mem; // 包含组成员用户名的字符串数组
16};
这个转换的过程非常简单,不再赘述。
需要注意的是:
getpwuid()
和getgrgid()
函数都是POSIX标准库函数,不是标准库函数,更不是系统调用。它们实际上是直接从系统的用户数据库(通常是
/etc/passwd
文件)和组数据库(通常是/etc/group
文件)中获取用户和组的信息。
Gn!
现在我们只剩下一件事情就可以完成简化版
ls -al
的编写了——将时间戳修改为年月日的日期。我们可以使用一个C语言标准库的库函数——
localtime
这个函数的目的是将一个时间戳(
time_t
类型)转换为表示本地时间的struct tm
结构体。时间戳是自 1970 年 1 月 1 日(UTC)以来的秒数。该函数的声明如下:
xxxxxxxxxx
212struct tm *localtime(const time_t *timer);
形式参数:
timer
:需要传入一个指向time_t
类型变量的指针,也就是指向时间戳变量的指针。注意:结构体stat的成员
st_mtim
是一个结构体类型,不能直接传参给localtime
函数。st_mtim.tv_sec
的类型则刚好匹配,可以传参。(time_t一般就是long int)当然,你还可以直接传参
stat_buf.st_mtime
,其中st_mtime
是st_mtim.tv_sec
的宏定义,可以简化代码。返回值:返回值是指向
struct tm
结构体的指针,该结构体包含了分解后的时间元素,例如年、月、日、小时、分钟、秒等。这个
struct tm
结构体的定义如下:xxxxxxxxxx
111struct tm {
2int tm_sec; /* 秒 – 取值区间为[0,59] */
3int tm_min; /* 分 - 取值区间为[0,59] */
4int tm_hour; /* 时 - 取值区间为[0,23] */
5int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
6int tm_mon; /* 月份(从一月开始,0代表一月)- 取值区间为[0,11] */
7int tm_year; /* 年份,其值从1900开始 */
8int tm_wday; /* 星期 – 取值区间为[0,6],星期日为0 */
9int tm_yday; /* 从每年的1月1日开始的天数 – 取值区间为[0,365],1月1日为0 */
10int tm_isdst; /* 夏令时标识符,夏令时时为正,不是夏令时时为0,不了解情况时为负 */
11};
注意,这些数值的取值范围,不是正常的生活中的表示方式。比如月份不是[1, 12],而是[0, 11]
最后,你不需要手动释放
localtime
返回的struct tm
结构体对象。localtime
函数返回的是一个指向静态分配的结构体的指针。这片内存空间由C系统自动管理和重用,程序员无需进行管理。获取这个结构体后,就可以对照
ls -al
指令,选择出需要的数据项,然后拼接处理得到一个表示最后修改时间的字符串。
Gn!
在最终实现时,我们要求大家多做一步处理:
允许启动可执行程序时,只输入可执行程序名这个命令行参数,此时表示打印当前目录下的信息。实际上
ls -al
也是这么设计的。允许输入第二个命令参数,表示要打印信息的目录,
ls -al
也同样具有这个功能。参考代码如下:
xxxxxxxxxx
1031// 设置文件类型和权限字符串
2void set_type_mode(mode_t mode, char *tm_str){
3// 处理第一个字符,即文件类型
4switch (mode & S_IFMT) {
5case S_IFBLK: tm_str[0] = 'b'; break;
6case S_IFCHR: tm_str[0] = 'c'; break;
7case S_IFDIR: tm_str[0] = 'd'; break;
8case S_IFIFO: tm_str[0] = 'p'; break;
9case S_IFLNK: tm_str[0] = 'l'; break;
10case S_IFREG: tm_str[0] = '-'; break;
11case S_IFSOCK: tm_str[0] = 's'; break;
12default: tm_str[0] = '?'; break;
13}
14
15// 处理后续九个字符,即文件的权限信息
16// 设置拥有者的权限信息
17tm_str[1] = (mode & 0400) ? 'r' : '-';
18tm_str[2] = (mode & 0200) ? 'w' : '-';
19tm_str[3] = (mode & 0100) ? 'x' : '-';
20// 设置拥有者组的权限
21tm_str[4] = (mode & 0040) ? 'r' : '-';
22tm_str[5] = (mode & 0020) ? 'w' : '-';
23tm_str[6] = (mode & 0010) ? 'x' : '-';
24// 设置其他人的权限
25tm_str[7] = (mode & 0004) ? 'r' : '-';
26tm_str[8] = (mode & 0002) ? 'w' : '-';
27tm_str[9] = (mode & 0001) ? 'x' : '-';
28tm_str[10] = '\0'; // 确保字符串以 null 结尾
29}
30
31// 获取格式化的时间字符串
32void set_time(time_t mtime, char *time_str){
33// 由于tm结构体中存储的是月份的整数值,我们需要的是月份字符串,所以用一个字符串数组来存储月份字符串
34const char month_arr[][10] = {
35"1月", "2月", "3月", "4月", "5月", "6月",
36"7月", "8月", "9月", "10月", "11月", "12月"
37}; // tm结构体当中的月份范围是[0, 11],刚好可以适配这个数组
38
39// 调用localtime函数,获取tm结构体指针
40struct tm* st_tm = localtime(&mtime);
41// 构建时间字符串,格式为:月份 天数 时:分
42sprintf(time_str, "%s %2d %02d:%02d",
43month_arr[st_tm->tm_mon],
44st_tm->tm_mday,
45st_tm->tm_hour,
46st_tm->tm_min);
47}
48
49/* Usage: ./07_myls pathname 或 ./07_myls */
50int main(int argc, char* argv[]) {
51char* dir_name; // 存储目录名的指针
52if(argc == 1) {
53dir_name = "."; // 如果命令行参数没有提供要打印的目录,就打印当前工作目录
54}
55else if (argc == 2){
56dir_name = argv[1]; // 否则使用提供的命令行参数作为待打印的目标
57}else {
58fprintf(stderr, "args num error!\n");
59exit(1);
60}
61
62DIR* dirp = opendir(dir_name); // 打开指定的目录
63ERROR_CHECK(dirp, NULL, "opendir");
64
65// 改变工作目录到指定目录
66int ret = chdir(dir_name);
67ERROR_CHECK(ret, -1, "chdir");
68
69struct dirent* pdirent;
70// 遍历目录项
71while ((pdirent = readdir(dirp)) != NULL) {
72struct stat stat_buf;
73// 获取目录项的详细信息
74int ret = stat(pdirent->d_name, &stat_buf);
75ERROR_CHECK(ret, -1, "stat");
76
77char mode_str[1024] = { 0 }; // 保存文件类型和权限信息
78set_type_mode(stat_buf.st_mode, mode_str); // 设置类型和权限
79
80char time_str[1024] = { 0 }; // 保存格式化后的时间信息
81/*
82localtime需要传入时间戳描述
83所以这里传参时可以写stat_buf.st_mtime(宏定义,更简写)
84也可以写stat_buf.st_mtim.tv_sec(宏定义的原版代码)
85但不能直接写stat_buf.st_mtim
86原因上面已经讲过了!
87*/
88set_time(stat_buf.st_mtime, time_str); // 获取时间字符串
89
90printf("%s %2lu %s %s %6lu %s %s\n",
91mode_str, // 文件类型与权限
92stat_buf.st_nlink, // 硬链接数,不足2个字符的在前面补空格
93getpwuid(stat_buf.st_uid)->pw_name, // 拥有者名
94getgrgid(stat_buf.st_gid)->gr_name, // 拥有者组名
95stat_buf.st_size, // 文件大小,使用%4lu表示最少输出4个字符,若不足4个字符则在前面补空格
96time_str, // 最后修改时间字符串
97pdirent->d_name); // 文件名
98}
99
100// 关闭目录
101closedir(dirp);
102return 0;
103}
实现这个代码最需要理解的地方就是——文件名和路径名的区别。具体的描述可以参考下图:
以上。
Gn!
shell指令
ls -al
的显示效果,实际上还附带一个排序效果,若想要实现这个排序效果也并非难事,我们现在学习过的知识点就已经足够了——使用qsort
函数给dirent
结构体数组排好序,然后再遍历打印详细信息即可。参考的实现代码如下(有部分细节问题,可以直接查看注释解决):
x12
3void set_type_mode(mode_t mode, char *tm_str);
4void set_time(time_t mtime, char *time_str);
5int compare(const void* a, const void* b);
6void str_to_lower(char *str);
7
8/* Usage: ./08_myls dirname */
9int main(int argc, char *argv[]){
10char *path;
11if(argc == 1){
12path = ".";
13}else if(argc == 2){
14path = argv[1];
15}else{
16fprintf(stderr, "args nums error!\n");
17return 1;
18}
19
20// path是一个目录的路径名
21DIR* dirp = opendir(path);
22ERROR_CHECK(dirp, NULL, "opendir");
23
24// 切换工作目录为待打印的目录
25int ret = chdir(path);
26ERROR_CHECK(ret, -1, "chdir");
27
28// 遍历目录项构建dirent结构体数组,用于实现排序
29// 为了更好的排序性能,选择使用结构体指针数组,而不是结构体数组
30// 为了程序能够更加灵活使用,选择动态内存分配这个结构体指针数组
31// 先遍历一次计算目录项的个数
32int count = 0;
33struct dirent* dp;
34
35// 第一次遍历:计算目录项数量
36while ((dp = readdir(dirp)) != NULL) {
37count++;
38} // while循环结束时,dp是一个空指针,count就是目录项的个数
39
40// 动态申请结构体指针数组
41struct dirent** dir_arr = (struct dirent**)malloc(sizeof(struct dirent*) * count);
42ERROR_CHECK(dir_arr, NULL, "malloc dlist");
43
44// 倒带目录流指针
45rewinddir(dirp);
46
47// 第二次遍历: 将目录项结构体对象指针存入数组中
48int idx = 0;
49while((dp = readdir(dirp)) != NULL){
50dir_arr[idx] = dp;
51idx++;
52} // while循环结束时,待打印目录下的所有文件的dirent结构体指针斗被存入了dir_arr数组
53
54// 按照名字的字典顺序,利用qsort函数排序dir_arr数组
55qsort(dir_arr, count, sizeof(struct dirent *), compare);
56
57// 到这里为止,看似目录流的操作就已经结束了,似乎可以直接关闭目录流了
58// closedir(dirp);
59
60// 最后一步:根据已排序的目录项指针数组,获取stat结构体,打印文件详细信息
61for(int i = 0; i < count; i++){
62struct stat sb;
63// 文件名不再从dirent结构体中获取,不要忘记修改
64int ret = stat(dir_arr[i]->d_name, &sb);
65ERROR_CHECK(ret, -1, "stat");
66
67// 1.处理stat的成员st_mode,将它转换成权限和类型字符串
68char tm_str[1024] = {0};
69set_type_mode(sb.st_mode, tm_str);
70
71// 2.获取用户名和组名
72char *username = getpwuid(sb.st_uid)->pw_name;
73char *gname = getgrgid(sb.st_gid)->gr_name;
74
75// 3.将时间戳转换为时间字符串
76char time_str[1024] = {0};
77set_time(sb.st_mtim.tv_sec, time_str);
78
79printf("%s %2lu %s %s %6lu %s %s\n",
80tm_str,
81sb.st_nlink,
82username,
83gname,
84sb.st_size,
85time_str,
86// 文件名不再从dirent结构体中获取,不要忘记修改
87dir_arr[i]->d_name);
88}
89
90// 不要忘记free动态申请的目录项结构体指针数组
91free(dir_arr);
92
93// readdir返回的目录流结构体内存是由目录流自身管理的,会随着关闭目录流而自动释放
94// 所以如果在上面关闭目录流,极有可能导致目录项结构体被释放,从而导致数据漏输出
95closedir(dirp);
96return 0;
97}
98
99void set_type_mode(mode_t mode, char *tm_str){
100// 处理第一个字符,即文件类型
101switch (mode & S_IFMT) {
102case S_IFBLK: tm_str[0] = 'b'; break;
103case S_IFCHR: tm_str[0] = 'c'; break;
104case S_IFDIR: tm_str[0] = 'd'; break;
105case S_IFIFO: tm_str[0] = 'p'; break;
106case S_IFLNK: tm_str[0] = 'l'; break;
107case S_IFREG: tm_str[0] = '-'; break;
108case S_IFSOCK: tm_str[0] = 's'; break;
109default: tm_str[0] = '?'; break;
110}
111
112// 处理后续九个字符,即文件的权限信息
113// 设置拥有者的权限信息
114tm_str[1] = (mode & 0400) ? 'r' : '-';
115tm_str[2] = (mode & 0200) ? 'w' : '-';
116tm_str[3] = (mode & 0100) ? 'x' : '-';
117// 设置拥有者组的权限
118tm_str[4] = (mode & 0040) ? 'r' : '-';
119tm_str[5] = (mode & 0020) ? 'w' : '-';
120tm_str[6] = (mode & 0010) ? 'x' : '-';
121// 设置其他人的权限
122tm_str[7] = (mode & 0004) ? 'r' : '-';
123tm_str[8] = (mode & 0002) ? 'w' : '-';
124tm_str[9] = (mode & 0001) ? 'x' : '-';
125tm_str[10] = '\0'; // 确保字符串以 null 结尾
126}
127
128void set_time(time_t mtime, char *time_str){
129// 由于tm结构体中存储的是月份的整数值,我们需要的是月份字符串,所以用一个字符串数组来存储月份字符串
130const char month_arr[][10] = {
131"1月", "2月", "3月", "4月", "5月", "6月",
132"7月", "8月", "9月", "10月", "11月", "12月"
133};
134// 调用localtime函数,获取tm结构体指针
135struct tm* st_tm = localtime(&mtime);
136// 构建时间字符串,格式为:月份 天数 时:分
137sprintf(time_str, "%s %2d %02d:%02d",
138month_arr[st_tm->tm_mon],
139st_tm->tm_mday,
140st_tm->tm_hour,
141st_tm->tm_min);
142}
143
144// 用文件名的字典顺序,排序目录项结构体指针数组
145int compare(const void* a, const void* b){
146// 此时a和b都是指向目录项结构体指针的指针,所以它们是二级指针,需要强转再解引用
147struct dirent* dir_a = *(struct dirent**)a;
148struct dirent* dir_b = *(struct dirent**)b;
149// 将文件名都转换成小写字母然后再按字典顺序排序
150// 原始文件名不能被修改,所以创建临时数组来存储文件名的副本
151char file_name[256];
152char file_name2[256];
153
154strncpy(file_name, dir_a->d_name, sizeof(file_name) - 1);
155strncpy(file_name2, dir_b->d_name, sizeof(file_name2) - 1);
156
157// 确保字符串以空字符终止
158file_name[sizeof(file_name) - 1] = '\0';
159file_name2[sizeof(file_name2) - 1] = '\0';
160
161// 将副本转换为小写
162str_to_lower(file_name);
163str_to_lower(file_name2);
164
165// 返回副本文件名的字典顺序
166return strcmp(file_name, file_name2);
167}
168
169// 将传入的字符串字母都转换成小写字母
170void str_to_lower(char *str) {
171while (*str) {
172*str = tolower(*str);
173str++;
174}
175}
以上。
Gn!
写一个小程序,实现青春版 tree 命令。大体效果如下:
xxxxxxxxxx
131.
2└───dir
3└───────dir1
4└───────file1
5└───────a
6└───────dir2
7└───main.c
8└───Makefile
9└───test.c
10└───simple_tree.c
11└───simple_tree
12
133 directories, 7 files
怎么实现呢?
我们可以把整个打印分为三个部分:
打印根目录的名字
带缩进格式的,递归打印根目录下每一个目录项的信息(文件名)
最下面打印统计文件和文件数量的信息
提示:
└
和─
这两个符号可以直接从tree指令执行结果里抄过来。递归打印根目录的每一个目录项的信息,其实就是深度优先(DFS)遍历树形结构,可以采用递归的方式来实现。(而且总是优先打印出父目录的名字,才深入打印目录里的内容,这是一个先序深度优先遍历策略)
拼接绝对路径的实现方式:
x1// 全局变量,用于统计总目录数和文件数
2static int dirs = 0;
3static int files = 0;
4
5// 深度优先搜索打印目录树
6// dirpath: 要递归遍历的根目录
7// width: 打印宽度,用于控制缩进,增强视觉层次感
8void DFS_print(char* dirpath, int width) {
9DIR* dirp = opendir(dirpath);
10ERROR_CHECK(dirp, NULL, "opendir"); // 检查目录是否成功打开
11
12struct dirent* pdirent;
13while ((pdirent = readdir(dirp)) != NULL) { // 循环读取目录项
14// 跳过 "." 和 ".." 这两个特殊目录
15if (strcmp(pdirent->d_name, ".") == 0 ||
16strcmp(pdirent->d_name, "..") == 0) {
17continue;
18}
19// 打印前缀和缩进,构建树形结构的视觉效果
20printf("└");
21for (int i = 1; i < width; ++i) {
22printf("─");
23}
24// 打印当前文件或目录的名称(先序遍历)
25printf("%s\n", pdirent->d_name);
26// 如果是目录,则递归调用自己,继续深入遍历
27if (pdirent->d_type == DT_DIR) { // DT_DIR 表示目录文件
28dirs++;
29char path[1024] = { 0 };
30sprintf(path, "%s%s%s", dirpath, "/", pdirent->d_name); // 构建递归调用的目录路径名
31DFS_print(path, width + 4); // 递归调用,增加宽度以增加缩进
32}
33else {
34// 文件数++
35files++;
36}
37}
38// 完成目录遍历后关闭目录
39closedir(dirp);
40}
41
42/* Usage: ./08_mytree pathname */
43int main(int argc, char* argv[]) {
44ARGS_CHECK(argc, 2);
45// 打印根目录名称
46printf("%s\n", argv[1]);
47// 从根目录开始深度优先遍历打印目录树, 初始-打印4个
48DFS_print(argv[1], 4);
49printf("\n%d directories, %d files\n", dirs, files);
50return 0;
51}
你当然也可以选择使用chdir切换目录来实现这个功能,但是在递归的过程中频繁切换工作目录,是一件比较麻烦的事情,思考难度比较大,代码容易摸不到头脑,建议优先考虑使用上面的拼接绝对路径的方式。
参考代码如下:
xxxxxxxxxx
461static int dirs = 0;
2static int files = 0;
3
4void DFS_print(char* dirpath, int width) {
5DIR* dirp = opendir("."); // 直接打开当前目录
6ERROR_CHECK(dirp, NULL, "opendir"); // 检查目录是否成功打开
7
8struct dirent* pdirent;
9while ((pdirent = readdir(dirp)) != NULL) { // 循环读取目录项
10// 跳过 "." 和 ".." 这两个特殊目录
11if (strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0) {
12continue;
13}
14
15// 打印前缀和缩进,构建树形结构的视觉效果
16printf("└");
17for (int i = 1; i < width; ++i) {
18printf("─");
19}
20printf("%s\n", pdirent->d_name); // 打印当前文件或目录的名称
21
22// 如果是当前正在处理的文件是目录文件,那么就递归深入该目录遍历
23if (pdirent->d_type == DT_DIR) { // DT_DIR 表示目录文件
24dirs++;
25int ret = chdir(pdirent->d_name); // 改变工作目录到当前遍历的目录
26ERROR_CHECK(ret, -1, "chdir"); // 检查目录是否成功切换
27DFS_print(".", width + 4); // 工作目录切换成功后,就递归的对当前目录进行深入遍历
28ret = chdir(".."); // 当前目录已经遍历完了,那就返回上一级目录
29ERROR_CHECK(ret, -1, "chdir"); // 检查是否成功返回
30} else {
31files++;
32}
33}
34closedir(dirp); // 完成目录遍历后关闭目录
35}
36
37/* Usage: ./08_mytree pathname */
38int main(int argc, char* argv[]) {
39ARGS_CHECK(argc, 2);
40printf("%s\n", argv[1]); // 打印根目录名称
41int ret = chdir(argv[1]); // 改变到提供的根目录
42ERROR_CHECK(ret, -1, "chdir");
43DFS_print(".", 4); // 从根目录开始深度优先遍历打印目录树, 初始打印4个横杠
44printf("\n%d directories, %d files\n", dirs, files);
45return 0;
46}
这两种实现都是可以的,从效果上来说,都是OK的。