深入探究文件IO
Linux 系统如何管理文件
静态文件与 inode
文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、 U 盘等外部存储设备, 文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件
文件储存在硬盘上,硬盘的最小存储单位叫做“扇区” (Sector),每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块” (block)。这种由多个扇区组成的“块” ,是文件存取的最小单位。 “块” 的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。
我们的磁盘在进行分区、格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据;另一个是 inode 区,用于存放 inode table(inode 表), inode table 中存放的是一个一个的 inode(也成为 inode节点),不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode
一个 inode 都有一个与之相对应的数字编号,通过这个数字编号就可以找到 inode table 中所对应的 inode。
文件打开时的状态
当我们调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区) ,并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、 缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件。
当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了, 数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中。
打开一个大文件的时候会比较慢;
文档写了一半,没记得保存,此时电脑因为突然停电直接掉电关机了,内容丢失因为磁盘、硬盘、 U 盘等存储设备基本都是 Flash 块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节) ,一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中, 所以导致对块设备的读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活
在 Linux 系统中, 内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(PCB)
PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors), 文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息
返回错误处理与 errno
当判断函数执行失败后,会调用 return 退出程序,但是对于我们来说,我们并不知道为什么会出错,什么原因导致此函数执行失败, 因为执行出错之后它们的返回值都是-1。
难道我们真的就不知道错误原因了吗?其实不然,在 Linux 系统下对常见的错误做了一个编号,每一个编号都代表着每一种不同的错误类型,当函数执行发生错误的时候, 操作系统会将这个错误所对应的编号赋值给 errno 变量, 每一个进程(程序)都维护了自己的 errno 变量, 它是程序中的全局变量
:
#include <stdio.h>
#include <errno.h>
int main(void)
{
printf("%d\n", errno);
return 0;
}strerror 函数
这里介绍一个 C 库函数strerror(), 该函数可以将对应的 errno 转换成适合我们查看的字符串信息
#include <string.h>
char *strerror(int errnum);从打印信息可以知道, strerror 返回的字符串是"No such file or directory",所以从打印信息可知,我们就可以很直观的知道 open 函数执行的错误原因是文件不存在!
perror 函数
除了 strerror 函数之外,我们还可以使用 perror 函数来查看错误信息
#include <stdio.h>
void perror(const char *s);exit、 _exit、 _Exit
程序在执行某个函数出错的时候,如果此函数执行失败会导致后面的步骤不能在进行下去时, 应该在出错时终止程序运行,不应该让程序继续运行下去,那么如何退出程序、终止程序运行呢?
在 Linux 系统下,进程(程序)退出可以分为正常退出和异常退出,注意这里说的异常并不是执行函数出现了错误这种情况,异常往往更多的是一种不可预料的系统异常,这里我们只讨论正常退出的情况 在 Linux 系统下, 进程正常退出除了可以使用 return 之外, 还可以使用 exit()、 _exit()以及_Exit()
_exit()和_Exit()函数
调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构, 关闭进程的所有文件描述符, 并结束进程、将控制权交给操作系统
#include <unistd.h>
void _exit(int status);exit()和_Exit()两者等价,用法作用是一样的,这里就不再讲了,需要注意的是这 2 个函数都是系统调用。
exit()函数
exit()函数_exit()函数都是用来终止进程的, exit()是一个标准 C 库函数, 而_exit()和_Exit()是系统调用。该函数是一个标准 C 库函数,使用该函数需要包含头文件<stdlib.h>
空洞文件
用 lseek 可以修改文件的当前读写位置偏移量,使用 write()函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写入数据,也就意味着 4096~6000 字节之间出现了一个空洞, 因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。
O_APPEND 和 O_TRUNC 标志
O_TRUNC 这个标志的作用非常简单,如果使用了这个标志, 调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0;
接下里聊一聊 O_APPEND 标志, 如果 open 函数携带了 O_APPEND 标志, 调用 open 函数打开文件,当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾, 从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。
多次打开同一个文件
一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符fd,同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符。
一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。
复制文件描述符
在 Linux 系统中, open 返回得到的文件描述符 fd 可以进行复制, 复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限
因为复制得到的文件描述符与旧的文件描述符指向的是同一个文件表,所以可知,这两个文件描述符的属性是一样,譬如对文件的读写权限、文件状态标志、文件偏移量等,所以从这里也可知道“复制”的含义实则是复制文件表。 同样,在使用完毕之后也需要使用 close 来关闭文件描述符。
dup 函数
dup 函数用于复制文件描述符
#include <unistd.h>
int dup(int oldfd);dup2 函数
dup 系统调用分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件描述符,这是 dup 系统调用的一个缺陷;而 dup2 系统调用修复了这个缺陷,可以手动指定文件描述符,而不需要遵循文件描述符分配原则,当然在实际的编程工作中,需要根据自己的情况来进行选择。
#include <unistd.h>
int dup2(int oldfd, int newfd);文件共享
所谓文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode) 被多个独立的读写体同时进行 IO 操作。 同一个文件对应两个不同的文件描述符 fd1 和 fd2,当使用 fd1 对文件进行写操作之后,并没有关闭 fd1,而此时使用 fd2 对文件再进行写操作,这其实就是一种文件共享。
文件共享的核心是:如何制造出多个不同的文件描述符来指向同一个文件。
常见的三种文件共享的实现方式
- 同一个进程中多次调用 open 函数打开同一个文件
- 不同进程中分别使用 open 函数打开同一个文件
- 同一个进程中通过 dup(dup2)函数对文件描述符进行复制
原子操作与竞争冒险
Linux 是一个多任务、多进程操作系统,系统中往往运行着多个不同的进程、任务, 多个不同的进程就有可能对同一个文件进行 IO 操作,此时该文件便是它们的共享资源,它们共同操作着同一份文件;
竞争冒险简介
本小节给大家竞争冒险这个概念,如果学习过 Linux 驱动开发的读者对这些概念应该并不陌生, 也就意味着竞争冒险不但存在于 Linux 应用层、也存在于 Linux 内核驱动层。
设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据),每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志,
以上给大家所描述的这样一种情形就属于竞争状态(也成为竞争冒险),操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的, 因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配, 这就是所谓的竞争状态。
原子操作
既然存在竞争状态,那么该如何规避或消除这种状态呢?接下来给大家介绍原子操作
阿源的知识库