文件操作
事实上我们C语言和C++的文件操作都已经历许多,接下来就在回顾C语言文件操作同时,认识系统调用接口的文件操作。进而理解文件的概念和缓冲区的概念。
C语言文件操作
要对一个文件操作首先要打开一个文件:

我们常用的代开操作就是fopen。输入要打开文件的路径(默认会在前面加上当前进程工作路径),以及打开的形式:

总共有六种打开方式,我们简单认识其中三种:rwa。
r是以只读形式打开文件,所打开的文件必须要存在。
w是以只写形式打开,打开文件时会自动清空文件。所打开文件如果不存在就会自动创建一个新的文件。
a则是以追加写的形式打开文件,打开文件时不会清空文件。所打开文件如果不存在就会自动创建一个新的文件。
如何理解打开文件
平时我们在任务资源管理器里也经常打开文件,那么c语言和操作系统的打开文件是否和我们是同一回事呢?
其实在前面冯诺依曼结构学习中我们知道一件事:对文件操作实际上是对硬件操作,但是用户是没有资格对硬件操作的,只有操作系统可以。因此打开文件实际上是将文件拷贝进内存文件缓冲区,加载到对应进程,由操作系统对其进行读写操作,之后再由操作系统将内存中文件缓冲区拷贝回文件。
Linux中一切皆文件。既然如此,键盘、显示器、磁盘这些都会看成文件。我们要对其统一封装成一个文件类,打开文件就是在进程pcb的打开文件列表中加上这个文件。随后进行文件操作也是对该列表上文件操作。
言归正传,我们演示一下简单的c语言文件操作:



可以看到我们一开始是没有这个文件的,然后我们以字符串形式写入了这些数据。
来简单尝试一下读操作吧:



完美达到了预期。
系统文件操作


这说明我们要接收open的返回值依此来close.
注意到write能传入一些flags,应该是和mods一样的功能,我们来看看有什么flags吧:

因此我们如果想效仿fopen中只写方式打开文件,就需要传入O_WRONLY O_CREAT,但是这样打开不会默认清空文件,因此还需要传入

当然我们的open还有第三个参数mode,这个是当文件是新创建时才需传入。传入的是八进制的权限表示。当然也会收到权限掩码的干预,我们也可以调用c语言库函数设置该进程的掩码:

尝试写入:



依旧完美达到了预期。
输出重定向
注意到我们system call的open返回值是一个整型,那么这些整型到底是什么?我们不妨打印一下:


原来如此,竟然是一堆序号啊!
事实上我们每个进程都会维护一个文件列表,类似如下:

那么我们的012是什么呢?
我也不卖关子了,其实012分别是stdin、stdout、stderr,第一个是键盘,后两个是显示器。我们可以简单论证一下:


可以看到原本输出到stdout的内容输出到了log.txt。
原因是我们将fd=1的stdout关闭了,然后打开log.txt就给他分配空缺的fd=1.此时对stdout输出就是对log.txt输出。至于为什么代码需要加上fflush,这个后面缓冲区部分再做讨论。
dup2
所以说我们的输出重定向本质就是将原本要输出给stdout的数据转而输出给其他文件。我们系统调用中也有可以实现输出重定向的接口dup2:


这里需要注意,我们是将oldfd赋值给newfd。因此我们如果想将输出给stdout的数据重定向到log.txt,就需要将log.txt赋值给stdout。相当于fd=1的文件指针指向log.txt:


异常标准地达到了我们想要的效果。
1> 2> 1>&2
可能有读者会想,既然1是stdout是显示器,那stderr的作用是什么呢?
先看一段代码:


可以看到都输出到了显示器。那如果我们重定向到log.txt呢?

靠北,这怎么搞的?
怎么一半在显示器,一半在文件里?
注意到stderr都在显示器,stdout都在文件里?
事实上>全称是标准输出重定向,是1>的简写,相当于执行了dup2(fd("log.txt"),1),因此只有stdout被修改指向了,stderr没有。
- 有了这样的功能,我们就能一下分开正确输出和错误输出,方便我们调试代码。
除此之外,我们还可以像这样用重定向:


不过注意到stderr的数据被覆盖了,因为我们写的方式是从头开始写自然会覆盖,正确的做法可以这样:

也可以这样:

2>&1相当于dup(1,2)
缓冲区
先说结论,C语言内的缓冲区和内存缓冲区是不一样的。
我们调用c语言接口io函数是写入c语言的用户级缓冲区,调用系统接口io时写入内核级缓冲区。
来看一段简单的代码:


这是因为,我们写入的字符串存在了用户级缓冲区,如果我们用的是c语言的fclose,确实会自动冲刷缓冲区到内核缓冲区。但是我们直接用系统调用的close,所以把内核上文件列表的log1.txt关闭了,用户级缓冲区的内容还没来得及冲刷到内核缓冲区。
刷新缓冲区策略
不管是用户级缓冲区还是内核级缓冲区都有一套相同的冲刷缓冲区策略:
- 立即刷新,如fflush、fsync
- 行刷新,如显示器
- 全缓冲,满了才刷新,如普通文件
特殊情况
进程退出,os会自动刷新;强制刷新。
我们来简单验证一下:


没有问题,稍微修改一下代码:


打印顺序和数量都对不上了,这是怎么回事?
很显然我们分析一下,前面输出给stdout是行刷新,因此我们每输出一行就对内核刷新,内核也是一行就对文件输出。因此fork之后,缓冲区已经没有内容了。
但是log.txt是普通文件,一开始printf和fprintf输出的内容都在用户级别缓冲区中,write则是在内核级缓冲区中。创建进程后,用户级缓冲区拷贝了一份,因此结束程序会自动刷新两份缓冲区内容到内核缓冲区中,最后再刷新到文件中!
文件系统
理解操作系统如何管理文件前,我们需要理解其物理结构。文件通常存储在磁盘中(当然现在大多数pc和laptop都是固态硬盘)。
一个磁盘有很多磁片,每个磁片正反面都能存储信息,每一面又以圆心分成不同的同心圆,这些同心圆称为磁道或者柱面,每个磁道又分成若干扇形区域,这些扇形区域称为扇区,这些扇区读写的最小单位,一般大小为512kb。
因此我们要确定一个文件所在位置,就需要确定其所在的柱面(Cylinder)、扇区(Sector),以及哪一面,由于磁片的每一面都有磁头(Header),因此我们只需找到对应的磁头即可。
这种定位方式称为CHS定址法。
操作系统将所有的扇区按物理逻辑排列成一个数组,只要对该数组进行管理就相当于对磁盘进行管理。同时一个扇区才512kb太小了,操作系统通常将8个扇区也即是4kb看成一个整体块,块就是操作系统读写的最小单位。
此时我们想找到一个文件的物理地址,就只需要知道文件对应的块号然后就可以转换成CHS,这种地址称为LBA逻辑区块地址(Logical Block Address)
一个磁盘太大,操作系统对其分区管理,类似windows系统将磁盘分为C盘、D盘这些。
因此我们只需研究每个分区的文件系统即可。
每个分区我们又将其分成若干组,现在研究其对组管理即可,分组类似:

其中Block group 0就是该分区的第一个组。
文件的数据就存储在Data blocks中,Block Bitmap就是Data blcoks的块位图,记录哪个块被使用,哪个没有。
- ps:数据可以跨组存放,但不能跨区存放
inode Table存放的是文件的属性,里面有inode结构体存放数据的各种属性,如文件类型、权限、acm时间、对应的Data blocks位置。通常inode结构体大小是128字节(Ext2),也就是一个块能放32个文件的属性。
每个inode都有其对应的编号,并且改编号在当前区 是唯一的:

而且inode结构体里面不存放文件名,所以我们只能通过文件的inode号找到文件!
inode BItmap就是记录哪个inode没被占用的位图。
Group Descriptor Table记录的是这个组的所有信息,包括位图,占用空间大小等。
Super Block记录的是整个分区的信息。按照效率来说Super Block理应只有一个,但是考虑文件系统的鲁棒性,一个分区通常有两三个组会有Super Block。
既然inode里不存储文件名,那么我们平时是如何找到对应的文件的?
事实上,文件夹 中会存储inode和文件名的映射关系.
这就是为什么同一个文件夹下的文件不允许重名。
因此我们操作系统只需记录根目录的inode就可以找到所有文件。而且对于常用的路径还会进行缓存方便查找。
目前还有一个问题,我们的inode号是分区唯一的,但我们如何找到对应的分区呢?事实上,Linux也和windows一样将分区挂在一个目录后面,类似于/C /D /E这样,因此我们只需要检测一个路径的前缀就可以找到其对应的分区。
软硬链接
软链接
我们可以通过指令ln -s 源文件 目标文件 形成软链接:

可以看到我们的软链接文件有独立的inode,因此是一个独立的文件。
其用法相当于windows的快捷方式,里面存放着test.txt的路径,能通过软链接直接找到该文件。
就好像一个文件存放在较深的路径中,我们可以在桌面创建一个快捷方式以快速找到他。
硬链接
我们可以通过指令ln 源文件 目标文件 形成硬链接:

可以看到硬链接和源文件有着一样inode,说明他们指向的是同一个文件。硬链接本质是在当前目录下形成一个inode和文件名的新映射。
文件权限旁的是文件的硬链接数,删除其中一个硬链接文件,对其他硬链接文件不会产生影响:

新建一个文件夹的硬链接数默认是2:


这是因为文件夹里的.默认是当前文件夹的硬链接。
因此我们可以通过目录的硬链接数-2知道该目录下有多少目录。
硬链接的作用:
- 构建Linux的路径结构,让我们可以通过.和...来进行路径定位
- 一般用硬链接来作为备份。