内核对象的基本概念
Windows系统是非开源的,它提供给我们的接口是用户模式的,即User-Mode API。当我们调用某个API时,需要从用户模式切换到内核模式的I/O System Services API。例如我们调用Kernel32.dll中的CreateFile创建文件,最终将执行ntdll.dll中的系统服务NtCreateFile。
内核为我们创建的文件对象以内核级数据结构FILE_OBJECT存储管理,内核级文件信息数据结构包括FILE_BASIC_INFORMATION、FILE_STANDARD_INFORMATION等,但是系统提供给我们在用户模式下与这个文件对象交互的接口是一个文件句柄(HANDLE)。
内核对象和普通的数据结构间的最大区别在于其内部数据结构是隐藏的,我们无法(系统没有操作接口)直接读或改变对象内部的数据结构。这里的文件句柄实际是文件内核对象在内核分配的内存中的一个索引,我们执行后期的用户模式下的文件操作调用都需传入文件句柄参数,内核根据该句柄来查找(定位)我们要操作的对象。类似的Windows提供的创建线程的API—CreateThread创建的内核对象也是以句柄(HANDLE)的形式返回给用户,后期对该线程内核对象的操作都需提供该句柄参数。
因为内核对象的所有者是内核,而不是进程,所以何时撤销内核对象由内核决定,而内核做这个决定的依据就是该内核对象是否仍然被使用。那么如何判断内核对象是否被使用呢?何时释放回收该内核对象资源呢?这就引出了内核对象的使用计数(Usage Count)问题。
内核对象是进程内的资源,使用计数属性指明进程对特定内核对象的引用次数,当系统发现引用次数是 0 时,它就会自动关闭资源。事实上这种机制是很简单的,一个进程在第一次创建内核对象的时候,系统为进程分配内核对象资源,并将该内核对象的使用计数属性初始化为1;以后每次打开这个内核对象,系统就会将使用计数加 1,如果关闭它,系统将使用计数减1,减到 0 就说明进程对这个内核对象的所有引用都已关闭,系统应该释放此内核对象资源。
我们编写程序都是在用户模式下进行的,要做内核级调试或者观察内部数据结构,需要下载系统符号(Symbols)。因为我们无法真正去操作其内部数据结构,又无内核源码,所以使用Windbg等工具一窥Windows系统内幕。结合具体内核对象的属性,通过观察其内部数据结构,感知其内核机制及操作流程。
进程和线程的基本概念
进程(Process)是具有一定独立功能的程序关于某个数据集合上的一次运行过程,是系统进行资源分配和调度的独立单位。进程是由进程控制块(PCB)、程序段、数据段三部分组成。其中进程控制块是存放进程管理和控制信息的数据结构,是进程存在的唯一标志。
“进程是一个正在运行的程序,它拥有自己的虚拟地址空间,拥有自己的代码、数据和其他系统资源,如进程创建的文件、管道、同步对象等。一个进程也包含了一个或者多个运行在此进程内的线程。”
进程是执行程序的实例。例如,当你运行记事本程序notepad.exe时,你就创建了一个用来容纳组成notepad.exe的代码及其所需调用动态链接库的进程。每个进程均运行在其专用且受保护的地址空间内。因此,如果你同时运行记事本的两个拷贝,该程序正在使用的数据在各自实例中是彼此独立的。在记事本的一个拷贝中将无法看到该程序的第二个实例打开的数据。
在以上语境中,存储在磁盘上的notepad.exe程序是一连串静态的指令,而进程则是一个容器,它包含了一系列运行在这个程序实例上下文中的线程使用的资源。进程是不活泼的。一个进程要完成任何事情,必须有一个运行在它的地址空间上的线程。此线程负责执行该进程地址空间的代码。每个进程至少拥有一个在它的地址空间中运行的线程。对一个不包含任何线程的进程来说,它是没有理由继续存在下去的,系统会自动地销毁此进程和它的地址空间。
线程是进程内执行代码的独立实体。线程(Thread)是进程内的一个独立执行单元,是CPU调度和分派的基本单位。一个线程就是运行在一个进程上下文中的一个逻辑流,它描述了进程内代码的执行路径。每个线程都有自己的线程上下文(Context),包括唯一的整数线程id,栈(Stack),栈指针(Stack Pointer),程序计数器(Program Counter),通用目的寄存器。所有运行在一个进程中的线程共享该进程的整个虚拟地址空间。
线程内核对象(Thread Kernel Object)和线程上下文(Thread Context)等概念,参考《线程的数据结构》。在WinDbg中,可通过lkd> dt nt!_kthread查看线程内核对象数据结构;通过lkd> dt nt!_teb查看线程TEB数据结构;通过lkd> dt nt!_context查看线程上下文数据结构。
抛开线程实体,进程中的程序代码是不可能执行的。操作系统创建进程后,会创建一个线程执行进程中的代码。通常我们把这个线程称为该进程的主线程,主线程在运行过程中可能会创建其他线程。一般将主线程创建的线程称为该进程的辅助线程。
从从属关系的角度来讲,线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间。一个进程可以包含若干线程,线程可以帮助应用程序同时做几件事(比如一个线程向磁盘写入文件,另一个则接收用户的按键操作并及时做出反应,互相不干扰),在程序被运行后中,系统首先要做的就是为该程序进程建立一个默认线程,然后程序可以根据需要自行添加或删除相关的线程。
线程不像进程按照严格的父子关系来组织。和一个进程相关的线程组成一个对等线程池,一个线程可以杀死其任意对等线程。每个线程都能读写相同的共享数据。当进程退出时该进程所产生的线程都会被强制退出并清除。
Win32多线程架构
主线程在运行过程中可以创建新的辅助线程,即所谓的多线程。多线程较之多进程的优点在于,线程的上下文要比进程的上下文小的多,所以线程的上下文切换要比进程的上下文切换快得多。
进程的主线程的进入点为main函数,辅助线程的进入点为线程函数(Thread Procedure)。
进程中同时可以有多个线程在执行,为了使它们能够“同时”运行,操作系统为每个线程轮流分配 CPU时间片。为了充分地利用 CPU,提高软件产品的性能,一般情况下,Win32基于窗口的GUI应用程序使用主线程接受用户的输入,显示运行结果,而创建新的线程(称为辅助线程)来处理长时间的操作,比如读写文件、访问网络等。这样,即便是在程序忙于繁重的工作时也可以由专门的线程响应用户命令。
换句话说,程序的主线程是一个老板,而其它线程是老板的职员。老板将繁重的工作丢给职员处理,而他自己保持和外界的联系。因为那些线程仅仅是职员,所以它们不会举行记者招待会,它们会认真地完成分内职务,将结果报告给老板,并等待他们的下一个任务。而对外的一切事务谈判都交由老板等管理层交涉。
一个程序中的线程是同一程序的不同部分,因此他们共享程序的资源,如内存和打开的文件。因为线程共享程序的内存,所以他们还共享静态变量。然而,每个线程都有他们自己的堆栈,因此动态变量(自动变量)对每个线程是唯一的。每个线程还有各自的处理器状态(和数学协处理器状态),这个状态在进行线程切换期间被储存和恢复,也即所谓的上下文切换。
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。理论上,安装了N个CPU的PC,在某一时刻,系统底层所能并发执行的线程个数为N。对于单核PC,多线程微观串行,如果开辟的线程过多,则频繁的线程上下文切换将会耗费较多的CPU时钟周期。因此,多线程并不是多多益善,这便涉及到多线程的池化管理问题。
Win32线程消息队列
与基于MS - DOS的应用程序不同,Windows的应用程序是事件(消息)驱动的。它们不会显式地调用函数(如C运行时库调用)来获取输入,而是等待windows向它们传递输入。 windows系统把应用程序的输入事件传递给各个窗口,每个窗口有一个函数,称为窗口消息处理函数。窗口消息处理函数处理各种用户输入,处理完成后再将控制权交还给系统。窗口消息处理函数一般是在注册一个窗口的时候指定的。
在Windows NT和Windows 98中,没有消息队列线程和无消息队列线程的区别,每个线程在建立时都会有它自己的消息队列,可通过lkd> dt nt!_kthread查看其中的_KTHREAD::_KQUEUE*对象Queue。应用程序调用GetMessage/PeekMessage函数从调用线程消息队列中取出指定窗口(HWND)的消息,调用SendMessage/PostMessage函数向调用线程消息队列中压入指定窗口(HWND)的消息。
书籍参考:
《Windows 2000系统编程》
《Windows核心编程》
《Win32多线程程序设计》
《C++面向对象多线程编程》
专题参考:
《C++多线程》
《VC多线程编程》
《从单线程到多线程》
《Multithreaded Programming with ThreadMentor》
《Chrome源码剖析[1] - Chrome的多线程模型》