内存管理

内存

内存分配

虚拟内存空间的管理,每个进程看到的是独立的、互不干扰的虚拟地址空间。

首先给出 32 位系统虚拟内存空间分布图。从程序的视角看,有 2^32 = 4G 的内存可以供自己使用。当然最顶部的空间是给内核使用的,下面的才是用户可以使用的。

  • 只读段,即 Text Segment,存放二进制可执行代码
  • 数据段,即 Data Segment,存放静态常量
  • 数据段上面其实还有 BSS Segment,存放未初始化的静态常量
  • 内核代码也是 ELF 格式的,也有上述这 3 个段

在 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 缓存从磁盘读取的数据,缓存的是磁盘读取文件的页缓存

在服务内存够用的情况下,Linux 内核为了加快对文件的读写效率会将文件放入之 buffer/cache 中以保证读写效率,但其实,尽管当你的应用程序对文件的读写运行结束后,buffer/cache 也不会自动释放该部分内存,而是作为缓冲进行保留,等到你的服务进程在下一次进行相同文件的读写时就可以直接使用,省去了各种重新进行内存初始化的操作。当服务器在内存压力较大的情况下时,则将会自动进行内存的回收,作为 free 空间分给其它进程使用。

手动进行 buffer/cache 回收:

# 将内存中数据强制先刷新到磁盘中
sync;
 
# 清理Buffer缓存区域
echo 3 > /proc/sys/vm/drop_caches 表示清除pagecache和slab分配器中的缓存对象
echo 1 > /proc/sys/vm/drop_caches 表示清除pagecache。
echo 2 > /proc/sys/vm/drop_caches 表示清除回收slab分配器中的对象(包括目录项缓存和inode缓存)。slab分配器是内核中管理内存的一种机制,其中很多缓存数据实现都是用的pagecache。

查看进程的 Buffer/Cache

首先得下载安装 hcache,然后:

# 全局显示10个最大的被缓存文件
hcache --top 10
# 查看指定进程ID所使用的buffer/cache 的使用情况
hcache -pid <pid>

内存回收机制

系统可以回收的内存:

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

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

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

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

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

内存泄露

介绍一个专门用来检测内存泄漏的工具,memleakmemleak 可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出一个未释放内存和相应调用栈的汇总情况(默认 5 秒)。当然,memleakbcc 软件包中的一个工具。

参考

  • 《程序员的自我修养》