Windows2000 进程/线程分析(一)

类别:软件工程 点击:0 评论:0 推荐:

Windows2000 进程/线程分析(一)

                           ---读书笔记

一、        内核对象

11 什么是内核对象

系统和应用程序使用内核对象来管理各种各样的资源,比如:进程、线程和文件等。每个内核对象只是内核分配的一个内存块,并且只能有内核访问。该内存块是一种数据结构,它的成员负责维护该对象的各种信息。有些数据成员(如:安全描述符、使用计数等)在所有对象类型中是相同的,但大多数数据成员属于特定的对象类型。例如:进程对象有一个进程ID、一个基本优先级和一个退出代码,而文件对象则拥有一个字节位移、一个共享模式和一个打开模式。

内核对象的数据结构只能被内核访问,因此应用程序无法在内存中找到这些数据结构,并直接改变它们的内容。Microsoft规定了这个限制条件,目的是为了确保内核对象的结构能够保持状态的一致性。这个限制也使Microsoft能够在不破坏任何应用程序的情况下,在这些结构中添加、删除和修改数据成员。

Windows提供了一组函数,用于创建和访问内核对象。(例如:CreateProcess、CreateFileMapping)当调用一个用于创建内核对象的函数时,该函数就返回一个用于标识该对象的句柄。该句柄可以被视为一个不透明值,进程中的任何线程都可以使用这个值。通过将每个内核对象的句柄传递给Windows的各个函数,系统就能知道你想操作那个内核对象了。

为了使OS更加健壮,内核对象的句柄是与进程密切相关的。因此,如果将该句柄传递给另一个进程中的某个线程(使用某种形式的进程间的通信)那么该线程使用此句柄所用的调用将会失败。当然,这并不是绝对的,使用“跨越进程边界共享内核对象”技术,就可使多个进程共享单个内核对象。

    猜测:内核对象不会位于单个进程的4GB虚拟空间中,而是位于处于内核控制下的某块内存区域中,这样的话,就能实现多个进程共享单个内核对象。内核对象的句柄到底是什么,没有确切的文档可以说明,但可以肯定同一个内核对象可以对应多个句柄,而同一个句柄只能对应一个内核对象,这就可以解释为什么通过给内核对象命名的方法,可以使两个毫无关系的进程访问同一个内核对象,但是这两个进程得到的针对同一内核对象的句柄却并不相同,这也是和子进程继承父进程句柄表的方式的区别。

 

1.1.1 内核对象的使用计数

    内核对象由内核拥有,这意味着,如果你的进程调用了一个创建内核对象的函数,然后你的进程就终止了运行。那么该内核对象不一定被撤销。当然,在大多数情况下,内核对象会被撤销。但是如果另一个进程正在使用你的进程创建的内核对象(如:子进程继承了父进程的句柄表,或者使用“跨越进程边界共享内核对象”技术),那么该内核知道,在另一个进程终止使用该对象前不能撤销该对象,必须记住:内核对象的存在时间可以比创建该对象的进程长。

每个内核对象包含一个使用计数,使用计数是所有内核对象类型常用的数据成员之一。当一个对象刚刚创建时,它的使用计数被置为1。然后,当另一个进程访问一个现有的内核对象时,使用计数就递增1。当进程终止运行时,内核就自动确定该进程仍然打开的所有内核对象的使用计数。如果内核对象的使用计数将为0,内核就撤销该对象。这样可以保证,在没有进程引用该内核对象时,系统将不会继续保留它。

 

1.1.2 安全性

内核对象能够得到安全描述符的保护。安全描述符用于描述谁创建了该对象,谁能够访问和使用该对象,谁无权访问该对象。安全描述符通常在编写服务器应用程序时使用。

内核对象的默认安全性为:对象管理组中的任何成员和创建者都拥有对该对象的全部访问权。

除了内核对象之外,应用程序也可以使用其他类型的对象,如:菜单、窗口、鼠标光彪、刷子和字体等。这些对象属于用户对象或GDI(图形设备接口)对象,而不是内核对象。如果创建对象的函数没有使用PSECURITY_ATTRIBUTES参数,那么创建的就不是内核对象。例如:

HICON CreateIcon(

           HINSTANCE  hInst,

           int           nWidth,

           int           nHeight,

           BYTE        cPlanes,

           BYTE        cBitsPixel,

           CONST  BYTE *pbANDbits,

           CONST  BYTE *pbXORbits );

 

12 进程的内核对象句柄表

    当一个进程被初始化时,系统要为他分配一个句柄表。该句柄表只用于内核对象,不用于用户对象或GDI对象。句柄表在Windows 2000、Windows98、Windows CE中的实现方式都不相同。而且没有关于句柄表的详细结构和管理方法的详细资料,因此,下面的阐述不保证所有的细节都正确无误。

1.2.1 创建内核对象

    当进程初次被初始化时,它的句柄表是空的。然后,当进程中的线程调用创建内核对象的函数时,内核就为该对象分配一个内存块,并对它初始化。这时,内核对象对该进程的句柄表进行扫描,找出一个空位置。然后将在该位置上设置相应的内核对象的数据结构的内存地址、访问屏蔽位和标志位。

    用于创建内核对象的所有函数均返回与进程相关的句柄,这些句柄可以被该进程中的所有线程使用。这些句柄值实际上是放入进程的句柄表中的索引,它用于标识内核对象存放的位置。实际上在Windows2000中,函数的返回值是创建内核对象时写入进程句柄表中的字节数。而不是索引本身。关于句柄的含义并没有文档资料,而且其实现方式是随时会变化的。

 

用于创建内核对象的一些函数:

 

 

HANDLE CreateThread(

PSECURITY_ATTRIBUTES       psa,

DWOD                        dwStackSize,

LPTHREAD_START_ROUTINE   pfnStartAddr,

PVOID                        pvParam,

DWORD                      dwCreationFlags,

PDWORD                     pdwThreadId );

 

HANDLE CreateSemaphore(

PSECURITY_ATTRIBUTES    psa,

LONG                      lInitialCount,

LONG                      lMaximumCount,

PCTSTR                    pszName );

 

猜测:首先,可以肯定,在Windows 2000下每创建一个进程就会对应的产生一个进程内核对象,Windows本身维护一张系统全局进程表,该进程表中的每一行对应一个进程内核对象,即一个实际的进程。当一个进程创建了一个子进程时,该子进程对应的内核对象句柄并不会写入其父进程的句柄表中。而是直接写入系统全局进程表中。同样,对于每个线程也会产生一个线程内核对象,关于该内核对象的信息也被保存在系统全局线程表中。而Windows就是依据这两个表来管理和调度系统中的进程和线程的。这里提到的进程内核对象应该就是《操作系统》一书中在进程控制结构一节中提到的进程控制块(Process Control Block)PCB。在该书中提到的进程映像(Process Image),实际上应该就是保存在磁盘或其他介质中的可执行文件。

 

Windows提供以下方法用于在不同进程之间共享数据:

l         动态数据交换(DDE)

l         OLE、COM

l         管道和邮箱

l         内存映射文件

 

 

二、        进程

21 概述

进程通常被定义为一个正在运行的程序的对象的实例,它有两个部分组成:

l         一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。

l         另一个是地址空间,它包含所有可执行模块或DLL的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆(Heaps)分配空间。

进程是不活泼的。要是进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含若干个线程,所有这些线程都“同时”执行进程地址空间中的代码。为此,每个线程都有他自己的一组CPU寄存器(即,线程的上下文,定义在:WinNT.h头文件中)和它自己的堆栈。但是,由于Windows中,是针对进程来分配系统资源的。所以,线程的堆栈实际上位于其所属进程的虚拟地址空间中。每个进程至少拥有一个线程,来执行进程地址空间中的代码。如果没有线程来执行进程的地址空间中的代码,那么进程就没有存在的理由了,系统将自动撤销该进程和它的地址空间。

若要使所有线程都能运行,操作系统就要为每个线程安排一定的CPU时间片。它通过一种循环方式为线程提供时间片(称为:量程),造成一种假象,仿佛所有线程都是同时运行的一样。

当创建一个进程时,系统会自动创建它的第一个线程,成为主线程。然后,该线程可以创建其他的线程或者进程。

 

22 Windows应用程序概述

Windows支持两类应用程序:

l         基于图形用户界面(GUI)的应用程序

l         基于控制台用户界面(CUI)的应用程序

 

Windows应用程序必须有一个在程序启动时调用的进入点函数。可使用的进入点如下图所示:

 

23 Windows应用程序的启动过程

操作系统实际上并不调用你编写的进入点函数。它调用的是C/C++ run-time startup function。该函数负责对C/C++ run-time library进行初始化,这样,就可以调用malloc和free之类的函数。它还能够确保已经声明任何全局对象和静态对象能够在代码执行以前正确的创建。

所有的C/C++ run-time startup function的作用是相同的,其差别在于:是处理ANSI字符串还是Unicode字符串,以及他们在对C run-time library进行初始化后,它们调用那个进入点函数。VC++配有C run-time library的源代码。可以在CRt0.c文件中找到这4个启动函数的代码。

启动函数的功能可概括如下:

l         检索指向新进程的完整命令行的指针

l         检索指向新进程的环境变量的指针

l         对C/C++ run time's global variables进行初始化。如果包含了stdlib.h头文件,你的代码就能访问这些变量。详见下表:

l         对C运行期内存分配函数(mallco和calloc)以及low-level input/output routines使用的heap(堆)进行初始化

l         为所有的全局和静态C++类对象调用构造函数

   当上述这些初始化操作完成后,C/C++ run-time startup function就调用应用程序的进入点函数。

如果编写了一个WinMain函数,它将以如下的形式被调用:

GetStartupInfo( &StartupInfo );

int nMainRetVal = WinMain( GetModuleHandle(NULL), NULL, pszCommandLineAnsi,

(StartupInfo.dwFlags & STARTF_USESHOWWINDOW)

? StartupInfo.wShowWindow : SW_SHOWDEFAULT );

 

当进入点函数返回时,启动函数便调用C运行期的exit寒暑,将返回值(nMainRetVal)传递给它。

Exit函数负责如下操作:

l         调用由_onexit函数的调用而注册的任何函数

l         为所有全局的和静态的C++类对象调用析构函数

l         调用操作系统的ExitProcess函数,将nMainRetVal传递给它。这使得操作系统能够撤销此进程并设置它的exit code(该代码保存在该进程对应的内核对象中)。

 

三、        线程

 

四、        纤程

Microsoft公司给Windows添加纤程(Fibers)是为了能够更容易的将现有的UNIX下的应用移植到Windows中。UNIX下的应用程序属于单进程应用程序,但是它能够为多个客户程序提供服务。UNIX下的开发人员使用自己的线程结构库来仿真纯线程。该线程包可创建多个堆栈,保存某些CPU寄存器,并且可在它们之间进行切换,以便为客户端请求提供服务。

纤程是在用户模式下实现的,内核并不知道纤程的存在,这与线程不同,线程是由内核实现的。并且,纤程采用非抢占式调用方式。

一个线程中可以包含一个或多个纤程。但,线程每次只能执行一个纤程的代码。当使用纤程时,必须使用ConvertThreadToFiber函数来将现有的线程转换成一个纤程。还可使用CreateFiber来创建另一个纤程。

(待续)

本文地址:http://com.8s8s.com/it/it37607.htm