内存

内存

作者:赵坤

内存分配

首先给出 32 位系统虚拟内存空间分布图:

在 C 语言中,内存分配采用 malloc() 函数进行分配。底层实现:

  • 申请的内存小于 128K,使用 brk() 函数完成,也就是从上图中的中分配的内存
  • 申请的内存大于 128K,使用 mmap() 内存映射函数完成,也就是从上图中的文件映射中分配的内存

内存回收

应用程序应通过 free()unmap() 来释放内存。

当然,系统也会监管进程的内存,当发现系统内存不足时,会采取措施:

  • 使用 LRU 算法回收缓存
  • 回收不常访问的内存,写到 Swap 区(位于硬盘上)
  • 杀死进程

虚拟内存

分段机制

分段机制下的虚拟地址由两部分组成,段选择子和段内偏移量。段选择子就保存在咱们前面讲过的段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。虚拟地址中的段内偏移量应该位于 0 和段界限之间。如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

分段对内存区域的映射以程序为单位,内存不足,换入换出到磁盘的是整个程序,粒度比较大,产生大量磁盘 IO,而根据程序的局部性原理,程序运行时,某个时间段,一般只是频繁用到很小的一部分数据。那么可以利用更小粒度的内存分割和映射方法,这就是分页

分页 (Paging)

对于物理内存,操作系统把它分成一块一块大小相同的页,这样更方便管理,例如有的内存页面长时间不用了,可以暂时写到硬盘上,称为换出。一旦需要的时候,再加载进来,叫做换入。这样可以扩大可用物理内存的大小,提高物理内存的利用率。

这个换入和换出都是以页为单位的。页面的大小一般为 4KB。为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址,再加上在页内的偏移量,组成线性地址,就能对于内存中的每个位置进行访问了。

虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。

内存映射

  • 用户态内存映射函数 mmap,包括用它来做匿名映射文件映射
  • 用户态的页表结构,存储位置在 mm_struct 中。
  • 在用户态访问没有映射的内存会引发缺页异常,分配物理页表、补齐页表。如果是匿名映射则分配物理内存;如果是 swap,则将 swap 文件读入;如果是文件映射,则将文件读入。

查看整个系统的内存

$ free
              total        used        free      shared  buff/cache   available
Mem:        6030036     2312004      266488      624252     3451544     2911700
Swap:       2097148         256     2096892

查看某个进程的内存

使用 topps

$ top
PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                
8883 zk        20   0   16.7g 477004 223504 S   4.7   7.9   2:02.69 chrome                                                                 
2075 zk        20   0 1233356  99568  53584 S   4.0   1.7   4:49.63 Xorg                                                                   
2681 zk        20   0  865052  56464  42040 S   2.7   0.9   0:11.70 gnome-terminal-                                                        
2247 zk        20   0 4335224 252364  99052 S   1.3   4.2   6:50.33 gnome-shell   

Buffer vs Cache

free 命令中的 buff/cache 一列的 buffcache 分别指代什么?

  • buff 即 Buffer,缓存磁盘数据,是对原始磁盘块的缓存,内核可以将分散写改为集中写。
  • cache 缓存从磁盘读取的数据,缓存的是磁盘读取文件的页缓存

内存回收机制

系统可以回收的内存:

  • 文件页(Buffer、Cache、文件映射页)
  • 匿名页(堆内存)

其中,文件页如果被修改过,那么就会变为脏页,必须先写入磁盘,才能内存释放。脏页写入磁盘有两种方式:

  • 调用 fsync() 写入脏页到磁盘
  • 内核线程定期 pdflush 刷新这些脏页到磁盘

而匿名页虽无法直接回收,但是通过 Swap 机制可以将不常用的内存写到磁盘中,然后释放内存,给其他程序用。

通过调节 /proc/sys/vm/swappiness 值(0 - 100)可以控制回收匿名页的优先级,值越大,越优先回收匿名页。

参考

  • 《程序员的自我修养》