深入浅出Windows驱动开发:栈的忧虑(图)

来源:互联网 发布:卡通农场怎么恢复数据 编辑:程序博客网 时间:2024/05/17 06:22
 普通的Win32线程有两个栈:一个是用户栈,另一个是内核栈;而如果是内核中创建的系统工作线程,则只有内核栈。只要代码在内核中运行,线程就一定是使用其内核栈的。栈的主要作用是维护函数调用帧,以及为局部变量提供空间。

  用户栈可以指定其大小,默认是1MB,通过编译指令/stack可改设其他值。

  普通内核栈的大小是固定的,由系统根据CPU架构而定,x86系统上为12KB,x64系统上为24KB,安腾系统上为32KB。对于GUI线程,普通内核栈空间可能不够,所以系统又定义了“大内核栈”概念,可以在需要的时候增长栈空间。只有GUI线程才能使用大内核栈,这也是系统规定的。

  关于GUI线程,笔者多说几句。Windows的发明,将GDI和USER模块,即“窗口与图形模块”的实现移到了内核中,称为Windows子系统内核服务,并形成一个win32k.sys内核文件。而用户层仅留调用接口,由User32.dll和GDI32.dll两个文件暴露出来。判断一个线程是不是GUI线程的依据,竟非常的简单:线程初建时,都是普通线程,第一次调用Windows子系统内核服务(只要用户程序调用了User32.dll和GDI32.dll中的函数,并导致相关内核服务在内核中被执行),系统即立刻将之转变为GUI线程,并从而切换到“大内核栈”;倘若至线程结束,并未有任何一个子系统内核服务被调用,那么它一直都是普通线程,一直使用普通内核栈。

  正是由于窗口与图形模块的内移,才导致了相关服务必须在内核中执行,从而不得不引入“大内核栈”概念。笔者知道UNIX系列的操作系统,包括Linux、Mac,都是在用户层实现窗口与图形子系统的,这类操作系统甚至可以毫不影响地在多个图形子系统间进行切换。回忆Windows NT 4以前的操作系统,其设计也和UNIX一样,相关模块放在用户层实现。在这种情况下,对于上述操作系统,按照笔者的理解,它们就没必要使用大内核栈的概念了笔者仔细查过Linux和UNIX相关书籍,确实未找到“大内核栈”的说明。

  和C编译器相比,C++编译器更善于为目标代码做较多优化,并因为创建数量不等的临时变量而占用一定的栈空间。对于用户栈和大内核栈,临时变量带来的栈空间支出一般不足以构成问题。但对于普通内核栈,C++编译器并不知道自己正在多么奢侈地挥霍着有限而珍贵的资源,几十K甚至十几K的内存很容易被耗尽,内核栈溢出因此成为一个非常大的威胁。

  下面给读者举一个语言上的例子。对于表达式:

  A = b + c

  如果a、b、c三个变量的类型为:

  int a, b, c;

  虽然不同的编译器间各有不同的实现,但一般来说编译后的结果是这样的:先把b的值存入一个寄存器中(如eax),将寄存器和c相加,再把寄存器值传入变量a。这里面不涉及临时变量。

  但如果a、b、c三个变量的类型为一个类,如ClsSome:

  ClsSome a, b, c;

  则编译后的结果就不像表面上那么简单了,编译器会创建一个ClsSome类型的临时变量tmp,并将b与c相加后的结果存入tmp中,最后用赋值操作将临时对象tmp赋值给a。临时变量tmp是编译器神不知鬼不觉创建的,程序员很难预知这一背后动作。

  对于上面的对象例子,如果有更多的对象参与并实现了更复杂的操作,则编译器创建的临时变量数将更多,可能超乎你的想象。Lippman在其《Inside The C++ Object Model》一书中举了一例,是三个对象之间的四则运算:

  a = b + c - b*c; // 见其书6.3节,原是a[i] = b[i]+c[i] – b[i]*c[i],是一个对象数组

  Lippman举此例后,称这里面将会导致创建5个临时变量,岂不令人惊讶!

  对于因为内核栈空间的瓶颈而引起的忧虑,目前并没有好的解决方法。可能读者会疑惑一个问题,即为什么不能把内核栈也设计得和用户栈一样呢?比如把内核栈默认大小设置为1MB,用户栈这么做并没有带来任何问题啊。

  提出这个问题的读者很会动脑子,但他忽略了一个问题,就是用户空间和内核空间的不同之处。用户空间是进程独立的,以x86系统为例,在正常情况下,每个进程都有独立的2GB用户空间,所以用户栈的1MB并不起眼。

  而内核空间是全局共享的,所有内核栈都在同一个内核空间中申请内存资源。如果内核栈也像用户栈一样,将大小设到1MB,我们来算一笔账吧。系统中的线程成百上千,就算平均500个线程吧,每个线程一个1MB大小的内核栈,一共占了500MB。这还了得吗?岌岌可危。500个线程太保守了,笔者在写作的当下系统中有967个线程(见图6-4),那就用掉将近1半的内核空间了!再倘若用户开启了/3G开关,那么内核空间就只有1GB系统要喊救命了,可了不得!

  在任务管理器中,在选择列对话框中勾上“线程数”,能看到各进程含有的线程数,将所得数相加能得到一个大概的系统线程总数;但更好的办法是查看系统的性能计数,可使用perfmon来查看,如图6-4所示。

  


  图6-4 查看系统线程数

  内核栈的问题,正是内核中使用C++的一个最大障碍。在实际编程时,为了尽量避免发生栈溢出错误,需要经常对栈剩余空间保持一份警惕,尤其在可能形成很深的调用栈(如递归调用)的情况下。内核函数IoGetStackLimits与IoGetRemainingStackSize分别用来获取当前内核栈的边界与剩余空间,可使用这两个函数实时控制栈状况。可在函数入口处包含下列代码。

  // 如果当前内核栈空间小于150字节,就让函数返回

  return; // 如有可能,可指定一个特殊的错误值

 

 本文转自《竹林蹊径:深入浅出Windows驱动开发》一书,张佩,马勇,董鉴源编著。ISBN 978-7-121-12555-3;2011年2月出版;定价:69.00元