linux的内存管理介绍

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

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

```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://www.kernel.org/doc/Documentation/filesystems/proc.txt)来做分析

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

未完待续