这篇笔记是我在读《Windows核心编程》第5版时作的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了不少我我的的思考和对实现的推断,所以很多条款和Windows实际机制可能有出入,但应该是合理的。开头几章因为我追求简洁,每每是不少单独的字句,后面的内容更为连贯。算法
海量细节。编程
1. GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖。windows
2. GetLastError可能用于描述成功的缘由(CreatEvent)。数组
3. VS监视窗口err,hr。缓存
4. FormatMessage。sass
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提供的避免缓冲区溢出攻击的函数在<StrSafe.h>文件中,包括StringCbCat和StringCchCat等函数(其中Cb表示Count of Byte,Cch表示Count of Character,都用于表示衡量目标缓冲大小的单位);另外<TChar.h>中有_tcscpy_s等_s后缀的函数。在源串太短时,<StrSafe.h>的函数截断,<TChar.h>的函数断言。
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,一、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分别在五、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占用的内存是系统申请的)。预留的大小必须按页面大小对齐(x8六、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)。
6. NUMA(非统一内存访问,Non-Uniform Memory Access。一种分布式计算机系统内存模型)机器中的内存管理:GlobalMemoryStatusEx获取各节点总内存。GetNumaHighestNodeNumber-获取系统中总的节点个数。GetNumaAvailableMemoryNode-获取某节点的内存。GetNumaNodeProcessorMask-获取某节点的CPU掩码。GetNumaProcessorNode-判断某CPU位于的哪一个节点。
7. VirtualQuery能够查询某地址所在的内存块(内存块是具备相同状态、保护属性和类型的连续页面),也提供了一些信息指出该内存块在reserve的时候其VirtualAlloc起始地址和保护属性等。
8. MEMORY_BASIC_INFORMATION(VirtualQuery)各字段解释:BaseAddress-内存块起始地址。RegionSize-内存块长度(jeffrey把reserve的叫区域把这儿的叫内存块,而Windows只把这儿叫region,我姑且同前者的概念)。State-块状态,能够是free、commited、reserved。Protect-保护属性,状态是commited时有效。Type-类型,能够是private(私有内存,虚拟存储器在页交换文件)、image(在写拷贝以前,其虚拟存储器就是映像文件(exe、dll),写拷贝(修改dll代码或全局变量等)以后虚拟存储器转移到page file)、mapped(相似image,写拷贝以前虚拟存储器是内存映射文件),状态是commited时有效。AllocationBase、AllocationProtect-reserve时候的基地址和保护属性,状态非free的时候字段有效。
9. Windows进程内存布局:分红不少内存块,其中部份内存块属于同一个区域(reserve的region)。若是要实现内存搜索的功能,能够用VirtualQuery遍历各块,在commited的块中搜索。
10. 线程栈的内存块具备PAGE_GUARD保护属性。
11. 一个进程内存使用的统计分析:输出见最后(单位均为KB)。第一部分是用VirtualQuery遍历各块进行统计,可见进程commit了5.8M内存到虚拟存储器,其中4.5M是映像文件(部分在pagefile中),1.1M是内存映射文件(部分在pagefile中),159K是私有内存(所有在pagefile中)。第二部分使用GetProcessMemoryInfo,可见进程占用主存(物理存储器)1.6M,虚拟存储器中有425K在pagefile中(包括第一部分中所有的private和部分的image、mapped),也说明进程使用的5.8M内存中有5.4M是共享的(5.8-425)。第三部分用GlobalMemoryStatus看出该进程可用虚拟内存为2G。
VirtualQuery :
commitedBytes = 5865.47
readAllowedBytes = 5865.47
imageBytes = 4517.89
mappedBytes = 1187.84
privateBytes = 159.744
GetProcessMemoryInfo :
WorkingSetSize = 1658.88
PagefileUsage = 425.984
PrivateUsage = 425.984
GlobalMemoryStatus :
dwAvailVirtual = 2.13826e+006
1. 用VirtualAlloc来reserve区域的时候:pvAddress为空表示由系统分配区域起始地址,同时使用MEM_TOP_DOWN标志,提示系统优先选择高地址,适用于长时间占用的内存。自定义起始地址的时候,实际reserve到的区域会包含自定义的范围(自定义的起始地址+长度),即返回的地址可能比自定义起始地址小,同时保证该区域起始地址与系统的分配粒度对齐,长度与分页大小对齐。若是找不到这样长的闲置连续空间,返回NULL。reserve和commit的保护属性相同,性能更好。
2. 用VirtualAlloc来commit内存块的时候:实际提交的块会包含自定义范围,而且起始地址和长度都与页面大小对齐。提交的块不该该跨两个区域。
3. VirtualAllocExNuma,适用于NUMA机器。
4. 在Visita以上的系统中,能够分配大页面,大页面是常驻内存的须要有锁定页面的权限(Lock Pages In Memory),同时要求在VirtualAlloc时知足三个条件:(1)大小必须与GetLargePageMinimum对齐(天然该函数必须返回非0)。(2)同时reserve和commit。(3)保护属性必须是PAGE_READWRITE。
5. 在须要使用有空洞的大段连续内存的时候,有一个技巧:reserve一大段,根据须要commit。由于只有commit了才占用虚拟存储器,所以很节省内存。
6. VirtualFree能够反提交和释放内存,其中MEM_RELEASE时的长度参数必须为0,表示释放整个区域。
7. VirtualProtect改变保护属性,注意一次调用不要跨多个区域。
8. VirtualAlloc的MEM_RESET标志,表示愿意暂时放弃一段内存的当前内容,若是系统的物理内存使用紧张,reset的这段内存对应的物理内存可能会被挪用,直到再次访问这段内存。
9. 即便经过VirtualAlloc来commit了,只要没有访问过这段地址,系统也不会分配内存。即若是commit的1.5G内存不读写,开销很小。
10. 地址窗口扩展(AWE,Address Windowing Extension):能够指定一段地址直接映射到物理内存,具备常驻内存和增长可用内存量的优势。以MEM_PHYSIC调用VirtualAlloc来指定要用于映射的虚拟地址段,而后AllocateUserPhysicalPages分配物理页面,再MapUserPhysicalPages将虚拟地址段和分配的物理页面关联,以后随意读写,使用完毕后以NULL做为参数调用MapUserPhysicalPages解除关联,最后FreeUserPhysicalPages、VirtualFree释放物理页面和地址段。一段虚拟地址能够经过Map和Unmap轮流访问多段物理内存,明显增长了进程可访问的内存总量。AWE也要求用户有锁定页面的权限。
1. 连接选项“/statck:reserve[,commit]”能够在PE文件中记录默认的线程栈保留大小和提交大小,实际栈大小还要结合_beginthreadex时的参数。
2. PAGE_GUARD属性的做用:第一次访问具备该属性的页面,会触发一个STATUS_GUARD_PAGE_VIOLATION异常,同时该属性被自动抹除,因而后续的访问正常。即该属性用于首次访问的通知。
3. 默认条件下,线程栈建立时先reserve一块1MB的内存,栈底的两块页面被提交,其中较低地址的那块页面具备PAGE_GUARD属性,被称为保护页面(Guard Page)。当栈的调用层次变深须要更多内存时,系统去掉当前保护页面的PAGE_GUARD属性并提交下一个页面做为保护页面(实现方式见条款4)。这个过程进行下去,栈顶所在的提交页面以后始终有一块被提交的保护页面,直到栈的调用层次足够深,当倒数第二个页面被提交并须要标记为保护页面的时候,这个标记行为终止并抛出EXCEPTION_STACK_OVERFLOW异常。栈最低地址的一个页面始终处于reserve状态,用来隔离栈和栈下方的内存空间,避免非法的栈操做访问越界。捕获了栈溢出结构化异常的线程因为没有了保护页面,须要调用_resetstkoflw来从新标记保护页,不然下次调用层次太深的时候会由于没有保护页不触发栈溢出异常直接访问到最低地址的reserve页,形成非法访问错误。
4. 栈上reserve页被从高到底依次commit的方式:当位于栈顶的函数帧在保护页面中时,访问保护页内存会触发异常,系统捕获异常,提交下一页,并判断下一页是不是倒数第二页,是的话抛出栈溢出异常,不然将一下页标记为保护页。若是栈顶函数帧很大(好比包含大数组),跨越多个分页,因为函数内部可能先访问函数帧中最低地址的reserve页的内存,引发非法访问错误,因而C++编译器对这种栈帧大于1个分页的函数进行了特殊处理:编译器会在大栈帧函数的开始插入_chkstk,后者会沿大栈帧的底部向顶部依次访问每一个分页,连续推进保护页,保证后来函数体中的随机访问都做用在commit分页上。
5. Debug版本程序在调用函数前,会备份当前栈的上下文,在函数返回后对比新的栈数据和备份数据,判断是否有栈上的越界错误。Release版本程序开启/Gs开关后能起到相似的效果。
1. 内存映射文件的主要应用场合:(1)映射到映像文件(Exe、Dll),加速进程启动。(2)映射到数据文件,代替标准的文件IO。(3)共享内存。
2. 当DLL被LoadLibray时若是发现预约基地址已经被占用时,可能会加载失败(构建DLL时指定了/FIXED连接选项),至少也会重定位,后者会占用额外存储空间和增长DLL载入时间。
3. 段的大小都按页大小对齐。
4. 使用dumpbin.exe /headers能够查看PE文件的各类段。常见段:.bss-未经初始化的全局变量等数据。.CRT-只读的C运行时数据。.data-已初始化的全局变量。.debug-调试信息。.didata-延迟导入名字表(delay imported names table)。.idata-导入名字表。.edata-导出名字表。.rdata-只读的运行时数据。.reloc-重定位表信息。.rsrc-资源。.text-代码段。.textbss-启用增量连接(Incremental Linking)时C++编译器生成。.tls-线程本地存储。.xdata-异常处理表。
5. 默认状况下.data段的页面具备写拷贝属性,所以PE文件的一个实例修改全局变量并不会影响其余进程实例。
6. 使用#pragma data_seg(“MyDataSeg1”); #pragma data_seg();能够声明一个新的数据段,其中初始化的变量会自动加入该段。没有初始化的变量能够经过__declspec(allocate(“MyDataSeg1”)) int g_i;来加入数据段。用#pragma comment(linker, “/section:MyDataSeg1, RWS”)来为段指定属性,”S”表示Shared,它经过去掉段页面的写拷贝保护属性,来达到多进程共享的效果。
7. CreateFileMapping:参数fdwProtect的PAGE_READONLY、PAGE_WRITECOPY等很容易理解,另外还有几种属性:SEC_COMMIT-默认值。SEC_IMAGE-表示该文件是映像文件,该文件被映射到内存时,系统会对其中不一样的段添加对应的保护属性。SEC_NOCACHE-无cache,驱动开发人员用。SEC_LARGE_PAGES-大页面支持,相似VirtualAlloc那边。SEC_RESERVE-经过这个标记映射的内存没有是没有被提交的,直到再调用VirtualAlloc来commit才能访问这些页面。参数dwMaximumSizeHigh、dwMaximumSizeLow表示要求的最大文件大小,尤为在共享内存对应的虚拟存储器在页交换文件中时特别有意义(hFile参数为INVALID_HANDLE_VALUE的状况),若是映射的可写磁盘文件自己的大小没有达到这个值,文件也会被自动扩大。若是最大大小为0,表示使用磁盘文件自己大小。
8. MapViewOfFile:建立映射对象的一个视图,多个视图之间的数据是严格同步的,由于同一个映射对象的多个视图尽管虚拟地址段不一样,但都映射到同一个虚拟存储器上。该函数返回后,内存已经被commit(除非CreateFileMapping时指定SEC_RESERVE参数)。参数dwFileOffsetHigh、dwFileOffsetLow、dwNumberOfBytesToMap共同决定要把文件的哪部分映射到内存,Offset必须与分配粒度对齐,Size为0的时候表示范围从Offset直到文件尾。对返回的地址VirtualQuery会获得Map的区域。
9. UnmapViewOfFile:释放映射的内存区域。
10. FlushViewOfFile:将缓存中已修改的数据Flush到文件中,若是没修改被直接丢弃。注意若是映射页面具备写保护属性,缓冲中的数据最多被Flush到Page File中。若是是映射到远程文件,该函数只保证数据被Flush到网上,而远程的文件不必定会被修改,除非CreateFile时指定了FILE_FLAG_WRITE_THROUGH。
11. 注意,虽然CreateFileMapping会增长文件对象计数,MapViewOfFile会增长映射对象的计数(也就是说,在UnmapViweOfFile以前这两个内核对象就能够被CloseHandle了),可是若是太早关闭映射对象,其余地方要打开映射对象时会失败(即OpenFileMapping失败或者CreateFileMapping的LastError不是ERROR_ALREADY_EXISTS),也就是说,内核经过视图对映射对象的引用,不能被用户模式代码检测到,所以最好仍是按传统顺序先UnmapViewOfFile再CloseHandle。
12. NUMA支持:CreateFileMappingNuma、MapViewOfFileExNuma。
13. 打开同一个磁盘文件的多个文件内核对象,因为各自拥有独立缓冲区,所以文件内容在不一样对象间不保证明时同步。
14. 映射到同一文件的多个映射对象的视图不保证数据的实时同步。
15. MapViewOfFileEx:参数pvBaseAddress非空的时候能够指定映射内存的起始地址。系统映射EXE和DLL的时候就这么干的。
16. 各类跨进程通信手段的通信双方都位于本机时,这些通信方式最终都实现为内存映射文件。
17. 要映射到磁盘文件时,必定要判断CreateFile的返回值,由于若是打开文件失败,INVALID_HANDLE_VALUE句柄会让CreateFileMapping建立映射到PageFile的对象,没有报错倒是歧义。
18. 对应VirtualAlloc那“reserve一大段内存再小块commit”的用法,内存映射文件中实现以下:以SEC_RESERVE为参数CreateFileMapping,以后MapViewOfFile获得reserve的区域,最后确保访问前要先用VirtualAlloc来commit。注意这样commit的共享内存不能VirtualFree。
1. 堆适合分配小内存块,不须要按分配粒度或者页大小对齐。堆在最初只是预约了一块区域,在客户分配时将预约的区域提交,在客户释放后可能反提交。
2. 关于默认堆:GetProcessHeap返回,用户模式代码没法销毁它,在进程结束后由系统销毁。进程能够经过连接选项“/HEAP:reserve[,commit]”来设置默认堆大小。由于默认堆属于进程,因此在DLL中不该设置该连接选项。Windows的ANSI版API向Unicode版转化的时候从默认堆分配字符串缓存,LocalAlloc、GlobalAlloc也从默认堆分配内存。默认堆对外界访问进行了同步,即没有使用HEAP_NO_SERIALIZE标记。
3. 使用独立堆的一些好处:(1)写堆内存出错后,不会影响其余堆的数据。(2)对特定类型数据使用独立堆的话,因为分配块大小相同,具备速度快、无碎片的优势。(3)相关数据使用独立的堆,在访问这些数据时访问的页面更集中,减小PageFault。(4)对特定线程上的逻辑结构使用独立堆,没必要加锁,提升性能。
4. HeapCreate:参数fdwOption,若是在建立堆的时候指定了部分标志(如HEAP_NO_SERIALIZE标志等),之后每次访问堆这些标志都生效;若是建立的时候没有指定,那后续的每次访问能够单独指定标志。 HEAP_NO_SERIALIZE-访问堆的时候不加锁。HEAP_GENERATE_EXCEPTIONS-分配内存失败的时候抛出异常,默认行为是返回NULL。HEAP_CREATE_ENABLE_EXECUTE-能够在堆内存上放置代码来执行。参数dwInitalSize-初始堆大小。参数dwMaximumSize-若是非0,表示若是堆内存使用量达到这个值后再分配会失败;为0,表示堆会自动增大,直到内存用尽。
5. HeapAlloc、HeapSize、HeapFree、HeapDestroy,容易理解。
6. HeapReAlloc:HEAP_ZERO_MEMORY-增大内存时,增长的字节初始化为0。HEAP_REALLOC_IN_PLACE_ONLY-要求不移动起始地址的状况下改变大小,须要增大时若是当前位置剩余空间不足会返回NULL。
7. HeapSetInformation:标记HeapEnableTerminationOnCorruption-Visita以上使用。默认状况下,堆内存被破坏后只在调试器中触发一个断言而后继续执行,这个标记容许发现堆破坏就抛出异常。该标记影响进程中全部堆,没法清空标记。标记HeapCompatibilityInformation-值为2的时候,表示启用低碎片堆(lowfragmentation heap)算法,启用该算法的堆针对内存碎片问题优化有更好的性能。
8. Heap32ListFirst、Heap32ListNext-遍历快照(CreateToolhelp32Snapshot)中的堆。Heap32First、Heap32Next-遍历指定堆中的块。GetProcessHeaps-得到包括默认堆在内的全部堆句柄。HeapValidate-检查指定堆中全部块或者单个块的有效性。HeapCompact-将堆中闲置块合并,并反提交。HeapLock、HeapUnlock-锁定堆。HeapWalk-遍历指定堆中的块,建议先锁堆。
1. Kernel32.dll-管理内存、线程、进程。User32.dll-窗口和消息。Gdi32.dll-绘制图像文字。ComDlg32.dll-经常使用对话框。ComCtl32.dll-经常使用控件。
2. DLL函数分配的内存应该由DLL本身提供的函数释放:主要是针对经过C/C++函数(malloc、new)分配的内存,由于当DLL和DLL的使用者都在引用静态库版本的CRT时(或有一方在引用静态库CRT),多个静态库版CRT中有多份CRT堆的管理数据(全局变量),若是从一个管理器分配资源交给另外一个管理器释放,显然会错误。所以,若是全部模块都使用DLL版CRT就不会有错(由于只有一份全局CRT堆管理数据),或者改用HeapAlloc(GetProcessHeap(),…)也不会错(显然DLL中和EXE中访问到的默认堆是同一个),固然最佳作法仍是DLL同时提供匹配的释放函数。
3. .lib文件中只包含函数、变量和类型的符号名。因为模块中只包含要引用的模块名而没有路径,因此主模块被载入后须要按必定的搜索顺序搜索被引用模块再载入,同时这也意味着修改.lib中的符号名,搜索DLL时也会搜索新名称。
4. DLL的导出段中按符号名顺序列出了导出项,每一项包括符号名和RVA(Relative Virtual Address,用于指出该符号在DLL模块中相对于模块基址的地址)。模块能够包含多个导入段,每一个导入段指出该段要依赖的DLL名以及须要的符号,导入符号对应的实际地址在DLL被载入后填充,其值为DLL基址+RVA。
5. 在为DLL的导出函数指定名称的时候,最好使用.def文件,其次能够选择连接选项#pragma comment(linker, “/export:MyFunc=_MyFunc@”)。
6. dumpbin.exe的/exports可以查看导出段,/imports可以查看导入段。
7. 关于MSVC编译器对符号更名的策略:C语言下默认不改变函数名,所以C++下使用了extern “C”的__cdecl也不会更名。
1. 加载一个DLL,系统至少会干几件事:(1)将不一样段的分页分别映射并赋予不一样的保护属性。(2)检查DLL依赖的其余DLL依次加载。(3)执行DllMain。
2. LoadLibraryEx:dwFlags参数-DON’T_RESOLVE_DLL_REFERENCES-将DLL映射到内存后,对于条款1中的三件事,只作按段分配保护属性这件。LOAD_LIBRARY_AS_DATAFILE-比起上个标志,连三件事中仅剩的一件也省了,只是映射文件,用作数据文件。能够加载EXE而后读取其中的资源。LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE-以独占方式映射数据文件。LOAD_LIBRARY_IMAGE_SOURCE-在AS_DATAFILE的基础上,将导出段的全部RVA转换成VA。LOAD_WITH_ALTERED_SEARCH_PATH-能够调整DLL路径的搜索方式。LOAD_IGNORE_CODE_AUTHZ_LEVEL-安全相关,该安全方案被后来的UAC取代。
3. SetDllDirectory:设置加载DLL时的搜索路径,DLL在搜索进程的当前路径事后就会搜索这里。当路径为空串(””\0”)的时候,表示搜索的时候跳过当前路径,当路径为NULL的时候恢复默认搜索方式。
4. FreeLibraryAndExitThread适用于一个场合:要调用FreeLibrary的代码正是位于DLL中。
5. LoadLibrary和LoadLibraryEx返回的地址不等价,不能混用。如先以LOAD_LIBRARY_AS_DATA_FILE作参数调用LoadLibraryEx,再用LoadLibrary加载同一个DLL,返回值是不一样的。
6. GetProcAddress。
7. 名为DllMain的函数不存在的时候,系统会使用默认入口。
8. DllMain的fdwReason参数:DLL_PROCESS_ATTACH-DLL第一次被加载的时候传入,对于隐式加载的DLL是主线程执行,而显式加载的DLL由LoadLibrary线程执行,用于执行DLL初始化操做。返回FALSE,程序会报错表示加载DLL失败。DLL_PROCESS_DETACH-隐式卸载的时候由主线程执行,显示卸载的时候由FreeLibrary线程执行,负责清理资源。DLL_THREAD_ATTACH-线程在建立时,会检查进程已经加载的DLL,而后依次通知每一个DLL的DllMain函数。进程启动时会先建立主线程,再加载各个DLL,所以这时主线程调用DllMain只会传入DLL_PROCESS_ATTACH而不是DLL_THREAD_ATTACH。DLL_THREAD_DETACH-线程退出的时候检测全部已经加载的DLL依次调用DllMain。
9. DisableThreadLibraryCalls:声明线程在建立和退出的时候不用通知指定DLL的DllMain函数。
10. 全部DLL的DllMain的调用被加载锁(Loader Lock,进程惟一的)序列化了。避免同时建立多个线程以DLL_THREAD_ATTACH调用DllMain时产生竞争。
11. 对于C++编写的DLL,实质上系统通知的是__DllMainCRTStartup,当fdwReason是DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH时,它会调用全局变量的构造或析构函数,而后再调用DllMain。
12. 延迟载入是指直到使用DLL导出的函数时系统才加载DLL和查找函数。优势:加速进程启动、让为高版本系统设计的程序在低版本系统中也能使用部分功能、特殊的设计用途等。部分DLL不能延迟加载:导出了数据的DLL(由于延迟加载利用的是GetProcAddress等功能)、Kernal32.dll。另外在DllMain中也不应使用延迟载入的DLL函数。
13. 延迟加载的使用:在Linker-Input-Delay Loaded Dlls中指定要延迟载入的DLL。若是要Hook延迟加载过程以及停用延迟加载的DLL,须要再导入DelayImp库和开启Linker-Advanced-Delay Loaded Dll的Support Unload。
14. 延迟加载的细节:模块引用的DLL要延迟加载的话,会删除该DLL的idata段,改成包含didata段,对延迟加载函数的调用会跳转到__delayLoadHelper2函数中,该函数会确保该DLL已经被加载,而后检查didata中对应函数的表项是否非空,为空的话用GetProcAddress查找并填充didata项,下次使用就不用再查找。用__FUnloadDelayLoadedDLL2卸载延迟加载DLL,以便以后再次使用延迟函数可以保证正常,该函数会清空didata中已经填充的各项。__pfnDliNotifyHook二、__pfnDilFailureHook2是延迟加载过程的Hook函数指针。
15. 函数转发器:#pragma comment(linker, “/export:SomeFunc=DllA.SomeOtherFunc”)。
16. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs包括一些影响LoadLibrary路径查找的信息。
17. 关于模块基地址重定位:DLL中的代码访问DLL中的全局变量时用的是绝对地址,同时会增长一个reloc段(重定位段)记录全部引用绝对地址的代码,若是DLL最终加载的位置不是默认基址,以前使用的绝对地址须要根据reloc的记录被修正,这就是重定位过程。可见若是进程加载的时候,多个DLL基址发生冲突,须要被重定位,修复绝对地址的操做增长了加载时间,同时也会由于修改Image内存页形成写拷贝,增长了系统的虚拟内存占用。最理想的状况下,全部使用DLL的进程都不须要重定位,这就须要安排合理的基址,能够使用Rebase.exe工具或者ReBaseImage函数。使用dumpbin.exe /headers命令能够查看包括基址的信息。使用/FIXED开关删除reloc段,禁用重定位。
18. 关于模块的绑定:默认状况下,模块的引入段,会在进程加载模块后被填入导入函数的绝对地址,所以包含引入段的模块会发生内存页的写拷贝。使用Bind.exe工具,能够在映像文件中的idata段填入绝对地址和对应DLL的时间戳,当进程加载时,发现被依赖DLL没有被重定位(即基址和默认基址相同)且时间戳和绑定的DLL相同,那么idata段就能够不用修改直接使用绑定值,避免了写拷贝。能够使用Bind.exe工具或BindImageEx函数来绑定模块。绑定操做应该在软件每次升级后执行。
19. 综合讨论重定位和绑定:一个DLL中,引入段最终包含所依赖的DLL的函数地址,若是所依赖的DLL没有被重定位,那引入段不用被修改避免了写拷贝;DLL内部的全局变量是用绝对地址访问,若是DLL自己没有被重定位,这些绝对地址不用被修改也避免了写拷贝。所以用ReBase.exe工具合理安排全部DLL的基址,而后在用Bind.exe工具写入导入函数地址,能提高性能和减小内存占用。
1. 动态TLS:每一个线程都有一个内部DWORD数组用于存放用户数据,MS保证数组至少有TLS_MINIMUM_AVAILABLE(64)个元素。用TlsAlloc申请一个空闲索引,调用TlsSetValue、TlsGetValue时传入这个索引能够访问每一个线程上的用户数组,用TlsFree释放索引,Windows会保证被释放的索引在各个线程上的数据都被清零。DLL中使用动态TLS的标准方式:DLL_PROCESS_ATTACH的时候TlsAlloc一个索引;在DLL_PROCESS_DETACH的时候用TlsFree释放;在DLL功能函数内部检测TlsGetValue返回的指针是否为空,为空的话分配一块内存包含DLL要使用的全部线程相关数据;在DLL_THREAD_DETACH中检测TlsGetValue返回值非空则释放掉。
2. 静态TLS:声明为__declspec(thread)的静态变量会保存在模块的tls段中, 每一个线程在建立的时候会根据当前全部模块的tls段总大小分配一块内存与线程对象关联,这块线程相关内存的大小也会随LoadLibrary、FreeLibrary增删包含tls段的DLL进行调整。静态TLS只在Vista以上才被完美实现。考虑这样一种实现:每一个模块都有一个动态TLS索引(__tls_index),每一个线程的该索引下保存的是malloc出来的特定模块的tls段数据,能够认为系统是经过1节中描述的惯用法实现静态TLS的。
1. 利用注册表注入DLL:HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\下的AppInit_Dlls能够填一系列要加载的DLL,仅当LoadAppInit_Dlls为1的时候。工做原理是:任何GUI程序在加载User32.DLL时,它的DllMain会先尝试加载注册表项AppInit_Dlls中的DLL。所以这种方法会影响全部的GUI程序。
2. 利用钩子来注入DLL:以参数WH_GETMESSAGE来调用SetWindowsHookEx,线程ID为0的时候会注入系统中全部有消息循环的进程。
3. 利用远程线程注入DLL:用CreateRemoteThread在目标进程中建立一个线程,线程函数就是LoadLibrary,线程函数的参数是要注入的DLL名。具体步骤:(1)用VirtualAllocEx在目标进程中分配内存,用WriteProcessMemory写入要注入DLL名的字符串,这样就在目标进程地址空间中准备好了参数(2)经过GetProcAddress获得LoadLibrary的地址。能够利用一般全部进程中Kernel32.DLL的基地址相同这个实时,把本进程的LoadLibrary地址当作目标进程中该函数的地址。(3)调用CreateRemoteThread,线程函数是LoadLibrary。(4)WaitForSingleObject来等待远程线程结束,表示加载完毕,而后VirtualFreeEx释放装有模块名的地址。(5)最后以任何方式在目标进程中释放注入的DLL。好比以 FreeLibrary为线程函数调用CreateRemoteThread。
4. 利用转发器替换DLL:要用本身的A.DLL替换合法的B.DLL,先用转发器在A.DLL中转发全部的函数到B.DLL,再实现本身的功能,最后将A.DLL更名为B.DLL,本来的B.DLL改为其余名。也能够在A.DLL中转发后,修改依赖B.DLL的模块的引入表,将它依赖的DLL名改成A.DLL,这种方式避免了更名。
5. 利用CreateProcess注入DLL:父进程用CreateProcess建立子进程时暂停子进程的主线程,而后查询子进程的入口函数(main),将入口函数的头几个字节改成跳到注入代码,而注入代码的末尾会跳转回入口函数开头。
6. API钩子的两种实现方式:(1)将原函数入口处的代码改成“跳转到挂钩函数;原代码B”,这样原函数的调用都会跳转到挂钩函数;为了可以访问原函数的功能,另外准备一个可执行缓冲区,内容为“原代码A;跳转到原代码B”。(2)修改进程中全部模块的引入表,将全部引入表中指定DLL的指定函数项地址改成挂钩函数地址。注意进程调用LoadLibrary后会引入新的DLL及其依赖DLL,所以须要再遍历一次全部DLL修改引入表。有必要的话也挂钩GetProcAddress返回伪造地址。用到的函数有ImageDirectoryEntryToData,能够查询指定DLL的引入表地址。
1. __finally块可能因为三种缘由被执行:(1)正常执行完__try块。(2)控制流中断__try块(这种情形叫局部展开,local unwind)。包括continue、break、return、goto、longjump等。(3)发生异常中断__try块,系统正在进行全局展开。包括SEH中的硬件异常(除0、写非法内存)和软件异常(RaiseException)。在__finally块中要区分是(1)或者(2)(3),能够用AbnormalTermination,该函数返回TRUE表示是(2)或(3)。(1)没有额外开销;(2)的开销较大,建议用__leave替代;(3)是正经常使用途。
2. 当 __finally块是因为__try块中的return被执行时,若是再在__finally块中调用return,最终函数会返回后一个return的值。
3. __finally中的return能够中断全局栈展开(global unwind)。即线程不会从__except的处理代码块继续执行(尽管exception filter返回的是EXCEPTION_EXECUTE_HANDLER),而是从return的__finally的上一级函数继续执行。
4. 不建议在__try或__finally块中使用return、goto等控制流语句。
1. __try、__except组合主要有filter块(__except后括号中的语句)和handler块(__except后的{}块),它们只在发生结构化异常的时候有可能被执行。发生结构化异常后,系统首先将异常信息(GetExceptionInformation返回值)压入栈顶,而后调用VEH注册函数(见25章),再执行最近的filter块,若是filter块返回EXCEPTION_CONTINUE_SEARCH,则继续查找下一个filter,这个过程当中包括异常发生点和异常信息的整个栈一直无缺(即GetExceptionInformation返回值有效),直到某个filter返回非EXCEPTION_CONTINUE_SEARCH。若是filter块返回EXCEPTION_CONTINUE_EXECUTION则从异常点继续执行,若是返回EXCEPTION_EXECUTE_HANDLER则先进行全局展开再执行handler块。全局展开会形成异常信息失效和从内到外的__finally块逐个执行,即handler中GetExceptionInformation返回值会失效,且若是某个__finally经过return中断全局展开,handler块将不执行。若是用户编写的全部__except都返回EXCEPTION_CONTINUE_SEARCH,最终系统将执行MS编写在系统线程函数中的最顶层__except的filter块,即UnhandledExceptionFilter,这个过滤函数25章讲。
2. 关于EXCEPTION_CONTINUE_EXECUTION:对于硬件异常(除0、内存非法访问等CPU异常),会从触发异常的那句汇编语句开始执行。对于软件异常,会从RaiseException的下一句汇编开始执行。即filter返回该值可能让硬件异常循环触发,而软件异常只会触发一次。
3. GetExceptionCode只能出如今__except后的filter块或者handler块中,而不能出如今filter函数中,这由编译器保证。GetExceptionInformation一样不能出如今filter函数中,但也不能出如今handler块中,根据条款1中的描述,当系统执行handler块时,全局展开已经结束,异常触发点到handler点之间的栈帧已经失效,固然异常信息也已经失效。
1. C++异常机制是由SEH实现的,C++的全部异常都是以EXCEPTION_NONCONTINUABLE为参数调用RaiseException抛出的软件结构化异常。因为EXCEPTION_NONCONTINUABLE只限制__except的filter块,因此VEH函数返回EXCEPTION_CONTINUE_EXECUTION来忽略C++异常是合法的。
2. 不管是C/C++线程(_beginthreadex)仍是Windows线程(CreateThread),它们的内部线程函数都将用户线程函数放在一组__try、__except中,当异常发生后全部的用户filter都返回EXCEPTION_CONTINUE_SEARCH时,系统将执行最外层的filter即UnhandledExceptionFilter(一个系统API),若是该函数发现当前进程正在被调试则将控制权交给调试器然后者会中断进程;非调试状态下它会尝试取出用户经过SetUnhandledExceptionFilter注册的顶层过滤函数,若是用户顶层函数返回EXCEPTION_EXECUTE_HANDLER或EXCEPTION_CONTINUE_EXECUTION,则UnhandledExceptionFilter再也不进一步处理。显然用户能够经过返回前者来记录日志并没有声退出,而返回后者能够实现相似栈内存经过guard page自动commit的功能。若是用户顶层过滤函数也返回EXCEPTION_CONTINUE_SEARCH,则进程再尝试调用经过AddVectoredContinueHandler注册的VEH函数,全部VEH函数都返回EXCEPTION_CONTINUE_SEARCH的话,系统就建立一个子进程并等待,子进程显示对话框询问用户要结束进程仍是附加调试器,等待结束后异常的进程要么退出要么已经被调试器附加。
3. SEH实现的理解:有一个叫SEH栈的容器被用来维护相关数据,栈的每一项多是一个__try/__except或__try/__fianlly组合;线程进入一个__try块就往SEH栈中压项,退出__try块就从SEH栈弹出一项。若是发生异常,系统判断离栈顶最近的__except项的filter返回值,若是其返回值为EXCEPTION_CONTINUE_SEARCH,则系统继续从栈顶往栈底查找__except项并执行其filter。若是找到一个filter返回EXCEPTION_CONTINUE_EXECUTION则流程结束,同时SEH栈保持不变;若是某个filter返回EXCEPTION_EXECUTE_HANDLER,则系统将SEH栈栈顶到该__except项的每一项都出栈,弹出的过程当中若是发现__finally项则执行其中的代码块。
4. VEH(Vectored Exception Handler,向量化异常处理),做为SEH的补充,能够经过AddVectoredExceptionHandler、RemoveVectoredExceptionHandler管理一组异常过滤函数,这组函数将在异常发生以后到用户filter被执行以前的这段时间被调用,它能够返回EXCEPTION_CONTINUE_SEARCH让系统执行下一个VEH函数或者用户filter;也能够返回EXCEPTION_CONTINUE_EXECUTION起到忽略异常的效果。这组回调的特殊调用时机能够用于实现异常Hook等。另外还能够经过AddVectoredContinueHandler、RemoveVectoredContinueHandler管理一组过滤函数,由UnhandledExceptionFilter在用户顶层过滤函数(SetUnhandledExceptionFilter)以后调用。
5. 两种状况下调试器会通知用户发生异常:(1)打开IDE相应开关后,一抛出异常就触发断点。另外,不管是否打开开关,调试器都在输出窗口打印异常相关信息。调试器显然经过AddVectoredExceptionHandler注册了VEH函数。能够模仿调试器来记录异常。(2)对于用户没有处理的异常,被调试状态下的UnhandledExceptionFilter内部会通知调试器。
6. 对异常发生时调试器弹出框的解释:(1)中断。保持中断的状态,便于调试。(2)继续。若是对话框在VEH函数中弹出,即异常刚抛出,这个选项会让VEH函数返回EXCEPTION_CONTINUE_SEARCH,继续查找下个处理函数。若是对话框是在UnhandledExceptionFilter中弹出,继续选项等价于忽略。(3)忽略。VEH过滤函数或UnhandledExceptionFilter中代码返回EXCEPTION_CONTINUE_EXECUTION,所以该选项用于忽略包括C++异常在内的软件异常(RaiseException)。
1. 本章介绍的WER(Windows Error Reporting,Windows错误报告)内容主要在Vista以上可用。
2. %SystemRoot%\system32\wercon.exe能够显示系统中出现过的错误。
3. WerSetFlags能够影响WER的行为,好比要求不dump堆、发送报告到MS网站等。WerAddExcludedApplication能够指定一些程序崩溃后跳过WER机制,适合正在调试的程序等。
4. WebRegisterMemroyBlock-指定WER的dump数据中要包括指定位置的内存。WerRegisterFile-要求将指定文件加入报告中。
5. 定制WER报告:WerReportCreate、WerReportSetParameter、WerReportAddDump、。WerReportAddFile、WerReportSetUIOption、WerReportSubmit、WerReportCloseHandle。
6. RegisterApplicationRestart-能够指定在何种错误状况下WER以特定参数重启程序。
7. RegisterApplicationRecoveryCallback-注册一个回调,进程将要非正常结束的时候被调用,以便用户自由备份一些状态等。用户能够在回调中以ApplicationRecoveryInProgress、ApplicationRecoveryFinished来通知UI进度。
1. 要在输出窗口中打印调试信息,区别#pragma message和OutputDebugString,前者是在编译期打印,后者是运行时打印。
2. 因为MS提供的函数DebugBreak会断点在Kernel32.DLL中,须要两次才能单步到下一行(第一次跳出Kernel32.DLL);而__asm int 3;是断点在用户代码中,更容易使用。二者都只适合调试器存在时,非调试状态这个断点异常没法捕获会崩掉程序。
3. 本身编写发布版本也有效的断言:VERIFY。
4. #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls'…”) 使GUI程序可以自动查找正确版本的ComCtrl32.DLL来自绘,达到自适应系统主题(Theme)的效果。
5. WindowsX.h中包含一些简单函数便于操做窗口,分别是消息处理宏、子控件宏和API宏。
1. 一个进程能够建立上万个用户对象(User Object)。内核对象属于内核,能够跨进程使用,不会随任何进程自动删除;图符、光标、窗口类、菜单、加速键表等用户对象属于进程,容许跨线程访问,进程结束后自动删除;窗口、挂钩两种用户对象属于线程,拥有者线程结束后自动删除。
2. 线程的内部数据结构THREADINFO中至少包括如下内容:Post来的消息队列、Send来的消息队列、Send的应答队列、ExitCode、激活标志、消息队列状态标志(QueueStatus)、虚拟输入队列(VIQ)、局部输入状态(鼠标/键盘焦点窗口、光标外形和可见性等)。
3. PostMessage、PostThreadMessage、PostQuitMessage、GetWindowThreadProcessId。
4. SendMessage发送消息时,若是目标窗口位于发送线程,则SendMessage内部直接调用窗口过程并返回,若是目标线程不是当前线程甚至位于其余进程,SendMessage往目标线程的Send消息队列内添加项事后(并设置QS_SENDMESSAGE),用MsgWaitForMultipleObjects等待处理完成通知(同时还处理本线程消息)。而SendMessageTimeout则包含等待Send处理完毕、处理本线程消息、检测超时三项功能,传入SMTO_BLOCK参数后只进行有超时的等待而不处理消息。
5. SendMessageCallback的目标线程是当前线程时,直接调用窗口过程并用回调通知;若是目标线程是其余线程,Send消息后直接返回,以后本线程应该用GetMessage来响应其余线程Post回来的Send处理完毕的通知,该通知的处理函数会调用注册的回调。SendNotifyMessage至关于回调为空的SendMessageCallback,它不关心完成通知,相比PostMessage它仍是具备Send消息的一些特色:比Post消息优先处理、目标线程是当前线程时直接调用窗口过程。一种获取全部窗口句柄的方法:以HWND_BROADCAST 为参数调用SendMessageCallback,而后GetMessage、DispatchMessage处理回调,在回调中搜集全部的窗口句柄。
6. SendMessage会在目标线程不是当前线程时阻塞等待,为避免没必要要的阻塞发送线程,消息处理函数一旦肯定处理结果就能够立刻调用ReplayMessage传入结果值来激活Send线程,处理函数后半段即便进行费时操做也再也不干扰Send线程(处理函数的返回值也被忽略)。
7. InSendMessage能够在消息处理过程当中判断当前线程是不是Send线程(线程不一样返回TRUE)。InSendMessageEx还能够判断抛出消息的具体函数,以及当前是否已经Reply告终果。
8. GetQueueStatus,检测当前线程的消息队列状态,是否有Post消息、Send消息、虚拟输入、以及QS_QUIT、QS_TIMER等特殊标志。
9. TranslateMessage在遇到WM_KEYDOWN/WM_SYSKEYDOWN时,会Post一个WM_CHAR/WM_SYSCHAR。所以若是使用了TranslateMessage,消息的处理顺序会变成WM_KEYDOWN->WM_CHAR->WM_KEYUP。
10. GetMessage/PeekMessage内部算法:先判断线程消息队列状态是否有QS_SENDMESSAGE标志,若是有则从Send队列取消息并处理但不返回(即GetMessage内部检测到Send的消息后,会ReplayMessage(DispatchMessage(msg));而若是将Send消息交给用户代码来Dispatch,后者可能忘记须要答复发送线程);再判断是否有QS_POSTMESSAGE,若是有则从Post队列取消息填充MSG结构而后返回(所以,用户经过MSG结构从GetMessage处取得的消息只能是Post的消息);判断是否有QS_QUIT标记,若有则表示已经PostQuitMessage因而填充MSG结构并返回(所以即便先PostQuitMessage再Post用户消息,也能保证退出前用户消息被处理)。再判断是否有QS_INPUT标志,若是VIQ中有输入则填充MSG结构并返回(所以即便有输入也能够退出且若是有输入则不重绘)。判断是否有QS_PAINT标志,有则表示窗口仍然有脏区域(直到BeginPaint)因而填充MSG产生一个WM_PAINT消息。最后判断是否有QS_TIMER标志,若有则表示刚到时,因而移除标志并填充MSG结构返回。能够看见有几种消息被赋予了至关低的优先级,并不加入消息队列:WM_QUIT是为了保证退出前处理完全部普通消息;WM_PAINT是由于开销大,只在空闲时处理;WM_TIMER是为了不处理慢触发快而致使消息队列溢出。
11. MsgWaitForMultipleObjects实现为,在事件对象数组后追加一项,若是要检测的消息队列标志被置位则触发新追加的事件对象。关于输入消息的监听,因为MS设计为只在新增输入消息时事件对象才触发,所以须要以MWMO_INPUTAVAILABLE为参数来调用MsgWaitForMultipleObjectsEx,达到一旦输入队列非空就触发的效果。另外MsgWaitForMultipleObjectsEx还支持WaitAll及APC等功能。
12. 对于跨进程用SendMessage发送WM_GETTEXT、WM_SETTEXT等消息,系统会自动使用共享内存来转换消息参数的地址值以跨越进程边界。显然用户自定义消息须要本身来处理跨进程问题。WM_COPYDATA能够用来跨进程发送数据,发送进程传入一个有数据的缓冲,接受进程获得的缓冲地址转而指向一块相同内容的共享内存,系统在SendMessage返回时释放共享内存(故这个消息只能Send)。
13. 任意一个窗口都有编码属性,这个属性在绑定消息处理函数时肯定(即调用RegisterClassA或以GWLP_WNDPROC调用SetWindowLongPtrA表示这是一个ANSI窗口而不是Unicode窗口),经过系统在不一样窗口间转发数据时,系统会自动进行编码转换。判断窗口的编码IsWindowUnicode。
14. 对GetKeyState和GetAsyncKeyState的理解:线程的局部输入状态中有一份键盘状态表,在处理每一个键盘消息的时候更新。GetKeyboardState获取整个表,GetKeyState获取某个表项,因为键盘消息不必定可以及时处理,所以内部表不必定够新,要得到实事状态,用GetAsyncKeyState,该API经过硬件中断得到最新按键状态。考虑一种GetAsyncKeyState的实现:线程先设置中断函数,再等待一个事件,中断到来时发现该线程中断函数指针非空因而执行函数,函数内部查询最新按键状态而后触发事件,中断结束后线程从等待的事件中被唤醒,最后返回按键状态。
1. 系统启动后建立RIT(Raw Input Thread,原始输入线程),它维护一个结构叫SHIQ(System Hardware Input Queue,系统硬件输入队列),鼠标键盘的硬件驱动将各自的消息添加到SHIQ中,若是消息是鼠标消息,RIT就检测当前光标下方的窗口,而后将鼠标消息抛到该窗口建立线程的VIQ中(Virtual Input Queue,虚拟输入队列),除非某个窗口调用了SetCapture,则RIT把鼠标消息抛给捕获窗口所在的线程;若是是键盘消息,RIT将消息抛给前台窗口所在的线程的VIQ中,前台窗口由SetForegroundWindow设置或者用户经过Alt+Tab/Alt+Esc/Ctrl+Alt+Del激活,线程接到键盘消息后根据局部输入状态将消息交给焦点窗口。