15.1 操作系统的进程与线程管理
本书的读者应该都是Windows的“专家级”用户,一定很清楚Windows可以同时运行多个程序,比如在使用浏览器上网冲浪的同时,使用多线程下载工具从互联网上下载文件,或者同时打开一个MP3播放器播放音乐……
从操作系统的角度来看,这些正在运行的程序都是“进程(Process)”。
15.1.1 进程与程序
程序并行执行的功能由操作系统提供,操作系统使用“进程”将正在执行的不同应用程序相互隔离开来,以免“城门失火”(一个程序的崩溃),“殃及池鱼”(其他程序也被影响)。
严格地说,进程(Process)是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。
这个定义太“理论化”了,用一句通俗的话取代它。
所谓“进程”,可以简单地理解成“一个正在运行的程序”。
程序与进程的区别可以用图形象地表达出来(见图15-1)。
如图15-1所示,程序源代码编译后会生成一个可执行文件,其中包含可以运行的机器指令(或虚拟机支持的指令)。此文件通常保存于计算机所配备的磁盘上。
当程序运行时,操作系统从磁盘上装入此文件,在内存中分配一块区域用于保存程序中的指令和数据,这块区域就是这一程序所使用的内存空间,被称为进程的“地址空间(Address Space)”。在32位的Windows操作系统中,进程的地址空间有2GB大小,这就是说:一个进程可以使用2GB大小的“虚拟”内存,注意,这是“虚拟”的,并不代表安装于某台计算机上的真实的物理内存数量。在后面的小节中还将对进程的内存分配方式作更详细的介绍。
将可执行文件装入内存之后,程序才能运行。
图15-1 进程与程序
15.1.2 操作系统的进程管理
每个正在运行的程序都对应着一个独立的进程,当这些程序装入内存开始执行时,操作系统会为每个进程创建好相关的数据结构(其实可将其看成是一张大表,保存了操作系统用于管理进程的各种相关控制信息)。
由于操作系统可以同时装入多个程序,为此必须有一种方法来保证这些同时运行的程序彼此不会相互影响,不会由于一个程序出现异常而直接影响其他程序甚至是操作系统的正常运行。
位于操作系统核心的“进程管理”模块负责管理并行执行的多个程序。
操作系统的用户模式与核心模式
为了避免应用程序有意无意地修改操作系统核心数据,Windows设计了两种代码运行环境:用户模式(User Mode,也可被翻译为“用户态”)和核心模式(Kernel Mode,也可被翻译为“核心态”)。
普通的应用程序运行于用户模式中,而操作系统的关键代码(比如负责分配与回收内存、创建和销毁进程等功能的代码)运行于核心模式下。
运行于核心模式下的代码可以访问所有的系统内存和执行所有的CPU指令,用户模式下运行的代码则只拥有“有限的权限”,不能“为所欲为”,比如不能访问某些内存单元。
当用户模式下的应用程序需要访问系统核心数据时,它必须发出一个“系统调用”提出访问核心数据的申请,操作系统内核在接到此申请之后,由运行于核心模式下的代码去访问这些数据,然后将结果再“转发”给处于用户模式下的代码。
在Windows中,“系统调用”主要指Win32 API中的特定函数,所以,Windows应用程序通过调用Win32 API函数来实现从“用户模式”到“核心模式”的转换。
句柄与系统核心对象
位于操作系统内核中,仅允许运行于“核心模式”下的代码访问的数据被称为“核心对象(Kernel Object)”。核心对象所包容的数据通常都是操作系统正常运行所必需的,比如用于管理进程的进程表。
提示
这里所谈到的操作系统“核心对象”是借用面向对象理论中的“对象”概念,因为两者都可以看成是对数据的一种封装。但要注意,面向对象理论中的“对象”拥有一些操作系统“核心对象”所不具备的特征,比如支持多态、拥有构造函数和析构函数等。
出于易于理解的目的,可以将系统核心对象简单地类比于C语言中的结构体(struct)变量。
操作系统在运行时,会在系统核心不断地创建和销毁“核心对象”,为了便于跟踪和访问这些对象,操作系统为这些对象分配了标识,这是一个32位的整数,被称为“句柄(Handle)”。许多Win 32 API函数通过句柄来定位所要访问的系统核心对象。
提示
句柄实际上是一个整数值,是相对于进程的。它实际上代表的是进程句柄表中的索引,在进程句柄表的对应行中,保存的是核心对象真正的地址。后文马上就会介绍进程相关的数据结构。
举个例子,请看以下Win32 API函数WaitForSingleObject的声明:
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
可以看到此函数的第1个参数就是一个句柄。当运行于“用户模式”下的应用程序调用此函数时,它会阻塞等待,直到此句柄所标识的系统核心对象的状态转换为“signaled”状态,为了避免无限期等待可能带来的问题,可以使用此函数的第2个参数指定一个最长的等待时间。
另外,还有不少的Win32 API函数可以创建系统核心对象,这些函数通常会将所创建的系统核心对象的句柄返回给应用程序,之后应用程序就可以通过这个句柄来访问核心对象。例如Win32 API函数CreateThread可用于创建一个线程,函数返回线程对象的句柄给应用程序:
HANDLE WINAPI CreateThread(……);
操作系统采用引用计数法来决定销毁核心对象的时机。
当一个线程调用某个Win32 API函数创建一个核心对象之后,此核心对象的初始引用计数为1,应用程序代码中对其句柄的每次引用都会导致引用计数加1。作为一个编程原则,线程用完核心对象之后须关闭句柄(可以通过调用Win32 API中的CloseHandle函数关闭句柄),这时,核心对象的引用计数值减1,当其值减为0时,操作系统销毁这一核心对象,并回收其占用的各种资源。
当线程调用CloseHandle函数关闭核心对象时,线程所属进程的句柄表中相应的内容被清空,从而应用程序将无法再访问此核心对象。但要注意,这时核心对象是否被销毁则取决于其引用计数。因为完全有可能另一个进程也在使用同一个核心对象,因此其引用计数不会为0。
如果使用Visual C++开发Windows应用程序,则上述工作需要软件工程师的深度参与,因为创建一个“普通对象”和创建一个“核心对象”的代码是有很大差别的。
幸运的是,在.NET托管环境中,.NET应用程序对“普通对象”和“核心对象”不加区分,使用new关键字就可以创建任何一种类型的对象,而对象的销毁工作由CLR负责。
交叉链接
Windows定义了许多种不同用途的核心对象,在第17章《线程同步与并发访问共享资源》中介绍的许多用于线程同步的对象都是对操作系统核心对象的一个封装,比如互斥信号量对象(Mutex)其实就是对Windows核心对象Mutex的一个托管封装。通过这种封装,.NET程序员就可以使用C#的new关键字在应用程序中直接创建一个Mutex对象,无须写代码去调用Win32 API中的相关函数。
与进程相关的数据结构
在Windows中,操作系统为每个进程创建的“大表”称为“EPROCESS”,这是一个复杂的数据结构,包含有相当多的数据项(见图15-2,其中仅绘出了与应用软件开发工程师关系密切的数据项)。
图15-2 EPROCESS中的数据项
如图15-2所示,进程的管理信息主要由EPROCESS结构表达,EPROCESS有一个数据项保存了一个指针,引用位于用户地址空间中的“进程环境块(Process Environment)”。
进程环境块中也有很多数据项,其中与应用软件工程师直接相关的就是“线程局部存储区(Thread Local Storage,TLS)”和“进程堆(Process Heap)”。
线程局部存储区用于保存线程的“私有”数据,此数据仅供线程自己访问,谢绝其他人的“拜访”。
而“进程堆”读者应该很熟悉了,当使用new关键字创建一个引用类型的对象时,此对象的数据就保存在与此进程相关联的“堆”中。
现在回过头来再看看位于系统地址空间中的EPROCESS结构。其中最引人注目的是第一项“核心进程块(Kernel Process Block)”,有时又将它称为“进程控制块(Process Control Block,PCB)”,Windows使用KPROCESS结构来表示它,这也是一个很复杂的数据结构,其中保存了进程所创建的所有线程清单、进程当前状态、所占用的处理器时间等信息,操作系统根据这些信息进行线程调度,即分配CPU给特定的线程运行。
EPROCESS结构中另一个很重要的信息就是“句柄表(Handle Table)”。句柄表中的每一项都引用着一个操作系统核心对象。进程每创建一个核心对象,就会在此句柄表中保存此核心对象的句柄,而进程关闭一个句柄时,此句柄表中的对应行被清空。
当进程销毁时,它的句柄表中引用的所有核心对象的引用计数都会减1。
扩充阅读
非托管应用程序的“资源泄露”问题
在非托管应用程序中,如果程序员创建了一个核心对象,却忘记了及时关闭其句柄,那么在进程的整个生命周期中,此核心对象将始终存在,并占用宝贵的系统资源。类似地,如果程序员在进程堆中创建了一个普通对象,忘记及时销毁它,这两种现象都被称为“资源泄露”。
在非托管应用程序中解决资源泄露的基本策略是“成对编程”,比如调用Win32 API函数创建了一个核心对象,用完之后马上就调用CloseHandle函数关闭它。在C++程序中使用new关键字创建一个对象,用完之后应马上使用delete关键字销毁它。
需要指出的是,“资源泄露”只发生于进程还“活着”的时刻,如果进程被中止了,操作系统会将它所引用的所有核心对象引用计数减1,并回收整个进程堆,“资源泄露”也就不可能再发生了。
那如何判断进程是“活着”还是“死了”?
很简单:只要进程所创建的所有线程还有一个“活着”,进程就“活着”,它引发“资源泄露”的可能性就始终存在。
进程的创建与运行
在Windows中,使用Win32 API中的CreateProcess函数创建一个进程,应用程序发出对CreateProcess函数的调用指令之后,操作系统其实是启动了一个复杂的处理流程,简单地说,首先是打开可执行程序文件读取相关信息,然后创建与进程相关的系统核心对象,初始化进程核心对象的各个属性,紧接着为其创建第一个线程(称为“主线程(Main Thread)”),然后主线程执行应用程序的入口函数,至此整个进程创建完成并且投入了运行。
可以通过Windows任务管理器来查看正在运行的进程信息。从“查看”菜单中选择“选择列…”命令,可以显示进程的其他信息,这些信息主要来自每个进程所对应的EPROCESS结构(见图15-3)。
图15-3 操作系统正在运行的进程
进程资源分配
操作系统除了要为进程创建多个系统核心对象,还要为进程分配它可以使用的内存空间。这一内存区域其内存单元地址的总和被称为“进程的地址空间”。这一地址空间依据其用途,又可以分为不同的子区域,请看图15-4。
图15-4 Windows进程的地址空间
图15-4所示为Windows Server 2003(32位系统)中默认的进程地址空间分配方式。从地址“00000000”到“7FFF FFFF”一共2GB的空间为用户进程空间;从“80000000”到“FFFF FFFF”为操作系统占用的系统空间,也有2GB,一般情况下应用程序不能直接访问这部分空间。
因此,每个32位的Windows应用程序都可以访问多达2GB的用户进程空间。
计算机所拥有的内存分为两种类型,一种是“物理内存”,在“实体上”体现为内存条(目前常见的内存条规格有512MB、1GB、2GB等),计算机主板中插上的多个内存条容量之和,就是此计算机所拥有的“物理内存”。
另一部分是“虚拟内存(Virtual Memory)”,它是进程所能访问的所有内存单元的地址的集合,这其中包括了物理内存。虚拟内存很大,前面说过,在Windows NT平台上,应用程序可以使用高达2GB的内存单元。
需要注意的是,计算机中真正意义上的内存是指“物理内存”,其容量是很有限的,比如某台PC机只安装有一条512MB的内存,那怎么说一个应用程序可以使用多达2GB的内存单元?多出来的内存从何而来?
答案是这部分内存可以从硬盘中得来。
Windows操作系统在硬盘上专门划分出一块区域,这部分区域被操作系统模拟成与物理内存一样的“存储区”,称为“虚拟内存分页文件”,应用程序可以用同样的代码访问“真正的”物理内存和用硬盘空间“虚拟”出来的内存空间,程序员只需指定一个内存单元地址,操作系统会根据这个地址自动判断这些数据是在物理内存中还是在虚拟内存分页文件中,如果此地址对应的数据已在物理内存中,则可以直接访问,否则操作系统会将要访问的数据从虚拟内存分页文件读入物理内存。在这个数据的“换入”过程中,有可能还同时发生一个“换出”过程,即操作系统将物理内存中一些暂时不用的数据保存到硬盘上的虚拟内存分页文件中,以挪出空间装入进程所需要的数据。
这里面的关键就是:
只有保存于物理内存中的数据才能被CPU处理。
提示
在Windows操作系统中,硬盘根目录下通常有一个隐藏的系统文件pagefile.sys,它就是Windows的虚拟内存分页文件,其容量高达数百兆字节甚至是上吉(G)字节。
为了提高效率,操作系统将所有内存进行分页,每页包容若干个内存单元,内存数据的换入和换出以页为基本单位。
当进程需要访问的页不在物理内存时,操作系统引发一个“缺页中断”,有一个专门的操作系统线程负责将新页调入物理内存,同时将部分旧的页面换出到虚拟内存分页文件中。
为了实现虚拟地址与物理地址的对应,操作系统设置了一个进程页表(EPROCESS块中有一个数据项指向此页表),当进程按虚拟地址访问内存时,操作系统查此页表即可确定要访问的页是否在物理内存中(可参看图15-4)。
操作系统这种对内存的管理方式称为“虚拟页式存储管理”。
15.1.3 进程中的线程
除了内存,CPU也是一个非常重要的资源,操作系统是一个“冷血的资本家”,它要“尽量榨取CPU的每一点时间”,不能让CPU光耗电而不干活。
各种进程要完成的任务是不同的,有些任务需要占用CPU进行大量的计算,比如正在加密或解密一大批的数据,或者正在进行某个复杂的数学运算;而有些任务则很少需要CPU的参与,比如将一个文件从磁盘的某个位置复制到另一个位置;还有些任务必须等待某些条件满足之后才开始执行(典型实例是Windows的自动系统更新机制),因此操作系统必须能区分开这些情况:
当一个进程正在等待某个条件满足时,让其他的进程使用CPU。
从前面介绍的内容可知,操作系统会为进程分配大量的资源。因此如果操作系统直接以进程作为调度CPU运行的基本单位,那么当进程投入运行和退出运行时,必然要花费大量的计算资源在进程运行环境的切换上。这会严重地影响操作系统的性能。为了避免出现这种情况,操作系统将进程切分为多个“线程(Thread)”,虽然线程也需要有一个运行环境,但这个运行环境往往只涉及到当前一些寄存器的值,数据量小,切换速度快得多。
按照线程而不是进程来分配CPU,这是Windows操作系统所采用的标准方法。
一个进程可以划分为多个线程,也可以只有一个线程。
线程是CPU调度的基本单位,它拥有一个线程标识(Thread ID),一组CPU寄存器,两个堆栈(Thread Stack)和一个专有的线程局部存储区(Thread Local Storage,TLS)。
属于同一个进程的线程共享进程所拥有的资源。
只有一个线程的进程称为“单线程”进程,拥有多个线程的进程称为“多线程”进程。对应地,将运行时只有一个线程的程序称为“单线程”程序,运行时拥有多个线程的程序称为“多线程”程序。
关于线程与进程,有以下结论:
进程是系统分配各种资源(比如内存)的单位,而线程则是操作系统分配CPU(即处理机调度)的基本单位。
举个例子,假设目前操作系统中只有两个进程在运行,进程A创建了10个线程,而另一个进程B只有2个线程,则操作系统会按照12个线程来分配CPU时间,这样一来,A进程有机会获得CPU的机率就大于B进程。
线程的最大特点是它们是可以并行执行的。就是说,在某个时间段内,可以同时有多个线程在运行。
拥有两个以上CPU的计算机的线程的确是并行执行的,但在只有一个CPU的计算机中,由于CPU一次只能执行一个线程的代码,所以操作系统使用一种“时间片轮转”的方法来制造一个线程“同时”运行的“假象”(见图15-5)。
如图15-5所示,操作系统将时间分成非常短的小时间片,并轮流为每个线程分配一个时间片。当前执行的线程在其时间片结束时被挂起,操作系统选择另一个线程运行。
图15-5 操作系统使用“时间片”来给线程分配CPU
为保证被中断的线程能在以后重新投入运行,并且能在原先工作的基础上“继续”,在剥夺一个线程的CPU使用权时,必须将它的运行环境保存起来。
线程的运行环境被称为“线程上下文(Thread Context)”,包括为使线程在线程的所属进程地址空间中继续执行所需的所有信息,例如线程的CPU寄存器组、线程堆栈和线程局部存储区。
当从一个线程切换到另一个线程时,操作系统将保存被换出线程的线程上下文,并重新加载投入运行线程的线程上下文。
时间片的长度取决于操作系统和处理器。由于每个时间片都很小,因此即使只有一个处理器,多个线程看起来似乎也是在同时执行。此即单处理器操作系统多线程管理的“宏观上并行,微观上串行”实现机理。
由于各个线程可能处理不同类型的工作,而这些工作的重要性又不一样,因此,操作系统根据线程处理工作的重要性为其设定了优先级,并为每个优先级立一个线程队列。当一个线程正在运行时,如果有高优先级的线程需要使用CPU,则当前线程在完成当前时间片的运行之后,会被剥夺CPU的使用权,让高优先级的线程投入运行。
15.1.4 CLR如何管理进程与线程
.NET是一个托管的运行环境,在其上运行的进程称为“托管进程(Managed Process)”,而在托管进程中创建的线程自然就是“托管线程(Managed Thread)”了。
对应地,操作系统直接创建和管理的进程和线程被称为“本地进程(Native Process)”和“本地线程(Native Thread)”。前一小节介绍的实际上都是本地进程和本地线程。
.NET Framework引入了面向对象的思想,用类来表示进程和线程。
Process类用于代表托管进程,它实际封装的是操作系统的本地进程,每一个托管进程对象都对应着一个操作系统真实的本地进程。
Thread类用于表示托管线程,每个Thread对象都代表着一个托管线程,而每个托管线程都对应着一个函数(称为“线程函数”),托管线程的执行过程就是线程函数代码的执行过程,线程函数代码执行完,线程也就执行完了。
提示
Thread类与操作系统真实的本地线程不是一一对应关系,它所代表的是一个“逻辑线程”,由CLR负责创建与管理。.NET Framework中另有一个ProcessThread类用于表示操作系统中真实的本地线程。
在操作系统中,线程直接运行于进程内部,而在.NET托管环境下,托管线程与托管进程之间还有一个中间层次,叫做“应用程序域(Application Domain)”。
读者可通过一个实例来直观地了解一下进程与线程,请看示例项目ProcessInfo(见图15-6)。
图15-6 “进程信息查看器”示例
如图15-6所示,示例程序在左边列表框中列出了所有的正在运行的进程,从中选择一个进程,会在右边的文本框中显示出进程的相关信息。
下面以本节示例程序ProcessInfo为“标本”,逐个介绍它运行时所对应的ProcessInfo进程基本信息,可以让读者对操作系统如何管理进程有一个直观的了解。
进程的标识
进程的唯一标识符(Id):2344
关联进程的本机句柄(Handle):604
打开的句柄数(HandleCount):145
关联进程的基本优先级(BasePriority):8
每一个进程都有一个唯一的标识符,它是一个int类型的整数,由Process类的Id属性给出,当此进程消亡后,此标识符可以被其他进程所使用,但在任何时候,都只有一个进程拥有此标识符。
Handle是本进程句柄,此句柄实际上引用的是本进程所对应的操作系统核心对象。
进程的运行信息
进程启动的时间(StartTime):2009/8/8 16:33:40
进程正在其上运行的计算机名称(MachineName):.
进程的主窗口标题(MainWindowTitle):进程信息查看器
进程主窗口的窗口句柄(MainWindowHandle):983476
进程的用户界面当前是否响应(Responding):True
进程的终端服务会话标识符(SessionId):1
进程终止时是否应激发Exited事件(EnableRaisingEvents):False
拥有图形界面的程序都有一个主窗体,此主窗体也对应着一个句柄,当主窗体关闭时,进程也随之结束。主窗体一般由主线程负责创建。
提示
在Windows Forms应用程序的Main方法体内,向Application.Run方法中传入的窗体对象就是主窗体。
进程的内存分配情况
进程的虚拟内存大小(VirtualMemorySize):154288128
峰值虚拟内存大小(PeakVirtualMemorySize):158666752
VirtualMemorySize信息表明当前进程所使用的虚拟内存大小。操作系统将每个进程的虚拟地址空间映射到物理内存中加载的页面,或者映射到磁盘上虚拟内存分页文件中存储的页面。
PeakVirtualMemorySize信息表明进程启动以来该进程使用的最大虚拟内存的大小。
进程允许的最大工作集大小(MaxWorkingSet):1413120
进程允许的最小工作集大小(MinWorkingSet):204800
进程的峰值工作集大小(PeakWorkingSet):13635584
物理内存使用情况(WorkingSet):13635584
“进程的工作集”是物理内存中当前对该进程可直接访问的内存页的集合。这些内存页常驻内存,可供应用程序使用,而不会触发缺页中断。
MaxWorkingSet和MinWorkingSet两项信息表明了进程所能存取的最大和最小内存页数量。每次创建进程资源时,系统都会保留等于该进程最小工作集大小的内存量。虚拟内存管理器会尝试在进程处于活动状态时至少保留最小的常驻内存量,但决不会保留超过MaxWorkingSet值的物理内存量。
PeakWorkingSet项信息则表明自启动进程以来该进程使用的最大工作集内存大小(这部分内存现在有一部分可能在硬盘上)。
WorkingSet项信息表明进程占用的物理内存数量。
在本示例中,可以看到ProcessInfo进程占用了13635584字节的物理内存空间(前面介绍的VirtualMemorySize属性表明进程使用的虚拟内存为154288128字节,两个数字之间有差额,表明此进程的一部分内存页被放到了虚拟内存分页文件中)。
分页的系统内存大小(PagedSystemMemorySize):217612
分配给此进程的未分页的系统内存大小(NonpagedSystemMemorySize):7680
分页的内存大小(PagedMemorySize):13672448
专用内存大小(PrivateMemorySize):13672448
操作系统的内存从用途上又分为两类:
一部分是供操作系统自身使用的,称为“系统内存(System Memory)”,分为分页池内存和非分页池内存两部分。非分页池的内存是连续的一块内存,不分页,“被锁定”在物理内存中。
另一部分是供应用程序使用的。
上述信息表明,ProcessInfo当前在系统内存分页池中占用了217612字节,在非分页池中占用了7680字节。
同时,ProcessInfo进程还使用了13672448字节的分页内存,而且这些内存还都是供此进程“独自享用”的(因为PagedMemorySize=PrivateMemorySize),不与其他进程共享。
进程的执行时间
可安排此进程中的线程在其上运行的处理器(ProcessorAffinity):3
进程的特权处理器时间(PrivilegedProcessorTime):00:00:00.1404009
进程的总的处理器时间(TotalProcessorTime):00:00:00.3120020
进程的用户处理器时间(UserProcessorTime):00:00:00.1716011
进程最终必须在CPU上执行,而一个进程的指令代码可分为两类:一类是由程序员编写的完成某种工作的代码,操作系统在“用户模式”下运行这部分代码;另一类则称为“特权指令”,由操作系统提供。程序员在代码中通过“系统调用”来请求操作系统执行这些指令,这些指令往往需要访问操作系统的核心对象,并使操作系统进入“核心模式”。
CPU执行第一部分“用户模式”代码所花费的时间就是UserProcessorTime这一项提供的信息,而CPU执行第二部分即操作系统特定功能所花费的时间就是PrivilegedProcessorTime这一项提供的信息。两者相加,即为CPU执行进程所花费的时间。
注意“ProcessorAffinity”这个属性,中文译为“处理器亲和性”,其意思是指此进程可以在哪个CPU上运行。
现在的个人电脑通常都是双核的,默认情况下进程平等地看待这两个CPU,它所包容的线程可以运行于任何一个CPU上。然而,在某些情况下,可能希望进程只运行于特定的CPU之上,图15-7所示的计算机中拥有8个CPU,分置于两个主板之上,可以看到每4个CPU共享同一物理内存。
图15-7 一个多处理器的计算机系统
很明显,如果能限制某一进程只能在某块主板上的4个CPU上运行,而不是将其分散到位于两个主板上的8个CPU上运行,这就避免了在不同主板物理内存间传送数据的开销,有可能获得更佳的性能。
那么,如何设置进程只能运行于特定的CPU之上?
其实很简单,直接给ProcessorAffinity赋值就行了。将ProcessorAffinity值转换为二进制,从右到左,每个CPU都对应一个位,当希望进程运行于此CPU时,将相应的位置1。请看表15-1。
表15-1 ProcessorAffinity值
查看表15-1,再对照ProcessInfo示例程序的输出,就很清楚了:ProcessInfo示例程序中的所有线程可以自由地在本机所拥有的两个CPU上运行。
进程中装载的模块信息
进程中要执行的代码可来自于多个软件组件,如图15-8所示,ProcessInfo进程共加载了一个EXE文件(即ProcessInfo.exe文件自身)和多个DLL(比如ntdll.dll和MSCOREE.DLL)。这些文件被称为“模块(Module)”。
图15-8 模块信息
可以在示例程序中选择一个模块,下部的文本框中即显示出相关的模块信息,比如MSCOREE.DLL模块的信息如下:
模块的完整路径(FileName):D:\Windows\SYSTEM32\MSCOREE.DLL
加载模块所需内存量(ModuleMemorySize):294912
加载模块的内存起始地址(BaseAddress):1893138432
系统加载和运行模块时运行的函数的内存地址(EntryPointAddress):1893150444
进程中启动的线程信息
示例程序还可以查看特定进程启动的所有线程信息(见图15-9)。
图15-9 进程启动的线程
以正在运行的ProcessInfo进程为例,它启动了5个线程,其中线程3016的信息如下:
线程的基本优先级(BasePriority):8
线程的当前优先级(CurrentPriority):10
是否让操作系统自动调整线程优先级(PriorityBoostEnabled):True
线程的优先级别(PriorityLevel):Normal
在操作系统内核中运行代码所用的时间(PrivilegedProcessorTime):00:00:00
操作系统调用的、启动此线程的函数的内存地址(StartAddress):2006614552
操作系统启动该线程的时间(StartTime):2009/8/8 18:55:45
此线程的当前状态(ThreadState):Wait
此线程使用处理器的时间总量(TotalProcessorTime):00:00:00
关联的线程在应用程序内(不是在操作系统内核)运行代码所用的时间(UserProcessorTime):00:00:00
线程等待的原因(WaitReason):UserRequest
有关线程的内容,将在后续的小节中详细介绍,此处不再赘述。
ProcessInfo示例技术要点
在示例项目ProcessInfo中,进程和线程信息的获取主要是通过访问Process、ProcessModule和ProcessThread三个类的属性实现的,代码很简单,请读者自行阅读项目源码了解相关的技术细节。
扩充阅读
神秘的Idle进程
当使用ProcessInfo工具查看正在运行的进程时,会发现有一个名为Idle的进程,ProcessInfo无法列出其详细信息。这说明这一进程是一个特殊的进程。
我们知道,进程是正在执行的程序,因此一个进程应该对应着相应的可执行文件,但事实上并不存在Idle这一进程,也找不到和Idle进程相关联的可执行文件。
事实上,Idel进程是一些进程信息查看工具(比如“Windows任务管理器”,或者是.NET基类库中的Process组件)仅仅是出于方便管理角度而“创建”的,它是一个“虚进程”,而且它在不同的进程信息查看工具中显示的名字可能还会不一样。这个“虚进程”可以看成是操作系统空闲线程的容器。所谓“空闲线程(Idle Thread)”,就是指CPU“什么也不干”的情况,由于CPU“什么也不干”,所以空闲线程也是“虚”的,它只是代表了未被真实进程所占用的CPU使用率。
有意思的是:Windows 7的“Windows任务管理器”中不再显示这个进程,但早期版本的“Windows任务管理器”中始终可以看到这一进程的踪迹。