前言
什么是缓冲区?
- 缓冲区是内存空间上的一小段内存,我们平常在写程序的时候,其实是很难感知到缓冲区的存在的,接下来看一段代码,可以很好地体现缓冲区的存在。
#include<stdio.h> #include<unistd.h> int main() { printf("这是我关于缓冲区的一次小测试"); sleep(2); return 0; }
运行后
2秒后
按照我们对于平常代码执行顺序的理解,我们应该先打印printf中的文本信息,在执行sleep函数休眠2秒,但实际并不是这样,事实恰恰相反。
对
对上述代码做一次改动,再次运行
#include<stdio.h> #include<unistd.h> int main() { printf("这是我关于缓冲区的一次小测试\n"); sleep(2); return 0; }
我们添加在文本信息后添加了换行符"\n",
结果
这一次,符合我们心中的预期,先打印文本信息,在进行休眠。
这其实就是缓冲区在发挥作用,有人说,缓冲区让我对代码执行产生误解,我摸不清代码怎么执行了,为什么要存在它?
为什么会有缓冲区,缓冲区的作用是什么?这就是今天我们所探究的。
缓冲区
缓冲区是什么?
- 缓冲区是内存空间上的一段内存,大小由操作系统分配。
- 一个暂时存储数据的区域
缓冲区存在于哪里?
- 缓冲区存在于内存空间
1.为什么要有缓冲区呢?
缓冲区是一个暂时存储数据的地方,当数据足够,就会向目标地点进行输送,我们现实生活中也存在这样的地方,就是我们日常的快递站。
当你需要将礼物送给你的朋友,而你们相距甚远的时候,比如一个在江西,一个在黑龙江,你会亲自将礼物送到他手上吗?
答案显然是否定的,那样成本不仅高,还会耗费你大量的时间,这时候,我们都会选择——寄快递,让快递员替我们将礼物送出,当你将快递交给快递站,他们也不会立刻将你的快递发出,送一个快递,对他们来说和让你自己送没有区别,不过耗费的不是你的时间和成本,快递站也有他们自己的寄送规则。
快递站寄送规则
- 立即寄送
- 等到一定数量寄送
- 存满快递寄送
以上三种规则,第一种时间快,但成本太大,收益几乎没有,甚至倒贴;
第二种,不仅能有收益,还能保障效率;
第三种,牺牲时间换取收益,收益提高了,但用户等待的时间很长。
快递站的这三种寄快递规则,也就相当于我们缓冲区的推送(刷新)数据规则
- 实时刷新
- 行刷新
- 存满刷新
通过上述,我们理解的缓冲区的作用
- 提高效率,暂时存储数据,待到数据数量达到一定时,刷新推送数据到磁盘文件当中,避免了多次读写带来的时间损耗,降低效率
缓冲区的运作过程
当一个文件被进程打开的时候,它就不存在于磁盘之上,而是存在于内存之上,这是因为操作系统会将其属性和内容均加载到内存上,属性我们知道,操作系统内核会形成一个struct file类型的结构体来保存属性。
那文件内容呢?
- 被操作系统拷贝到缓冲区当中,内核文件管理不仅会创建文件结构体,还会为文件内容分配一段内存空间,也就是我们的缓冲区,将文件内容放入其中,并为文件结构体添加相关指针,用来管理这一段内存空间。
- 当进程对文件内容进行操作的时候,实际并不是对磁盘上的文件进行操作,而是对内存上的文件进行操作,修改的,增加的,或者删除的内容,都是在对内存上缓冲区里面的内容操作,当操作结束,关闭文件,结束进程,会自动刷新缓冲区,将缓冲区内的内容拷贝到磁盘当中。
所以,缓冲区的运作过程
- 文件被打开,文件内容从磁盘上拷贝到缓冲区当中
- 用户进程对文件内容操作,实际是对内存中缓冲区中的内容操作
- 关闭文件,用户进程结束,缓冲区刷新,其中的数据被重新拷贝到磁盘文件中。
总结:对文件内容的操作,本质就是文件内容数据的来回拷贝。
缓冲区的刷新规则
通过上面的学习
我们知道缓冲区的刷新的三个规则
- 即时刷新
- 行刷新
- 全满刷新
在我们开头的代码中,为什么加换行符之前不刷新,加了换行符后立刻刷新呢?
除了等待进程退出,自动刷新缓冲区
我们还有两种方式刷新缓冲区
- 使用换行符号强制刷新
- 使用flush函数强制刷新
什么时候行刷新,什么时候全满刷新呢?
- 一般对于显示器文件,我们采用行刷新的策略
- 对于磁盘文件,我们一般全满刷新。
来看一个样例
#include<stdio.h> #include<unistd.h> #include<string.h> int main() { fprintf(stdout,"C: hello fprintf\n"); printf("C: hello printf\n"); fputs("C: hello fputs\n",stdout); const char*str="system call:hello write"; write(1,str,strlen(str)); fork(); return 0; }
当我们向显示器当中打印的时候
结果:
当我们向文件当中打印的时候
结果:
这是为何?
- 当我们向显示器中打印信息的时候,为了照顾用户体验,显示器的刷新方式默认为行刷新,并且每个打印语句都有\n,当执行到fork的时候,这时候的缓冲区已经被刷新完了,缓冲区当中已经没有数据了。
- 当我们重定向到文件中的时候,刷新方式变成了全缓冲,即等到缓冲区数据到达一定体量再刷新数据,切刷新方式为全缓冲的时候,缓冲区也会变大,这时候代码执行到fork语句的时候,缓冲区内还有数据。
- 子进程和父进程共享代码和数据,如果执行到fork函数的时候,父进程缓冲区中还有数据,子进程也会一并将其继承下来。
所以,当父子进程都退出的时候,父进程缓冲区的数据被刷新,子进程缓冲区的数据被刷新,子进程缓冲区的数据是继承父进程的,所以会重复打印文本信息。
C式缓冲区和内核缓冲区
当我们重定向,将信息输出到指定文件中的时候
我们可以发现,C语言函数接口的语句,每个都打印了两次,而系统接口函数的文本信息仅仅只打印了一次,这是为什么?
- 利用子进程会继承父进程数据的特点,我们也可以让子进程继承缓冲区中的数据,实现两次打印的效果,但这都是针对C语言函数接口而言的,对于系统函数接口write,它写入的文本信息其实并不在父进程的缓冲区当中,而是在内核缓冲区,直接和操作系统打交道的缓冲区,而父进程和子进程的缓冲区,则是C语言自己维护的缓冲区。
- 也就是说,此时共有三个缓冲区,父进程一个C语言缓冲区,子进程一个C语言缓冲区,一个内核缓冲区,父子进程的缓冲区中的数据是一样的,重复的,内核缓冲区的数据则是系统调用接口write写入的,独一份的,当进程退出的时候,父子进程缓冲区数据被刷入内核缓冲区,再和内核缓冲区的数据一同被刷新拷贝到磁盘文件当中。
缓冲区的作用就是提高效率,解决各个设备读写速度不匹配带来的低速拖累高速的情况,C式缓冲区存在的目的也是为了减少用户进程与内核缓冲区的交互,希望数据积攒到一定体量再写入内核缓冲区,就像内核缓冲区和磁盘文件的存储关系一样。
如果想直接写入内核缓冲区,就使用操作系统提供的系统调用接口,它们会直接对内核缓冲区读写,如read和write;如果想写入C式缓冲区,则使用C语言提供的调用接口,如printf和scanf。
总结
- 无论是C式缓冲区还是内核缓冲区,它们的存在目的都是为了避免频繁读写,从而提高整体效率
- 使用C调用接口,读写经过C式缓冲区;使用系统调用接口,读写跳过C式缓冲区,直接进入内核缓冲区。
磁盘上的文件管理
文件分为被打开、未被打开两种状态,被打开的文件加载到内存空间当中,由内核操作系统的文件系统同一管理,那未被打开的文件呢?
- 未被打开的文件存在于磁盘之上,由磁盘的文件系统管理
接下来,进入磁盘的文件管理学习!
磁盘物理结构
首先来认识一下,磁盘
磁盘由许多盘片组成,每个盘片由有正反两个面,每个面分配一个磁头,也就是说,有多少个盘面,就有多少个磁头。
俯瞰盘片
- 盘片分成许多空心圆,每个空心圆的边缘都是一圈磁道。
- 磁道平均分,每一段磁道就是扇区,扇区是磁盘存储数据的基本单位。
- 越内的扇区越小,但存储的数据量不变,因此,它的数据密度更大。
注:扇区是磁盘存储数据的基本单位,大小一般为512个字节。
CHS方法
- 定位磁道
- 定位盘片
- 定位扇区
磁盘逻辑结构
如图,将卷成圆形的磁带抽出来,就是一条长带。
同样,我们也可以将磁盘看成卷起来的磁带,将其扯出来,也是一条长带。
这是一种线性结构,因此,我们可以将磁盘看成一个数组,一个元素就是一个扇区,对文件的查找,就可以转换成对数组元素的查找!
磁盘的大小很大,而一个扇区的大小仅仅只有512字节,如果真正按照以扇区作为一个数组元素的方式划分,那么数组元素的数量将达到恐怖的数字,这个数量也是我们不愿意看到的。
借用现实生活中,我们分省分市的划分方法,为了更好地管理文件,磁盘管理系统对文件采用分区分组的管理方式。
先将磁盘分成几个大区,再在一个大区中分组,最后组中存储文件。
组中又分为几个块,这些块就是磁盘文件管理的核心,我们认识一下
- Data blocks:存储文件内容的块区,该块区中有许多数据块,数据块用来存储对应文件的内容,每个数据块大小为4KB(8个扇区)
- inode Table:存储的是struct inode结构体,每个结构体大小为128字节,该结构体中保存的是文件的inode编号,属性,数据块指针(指向Data blocks中的数据块,找到文件内容)
- inode Bitmap:文件位图,来判断一个文件是不是存在,如果该文件存在,对应比特位为1,否则为0。
- Block Bitmap:用来判断对应的数据块是否被占用,如果被占用,对应比特位为1,否则为0.
- Group Descriptor Table:存储的是当前组的信息,如属性,数据块使用量,文件量,组大小。
- Super Block:存放文件系统本身的结构信息。记录的信息主要有:大区中block和inode的总量,未使用的block和inode的量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息,Super Block的信息被破坏,该大区的文件系统结构就被破坏了
- 一般一个分区中,有好几个组都有Super Block,防止分区文件系统结构被破坏后无法被修复。
在组中,我们不止一次见到了inode。那么inode是什么?
- inode是文件编号,一个inode代表一个文件,它是文件的唯一标识。
inode虽然是文件的唯一标识,但我们在平常Linux文件操作中,却从未见过,一般都是靠文件名来查找,操作文件,这是为什么?
- inode是文件的唯一标识符,这是对内核而言的,内核通过inode对文件进行定位。
- 当文件被创建的时候,内核会为其分配inode,并为其文件名和inode建立映射关系,这个映射关系会被存储到该文件的目录文件当中。
- 当用户层使文件名操作文件,内核操作系统会先在该目录下找到对应目录关系,用inode替换文件名,再进行后续操作。
Linux操作系统怎么在磁盘中查找文件?
- 依靠文件路径查找文件,对于一个文件而言,文件路径是唯一的。
原理:
- 文件系统管理分区首先要进行挂载才能被使用,挂载点一般是一个目录,后序将该目录当成入口访问某个分区上的文件。
- 查找文件,依靠的是文件路径,通过目录进入分区,在通过文件名和inode的映射关系找到文件。
删除或者修改一个文件的本质是什么?
- 删除一个文件的本质,就是将其inode Bitmap中的比特位置为0,将其Block Bitmap中的比特位置0,这样在逻辑上,该文件就被删除了,当该inode被重新使用,数据块内容被覆盖,才是真正被删除。这也为恢复文件提供机会,前提是保存了inode。
- 修改一个文件的本质是通过struct inode结构体中的数据块指针找到对应的数据块,修改数据块中的内容。
创建一个文件的过程是怎么样的?
- 权限检查:
- 在创建文件之前,内核会检查当前用户是否有在目标目录中创建文件的权限。
- 这通常涉及读取目录的权限位和当前用户的权限。
- 分配inode和数据块:
- 如果权限检查通过,文件系统会为新文件分配一个inode,用于存储文件的属性(如文件大小、权限、时间戳等)。
- 同时,文件系统还会为新文件分配必要的数据块,用于存储文件内容。
- 更新目录结构:
- 文件系统会在目标目录中为新文件创建一个目录项,将文件名与inode关联起来。
- 这通常涉及更新目录的数据块,以包含新文件的条目。
寄语
本次文章介绍了缓冲区和磁盘上的文件管理,希望其中的一些观点能够帮助大家学习,缓冲区我个人认为难度较大,难以理解,其中有问题的地方希望大家能够指出来,在评论区进行讨论。