Linux内核设计与实现 读书笔记:
http://www.cnblogs.com/wang_yb/tag/linux-kernel/
http://blog.csdn.net/yrj/article/category/718110
第一篇 内存的测量
2.1. 系统当前可用内存
- # cat /proc/meminfo
MemTotal: 8063544 kB
MemFree: 900952 kB
Buffers: 1183596 kB
Cached: 1596808 kB
MemTotal:总共可用物理内存
Buffers:主要是用来给 Linux 系统中块设备做缓冲区
Cached:用来缓冲我们所打开的文件(Linux 的思想是,如果内存充足,不用白不用,它会使用内存来 cache 一些文件,从而加快进程的运行速度;当内存不足时,这些内存又会被回收,供程序使用。)
所以真正可用的内存 = MemFree + Buffers + Cached
被用掉的物理内存 = MemTotal - 真正可用的内存
- # ls /proc
- 1 1139 1536 1832 2042 2089 2151 2257 2647 313 4 4407 63 901 bus iomem modules sysrq-trigger
- 10 12 1541 1833 2043 2090 2155 22806 26820 32 40 47 6996 902 cgroups ioports mounts sysvipc
- 1003 1263 1661 1861 2045 2098 2172 22961 27 323 41 48 7 932 cmdline irq mtrr timer_list
- # ls /proc/1139
- attr cmdline environ io maps mountstats oom_score sched stack syscall
auxv coredump_filter exe latency mem net pagemap schedstat stat task
cgroup cpuset fd limits mountinfo numa_maps personality sessionid statm wchan
clear_refs cwd fdinfo loginuid mounts oom_adj root smaps status - # cat status
数字代表着一个个目录;同时这些数字也与当前系统中运行进程的 PID 一一对应。在这些目录下面的文件,记录着这些进程在 Linux 内核中相应的数据。其中status记录了一些比较有用的信息,而oom_adj是OOM机制的重要参考。
2.2. 虚拟内存与物理内存
malloc只是分配了虚拟内存,kernel 不会分配物理页面给进程。
strcpy一类操作时,进程需要使用这块内存了,kernel 会产生一个页故障,从而为系统分配一个物理页面。
kernel 分配物理内存的最小单位为一个物理页面,一个物理页面为(4K Byte)
- # cat statm
- 345 87 74 1 0 58 0
这里有 7 个数,它们以页(4K)为单位。
Size (total pages,345) 任务虚拟地址空间的大小
Resident(pages,87) 应用程序正在使用的物理内存的大小
Shared(pages,74) 共享页数
Trs(pages,1) 程序所拥有的可执行虚拟内存的大小
Lrs(pages,0) 被映像到任务的虚拟内存空间的库的大小
Drs(pages,58) 程序数据段和用户态的栈的大小
dt(pages,0) 脏页数量(已经修改的物理页面),这个数一些系统可能修改成直接返回0了
其中Size Trs Lrs Drs对应于进程的虚拟内存,Resident shared dr对应于物理内存
- #cat maps
- 00008000-00009000 r-xp 00000000 1f:12 288 /mnt/msc_int0/hello // 该段内存地址对应于进程的代码段,4k
- 00010000-00011000 rw-p 00000000 1f:12 288 /mnt/msc_int0/hello // 该段内存地址对应与进程的数据段,4k
- 00011000-00032000 rwxp 00011000 00:00 0 // heap,132k
- 40000000-40002000 rw-p 40000000 00:00 0 //
- 41000000-41017000 r-xp 00000000 1f:0d 817360 /lib/ld-2.3.3.so
- 4101e000-41020000 rw-p 00016000 1f:0d 817360 /lib/ld-2.3.3.so
- 41028000-41120000 r-xp 00000000 1f:0d 817593 /lib/libc-2.3.3.so
- 41120000-41128000 ---p 000f8000 1f:0d 817593 /lib/libc-2.3.3.so
- 41128000-41129000 r--p 000f8000 1f:0d 817593 /lib/libc-2.3.3.so
- 41129000-4112c000 rw-p 000f9000 1f:0d 817593 /lib/libc-2.3.3.so
- 4112c000-4112e000 rw-p 4112c000 00:00 0
- befeb000-bf000000 rwxp befeb000 00:00 0 // stack, 84K
00008000-00009000 r-xp 00000000 1f:12 288 /mnt/msc_int0/hello分别对应:
内存段虚拟地址起始,权限,偏移量,映射文件的主设备号和次设备号,映射文件节点号,映像文件的路径。
我们可以通过 cat /proc/devices 来查看指定映射文件的主设备号的设备信息。
32位操作系统每个进程4G虚拟内存,由上图可知:0x00000000-0xbfffffff,用户空间3G;0xc0000000-0xffffffff,内核空间1G
实际物理内存如下:
- #cat memmap // 这个命令不一定每个linux版本都有,没有的话可能要自己写驱动
- 2 // 对应cat maps第一行,也就是进程的代码段
1 // 对应cat maps第二行,也就是进程的数据段
100000000000000000000000000000000 // 33个字符,对应132k内存,仅第一页实际使用了
11
4949494949494949491649494949494949494949494949
11
4917171717111617171717171717174949 0 049 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 049 0 0 0
0 0 0 0 0 0 0 044 042424242 0 0 0 041 0 0 0 0 0 0 0 0 0 0 0 0 0 04847 0 0 0 0 0 0 0
04747484949494949494949 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 049 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 04849 0 0 0 04849 0 0 047 049 0 0 0 0 0 0 0 0 0 0 04949 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 046 0 0 0 0 0 0 0 0 0 0 0 0 0 044 0
000000000
00000000
49
111
11
000000000000000000011
每个数字对应内存的一个页面(4k),如果为0,表示该页面空闲。其中代码段系统共享,数据段,堆栈是每个进程私有的。
2.3 关于交换分区和内存回收
之所以移动设备不采用交换分区:
1)交换分区会导致整体性能会下降很多
2)嵌入式设备一般使用 Flash 做为存储介质, Flash 写的次数是有限的。而如果在 Flash 上面建立交换分区的话,必然导致对 Flash 的频繁写,进而影响 Flash 的寿命
内存回收机制:
在 Linux 物理内存,每个页面有一个 dirty 的标志,如果该页面被改写了,我们称之为 dirty page。非dirty page都可以回收。也就是说,一个进程的代码段全部可以回收,数据段视情况而定,堆栈实际分配的物理内存均不能回收。
第二章 内存的优化
2.4 进程
一个进程运行时,所占的内存除了堆栈外,还包括:
全局变量、静态变量:初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。(数据段)
文字常量:常量字符串就是放在这里的,程序结束后由系统释放。(代码段)
程序代码:存放函数体的二进制代码。(代码段)
堆段:
malloc、free、new、delete 是我们从程序的角度去看待堆内存;而在 Linux 内核中会专门为进程分配一段内存地址,用来存放堆的内容;随着进程申请内存的增加,进程会通过系统调用brk,来让Linux内核扩展这段内存空间,从而分配给进程更多的内存;当进程释放内存时,进程又会通过系统调用brk,来告诉内核缩减这段内存空间,Linux 内核便会将其一部分物理内存进行回收。
由于用户态进程申请内存是以字节为单位,而在内核中内存的管理是以页面(4K)为单位;如何减少brk次数成为提升系统性能的一个关键。
堆内分配内存最少为16个字节,以8位对齐,也就是说16位,24位,32位,以此类推。
char* p;
p = malloc(20);
那么,实际分配的大小会写在:*(p-4),具体内存分布为:
*(p-5): prev_size
*(p-4): size
*(p-3): 标志位
*(p-2): M值。M=1表示该内存块通过mmap来分配,只有在分配大块内存时,才采用mmap的方式,那么在释放时会的munmap_chunk()去释放;否则,释放时由chunk_free()完成。实际上在glibc的内存管理中,采用brk的方式,只能管理1G 地址空间以下的内存,如果大于了1G,glibc 将采用mmap的方式,为堆申请一块内存。brk用于更改堆顶地址,而mmap则为进程分配一块虚拟地址空间。
*(p-1): P值。P=0,表示上一块空闲,这时prev_size通常为上一块的大小;P=1,表示上一块正在被使用,这时prev_size通常为0。
为了提高速度,glibc的malloc实现了fastbins,可以通过mallopt(M_MXFAST, value)来设置其伐值。对于所有小于M_MXFAST阀值的小块内存释放时,不会去尝试合并,还会被复用;大于M_MXFAST的会尝试合并和复用。M_MXFAST阀值设为0相当于禁用fastbins功能,也就是说会一直尝试合并和复用内存。总体来说,M_MXFAST对于内存使用的影响没有一个定论,但它肯定会加快进行的运行速度。
大块内存分配:
大块内存分配以后,通过查看 maps 文件,你可以发现增加一段线性区(也就是多了一行)。其权限为 rw-p。与进程缺省堆内存的权限rwxp是不同的。
一些阀值的设置方式也是调用mallopt。取值分别为M_MMAP_THRESHOLD、M_MMAP_MAX。
M_MMAP_THRESHOLD
libc中大块内存阀值,大于该阀值的内存申请,内存管理器将使用mmap系统调用申请内存;如果小于该阀值的内存申请,内存管理其使用brk系统调用来扩展堆顶指针。该阀值缺省值为128kB。
M_MMAP_MAX
该进程中最多使用mmap分配地址段的数量。设为0表示该进程禁用mmap功能。默认值为65536。
内存释放:
在libc中,只有当堆顶有连续的128k空闲内存时,libc才会掉用brk,来通知内核释放这段内存,将其返还给系统。这个默认的128k触发条件可以通过mallopt修改M_TRIM_THRESHOLD来改变。
M_TRIM_THRESHOLD
堆顶内存回收阀值,当堆顶连续空闲内存数量大于该阀值时,libc的内存管理其将调用系统调用brk,来调整堆顶地址,释放内存。该值缺省为128k。
M_TOP_PAD
该参数决定了,当libc内存管理器调用brk释放内存时,堆顶还需要保留的空闲内存数量。该值缺省为 0.
内存空洞:
其实是由于linux内存分配释放机制导致的。只要堆顶没释放,下面的释放将不会实质上真正释放。
应对策略:1)调低mmap阀值,用mmap,代价是,会多次调用系统调用(mmap和unmmap),使进程整体性能下降。
2)尽量优先释放堆顶内存。代码习惯问题,比较难以彻底解决。
内存空洞和内存泄漏是有区别的,后者会一直增加,前者增加到一定地步趋于稳定。
2.4 内存的跟踪
使用 mtrace 检测内存泄漏
1、 引入头文件 #include
2、 在需要跟踪的程序中需要包含头文件,而且在main()函数的最开始包含一个函数调用:mtrace()。由于在main 函数的最开头调用了mtrace(),所以该进程后面的一切分配和释放内存的操作都可以由mtrace来跟踪和分析。
3、 在运行进程前定义一个环境变量,用来指示一个文件。该文件用来输出 log 信息。如下的例子:
$ export MALLOC_TRACE=mymemory.log
4、 正常运行程序。此时程序中的关于内存分配和释放的操作都可以记录下来。