文章目录
- 一、物理内存和磁盘交换数据的最小单位
- 二、操作系统如何管理内存
- 三、文件的页缓冲区
- 四、基数树or基数(字典树)
- 五、总结
一、物理内存和磁盘交换数据的最小单位
我们知道系统当中除了进程管理、文件管理以外,还有内存管理
内存的本质就是对数据的一种临时存取,所以我们可以把内存看作一个非常大的缓冲区就可以了。
当内存需要数据的时候,可以直接从磁盘中读取,不需要的时候可以直接释放或者与磁盘进行交换
为了方便物理内存与磁盘进行交互,我们会将物理内存看作一个一个的小格子,内存也是一种线性的。这个一个个的小单位是4KB
像我们平时形成的可执行程序也是一个一个的以4KB为单位的小的数据段。
也就是说,如果一个可执行程序是4M,那么其实这个可执行程序也是4KB,4KB进行划分的。这是因为文件系统中数据块的大小就是4KB。
所以可执行程序在文件系统中天然就是每读一个块就是4KB
而我们就将物理内存的这一个个4KB就叫做页框,将磁盘当中的这一个个4KB叫做页帧
那么为什么必须是4KB,可以是1KB,2KB吗?
当然是可以的,不过这样我们需要修改操作系统的底层源代码。最终重新编译操作系统
而我们为什么要选择4KB呢,我们的文件压根可能就没有4KB。可能就是1KB,那么那3KB就浪费了。即便某个文件只有一个比特位,我们也不能只拿这1个比特位,必须将这4KB全部加载进来。
我们知道磁盘本身就是一个机械设备,注定了它IO时候访问的周期比较长,即比较慢,一次4KB很显然要比一次1KB效率要更高一些。(因为只需要磁头定位一次即可。比要定位四次快得多)
其次就是计算机中存在着局部性原理:在访问某些代码和数据时候,它附近的代码和数据也有很大概率被访问。而且因为机械运动才是慢的主要矛盾,有可能我们的文件只有100字节,但是我们也要读取4KB,这两个的效率其实差不多。而且100字节可能更加分散,需要更加精细
所以就有了基于局部性原理的预加载机制
它可以减少IO的次数,从而对系统整体进行提速 ----硬件
基于局部性原理,有了预加载机制 ----软件
注意这里的4KB是物理内存和磁盘交换数据的单位
二、操作系统如何管理内存
在操作系统层面上,要管理内存,肯定会用到虚拟地址。
而操作系统管理内存,也肯定是能看到内存的物理地址的。
那么操作系统如何管理内存呢??
先描述后组织
所以在操作系统里面肯定有一个东西
struct page { //page页必要的属性信息。 };
像我们的系统如果有4GB的内存的话,那么最终会存在1024*1024*\1024*4 /4/1024,即约100万多个页。
然后我们在操作系统内核里面直接定义
struct page mem_array[1048579]
所以我们发现对内存的管理变为了对数组的管理。
即先描述后组织
而我们知道数组是天然有下标的,所以我们就天然的有了页号的概念
所以以后当有了一个地址以后,我们就可以知道它是在哪一个页号上的
因为4KB,需要用12位
所以我们只需要将这个低12位全部清零即可
比如0x11223344,我们直接让他按位与上0xFFFFF000
所以它最终的页号就是0x11223000
所以我们就直接用这个页号就找到了对应的属性
所以,我们要访问一个内存,我们只需要先直接找到这个4KB对应的Page,就能在系统中找到对应的物理页框
在我们系统中,所有申请内存的动作,都是在访问内存Page数组
而且这个Page结构体不会很大,因为会有一个Page类型的数组,它最终也是要在内存中存放着的,所以它不能太大,所以这也再次说明了前面的页框大小不能太小,因为它越小,这个数组就越大,占据的内存空间越大。
如下所示,它的page里面都是一些union,这个flags代表它的使用状态。(每一个比特位都有它的含义,比如当我要使用这个页框的时候,我们只需要判断其中的一个标志位是否为0,如果为0那么改为1,这个内存就被使用了)。下面的这个count代表的就是引用计数。用来判断该内存被多少人使用。
同时在这个Page中还有一个lru,它是最近最少使用。也就是说操作系统会将最近最少使用的东西拿出来给刷新出去。
三、文件的页缓冲区
如下图所示
在我们开机的时候,不仅仅是为我们创建了进程了,内存管理做好了等等。
还会将我们文件系统相关的数据都已经预加载到内存了,尤其是Super Block等这些文件系统相关的信息
我们可能会说,那在操作系统上存在着很多分区,这也无所谓,因为可以用链表将他们组织起来
如下是一个操作系统,里面有进程、files_struct等内核数据结构
所以最终操作系统上层用的都是fd文件描述符
当我们在打开文件的时候,我们必须知道这个文件的路径+文件名,然后我们就能读取当前目录的数据块,从而找到文件的inode
因为这些inode Bitmap,Block Bitmap已经被提前加载到内存中了。然后我们确认这个inode是否存在,如果存在,直接将这个inode给加载进来,最后也就能读取到对应的数据块了。
以上都是一个文件被加载到内存当中的过程。
而现在我们关心的是这两件东西:文件的属性+文件的内容
所以我们需要做的就是,文件的属性如何被拿到。
我们知道文件的属性都在inode里面,struct file里面也有文件的属性,不过只有少量的属性。
所以我们会创建一个内核数据结构,struct inode,然后直接将磁盘中的inode里面的内容填写到这个内核数据结构中。而这个struct file是可以找到struct inode的
可是我们之前说过,我们在上层调用fprintf以后,就会通过这个fd,往对应进程中找到对应的文件描述符,从而去找到struct file结构体。那么在这里如何将数据写到磁盘中呢?
我们现在只能去找到文件的属性
其实在struct file里面还存在一个结构叫做address_space。
而radix_tree_root它是一颗多叉树
它结点里面是这样的结构
如下图所示,它的每一个叶子结点都指向一个struct page对象,而这样的每一个struct page对象都对应着4KB的内存大小
所以说当我们将数据拷贝到struct file以后,就会找到address_space,然后一路找到这棵树的叶子节点中,最终通过这个叶子节点的struct page去管理对应的内存
而上面这个就是文件的页缓冲区
四、基数树or基数(字典树)
在Linux中,我们的每一个进程,打开的每一个文件都要有自己的inode属性和自己的文件页缓冲区
什么是字典树呢?
类似于下面的26叉树每个结点可以指向26个字母
我们可以用下面这个3个字母简单的来代替
当我们要查找某个对象的时候,我们可以用bbb来作为key值,从而找到某个对象
文件的内容按照4kb是有偏移量的
比如一个10MB的文件,它占据的内存就是10*1024*1024
而文件的内容是按照4KB一块一块的进行存储着的
而这刚好就是2560块
所以我们就可以给他进行编号[1,2560]
而每一块乘以4KB就是他们的相对于原始数据的偏移量
所以前面的这一批数字[1,2560]它刚好每一个编号都是一个int类型的
而int类型是占据32位的
比如有一个数据是0xFF FF FF FF
这个整数我们可以将第一个数字看作一个b,第二个数字看作一个c,第三个数字看作一个a
如果我们可以像前面那样构建出一颗字典树
我们就可以利用这个文件的内容所在的区域
就可以利用字典树,找到对应的page的映射关系。
所以当我们进行读写文件的时候,从开头读,每一个读写都有偏移量。
根据这个偏移量,就可以将这个偏移量转化为树中的某一个page
这样我们就可以根据它的page偏移量,就确定先刷新哪一个page,后刷新哪一个page,就可以让文件有序的进行刷新了
最终我们的数据就成功的写入到了内存中
当我们将数据写入到了内存中以后,后序数据从内存如何写入到磁盘,就不是操作系统需要关心的事情了,这就导致了当我们突然断电以后,内存里面的数据都无法保存起来
上面的这个从内存刷新到磁盘当中的过程就是驱动层的事情了, 需要IO子系统来进行完成
五、总结
总之上面的过程其实就是下面的这张图
也就是说,操作系统里面也有一个文件缓冲区,最后它会被刷新到磁盘上去
上面的过程,我们就把打开文件和文件系统的文件 产生关联了!
我们也可以发现,这里一共要经历三次拷贝,第一次将数据写入到C语言缓冲区中,第二次将数据从C语言缓冲区写入到文件缓冲区中,第三次是写在磁盘当中去