理解linux内存管理首先得理解内存映射。转自:http://blog.chinaunix.net/uid-27119491-id-3282175.html
程序用的都是逻辑地址,以下为objdump反汇编程序的结果,左边一列都是逻辑地址:
- 000000000040053c:
- 40053c: 55 push %rbp
- 40053d: 48 89 e5 mov %rsp,%rbp
- 400540: bf 4c 06 40 00 mov $0x40064c,%edi
- 400545: e8 e6 fe ff ff callq 400430
- 40054a: b8 00 00 00 00 mov $0x0,%eax
- 40054f: c9 leaveq
- 400550: c3 retq
在vmcore中,我们看到内存地址也是逻辑地址:
- crash> bt 1300
- PID: 1300 TASK: ffff81024fba1810 CPU: 0 COMMAND: "sshd"
- #0 [ffff81026a2e9cd8] schedule at ffffffff802d9025
- #1 [ffff81026a2e9da0] schedule_timeout at ffffffff802d9a6b
- #2 [ffff81026a2e9df0] do_select at ffffffff801935f2
- #3 [ffff81026a2e9ed0] sys_select at ffffffff80193880
- #4 [ffff81026a2e9f80] system_call at ffffffff8010ad3e
MMU固件完成逻辑地址到物理地址的转换:
从上图看到转换分成两部分,段式内存管理将逻辑地址转换成线性地址,线性地址再经过页式内存管理单元转换成物理地址。
对于intel x86架构的cpu,段的基址为0,段式内存管理并不真正起作用,我们看到的逻辑地址可以理解为就是线性地址。
线性地址到物理地址的转换关系,由内核中的页表(page table)维护,下图说明了线性地址转换成物理地址的过程:
寄存器cr3中保存了进程的页基地址,它是物理地址,每个进程的页基地址都不相同,通过这样实现进程间物理地址空间隔离。
64位操作系统中,线性地址各段长度在中定义。
以上计算地址的“+”操作由cpu完成,而页表由内核维护。
进程内存布局
对于32位操作系统,进程的内存布局如下:
我们使用c库的malloc申请内存时,malloc调用brk()或mmap()实现堆(heap)或映射区域(memory mapping segment)的增长。
用malloc申请内存,其实申请到的不是真正的内存,而是虚拟内存,当程序使用这些内存的时候,将出发缺页异常,内核这时才分配物理内存页面给用户。
内核中,由do_page_fault函数处理缺页异常,do_page_fault调用handle_mm_fault,该函数中计算线性地址中的pgd、pud、pmd和pte值,然后调用handle_pte_fault,在handle_pte_fault函数中,调用do_anonymous_page等函数完成内存申请工作。
pmap命令读取/proc/PID/maps文件,使用该命令可以查看特定进程的内存映射情况,以下为当前bash进程的部分堆、栈、文件映射和匿名内存段信息:
- linux # pmap $$
- 7561: bash
- START SIZE RSS PSS DIRTY SWAP PERM MAPPING
- 0000000000400000 552K 480K 248K 0K 0K r-xp /bin/bash
- 000000000068f000 1180K 1104K 1104K 1104K 0K rw-p [heap]
- 00007fd7397b9000 20K 16K 16K 16K 0K rw-p [anon]
- 00007fd7399c0000 4K 4K 4K 4K 0K r--p /lib64/libdl-2.11.1.so
- 00007fff5ec37000 84K 24K 24K 24K 0K rw-p 0000000000000000 00:00 [stack]
以上pmap结果显示的每个地址段,在内核中由vm_area_struct结构表示,vm_area_struct结构在中定义,该结构包含地址段起始地址、结束地址、权限标志等信息。
vmcore中,通过以下方法,我们可以找到某个进程的vm_area_struct结构,查看其内存映射信息。
首先获取到进程task_struct结构的地址:
- crash> bt
- PID: 1300 TASK: ffff81024fba1810 CPU: 0 COMMAND: "sshd"
- ……
找到mm或active_mm字段的值:
- crash> struct task_struct ffff81024fba1810
- struct task_struct {
- ……
- mm = 0xffff810273d6ef80,
- active_mm = 0xffff810273d6ef80,
- ……
- }
解析mm地址对应的mm_struct结构,第1个字段就是mmap:
- crash> struct mm_struct 0xffff810273d6ef80
- struct mm_struct {
- mmap = 0xffff810272b20870,
- ……
- }
解析mmap地址对应的vm_area_struct结构,就找到了该进程的一段内存映射,继续解析vm_next,就可以遍历该进程的所有内存映射:
- crash> struct vm_area_struct 0xffff810272b20870
- struct vm_area_struct {
- vm_mm = 0xffff810273d6ef80,
- vm_start = 47474870689792,
- vm_end = 47474870800384,
- vm_next = 0xffff810276839b50,
- ……
- }
伙伴算法
对内核而言,内存最小的分配单位为4k,为解决外部碎片问题,内核提供了伙伴算法(buddy algorithm),伙伴算法用于管理最小单位为4k的连续的内存块。
使用以下命令,可以将系统中内存相关的信息显示到/var/log/messages和dmesg中:
- echo m > /proc/sysrq-trigger
由该命令打出的信息中,包含了伙伴算法管理的内存块信息:
- Jul 20 01:38:39 linux197 kernel: Node 0 Normal: 1362*4kB 1137*8kB 491*16kB 336*32kB 192*64kB 100*128kB 69*256kB 34*512kB 14*1024kB 3*2048kB 795*4096kB = 3370112kB-
以上输出说明4kB的内存有1362块,8kB的连续内存有1137块,以此类推。
伙伴算法分配与回收内存的策略如下:
- 当所需的小块连续内存不足以分配时,将拆分较大块的连续内存块
- 当内存返回给内核时,若相邻的内存块为空闲内存,则与相邻的内存合并
slab
slab解决内部碎片问题,申请一块保存task_struct结构的内存,该块内存小于4k,而分配出去的内存若还是4k,则会造成内存浪费。
slab依据内核中各种数据结构为单位,进行内存分配。/proc/slabinfo提供了查询slab信息的接口。
对于系统内存的整体使用情况,我们可以通过/proc/meminfo接口查到:
- cat /proc/meminfo
- MemTotal: 8110624 kB
- MemFree: 6336284 kB
- Buffers: 78020 kB
- Cached: 1514352 kB
- ……
free命令就是读取/proc/meminfo,进行内存信息显示的。
buffers用于块设备缓存,cached用于文件缓存。查看内核中的meminfo_proc_show函数,我们就能发现buffers与cached的具体计算方法。
执行以下dd命令,我们可以看到free命令中buffers值的增长:
- dd if=/dev/sda2 of=/dev/null bs=1000 count=10000000
内存回收
对于linux server,以下两种情况会触发内核通过页帧回收算法(page frame reclaiming algorithm, PFRA)进行内存回收:
- 内存使用紧张
- 内核线程kswapd周期性回收
回收内存的方式大体有两种:
- 丢弃数据
- 将数据交换出去
下图说明了内核中回收内存相关的函数调用:
Reference: Chapter 12 - Memory Management, Linux kernel development.3rd.Edition