内存方面的知识真是博大精深,把一些看到的零碎的知识点做个笔记总结下。
这些知识只是自己的理解,并不能保证正确,ULK和《深入理解Linux虚拟内存管理》这两本书看完以后还会来更新这篇的内容。
段式管理与页式管理 
在操作系统理论中
段式管理就是将内存分成段,段的大小不是固定的。虚拟地址被分为段号和段内地址,根据段表查到段号对应的起始地址,将起始地址和段内地址相加即为物理地址。
段表 
 
页式管理就是将内存分成页,页的大小都是固定的。虚拟地址被分为页号和页内地址,根据页表查到页号对应的页面号,将页面号对应起始地址和页内地址相加即为物理地址。
页表 
 
为什么有段页管理呢,为什么有虚拟地址呢
内存管理的发展是和硬件发展挂钩的,在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来标识。
这里有张图是用户空间的虚拟地址的实际分布情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 --------------------------------------- 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.,8 MB)         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的文件格式下还有一些其他内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Segment header table text seg: .init 初始化段 .text 代码段 .rodata 只读数据(常量等) data seq: .data 可读写数据(全局变量等) bss seq: .bss 未初始化数据 可以不必加载的seg: .symtab 符号表 .debug 调试信息 .line 指令和源文件行对应 .strtab 符号字符串实际存放处 
内存相关的一些工具 
free 
查看内存使用情况最常用的手段就是free -m了
1 2 3 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 来做分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 kBActive (file) :    4852520 kB  file是文件内存Inactive (file) :  1911440 kBUnevictable:           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_*的总和就是进程准确的独占内存总量
这里有个前辈的脚本可以进行统计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 #! /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
1 2 3 4 5 6 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
1 2 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)指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 pmap 1     1 :   /sbin/init00007f 53eda00000     48 K r-x--  /lib64/libnss_files-2.12 .so00007f 53eda0c000   2048 K -----  /lib64/libnss_files-2.12 .so00007f 53edc0c000      4 K r----  /lib64/libnss_files-2.12 .so00007f 53edc0d000      4 K rw---  /lib64/libnss_files-2.12 .so00007f 53edc0e000   1576 K r-x--  /lib64/libc-2.12 .so00007f 53edd98000   2048 K -----  /lib64/libc-2.12 .so00007f 53edf98000     16 K r----  /lib64/libc-2.12 .so00007f 53edf9c000      4 K rw---  /lib64/libc-2.12 .so00007f 53edf9d000     20 K rw---    [ anon ]00007f 53edfa2000     88 K r-x--  /lib64/libgcc_s-4.4 .7 -20120601. so.1 00007f 53edfb8000   2044 K -----  /lib64/libgcc_s-4.4 .7 -20120601. so.1 00007f 53ee1b7000      4 K rw---  /lib64/libgcc_s-4.4 .7 -20120601. so.1 00007f 53ee1b8000     28 K r-x--  /lib64/librt-2.12 .so00007f 53ee1bf000   2044 K -----  /lib64/librt-2.12 .so00007f 53ee3be000      4 K r----  /lib64/librt-2.12 .so00007f 53ee3bf000      4 K rw---  /lib64/librt-2.12 .so00007f 53ee3c0000     92 K r-x--  /lib64/libpthread-2.12 .so00007f 53ee3d7000   2048 K -----  /lib64/libpthread-2.12 .so00007f 53ee5d7000      4 K r----  /lib64/libpthread-2.12 .so00007f 53ee5d8000      4 K rw---  /lib64/libpthread-2.12 .so00007f 53ee5d9000     16 K rw---    [ anon ]00007f 53ee5dd000    256 K r-x--  /lib64/libdbus-1. so.3 .4 .0 00007f 53ee61d000   2044 K -----  /lib64/libdbus-1. so.3 .4 .0 00007f 53ee81c000      4 K r----  /lib64/libdbus-1. so.3 .4 .0 00007f 53ee81d000      4 K rw---  /lib64/libdbus-1. so.3 .4 .0 00007f 53ee81e000     36 K r-x--  /lib64/libnih-dbus.so.1 .0 .0 00007f 53ee827000   2044 K -----  /lib64/libnih-dbus.so.1 .0 .0 00007f 53eea26000      4 K r----  /lib64/libnih-dbus.so.1 .0 .0 00007f 53eea27000      4 K rw---  /lib64/libnih-dbus.so.1 .0 .0 00007f 53eea28000     96 K r-x--  /lib64/libnih.so.1 .0 .0 00007f 53eea40000   2044 K -----  /lib64/libnih.so.1 .0 .0 00007f 53eec3f000      4 K r----  /lib64/libnih.so.1 .0 .0 00007f 53eec40000      4 K rw---  /lib64/libnih.so.1 .0 .0 00007f 53eec41000    128 K r-x--  /lib64/ld-2.12 .so00007f 53eee51000     20 K rw---    [ anon ]00007f 53eee5f000      4 K rw---    [ anon ]00007f 53eee60000      4 K r----  /lib64/ld-2.12 .so00007f 53eee61000      4 K rw---  /lib64/ld-2.12 .so00007f 53eee62000      4 K rw---    [ anon ]00007f 53eee63000    140 K r-x--  /sbin/init00007f 53ef085000      8 K r----  /sbin/init00007f 53ef087000      4 K rw---  /sbin/init00007f 53f102d000    132 K rw---    [ anon ]00007f ff9965c000     84 K rw---    [ stack  ]00007f ff996c8000      4 K r-x--    [ anon ]ffffffffff600000      4 K r-x--    [ anon ]  total            19232 K 
这里能看到共享的动态库,和栈stack,另外anon是指的类似malloc这样的匿名页面
未完待续