进程的用户空间
Windows系统中,系统空间与用户空间的分界线为0x80000000,该地址以上为系统空间,以下为用户空间,各占2GB(默认情况下)。2GB的用户空间不是全部可访问的,它被划分成了以下几个部分:
用户空间的两端各有64KB的隔离区,是不允许访问的。上端的隔离区将用户空间与系统空间隔离开,下端的隔离区就是我们编程时遇到的NULL,因此NULL实际所表示的区域不是一个地址,而是一个64KB的地址范围。这样,实际可访问的用户空间地址范围为0x10000-0x7ffeffff。
进程的内存布局
程序加载到内存后的布局如下图所示:
(注:图片来自潘爱民编著的《Windows内核原理与实现》)
程序中不同的部分在上图中占据不同的空间。主要为以下几部分:
- 代码段。为程序编译后的机器码指令,然后映射到进程地址空间。
- 已初始化数据段。在程序中已经显式初始化的全局变量和静态变量。这些变量的值在编译时保存于可执行文件中,程序加载到进程中时读取这些变量的值,映射到相应的内存空间。
- 未初始化数据段。程序中声明而没有初始化的全局变量和静态变量。为了缩小可执行文件的大小,这些变量的值在编译时是不会保存到可执行文件中,可执行文件中只记录了所需要的大小,加载程序时才分配空间,映射到相应的内存空间。
- 堆。运行时动态为变量分配的内存,如new和malloc,堆是由低地址向高地址增长的。
- 栈。运行时函数调用占用的内存,每个当前调用的函数占据一个栈帧,其中保存了局部变量,参数,返回地址等。栈是由高地址向低地址增长的。
《Linux/UNIX编程手册》第6章中用实例进行了讲解,这里就不重复列出来了。该章的末尾有一个练习,说示例中声明了一个10MB的数组,为什么编译后的可执行文件大小远小于10MB?如下:
1 | static char mbuf[10240000]; |
这就是前面所说的未初始化的静态变量,在可执行文件中不会保存实际的数据,因而只占用很少的空间。
程序可自由分配的空间
不知道大家有没有产生这样的疑问,结合前面所述的,用户空间除去隔离区域,其它都是编程时可以自由支配的吗?首先明确一点,可自由支配的空间是一定的,因此,不要认为new一定会成功,如果不断地分配而不释放,它总会失败的。而且,大家编程时也遇到过无限递归的错误,无限递归同样造成栈帧的分配导致空间耗尽,所以同样会失败。所以,我们到底可以支配多少空间呢?
回答这个问题之前,先看上面的图,想想编程时几乎必不可少的一个东西——动态链接库(DLL),我们知道,一般情况下,进程只能访问自己的地址空间,而程序要访问动态链接库(包括Windows提供的和我们自己定义的)中的函数和变量,就必须把DLL映射到自己的地址空间,因而,这些动态链接库会消耗一部分空间。此处讲一点题外话,不同的进程使用同一个DLL都会映射到自己的地址空间,但是同一个DLL在物理页面上只有一份,这是通过共享映射区(Section)实现的,使用了同一个DLL的进程的虚拟页面对应同一组物理页面。
如果程序使用到大量DLL,则会占用比较多的空间。同时,进程还有一些自己的数据需要保存,如用户空间的进程结构,这些空间会从堆上分配。因此,程序实际可以自由支配的空间还要压缩。
下面我们实际对一个进程的虚拟地址空间中的空闲页面进行统计,看看有多少可以分配的空间。Windows提供了函数VirtualQuery可以查询当前进程空间中虚拟页面的信息,具体的用法就不说了。下面是代码:
1 |
|
运行结果:
你可以分配全局变量来查看结果的变化。说明一点,上面我写的程序只统计了MEM_FREE的页面,实际上还要判断页面的访问类型,除去特殊如无法访问的页面。