前言
本篇内存管理学习总结为后面学习I/O的前置。关于I/O我们常听的词都有磁盘I/O、网络I/O、BIO、NIO、多路复用、epoll、mmap、零拷贝、顺序/随机读写,如需深入了解需要有机组的部分知识。楼主大学专业软件工程,有开机组的课程,不过毕业后全部交给老师了。那就先从内存管理开始吧。收!!!开始上干货。
一、内存管理的发展历程
在DOS时代,受到内存大小的限制,同一个时间只能有一个进程在运行。
在Windows 9X时代,增大了机器的内存,此时进程是可以通过内核的调度实现多进程同时运行,但是也存在两个致命的问题:
- 内存无限增长,直至撑爆;
- 互相打扰。多个进程之间共用物理内存,A进程可以修改B进程运行的数据、用户态可以随意修改内核态的数据;
为解决以上两个问题,分别采用了内存分页和虚拟内存,至此诞生了现在的内存管理模型。
1.1 内存分页(内存撑爆)
假设早期在使用QQ时,会将QQ整个程序所有的文件全部加载的内存中,想想我们再使用一个程序时不会一次性需要程序的所有文件,可不可以程序准备一个文件列表并且每个文件的大小相同,需要用到时候再按照列表将需要的文件加载到内存中,如此可以大大的节省内存空间。其实现在的操作系统就是按照这种设计思路实现的。
如今程序在物理内存都是被分割成N多个的4K内存空间,这4K的内存空间我们可以称之为pagecache,程序需要用到那一块,再加载那一块。如果内存已满则将最不常用的的数据放至SWAP分区。这就是著名的LRU缓存过期算法(哈希表 + 双向链表 O(1)),基本上涉及到缓存的都会有用到此算法,诸如ehcache、redis、mysql的buffer等。
1.2 虚拟内存(互相打扰)
鉴于早期A程序可以随意修改B程序内存的数据,则在物理内存之上增加了一层虚拟文件系统的介质,程序不可以直接去操作物理内存,内存数据的调度需要交由内核去管控,每个进程都虚拟的独占整个CPU,如此一来,进程与进程是完全隔离的。此介质在Linux中称之为VFS。
每个进程的虚拟内存结构都如上图,在每个区域中都是由多个pagecache组成。
那么既然有了虚拟地址,物理地址和虚拟地址之间的映射时如何完成的?
- Main方法开始执行时,代码片段肯定是在数据区,在数据区上会有一个内存地址的下标,称为逻辑地址或者偏移量;
- 整个虚拟空间在内存中也是有一个下标,称为基地址。段的基地址+偏移量=线性地址;
- 再通过OS和MMU(硬件)完成线性地址到物理地址的转换;
下图看看打开程序QQ的一个逻辑图;(没鼠标画图真的蛋疼!!!)
在来看两个问题:
- 当程序获取虚拟内存中的pagecache不存在时,数据又是如何被内核加载的呢?
- 虚拟内存中的pagecache数据的更新存储又是通过什么机制去完成的?
带着疑问我们来细看Linux 的VFS虚拟文件系统是如何实现。
二、VFS虚拟文件系统
2.1 inode id
每个VFS中都有一个唯一的inode id,用来区别标识不同进程的VFS。
2.2 fd文件描述符
众所周知,linux一切皆文件。程序在运行过程中内核是通过fd来访问文件的,在fd上记录了已打开文件的索引信息。其本质就是索引,内核为每个进程维护该进程打开文件的记录表。其实可以理解为java中的迭代器模式中的iterator,每个线程对集合进行遍历,其中的游标信息都是由各自线程自行维护。
2.2.1 linux文件类型
之前记录的笔记直接copy过来,还是比较全面的。
2.2.2 输入输出表示
2.2.3 查看fd的信息
在linux中,如需查看当前bash的fd信息,可以通过如下操作,即可查看当前进程记录的fd信息。
也可以通过命令lsof -p pid
查看fd的具体的描述信息。
从图中列出了fd的类型、读取游标、输入输出情况。
2.3 pagecache
2.3.1 data load
前面提到了数据是需要时被加载,那下面先来看看pagecache是如何被加载的?
- APP读取fd9;
- 触发80中断(软中断);
- CPU收到中断信息,由用户态切到内核态;
- 内核中查看fd9是否存在,存在则返回;当不存在时触发缺页异常;
- 从磁盘中获取pagecache的信息;
- 由DMA完成数据返回的过程,在此期间程序进入挂起状态;
- DMA数据copy完成后,CPU会收到DMA中断信号,恢复线程,程序进入就绪状态;
2.3.2 中断
中断是硬件跟操作系统打交道的一种机制。在操作系统中维护了1个字节的中断向量表,例如 0x01 键盘输入等。在我们程序软件中如果需要调用内核函数,都需要通过0x80中断去实现,这也就是著名的软中断,有软件发出的中断信息。
程序发出中断信号,由操作系统函数system call()按照中断信号找到相应的系统调用函数并去执行。
实例:java读取文件
- jvm read();
- c库 read();
- 内核空间;
- system call();
- sys_read();
2.3.3 data save
在pagecache中有一个标识用来标识当前pagecache的状态。当应用程序把当前页修改后,会被标识为dirty。dirty数据刷新至磁盘有两种方式:
- 用户自己调用flus直接写入磁盘;
- 由内核决定写入磁盘的方式;
- 固定时间刷新至磁盘;
- 占用物理内存的百分比;
通过cat /proc/vmstat |grep dirty
查看系统dirty数据情况;
针对dirty数据默认的刷新阈值,通过命令sysctl -a |grep dirty
即可查看。需要根据实际的业务场景进行调优;
以上参数都是针对内存中dirty数据刷写的处理阈值,其中包含三个纬度:
- 内存百分比;
- 固定字节;
- 固定刷新时间;
当dirty数据达到阈值时: - 当物理内存充足时,会直接触发dirty数据直接写磁盘;
- 当物理内存不足时,会触发LRU淘汰机制。在淘汰中会判断当前数据是否为dirty数据,如果是dirty数据,则刷新磁盘并淘汰;反之直接淘汰。
其中参数中包含background意为开启一个新的线程去处理,不会阻塞新的写入。反之阻塞程序写的写入。
示例:
vm.dirty_background_ratio = 10 : 当内核中的dirty数据达到物理内存的10%,开启一个新的线程执行以上策略;
vm.dirty_ratio = 30:当内核中的dirty数据达到物理内存的30%,则会阻塞新的写入,并执行以上策略;
以下单位: 1/100秒
vm.dirty_expire_centisecs = 3000 : dirty数据的生命周期,dirty数据的超时回刷时间,表示dirty数据可以存30秒;
vm.dirty_writeback_centisecs = 500 : 每5秒将dirty数据刷新至磁盘;