Windows核心编程第五版学习笔记(二) - 平常人做平常事
Windows 核心编程
第三章 内核对象
1、每个内核对象都只是内核分配的一个内存块,并且只能由内核访问,这个内存块是一个数据结构,应用程序不能在内存中定位这些数据结构并直接更改其内容。调用一个会创建内核对象的函数后,函数会返回一个句柄(32位机句柄为32位值,64位机句柄为64位),为了增强操作系统的可靠性,这些句柄值是与进程相关的,所以将句柄值传给一个进程中的线程(通过某种进程间通信方式),那么另一个进程用你的进程句柄值来发出的调用就可能失败;甚至更糟,它们会在你的进程句柄表的同一索引位置处创建到一个完全不同的内核对象的引用。后面会讲到“跨进程边界共享内核对象”利用它们实现多个进程成功共享同一个内核对象。
2、内核对象的拥有者是内核,而不是进程,如果你的进程调用一个函数创建了一个内核对象然后进程退出了,内核对象并不一定会销毁,总之内核对象的生命期可能长于创建它的那个进程。
3、内核可以用一个安全描述符来保护。用于创建内核对象的函数几乎都有指向一个SECURITY_ATTRIBUTES结构的指针作为参数。大多数程序只是为这个参数传入NULL,这样创建的内核对象就具有默认的安全性,具体包括那些安全性取决于当前进程的安全令牌(security token)。如果想通过函数访问现有的内核对象(不是新建一个)必须指定打算对此对象执行哪些操作,在函数返回一个有效的句柄值之前会进行安全性检查,通过检查才会得到一个有效的句柄值,否则返回NULL,可以通过GetLastError得到错误码5(ERROR_ACCESS_DENIED)。
4、为以前版本的Windows设计的一些应用程序之所以在WindowsVista上不能正常工作就是因为实现这些程序时,没有充分考虑安全性。忽略了正确的安全访问标志是很多开发人员最大的失误之一,只要使用了正确的安全访问标志,你的程序在不同版本的Windows之间移植时,绝对会变得更容易。不过应该注意新版本windows的新的限制,例如在Vista中的“用户帐户控制UAC”特性。
5、除了内核对象,用户还可能使用其他类型的对象,如菜单、窗口、光标、画刷和字体等,这些属于User对象或GDI对象,而不是内核对象,判断一个对象是不是内核对象,最简单的方式是查看创建这个对象的函数,几乎所有创建内核对象的函数都有一个允许你指定安全属性信息的参数。
6、进程初始化的时候,系统将为它分配一个句柄表,这个句柄表仅供内核对象使用,不适用于User或GDI对象。作者假设句柄表是一个由数据结构组成的数组,每个结构包含一个指向内核对象的指针、一个访问掩码和一些标志。
索引 |
指向内核对象内存块的指针 |
访问掩码(包含标志位的一个DWORD) |
标志 |
1 |
0 x ? ? ? ? ? ? ? ? |
0 x ? ? ? ? ? ? ? ? |
0 x ? ? ? ? ? ? ? ? |
2 |
0 x ? ? ? ? ? ? ? ? |
0 x ? ? ? ? ? ? ? ? |
0 x ? ? ? ? ? ? ? |
进程首次初始化的时候,句柄表为空,当进程中的线程调用会创建内核对象的函数的时候,内核会为这个内核对象分配内存,然后内核会在进程的句柄表中找到空白项,并对其进行初始化。用于创建内核对象的任何函数都会返回一个相对于进程的句柄,这个句柄可以由同一个进程中运行的所有线程使用(句柄值实际应该除以4,以忽略windows操作系统内部使用的最后两位)。由于句柄值实际是作为进程句柄表的索引来使用的,所以这些句柄是相对于当前这个进程的,无法供其他进程使用。如果你针对饿使用它,那么实际引用干得是那个进程的句柄表的同意索引位置处的内核对象(只是索引值相同而已,你根本不知道它指向什么对象)。
7、调用函数创建内核对象时,调用失败通常返回0(NULL),这就是为什么第一个有效的句柄值为4的原因,但是有几个函数在调用失败时会返回-1(INVALID_HANDLE_VALUE),所以用于创建内核对象的函数在检查返回值的时候要注意。无论以什么方式创建内核对象都要调用CloseHandle向系统指出你已经结束使用对象,如果你用变量保存过这个句柄,那个变量应该同时被置为空以避免产生类似于野指针的情况。如果忘记CloseHandle可能会发生内存泄露。因为进程终止时,操作系统会确保此进程使用的所有资源都被释放。
8、Windows提供了三种机制来允许进程共享内核对象:使用对象句柄继承;为对象命名;复制对象句柄。
l 只有进程间有父子关系时才能使用对象句柄继承。详情参考3.3.1。注意对象句柄继承只会在生成子进程的时候发生,加入父进程后来又创建了新的内核对象,并同样将它们的句柄设为可继承的句柄,那么正在运行的子进程是不会继承这些新句柄的。子进程不知道自己继承的句柄值有什么,父进程还必须把这些句柄值通过某些方法告诉子进程,例如:当作启动子进程的命令行参数、设置到环境变量中、或者等待子进程初始化完毕(WaitForInputIdle)将一条消息send或者post到子进程的一个线程创建的窗口。父进程创建内核对象得到一个可继承的句柄,然后生成两个子进程,但是父进程只希望其中的一个子进程继承内核对象句柄,可以调用SetHandleInformation函数来改变内核对象句柄的继承标志。
l 许多(但不是全部)内核对象都可以命名。如果要根据对象名称来共享一个对象,你必须为此对象指定一个名称。但是,Microsoft没有提供任何机制保证内核对象指定的名称是唯一的,即使他们的类型不同,例如
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT(“JeffObj”);
HANDLE hSem = CreateSemaphore(NULL, 1, 1, TEXT(“JeffObj”);
DWORD dwErrorCode = GetLastError();
CreateSemaphore肯定会返回NULL,因为已经有一个同名的mutex对象了。
共享对象的过程为:ProcessA调用如下代码HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, TEXT(“JeffMutex”));创建一个Mutex类型的内核对象,进程B(和进程A可能没关系)开始执行时,执行以下代码HANDLE hMutexProcessB =
CreateMutex(NULL, FALSE, TEXT(“JeffMutex”));执行这句话时,系统首先检查是否存在名为”JeffMutex”的内核对象,存在的话继续检查该内核对象的类型,类性匹配的话(都是Mutex),执行一次安全性检查,验证调用者是否拥有对象的完全访问权限,如果答案是肯定的,系统就会在进程B的句柄表中查找一个空白项,并将其初始化指向现有的内核对象,如果任何一项验证不通过的话,函数调用就会失败。用于创建内核对象的函数(CreateMutex等)总是返回具有完全访问权限的句柄,如果想要限制一个句柄的访问权限,可以使用这些函数的EX版本。
注意内核对象通过名称来实现共享时,进程B调用CreateMutex时,它会向函数传递安全属性信息和第二个参数,如果已经存在一个指定名称的对象,这些参数会被忽略(函数不知道是创建了一个新的,还是打开了一个现有的).当然,可以马上调用GetLastError来判断到底是创建了一个新的还是打开了一个已经存在的对象。当然调用Open*函数也可以是想共享。调用Create和调用Open的区别在于,如果对象不存在,create会创建它,Open简单地以失败告终。
Terminal Services(终端服务)不一样,运行TerminalServices的计算机,有多个用于内核对象的命名空间,一个是全局命名空间,所有客户端都能访问的内核对象要放在这个命名空间中,这个空间主要由服务使用,此外每个客户端会话都有一个自己的命名空间。一个会话不能访问另一个会话的对象,即使对象的名称相同。RemoteDesktop和FastUserSwitching特性也是利用TerminalServices会话来实现的。可以借助于ProcessIdToSessionId函数来获知你的进程在那个TerminalServices中。服务的命名内核对象始终在全局命名空间中,默认情况下,TerminalServices中,应用程序自己的命名内核对象在会话的命名空间内,不过可以通过在其名称前加”Global\”前缀强迫一个命名对象进入全局命名空间。HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT(“Global\\MyName”)); 也可以加”Local\”关键字使得一个内核对象进入当前的会话的命名空间。Global、Local、Session是Windows保留关键字,区分大小写。
- 使用DuplicateHandle函数也可以跨越进程边界共享内核对象。
第四章 进程
1、集成开发环境会设置各种连接器开关,使连接器将子系统的正确类型嵌入最终的执行体,对于CUI程序,这个连接器开关是/SUBSYSTEM:CONSOLE;对于GUI程序,这个开关是/SUBSYSTEM:WINDOWS。
2、操作系统不会调用你写的入口函数,它会调用由C/C++运行库实现并在连接时使用-entry:命令行选项设置的一个C/C++运行时启动函数,该函数初始化运行时库,确保你声明的全局和静态C++对象都被正确地构造。
应用程序类型 |
入口函数(入口点) |
嵌入执行体的启动函数 |
处理ANSI字符和字符串的GUI应用程序 |
_tWinMain (WinMain) |
WinMainCRTStartup |
处理Unicode字符和字符串的GUI程序 |
_tWinMain (wWinMain) |
wWinMainCRTStartup |
处理ANSI字符和字符串的CUI应用程序 |
_tmain (Main) |
mainCRTStartup |
处理Unicode字符和字符串的CUI程序 |
_tmain (Wmain) |
WmainCRTStartup |
对于/SUBSYSTEM:WINDOWS开关链接器会找上表中1和2入口函数找不到就弹出链接错误,而/SUBSYSTEM:CONSOLE开关连接器找3和4入口函数找不到也出现链接错误。但是可以认为地去掉连接器开关,这样程序就会自动判断应该将应用程序设为哪一个子系统,链接器会查找程序存在上表中的哪个入口函数从而决定是哪种程序。
3、HMODULE和HINSTANCE完全是一回事,如果某函数需要一个HMODULE参数,完全可以传入一个HINSTANCE,之所以有两种数据类型,是因为在16位的Windows中,两者是两回事。WinMain中的hInstanceExe参数实际是一个基内存地址,在这个位置,系统将执行体文件的映像加载到进程的地址空间中,例如假设系统打开执行体文件并将内容加载到0x00400000,则hInstanceExe就是0x00400000;这个值由链接器决定,历史原因vs使用的默认基地址是0x00400000,当然可以在/BASE:address链接器开关处修改该值。执行时可以通过GetModuleHandle(PCTSTR pszModule)函数获取。GetMoudleHandle只检查主调进程的地址空间,并别调用GetMoudleHandle并传递NULL会返回进程地址空间中的执行体文件的基地址,即使从包含在DLL中的代码调用,返回值仍是执行提文件的基地址而非DLL文件的基地址。
4、应该始终使用GetEnvironmentVariable、ExpandEnvironmentString、SetEnvironmentVariable这些函数来操纵进程的环境块。
5、系统内部跟踪记录着一个进程的当前驱动器和目录,这种信息是以进程为单位来维护的,所以加入进程中的一个线程修改了当前驱动器或目录,该进程中的所有线程都会更改此信息。一个线程可以调用下面两个函数来获取和设置当前驱动器和目录:
DWORD GetCurrentDirectory(DWORD cchCurDir, PTSTR pszCurDir);
BOOL SetCurrentDirectory(PCTSTR pszCurDir);
WinDef.h文件中被定义为260的常量MAX_PATH是目录名称或文件名称的最大字符数。
系统跟踪记录着进程的当前驱动器和目录,但是没有记录每个驱动器的当前目录,利用操作系统提供的支持,可以处理多个驱动器的当前目录,这个支持通过环境字符串来提供。例如:
=C:=C:\Utility\Bin
=D:=D:\Program Files
上述变量指出了进程在c驱动器的当前目录是\Utility\Bin,在D驱动器的当前目录为\Program Files。如果调用一个函数,向其传递一个限定了驱动器的名称,而且指定的驱动器不是当前驱动器,系统会在进程的环境块中查找与指定盘符关联的环境变量,如果找到与指定盘符关联的变量,系统将变量的值作为当前目录使用。如果变量没有找到,系统就假定指定驱动器的当前目录是它的根目录。
例如:假定进程的当前目录为C:\Utility\Bin,而你调用CreateFile来打开D:ReadMe.txt,那么系统就会查找环境变量=D:,由于=D:变量是存在的,所以系统将尝试从D:\Program Files目录打开ReadMe.txt文件,如果=D:变量不存在,系统就会试着从D盘根目录打开ReadMe.txt文件。
可以使用c运行库函数_chdir而不是windows SetCurrentDirectory函数来更改当前目录,_chdir函数在内部调用SetCurrentDirectory,但_chdir还可以调用SetEnvironmentVariable来添加或修改环境变量,从而使不同驱动器的当前目录得以保留。
如果一个顾金成创建一个希望传给子进程的环境块,子进程的环境块就不会自动继承父进程的当前目录,相反,子进程的当前目录默认为每个驱动器的根目录,如果希望子进程继承父进程的当前目录,父进程就必须在生成子进程之前,创建这些盘符环境变量,并把它们添加到环境块中。父进程可以通过调用GetFullPathName来获得它的当前目录。
6、CreateProcess在进程完全初始化好之前就会返回TRUE,这意味着操作系统加载器尚未尝试定位所有必要的DLL,如果一个DLL找不到或者不能正确初始化,进程就会终止,因为CreateProcess返回TRUE,所以父进程不会注意到任何初始化问题。
BOOL CreateProcess(PCTSTR pszApplicationName, PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess, PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles, DWORD fdwCreate, PVOID pvEnvironment, PCTSTR pszCurDir, PSTARTUPINFO psiStartInfo, PPROCESS_INFORMATION ppiProcInfo);
为创建一个进程,系统必须创建一个进程内核对象和线程内核对象(用于进程的主线程),由于这些都是内核对象,所以父进程有机会将安全属性关联到这两个对象上,可以分别使用psaProcess和psaThread参数为进程对象和线程兑现给指定安全性,也可以为这两个参数传递NULL,在这种情况下,系统将为这两个内核对象指定默认的安全描述需,也可以分配并初始化两个SECURITY_ATTRIBUTES结构,以便创建你自己的安全权限,并将他们分配给进程对象和线程对象。为两个参数使用SECURITY_ATTRIBUTES结构的另一个原因是:这两个对象句柄可以由父进程将来生成的任何子进程继承(进程A创建进程B,A就持有B的进程对象句柄和主线程对象句柄,如果psaProcess的SECURITY_ATTRIBUTES结构中的bInheritHandle为TURE,而psaThread的SECURITY_ATTRIBUTES结构中的bInheritHandle为FALSE,当进程A再调用CreateProcess创建进程C的时候,如果为bInheritHandles参数传TRUE的话就是希望子进程可以继承父进程能访问的内核对象,这时进程C就继承了B的进程对象句柄,但是不能继承进程B的主线程对象句柄。如果A调用CreateProcess并为bInheritHandles参数传入FALSE,则C不会继承A当前所有的任何一个句柄)。
Windows在创建新进程的时候使用STARTUPINFO结构的成员,但大多数应用程序都希望生成的应用程序只是使用默认值,最起码要将该结构中的所有成员初始化为0,并将cb成员设为此结构的大小,如果不清零的话会导致新进程有时能创建有时不能创建,这事开发人员经常忘记的一个工作。
创建新的进程,会导致系统创建一个内核对象和一个线程内核兑现,在创建时,系统会为每个对象指定一个初始的使用计数1,然后CreateProcess函数返回前,会使用完全访问权限来打开进程对象和线程对象,并将各自的与进程相关句柄放入PROCESS_INFORMATION结构的hProcess和hThread成员中,当CreateProcess在内部打开这些对象时,每个对象的使用计数为2。这意味着系统想释放进程对象,进程必须终止(使用计数减1),而且父进程必须调用CloseHandle(使用计数再减1为0),类似的,要想释放线程对象,线程必须终止,而且父进程必须关闭到线程的句柄。应用程序运行期间,必须关闭到子进程及其主线程的句柄,以避免资源泄露。当然,系统会在你的进程终止后自动清理这种泄露,但是,编写一个精妙的软件应该在进程不再需要访问一个子进程及其主线程的时候,显示地调用CloseHandle来关闭这些句柄。关闭一个进程或线程的句柄,并不会强迫系统杀死此进程或线程,只是告诉操作系统你对进程或线程的统计数据不再感兴趣了,进程或线程会继续执行,直至自行终止。
进程ID和线程ID会被系统立即重用。例如:创建一个进程后,系统初始化一个进程对象,并将ID值为124分配给它,如果再创建一个新的进程后,系统会将一个不同的ID分配给它,但是,如果第一个进程对象已经释放,系统就可以将124分配给下一个创建的进程对象,请务必牢记这个特点,避免你自己的代码引用不正确的进程或线程对象。
个别情况下你的应用程序可能想确定它的父进程,但是,你首先应该知道的是,只有在一个子进程生成的那一瞬间,才存在一个父子关系,到子进程开始执行代码之前的那一刻,Windows就已经不认为存在任何父子关系了ToolHelp函数允许进程姑娘通过PROCESSENTRY32结构查询其父进程。系统确实会记住每个进程的父进程ID,但是请记住ID会被立即重用,所以等你获得父进程的ID的时候,那个ID标志的可能已经是系统中运行的一个完全不同的进程。所以要想程序与它的“创建者”通信,最好不要使用ID,应该定义一个更持久的通信机制,比如内核对象、窗口句柄等。
7、许多应用程序不能正确清理它自己,都是因为显示调用了ExitProcess和ExitThread的原因,ExitThread的情况下,进程会继续运行,但可能泄露内存或其他资源。
TerminateProcess函数是异步的,它告诉系统你希望进程终止,但到函数返回的时候并不能保证进程已经被“杀死”了,所以,为了确定进程是否已经终止,应该调用WaitForSingleObject或一个类似的函数,并将进程的句柄传给它。
8、vista以前的系统,用户用管理员账户登录时,会创建一个安全令牌,每当有代码视图访问一个受保护的安全资源时,操作系统就会使用这个安全令牌,这个令牌与新建的所有进程关联,第一个是资源管理器,后者随即将令牌拿给它的所有子进程。
在vista中,用户使用Administrator这样的一个账户登录时,除了与这个账户对应的安全令牌外,还会创建一个经过筛选的令牌,后者将只被授予标准用户权限,以后系统代表最终用户启动的所有新进程都会关联这个筛选令牌。
9、对权限提升/筛选的进程进行调试可能比较麻烦,但你可以遵循一条非常简单的黄金法则:希望被调试的进程继承什么权限,就以哪种权限启动visual Studio。
如果需要调试的是一个以标准用户身份运行的已经筛选的进程,就必须以标准用户的身份来启动VisualStudio,每次单击它的默认快捷方式(或通过开始菜单启动)时,都是以标准用户的什么启动它的,否则被调试的进程会从以管理员身份启动的一个Visual Studio实例中继承提升后的权限,这并不是你所期望的。
如果需要调试的是一个以管理员身份运行的进程啊(例如,根据那个进程的manifest文件中描述的,它可能必须以管理员身份运行),那么visualStudio必须同样以管理员身份启动,否则就会显示一条错误消息,指出“请求的操作需要提升权限”,而且被调试的进程根本不会启动。
第五章 Job
1、Windows提供了一个作业内核对象,它允许你将进程组合在一起并创建一个”沙箱”来限制进程能够做什么,最好将作业对象想象成一个进程容器,但是,即使作业中只包含一个进程,也是非常有用的,因为这样可以对进程施加平时不能施加的限制。
2、IsProcessInJob(HANDLE hProcess,HANDLE hJob, PBOOL pbInJob);
默认情况下,在Windows vista中通过Windows资源管理器来启动一个应用程序,进程会自动同一个专用的作业关联,此作业的名称使用”PCA”字符串前缀。Windows Vista提供这个功能的目的是检测兼容性问题。所以,如果你已经像第4章描述的那样为应用程序定义了一个manifest,windows资源管理器就不会将你的进程同”PCA”前缀的作业关联,它会假定你已经解决了任何可能的兼容性问题。但是在需要调试应用程序的时候,如果调试器是从Windows资源管理器启动的,即使有一个mainifest,应用程序也会从调试器继承带有”PCA”前缀的作业,一个简单的解决方案是从命令行而不是Windows资源管理器中启动调试器,这样,就不会与作业关联。
3、如果确定在自己的代码中不再访问作业对象,就必须调用CloseHandle来关闭它的句柄,务必记住,关闭一个作业对象,不会迫使作业中的所有进程都终止运行,作业对象实际只是加了一个删除标记,只有在作业中的所有进程都已终止运行之后,才会自动销毁。注意:关闭作业的句柄会导致所有的进程都不可访问此作业,即使它仍然存在。
4、可以向作业应用以下几种类型的限制:基本限制和扩展限制,防止作业中的进程独占系统资源;基本的UI限制,防止作业中的进程修改用户界面;安全限制,防止作业内的进程访问安全资源(文件、注册表子项).应用限制的函数SetInformationJobObject.
5、作业中的进程可以调用QueryInformationJobObject获得所属作业的相关信息(为作业句柄参数传递NULL值),这是很有用的一个技术,因为它使得进程可以看到自己被施加了哪些限制,不过,如果为作业句柄参数传递NULL值,SetInformationJobObject函数调用会失败–目的是防止进程删除施加于自己身上的限制。
6、作业中的进程如果尚未用我已经分配的CPU时间,作业对象就是nonsignaled的,一旦用完所有已分配的CPU时间,Windows就会强行杀死作业中的所有进程,作业对象的状态会变成signaled。通过调用WaitForSingleObject(或者一个类似的函数),可以轻松捕捉到这个事件。可以调用SetInformationJobObject并授予作业更多的CPU时间,将作业对象重置为原来的nonsignaled状态。
第六章 线程基础
1、注意:CreateThread函数是用于创建线程的Windows函数,不过,如果写的是C/C++代码,就绝对不要调用CreateThread,相反,正确的选择是使用Microsoft C++运行库函数_beginthreadex,如果使用的不是Microsoft C++编译器,你的编译器的提供商应该提供类似的函数来替代CreateThread。不管这个替代函数是什么,都必须使用它。
终止线程运行的推荐方法是让它的线程函数返回,但是如果使用本节的方法,务必注意ExitThread函数是用于“杀死”线程的Windows函数,如果你要写C/C++代码,就绝对不要调用ExitThread。相反,应该使用C++运行库函数_endthreadex。如果使用的不是Microsoft的C++编译器,那么你的编译器提供方应该提供它们自己的ExitThread的替代函数,不管这个替代函数是什么,都必须使用它。
2、TerminateThread函数是异步的,也就是说,它告诉系统你想终止线程,但在函数返回时,并不保证线程已经终止了,如果需要确定线程已经终止运行了,还需要调用WaitForSingleObject(或类似函数),并向其传递线程的句柄。
如果通过返回或者调用ExitThread函数的方式来终止一个线程的运行,该线程的堆栈也会被销毁。但是,如果使用的是TerminateThread,那么除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈,Micorsoft故意以这种方式来实现TerminateThread,否则,加入其它还在运行的线程要引用被“杀死”的那个线程的堆栈上的值,就会引起访问冲突,让被“杀死”的线程的堆栈保留在内存中,其它的线程就可以继续正常运行。此外,DLL通常会在线程终止运行时收到通知,不过,如果线程使用TerminateThread强行“杀死”的,则DLL不会收到这个通知,其结果是不能执行正常的清理工作。
3、GetCurrentProcess()和GetCurrentThread()函数都返回到主调线程的进程和线程内核对象的一个伪句柄,它们不会在主调进程的句柄表中新建句柄,而且,调用这两个函数,不会影响进程或线程内核对象的使用计数,如果调用CloseHandle,将一个伪句柄作为参数传入,CloseHandle只是简单地忽略此调用,并返回FALSE,GetLastErrro返回ERROR_INVALID_HANDLE。调用一个Windows函数时,如果此函数需要到进程或线程的一个句柄,那么可以传递一个伪句柄,这将导致函数在主调进程或线程上执行它的操作。
注意:线程的伪句柄是一个指向当前线程的句柄,换言之,指向的是发出函数调用的那个线程。Duplicate函数可用于把进程的伪句柄转换为真正的进程句柄。
Seven Chapter Thread Scheduling, Priorities, and Affinities
1、Any thread can call this function to suspend another thread,a thread can suspend itself but cannot resume itself, a thread can be suspended as many as MAX_SUSPEND_COUNT times(127 Defined in WinNT.h). SuspendThread is asynchronous with respect to kernel-mode execution, but user-mode execution does not occur until the thread is resumed.
2、VOID Sleep(DWORD dwMilliseconds), The system makes the thread not schedulable for approximately the number of milliseconds specified, but possibly several seconds or minutes more, because windows is not a real-time operating system.
you can pass 0 to Sleep, this tell the system that the calling thread relinquishes the remainder of its time slice and it forces the system to schedule another thread.
3、SwitchToThread function allows a thread that wants a resource to force a lower-priority thread that might currently own the resource to relinquish the resource .if no other thread can run when SwitchToThread is called, the function returns FALSE, otherwise it returns a nonzero value.Calling SwitchToThread is similar to calling Sleep and passing it a timeout of o millisends.The difference is that SwitchToThread allows lower-priority threads to execute.Sleep reschedules the calling thread immediately even if lower-priority threads are being starved.
Note: the yield of execution is limited to the processor of the calling thread, the operating system will not switch execution to another processor, even if that processor is idle or is running a thread of low priority.
4、You should call SuspendThread before calling GetThreadContext; otherwise, the thread might be scheduled and the thread\’s context might be different from what you get back.A thread actually has two contexts:user mode and kernel mode. GetThreadContext can return only hte user-mode context of a thread.if you call SuspendThread to stop a thread but that thread is currently executing in kernal mode, its user-mode context is stable even though SuspendThread hasn\’t actually suspended the thread yet. But the thread cannot execute any more user-mode code until it is resumed,so you can safely consider the thread suspended and GetThreadContex will work.
5、Process Priority Class and Thread Priority Level.
The concept of a process priority class confuses some people.They think that this somehow means that processes are scheduled.Processes are never scheduled;only threads are scheduled.The process priority calss is an abstract concept that Microsoft created to help isolate you from the internal workings of the scheduler;it serves on other purpose.
In general, a thread with a high priority level should not be schedulable most of the time, when the thread has something to do, it quickly gets CPU time.At this point , the thread should execute as few CPU instuctions as possible and go back to sleep,waiting to be schedulable again. In contrast, a thread with a low priority level can remain schedulable and execute a lot of CPU instructions to do its work. If you follow these rules, the entire operating system will be responsive to its users.
6、a thread\’s current priority level never goes below ther thread\’s base priority level.
To improve the responsiveness of the foreground process, Windows tweaks the scheduling algorithm for threads in the foreground process, the system gives foreground process threads a larger time quantum than they would usually receive. this tweak is performed only if the foreground process is of the normal priority class, if it is of any other priority class, on tweaking is performed.
Chapter 8 Thread Synchronization
1、InterLocked function family
InterlockedExchangeAdd、InterlockedExchangeAdd64;
InterlockedExchange、InterlockedExchange64、InterlockedExchangePointer;
InterlockedCompareExchange、InterlockedCompareExchangePointer
InterlockedCompareExchange64;
InterlockedIncrement、InterlockedDecrement;
2、The hardest thing to remember is that any code you write that touches a shared resource must be wrapped inside EnterCriticalSection and LeaveCriticalSection functions. if you forget to wrap your code in just one place ,the shared resource will be subject to corruption.For instance.
when you can\’t solve your synchronization problem with interlocked functions, you should try using critical sections.The great thing about critical sections is that they are easy to use and they use the interlocked functions internally, so they execute quickly. The major disadvantage of critical sections is that you cannot use them to synchronize threads in multiple processes.
3、InitializeCriticalSection(PCRITICAL_SECTION pcs);
DeleteCriticalSection(PCRITICAL_SECTION pcs);
EnterCriticalSection(PCTITICAL_SECTION pcs);
TryEnterCriticalSection(PCRITICAL_SECTION pcs);
LeaveCriticalSection(PCRITICAL_SECTION pcs);
when a thread attempts to enter a critical section owned by another thread, the calling thread is placed immediately into a wait state. this means that the thread must transition from user mode to kernel mode(about 1000 CPU cycles). This transition is very exensive. On a multiprocessor machine, the thread that currently owns ther resource might execute on a different processor and might relinquish control of ther resource shortly. in fact, the thread that owns ther resource might release it before the other thread has completed executing its transition into kernel mode, if this happens, a lot of CPU time is wasted.under this condition, you can use the api – InitializeCriticalSectionAndSpinCount , SetCriticalSectionSpinCount.
4、InitializeSRWLock(PSRWLOCK SRWLock);
AcquireSRWLockShared(PSRWLOCK SRWLock);
ReleaseSRWLockShared(PSRWLOCK SRWLock);
AcquireSRWLockExclusive(PSRWLOCK SRWLock);
ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
5、if you want to get the best performance in an application. you should try to use nonshared data first and then use volatile reads, volatile writes, interlocked APIs, SWRLocks, critical sections.And if all of these won\’t work for your situation, then and only then , use kernel objects.
Chapter 9 :Thread Synchronization with Kernel Objects
1、the interlocked family of functions operates only on single values and never places a thread into a wait state.You can use critical sections to place a thread in a wait state, but you can use them only to synchronize threads contained within a single process, Also, you can easily get into deadlock situations with critical sections because you cannot specify a timeout value while waiting to enter the critical section.
2、Successful Wait Side Effects:A successful call is one in which the function sees that the object was signaled and returns a value relative to WAIT_OBJECT_0.A call is unsuccessful if the function returns WAIT_TIMEOUT or WAIT_FAILED.Objects never have their state altered for unsuccessful calls. for example, let\’s say that a thread is waiting on an autuo reset event object.when the event object becomes signaled, the function detects this and can return WAIT_OBJECT_0 to the calling thread ,however, just before the function returns, the event is set to the nonsignaled state – the side effect of the successful wait.
Process and thread kenel objects have on side effects.
3、When a manual-reset event is signaled , all threads waiting on the event become schedulable.when an auto-reset event is signaled , only one of the threads waiting on the event becomes schedulable.
PuseEvent makes an event signaled and thren immediately nonsignaled;it\’s just like calling Set-Event immediately followed by ResetEvent.if you call PulseEvent on a manual-reset event, any and all threads waiting on the event when it is pulsed are schedulabel.if you call PulseEvent on an auto-reset event. only one waiting thread becomes schedulable.if no threads are waiting on the event when it is pulsed, there is no effect.
4、CreateSemaphore、CreateSemaphoreEx、OpenSemaphore、ReleaseSemaphore.
5、For mutexes, there is one special exception to the normal kernel object signaled/nonsignaled rules. Let\’s say that a thread attempts to wait on a nonsignaled mutex object. In this case, the thread is usually placed in a wait state. However, the system checks to see whether the thread attempting to acquire the mutex has the same thread ID as recorded inside the mutex object. If the thread IDs match, the system allows the thread to remain schedulable—even though the mutex was nonsignaled. We don\’t see this “exceptional” behavior applied to any other kernel object anywhere in the system. Every time a thread successfully waits on a mutex, the object\’s recursion counter is incremented. The only way the recursion counter can have a value greater than 1 is if the thread waits on the same mutex multiple times, taking advantage of this rule exception.
6、
Object |
When Nonsignaled |
When Signaled |
Successful Wait Side Effect |
Process |
While process is still active |
When process terminates (Exit-Process, TerminateProcess) |
None |
Thread |
While thread is still active |
When thread terminates (Exit-Thread, TerminateThread) |
None |
Job |
When job\’s time has not expired |
When job time expires |
None |
File |
When I/O request is pending |
When I/O request completes |
None |
Console input |
No input exists |
When input is available |
None |
File change notifications |
No files have changed |
When file system detects changes |
Resets notification |
Auto-reset event |
ResetEvent, PulseEvent, or successful wait |
When SetEvent/PulseEvent is called |
Resets event |
Manual-reset event |
ResetEvent or PulseEvent |
When SetEvent/PulseEvent is called |
None |
Auto-reset waitable timer |
CancelWaitableTimer or successful wait |
When time comes due (SetWaitableTimer) |
Resets timer |
Manual-reset waitable timer |
CancelWaitableTimer |
When time comes due (SetWaitableTimer) |
None |
Semaphore |
Successful wait |
When count > 0 (ReleaseSemaphore) |
Decrements count by 15 |
Mutex |
Successful wait |
When unowned by a thread (ReleaseMutex) |
Gives ownership to a thread |
Critical section (user-mode) |
Successful wait ((Try)Enter-Critical-Section) |
When unowned by a thread (LeaveCriticalSection) |
Gives ownership to a thread |
SRWLock (user-mode) |
Successful wait (Acquire-SRWLock(Exclusive)) |
When unowned by a thread (ReleaseSRWLock(Exclusive)) |
Gives ownership to a thread |
Condition variable (user-mode) |
Successful wait (SleepConditionVariable*) |
When woken up (Wake(All)ConditionVariable) |
None |