作者:dog250
windows的内存管理很是严谨,使用内存必须首先分配,当然每个操作系统都是这样,然而windows的严谨在于分配的过程,分为保留和提交两个阶段,其中保留的含义就是在进程的虚拟地址空间保留一块空间,不能用作他用,保留的概念是针对虚拟地址空间的,而提交的含义是将刚才保留的虚拟地址空间的虚拟内存块映射到物理内存,这里windows扩展了物理内存的含义,包括内存条代表的物理内存和磁盘页文件以及任何可以和真正的物理内存进行换入换出操作的后备存储,提交的概念其实就是一个映射,为了将虚拟内存变得可用而做的一个到实际物理存储的一个映射,就是将假的变真了。
windows的保留和提交两阶段方式涉及到几件事情,一个就是页表什么时候确立,我们可以设想一种合理的方式,就是在内存块保留的时候,不操作页表,仅仅将虚拟内存段插入到一个便于查找和插入,删除的数据结构中,而在提交阶段操作页表,当然此时内存不一定已经到了真正的物理内存,很有可能只在页文件中为之分配了一个slot而已,此情形下,页表的相关位就可以用来描述这个slot的位置以及别的信息,只要页表的存在位为0即可,这一点和linux可以一样,事实上,提交内存只是在扩展物理内存含义的前提下才表示映射物理内存,虚拟内存真正被映射到原始物理内存只有在该内存被访问的时候才会发生,这是{jd1}懒惰的。事实上我们可以看到windows的方式不够懒惰,linux没有保留和提交的概念,当一个执行绪调用mmap或者malloc或者brk等等不同层次的函数时,实际上就等于保留了内存区域,而只有在该内存被访问的时候,才会直接映射到物理内存而在这之前,根本不会将虚拟内存和物理的事实有任何联系,真对于假只有在不能再隐蔽事实的的时候才会显露,linux的内存管理是一种{jd1}的懒惰,访问内存其实就可以被看做内存提交。windows之所以采用这一种的方式来管理内存其实是为了用一种更加统一的方式去管理所有的内存,只要内存提交了,那么内存管理器就要跟踪这块内存,不管它在物理存储器还是在磁盘页文件。linux的方式看来更加不规范,linux使用页表来充当双面角色,既可以查找物理存储器又可以查找交换分区内存的位置,并且linux中没有一种机制来统一管理物理存储器和交换分区的空间,靠强大的文件系统功能和高效的内存管理和文件管理数据结构就可以轻易做到内存的高效换入换出,解除了物理存储器和交换分区的耦合,相反,在windows下,统一华丽的外表下扭曲着混乱不堪的繁杂,比如说如果想修改页文件的格式,那么必须涉及内存提交时的逻辑,而在linux中只需要换一个file_operations就可以了,统一华丽的外表xx不是仅仅带来了观感上的舒服,同样也付出了代价,比如平衡进程间内存数量的任务就交给了用户,其实用户只要可以在本进程内存分配和管理内存上保持高度灵活就可以了,进程间的内存平衡这样的任务显然是操作系统应该担负起来的,由于只要提交内存,或者在物理存储器或者在磁盘页文件会占据一定的空间,而这些空间是所有的进程共享的,如果一个进程疯狂的提交了过多的内存,那么别的进程就要忍饥挨饿,这一点上操作系统作为一个协调者实际上帮不上什么忙,顶多将贪婪者灭掉了事,物理内存在各个进程间的分配比例xx取决于进程自己而失去了别的进程的监督以及内核机制的协调,这一点看起来不如linux,在linux中内存管理模块尽量使内存在进程间公平的分配,即使一个进程自己分配了大量的内存,只要它不访问这些内存,这些内存连交换分区都不会占据更别说物理存储器了,当然如果这个贪婪的进程要是访问了这些内存,那结果就和windows一样了,从程序的行为应该很容易辨别出这个进程,不过不管怎样也比windows那种允许占着茅坑不拉屎的策略要好得多,虽然内存已经很便宜,但是对于同样增长的应用来讲内存仍然是稀缺资源,因此xx懒惰式的分配方式应该就是最节省的方式。
作为以上讨论的直接结果,我们来看一下两个系统中的堆栈。在windows中堆栈的分配是静态的,也就是说在PE文件中确定了线程堆栈的大小并且一般不能在运行时动态改变,在对堆栈进行管理的时候,windows使用了一种稍微复杂一点但是考虑的很周到的方法,windows尽力去保护自己的堆栈不会溢出,怎么保护呢?在《windows核心编程》上有详细的描述,大致就是说首先为你的堆栈确定一个大小,然后将这段如此大小的内存块的{dy}个和{zh1}一个页面设置为保留,其余的页面遵循以下原则:假设堆栈向下增长,windows将依次把正在被使用的下一个页面设置为保护提交,当然正在被使用的页面肯定是提交的了,每当保护提交页面被访问时系统会得到通知,注意得到通知而不是出错信息,并没有什么严重的错误,因为保护提交页面可以被访问,它已经提交了,只不过由于具有保护属性,所有要告知系统这一件事,系统得知后可以将保护提交属性设置给后一个页面,依次类推,堆栈有着严格的顺序访问特性,就是说首先是高地址被访问,在略低的地址不被使用之前更低的地址不会被使用,当然除非你使用汇编语言xx脱离堆栈的概念,这样的话,线程的堆栈空间页面将按照从高到底的顺序一个个被提交,而紧接着被提交的页面将被设置为保护提交,直到{zh1},到达堆栈的末尾的时候,windows会检测到,此时不再将{zh1}一个页面设置为保护提交,而是引发一个栈溢出异常。windows的这种机制的结果就是有效地保护了堆栈后面的数据不被堆栈数据覆盖,但是这种机制并不是每次都奏效的,比如一个足以使栈溢出的大数组分配在栈上,数组的起始其实已经出了堆栈,如果我直接存取这{dy}个元素的话,并且恰好该元素覆盖的内存已经被提交,那就完蛋了,如果你觉得上述实例会被编译器发现的话,那么考虑下面的例子:
char s[1];
s -= 100000;
*s = 100;
看看linux是怎么做的,很简单,十分懒惰,linux没有为堆栈分配静态的大小,而是利用缺页中断使得堆栈在运行期动态增长,当然没有了固定的大小也就不存在溢出的问题了,只要虚拟内存足够,动态增长的需求就有可能被满足,那么linux有没有什么办法来保护非堆栈数据被堆栈数据损坏或者反过来的情况呢?说实话,没有,主要是因为一来实现那个机制很复杂,维护引入的额外数据结构肯定会影响效率,二来这是用户空间的事情,程序员如果不合格直接开掉他就是了,内核不用为他擦屁股,实际上内核如果真的用雕虫小技帮他擦了屁股,没有会说内核很高明的,因此开源的linux没有这种复杂而且单单对内核没有什么用的机制,实际上如果程序员不合格,那么他写的程序是防不胜防的,机器能和人PK吗?很显然不能,再好的操作系统面对一般烂的程序员也是无力去爱谁啊!
{zh1}讨论一下“如何分配内存以及在哪里分配到底要不要让用户看到”这个有点哲学味道的问题,这个问题关键要看分配的内存做什么用以及这种作用和系统机制的联系的紧密程度,比如说我需要一块内存保存一些我程序里面的结构,比如大型数据库缓冲,比如一个字符串,这种情况下分配越透明越好,因为程序没有必要和实现机制交流,这样程序可以更加集中精力解决所谓的业务问题,但是如果一块内存被一个管理机制需要,那么就有必要导出给用户更多的信息,因为这种需求往往都是关注实现本身的需求,而不是接口需求,比如线程栈的位置,因为线程是操作系统的一种机制,目的是优化程序执行,它其实和业务逻辑没有什么太大的关系,线程更多的被程序流程的管理机制使用而不是被业务流程使用。在这一点点上,linux要比windows好得多,看看clone系统调用的参数,用户必须为线程分配栈空间,而这在windows中却是被默默执行的,实际上windows尽力去向用户隐藏底层的很多重要的信息,然而类似线程栈的位置这样的信息很多用户空间的管理机制还是要用到的,因此{zh0}将这一切都交给用户,系统不要管的太多。