目录
一、缓冲区
1.1 概念
1.2 用户缓冲区和内核缓冲区
二、磁盘的结构
三、文件系统
3.1 初识“块”和inode
3.2 磁盘分区和文件系统
一、缓冲区
1.1 概念
要理解什么是缓冲区,先看这段代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *fstr = "fwrite\n";
const char *str = "write\n";
printf("printf\n");
fprintf(stdout, "fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
write(1, str, strlen(str));
fork();
return 0;
}
执行程序,会向终端打印四行内容
但是如果我们把程序输出的内容重定向到一个普通文件中,会打印多少行呢?
此时变成了7行内容,其中printf、fprintf和fwrite打印了两次,为什么?
我们可以看到,在程序的最后创建了一个子进程,所以肯定是因为子进程所以才会重复打印
并且重复打印的接口都是库函数,而系统调用接口write并没有打印两次
在前面的学习中,我们已经初步认识了什么是缓冲区,这里简单回顾一下
实际上我们在对一个文件进行写入时并不是直接写入到文件中的,而是先写入到缓冲区,缓冲区再根据不同的方式将其内容刷新到文件中
缓冲区有三种刷新方式:
- 无缓冲,即一进行写入就对缓冲区进行刷新,一个一个字符写入
- 行缓冲,遇到\n才对缓冲区进行刷新,一行一行写入
- 全缓冲,缓冲区满了才进行刷新,一堆一堆写入
一般我们在向显示器文件进行写入时采用的策略是行缓冲,即遇到\n就打印一行;而对于普通文件系统则采用全缓冲策略。缓冲区的存在能够减少消耗,提高效率
而我们还知道,子进程虽然会继承父进程的代码和数据,不过一旦一方修改了数据,就会发生写时拷贝
而缓冲区不同的刷新方式配合上写时拷贝,你是否已经知道为什么会发生重复打印了呢?
没错,因为向显示器文件中打印的时候采用行缓冲策略,所以缓冲区中不会有内容堆积,遇到\n就刷新一次缓冲区
所以在最后创建子进程时父进程的缓冲区空空如也,所以子进程的缓冲区也是空的
但是,如果重定向到了普通文件,此时就要采用全缓冲策略,所有的内容都堆积在缓冲区中
子进程创建时继承了父进程包含一大堆内容的缓冲区,而父进程在退出时刷新缓冲区的时候发生写时拷贝,不会影响到子进程的缓冲区,此时子进程再进行写入,就导致部分内容被写入了两次
但是为什么write不会重复写呢?
1.2 用户缓冲区和内核缓冲区
我们观察到,在向普通文件中打印时,库函数被打印了两次,而只有系统调用接口write打印了一次
为什么?
实际上C语言中的库函数写入的缓冲区是C语言提供的用户级缓冲区,而write这种系统调用接口直接写入的是内核的缓冲区
父子进程间的写时拷贝局限于用户态,不会影响到内核缓冲区
C语言的部分文件操作接口封装了系统调用接口write,库函数首先将内容写入到用户级缓冲区,然后根据不同的刷新方式调用write将内容写入到内核的缓冲区
我们再来看一段代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *fstr = "fwrite\n";
const char *str = "write\n";
printf("printf\n");
fprintf(stdout, "fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
write(1, str, strlen(str));
close(1); // 关闭标准输出流
return 0;
}
将打印的内容重定向到普通文件,此时发现只有write被打印了
这也是因为close关闭文件描述符时只会把内核缓冲区中的内容刷新到文件中,所以库函数向用户缓冲区写入的内容采用全缓冲还停留在用户缓冲区内,而write向内核缓冲区写入的内容被刷新到了文件中
C语言提供的用户缓冲区在哪呢?前面我们见过C语言提供的FILE结构体,存放了文件描述符等内容,其内部还有对应文件的缓冲区字段和维护信息
语言都属于用户层,C语言提供的FILE结构体自然也属于用户,所以其内部的缓冲区属于用户级缓冲区
二、磁盘的结构
磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失。早期计算机使用的磁盘是软磁盘(Floppy Disk,简称软盘),如今常用的磁盘是硬磁盘(Hard disk,简称硬盘)
文件在没被打开的时候,是以二进制形式存储在硬盘中的,而硬盘又是如何存储这些二进制数据的呢?
机械硬盘的内部结构:
一个硬盘里可能堆叠了多张盘片,盘片的每一面都有一个磁头,这些所有的磁头都是连在同一个磁头臂上的,所以只能共同移动。磁头和盘面不接触
盘片表面涂有磁性物质,通过输入不同的电流方向,来改变不同部位的磁性,从而就能区分出0和1
而一个盘面又分为许多不同半径的同心圆,每个圆环是一个磁道,磁道的内部又分为许多的扇区
每个扇区能够存储的内容大小一样,其中,最内侧磁道上的扇区面积最小,因此数据密度最大
每张盘片的大小相同,而在不同的盘片中处于相同位置(同样半径)的磁道组成了柱面
磁盘被访问的最基本单元是扇区,一个扇区一般是512字节
因此,我们可以把磁盘看作由无数个扇区构成的存储介质
既然有了扇区,要把数据保存到磁盘中或取出数据,首先要解决的问题是:如何定位一个扇区
要定位一个扇区,我们得先知道要保存到哪一面(即哪个磁头),和哪一个磁道(柱面)
因此产生了CHS寻址方式:依据柱面(磁道)、磁头、扇区来查找数据位置
首先定位对应数据所在的盘面,移动对应盘面的磁头 ;磁头臂带动磁头在盘面上移动,就是定位磁道的过程;而主轴带动盘片旋转,则是定位扇区的过程
如果我们像磁带一样把磁盘的所有盘面展开成一个线性的结构,那么整个磁盘就像是一个由无数扇区构成的数组,每个扇区就有自己独立的编号,我们称为LBA地址
现在我们可以把磁盘视为一个数组,只需要知道每个盘面有多少个扇区,每个磁道有多少个扇区,我们就可以通过某个扇区的下标找到其对应的磁头、磁道、扇区了
例如我们假设一个盘面有20000个磁道,一个磁道有400个扇区
通过计算,我们就可以把LBA地址转化为CHS地址了
三、文件系统
3.1 初识“块”和inode
一个文件如果为空,那么它存储在磁盘中是否不会占用空间呢?答案是否
文件不单单包含文件内容,还包含文件属性。文件的内容由一个个数据块存储,而文件的属性则需要一个名为inode的结构保存
数据块(block)的大小通常为4KB,即8个扇区组成一个块。块是文件存取的最小单位,即操作系统在读取硬盘时不会一个个扇区存取,这样影响效率,而是最少取一个块。文件的属性则由名为inode的结构存储,其内部存放了inode编号、文件类型、文件权限等属性
每个数据块也有编号,因为文件的内容存储在许多的数据块中,inode中一定会有一个数组存储该文件的所有块编号。但是如果块很多,一个数组大小不一定存的下这么多编号
为了让数组存下整个文件的所有块编号,数组后几个位置存在多级索引,具体实现为其编号对应的块中存储的不是文件内容,而是其他块的编号。在逻辑上体现为一颗多叉树的结构
需要注意的是,inode中的属性不包含文件名,每个inode都有自己唯一的编号,系统中标识一个文件只看inode编号
我们也可以通过 ls -li 命令来查看一个文件的inode编号:
除此之外,我们还可以通过stat命令来获取更详细的文件属性信息:
3.2 磁盘分区和文件系统
我们发现,即使我们的电脑里只有一块硬盘,但也能分出很多个区,即我们电脑中的C盘、D盘等,并且我们还可以自己设置这些分区的大小
但是这些分区还是太大了,不便于管理,因此我们再将一个分区划分为多个块组
在Linux的ext2文件系统中,Boot Block(引导块)位于分区的开头,占用1024字节,用于存放开机管理程序
而每个Block group又细分为
其中:
- Super Block(超级块):存放文件系统本身的结构信息,主要有block和inode的总量、未使用的block和inode的数量、单个block和inode的大小、最近一次写入数据的时间等等。Super Block对于文件系统而言是至关重要的,所以ext2文件系统会在多个块组中保存超级块的信息
- Group Descriptor Table(GDT):块组描述符表,描述块组的属性信息
- Block Bitmap(块位图):记录Data blocks中哪些数据块已被使用,哪些数据块未被使用
- inode Bitmap(inode位图):记录inode的使用情况
- inode Table(inode表):存放inode
- Data blocks:存放数据块
所以对一个文件的增删查改,实际上就是对文件系统的操作
新建一个文件,在inode位图中找到最近一个未被使用的位置来分配inode并初始化信息,然后根据文件的大小分配数据块并修改inode位图和块位图结构
删除一个文件,将文件所属的inode位图和块位图中的位置修改为0即可(删除即允许被覆盖)
查找一个文件,通过其路径(哪个盘)就可以找到所在分区,然后根据inode编号知道其属于哪个块组的,在inode的位图中确认其是否存在,确认存在就在inode表中取出其inode中的信息并载入内存,通过这些信息找到文件所属的数据块,将这些块拼接得到文件内容
修改一个文件,还是先找到其inode,然后修改对应的属性,如果文件大小发生变动还需要决定是否新增数据块或删除数据块并修改位图
但是问题来了:我是用户,只用文件名,我怎么知道文件的inode编号?
在Linux下一切皆文件,目录也是文件,也有自己的内容和属性,和自己的inode编号
而目录的内容,就是该目录下文件的文件名与inode编号的映射关系!
这也解释了为什么同一目录下不能有同名文件,就像map中不能有相同的key一样
但是,要找一个文件得先知道其目录的inode编号,但是其目录的inode编号又被这个目录的父目录管理着,还得知道父目录的inode编号...一路下去我们就得先从根目录开始找起
这样的方式一定程度上会影响效率,所以系统把我们常访问的目录存到dentry缓存中
简单的介绍了一下文件系统,如有错误欢迎在评论区指出
完.