关于内存
tags: OS计算机分级存储
计算机的存储有一个特点:速度快的存储设备,通常容量小,价格贵。而容量大,价格便宜的存储设备一般存储速度会比较慢。 计算机的分级存储从快到慢:
CPU寄存器 -> CPU cache -> 内存RAM -> SSD -> HDD
|
L1 L2 L3
cache:cache的存在满足局部性原理
- 时间局部性,即一个刚刚被访问的数据,有可能很快再次被访问。放在cache中非常划算,可以防止大量未命中
- 空间局限性:一个数据被访问,他周围的数据也有可能被访问,cpu可能把一块连续的内存放入cache。顺序读取后续数据会比随机访问快 对于L1,L2,L3cache来说,距离越远,容量越大,操作速度越慢。但是一般每个CPU都有自己的L1,而在很多CPU中L3是多个CPU共享的 内存RAM:内存断电之后容易丢失,是易失型存储。程序运行时主要在内存内运行 如果内存不够程序运行,操作系统会把暂时不用的资源放到磁盘,这个称为SWAP。但交换空间严格意义上不算内存,只是用硬盘模拟内存 SSD:固态硬盘,里面的数据断电不会丢失,内部主要使用闪存falsh memory HDD:机械硬盘,读取数据需要磁盘旋转,移动磁头,找到数据,读取数据,随机访问会比较慢
磁头
↓
───────
/ \
| 磁盘 |
\ /
───────
↻
数据访问的时候先访问三级cache,如果都没有的话,查询Ram内存,如果内存中也没有的话,那可能发生缺页异常。操作系统发现这个数据不在物理内存,从SSD/HDD里面加载数据,然后逐级上传,缓存,最后存入CPU寄存器
接下来主要对计算机中的内存部分展开讲解,设置虚拟内存->物理内存映射,页表,堆栈,零拷贝等
内存
对于操作系统来说物理内存是固定的,为每个程序指定一块连续内存,如果后续需要扩容或缩减。就涉及到对其他地址数据的修改。处理起来会比较麻烦。对此,操作系统为每个进程分配一个页表,用页表的虚拟地址对物理地址进行映射。
虚拟内存
虚拟内存可以很好的解决:
- 进程间地址冲突的问题,每个进程维护自己的页表,并且没有办法访问其他进程的页表。
- 运行内存超过物理内存实际大小的问题,因为程序运行符合局部性原理,CPU访问内存有很明显的重复访问的倾向。对于长期大量未访问的数据,可以把它放到硬盘
- 标记状态,页表除了地址,还存放了进程对对一个页的状态以及权限记录位,可以标记页是否存在,对页的读写权限之类的。
页表存储在内存里,由MMU做内存管理操作
页表
页表的操作过程,以二级页表为例: 从页目录表中拿到页表位置,通过页表中的对应位置拿到物理页位置,物理页页号*页大小加偏移地址就是实际的物理地址,如果用段表做映射,就是段基址 * 16+偏移地址
页表和段表有什么区别呢
页表用页来映射,大小一般是固定的,初始一个页是4kb。而且页表不关心自己映射的东西具体是什么,堆?栈?或者代码,页不在乎,只是映射… 段表用段来进行映射,段不确定大小。分段的思想本上就有一些逻辑划分,代码段,堆段,栈段可以存放不同的数据。 一般用页来映射而不用段,因为分段可能产生外部碎片,总空闲空间足够,但是会产生很多小块,而没有一块连续可用的空间。而分页也不是完美的,可能产生内部碎片
程序的内存布局
这里从低到高分别是:
代码段:包括二进制可执行代码
数据段:包括已初始化的静态变量和全局变量
BSS段:包括未初始化的全局变量和静态变量
堆段:包括动态分配的内存,从低地址开始向上增长
文件映射段:包括动态库,共享内存等
栈段:包括局部变量和程序的上下文,栈的大小一般是固定的8MB
程序有自己的虚拟内存布局,里面有代码、数据、BSS、堆、内存映射区、栈等区域;现代 Linux 再通过分页机制,把这些虚拟地址映射到物理内存。
堆和栈有什么区别
分配方式:堆是动态内存分配,由程序员主动申请或者释放内存,栈是静态分配内存,由编译器申请或者释放内存 内存管理:堆需要程序员手动管理或者释放内存,管理不当可能产生内存泄漏,栈由编译器自动管理内存,遵循后进先出,变量的生命周期由他的作用域决定
copy on write
主进程fork的时候,操作系统会把父进程的页表copy一份给子进程,子进程拥有的页表和父进程拥有的页表指向的是同一块虚拟内存。但是子线程对该页表只有只读权限。这个是为了防止写过程太长导致父子进程创建的时候长时间阻塞。 copy on write还一定程度上节省了物理内存资源,子进程想要拥有父进程的资源,不需要复制一份同样的资源,而是复制一份页表。父子进程任意一方对这个进程进行修改,才会触发写时复制
malloc
程序触发malloc的时候,会分配一片虚拟内存,具体操作是: 先brk(),移动堆指针指定一块区域,再mmap()通过私有匿名映射(供进程自己使用,不关联任何磁盘文件)来获取一块文件映射区的内存 当程序访问这一块内存,就会寻找这一块虚拟内存对应的物理内存。此时会发生缺页中断。进程从用户态切换到内核态,移交缺页中断执行
缺页中断检查是否有空闲的物理内存,如果有,就分配物理内存。如果没有空闲的内存,那么内核会先回收内存。
如果回收之后内存依旧不够,那么会触发OOM机制: OOM机制根据算法选择一个物理内存占用最高的进程,杀死进程,重复操作直到有足够的内存空间。
内存回收
内存回收主要回收两类: 文件页:内核缓存的磁盘数据,内核缓存的文件数据。都可以叫作文件页。针对脏页——修改过但还没有写入磁盘的数据,就需要先写入磁盘,再释放内存。回收干净页则可以直接释放。 匿名页:这部分内存没有实际载体,回收通过上文提到的SWAP机制,先把一部分内存写入磁盘,再释放内存,需要的时候再从磁盘取出来。
文件页的回收和匿名页的回收都基于LRU算法:优先回收不常访问的内存,维护两个双向链表 active_list:活跃内存页链表,存放的是最近活跃的内存页 inactive——list:不活跃内存链表,存放最近不活跃的内存页 越接近尾部,代表越不活跃