内存方面的知识真是博大精深,把一些看到的零碎的知识点做个笔记总结下。
这些知识只是自己的理解,并不能保证正确,ULK和《深入理解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来标识。
这里有张图是用户空间的虚拟地址的实际分布情况:
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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 --------------------------------------- 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的文件格式下还有一些其他内容 ```c Segment header table text seg: .init 初始化段 .text 代码段 .rodata 只读数据(常量等) data seq: .data 可读写数据(全局变量等) bss seq: .bss 未初始化数据 可以不必加载的seg: .symtab 符号表 .debug 调试信息 .line 指令和源文件行对应 .strtab 符号字符串实际存放处 ``` ## 内存相关的一些工具 **free ** 查看内存使用情况最常用的手段就是free -m了 ```c 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](https: ```c 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_ 的总和就是进程准确的独占内存总量
这里有个前辈的脚本可以进行统计
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这样的匿名页面
未完待续