天天看点

Linux内核设计与实现 读书笔记 转

Linux内核设计与实现  读书笔记:

http://www.cnblogs.com/wang_yb/tag/linux-kernel/

《深入理解LINUX内存管理》

http://blog.csdn.net/yrj/article/category/718110

Linux内存管理和性能学习笔记(一)

:内存测量与堆内存 

第一篇 内存的测量

2.1. 系统当前可用内存

  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 - 真正可用的内存

  1. # ls /proc
  2. 1 1139 1536 1832 2042 2089 2151 2257 2647 313 4 4407 63 901 bus iomem modules sysrq-trigger
  3. 10 12 1541 1833 2043 2090 2155 22806 26820 32 40 47 6996 902 cgroups ioports mounts sysvipc
  4. 1003 1263 1661 1861 2045 2098 2172 22961 27 323 41 48 7 932 cmdline irq mtrr timer_list
  5. # ls /proc/1139
  6. 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

  7. # cat status

数字代表着一个个目录;同时这些数字也与当前系统中运行进程的 PID 一一对应。在这些目录下面的文件,记录着这些进程在 Linux 内核中相应的数据。其中status记录了一些比较有用的信息,而oom_adj是OOM机制的重要参考。

2.2. 虚拟内存与物理内存

malloc只是分配了虚拟内存,kernel 不会分配物理页面给进程。

strcpy一类操作时,进程需要使用这块内存了,kernel 会产生一个页故障,从而为系统分配一个物理页面。

kernel 分配物理内存的最小单位为一个物理页面,一个物理页面为(4K Byte)

  1. # cat statm
  2. 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对应于物理内存

  1. #cat maps
  2. 00008000-00009000 r-xp 00000000 1f:12 288 /mnt/msc_int0/hello // 该段内存地址对应于进程的代码段,4k
  3. 00010000-00011000 rw-p 00000000 1f:12 288 /mnt/msc_int0/hello // 该段内存地址对应与进程的数据段,4k
  4. 00011000-00032000 rwxp 00011000 00:00 0                       // heap,132k
  5. 40000000-40002000 rw-p 40000000 00:00 0                       // 
  6. 41000000-41017000 r-xp 00000000 1f:0d 817360 /lib/ld-2.3.3.so
  7. 4101e000-41020000 rw-p 00016000 1f:0d 817360 /lib/ld-2.3.3.so
  8. 41028000-41120000 r-xp 00000000 1f:0d 817593 /lib/libc-2.3.3.so
  9. 41120000-41128000 ---p 000f8000 1f:0d 817593 /lib/libc-2.3.3.so
  10. 41128000-41129000 r--p 000f8000 1f:0d 817593 /lib/libc-2.3.3.so
  11. 41129000-4112c000 rw-p 000f9000 1f:0d 817593 /lib/libc-2.3.3.so
  12. 4112c000-4112e000 rw-p 4112c000 00:00 0 
  13. 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

实际物理内存如下:

  1. #cat memmap                         // 这个命令不一定每个linux版本都有,没有的话可能要自己写驱动
  2. 2                                   // 对应cat maps第一行,也就是进程的代码段

    1                                   // 对应cat maps第二行,也就是进程的数据段

    100000000000000000000000000000000   // 33个字符,对应132k内存,仅第一页实际使用了

    11

    4949494949494949491649494949494949494949494949

    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

    49

    111

每个数字对应内存的一个页面(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、 正常运行程序。此时程序中的关于内存分配和释放的操作都可以记录下来。