Linux下的内存使用技巧点滴
任何语言编程中,最核心的问题都会包括内存的管理。我认为,从性能上来说,任何系统提供高性能的关键都是缓存的有效利用,在本机编程上,RAM内存则是最有效的CACHE。从代码到程序到进程,编译环境和运行环境在其中对内存的管理起到至关重要的作用。即使如JAVA这种平台无关的语言,它同样通过参数受限于实际环境的限制,它在操作系统平台中的内存布局,同样是按照内核的规矩来的。
当前Linux由于是免费的,所以大部分SERVER类程序都运行在Linux上,弄清楚Linux关于内存的管理是有实用价值的,Linux2.6内核是当前主流的Linux内核,所以下面仅谈Linux2.6内核中的内存管理一些编程技巧,但是对使用跨平台语言或者中间件编程来说,还是有参考意义的。
我们在使用内存时,最关心的是:无错、高效。无错,是程序的健壮性要求,高效,是对程序性能的要求,这两点都需要对内核管理内存有了解才能做到。下面重点说下为了保证无错和高效需要了解的基本原理。
首先举几个例子说下我们常见的内存错误或者低效使用导致的问题。
1、我们在写服务器端程序时,通常喜欢用多线程实现(在Linux上用轻量级进程实现)高效的并发,并且为了提高效率习惯每个线程都会在初始化时就预分配大块内存,这样可以在运行过程中长期复用。在运行过程中每个线程也会有动态堆内存的分配。因为不同的硬件配置和操作系统设置,会导致不同的线程数带来不一样的性能,所以这个线程数通常是可配的。那么,如果我们的预分配内存需要大容量时,同时我们为了提高性能把线程数提高后,可能会带来进程崩溃甚至数据变乱(32位Linux不打开PAE扩展时,支持的最大进程地址空间是3G)。
2、复杂的程序常常有几十次的方法调用,如果不是很注重的在方法中分配了过大的栈内存,超过了系统允许的最大值(Linux默认为8M),同样引发进程崩溃。
3、内存越界,C/C++中常见。一些攻击程序常利用栈地址空间越界,拿到指令寄存器里的地址,将代码执行到任意的线性地址中,比如具备ROOT权限的父进程的代码段。
4、调用mmap系统调用来映射内存时,进程会新分配一个线性区给这段映射。那么这块线性区的大小是有限制的,而且内存向磁盘中刷数据时是重量级系统调用,必须慎重设计。
了解Linux内存,我认为最快的方式是了解主流体系架构下的分页机制以及进程的地址空间。
Linux进程的地址空间,是由许多组线性地址组成的,默认情况下,最多可以拥有65536组线性地址。Linux2.6内核通过一个双向链表和红黑树(2.6内核前没有,普通进程只有几十组线性地址,这时是高效的,但线性区很多时没有红黑树是很低效的)作为全部进程线性区的容器。这些线性区用来存储代码段,数据段,栈,堆,文件内存映射,IPC共享线性区等等。这些线性区,在进程生命周期内可能被增加、删除、修改。我们需要清楚,代码和运行时分配的对象存储在哪些线性区中,以及这些线性区的限制和如何有效的使用它们。
用户态下的进程启动时,会复制父进程的地址空间的指针,如果进程不会修改父进程地址空间中的页,则父进程的页将永远不会复制到当前进程的地址空间中。早期的Linux版本没有这种写时复制的机制,效率低下,目前的Linux内核都支持这种,效率有很大提高。