内存方面的知识真是博大精深,把一些看到的零碎的知识点做个笔记总结下。
这些知识只是自己的理解,并不能保证正确,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这样的匿名页面
未完待续