内存
内存分配
虚拟内存空间的管理,每个进程看到的是独立的、互不干扰的虚拟地址空间。
首先给出 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
查看某个进程的内存
使用 top
或 ps
:
$ 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
一列的 buff
和 cache
分别指代什么?
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)可以控制回收匿名页的优先级,值越大,越优先回收匿名页。
内存泄露
介绍一个专门用来检测内存泄漏的工具,memleak
。memleak
可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出一个未释放内存和相应调用栈的汇总情况(默认 5 秒)。当然,memleak
是 bcc
软件包中的一个工具。
参考
- 《程序员的自我修养》