Linux内存使用调整
版权声明:本文为博主原创文章,未经博主允许不得转载。
前段时间在做播放器的时候,遇到个问题,花了很长时间,做个记录,希望对有需要的人有所帮助:
播放器的播视频的时候,无论是手动切换视频还是到视频播放完成,自动切换视频,一定次数后均出现黑屏现象,偶尔有声音,问题出现后,不可恢复,Kernel输出如下Log:
DMA free:71672kB min:616kB low:768kB high:924kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB isolateds
lowmem_reserve[]: 0 1244 1593 1593
Normal free:1249804kB min:4212kB low:5264kB high:6316kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB o
lowmem_reserve[]: 0 0 2794 2794
HighMem free:223000kB min:348kB low:640kB high:936kB active_anon:39248kB inactive_anon:140kB active_file:61020kB inactive_file:34108kB unevio
lowmem_reserve[]: 0 0 0 0
DMA: 22*4kB 90*8kB 21*16kB 82*32kB 7*64kB 137*128kB 69*256kB 3*512kB 8*1024kB 5*2048kB 3*4096kB 0*8192kB 0*16384kB 0*32768kB = 71672kB
Normal: 1*4kB 53*8kB 16*16kB 6*32kB 6*64kB 4*128kB 1*256kB 1*512kB 2*1024kB 2*2048kB 3*4096kB 2*8192kB 4*16384kB 35*32768kB = 1249772kB
HighMem: 14*4kB 10*8kB 437*16kB 386*32kB 374*64kB 27*128kB 10*256kB 7*512kB 8*1024kB 7*2048kB 4*4096kB 2*8192kB 1*16384kB 3*32768kB = 223000B
23829 total pagecache pages
0 pages in swap cache
Swap cache stats: add 0, delete 0, find 0/0
Free swap = 0kB
Total swap = 0kB
524288 pages of RAM
387015 free pages
73287 reserved pages
1169 slab pages
8545 pages shared
0 pages swap cached
Physical memory allocation error!
Physical memory allocation error!
通过Log,定位到输出的位置是解码驱动中,驱动在DMA请求失败后,输出以上信息。针对这个现象,按以下几个思路进行的调查:
1. 内存泄露
在驱动中增加了一些Log,随着视频切换,分配的地址会一定程度的变大,直到最后分配失败。但仔细研究发现分配与释放是配对的,驱动中记录的内存使用信息完全正确,没有任何迹象表明有内存泄露发生。
2. 内存碎片
官论上有同样的问题登录,但无明确结论,官方应该是没重现。民间有人指出问题在于驱动请求的连续内存块比较大,但系统已无相应的内存可供分配,即内存碎片了。同时有人给出俩个方案:缓存每次的请求的内存,分配后,不再返还给系统,这个方案我测试后,发现会导致其它视频驱动出现同样问题,不可用;另一个方案是认为导致内存碎片的原因在于,文件系统的Cache机制,要解决这个问题,需要清除被Cache占用的内存,方法如下:
echo 3 > /proc/sys/vm/drop_caches
试验之后,果然有效,测试了手中的测试用例几千次后,发了测试软件,结果悲剧,开发部的测试用例几次就测出同样的问题,该方案水土不服。
3. 调查Linux内存使用
从内存碎片这个思路出发,分析什么导致了驱动的DMA空间碎片化。查了一些资料发现,Linux的内存划分分成3块:DMA,Low Memory和High Memory。一般32位PC的配置是DMA Zone位于0x00010000-0x00ffffff,大约16MB,Low Memory是0x01000000-0x37bfdfff,约800多MB,再往上的空间给High Memory,这部分的访问要通过跳转,性能不如DMA Zone和Low Memory Zone快。而内存分配的策略是从上往下分配,一旦高位的内存空间不足,则向低地址的空间请求内存。在我们的产品上,总的内存大小是512MB,某些驱动预留128MB,DMA Zone的配置是184MB,那么实际剩下的Low Memory Zone只有200MB,而这200MB要运行UI,媒体管理,互联等等一系列进程。一旦Low Memory Zone的内存不够用,系统会向DMA Zone请求内存,从而导致的DMA空间的内存碎片化!而这时,通过free命令是看不到正确的内存信息的,因为看到的空间还有,但一是被Cache占用,二是计算的是总数,而不是真正的连续内存,这时查看内存应该使用系统提供的Buddy Info来看真正剩余的内存块:
$cat /proc/buddyinfo
Node 0, zone DMA 3 1 3 4 1 2 2 0 1 2 0 3 2 2
Node 0, zone Normal 74 36 29 8 6 4 3 2 1 2 3 2 0 2
输出的数据从左侧开始是4K内存块的个数,依次乘2到最右侧的32M;在出问题时,系统基本上只有小块的内存,而问题的原因就是一些文件访问的操作导致DMA Zone的空间被Cache使用了。
4. 控制内存分配算法
找到问题的原因后,调查如何能禁止系统从DMA Zone分配内存。查找到可以设置lowmem_reserve_ratio,来告诉系统,DMA Zone的预留空间。其参数有3个(系统默认:256,32,32),与DMA Zone有关的是第一个,其值是除数,用DMA Zone的大小除以256,得到的基本就是预留的大小了。做了几个测试后,发现只有设置为1才能有效保护DMA Zone不被占用,:(!设置为1后,视频不再出现黑屏,但其他问题很快出现,因为系统可用的内存减少,内存的回收算法被频繁执行,性能急剧下降。
5. 最终方案
由于产品的总内存太小,而预留的DMA Zone太大,所以需要调整DMA Zone预留的空间,但lowmem_reserve_ratio已无调整空间,所以考虑从调整Kernel上下手。最后,将Kernel中的DMA Zone总大小由184MB调整为原来的3/4,即138MB,得到一个近似合理的配置,保证的DMA和Low Memory都可以申请到足够的空间,长时间测试后,未再现相关问题。
//
解读vmstat中的ACTIVE/INACTIVE MEMORY
vmstat 命令能够报告关于内核线程、虚拟内存、磁盘、陷阱和 CPU 活动的统计信息,那么我们又该如何理解其工作原理呢?
vmstat -a 命令能看到active memory 和 inactive memory:
$ vmstat -a procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free inact active si so bi bo in cs us sy id wa st 1 0 138096 319560 1372408 1757848 0 0 2 3 2 3 1 0 99 0 0
但它们的含义在manpage中只给了简单的说明,并未详细解释:
inact: the amount of inactive memory. (-a option) active: the amount of active memory. (-a option)
在此我们试图准确理解它的含义。通过阅读vmstat的源代码(vmstat.c和proc/sysinfo.c)得知,vmstat命令是直接从/proc/meminfo中获取的数据:
$ grep -i act /proc/meminfo Active: 1767928 kB Inactive: 1373760 kB
而/proc/meminfo的数据是在以下内核函数中生成的:
fs/proc/meminfo.c: ================== 0023 static int meminfo_proc_show(struct seq_file *m, void *v) 0024 { ... 0032 unsigned long pages[NR_LRU_LISTS]; ... 0051 for (lru = LRU_BASE; lru < NR_LRU_LISTS; lru++) 0052 pages[lru] = global_page_state(NR_LRU_BASE + lru); ... 0095 "Active: %8lu kB\n" 0096 "Inactive: %8lu kB\n" 0097 "Active(anon): %8lu kB\n" 0098 "Inactive(anon): %8lu kB\n" 0099 "Active(file): %8lu kB\n" 0100 "Inactive(file): %8lu kB\n" ... 0148 K(pages[LRU_ACTIVE_ANON] + pages[LRU_ACTIVE_FILE]), 0149 K(pages[LRU_INACTIVE_ANON] + pages[LRU_INACTIVE_FILE]), 0150 K(pages[LRU_ACTIVE_ANON]), 0151 K(pages[LRU_INACTIVE_ANON]), 0152 K(pages[LRU_ACTIVE_FILE]), 0153 K(pages[LRU_INACTIVE_FILE]), ...
这段代码的意思是统计所有的LRU list,其中Active Memory等于ACTIVE_ANON与ACTIVE_FILE之和,Inactive Memory等于INACTIVE_ANON与INACTIVE_FILE之和。
LRU list是Linux kernel的内存页面回收算法(Page Frame Reclaiming Algorithm)所使用的数据结构,LRU是Least Recently Used的缩写词,这个算法的核心思想是:回收的页面应该是最近使用得最少的,为了实现这个目标,最理想的情况是每个页面都有一个年龄项,用于记录最近一次访问页面的时间,可惜x86 CPU硬件并不支持这个特性,x86 CPU只能做到在访问页面时设置一个标志位Access Bit,无法记录时间,所以Linux Kernel使用了一个折衷的方法——它采用了LRU list列表,把刚访问过的页面放在列首,越接近列尾的就是越长时间未访问过的页面,这样,虽然不能记录访问时间,但利用页面在LRU list中的相对位置也可以轻松找到年龄最长的页面。Linux kernel设计了两种LRU list: active list 和 inactive list, 刚访问过的页面放进active list,长时间未访问过的页面放进inactive list,这样从inactive list回收页面就变得简单了。内核线程kswapd会周期性地把active list中符合条件的页面移到inactive list中,这项转移工作是由refill_inactive_zone()完成的。
vmstat看到的active/inactive memory就分别是active list和inactive list中的内存大小,如果inactive list很大,表明在必要时可以回收的页面很多;而如果inactive list很小,说明可以回收的页面不多,Active/inactive memory是针对用户进程所占用的内存而言的,内核占用的内存(包括slab)不在其中。
至于在源代码中看到的ACTIVE_ANON和ACTIVE_FILE,分别表示anonymous pages和mapped pages。用户进程的内存页分为两种:与文件关联的内存(比如程序文件、数据文件所对应的内存页)和与文件无关的内存(比如进程的堆栈,用malloc申请的内存),前者称为file pages或mapped pages,后者称为anonymous pages,File pages在发生换页(page-in或page-out)时,是从它对应的文件读入或写出;anonymous pages在发生换页时,是对交换区进行读/写操作。