POSIX-C
文件系统编程是建立在不同 系统 / 文件系统 基础上的,比如win平台的文件系统编程接口一般在Windows.h这些头文件中,而linux一般在sys/下的头文件中,但是不管win还是linux都遵循ISOC标准,但是Linux实际C标准的是POSIXC,POSIXC是ISOC的超集,在ISOC的基础上进行了非常多的扩展,例如文件系统编程(对比win的Windows.h的一些文件系统接口函数)。
整个目录流的学习本质上就是函数和模型的学习,心中得构建一个自己的理解的模型,在此基础上学习和记忆函数,了解函数的基本用法,注意项,这就差不多了。
本次文件目录的编程平台为Linux。
工作目录
基本概念&如何获取进程的工作目录
Linux当前目录的基本概念:
在Linux系统中,当前目录(Current Working Directory, CWD)是指用户或程序当前操作所处的目录路径。每个进程(包括shell和运行的程序)都有自己的工作目录,由内核维护。
并且这个当前目录的信息,是存储在进程的PCB中的,PCB也成为进程控制块,linux内核管理的系统里面运行了非常多的用户进程和内核进程,他们由内核和硬件管理。每个进程对应一个PCB,其运行的状态信息都存储在PCB中,方便内核管理和调度。其进程的当前目录相关信息也存储在PCB中。
在linux中可以通过pwd指令来展示当前工作目录,pwd展示的执行pwd这个指令的子进程的当前工作目录,例如你在shell中输入pwd,你的shell有自己的工作目录,例如你当前在如下目录:

- 你的终端(如图所示的绿色标记的用户打开目录),本质也是一个进程,一个shell进程,你在这个目录下面输入pwd,当前的shell进程会先fork一个子进程。
- 这个子进程会加载并运行/bin/pwd程序。
- pwd程序的核心就是调用getcwd系统调用,读取当前这个被fork出来的子进程的工作目录,然后输出。
- 当前这个被fork的子进程是继承了父目录的工作目录,也就是继承了/home/des目录,所以你虽然不是shell进程执行pwd指令,但是输出结果还是和父进程的工作目录/gome/des一致。
- 注意:如果在这个过程中,你用chdir改变了当前工作目录,那么pwd的结果也会随着父进程的工作目录改变而改变。因为shell进程执行pwd 的时候,会重新创建一个子进程来执行pwd指令,并且这个子进程依旧会继承当前的工作目录,所以还是会输出新的工作目录。
我们上述讲解的pwd是shell指令的工作目录,这跟我们执行的可执行文件的工作目录有一定的差别,虽然底层都是使用的getcwd的系统调用,但是逻辑略微有所不同:
- getcwd是由posix标准定义的类linux编程接口,是系统默认自带的核心库函数
- 在/bin/pwd中的独立程序是直接封装并且调用了getcwd编程接口了的,shell内置的pwd是直接调用系统调用来完成的,目的和getcwd一致。
- 无论是getcwd(编程接口,提供给程序员的),还是pwd指令(shell指令,程序员和系统交互的联机指令),还是/bin/pwd 可执行文件(bin目录下的一个可执行文件,可以像.exe文件一样运行),本质都是调用了同一套系统调用,例如openat(), fstat()等。
- 简单说:getcwd封装了系统调用的函数,pwd跳过这些封装,直接调用系统调用,/bin/pwd封装了getcwd函数,他们的目的都是一致的:获取当前进程的工作目录,只不过pwd会fork一个子进程,而子进程在被创建的时候刚好又继承了父目录的工作目录,因此pwd的结果还是跟父目录一致,从而实现了查看当前目录的功能。
./bin/pwd 查看目录的例子:

使用库函数来简介调用系统调用的例子(getcwd):
先用手册man 3 getcwd查查

- 返回值所代表的内存情况完全由你传入的buf决定,如果你手动指定了buf参数,那么最终的数据会被填入你所传入的buf,然后返回的指针也同样指向这块内存。此时其内存管理就分为两种情况:
- buf为在堆区申请的内存,那么就需要在必要的时候手动free,否则就会内存泄露。
- buf为在栈区定义的字符数组,那么交由栈自动管理即可(随着栈的销毁而销毁,但是也要考虑栈溢出的情况)。
- 如果你为手动指定buf,而是传入了一个NULL,那么getcwd会为你在堆区malloc一个相对的空间,然后填入cwd的值,然后返回指向这个空间的指针。
- 失败的时候返回一个NULL,并且设置errorno。
编写程序my_getcwd:
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
int main(int argc, char* argv[]) {
char cwd[PATH_MAX] = {0};
char* p_pwd = getcwd(cwd, PATH_MAX);
if (p_pwd == NULL) {
return -1;
}
printf("cwd中存储的当前的工作目录是:%s\n", cwd);
printf("检查p_pwd是否和cwd指向同一个内存:\np_pwd中存储的当前的工作目录是:%s\n", p_pwd);
return 0;
}
编译:

查看当前目录:

可以看到my_getcwd程序存放在/home/des/test下面,当前目录也为/home/des/test,我们执行他:

可以看到都是指向的同一个工作目录。
当前这个可执行文件在/home/des/test/中,那么执行他的时候,my_getcwd这个进程就是被shell进程创建的子进程,这个进程继承了shell的工作目录,因此输出了shell当前的工作目录。
但是子进程(my_getcwd)和shell进程(父进程)之间并不是同步工作目录,可以理解为两者的工作目录相互独立,但是初始化为父进程的工作目录。可以在子进程中切换目录,会有不一样的效果。
我们在获取当前工作目录之前先切换到上一级目录,然后再调用getcwd:
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
int main(int argc, char* argv[]) {
char cwd[PATH_MAX] = {0};
// 进入上一级目录, 这里应该有一个完整的错误处理逻辑,但是为了直观,直接省略
chdir("..");
char* p_pwd = getcwd(cwd, PATH_MAX);
if (p_pwd == NULL) {
return -1;
}
printf("cwd中存储的当前的工作目录是:%s\n", cwd);
return 0;
}

可以发现父进程和子进程的工作目录不一样了,关系为相互独立,并非同步共用一个工作目录,只是初始化的时候关系为继承。
你还可以在程序所在目录的上一级去调用这个程序:结果如下:

可以看到当前目录还是跟pwd一样,这也验证了父进程和子进程的工作目录的继承关系。
工作目录的本质
上面的讲解涉及到工作目录的基本使用和情况,对于一般的日常使用和理解来说够用了,但是工作目录其实有更本质的特征。
在文件系统里面,每个目录和文件都有一个对应的唯一的索引节点用来存储信息,就好比一本书里面的目录一样,你可以通过翻阅目录,然后通过目录的索引找到对应的内容一样,索引节点就好比书籍的内容,而目录则由索引号构成,一个索引号是唯一的,并且一对一对应上索引节点。
根目录也肯定对应了一个索引节点,根目录一般在开机的时候就被加载到内存,然后根目录的索引中存储了当前目录的一般信息,例如文件个数,索引节点大小,权限等,然后最关键的是数据区的数据块指针,目录的数据区的每一项数据被称作为目录项,每个目录项至少包含文件名和索引节点号两个信息。索引节点号也就是索引节点的指针(索引号), 以此构建出来了一个树状的结构。
简单来说你可以通过根目录的索引数据区罗列的很多目录项,包含文件名和其索引号,你在linux中对根目录使用ls -al的作用本质就是将这个根目录的所有数据区(很多个目录项)的内容全都读出来,然后进行展示:

当你要进入home目录,根据根目录索引节点提供的信息,那么你已经拥有home的名称和其索引号,你cd home的时候,系统就会根据索引号把home目录的索引节点加载到内存,然后读取其数据区的目录项(文件名和索引号),以此类推。
从这个过程可以看出,想要得到一个目录的索引节点信息,不是直接就能获取的,而是需要一级一级的加载,从根目录一级一级到你所需要的文件目录的索引节点,然后将他的索引节信息加载到内存,这样,你就用了了这个文件目录的信息,需要什么文件就通过其数据区的索引号来获取即可。
因此工作目录的本质其实就是进程拥有某个目录的索引节点信息,也就是看你想要操作的目录的索引节点的信息是否加载到了内存,这样,你才能对该目录下的文件或文件目录进行相关操作,至于有没有权限,那是权限的事啦~。
更具体的文件系统相关知识可以翻阅 操作系统相关书籍。
目录相关系统调用
改变当前工作目录
学习了什么是工作目录之后,就应该来学着如何改变一个可执行程序的当前工作目录,其实这点在上面就有所展示,这里再详细说一下,其函数基本信息如下:
// 这个库很关键:它是基于POSIXC标准的操作系统API的标准头文件,
// 主要用于Unix/Linux系统。它的名字
// 是"Unix Standard"的缩写。
#include <unistd.h>
// 函数声明
int chdir(const char *path);
参数:
- path:传入一个字符串的指针,其内容被const修饰,不会在函数执行的过程被修改,表示一个你想要跳转的路径,可以是相对路径也可是绝对路径。
返回值:
- 0:成功
- -1:失败并设置errno
示例:(再粘贴一下上面的代码,输出结果略)
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
int main(int argc, char* argv[]) {
char cwd[PATH_MAX] = {0};
// 进入上一级目录, 这里应该有一个完整的错误处理逻辑,但是为了直观,直接省略
chdir("..");
char* p_pwd = getcwd(cwd, PATH_MAX);
if (p_pwd == NULL) {
return -1;
}
printf("cwd中存储的当前的工作目录是:%s\n", cwd);
return 0;
}
创建目录
创建一个目录,其所需的头文件和函数声明如下:
// sys/ 是 System Header Files 的缩写
// 是一种系统级功能,能访问操作系统底层(内存,文件)功能,属于POSIX规范的一部分
#include <sys/stat.h> // 提供文件状态查询和操作函数
#include <sys/types.h> // 定义系统数据类型,确保跨平台一致性
// 声明
int mkdir(const char *pathname, mode_t mode);
参数:
- pathname:字符串指针类型,路径/目录名 ,例如/home/user/my_dir,其中my_dir为你要创建的文件夹,前面的/home/user就是路径,此处为绝对路径,当然也可以是相对路径,相当于在一个pathname变量里面写清楚了你要创建的目录在哪个路径,叫啥名
- mode:mode_t类型,该参数指定新目录的权限(如0755、0777,但是实际创建的目录的权限会受到进程umask的影响。
返回值:
- 0:成功
- -1:失败并设置errno
示例:
当前shell的工作目录:

无目录:

使用如下代码:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
int main()
{
// 定义绝对路径然后创建 dir1;
char arr[] = "/home/des/test/blog_examples/dir1";
// 定义相对路径然后创建dir2;
char arr2[] = "./dir2";
int ret = mkdir(arr, 0777);
if (ret == -1) {
// ... 判断逻辑
}
mkdir(arr2, 0777);
// 这里也应该写判断逻辑
return 0;
}

删除目录
有创建就有删除,删除目录所需头文件和函数为:rmdir
#include <unistd.h> // posixc标准新增系统编程头文件
int rmdir(const char *pathname); // 声明
参数:
- pathname: 要删除的目录的路径和名称,例如/home/des/test/dir1, 删除的就是/home/des/test目录下的dir1目录。可以是绝对路径,也可以是相对路径。
返回值:
- 0:成功
- -1:失败并设置errno
示例:
当前工作目录和想要删除的两个目录为dir1和dir2:

编写代码编译并执行:
#include <stdio.h>
#include <unistd.h>
int main()
{
// 绝对路径删除dir1
char paht_dir1[] = "/home/des/test/blog_examples/dir1";
// 相对路径删除dir2
char path_dir2[] = "./dir2";
// 此处应该用返回值判断是否删除成功
rmdir(paht_dir1);
rmdir(path_dir2);
return 0;
}
执行结果:被成功删除:

目录流
我们都知道文件流,本质上就是将一个文件的索引节点加载到内存,然后预读取一些数据到内核,然后每次read都会将一部分数据加载到内存,然后通过特定的函数去读取这些数据,每次读取都会返回一个目录条目,读取完之后,流会继续预读取一些目录数据,直到读取完毕。
目录流的主要特点就是:
- 提供对目录的顺序访问
- 将访问到的数据结构化为一个结构体,便于程序员访问目录内容

假如你是一个编程小白,并不了解编程知识,你现在看到了一个目录,如下:

你看到的是如图所示的deletable的一个文件夹,你看到了这个文件夹意味着什么?意味着你已经获取了这个文件的文件名和文件的索引节点号,如下:

提示:你所看到的这一整个文件夹(包括deletable在内),还可以看到很多其他的文件或者文件夹的索引节点,本质就是因为这一块内容本来就是别的文件夹的索引节点的数据区的条目,接下来你要双击打开这个deletable文件夹本质也是在读取deletable文件夹的索引数据区
现在你想知道这个deletable文件夹里面有什么东西,于是你双击打开这个文件,然后就把这个deletable的索引节点加载到内存了,但是还没完,你家在只是索引节点的内容,而索引节点本身存储的一般内容是索引节点大小,权限,这些,只有索引数据区才会存储指向存储数据的指针
索引节点(inode)是文件系统中用于描述文件或目录元数据的核心数据结构。对于deletable文件夹,其索引节点需要包含以下关键信息:
- 文件类型和权限:标识为目录类型(如S_IFDIR),并设置访问权限(如755表示所有者可读/写/执行,其他用户可读/执行)。
- 所有者信息:用户ID(UID)和组ID(GID),用于权限管理。
- 时间戳:创建时间(ctime)、修改时间(mtime)、访问时间(atime)。
- 大小信息:目录的大小(通常为块大小的整数倍)。
- 链接计数:硬链接数量,初始值为2(目录本身和.条目)或更高(若包含子目录的..条目)。
当然最关键的还是索引数据区指针:
索引节点需包含指向实际数据块的指针,用于存储目录内容。常见的指针结构包括:
- 直接指针:直接指向存储目录项(如文件名和子文件inode号)的数据块。
- 间接指针:适用于大型目录,通过多级指针(如一级、二级间接块)扩展寻址能力。
下面是索引节点可能的数据结构,提供你参考,后面再提及索引节点的数据结构,你或许就不会那么陌生:(了解即可,无需记忆)
struct inode {
uint16_t mode; // 文件类型和权限
uint16_t uid; // 所有者UID
uint16_t gid; // 组GID
uint32_t size; // 目录大小(字节)
uint32_t ctime; // 创建时间戳
uint32_t mtime; // 修改时间戳
uint32_t atime; // 访问时间戳
uint16_t links; // 硬链接计数
uint32_t blocks[15]; // 数据块指针数组
};
也就是说你会把deletable的索引节点加载到内存,如下:

(首先看看deleteble目录下有什么)
可以看到有三个文件,分别是:
- 1.txt
- 2.txt
- aaaaa.xls
然后通过其中的数据块指针数组,找到对应的目录数据,如何找?遍历如下数组:
![]()
遍历blocks数组,你就会得到这三个文件的文件名和索引节点号。
如果你想继续得到这三个文件的内容,那么就需要加载其各自的索引节点号到内存中,然后通过索引数据区的指针来读取其数据。
提示:目录的索引节点数据区指针指向的是目录项类型的数据,文件的索引节点数据区指针指向的是文件本身内容的数据。
在linux上打开一个目录流
使用opendir函数,其声明如下:

参数可以是一个绝对或者是相对路径,返回值为一个DIR*类型,这个DIR是一个结构体,其内容如下:
编写如下代码,试图访问dir:
关闭目录流
函数声明如下:
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);

成功返回0,失败返回-1,并设置errno。
读目录流
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
- 返回值: 成功时,返回指向
struct dirent结构体对象的指针,这个结构体当中包含了目录下文件和子目录的信息(如文件名等)。 - 当目录中没有更多条目时返回
NULL。
提示:官方开发者明确提出了:返回值指针指向的结构体对象是静态分配的内存,这意味着这个结构体对象的存储方式是由系统管理的,而不需要由程序员手动管理。
我们上述的opendir的过程其实本质就是在加载索引节点,这个时候返回了你一个DIR结构体指针,其实就对应这个索引节点,然后读目录流其实将相当于读取数据区指针, 对这个dir的数据区指针进行遍历,就相当于对这个流进行流式遍历。
也就是说,每次遍历都会返回一个dirent结构体(dirent其实就是dirctory,和entry的混合拼写),这个结构体代表目录中数据区的一个目录项。
dirent结构体的内容如下:
// dirent是directory entry的简写,就是目录项的意思
struct dirent {
// 此目录项的inode编号,目录项中会存储文件的inode编号。一般是一个64位无符号整数(64位平台)
ino_t d_ino;
// 到下一个目录项的偏移量。可以视为指向下一个目录项的指针(近似可以看成链表),一般是一个64位有符号整数
off_t d_off;
// 此目录项的实际大小长度,以字节为单位(注意不是目录项所表示文件的大小,也不是目录项结构体的大小)
unsigned short d_reclen;
// 目录项所表示文件的类型,用不同的整数来表示不同的文件类型
unsigned char d_type;
// 目录项所表示文件的名字,该字段一般决定了目录项的实际大小。也就是说文件名越长,目录项就越大
char d_name[256];
};
可以看到有几个关键内容:
- d_ino 索引节点号
- d_name 名称
这两个内容就和上面所表述的内容对上了。
因此我们可以开始遍历这个目录和打印这个目录项,如下:
#include <dirent.h> // dirent是directory entry的简写,就是目录项的意思
#include <sys/types.h>
#include <stdio.h>
int main()
{
DIR* dir = opendir("./blog_examples");
struct dirent* dirent;
while((dirent = readdir(dir)) != NULL) {
printf("文件名:%s, 索引号:%lu\n",dirent->d_name, dirent->d_ino);
}
// 关闭流
closedir(dir);
return 0;
}


1923

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



