您的位置:首页 > 运维架构 > Linux

Linux内存管理 —— 进程的虚拟地址空间和VSS

2018-03-06 17:47 253 查看

1. 进程虚拟地址空间

进程的虚拟地址空间记录在其task_struct结构的指针成员mm中(struct mm_struct),这是用户态进程才有的,里面包含pgd、代码段、堆、栈地址等信息。(kernel不需要mm,因为kernel不需要引用动态库,内核线程有自己的栈空间,虚拟地址映射关系也是全局可见的,注意vmalloc和kmap产生的映射是由内核中的全局变量swapper_pg_dir和pkmap_page_table记录的,无论当前哪个用户进程处于活动状态,虚拟地址空间内核部分的内容总是相同的。)

mm有一个名为mmap的指针,它指向一个链表,链表每一个元素都是vm_area_struct结构,称为VMA。这个链表的每一段(每个元素)都是这个进程的一个虚拟地址空间。

也就是说,一个进程的地址空间都是一段一段的,通过pmap pid命令、/proc/pid/maps、/proc/pid/smaps都能看出一个进程的虚拟地址空间分布情况,第三个看得最仔细,其中的每一个(虚拟地址)连续的段都对应一个VMA,例如代码段、数据段、BSS、堆、动态库的内存映射、栈空间等。

root@jac:~# cat /proc/1750/maps
00008000-0000b000 r-xp 00000000 1f:06 537        /bin/test
00013000-00014000 rw-p 00003000 1f:06 537        /bin/test
0186a000-0187c000 rw-p 00000000 00:00 0          [heap]
4004b000-40052000 r-xp 00000000 1f:06 436        /lib/ld-uClibc.so.0
40059000-4005a000 rw-p 00006000 1f:06 436        /lib/ld-uClibc.so.0
400be000-400d2000 r-xp 00000000 1f:06 405        /lib/libpthread.so.0
400d2000-400d9000 ---p 00000000 00:00 0
400d9000-400da000 rw-p 00013000 1f:06 405        /lib/libpthread.so.0
400da000-400dc000 rw-p 00000000 00:00 0
400e5000-400e6000 rw-p 00000000 00:00 0
400e6000-400e8000 r-xp 00000000 1f:06 439        /lib/libdl.so.0
400e8000-400f0000 ---p 00000000 00:00 0
400f0000-400f1000 rw-p 00002000 1f:06 439        /lib/libdl.so.0
401bf000-401c0000 rw-p 0000f000 1f:06 443        /lib/libm.so.0
401c0000-4024c000 r-xp 00000000 1f:06 349        /lib/libc.so.0
4024c000-40253000 ---p 00000000 00:00 0
40253000-40255000 rw-p 0008b000 1f:06 349        /lib/libc.so.0
40255000-4025a000 rw-p 00000000 00:00 0
4025a000-4025b000 ---p 00000000 00:00 0
4025b000-40a5a000 rw-p 00000000 00:00 0
be919000-be93a000 rw-p 00000000 00:00 0          [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]


每个vm_area_struct的vm_start成员代表这个区域的开始地址(包含vm_start自身),vm_end标记结束地址(不包含vm_end自身)。两个区域不能落在一个页里,所以起始地址都是4K对齐的。

不在任何一个区域里的虚拟地址对进程来说就是非法的,你访问它就会导致一个page fault,kernel发个信号,程序就挂了。那么我们可以想象,free释放的内存如果还没有还给内核,则页表和vma都没变,进程实际上可以在没有malloc的情况下访问这块内存而不会产生page fault,所以有时候数据越界了也不会挂掉,只不过风险是你并不知道到底有没有还给内核以及该进程其他地方malloc的时候又用了该段内存就写覆盖了。

所有的VMA就构成了进程的虚拟地址空间。但是在VMA的东西不一定在内存,他只是虚拟地址而并一定占用实际的内存,就像前面说的写时拷贝的数据,申请了10MB,VMA就增加了10MB(权限是r+w),但可能还没有在内存里。你拿到的内存就是RSS,假的内存就是VSS。

所以,VMA有很大的参考价值,首先,你访问的地址必须落在VMA里,其次,即使落在VMA,可能因为权限问题也不一定能访问成功。MMU会通过page fault告知kernel异常的地址和原因。

发生page fault的几种可能性:

访问的地址没有落在VMA里面,MMU产生缺页中断,设置寄存器保存缺页原因,CPU收到缺页中断,从寄存器读出地址和原因,非法地址,就给进程产生segmentation fault。

上述malloc了10MB但没写的情况,在写的时候,访问的地址落在VMA里面,但页表显示页的权限是只读,MMU产生缺页中断,设置寄存器保存缺页原因,CPU收到缺页中断,发现是权限问题,但是查找VMA发现这个地址是可读写的,因此申请一页内存,更新页表。

假如由于栈溢出等原因,pc指向了栈空间,或试图写代码段,同2,但CPU发现VMA中也是权限不允许的,进程同样就segmentation fault了。

执行代码段,但代码段尚未读到内存里的时候。则同2,权限是对的,因此申请内存,读出代码段。

注意,“缺页中断”不是中断,而是一种异常状态,只是翻译问题~

上述2和4都是缺页后申请内存的,4拿了内存并且产生了I/O,属于major缺页,2是直接拿内存,属于minor缺页。因此major缺页的处理时间要远大于minor缺页。

2. 评估进程的内存使用情况

一个应用程序究竟消耗了多少内存。很多内存通过lazy方式拿内存,那这部分内存是怎么统计的呢。内存共享(例如共享库的代码段)的情况如何统计呢,共享库的数据段仍然是写时拷贝的。或者一个程序执行两次跑起两个进程,代码段也是共享的。这都怎么统计呢?

我们使用如下几个值来评估内存是如何被进程瓜分的:

VSS: Virtual Set Size

RSS: Resident Set Sise

PSS: Proportional Set Sise

USS: Unique Set Sise



图的中间是一个内存条,图中有三个进程,其中两个进程由同一个程序执行而成,三个进程都引用了libc。图中4/5/6是指已经载入内存的部分。其中:

VSS是进程认为自己应该拿到的内存的大小,但实际可能还没完全拿到,对于图中1044进程,就包括整个进程text和libc的text以及堆,但进程代码段和libc代码段可能只有部分载入内存了。

RSS则记录了进程实实在在占用的内存的大小,即图中的4+5+6。但RSS没有体现多个进程引用同一个共享库时,它们瓜分共享库代码段的比例情况。

PSS则在RSS的基础上,对于多个进程共享的资源(如共享库中的代码段和未cow的数据)会精确计算出每个进程的占比,例如图中1044和1045两个进程是同一个程序执行后启动的,因此他们共享这个程序的代码段,所以PSS统计的代码段大小只有5/2(这里只是方便说明直接除2,实际占比计算会更精确)。因此PSS会比RSS小。

USS是进程的独占内存,因此共享库、程序代码段这些就不算在内。

smem工具,它有命令行和图形界面工具。可以看出进程的RSS/USS/VSS/PSS的情况。

共享库和共享内存都不会反映在uss里面,因为uss意思是独占内存。一个进程使用了共享库和共享内存的“实际部分”会反映在pss上。而rss是包含“整个”共享库和共享内存的。另外注意这些量统计时都不包含3G以上的内存。

如果一个进程的内存被换出到swap分区,那这个进程的RSS/PSS/USS就会相应减少,因为它们记录的是物理内存里真实存在的量,被换出的页已经不在内存了。而VSS不会随之改变。

内存泄漏

进程运行过程中,但是申请和释放不匹配,导致随着时间推移,进程耗费越来越多的内存。现象就是通过smem等工具或进程的/proc/pid/status定期地去查看物理内存使用情况,发现内存用量越来越多。看内存泄漏主要看USS就够了,因为泄漏的主要是heap。

检查内存泄漏的工具:

1. valgrind,缺点是极大影响程序运行速度(它会把程序放到一个虚拟机里面跑),好处是不用重新编译程序。

valgrind –tool=memcheck –leak-check=yes ./a.out

2. addresss sanitizer,简称asan,需要gcc4.9以后版本,且目前只支持Linux x86_64。需要添加 -g -fsanitize=address 的编译选项重新编译程序。然后执行程序。

可以在程序里调用 __lsan_do_leak_check() 指定在此处进程check(头文件 sanitizer/lsan_interface.h)。

3. dmalloc,需要内核的支持,可以探测和统计某一段代码运行过程中有没有内存泄漏。它实际上是重写了malloc/free等函数并增加回溯和统计信息,同样的,dmalloc会影响程序运行速度。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息