【Windows】线程漫谈——线程栈(二)

预定和调拨内存的时机:

《windows核心编程》对预定和调拨内存的时机并没有讲的很清楚,查阅了很多资料,觉得下面这个解释是合理的:

int *p = new int(); 会内部调用malloc分配内存。而malloc如果进行到了VirtualAlloc这步时,操作系统会从未使用的虚拟地址空间中分出相关大小的连续页,然后将这些个地址页加入地址映射表(但没有具体映射到物理内存)。只有当具体读写该页时,cpu访问地址映射表翻译物理地址出错产生中断,操作系统通过中断介入,才会具体映射当前虚拟地址页和物理页的对照关系然后cpu重新执行,如果操作系统发现没有空闲物理内存页可以映射,那么它会将目前占用但暂时没有使用的物理内存页中的内容写入文件,然后将该页重新映射给当前虚拟地址页。

归纳起来,就是说new只是执行到预定操作,只有当CPU访问这块内存的时候发现没有调拨,才去调拨吧!

本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。

预备知识

众所周知,线程在初始化时,系统会为其分配线程栈,用于局部变量、函数调用时的参数等。在开始讨论前,先交代一些背景知识。

栈:一种先入后出的数据结构,push和pop是它典型的操作,对应“入栈”和“出栈”的术语。

系统内存的分配机制:简单的说包括“预订”和“调拨”两个过程。预订并不真正分配物理存储器,只是对进程虚拟地址空间中的内存进行“预分配”,以使得这块内存不至于被当前进程的其他指令分配;调拨就是为预订的内存空间分配物理存储器(windows中物理存储器可能是物理内存,也可能是内存页交换文件)。windows之所以这样做,归根到底是为了让进程以为自己占用了所有的物理存储器。内存以页面为单位,x86平台的页面大小为4KB,每个页面会有一个保护属性,例如:PAGE_READWRITE、PAGE_GUARD等。

 疑惑:什么时候调拨实际物理内存,什么时候调拨页交换文件? 

 我觉得可能是这样的,有时候系统预定一块内存之后不会立即访问它,但可能会为其调拨一部分的存储器(例如下:面讲的线程栈,预定1M只调拨两个页面),这时候调拨的是在页交换文件上的;有时候预定一块内存,仅仅是预定,在CPU访问它的时候,才去调拨存储器,这时候调拨的肯定是物理内存,因为马上CPU就需要访问这块内存了。

以上是个人理解,欢迎指正!

栈内存结构和工作原理

默认情况下,线程栈的大小为1MB,对应256个页面。可以用Microsoft C++链接器的链接选项改变默认栈的大小,也可以通过CreateThread方法的参数改变。(最近看了点汇编的东西,我觉得事实上并非线程一定需要栈,而是C++链接器会自动在PE文件中写入初始化栈的信息,PE加载器能识别并初始化SS、SP。而编译器在处理变量和函数调用时会默认用这个栈)。我们知道x86的进栈指令PUSH总是递减栈顶指针,所以栈顺序总是从高位到低位。初始默认情况下,操作系统会为线程栈预订1MB的空间,并调拨2个页面的空间。下图为线程栈初始化的状态:

【Windows】线程漫谈——线程栈(二)_第1张图片

上图中我们看到,前两个页面是被调拨的,而只有第一个页面被设置成了PAGE_READWRITE,第二个页面被设置成PAGE_GUARD。随着调用越来越多的函数,线程需要越来越多的栈,当访问到第二个页面的时候(PAGE_GUARD),系统会得到通知,接下来系统会修改PAGE_GUARD为PAGE_READWRITE,并为下一个页面调拨存储器。

注意:只有需要访问到这块内存,才调拨存储器,之前只是预留了虚拟地址空间,并没有实际分配内存!

当线程访问到倒数第三个页面的的时候,系统会为倒数第二个页面调拨物理存储器,此时还会抛出EXCEPTION_STACK_OVERFLOW,如果用结构化异常处理掉了,并且线程继续使用栈空间,那么倒数第二个页面会被用尽,此时不得不访问栈底页面,然而栈底页面并没有被调拨,这时发生的访问违规将终止整个进程!这就是栈溢出错误!系统这么做自然是为了保护进程中的其他内存空间。

相比StackOverflow,还有一个StackUnderflow的错误。下面的代码展示了StackUnderflow

1
2
3
4
5
int  WINAPI WinMain( HINSTANCE  hInstance, HINSTANCE , PTSTR  pszCmdLine, int  nCmdShow){
     BYTE  aBytes[100];
     aBytes[10000H] = 0; //默认分配1MB的栈,此时访问了1MB以外的空间
     ...
}

如果此时,aBytes[10000H]处的内存没有被调拨,则会发生访问违规;如果已调拨了物理内存,则其他的内存被破坏。

 

C++运行库的栈检查函数

上面所述的调拨栈空间的策略看似“无懈可击”,可是“暗藏漏洞”。先看下面这段代码:

1
2
3
4
void  SomeFunction(){
     int  nValues[4000];
     nValues[0] = 0; //assign a value
}

在32位系统中,这个函数至少需要4000*4=16000字节,其中index为0的元素在哪里呢?在栈的低地址!如果以默认1MB的栈空间分配的话,nValues[0]将访问尚未调拨的空间。为了解决这个问题,编译器会自动插入栈检查代码。编译器能够计算出函数所需要的栈空间,如果所需要的空间大于一个页面的大小,编译器就会为函数插入检查代码。检查代码的原理很简单:每次试图访问下一个页面中的某个地址,以使系统自动为它分配调拨内存,直到需要的栈空间都满足为止。当然如果预设的栈空间不够的话,还是会先引发溢出异常。

劳动果实,转载请注明出处:http://www.cnblogs.com/P_Chou/archive/2012/06/14/thread-stack.html

你可能感兴趣的:(Wondows)