【转】《windows核心编程》读书笔记
2020-11-16 19:43
标签:des 使用 os 文件 数据 io 这篇笔记是我在读《Windows核心编程》第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的。开头几章由于我追求简洁,往往是很多单独的字句,后面的内容更为连贯。 海量细节。 1. GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖。 2. GetLastError可能用于描述成功的原因(CreatEvent)。 3. VS监视窗口err,hr。 4. FormatMessage。 5. SetLastError。 1. ANSI版本的API全部是包装Unicode版本来的,在传参和返回是多了一次编码转换。 2. MS的C库的ANSI和Unicode版本之间也是没有互相调用关系的,没有编码转换开销。 3. 宽字符函数:_tcscpy,_tcscat,_tcslen。 4. UNICODE宏是Windows
API使用的,而MS的C库中,对于非标准的东西用_前缀区分,所以_UNICODE宏是MS的C API使用的。 5. MS提供的避免缓冲区溢出攻击的函数在 6. 要想接管CRT的错误处理(比如assert),使用_set_invalid_parameter_handler设置自己的处理函数,然后使用_CrtSetReportMode(_CRT_ASSERT,
0);来禁止CRT弹出对话框。 7. Windows也提供了字符串处理函数,但lstrcat、lstrcpy(针对T字符的)已经过时了,因为没考虑缓冲区溢出攻击。考虑使用StrFormatKBSize、StrFormatByteSize、CompareString(有很多比较选项)、CompareStringOrdinal(相当于_tcscmp)。 8. GetThreadLocale返回线程的语言信息:LCID(Locale
ID),供很多函数使用(包括使用CompareString针对语言来比较的时候)。 9. 宽字节转多字节WideCharToMultiByte,反之MultiByteToWideChar。其中,在宽字节转多字节的时候,如果有Unicode字符在多字节编码中没有对应项,那宽字节会被替换成参数lpDefaultChar,并且lpUsedDefaultChar会被标记为TRUE。当用这两个函数计算结果串的大小时,返回的是字符数。 10. IsTextUnicode。 1. 简单区分内核对象和其他对象的方法:创建需要安全信息的多半是内核对象。 2. 每个进程有一个内核对象表,表的每一项是一个简单结构,包括真实内核对象地址和访问权限等。用户代码持有的内核对象句柄其实是对象表中对应项的索引。因此如果CloseHandle关闭一个对象后没有清空变量,且在对象表的同样位置恰好又创建了一个新的内核对象,对之前没清空的无效变量的访问会造成bug。(比如对同一个句柄多调用了一次CloseHandle导致另一个内核对象被关闭。) 3. 进程退出时,会释放各种内存、内核对象、GDI对象等。 4. 跨进程使用内核对象的理由:跨进程传输:用文件映像对象实现共享内存、邮件槽和命名管道实现数据通信、信号量和互斥量进行同步等。 5. 跨进程使用内核对象的三种方式:对象句柄继承、命名内核对象、复制对象句柄。 6. 对象句柄继承:创建内核对象的时候可以指定SECURITY_ATTRIBUTES. bInheritHandle表示可继承(任何时候可以使用SetHandleInformation修改可继承性等属性),创建子进程时指定CreateProcess的参数bInheritHandles为TRUE,则子进程从父进程的对象表中拷贝所有可继承的对象到自己的对象表的相同表位置中(并增加引用计数),因为表项结构被完全拷贝且内核对象实际地址在地址空间后2G的内核地址段中,所以拷贝过来的表项完全有效,进而父子进程的可继承内核对象的句柄值完全相同,于是只要以任何方式将要继承的对象的句柄值跨进程交给子进程(创建子进程时的命令行参数、环境变量、共享内存、消息等手段),则后者可以使用。 7. 命名内核对象:要访问已经存在的命名内核对象,可以使用CreateXXX或者OpenXXX,后者在对象不存在的时候返回NULL。如果打开了一个已经存在的命名对象,在打开时为API指定的对象名以外的参数被忽略。注意,一个进程打开同一对象两次,除了增加引用两次外,返回的句柄值是不同的,需要分别关闭一次,即打开和关闭完全对称(很合理的行为)。在Vista及以上的系统,对象名可以包括在命名空间下,避免被低授权用户访问。 8. 复制对象句柄:DuplicateHandle。 1. 进程是执行文件的运行时形态。包括两部分:内核数据(对应内核对象)、地址空间(包括执行文件代码和栈堆等动态内存)。 2. 把VC的“系统-子系统”值删除掉,即不指定控制台或GUI,则编译器会根据代码中存在main或者WinMain来自动选择子系统(这里不谈Unicode了),很方便。 3. 启动程序:根据子系统执行mainCRTStartup/WinMainCRTStartup,在该函数中干几件事(1)准备命令行和环境变量(用于char
*argv[]和char
*env[])(2)初始化CRT的全局变量(包括_osver、_winmajor、_winver、__argc、_environ等)(3)初始化CRT运行库的内存分配(malloc、free)、IO函数等(4)初始化全局对象调用C++构造函数。 4. 退出程序:main返回后mainCRTStartup会调用exit,exit干以下几件事:(1)执行通过_onexit注册的函数(2)执行全局对象的C++析构函数(通过atexit注册的)(3)判断_CrtDumpMemoryLeaks设置的内存泄漏检测标志,尝试检测内存泄漏(4)调用ExitProcess。 5. HINSTANCE和HMOUDLE完全相同,都是表示映像文件加载到内存后的基址(链接器中可以配置)。GetModuleHandle传入文件名可以获得模块基址;传入NULL可以得到执行文件的HINSTANCE(即使调用者位于某个模块中同样返回应用程序基址);GetModuleHandleEx可以根据函数地址得到模块基址 6. 访问环境变量:char
*env[]参数、GetEnvironmentStrings、GetEnvironmentVariable、ExpandEnvironmentStrings(将一个使用了类似”%USERPROFILE%”环境变量的字符串中的变量替换成值)。 7. 系统环境变量:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session
Manager\Enviroment。用户环境变量:HKEY_CURRENT_USER\Enviroment。 8. 修改环境变量后可以通知相关的系统窗口(如控制面板等):SendMessage(HWND_BROADCAST,
WM_SETTINGCHANGE, 0, (LPARAM) “Enviroment”)。 9. 可以设置特定线程在一个CPU核心集合上执行。 10. SetErrorMode。设置该进程如何响应各种错误。 11. 关于相对路径:在通过GetEnvironmentStrings返回的环境变量中,有一部分不是真正的环境变量,比如“=C:=C:\Windows”“ =F:=F:\Projects\Test05”,他们表示一种进程相关配置“本进程在特定驱动器下对应的当前文件夹”。一个进程除了有以上配置外,还有一个当前驱动器,最终GetCurrentDirectory返回的当前路径就是当前驱动器+当前驱动器对应的当前文件夹。使用SetCurrentDirectory会改变该驱动器的当前文件夹,还会改变进程的当前驱动器(但这个API的改变并不会在GetEnvironmentStrings上体现出来,使用C函数_chdir可以同时改变两者,故C函数更优)。进程刚启动时,如果不考虑从父进程继承的环境,则只有进程当前驱动有当前文件夹,其他驱动都无配置。使用相对路径访问文件的时候,其绝对路径可以用GetFullPathName得到。”文件名”这样的相对路径的绝对路径是GetCurrentDirectory()
+
“文件名”;”驱动器盘符:文件名”(注意不是”驱动符:/文件名”)这样的相对路径的绝对路径就是”该驱动器的当前文件夹”(如果无配置,则是根目录) +
“文件名”。 看如下代码: _chdir("D:/Downloads"); // 修改D:的当前路径为Downloads,且进程当前驱动器为D: _chdir("F:/Projects"); // 修改F:的当前路径为Projects,且进程当前驱动器为F: std::ofstream("1.txt"); // 当前驱动器是F:,所以绝对路径是F:/Projects/1.txt std::ofstream("d:1.txt"); // D:的当前路径是Downloads,所以绝对路径是D:/Downloads/1.txt 这种行为从cmd的cd命令也可以看得出点端倪。 归纳:相对路径访问文件的时候,首先将相对路径展开成绝对路径,使用GetFullPathName,后者分两步:首先判断是否包含驱动器(以X:开头),如果没有,则在开头添加进程当前驱动器;然后检查是否以”X:/”开头,如果没有,则将”X:”展开成”X:/”
+ “对应驱动的当前文件夹”。两步过后得到绝对路径。 12. GetVersionEx获取系统版本信息。VerifyVersionInfo检测当前系统是否满足版本需要。 13. CreateProcess的参数:关于lpApplicationName和lpCommandLine,有两种用法:(1)前者指定应用程序路径,后者指定参数(第一个参数前面要有一个空格,似乎底层会直接连接两个串)(2)前者为NULL,后者指定路径和参数,空格隔开。常用第二种方法。注意,lpCommandLine中由于是用空格分隔参数的,所以对其中含有空格的路径一定要用内层引号括起来。另外CreateProcessW有一个奇怪的行为,它会修改参数lpCommandLine(似乎只在lpApplicationName为空的时候会修改),所以使用Unicode版本的时候传入的该参数不能是常字符串(如L”Nodepad
1.txt”),而应该另外准备缓冲传给该API供其修改,因为ANSI版本是调用Unicode版本的且在编码转换的时候内置了缓冲,所以CreateProcessA的lpCommandLine参数可以是常串(最终API会修改转换编码的临时缓冲)。默认情况下,CUI的CUI型子进程会和父进程共享控制台,在参数dwCreationFlags中添加DETACHED_PROCESS或CREATE_NEW_CONSOLE标志可以阻止这种行为。在dwCreationFlags中添加CREATE_NEW_PROCESS_GROUP标志,可以控制进程组的组织,用户按下Ctrl+C的时候同一进程组的所有进程得到通知。lpEnvironment指定为NULL的时候,底层为用GetEnvironmentStrings来填充。lpCurrentDirectory为NULL的时候,子进程继承父进程的当前目录。lpStartupInfo不能为空,至少要初始化结构为0并将cb赋为sizeof。使用STARTUPINFOEX结构作为lpStartupInfo参数,还可以具体指定子进程要继承哪些父进程的可继承内核对象(即使bInheritHandles参数为FALSE)。 14. cmd进程输入命令行前显示的路径,就是其当前路径(GetCurrentDirectory)。在CreateProcess时,cmd没有设置子进程当前路径,而资源管理器将路径设置成子进程镜像目录。因为cmd的子进程会继承cmd的当前路径(lpCurrentDirectory为空的结果),因此最好在用cmd启动程序的时候先将cmd的当前路径设置为新进程的镜像路径。 15. 进程和线程结束后,句柄对象被标记为激活, WaitForSingleObject会返回。 16. CreateProcess后,可以使用WaitForInputIdle或类似函数来等待新进程初始化环境完毕开始运行。 17. WoW64:Windows 32 On Windows
64。所有64位windows运行着这个虚拟机,用来执行32位程序。判断一个32位程序是否是运行在64位系统的32位虚拟机中:IsWow64Process。 18. 父进程创建子进程时使用的lpStartupInfo,在子进程中可以使用GetStartupInfo来查询。 19. 创建一个子进程时,进程和主线程本身的存在就有了引用1,而调用CreateProcess的父进程又会有他们的引用所以计数到了2。要完全销毁进程和线程,需要计数为0,所以除了需要进程本身结束外,引用的该进程的其他线程也要释放引用。当然,CreateProcess过后父进程马上CloseHandle并不会结束子进程,只是释放自己的引用,使其计数为1,这是正常的行为。要确保某个进程或线程不被销毁,不调用CloseHandle即可。如果进程本身已经退出了,但还有其他进程引用它,则它的地址空间被回收,只有内核对象还存在(比如这时再对句柄使用API查看内存,则内存信息为空),这也是为什么可以查看已经退出的进程的退出码的原因(退出码保存在内核对象中)。 20. 进程和线程的ID位于同一个系统顶层名空间。即任意进程的任意线程ID绝不可能和任意进程ID相同。这个ID会被系统循环利用。 21. GetProcessIDOfThread。 22. 进程只有在它所有线程都结束后才会结束。ExitProcess会杀死所有线程,所以可以直接结束进程,在主线程中调用ExitThread只会结束主线程(即,主线程创建一个死循环线程后自己_exitthreadex,这个进程不会退出。)。main返回后CRT调用exit后者再调用ExitProcess,所以在main中return可以直接结束进程。 23. 通过ExitProcess或ExitThread(单线程时)结束进程,由于这些API比CRT更底层,他们只能保证正确的释放Windows资源(内存、内核对象引用),并不保证释放C++资源(CRT底层资源、全局对象的析构函数),故一定要从main中返回自然的结束进程(其他原因在后面章节说明)。TerminateProcess也出于相同的原因应该避免使用。 24. CreateProcess创建的子进程会继承父进程的Security
Token权限,而ShellExecuteEx可以提高子进程的权限(令lpVerb参数为”runas”)。资源管理器使用前者创建子进程,所以通过它开打的程序都具有和资源管理器相同的权限。 25. 关于Vista及更高系统的UAC(User Account
Control):Vista以前的系统如果以管理员账号登陆,资源管理器(Explorer)会获得一个管理员权限的Security
Token,然后从资源管理器打开的子进程都会继承这个最高权限,这种行为非常危险。Vista以后,即使以管理员账号登陆,资源管理器仍然只持有一个一般权限的Token(Filtered
Token),子进程如果想提升权限,有两种途径:(1)用户“以管理员身份运行”启动该进程(2)子进程自己提出请求要求用户提升权限(子进程是安装程序、或者子进程配置有.manifest文件说明权限需求)。另外,在很多软件中出现有小盾牌图标的按钮,也是要求提高权限,点击过后会结束当前进程,重启一个高权限进程(如资源管理器中“显示所有用户的进程”按钮)。其实这三种提高权限都是父进程调用了ShellExecuteEx。 26. IsUserAnAdmin判断当前用户是否是管理员。在Vista及以上的系统中,即使是管理员,进程也有可能因为筛选Token而不具备最高权限。 27. 枚举所有进程:Process32First、Process32Next、EnumProcesses。 28. 可以从HMOUDLE中读取IMAGE_DOS_HEADER和IMAGE_NT_HEADERS,进而从这些PE头中取得模块的推荐加载地址等信息。 29. PEB(Process Enviroment
Block)包含了进程的启动命令行、当前路径等数据。该字段可以通过NtQueryInformationProcess的PROCESS_BASIC_INFORMATION参数取得。 30. 可以通过WinDbg的dt命令,查看一些结构的具体成员布局,如PEB等。 31. Windows完整性机制(Windows Integrity
Mechanism):这是UAC之外的另一套安全机制,Windows通过在系统访问控制表(SACL, System Access Control
List)中增加访问控制项(ACE, Access Control Entry)实现,每一种受保护的资源都有对应的完整性级别(Integrity
Level),每个进程都有一个基于Token计算的完整性级别,如果进程的级别小于资源的级别,则不能访问资源。提升Token权限之前的进程级别为中,提升后为高,而像IE这样可以能执行网络代码的进程为低。可以通过GetTokenInfomation查看一些和完整性级别相关的策略。窗口系统也根据完整性级别,拒绝低级别者向高级别使用PostMessage、SendMessage等API。 32. Vista以上有一些进程是特殊的受保护进程,ToolHelp
API对他们无效,因此无法查看进程信息。 33. GetProcessTime查看进程时间,GetProcessIoCounters查看IO次数。 34. GetProcessImageFileName返回内核格式的文件名。 1. Job(作业),也就是进程组的概念,添加进同一个作业的进程能够通过作业内核对象来集中控制,设置一些额外的属性等。添加进一个作业就不能再移出。 2. IsProcessInJob、CreateJobObject、OpenJobObject。 3. 作业内核对象在它内部的所有进程都结束后才会被销毁。 4. 细节:当客户的作业句柄变量都被关闭后,即使作业对象还存在(因为进程没有全部结束),也不能再通过作业名打开作业再操作了。 5. Vista以上,通过任务管理器创建的进程,都被添加进了一个独立的作业;从命令行(cmd)创建的进程则不然。 6. 能够对作业添加的限制:基本限制(限制进程时间、优先级、物理内存占用等)、扩展限制(基础限制之上,还能限制内存使用总量,以及查看峰值内存使用)、UI限制(限制关机/重启、访问剪切板、切换桌面、改变显示器设置、访问作业外进程的句柄等)、安全限制(安全限制一旦设置,则不能修改)。SetInformationJobObject、QueryInformationJobObject用于设置和查询限制。 7. AssignProcessToJobObject添加进程到作业。 8. 父进程位于某一作业中,子进程创建后也自动加入同一作业。除非作业的基本限制中包含JOB_OBJECT_LIMIT_BREAKAWAY_OK(允许进程时脱离作业),并且CreateProcess时指定CREATE_BREAKAWAY_FROM_JOB标记。 9. TerminateJobObject强制结束作业,同时结束作业内所有进程(等价于对作业内每个进程调TerminateProcess)。 10. QueryInformationJobObject除了查看作业限制外,也可以查看作业信息,包括总进程数、活跃进程数、总时间、总IO次数、进程ID列表等。 11. 作业结束后(所有内部进程结束),内核对象处于激活态,WaitForSingleObject返回。 12. 作业通知机制:将作业对象和IO完成端口绑定,作业中的事件(进程结束、时间到期、内存达到限制等)将通过完成端口事件来通知。 1. 像进程一样,线程在数据上也分为两个部分:线程内核对象(包括统计信息)、栈。(进程的两个部分是,内核对象和地址空间)。 2. 比起ExitThread和TerminateThread,应该让线程的主函数返回来结束线程,否则一些栈对象不能正常析构(这里不再考虑CRT函数)。 3. 在C/C++编程中不要使用CreateThread、ExitThread,应该使用编译器厂商提供的包装函数,如MS的_beginthreadex、_endthreadex。因为使用前者,C/C++的CRT不能正常初始化和释放线程相关资源(C/C++中有一些全局变量如errno和一些有内部状态的函数strtok、asctime都需要通过TLS来正确实现,毕竟C库函数的诞生早于多线程)。事实上,如果在C/C++中使用了CreateThread和EndThread,部分有内部状态的函数还是可以正常使用的,因为这些函数内部会尝试取得TLS,发现还未分配的话会自动分配,CRT的Dll版本库也会在得到线程退出通知时尝试释放TLS,只是因为这份TLS是中途分配的信息不够全面,部分状态函数还是会有问题,因此在C/C++中还是要尽量使用后者。 4. 线程栈最大为CreateThread的dwStackSize参数和/STACK链接选项(VC中默认为1MB)两者中的较大值。 5. TerminateThread的一些细节:该函数是异步的,函数返回时,线程还没有结束,需要WaitForSingleObject;DllMain不会收到被Terminate线程的结束通知。 6. 只有当线程函数结束(正常返回或Exit掉)后,该线程的栈空间才会被回收(也就是说TerminateThread函数刚返回时被杀死线程栈空间还在,直到线程对象处于激活态)。 7. 对进程中的各个线程来说,ExitProcess和TerminateProcess都将导致对线程的TerminateThread调用,因此进程的main函数结束前,尽量确保工作线程都正常退出。 8. 大部分的资源都是进程相关的,窗口句柄和hook句柄是线程相关的,线程退出时会释放他们(在C/C++中还有CRT的TLS变量)。 9. GetCurrentProcess、GetCurrentThread返回的都是伪句柄,如果想要把这个句柄保存下来在其他线程、进程中使用的话,是有歧义的,可以用保存ID来代替,如果一定要保存句柄的话,两种方法:(1)DuplicateHandle(2)先GetCurrentThreadID,再OpenThread。 1. Windows线程调度的时间间隔(发生上下文切换的时间片)大概是15毫秒(GetSystemTimeAdjustment的lpTimeIncrement参数)。 2. 每个线程都有一个挂起计数,当计数非0的时候,该线程不参与线程调度。CreateThread、CreateProcess传入特定的参数可以使计数初始化为1。SuspendThread可以增加计数,ResumeThread可以减少计数,两者都返回新的挂起计数。显然线程无法对自身调用ResumeThread。 3. 调试进程的WaitForDebugEvent返回后,被调试进程的所有线程被挂起,直到调试进程调用ContinueDebugEvent。 4. Sleep的休眠时间可能不精确,取决于线程调度时间片大小(一般是15毫秒左右)以及其他线程的运行情况。 5. Sleep(0)和SwitchToThread的区别在于:如果存在另一个更低优先级的线程,前者不会将CPU让出,而后者会。即如果存在多个线程,SwitchThread总是让出CPU。 6. YieldProcessor用于支持超线程技术的CPU切换超线程。 7. GetThreadTimes、GetProcessTimes返回指定线程或进程的内核代码时间和用户代码时间(两者都是绝对的CPU执行代码时间,不包括调度过程中的中断时间以及主动的Sleep或者Wait时间)。因此在对代码段计时的时候,使用GetThreadTimes明显优于GetTickCount等,因为后者得出的时间包括了其他线程的时间片。 8. 用于计时,最基本的有clock、GetTickCount、timeGetTime等;为了地提高精度,可以使用QueryPerformanceCounter;为了去掉因线程调度中断的时间和Sleep、Wait的时间,可以使用GetThreadTimes、GetProcessTimes等。在Vista以上的系统中,有新的机制,可以使用ReadTimeStampCounter(对应GetTickCount)、QueryThreadCycleTime(不考虑中断休眠,对应GetThreadTimes)、QueryProcessCycleTime等。对于没有考虑线程调度影响的函数,可以先用SetThreadPriority提高优先级尽量独占时间片。应该确保每次调用QueryPerformanceCounter的时候在同一CPU核心上,使用SetThreadAffinityMask。 9. 线程上下文(CONTEXT)保存在线程的内核对象数据中,主要包括线程相关的CPU寄存器状态等。上下文有两份,分别记录内核和用户模式,GetThreadContext只能返回用户模式上下文,在调用该函数前应该确保用户上下文不再改变了,即线程正处于内核态或者虽然在用户态但已经调用过SuspendThread。 10. 先SuspendThread、再SetThreadContext改变线程上下文,可以改变执行流等,一般用于调试器 “跳到指定位置执行” 的功能等。 11. 高优先级线程可以被调度时(没有Sleep、Wait等),低优先级线程得不到时间片;即使低优先级线程正在执行,一旦有高优先级线程可以调度,前者会被中断并让出CPU资源。 12. SetPriorityClass设置进程的优先级类,SetThreadPriority设置线程的相对优先级(相对于进程优先级类),二者共同决定线程的实际优先级(这个映射根据Windows版本不同而异,是一个0~31的整数,用户不可访问)。将线程的实际优先级设置为最高(31)是危险的,因为它将抢占系统资源,导致IO不能响应等。 13. 当线程有IO事件或消息到来时,操作系统会暂时提高线程的优先级;或者线程可调度但长时间(数秒)都得不到时间片的时候,系统也会暂时提高线程优先级。可以设置是否允许系统自动提升优先级:SetProcessPriorityBoost、SetThreadPriorityBoost。 14. 特定类型计算机的几个相关CPU核心之间可以共享内存缓存等,因此Windows支持设置线程关联CPU核心SetProcessAffinityMask、SetThreadAffinityMask。当然这组API也可以用于为特定线程提供专用CPU资源以提高性能。子进程默认继承父进程的核心关联设置。 15. SetThreadIdealProcessor设置线程最多可以使用的闲置CPU数量。该设置会覆盖AffinityMask。 16. 进程的默认AffinityMask可以在镜像文件头中设置(因为没有链接选项只有手工写文件):ImageLoad->GetImageConfigInformation->ilcd.ProcessAffinityMask->SetImageConfigInformation->ImageUnload。 1. Interlocked系列函数:InterlockedIncrement(对应++)、InterlockedExchangeAdd(对应+=)、InterlockedExchange(对应=)、InterlockedCompareExchange(cas)。 2. _aligned_malloc可以指定分配内存的对齐边界。 3. spinlock(自旋锁)是CAS的应用。使用自旋锁的时候因为有while(true)
{ …; Sleep(0);
}这样的循环,因此线程优先级不能太高,使用SetThreadPriorityBoost来禁用优先级提升,避免被自动提升后不会让出CPU(或者使用SwitchToThread)。自旋锁适用于单个线程不会占用资源太久的情况(因为一个线程占有资源期间,其他线程在循环检测浪费CPU)。 4. CAS(InterlockedCompareExchange)必须是原语!必须!用C++编写的CAS是不行的。 5. InitializeSListHead、InterlockedPushEntrySList、QueryDepthSList等API可以以Interlocked的方式操作一个单链表。 6. CacheLine:是Cache和内存通信的基本单位,可能是32/64字节等,CPU读写内存的时候会先将对应的CacheLine加载进Cache,修改完成后Flush到内存上。因此数据组织为CacheLine
Size对齐、以及将只读和读写数据分别组织到不同的CacheLine都能提高效率。多个CPU(或者具有独立Cache的多个CPU核心)访问同一地址时,该地址附近的数据会被多个Cache映射成各自的CacheLine,如果其中某个CPU修改了其CacheLine的数据,该CPU会通知其他CPU更新各自的CacheLine,这种行为会影响性能,故尽量避免跨线程共享数据以及利用AffinityMask尽量使用同一个CPU。 7. GetLogicalProcessorInformation提供CPU描述信息(比如能够查询到包括4个CPU核心,3级Cache,1、2级Cache为各个核心独有,3级Cache为共享Cache,其Cache
Line Size为64字节等)。 8. 所有线程都处于等待状态数分钟后,电源管理器介入。 9. volatile的作用:编译器不会将变量优化成寄存器变量,即每次读写都会访问内存。对struct应用该关键字会影响每个字段。 10. CRITICAL_SECTION内部记录了拥有访问权的线程以及引用次数。TryEnterCriticalSection如果返回TRUE,则已经增加了计数需要对称调用LeaveCriticalSection。 11. CRITICAL_SECTION在实现上结合了spinlock(自旋锁),调用EnterCriticalSection时发现资源正被占用需要切换到内核态休眠之前(切换到内核态开销很大,高达数千CPU周期),可以尝试进行一定次数的循环判断。使用InitializeCriticalSectionAndSpinCount可以启用结合自旋锁功能(作为参考,用于保护进程堆的CS的SpinCount为4000),使用SetCriticalSectionSpinCount可以修改旋转次数。当SpinCount为1的时候,关键段内部用于休眠和唤醒的事件对象会第一时间创建,而不是等到EnterCriticalSection的时候才创建。建议总是启用自旋锁。 12. Slim Reader/Writer
Lock是性能比关键段更好的选择,相比后者,它的缺陷是不能递归加锁、且没有TryLock。InitializeSRWLock、AcquireSRWLockShared(申请读锁)、AcquireSRWLockExclusive(申请写锁)。 13. 在都能完成任务的情况下,性能从高到底依次是:无锁、volatile、Interlocked、SRW、CRITICAL_SECTION、内核对象(因为切换到内核态开销很大)。 14. SleepConditionVariableCS、SleepConditionVariableSRW用法:已经获得锁(CS、SRW)的线程开始在一个ConditionVariable对象上睡眠,同时释放锁;如果其他线程Wakeup这个ConditionVariable对象,则函数返回TRUE,且再度获得锁;如果超时,返回FALSE,不会获得锁。应用:消费者获得锁后发现没有产品于是开始休眠等待生产者产出产品后唤醒。 15. 技巧:按资源的逻辑个数而不是对象个数来组织锁;需要加多层锁的时候,总是按固定顺序,比如按锁的地址大小来依次加锁,避免死锁;通过拷贝资源等方式来减小锁粒度。 1. 内核对象用于线程同步更灵活比如可以设置等待时间以及跨进程等,但开销更大(需要切换到内核模式)。 2. 内核对象中都有一个表示触发状态的BOOLEAN值。 3. 进程和线程对象在结束前是非触发,结束后是触发状态,其他时候不会再改变。 4. 文件对象有正在处理的异步IO请求时处于非触发,其他时候触发。 5. 控制台输入句柄在没有输入的时候非触发。 6. 内核对象触发后,Wait在上面的线程被唤醒,决定哪一个线程首先被唤醒的规则基本上就是等待顺序的先入先出,和线程的优先级等无关。 7. PulseEvent会在Event对象上产生一个触发脉冲。近似于SetEvent(h);ResetEvent(h);两句。 8. WaitableTimer在平时处于非触发,第一次时间到或者之后周期性时间到都会处于触发状态。另外在SetWaitableTimer的时候可以传入回调指定在触发的时候往APC(Asynchronous
Procedure
Call)队列中加入回调,但必须定时器触发时线程正处于Alertable(使用SleepEx等带Ex的API)状态下才会入队列(避免因为回调处理太慢及其他因素导致过量入队)。一般定时器的APC和WaitFor两种模式不混用。SetWaitableTimer指定第一次的时间时,正数表示绝对时间(SystemTimeToFileTime得到),负数表示相对时间。每次调用SetWaitableTimer会自动取消上次调用的设置,故两次调用间不必CancelWaitableTimer。该定时器和基于消息的SetTimer定时器建议适时选用。 9. Semaphore的当前计数非0时处于触发。ReleaseSemaphore增加计数发现达到最大时会返回FALSE,WaitFor减少计数到0的时候会休眠。 10. Mutex和CriticalSection在使用上完全相同,都记录了Owner线程和递归次数。由于CriticalSection和Mutex记录了Owner线程,因此需要该线程来释放计数,如果在计数减少到0前线程退出了,则同步对象处于Abandoned(遗弃)状态。对于Abandoned的情况,系统能检测到发生在Mutex上的问题,并在底层自动释放计数,只是WaitFor会返回WAIT_ABANDONED表示Mutex对象的计数是由系统自动回收的,该Mutex保护的资源可能处在未定义状态。而CS的计数不会被自动释放,一旦Abandoned则CS永远的失效了。 11. WaitForInputIdle:进程中创建第一个窗口的线程的消息队列中没有需要处理的输入消息后返回。 12. MsgWaitForMultipleObjects:等待的内核对象触发后或者线程的消息队列中有相应消息后返回。 13. SignalObjectAndWait增加一个对象计数的同时原子地等待另一个对象。能够增加计数的对象只限于Event(SetEvent)、Mutex(ReleaseMutex)、Semaphore(ReleaseSemaphore),而等待的对象类型不限。使用:客户端填充好请求于是通知服务端准备处理并等待服务端处理完毕。 14. 在Vista以上可以通过WCT(等待链遍历,Wait Chain
Traversal)相关API来追踪死锁。OpenThreadWaitChainSession、GetThreadWaitChain。 1. 打开设备的方式:文件-CreateFile,参数时路径名或UNC路径名。目录-CreateFile,参数为路径名或UNC路径名,另外指定FILE_FLAG_BACKUP_SEMANTICS允许改变目录属性。逻辑磁盘驱动器-CreateFile,参数为””” \\.\x:”,打开后可以格式化和检测大小等。物理磁盘驱动器-CreateFile,参数为””” \\.\PHYSICALDRIVEx”,(其中x为012等)。串口-CreateFile,参数为””
COMx”。并口-CreateFile,参数为””
LPTx”。邮件槽服务器-CreateMailSlot,参数为”\\.\mailslot\abcd”。邮件槽客户端-CreateFile,参数为””\\serverName\mailslot\abcd””。命名管道服务器-CreateNamedPipe,参数为”\\.\pipe\abcd “。命名管道客户端-CreateFile,参数为””\\serverName\pipe\abcd “。匿名管道-CreatePipe。套接字-Socket、accept、AcceptEx。控制台-CreateConsoleScreenBuffer、GetStdHandle。前面的设备路径规则:””””\\服务器\设备”,其中如果在本机的话,服务器就是””
.”。 2. SetCommConfig可以设置串口波特率等属性。 3. SetMailSlotInfo可以设置超时。 4. 一般用CloseHandle关闭设备。closesocket关闭套接字。 5. GetFileType可以返回设备的类型:FILE_TYPE_DISK-磁盘文件;FILE_TYPE_CHAR-字符文件,包括控制台和打印机等;FILE_TYPE_PIPE-命名管道或匿名管道。 6. 多次CreateFile打开同一个文件得到的是不同的内核对象,各自维护自己的文件指针等数据; DuplicateHandle得到的多个句柄仍然标志的是同一个对象。 7. CreateFile的dwShareMode参数:0表示独占,如果文件已经被打开,则本次打开失败;如果本次打开成功,在关闭前不能在其他地方打开同一个文件。FILE_SHARE_READ,如果本次打开前已经有写句柄,本次打开失败;如果本次打开成功,在关闭前在其他地方不能打开写句柄。FILE_SHARE_WRITE也类似。FILE_SHARE_DELETE表示,如果本次打开成功,其他地方又删除了文件,则删除时只是打上删除标记,待这里的句柄关闭后才真正删除。 8. CreateFile的dwFlagsAndAttributes参数:(1)关于内置缓冲。内置缓冲至少有两个作用,首先,加速,频繁的小字节块访问会被缓冲为少数大字节块的设备读写;其次,最底层设备访问需要按一定的字节块对齐(文件无缓冲读写需要按磁盘扇区大小对齐),缓冲屏蔽了这个限制,方便上层使用。FILE_FLAG_NO_BUFFERING,底层不提供缓冲,需要上层自己提供缓冲,缓冲区首地址、文件读写偏移/指针、读写字节数三者都必须按磁盘扇区大小对齐(扇区大小可以通过GetDiskFreeSpace获得,比如512字节)。文件太大有可能打开失败,也需要指定这个标记。当有缓冲时,FILE_FLAG_SEQUENTIAL_SCAN承诺会连续访问(不会用SetFilePointer),因此底层可以尝试缓冲更多连续内容;FILE_FLAG_RANDOM_ACESS表示会随机访问,因此底层会尽量不要缓冲太多(缓冲的作用还剩下避免要求扇区对齐)。FILE_FLAG_WRITE_THROUGH,表示写文件不使用缓冲,这样避免在数据Flush到文件前对象就被非法关闭导致数据丢失。(2)其他标志。(1)FILE_FLAG_DELETE_ON_CLOSE,关闭文件的时候删除,适合临时文件。FILE_FLAG_OVERLAPPED异步IO。 9. CreateFile的dwFlagsAndAttributes参数:只在创建文件的时候有效,用于指定ARCHIVE、ENCRYPTED(加密)、HIDDEN、READONLY、SYSTEM、TEMPORARY等属性 10. CreateFile的hFileTemplate参数:只在创建新文件时有效,传入另一个文件句柄的话,系统会忽略dwFlagsAndAttributes参数和直接使用该句柄对应的dwFlagsAndAttributes。 11. FILE_ATTRIBUTE_TEMPORARY和FILE_FLAG_DELETE_ON_CLOSE标记结合适用于临时文件,前者会让系统尽量将文件维护在内存而不是磁盘中,后者会在关闭句柄时删除文件。 12. 获取文件大小:GetFileSizeEx、GetCompressedFileSize(尤其针对压缩属性的文件)分别返回逻辑大小和磁盘上的实际大小。 13. SetFilePointerEx可以超出文件实际大小,超出后,除非写文件或者SetEndOfFile否则文件不会变大。 14. SetEndOfFile是减小文件的唯一手段。 15. FlushFileBuffers。 16. 在Vista以上,可以用CancelSynchronousIo来中止一个线程的同步IO。 17. 异步IO的实际访问设备顺序不一定和请求顺序(API调用顺序)相同(比如驱动会根据磁盘磁头位置选择先处理距离最近的IO请求)。 18. 对异步IO的文件发出IO请求有可能是同步操作,因为可能数据正好在底层缓冲中可以立即完成。 19. 关于取消异步IO请求:(1)CancelIo取消调用线程在指定设备上的异步IO请求。(2)线程结束会取消该线程的所有异步请求。(3)关闭设备会取消所有该设备的请求。(4)CancelIoEx能取消调用线程以外线程在指定设备上的特定请求。(5)CancelIoEx能取消特定设备的所有请求。 20. OVERLAPPED结构的Internal表示错误码,InternalHigh表示传输的字节。由于异步IO跟文件指针无关(文件指针来不及修改),所以偏移存储在该结构中。 21. GetOverlappedResult函数实现为,访问结构的Internal、InternalHigh字段,另外如果结构的hEvent为空尝试Wait设备否则Wait事件(函数参数bWait为TRUE的时候)。 22. QueueUserAPC向线程的APC队列抛出一个用户自定义函数。 23. QueueUserWorkItem向线程池抛出任务。 24. 异步IO有四种方式得到完毕通知:(1)设备内核对象触发。(2)OVERLAPPED的hEvent内核对象触发。(3)APC回调(ReadFileEx)。(4)IO完成端口。 25. 异步IO-设备内对象触发:对FILE_FLAG_OVERLAPPED的文件使用ReadFile,将OVERLAPPED的hEvent设置为空,IO完成时设备句柄将触发,因此只能同时进行一次IO(瓶颈)。可以一个线程请求,另一线程响应完成。 26. 异步IO-事件内核对象的触发:将OVERLAPPED的hEvent设置为事件以获得通知。可以用SetFileCompletionNotificationModes来避免IO完成时去触发设备对象。可以一个线程请求,另一线程响应完成。 27. 异步IO-APC队列:ReadFileEx后使用SleepEx等让线程进入Alertable状态。同一个线程发出请求和响应完成(瓶颈)。 28. 异步IO-IO完成端口:步骤(1)CreateIoComplitionPort创建完成端口,指定活跃线程数(建议为CPU核心数)。(2)用CreateIoComplitionPort向完成端口添加异步设备。(3)创建完成端口服务线程(建议为CPU核心*2个,或者动态估计),初始化后使用GetQueuedCompletionStatus使线程和完成端口绑定并休眠。(4)执行异步IO,IO完成后底层会用PostQueuedCompletionStatus令正在GetQueuedCompletionStatus上休眠的服务线程苏醒响应。细节:可以在OVERLAPPED的hEvent指定一个值为hEvent
|
1的数,令IO完成后不发出完成通知(即不Post)。可以使用GetQueuedCompletionStatusEx来一次响应多个请求。完成端口服务线程中,使用GetQueuedCompletionStatus休眠的线程叫等待线程,从GetQueued…返回的线程叫释放线程(活跃线程),活跃线程如果因其他原因(如Sleep、Wait)再挂起叫暂停线程,完成端口能够检测到各个线程的数量,会控制GetQueuedCompletionStatus的返回以使活跃线程尽量逼近创建完成端口时指定的数目。默认情况下异步IO即使同步完成,也会Post…,可以使用SetFileCompletionNotificationModes来禁用Post…。对于完成事件的响应是先入先出的,但服务线程的激活却是后入先出的(尽量激活相同线程,其他线程长期休眠其栈内存可以换出到页面文件提高性能)。 1. MessageBox弹出的对话框是可用修改的,FindWindow找到后,0x0000ffff是静态文本框的控件ID等,因此很容易实现倒计时自动关闭的消息框。 2. 从win2000开始提供的线程池主要有4种用法:(1)异步调用函数(QueueUserWorkItem)。(2)定时器回调(CreateTimerQueueTimer)。(3)内核对象触发后回调(RegisterWaitForSingleObject)。(4)内置IOCP实现(BindIoCompletionCallback)。 3. 线程池模块下有几种底层线程:(1)可变数量的长任务线程,用于执行标记为WT_EXECUTELONGFUNCTION的长时间回调。(2)1个Timer线程。所有CreateTimerQueueTimer调用都被转发为在Timer线程上创建以APC方式通知的WaitableTimer,这个线程除了删除和创建WaitableTimer外,就是在Alertable态下休眠等待定时器的APC。由于这个线程一旦创建就贯穿进程生命期不会销毁,因此WT_EXECUTEINPERSISTENTTHREAD标志的线程池回调也由本线程执行。(3)多个Wait线程。服务于RegisterWaitForSingleObject,每个线程用WaitForMultipleObjects等待最多63(MAXIMUM_WAIT_OBJECTS减去一个用于维护对象数组的工作对象)个内核对象,对象触发后执行回调。(4)可变数量的IO线程。由于发出异步IO请求(ReadFileEx)后,一旦请求线程结束,请求将被撤销,因此请求被驱动执行完毕之前IO请求线程一定要存在,而线程池内的线程大都会根据CPU繁忙情况动态创建和删除,因此线程池中有一部分线程被赋予了特殊行为,他们会检测自己执行回调时发出的异步IO请求是否完成,如果没有,就不会结束运行,这些追踪自身发起的异步IO请求执行情况的特殊线程叫做IO线程。因此只能在线程池的IO线程上执行异步IO调用。(5)可变数量的非IO线程。线程池内部实现了一个IO完成端口,服务于BindIoCompletionCallback,其中IOCP的服务线程(在GetQueuedCompletionStatus上休眠)由于数量会根据CPU情况动态调整,不应用于执行异步IO,故叫非IO线程。 4. 四种用法中,如果Flags参数指定的回调执行线程与默认线程不符,底层可以使用QueueUserWorkItem来切换线程。比如CreateTimerQueueTimer用法的默认线程肯定是Timer线程,发现WT_EXECUTELONGFUNCTION标记后,使用Queue…来切换到专门执行长任务的线程避免阻塞Timer线程影响定时器功能。 5. 用法1-异步函数调用:QueueUserWorkItem 。Flags参数为0(WT_EXECUTEDEFAULT)的时候回调交给非IO线程执行(通过PostQueuedCompletionStatus通知非IO线程)。还可以指定WT_EXECUTEINIOTHREAD交给IO线程、指定WT_EXECUTEINPERSISTENTTHREAD交给Timer线程、指定WT_EXECUTELONGFUNCTION交给长任务线程等。 6. 用法2-定时器回调:CreateTimerQueue-创建专用TimerQueue。DeleteTimerQueueEx-删除专用TimerQueue,参数CompletionEvent是用于接受删除Queue完毕通知的事件对象,如果设置为NULL表示不接受通知,设置为INVALID_HANDLE_VALUE表示阻塞等待删除完成。注意不能在Timer线程上的回调中以INVALID_HANDLE_VALUE为参数调用DeleteTimerQueueEx,因为后者实现为向Timer线程抛出一个要求维护Timer列表的APC,在线程的APC回调中抛出新的APC并且还阻塞等待,结果就是死锁。CreateTimerQueueTimer-创建具体的Timer对象,TimerQueue参数指定为NULL表示在默认的Queue上创建对象,适用于Timer对象不多的用法。使用WT_EXECUTEINTIMERTHREAD标记即要求在Timer线程上执行回调,因不必切换线程效率较高,注意回调不能过长影响Timer线程的功能。ChangeTimerQueueTimer-改变Timer对象的一些参数。DeleteTimerQueueTimer-删除Timer对象,注意使用INVALID_HANDLE_VALUE参数造成死锁的可能。 7. 用法3-等待内核对象触发回调:RegisterWaitForSingleObject-在内核对象触发或超时后执行回调。标记WT_EXECUTEINWAITTHREAD表示在Wait线程上执行,效率较高。WT_EXECUTEONLYONCE只执行一次回调,适用于进程/线程句柄这种触发后不再重置的对象。PulseEvent的脉冲可能不会被Wait线程检测到(线程刚好在干其他事)。UnregisterWaitEx-取消回调,注意INVALID_HANDLE_VALUE参数可能的死锁。 8. 用法4-内置IOCP实现:BindIoCompletionCallback。将异步IO设备和内置的IO完成端口管理起来,异步完成后执行回调。标志只能为0,默认在非IO线程(IOCP的服务线程)上执行,如果需要切换线程,手工QueueUserWorkItem。 1. Visita以上的新线程池框架下四种用法:(1)异步调用函数(TrySubmitThreadpoolCallback、CreateThreadpoolWork)。(2)定时器回调(CreateThreadpoolTimer)。(3)内核对象触发后回调(CreateThreadpoolWait)。(4)内置IOCP实现(CreateThreadpoolIo)。 2. 新线程池的实现包括IOCP。 3. 用法1-异步函数调用:TrySubmitThreadpoolCallback-通过IOCP的Post…提交一个回调到线程池。使用Work对象允许一次创建多次提交效率更高:CreateThreadpoolWork、SubmitThreadpoolWork、WaitForThreadpoolWorkCallbacks、CloseThreadpoolWork。其中WaitFor可以等待所有提交项被执行完毕,或者取消掉进入队列但还没开始执行的项。注意不应该在回调中WaitFor,可能死锁。 4. 用法2-定时器回调:CreateThreadpoolTimer、CloseThreadpoolTimer
-创建/删除。SetThreadpoolTimer-设置Timer参数。起始时间为-1表示立即开始。如果将起始时间设置为NULL,表示停止Timer,停止后用IsThreadpoolTimerSet判断返回FALSE。另外msWindowLength表示允许回调触发时间有一个向后的波动(0~msWindowLength),这样底层可以在这个波动范围内将多个回调连续执行,避免多次Wait和Wakeup(比如Timer
A、B分别在5、6秒后执行,A的波动为2秒,这样系统可以连续执行A、B回调,不必在两者之间插入SleepEx导致额外的线程切换开销)。 5. 用法3-等待内核对象触发回调:CreateThreadpoolWait、CloseThreadpoolWait、WaitForThreadpoolWaitCallbacks类似前面。SetThreadpoolWait指定要等待的内核对象,每次调用只会导致执行一次回调,除非再Set…(即如果Wait进程句柄,进程结束后只会执行一次回调,想要多执行需要再调用Set…)。PulseEvent的脉冲有可能不会触发回调。 6. 用法4-内置IOCP实现:CreateThreadpoolIo、CloseThreadpoolIo同前面。每次异步IO请求之前(ReadFileEx)需要调用StartThreadpoolIo。发出IO请求后停止回调用CancelThreadpoolIo。 7. 对于新线程池回调中的参数PTP_CALLBACK_INSTANCE,可以执行一些操作:LeaveCriticalSectionWhenCallbackReturns、ReleaseMutextWhenCallbackReturns、ReleaseSemaphoreWhenCallbackReturns、SetEventWhenCallbackReturns-这些函数都近似等价于在回调的最后一行释放相关资源(模仿RAII?),不过以上API只有最后一次调用有效(即只能注册一个资源)。FreeLibraryWhenCallbackReturns-回调返回后释放某个Dll,当回调代码本身位于要释放的Dll中时有价值。CallbackMayRunLong-通知线程池回调可能执行较长时间,返回TRUE表示当前线程池有空闲线程,否则表示线程池紧张,建议将剩余执行任务拆分以减少回调时间。DisassociateCurrentThreadFromCallbacks-一般回调返回后,回调就和执行线程解除关系了,那些WaitForThreadpool…Callbacks就能返回,而这个Disassociate函数就是为了在回调结束前提前打上脱离关系的标记,影响包括WaitForThreadpool…的函数等。 8. 定制私有线程池:CreateThreadpool、CloseThreadpool、SetThreadpoolThreadMaximum、SetThreadpoolThreadMinimum-创建线程池对象,设置线程数量范围。注意如果数量上下界相同,那么在线程池中的线程一旦创建就不会销毁,可以用来进行异步IO调用等。InitializeThreadpoolEnviroment、DestroyThreadpoolEnviroment-构建环境。SetThreadpoolCallbackPool-将线程池对象置入环境。SetThreadpoolCallbackRunsLong-标记环境对应的线程池用于执行长任务。SetThreadpoolCallbackLibrary-标记环境对应的线程池中有任务执行期间,该Dll一直在内存中。 9. 线程池清理组(CleanupGroup):一个WaitForThreadpool…Callbacks+CloseThreadpool…的可选替代方案。CreateThreadpoolCleanupGroup、CloseThreadpoolCleanupGroup-创建/删除。SetThreadpoolCallbackCleanupGroup-将清理组置入环境。CloseThreadpoolCleanupGroupMembers-用来在线程池关闭前清理资源,一旦调用该函数就不必再 “遍历每种资源(Work、Timer、Wait、IO)依次调用WaitForThreadpool…Callbacks、CloseThreadpool…”,即该函数调用后,所有以前的线程池组件都被销毁了,句柄也失效。如果该函数的bCancelPendingCallbacks参数为TRUE,那些还在线程池中排队的任务直接取消不再执行,但会通过SetThreadpoolCallbackCleanupGroup注册的函数通知每个被直接取消掉的任务。 1. 纤程其实就是Windows在用户模式实现的协程(coroutine)。 2. 将线程自身转化为纤程:ConvertThreadToFiber-它会创建相应的结构保存当前线程的各种寄存器等数据。ConvertThreadToFiberEx-默认的结构中是不包含浮点寄存器的,使用这个API传入FIBER_FLAG_FLOAT_SWITCH可以保证浮点运算正确。ConvertFiberToThread-当不使用纤程后,应该用这种方式还原为线程。 3. CreateFiber、CreateFiberEx:创建一个包括独立栈和寄存器记录结构的新纤程,后一个函数能够指定初始化的栈物理内存、虚拟内存以及浮点寄存器支持标志。不使用这种纤程后,在其他纤程中使用DeleteFiber来结束Create出来的纤程。 4. 从纤程函数中返回会结束当前线程(当然也结束该线程上所有其他纤程)。 5. SwitchToFiber-切换纤程。 6. FLS支持(Fiber Local
Storage):FlsAlloc-可以指定一个回调,这个回调在FlsFree或纤程销毁时以FlsGetValue的返回值为参数被执行,可用于清理等。FlsGetValue、FlsSetValue。 7. IsThreadAFiber-判断当前是否在某个纤程的上下文中执行。GetCurrentFiber-返回当前纤程上下文。GetFiberData-返回当前纤程主函数的参数。 1. 在32位系统上,虚拟地址空间大致分为4段(64位系统也分为4段,只是大小不同):(1)0x00000000~0x0000ffff,空指针赋值区,辅助调试,禁止任何方式的访问。(2)0x00010000~0x7ffeffff,用户模式分区,各进程单独维护,同一地址值在不同的进程可以有不同解释,各种映像文件(dll、exe)和内存映射文件也载入本区,近2G。(3)0x7fff0000~0x7fffffff,64k禁入分区。(4)0x80000000~0xffffffff,内核模式分区,系统存放内核代码、设备驱动代码、输入输出高速缓存、进程页表等,2G。 2. 32位系统可以配置系统参数让进程用户模式分区达到3G,内核减小为1G。内核内存减小,会影响可以创建的总线程、内核对象数量等。(Visita系统以上,使用bcdedit
/set IncreaseUserVa 3072;xp使用…)。 3. 链接选项-启用大地址(/LARGEADDRESSAWARE):因为过去32位系统用户地址空间固定为2G(直到可以设置用户地址最大到3G),所以有惯用法依赖于这种行为(系统对地址参数会先&0x7fffffff的行为)擅自将地址最高位用于其他目的,为了兼容大量的这种用法并且又允许选择使用3G用户内存,MS增加了这个链接选项。如果开启,表示承诺不使用最高位,想要访问超过2G的用户地址;关闭,表示只使用2G内存,最高位可能有其他解释(在实际的系统实现上,如果用户地址最高位非0会报错)。64位系统中,为了便于大量32位程序向64位移植(32位程序中有大量用法如:int
i = (int)p; …; int *p =
(int)i;),系统默认程序只使用2G用户空间,所以分配的用户地址总是小于2G,直到开启该连接选项。总之,无论32位或64位系统,如果只使用2G,关闭选项,否则开启。 4. VirtualAlloc的MEM_RESERVE参数表示要预定一段空间(如线程栈,即使大部分时候栈都很小,但也需要预留1M左右),叫区域(region)。用户代码申请预留的起始地址必须按allocation
granularity(分配粒度,因CPU而异,但当前CPU大都为64KB)对齐,系统的预留申请无限制(如PEB占用的内存是系统申请的)。预留的大小必须按页面大小对齐(x86、x64CPU的页面大小为4KB)。VirtualAlloc的MEM_COMMIT参数表示将区域commit给虚拟存储器,系统会在使用时将对应的页缓存到物理存储器。 5. 在操作系统内存管理模型中,虚拟地址用于访问虚拟存储器,后者存放于磁盘上,主存作为虚拟存储器和CPU之间的缓存(DRAM)被叫做物理存储器。当CPU要访问内存时,首先,检查该虚拟地址是否对应合法的虚拟存储器(是否commit),如果否则报错表示无效地址,如果是,然后判断该虚拟页(VP,Virtual
Page)是否被缓存到内存,即是否有对应的物理页(PP, Physical Page),如果否则产生缺页错误(Page
Fault)进而判断主存中是否有闲置页面,如果没有闲置页面,则尝试释放一个物理页,先判断要释放的物理页是否被修改,如果被修改了则Flush到对应的虚拟页上然后释放物理页,有了闲置的物理页后,将虚拟地址对应的虚拟页缓存到空闲的物理页上进而更新虚拟地址到物理地址的映射表,然后CPU的MMU(memory
management unit,内存管理单元)将虚拟地址翻译为物理地址,再判断该地址对应的内容是否已经在Cache上,如果否则Cache
Miss然后再将对应的Cache
Line缓存到Cache中,最后读取到CPU寄存器中。在Windows中,虚拟存储器对应的磁盘空间进一步细分到页交换文件(page
file)、映像文件(exe、dll)、内存映射文件(mapped
file)中,后两者被当做虚拟存储器的时候还可以在多个进程间共享(写时拷贝),由于存在共享机制因而Windows的虚拟存储器占的磁盘空间远小于所有进程提交的用户模式内存之和。 6. VirtualAlloc、VirtualProtect等函数可以设置页保护属性:PAGE_EXECUTE(只能运行代码不能读写)、PAGE_EXECUTE_READ(只读和运行代码)、PAGE_NOACCESS等。其中PAGE_WRITECOPY、PAGE_EXECUTE_WRITECOPY属性表示页面可以被多个进程共享,直到被修改,修改时是先拷贝到进程私有页中再修改私有页,这是copy-on-write。Reserve状态下的保护属性会被Commit下的属性覆盖,但两者都可以在VirtualQuery中查询到。 7. 在CPU体系结构中,CPU要访问的数据需要按数据大小对齐(WORD地址按2对齐,DWORD地址按4对齐),否则会产生异常。修复数据未对齐异常有几种途径:(1)x86
CPU会自动进行硬件修复,访问没对齐的数据只是更慢。(2)SetErrorMode传入SEM_NOALIGNMENTFAULTEXCEPT参数,通知Windows通过软件修复未对齐问题。(3)编译选项__unaligned会自动产生额外代码修复问题。综上,后两种软件修复方案适用于非x86
CPU速度更慢,最好还是按数据大小对齐内存。 1. 工作集(Working
Set):缓存到主存中的那些页面。 2. 32位系统中的32位程序和64位系统中的64位程序,都用GetSystemInfo来获取系统信息,而64位系统中的32位程序(IsWow64Process返回TRUE)用GetNativeSystemInfo。获取处理器信息用GetLogicalProcessorInformation,获取内存信息用GlobalMemoryStatus。 3. SYSTEM_INFO(GetSystemInfo)各字段的解释:dwPageSize-页面大小。lpMinimumApplicationAddress、lpMaximumApplicationAddress-用户模式内存大小,32位系统中是0x0001000到0x7ffeffff。dwActiveProcessorMask-CPU掩码,同AffinityMask。dwNumberOfProcessors-处理器个数。wProcessorArchitecture、wProcessorLevel、wProcessorRevision-决定CPU型号。 4. MEMORYSTATUS(GlobalMemoryStatus)各字段的解释:dwMemoryLoad-内存管理系统负载的大致估计,0~100,可以忽略。dwTotalPhys、dwAvailPhys-系统总的物理内存和剩余物理内存。dwTotalPageFile、dwAvailPageFile-系统总的页交换文件和剩余页交换文件。dwTotalVirtual-系统各进程最大用户模式内存(32位是2G-128K)。dwAvailVirtual-当前进程剩余用户模式内存。 5. PROCESS_MEMORY_COUNTERS_EX(GetProcessMemoryInfo)各字段的解释:PageFaultCount-缺页错误数。WorkingSetSize-工作集,即当前进程物理内存占用。PagefileUsage-当前进程的页交换文件占用(包括全部的类型为private内存块和部分的image、mapped块,后者在写拷贝后其虚拟存储器才转移到page
file中)。PrivateUsage-当前进程私有的内存占用,其虚拟存储器位于页交换文件中,虚拟存储器中除去这部分其他的都位于共享文件中了(一般值等于PagefileUsage第1章 错误处理
第2章 字符和字符串处理
第3章 内核对象
第4章 进程
第5章 作业
第6章 线程基础
第7章 线程调度、优先级和关联性
第8章 用户模式下的线程同步
第9章 用内核对象进行线程同步
第10章 同步设备I/O与异步设备I/O
第11章 线程池的使用(第4版)
第11章 Windows线程池
第12章 纤程
第13章 Windows内存体系结构
第14章 探索虚拟内存