本篇内存管理学习总结为后面学习I/O的前置。关于I/O我们常听的词都有磁盘I/O、网络I/O、BIO、NIO、多路复用、epoll、mmap、零拷贝、顺序/随机读写,如需深入了解需要有机组的部分知识。楼主大学专业软件工程,有开机组的课程,不过毕业后全部交给老师了。那就先从内存管理开始吧。收!!!开始上干货。
在DOS时代,受到内存大小的限制,同一个时间只能有一个进程在运行。
在Windows 9X时代,增大了机器的内存,此时进程是可以通过内核的调度实现多进程同时运行,但是也存在两个致命的问题:
为解决以上两个问题,分别采用了内存分页和虚拟内存,至此诞生了现在的内存管理模型。
假设早期在使用QQ时,会将QQ整个程序所有的文件全部加载的内存中,想想我们再使用一个程序时不会一次性需要程序的所有文件,可不可以程序准备一个文件列表并且每个文件的大小相同,需要用到时候再按照列表将需要的文件加载到内存中,如此可以大大的节省内存空间。其实现在的操作系统就是按照这种设计思路实现的。
如今程序在物理内存都是被分割成N多个的4K内存空间,这4K的内存空间我们可以称之为pagecache,程序需要用到那一块,再加载那一块。如果内存已满则将最不常用的的数据放至SWAP分区。这就是著名的LRU缓存过期算法(哈希表 + 双向链表 O(1)),基本上涉及到缓存的都会有用到此算法,诸如ehcache、redis、mysql的buffer等。
鉴于早期A程序可以随意修改B程序内存的数据,则在物理内存之上增加了一层虚拟文件系统的介质,程序不可以直接去操作物理内存,内存数据的调度需要交由内核去管控,每个进程都虚拟的独占整个CPU,如此一来,进程与进程是完全隔离的。此介质在Linux中称之为VFS。
每个进程的虚拟内存结构都如上图,在每个区域中都是由多个pagecache组成。
那么既然有了虚拟地址,物理地址和虚拟地址之间的映射时如何完成的?
下图看看打开程序QQ的一个逻辑图;(没鼠标画图真的蛋疼!!!)
在来看两个问题:
带着疑问我们来细看Linux 的VFS虚拟文件系统是如何实现。
每个VFS中都有一个唯一的inode id,用来区别标识不同进程的VFS。
众所周知,linux一切皆文件。程序在运行过程中内核是通过fd来访问文件的,在fd上记录了已打开文件的索引信息。其本质就是索引,内核为每个进程维护该进程打开文件的记录表。其实可以理解为java中的迭代器模式中的iterator,每个线程对集合进行遍历,其中的游标信息都是由各自线程自行维护。
之前记录的笔记直接copy过来,还是比较全面的。
在linux中,如需查看当前bash的fd信息,可以通过如下操作,即可查看当前进程记录的fd信息。
也可以通过命令lsof -p pid
查看fd的具体的描述信息。
从图中列出了fd的类型、读取游标、输入输出情况。
前面提到了数据是需要时被加载,那下面先来看看pagecache是如何被加载的?
中断是硬件跟操作系统打交道的一种机制。在操作系统中维护了1个字节的中断向量表,例如 0x01 键盘输入等。在我们程序软件中如果需要调用内核函数,都需要通过0x80中断去实现,这也就是著名的软中断,有软件发出的中断信息。
程序发出中断信号,由操作系统函数system call()按照中断信号找到相应的系统调用函数并去执行。
实例:java读取文件
在pagecache中有一个标识用来标识当前pagecache的状态。当应用程序把当前页修改后,会被标识为dirty。dirty数据刷新至磁盘有两种方式:
通过cat /proc/vmstat |grep dirty
查看系统dirty数据情况;
针对dirty数据默认的刷新阈值,通过命令sysctl -a |grep 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数据刷新至磁盘;