linux的内存管理介绍

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

这些知识只是自己的理解,并不能保证正确,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.,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的文件格式下还有一些其他内容

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 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/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这样的匿名页面

未完待续