网站建设怎么报价,seo网站推广软件 快排,批量查询收录,商业空间设计说明序言 在编程的世界里#xff0c;文件操作是不可或缺的一部分。无论是数据的持久化存储、日志记录#xff0c;还是简单的文本编辑#xff0c;文件都扮演着至关重要的角色。然而#xff0c;当我们通过编程语言如 C、Java 等轻松地进行文件读写时#xff0c;背后隐藏的复杂机…序言 在编程的世界里文件操作是不可或缺的一部分。无论是数据的持久化存储、日志记录还是简单的文本编辑文件都扮演着至关重要的角色。然而当我们通过编程语言如 C、Java 等轻松地进行文件读写时背后隐藏的复杂机制和底层细节往往被我们所忽略。 本文将带着大家理解文件操作在底层的样子原来文件不仅仅是被简单地读入或写入内存中。 1. 系统调用接口 用户编程接口 操作系统简称 OS 是计算机系统的核心软件它管理计算机的硬件和软件资源为上层应用程序提供一个稳定、统一的运行环境。 如果将你的电脑比作财产的话那么操作系统就是你的管家而且还是一个强势的管家。
1.1 系统调用接口
1. 系统调用接口的概念 当你想要访问你的计算机的软硬件资源时必须符合操作系统的规定而系统调用就是操作系统为用户空间程序提供的一种服务接口。
2. 为什么要存在系统调用接口 我的电脑我想干嘛就干嘛呀 为什么操作系统管的这么严呀还要按照他的要求来使用到底谁是主人呀 操作系统的存在就是避免用户不合法的行为比如错误的修改数据不正确的使用导致争整个系统的崩溃所以正是想要用户得到一个良好的运行环境才约束用户的行为。
3. 系统调用接口的作用 系统调用是计算机操作系统中非常核心的概念它的作用包含但不限于如下内容
硬件保护与隔离系统调用作为用户程序和硬件设备之间的中介确保用户程序不能直接访问硬件从而保护硬件资源免受非法访问和破坏。资源管理与分配操作系统通过系统调用来管理CPU时间、内存、文件和设备等系统资源确保资源的公平分配和有效利用。实现操作系统功能系统调用是操作系统实现各种功能如进程管理、内存管理、文件系统、网络通信等的基础。 1.2 用户编程接口
1. 用户编程接口的概念 用户操作接口接口API是操作系统或库函数提供给程序员的接口在 C 语言环境中API 通常以库函数的形式出现这些函数封装了系统调用的细节为程序员提供了更为简便、易用的编程接口。
2. 为什么要存在用户编程接口 API 提供了一种对系统调用抽象和封装。通过将复杂的系统细节隐藏起来API 只暴露用户需要的功能和接口使得用户包括程序员可以更容易地理解和使用这些功能。这种抽象和封装降低了直接与系统底层交互的难度和复杂性。
3. 编程接口的跨平台性 除了简化用户对系统调用的使用编程接口还具有一个非常重要的性质那就是 跨平台性。 系统调用接口对应不同的系统如 Linux, WindowsMac等实现的细节肯定是不一致的。所以你在 Linux 系统下使用了系统调用的程序在 Mac 下就不一定能运行。 但是 API 通过特定的方法如条件编译配置可以实现在不同的平台下都能正常运行。 2. 利用系统调用接口进行文件操作 在使用 C 语言进行文件操作时我们利用 fopen 函数以特定的方式打开一个文件利用 fread 读取文件或者是利用 fwrite 写文件最后利用 fclose 函数关闭文件。 这都是将系统接口进行封装过后的 API今天我们尝试直接使用系统接口这更加接近底层更好的帮助我们引出后续的内容。
2.1 open 函数 系统提供的 open 函数有两个版本
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);下面多出的 mode 参数用于指定新创建文件的权限所以我们使用下面那个的版本。我们看看他整体的参数
pathname 指向要打开的文件的路径名flags : 用于指定打开文件的方式以及其他选项如是否创建文件。这些选项可以通过位或操作组合起来。mode : 指定新创建文件的权限 。返回值为一个 int叫做文件描述符fd
我们在这里详细介绍 flags 参数 以及 mode 参数
flags 参数 — 位图 某些函数需要我们传递标志位该函数根据标志位执行特定的功能就比如 1 #include iostream2 using namespace std;3 4 5 int Func(int num, int flag1, int flag2, int flag3){6 if(flag1 1){7 num num 1;8 }9 if(flag2 1){10 num num - 1;11 }12 if(flag3 1){13 num num * 2;14 }15 16 return num;17 }18 19 20 int main(){21 int num 10;22 int flag1 1, flag2 0, flag3 1;23 24 num Func(num, flag1, flag2, flag3);25 cout num num endl; 26 27 return 0;28 }我们根据 3 个标志位质的不同执行不同的逻辑但现在只是 3 个标志位如果是 10 个20 个那我们也设置相同数量的形参吗这太麻烦也太浪费空间了。
那怎么办呢位图。一个 int 变量在该环境下是 32 位是否可以表示为 32 种状态呢为了简单就拿 8 位举例
#define FLAG1 1 // 0000 0001
#define FLAG2 2 // 0000 0010
#define FLAG3 4 // 0000 0100不同的状态对应的位置我就取 1 如果这 3 个状态我都想要呢那就利用 | 或对他们进行运算
FLAG1 | FLAG2 | FLAG3 // 0000 0111可以看到这就代表这三个状态我都表示并且不同的状态之间是相互不干扰的。
根据这个方法我们更新上述代码 1 #include iostream2 using namespace std;3 4 #define FLAG1 1 // 0000 00015 #define FLAG2 2 // 0000 00106 #define FLAG3 4 // 0000 01007 8 // 利用 操作判断是否选取该状态9 int Func(int num, int flags){10 if(flags FLAG1){11 num num 1;12 }13 if(flags FLAG2){14 num num - 1;15 }16 if(flags FLAG3){ 17 num num * 2;18 }19 20 return num;21 }22 23 24 int main(){25 int num 10;26 27 num Func(num, FLAG1 | FLAG3);28 cout num num endl;29 30 return 0;31 }flags 参数就采用了位图的方式我将列举我们常用的选项
O_RDONLY以只读方式打开文件。O_WRONLY以只写方式打开文件。O_RDWR以读写方式打开文件。O_CREAT如果文件不存在则创建它。需要 mode 参数来指定新文件的权限。O_TRUNC如果文件已存在且为只写或读写模式打开则清空文件内容。O_APPEND以追加方式打开文件数据会被写入到文件尾。
mode 参数 — 文件权限 关于文件权限的知识在这里就不展开细说了这样的话篇幅太长了也不易消化。我专门有一篇文章 点击查看 详细地介绍了文件权限相关的知识大家有兴趣可以看一下哈。 前置知识结束了现在我们可以进入正题了我们以写的方式创建一个新的文件 16 // 写方式打开文件并且文件不存在就创建一个文件的权限是 rw-rw-r--17 int fd open(log.txt, O_WRONLY | O_CREAT, 0666);18 if(fd -1) perror(open);注意这里就可以看出系统调用接口和编程接口的区别在 C语言中的 fopen 函数当文件不存在时会为我们自动创建一个文件而系统调用接口就需要我们指定。
2.2 write 函数 write 函数的参数就稍显简单一些
ssize_t write(int fd, const void *buf, size_t count);fd : 要写入数据的文件描述符。buf指向要写入数据的缓冲区的指针。count要写入文件的字节数。返回值写入错位时返回 -1
我们就直接写入内容吧 20 // 写入指定信息21 const char* msg Its a test!!!\n; 22 ssize_t ret write(fd, msg, strlen(msg));23 if(ret -1) perror(write);2.3 运行结果 系统按照我们的需求创建了一个指定权限的文件 并且为我们写入了相应的内容 3. 提出几个为什么 我们需要根据现有的现象来发现问题不断地问自己为什么这才能提升自己。
3.1 fd 文件描述符是什么 我们通过接受 open 函数返回的 fd 文件描述符并将 fd 传入到 write 函数的参数中就可以向指定文件写入内容这是为什么呢怎么就可以根据一个 fd 的整数向指定文件写入内容呢
3.2 open 函数的返回值 我们先看一段代码 21 int main(){22 int fd1 open(log1.txt, O_WRONLY | O_CREAT, 0666);23 printf(fd1: %d.\n, fd1);24 25 int fd2 open(log2.txt, O_WRONLY | O_CREAT, 0666);26 printf(fd2: %d.\n, fd2);27 28 int fd3 open(log3.txt, O_WRONLY | O_CREAT, 0666);29 printf(fd3: %d.\n, fd3);30 31 int fd4 open(log4.txt, O_WRONLY | O_CREAT, 0666);32 printf(fd4: %d.\n, fd4); 33 return 0;34 }这段代码的输出结果是 fd1: 3. fd2: 4. fd3: 5. fd4: 6. 我们知道 open 函数的返回值是 fd 但为什么他们的值是连续的并且唯独缺少 0, 1, 2这是巧合吗如果一次运行时这样有理由怀疑是巧合。但是我们多次运行还是这样那就肯定不是了。 4. struct file 结构体 当一个文件没有被任何进程调度时该文件就静静的躺在你的磁盘中。但是当文件被调度时就会被送往内存中所以内存中的文件就只是文件的内容吗肯定不是同一时间内存中肯定存在着大量需要操作的文件操作系统需要高效的管理文件那怎么办呢 使用 结构体 双链表 的结构类似于管理进程的结构 在这篇文章有较详细的说明点击查看。
4.1 struct file 中的内容 该结构体中包含了文件的基本信息包括
f_op指向一个 file_operations 结构体的指针该结构体包含了指向文件操作函数的指针如 read、write、open、release 等。f_count表示有多少进程或文件描述符引用了这个文件。当 f_count 降到 0 时文件将被关闭。f_flags包含文件的标志如 O_RDONLY、O_WRONLY、O_RDWR 等表示文件的打开模式。f_mode表示文件的访问模式只读、只写、读写。f_pos当前的文件偏移量表示下一次读写操作将从哪个位置开始。f_owner包含文件的所有者信息用于权限检查。…
4.2 相关的内核级别的缓存区 还有一个空间叫做 缓冲区 尽管不直接受到该结构体的管理但是和结构体紧密相关: 可以看出该 缓存 就是一块在内存中的空间那有啥用呢用处可大了
当用户请求读取文件时内核会首先检查缓冲区中是否已经缓存了所需的数据。如果是则直接从缓冲区中读取数据避免了磁盘访问的延迟。如果不是内核会从磁盘读取数据到缓冲区中并更新缓冲区的内容。
当用户更新文件内容时内核并不会直接将该内容立马更新到磁盘上而是先放在缓冲区等到累积到一定的量在一次写入磁盘减少 I/O 操作。
所以对于内存这种告诉设备来说内核级缓冲区通过减少对磁盘等低速设备的直接访问次数显著提高了文件I/O操作的性能。
4.3 理解 Linux 一切皆文件 相信大家肯定听过一句话Linux 中一切皆文件。但是对于我们的键盘鼠标显示屏来说他们确实是实实在在的硬件呀我该怎么把它看作文件呢
属性 方法 对于任何的硬件设备都离不开两个概念 属性 方法但是本章节我们主要关注该结构体的 方法 。Linux 将一切的对象都视作一个文件那么就拿键盘举例吧请问键盘这个文件有什么方法呢无非就是 读方法 或者是 写方法键盘你能读我理解写方法 又是什么呢键盘确实没有写方法该方法置空不就好啦
所以我们可以这么看待硬件 对于操作系统来说对于普通文件的管理使用的是一个 struct file 里面包含文件的基本属性以及读写方法等对于硬件我也可以一同看待呀也还不是硬件的基本属性以及读写方法 所以说当操作系统调用一个文件的时候才不关心该文件本质是硬件还是啥我该使用读方法就使用读方法改写就使用写方法反正所有文件的操作函数接口是一致的。
这就是使用 struct 封装的好处尽管底层千差万别但是上层的调用都是一致的 5. fd 含义以及文件描述符表 上面说到操作系统会将系统调度的所有文件的 struct 利用双链表的形式管理起来。我们的一个进程可能同一时间调度多个文件又该如何管理呢
5.1 文件描述符表 进程会将自己所控制的所有文件的 struct 结构体的指针放在一个文件描述表中:注该表中包含其他信息但在本文中不关注。 该表包含一个结构体指针数组每一个存储的元素就是你所控制文件结构体的指针该表可以在进程的 PCB 在 Linux 下叫做 struct_task中找到。
5.2 fd 的含义 到这里就不难理解 fd 的含义了他为你想要操作文件的结构体指针在结构体指针数组里面的下标。依靠他就能找到想要操作文件的位置环环相扣
5.3 fd 的分配方式 那进程新加入文件描述符表的文件怎么分配 fd呢会从上到下遍历该表哪个位置是空闲的就放到哪个位置。 6. 解答疑惑 有了一定的知识基础之后我们尝试解决在 3 中提出的问题。
6.1 fd 文件描述符是什么 进程新使用的文件会将该文件的结构体指针放入进程描述符表的结构体指针数组中该 fd 就是指针放入结构体指针数组的下标位置。有点绕口哈
6.2 open 函数的返回值 为什么 open 函数的返回值缺少 0, 1, 2 呢因为这几个文件描述符已经被标准输入stdin、标准输出stdout和标准错误输出stderr在进程启动时占用。
0 通常被分配给标准输入 键盘stdin1 通常被分配给标准输出 显示器stdout2 通常被分配给标准错误输出 显示器stderr
怎么证明呢 证明 1 14 int main(){15 16 const char* msg Its a test!!!\n;17 ssize_t ret write(1, msg, strlen(msg)); 18 19 return 0;20 }
我们直接向 1 对应的文件写入信息运行成功在显示器打印 Its a test!!! 证明 2 13 int main(){14 15 umask(0);16 17 close(1);// 关闭显示器文件18 19 // 写方式打开文件并且文件不存在就创建一个文件的权限是 rw-rw-r--20 int fd open(log.txt, O_WRONLY | O_CREAT, 0666);21 if(fd -1) perror(open);22 printf(fd %d.\n, fd); 23 24 25 return 0; 26 }在这里我们关闭 1 对应的文件之后我们打开一个文件并打印。 最后我们发现并没有输出任何内容到屏幕上反而内容在 log.txt 文件上
大家思考一下这是为什么 7. 总结 在这篇文章中介绍了部分文件系统的内容希望大家有所收获