Chinaunix首页 | 论坛 | 博客
  • 博客访问: 791698
  • 博文数量: 142
  • 博客积分: 3505
  • 博客等级: 中校
  • 技术积分: 1501
  • 用 户 组: 普通用户
  • 注册时间: 2011-07-30 19:30
文章分类

全部博文(142)

文章存档

2012年(33)

2011年(109)

分类: LINUX

2011-09-06 21:56:08

Mulyadi Santosa

也许你很少面临这一情况,但是一旦如此,你一定知道出什么错了:可用内存不足或者说内存用尽(OOM)。结果非常典型:你不能再分配内存,内核会杀掉一个任务(一般是正在运行那个)。一般半随着大量的交换读写,你可以从屏幕和磁盘动向看出来。

这个问题下面隐含着别的问题:你需要分配多少内存?操作系统给你分配了多少?OOM的基本原因很简单,你申请的内存多于系统可用量。我得说是虚拟内存,因为交换分区也包括在内。

了解OOM

开始了解OOM,首先试试这段会分配大量内存的代码:

#include
#include

#define MEGABYTE 1024*1024

int main(int argc, char *argv[])
{
void *myblock = NULL;
int count = 0;

while (1)
{
myblock = (void *) malloc(MEGABYTE);
if (!myblock) break;
printf(”Currently allocating %d MBn”, ++count);
}

exit(0);
}

编译一下,运行它之后等一会。系统早晚会OOM。然后试试下面这段,分配大量内存并用1写入:

#include
#include

#define MEGABYTE 1024*1024

int main(int argc, char *argv[])
{
void *myblock = NULL;
int count = 0;

while(1)
{
myblock = (void *) malloc(MEGABYTE);
if (!myblock) break;
memset(myblock,1, MEGABYTE);
printf(”Currently allocating %d MBn”,++count);
}
exit(0);

}

发现差别了么?A比B分配了更多内存。而且B被杀掉的更早一些。两个程序都因为没有可用内存而退出。更准确的说,A因为失败的malloc()而优雅的退出了,B是被OOM杀手干掉了。

首先观察分配的内存块数。假设你使用256M内存,888M交换分区(我的情况),B结束时:

Currently allocating 1081 MB

而A结束时:

Currently allocating 3056 MB

A怎么弄来的另外1975M?我骗人?没有!如果你仔细看,你会发现B用1填满得到的内存,而A几乎不拿他们干什么。Linux允许推迟的页分配, 换句话说,只当你真的要用的时候才开始分配动作,比如写入数据时。所以,除非写入数据,否则你可以一直要更多内存。术语称之为乐观的内存分配。

查看/proc//status来确认信息。

$ cat /proc//status
VmPeak: 3141876 kB
VmSize: 3141876 kB
VmLck: 0 kB
VmHWM: 12556 kB
VmRSS: 12556 kB
VmData: 3140564 kB
VmStk: 88 kB
VmExe: 4 kB
VmLib: 1204 kB
VmPTE: 3072 kB

这是在B被杀之前的记录:

$ cat /proc//status
VmPeak: 1072512 kB
VmSize: 1072512 kB
VmLck: 0 kB
VmHWM: 234636 kB
VmRSS: 204692 kB
VmData: 1071200 kB
VmStk: 88 kB
VmExe: 4 kB
VmLib: 1204 kB
VmPTE: 1064 kB

VmRSS需要再详细点解释。RSS是Resident Set Size,也就是当前进程在内存中分配的块。也注意,在B到OOM之前已经用掉了几乎全部交换分区,而A根本没用。很明显malloc()除了保留内存之外什么也没做。

另外一个问题是:既然没有写页,为什么有3056M这个上限?这暴露出另外一个限制。在32位系统上,内存地址有4GB。其中0-3GB是用户使用,3-4GB为内核空间。

注意:有内核补丁可以实现全部分配4GB给用户空间,需要一些上下文切换的开销。

OOM的结论:

  1. VM中没有可用页。
  2. 没有足够的用户地址空间。
  3. 以上两者。

所以避免这些情况的策略是:

  1. 知道用户空间有多少。
  2. 知道可用页有多少。

当使用malloc()申请内存块时,你实际是要runtime的C库查看是否有预先分配的块可用。这个块尺寸至少应当和用户请求一样大。如果 有,malloc()会指派这个块给用户并标记为使用。否则malloc()必须通过扩展堆栈heap得到更多内存。所有申请的块都放在堆栈里。不要和 stack混淆,stack是用来存储本地变量和函数返回地址的。

Heap到底在哪里?可以看看进程地址映射:

$ cat /proc/self/maps
0039d000-003b2000 r-xp 00000000 16:41 1080084 /lib/ld-2.3.3.so
003b2000-003b3000 r-xp 00014000 16:41 1080084 /lib/ld-2.3.3.so
003b3000-003b4000 rwxp 00015000 16:41 1080084 /lib/ld-2.3.3.so
003b6000-004cb000 r-xp 00000000 16:41 1080085 /lib/tls/libc-2.3.3.so
004cb000-004cd000 r-xp 00115000 16:41 1080085 /lib/tls/libc-2.3.3.so
004cd000-004cf000 rwxp 00117000 16:41 1080085 /lib/tls/libc-2.3.3.so
004cf000-004d1000 rwxp 004cf000 00:00 0
08048000-0804c000 r-xp 00000000 16:41 130592 /bin/cat
0804c000-0804d000 rwxp 00003000 16:41 130592 /bin/cat
0804d000-0806e000 rwxp 0804d000 00:00 0 [heap]
b7d95000-b7f95000 r-xp 00000000 16:41 2239455 /usr/lib/locale/locale-archive
b7f95000-b7f96000 rwxp b7f95000 00:00 0
b7fa9000-b7faa000 r-xp b7fa9000 00:00 0 [vdso]
bfe96000-bfeab000 rw-p bfe96000 00:00 0 [stack]

这是cat实际的映射分布。你的结果可能不一样,取决于内核和调度的C库。最近的内核(2.6.x)都有标记,但是不能完全依赖这些标记。

Heap基本上是没有分配给程序映射和stack的自由空间,所以会缩小可用的地址空间,也就是3GB减去所有映射掉的部分。

How does the map for program A look when it can’t allocate more memory blocks? With a trivial change to pause the program (see loop.c and loop-calloc.c) just before it exits, the final map is:

当A不能分配内存块时看起来什么样子?对程序小小调整一下,暂停下来看看:

0009a000-0039d000 rwxp 0009a000 00:00 0 ---------> (allocated block)
0039d000-003b2000 r-xp 00000000 16:41 1080084 /lib/ld-2.3.3.so
003b2000-003b3000 r-xp 00014000 16:41 1080084 /lib/ld-2.3.3.so
003b3000-003b4000 rwxp 00015000 16:41 1080084 /lib/ld-2.3.3.so
003b6000-004cb000 r-xp 00000000 16:41 1080085 /lib/tls/libc-2.3.3.so
004cb000-004cd000 r-xp 00115000 16:41 1080085 /lib/tls/libc-2.3.3.so
004cd000-004cf000 rwxp 00117000 16:41 1080085 /lib/tls/libc-2.3.3.so
004cf000-004d1000 rwxp 004cf000 00:00 0
005ce000-08048000 rwxp 005ce000 00:00 0 ———> (allocated block)
08048000-08049000 r-xp 00000000 16:06 1267 /test-program/loop
08049000-0804a000 rwxp 00000000 16:06 1267 /test-program/loop
0806d000-b7f62000 rwxp 0806d000 00:00 0 ———> (allocated block)
b7f73000-b7f75000 rwxp b7f73000 00:00 0 ———> (allocated block)
b7f75000-b7f76000 r-xp b7f75000 00:00 0 [vdso]
b7f76000-bf7ee000 rwxp b7f76000 00:00 0 ———> (allocated block)
bf80d000-bf822000 rw-p bf80d000 00:00 0 [stack]
bf822000-bff29000 rwxp bf822000 00:00 0 ———> (allocated block)

六个虚拟内存区域VMA,反映出了内存请求。VMA是一组有相同访问权限的内存页,可以存在于用户空间的任意位置。

你现在会想,为什么是六个,而不是一个大区域?有两个原因。第一,一般很难在内存中找到这么大的“洞”。第二,程序不会一次申请所有的内存。所以glibc分配器可以在可用的页根据需要自由规划。

为什么我说是在可用的页?内存分配是以页的尺寸为单位的。这不是OS的限制,而是内存管理单元MMU的特性。页的尺寸不一定,一般x86平台是 4K。你可以通过getpagesize() 或者 sysconf() (_SC_PAGESIZE参数)获得。libc分配器管理所有页:分成较小的块,指派给进程,释放,等等。比如说,程序使用4097字节,你需要两个 页,尽管实际上分配器给你的在4105-4109字节之间。

使用256M内存,无交换分区的情况下,你有65536个可用页。对吗?不完全是。要知道一些内存区域被内核代码和数据占用,还有一些保留给紧急情况或者高优先的需求。dmesg可以显示这些信息:

$ dmesg | grep -n kernel
36:Memory: 255716k/262080k available (2083k kernel code, 5772k reserved,
637k data, 172k init, 0k highmem)
171:Freeing unused kernel memory: 172k freed

内核代码和数据在初始化时使用的init部分172K,之后会被内核释放。这样实际占用了2083 + 5772 + 637 = 8492字节。。实际的说,2123个页没有了。如果使用更多内核特性和模块,就会消耗更多。

另外一个内核的数据结构是页缓冲。页缓冲储存着读块设备的内容。缓冲的越多,可用的内存越少。不过如果系统内存不够,内核会回收缓冲占用的内存。

从内核和硬件的角度,以下非常重要:

  1. 不能保证分配的内存物理上连续;他们只是虚拟的连续。
    这个假象来自地址转换的方式。在保护模式环境,用户使用虚拟地址,而硬件使用物理地址。页目录和页表起到转换作用。比如说两个开始于0和4096的块实际上可能映射到1024和8192地址。

  2. 这样分配更容易。因为很难找到连续的块。内核将寻找满足需要的块而不是连续的块,也会调整页表使之看起来虚拟连续。
    这也有代价。因为内存块的不连续,有时CPU L1和L2的缓冲会欠满,虚拟连续的内存分散在不同的物理缓冲行,会减慢连续的内存访问。
    内存分配包括两步:第一步扩展内存区域的长度,然后根据需要分配页。这就是按需分页。在VMA扩展过程中,内核只检查请求是否和现有VMA重叠,范围是否在用户空间内。默认情况下,会忽略检查是否能进行实际的分配。
    所以,如果你的应用程序能请求并得到1G内存,而你只有16M加64Mswap也没什么奇怪。这种乐观的方式大家都满意。内核有对应的参数可以调整过度承诺。

  3. 有两种页类型:匿名页和文件页。当你在磁盘上mmap()一个文件就产生了文件页,匿名页来自malloc()。他们和文件无关。当内存紧张时,内 核会把匿名页交换出去并清空文件页。换句话说,匿名页会消耗交换分区。例外是,mmap()的文件有MAP_PRIVATE标签。这时文件的修尬只发生在 内存中。

    这些帮助你理解如何把swap当内存扩展。当然,访问一个页需要它回到内存里。



分配器内幕

实际的工作由glibc内存分配器完成。分配器把块交给程序,从内核的heap中去掉。

分配器就是经理,内核是工人。这样就能明白,最大的效率来自好的分配器而非内核。

glibc uses an allocator named ptmalloc. Wolfram Gloger created it as a modified version of the original malloc library created by Doug Lea. The allocator manages the allocated blocks in terms of “chunks.” Chunks represent the memory block you actually requested, but not its size. There is an extra header added inside this chunk besides the user data.
glibc使用ptmalloc作为分配器。Wolfram Gloger创造了这个修改版以替代Doug Lea的malloc。分配器使用chunk管理所有分配的块。chunk代表你实际申请的内存块,但不是那个尺寸。在块内部还有一个额外的头信息。

The allocator uses two functions to get a chunk of memory from the kernel:
分配器使用两个函数得到对应的内存chunk:

  • brk() 设置进程数据段的结尾。
  • mmap() 创建一个VMA,传递给分配器。

当然,malloc()只当当前池中没有chunk时才使用这些函数。

The decision on whether to use brk() or mmap() requires one simple check. If the request is equal or larger than M_MMAP_THRESHOLD, the allocator uses mmap(). If it is smaller, the allocator calls brk(). By default, M_MMAP_THRESHOLD is 128KB, but you may freely change it by using mallopt().
使用brk()或者mmap()需要一个简单的检查。如果请求大于等于M_MMAP_THRESHOLD,分配器使用mmap()。如果是小于,就使用brk()。默认情况下M_MMAP_THRESHOLD为128K,可以使用mallopt()调整。

在OOM情况下,ptmalloc如何释放内存是很有趣的。使用mmap()分配的块通过unmap()释放之后就完全释放了,使用brk()分配的块是 做释放标记,但是他们仍在分配器控制之下。如果另外一个malloc()请求尺寸小于等于自由chunk。分配器可以把多个连续的自由chunk合并,也 可以把它分割来满足要求。

这也就是说,一个自由chunk可能因为不能用来满足请求而被丢弃。失败的自由chunk合并也会加速OOM的产生。这也是糟糕的内存碎片的标志。

恢复

一旦发生了OOM,怎么办?内核会终止一个进程。为什么?这是唯一终止进一步请求内存的方法。内核不会假设进程有什么机制能自动终止,唯一的选择就是杀掉。

内核怎么知道改杀谁呢?答案在mm/oom_kill.c源码中。这个所谓的OOM杀手用函数badness()衡量现有进程的得分。得分最高的就是受害者。以下是评分标准:

  1. VM尺寸。这不是所有分配页的尺寸,而是进程拥有的所有VMA的总量。尺寸越大得分越高。
  2. 和一有关,子进程的VM尺寸也很重要。这个计数是累积的。
  3. 进程优先级小于0的(nice过的)得分高。
  4. 超级用户的进程被假设更重要,因而得分低。
  5. 进程运行时。时间越长得分越低。
  6. 进程进行直接硬件访问的可以免疫。
  7. swapper和init以及其他内核线程都免疫。

进程得分最高的赢得选举,然后被杀。

这个机制不完美,但是基本有效。标准一和二非常明确的表明VMA的尺寸的重要性,而不是实际页的数量。你可能觉得VMA尺寸也许会导致假警报,但是 其实不会。badness()调用发生在页分配函数中,当只有少数自由页而回收失败时,所以基本上这个值很接近进程拥有的页数。

为什么不数实际的页数呢?因为这样需要更多时间和更多锁,也导致快速判断的开销增大。所以OOM并不完美,也可能杀错。

内核使用SIGTERM信号通知目标进程关闭。

如何降低OOM风险

简单的规则:不要分配超出实际空闲的内存。然而,有很多因素会影响结果,所以策略要更精细一点儿:

通过有序的分配减少碎片

不需要高级的分配器。你可以通过有序的分配和释放减少碎片。使用LIFO策略:最后分配的最先释放。

比如以下代码:

void *a;
void *b;
void *c;
…………
a = malloc(1024);
b = malloc(5678);
c = malloc(4096);


………………….



free(b);
b = malloc(12345);

可以换成:

a = malloc(1024);
c = malloc(4096);
b = malloc(5678);
………………….


free(b);
b = malloc(12345);

这样,a 和c 两个chunk之间就不会有漏洞。你也可以考虑使用realloc()来调整已经产生的malloc()块的尺寸。

两个示例演示了这个影响。程序结束时会报告系统分配的内存字节数(内核和glibc分配器)以及实际使用的数量。例如,在2.6.11.1内核和glibc2.3.3.27上,不用参数fragmented1浪费了319858832 字节(约 305 MB) 而fragmented2 浪费了 2089200 字节 (越 2MB).152倍!

你可以进一步实验传递各种参数的结果。参数是malloc()的请求尺寸。

调整内核的overcommit行为

You can change the behavior of the Linux kernel through the /proc filesystem, as documented inDocumentation/vm/overcommit-accounting in the Linux kernel’s source code. You have three choices when tuning kernel overcommit, expressed as numbers in /proc/sys/vm/overcommit_memory:
你可以根据Documentation/vm/overcommit-accounting通过/proc目录的配置改变linux内核的行为。有三个选择:

  • 0意味着使用默认的模式判断是否overcommit。
  • 1意味着总是overcommit。 你现在应该知道有多危险了。
  • 2防止过度overcommit。可以调整/proc/sys/vm/overcommit_ratio. 最大的承诺值是swap + overcommit_ratio*MEM.

一般默认就够用了,但是模式2有更好的保护。相应的,模式2也需要你小心估计程序的需求。你肯定不想程序因为这个不能执行。当然这样也可以避免出现被杀掉。

分配内存后检查NULL指针,审计内存泄露

这是个简单的规则,但是容易被忽略掉。检查NULL可以知道分配器能够扩展内存区域,虽然不保证能分配需要的页。一般你需要担保或者推后分配,取决于情况。和overcommit配合, malloc()会因为认为不能申请自由页而返回NULL,从而避免了OOM。

内存泄露是不必要的内存消耗。应用程序将不再追踪泄露的内存块但是内核也不会回收,因为内核认为程序还在用。valgrind可以用来追踪这一现象。

总是查询内存分配统计

linux内核提供了/proc/meminfo来找到内存状态信息。top free vmstat的信息皆来于此。

你需要检查的是自由的和可回收的内存。自由不用解释,但什么是可回收的?这是指buffer和页cache。当内存紧张系统可以写回磁盘来回收。

$ cat /proc/meminfo
MemTotal: 255944 kB
MemFree: 3668 kB
Buffers: 13640 kB
Cached: 171788 kB
SwapCached: 0 kB
HighTotal: 0 kB
HighFree: 0 kB
LowTotal: 255944 kB
LowFree: 3668 kB
SwapTotal: 909676 kB
SwapFree: 909676 kB

基于以上输出,自由的虚拟内存为MemFree + Buffers + Cached + SwapFree

I failed to find any formalized C (glibc) function to find out free (including reclaimable) memory space. The closest I found is by using get_avphys_pages() or sysconf() (with the_SC_AVPHYS_PAGES parameter). They only report the amount of free memory, not the free + reclaimable amount.我不能找到一个正式的C函数来找出自由(含可回收)内存的空间。最接近的是get_avphys_pages() 或者 sysconf() (加 _SC_AVPHYS_PAGES 参数)他们只报告自由内存总量而不是自由加可回收。

这意味着为了精确的信息,你需要自己解析/proc/meminfo并计算。如果你懒,可以参考procps源代码。它包含ps top free工具。

关于其他内存分配器的实验

不同的分配器使用不同方法管理内存chunk。Hoard是一个例子。Emery Berger from the University of Massachusetts用它来进行高性能的内存分配。用于多线程程序,引入了每CPU heap的概念。

使用64位平台

需要使用更大用户地址空间的人可以考虑64位计算。内核不再使用3:1方式分割VM,因而对大于4G内存的机器也很合适

这个和扩展地址无关,比如INTEL的PAE,允许32位的处理器定址64G内存。这个是物理地址定址,跟用户无关。在虚拟地址部分用户仍然使用3GB。多余的内存可以访问,但是不是都可以映射到地址空间。不能映射的部分就不可用。

考虑在结构中使用打包的类型

Packed attributes can help to squeeze the size of structs, enums, and unions. This is a way to save more bytes, especially for array of structs. Here is a declaration example:打包的属性可以压缩struct enum 和 union的尺寸。这样对struct尤其可以节省

struct test
{
char a;
long b;
} __attribute__ ((packed));

这个招数在于它使各行不对齐,因而消耗了更多的CPU周期。对齐意味着变量的地址是数据类型的原本地址的整数倍。基于数据的访问频率,这样会更慢,但是考虑到排序和缓冲的相关性。

在用户进程使用ulimit() 

使用ulimit -v可以限制用户能mmap()的内存地址空间。到上限后,mmap(),以及malloc()会返回0因而OOM不会启动。对于多用户系统很有用,因为避免了乱杀无辜。

阅读(5560) | 评论(6) | 转发(8) |
给主人留下些什么吧!~~

无敌秋风2011-10-21 11:19:59

唉,不懂C呀。说的倒是在理

救世主¥2011-09-29 15:09:50

救世主¥2011-09-29 15:09:48

shenren5202011-09-22 15:31:14

太长了 头大

swfczm2011-09-07 18:08:22

nice.