linux的内存管理介绍

原创内容,转载请注明出处

Posted by Weakyon Blog on July 8, 2015

内存方面的知识真是博大精深,把一些看到的零碎的知识点做个笔记总结下。

这些知识只是自己的理解,并不能保证正确,ULK和《深入理解Linux虚拟内存管理》这两本书看完以后还会来更新这篇的内容。

段式管理与页式管理

linux下的段页式管理

linux下的内存分布

内存相关的一些工具

free

top


段式管理与页式管理

在操作系统理论中

段式管理就是将内存分成段,段的大小不是固定的。虚拟地址被分为段号和段内地址,根据段表查到段号对应的起始地址,将起始地址和段内地址相加即为物理地址。

段表

页式管理就是将内存分成页,页的大小都是固定的。虚拟地址被分为页号和页内地址,根据页表查到页号对应的页面号,将页面号对应起始地址和页内地址相加即为物理地址。

页表

为什么有段页管理呢,为什么有虚拟地址呢

内存管理的发展是和硬件发展挂钩的,在x86王朝之前内存寻址都是绝对寻址,地址都是物理地址。直到8086产生以后,这一款16位的处理器被设计为可以访问1MB的内存(20位的地址空间)。因此,段被引入来解决地址总线的宽度一般要大于寄存器的宽度的问题。

最开始的分段寻址,简单的说就是用两个16位数据来表示地址空间。第一个地址相当于段号,第二个地址相当于段内地址,简单的把段号左移四位然后与段内地址相加得到的就是物理地址。这就是实模式

到286系列开始,它使用保护模式,使用MMU这个硬件来对两个16位数据进行转换和检查,从而得到一个物理地址。MMU相当于硬件维护的段表了。

但是段式管理的引入并没有解决小内存运行大作业的问题。

于是页式管理登场了,页式管理的思想是同一时间并不是所有的地址都是活跃的,把活跃的页面保留在内存中,如果空间不够,将不活跃的页面换出内存即可。

例如,linux下将内存和硬盘分为4KB大小的页,一个程序运行时只加载硬盘中头几个4KB大小的页,即使这个程序的虚拟地址有几个G,在最初的时候也只是维护那么几十K的内容。而后这个程序访问到的虚拟地址带了一个无效的页面号,那么将报出一个页面错误。而后系统将从硬盘中读取这个虚拟地址在硬盘中对应的页。假如根据页面分配算法得到此时内存全满的信息,那么就会根据页面置换算法将不活跃的页面换出内存,然后将这个页写入内存。

当然,实际上这个过程是相当复杂的。每一步都是有软件和硬件的多层cache来保证效率的。另外,当页面被换出时,如果页面没被修改,那么简单的丢弃,下次再从内存加载即可,否则,那么这个页面叫做脏页,只能被换出到磁盘的swap分区。等待后续被换入。

小结:

1 段的大小不是固定,而页的大小是固定的,相对段来说非常小,因此页表会很大,需要由操作系统来维护,段表则可以由硬件(MMU)维护。

2 段式管理为了解决地址总线的宽度一般要大于寄存器的宽度的问题

3 页式管理为了解决小内存运行大作业的问题

linux下的段页式管理

严格的来说linux下是段页式内存管理,虚拟地址映射为物理地址时,先确定段号,段内分页,再找页号,通过页表最终计算得到实际的物理地址。

linux将内存分为内核代码段,内核数据段,用户代码段,用户数据段,TSS 段和默认LDT段等,但是这些段的起始地址都为0,因此linux实际上是页式管理。

这么实现是因为例如RISC下的CPU架构,并没有支持段表的硬件,所以这么做兼容性更好。

linux实际的页式管理采用了三级页表来实现。包括:

页全局目录 (Page Global Directory,pgd),页中间目录 (Page Middle Directory,pmd),页表条目 (Page Table Entry,pte)

在32位系统上,是pmd,pte两级页表,10位的pmd,10位的pte以及12位的offset,也就是2^10 * 2^10 * 2^12 = 4GB

二级页表

在64位系统上,则是pgd,pmd,pte三级页表,10位的pgd,10位的pmd,10位的pte以及13位的offset,另外21位无效,有效内存即为2^43 = 8TB

三级页表

linux下的内存分布

由于linux被设计用于支持一些分布式的架构场景,这种场景下linux集群由多台计算机构成,这些计算机的内存通过高速网络互联,每台计算机可以通过网络访问非本地内存。

每台计算机的内存叫做一个内存节点,一般的单机情况下只有一个内存节点。

我们来看看内存节点的下一级是什么。

在经典的32位系统中,节点被分为不同区域(zone),各自对应着物理内存的地址,区域分为以下类型:

ZONE_DMA(0-16MB):一些特殊低端物理内存需要区域,主要是供一些外设使用,外设和内存直接访问数据访问,而无需系统CPU的参与。

ZONE_NORMAL(16-896MB):由内核直接映射到高端范围的物理内存的内存范围。

ZONE_HIGHMEM(896MB以及更高的内存):系统中内核不能直接映射的物理内存

在实际的内存分配中,按照ZONE_NORMAL、ZONE_HIGHMEM 和 ZONE_DMA的顺序依次进行分配,如果分配失败才会转入下一级。

在64位系统中,区域被分为ZONE_DMA(0-16MB),ZONE_DMA32(16MB-4GB),ZONE_NORMAL(4GB以及更高内存)

32位下内核的虚拟地址空间是0xc0000000到0xffffffff,也就是3G-4G,另外为了操作系统的稳定性和性能考虑,这部分虚拟地址装载入内存后,永远不会被转出到磁盘上。

内核使用的3G - 3G+896MB的虚拟地址会直接映射到DMA和NORMAL的物理内存区域,而剩余的128MB(3G+896MB - 4G)会通过页表映射到任何可用的物理地址区域。

这就是为什么highmem存在的原因之一了,对32位来说,如果没有highmem,那么内核的1G的虚拟地址空间无法映射到整个4G的物理地址。对64位的系统而言,内核地址空间远大于4G,不需要highmem

highmem存在的另外一个原因是PAE能让32位下4G的虚拟地址空间管理大于4G的物理内存,896MB以及更高内存(最高到64GB)的空间都由highmem来标识。

这里有张图是用户空间的虚拟地址的实际分布情况:

--------------------------------------- 0xffffffff
kernel space
user code CANNOT read or write here
otherwise results in Segmentation falut
--------------------------------------- 0xc0000000
                                       
                                        random stack offset

---------------------------------------     -
        stack (grows down)                  |
              ||                            |
              \/
--------------------------------------- RLIMIT_STACK(e.g.,8MB)
        for stack grows                     |
---------------------------------------     -

                                        random mmap offset

---------------------------------------
        memory mapping segment
file mappings and anonymous mappings
        (e.g.,/lib/lib.so)
              ||
              \/
---------------------------------------


--------------------------------------- program break brk
              /\
              ||
        heap(grows up)
--------------------------------------- start_brk

                                        random brk offset

---------------------------------------
        bss segment
uninit static varrables, filled with
zeros,(e.g.,static int i)
---------------------------------------
        data segment
static variables init by the programmer
(e.g.,static int i = 1
---------------------------------------
        text segment
stores the binary image of the process
---------------------------------------0x08048000

---------------------------------------0x00000000

bss segment:

保存未初始化的全局变量

在可执行程序中仅记录符号名和所占空间大小,仅占极少空间

系统加载时将其分配到一块已初始化为0的bss数据区

data segment:

保存已初始化的全局变量

在可执行程序中占据大量空间,记录了其初始化的值

系统加载时为其加载初始值

text segment

保存二进制代码

data和bss又称作全局数据区

一般用size指令能打印出bss,data和text的大小

然而ELF的文件格式下还有一些其他内容

Segment header table
text seg:
.init 初始化段
.text 代码段
.rodata 只读数据(常量等)

data seq:
.data 可读写数据(全局变量等)

bss seq:
.bss 未初始化数据

可以不必加载的seg:
.symtab 符号表
.debug 调试信息
.line 指令和源文件行对应
.strtab 符号字符串实际存放处

内存相关的一些工具

free

查看内存使用情况最常用的手段就是free -m了

free -m
             total       used       free     shared    buffers     cached
Mem:          7872       7708        163          0       1618       4987

可以看到这里free空间非常小,这是因为这是一台文件存储服务器,大部分内存被用来放入buffers和cached

实际的空闲内存是free + buffers + cached

free -m的信息来自于/proc/meminfo

参考linux文档proc info来做分析

MemTotal:        8061012 kB  总共可用的RAM,(物理内存减去预留位和内核二进制代码大小)
MemFree:          165888 kB  LowFree+HighFree的总和
Buffers:         1656944 kB  暂存磁盘块,不应该非常大
Cached:          5107180 kB  用来给文件从磁盘中读取做cache,不包括swapcahed
SwapCached:            0 kB  已经被换出的内存,但仍然被存放在swapfile中,用来在需要的时候很快换入页面而不用再次IO
Active:          4940852 kB  最近经常被使用的内存,除非非常重要否则不会被回收
Inactive:        2090628 kB  最近不常被使用的内存,非常有可能被回收做其他用途
Active(anon):      88332 kB  anon是匿名内存,例如malloc
Inactive(anon):   179188 kB
Active(file):    4852520 kB  file是文件内存
Inactive(file):  1911440 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:       2097144 kB  交换区总大小
SwapFree:        2097144 kB  交换区剩余大小
Dirty:                28 kB  等待从内存写回交换区磁盘的脏页
Writeback:             0 kB  正在从内存写回交换区磁盘的页
AnonPages:        267364 kB  Non-file backed pages mapped into userspace page tables
Mapped:            19068 kB  已经被mmap映射的文件,例如动态库
Shmem:               164 kB
Slab:             790312 kB  内核数据结构缓存
SReclaimable:     753132 kB  slab的部分,必须被回收,例如缓存
SUnreclaim:        37180 kB  slab的部分,on memory pressure不能被回收
KernelStack:        2960 kB  
PageTables:         4420 kB  内存用户页表的最低总量
NFS_Unstable:          0 kB  
Bounce:                0 kB  用作"bounce buffers"阻塞设备的内存
WritebackTmp:          0 kB  Memory used by FUSE for temporary writeback buffers
CommitLimit:     6127648 kB
Committed_AS:     514308 kB
VmallocTotal:   34359738367 kB vmalloc能映射的内存
VmallocUsed:      288992 kB  vmalloc已经使用的内存
VmallocChunk:   34359446648 kB 最大一个vmalloc能用的空闲块
HardwareCorrupted:     0 kB
AnonHugePages:    192512 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:       10240 kB
DirectMap2M:     8378368 kB

这里还有两篇利于理解free -m输出信息的文章,实际上两篇讲的都差不多,看一篇即可

Linux Used内存到底哪里去了?

也看linux内存去哪儿了

top

free -m主要是看整体情况

而top能显示单个进程的VIRT(虚拟内存),RES(实际驻留内存),SHR(共享内存)的情况,这些信息来自于/proc/(pid)/statm文件(文件中都是页面数,乘以4KB就是TOP中的值)

根据前面讨论的linux下个页面管理方式,一个进程并不会全部调入内存,因此RES指的实际的物理内存开销,而SHR指的多个进程之间共享使用的页面。

这里的VIRT和RES都是没问题的,但是SHR却并不一定是共享内存的实际占用情况

在内核的task_statm函数能看到,SHR是mm->file_rss,SHR这是一种指示性表述,并不是指一定是共享内存的大小。

1 程序的代码段。

2 动态库的代码段。

3 通过mmap做的文件映射。

4 通过mmap做的匿名映射,但指明了MAP_SHARED属性。

5 通过shmget申请的共享内存。

这些都是有可能被计入SHR字段的。

当某个动态库只有一个进程使用时,他被记入了SHR,其实他是独占的。另外当进程fork出子进程以后,由于copy on write机制,在页面修改前,其实他是共享的,但是并没有被记入在SHR字段。

那么如何准确统计一个进程的共享内存和独占内存呢?

/proc/(pid)/smaps文件能给我们准确的答案。

内核对smaps的生成时,将一个页面被映射两次以上时,记入share_,否则记入private_(脏页记入_dirty,否则记入_clean)

所以所有share_的总和就是进程准确的共享内存总量,private_的总和就是进程准确的独占内存总量

这里有个前辈的脚本可以进行统计

#! /bin/bash
 
awk '
{
     pflag = 0
     if(NF>3)
     {
         if($2 ~ /[a-z-][a-z-][a-z-][p]/)
         {
              pflag = 1
         }
         else
         {
              pflag = 0
         }
         file = $6
     }
     while(getline)
     {
         if(NF > 3 && pflag)
         {
              pmap[NR] = vmsize" kb\t"prssc" kb\t"prssd" kb\t"file
              if($2 ~ /[a-z-][a-z-][a-z-][p]/)
              {
                   pflag = 1
              }
              else
              {
                   pflag = 0
              }
              file = $6
              continue
         }
         if(NF > 3 && !pflag)
         {
              smap[NR] = vmsize" kb\t"srssc" kb\t"srssd" kb\t"file
              if($2 ~ /[a-z-][a-z-][a-z-][p]/)
              {
                   pflag = 1
              }
              else
              {
                   pflag = 0
              }
              file = $6
              continue
         }
         if($1 ~ /^Size/)
         {
              VMSIZE += $2
              vmsize = $2
         }
         if($1 ~ /Rss/)
         {
              RSS += $2;
         }
         if($1 ~ /Shared_Clean/)
         {
              shared += $2
              srssc = $2
         }
         if($1 ~ /Shared_Dirty/)
         {
              shared += $2
              srssd = $2
         }
         if($1 ~ /Private_Clean/)
         {
              pclean += $2
              prssc = $2
             
         }
         if($1 ~ /Private_Dirty/)
         {
              pdirty += $2
              prssd = $2
         }
         if($1 ~ /^Swap/)
         {
              swap += $2
         }
        
     }
     if(pflag)
     {
              pmap[NF] = vmsize" kb\t"prssc" kb\t"prssd" kb\t"file
     }
     else
     {
              smap[NF] = vmsize" kb\t"srssc" kb\t"srssd" kb\t"file
     }
}
END{
     print "SWAP:\t"swap" kb"
     print "VMSIZE:\t"VMSIZE" kb"
     print "RSS:\t"RSS" kb \ttotal"
     print "\t"shared" kb \tshared"
     print "\t"pclean" kb \tprivate clean"
     print "\t"pdirty" kb \tprivate dirty"
     print "PRIVATE MAPPINGS"
     print "\tvmsize\trss clean\trss dirty\tfile"
     for(i in pmap)
     {
         print "\t"pmap[i]
     }
    
     print "SHARED MAPPINGS"
     print "\tvmsize\trss clean\trss dirty\tfile"
     for(i in smap)
     {
         print "\t"smap[i]
     }
     }' "/proc/$1/smaps"

例如查看init进程./smem.sh 1

SWAP:   0 kb
VMSIZE: 19236 kb
RSS:    1568 kb         total
        1032 kb         shared
        240 kb  private clean
        296 kb  private dirty

这里虚拟内存总量19236KB,实际驻留内存1568KB,其中共享内存1032KB,独占内存536KB

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
  1 root      20   0 19232 1568 1272 S  0.0  0.0   0:00.77 init   

TOP的VIRT和RES能对上,而SHR就偏大了

另外/proc/(pid)/smaps毕竟信息太多了,如果只是想看看有哪些内存占用,可以用pmap (pid)指令

pmap 1   
1:   /sbin/init
00007f53eda00000     48K r-x--  /lib64/libnss_files-2.12.so
00007f53eda0c000   2048K -----  /lib64/libnss_files-2.12.so
00007f53edc0c000      4K r----  /lib64/libnss_files-2.12.so
00007f53edc0d000      4K rw---  /lib64/libnss_files-2.12.so
00007f53edc0e000   1576K r-x--  /lib64/libc-2.12.so
00007f53edd98000   2048K -----  /lib64/libc-2.12.so
00007f53edf98000     16K r----  /lib64/libc-2.12.so
00007f53edf9c000      4K rw---  /lib64/libc-2.12.so
00007f53edf9d000     20K rw---    [ anon ]
00007f53edfa2000     88K r-x--  /lib64/libgcc_s-4.4.7-20120601.so.1
00007f53edfb8000   2044K -----  /lib64/libgcc_s-4.4.7-20120601.so.1
00007f53ee1b7000      4K rw---  /lib64/libgcc_s-4.4.7-20120601.so.1
00007f53ee1b8000     28K r-x--  /lib64/librt-2.12.so
00007f53ee1bf000   2044K -----  /lib64/librt-2.12.so
00007f53ee3be000      4K r----  /lib64/librt-2.12.so
00007f53ee3bf000      4K rw---  /lib64/librt-2.12.so
00007f53ee3c0000     92K r-x--  /lib64/libpthread-2.12.so
00007f53ee3d7000   2048K -----  /lib64/libpthread-2.12.so
00007f53ee5d7000      4K r----  /lib64/libpthread-2.12.so
00007f53ee5d8000      4K rw---  /lib64/libpthread-2.12.so
00007f53ee5d9000     16K rw---    [ anon ]
00007f53ee5dd000    256K r-x--  /lib64/libdbus-1.so.3.4.0
00007f53ee61d000   2044K -----  /lib64/libdbus-1.so.3.4.0
00007f53ee81c000      4K r----  /lib64/libdbus-1.so.3.4.0
00007f53ee81d000      4K rw---  /lib64/libdbus-1.so.3.4.0
00007f53ee81e000     36K r-x--  /lib64/libnih-dbus.so.1.0.0
00007f53ee827000   2044K -----  /lib64/libnih-dbus.so.1.0.0
00007f53eea26000      4K r----  /lib64/libnih-dbus.so.1.0.0
00007f53eea27000      4K rw---  /lib64/libnih-dbus.so.1.0.0
00007f53eea28000     96K r-x--  /lib64/libnih.so.1.0.0
00007f53eea40000   2044K -----  /lib64/libnih.so.1.0.0
00007f53eec3f000      4K r----  /lib64/libnih.so.1.0.0
00007f53eec40000      4K rw---  /lib64/libnih.so.1.0.0
00007f53eec41000    128K r-x--  /lib64/ld-2.12.so
00007f53eee51000     20K rw---    [ anon ]
00007f53eee5f000      4K rw---    [ anon ]
00007f53eee60000      4K r----  /lib64/ld-2.12.so
00007f53eee61000      4K rw---  /lib64/ld-2.12.so
00007f53eee62000      4K rw---    [ anon ]
00007f53eee63000    140K r-x--  /sbin/init
00007f53ef085000      8K r----  /sbin/init
00007f53ef087000      4K rw---  /sbin/init
00007f53f102d000    132K rw---    [ anon ]
00007fff9965c000     84K rw---    [ stack ]
00007fff996c8000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
 total            19232K

这里能看到共享的动态库,和栈stack,另外anon是指的类似malloc这样的匿名页面

未完待续

08 Jul 2015