您好,欢迎来到汇意旅游网。
搜索
您的当前位置:首页Windows编程基本手册

Windows编程基本手册

来源:汇意旅游网


6.3 注意点

1.路径分隔符是反斜杠\\,但是在CreateFile等其他低级的API中正斜杠也可以用,最好避免造成不兼容性,目录和文件名大小不敏感,但是大小写保持,路径名最大为MAX_PATH 260长,但可以通过转义字符指定非常长的名称,如加上\\ \\ ?及使用Unicode字符避开这个,可以长达32K个字符的名称

2. Unicode 字符集,使用#define _UNICODE 必须在前语句给出,默认使用的是8位字符,L使用的是16位字符,_T使用通用文本字符,包含tchar.h。1.在所有的头文件之前加入#define UNICODE 和#define _UNICODE #include

2.tchar.h中的通用C库中没有memchr

_fgettc,

_itot替代itoa

_stprintf替代sprintf

_tcscpy替代strcpy

_ttoi,_totupper,_totlower,以及_ftprintf(输出到文件)

3.系统保留CONIN$和CONOUT$为控制台的输入输出,或直接使用GetStdHandle()

4.CopyFile(lpExistingName,lpNewFileName,fFailifExists)如果已经有使用新名称

的文件存在,那么只有在fFailIfExits等于FALSE 时这个文件才会被替换,CopyFile也复制文件的元数据,比如创建时间

5.MoveFile如果新文件已经存在的话,一个进程在一个时间中只能有一个控制台

6.利用va_list实现可变参数

va_list arg_ptr:定义一个指向个数可变的参数列表指针;

va_start(arg_ptr, argN):使参数列表指针arg_ptr指向函数参数列表中的第一个可选参数,说明:argN是位于第一个可选参数之前的固定参数,

(或者说,最后一个固定参数;…之前的一个参数),函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。

如果有一va函数的声明是void va_test(char a, char b, char c, …),

则它的固定参数依次是a,b,c,最后一个固定参数argN为c,因此就是va_start(arg_ptr, c)。

va_arg(arg_ptr, type):返回参数列表中指针arg_ptr所指的参数,返回类型为type,并使指针arg_ptr指向参数列表中下一个参数。

va_copy(dest, src):dest,src的类型都是va_list,va_copy()用于复制参数列表指针,将dest初始化为src。

va_end(arg_ptr):清空参数列表,并置参数指针arg_ptr无效。说明:指针arg_ptr被置无效后,可以通过调用va_start()、va_copy()恢复arg_ptr。

每次调用va_start() / va_copy()后,必须得有相应的va_end()与之匹配。参数指针可以在参数列表中随意地来回移动,但必须在va_start() … va_end()之内。

3. 控制台输入CONIN$ CONOUT$

4. 进程之间共享数据的最好方法是使用内存映射

5. ExitProcess或ExitThread无法保证正确的清除对象,在入口函数中调用ExitThread只会使主线程停止,如果进程中还有其他线程运行,进程不会终止

6. TerminateProcess函数,这种方式终止不会得到任何有关终止运行的通知,终止之前不会将内存中它所拥有的任何东西写回磁盘。该函数是异步执行的,它会告诉系统需要某进程运行终止,但是函数返回时,无法保证这个进程已经终止。如果要确切知道是否已经终止运行要调用WaitForSingleObject函数。

7. 所有线程终止,会将进程的退出代码设为最后一个终止运行的线程的终止代码。

进程内核对象的存在时间可能大大超过进程的存在时间,即使进程已经终止运行,这些信息也可能还是有用的,可以通过GetExitCodeProcess获得目前已经销毁的进程退出代码

如果调用这个函数时进程尚未终止,返回一个STILL_ACTIVE标识符,否则返回数据退出代码值。值得注意的是调用CloseHandle会让系统停止维护进程的统计数据。

8. 如果以二进制方式打开,文本方式写,文件的末尾写入会出错,如果以文本方式打开,二进制方式写,文件只能读入一部分的内容

6.4进程

进程是由

<1>一个私有的虚拟地址空间,即进程可以使用的一组虚拟内存地址

<2>一个可执行程序,定义了初始的代码和数据,映射到进程的虚拟地址空间中

<3>一个已打开句柄的列表,指向各种系统资源,比如信号量,通信端口和文件,该进程中的所有线程都可访问这些系统资源

<4>一个被称为访问令牌的安全环境,标识了与该进程关联的用户,安全组和特权

<5>一个被称为进程ID的惟一标识符(在内部被称为客户ID)

<6>至少一个执行线程

对线程句柄的关闭并不会终止线程,CloseHandle函数只是删除对CreateProcess进行调用的进程内的该线程的引用 ,Windows不维护进程中的父子关系.父子进程可能使用不同的文件指针来访问相同的文件,关闭子进程的句柄不能销毁子进程,只销毁父进程对子进程的访问权.

1. 复制句柄,计数代表引用一个对象的不同句柄数量,但它对应用程序不可用,在最后一

个句柄被关闭而且引用计数变成零之前,对象不能被销毁.

2. 使用GetProcessTimes或GetThreadTimes获得运行时间

6.5线程

最基本的部件

<1>一组代表处理器状态的CPU寄存器的内容

<2>两个栈,一个用于当线程在内核模式下执行的时候,另一个用于线程在用户模式下执行的时候

<3>一个被称为线程局部存储区(TLS,thread-local storage)的私有存储区域,各个子系统,运行库和DLL都会用到该存储区域

<4>一个被称为线程ID的惟一标识符(在内部也被称为客户ID-进程ID和线程ID是在同一个名字空间中生成的,所以它们永远不会重叠)

<5>线程也有自已的安全环境,如果多线程服务器应用程序要模仿其客户的安全环境,则往往可以利用线程的安全环境.

易失的寄存器,栈和私有存储区域合起来被称为线程的环境(context),因为这些信息随着Windows所在机器的体系结构不同而有所不同,这些数据结构必须是与底层体系结构相关的,通过GetThreadContext可以访问这一信息

一个进程中的线程不可能直接引用另一个进程的地址空间,除非

<1>第二个进程将它的一部分私有地址空间变成共享内存区(文件映射对象)

<2>第一个进程有权打开第二个进程,可以使用ReadProcessMemory和WriteProcessMemory等跨进程的内存函数

在默认情况下,线程没有自已访问令牌,也可以包含一个访问令牌,因此单独的线程可以模仿另一个进程的安全环境—包括远程Windows系统上运行的进程,而不会影响当前进程中的其他线程

6.6虚拟内存

对于映射非常大的数据库,在32位系统上,Windows提供了地址窗口扩展(AWE,Address Windowing Extension)的机制,使得32位应用程序可以申请多达GB物理内存,然后将内存视图或者窗口映射到它的2GB虚拟地址空间中

6.7内核模式和用户模式

内核模式的操作系统和设备驱动程序共享同一个虚拟地址空间,一旦进入内核模式,操作系统和设备驱动程序的代码可以完全访问系统空间的内存,也可以绕过Windows的安全机制直接访问对象,用户模式到内核模式的切换不会影响线程的调度,大部分图形和窗口系统运行在内核模式下,图形密集的应用程序花在内核模式下的时间比用户模式下的时间多,当Windows无事可做时,它运行在内核模式上.

6.8注册表

注册表是一个反映内存中易失数据的窗口,比如系统中当前硬件的状态(哪些设备驱动程序已经加载到系统中了,用了哪些资源)以及Windows性能计数器,性能计数器不位于注册表之中,可以通过注册表函数来访问

6.9用户模式调试

<1>侵入式

通过Windows的DebugActiveProcess,可以检查或修改内存,设断点,可以断开调试连接

<2>非侵入式

利用OpenProcess打开被调试进程,使得你可以检查和修改目标进程中的内存,不能设置断点,

6.10可移植性

<1>分层设计,系统低层是与处理器体系结构相关的,内核(ntoskrnl.exe)和硬件抽象层(HAL,包含在Hat.dll中)

<2>绝大部分代码是用C写的,少部分是用C++写的,只有那些需要直接与系统硬件通信的部分(比如中断陷阱处理器,interrupt trap handler),或者对性能极端敏感的部分(如环境切换,context switching)才用汇编写

6.11 Windows子系统

<1>环境子系统进程(csrss.exe)

控制台窗口,创建或删除进程和线程,16位虚拟DOS机(VDM)进程的一部分支持,

<2>内核模式设备驱动程序(Win32k.sys)

窗口管理器(window manager),图形设备接口,

<3>子系统DLL,将已档化的Windows API函数翻译成ntoskrnl.exe和Win32k.sys中恰当的且绝大多数未文档化的内核模式系统服务调用.

6.12系统进程

<1>idle进程,每个CPU一个线程,占用空闲的CPU时间

<2>System进程,包含大多数内核模式系统线程

<3>smss.exe 会话管理器

<4>csrss.exe Windows子系统

<5>Winlogon.exe 登录进程

<6>services.exe 服务控制管理器和它创建的子服务进程(比如通用服务宿主进程svchost.exe)

<7>lsass.exe 本地安全认证服务器

每个Windows线程都是从一个从系统提供的函数开始执行的,所以每个Windows进程的0号线程的启动线程都是相同的,

6.13停机

<1>如果某个进程调用ExitWindowsEx发出停机命令,会有一个消息被发送到给Csrss,指示它执行停机处理,Csrss给Winlogon的一个隐藏窗口发送一个Windows消息,说明要执行系统停机命令,然后WinLogon模仿当前用户的身份,用一些内部标识来调用ExitWIndowsEx,再一次,此调用导致一个消息被发给Csrss,请求执行系统停机命令.

这一次Csrss看到请求来自Winlogon,它按照进程的停机级别的反序,来经历当前交互用户的登录会话中的所有进程,每个进程可以调用SetProcessShutdownParameters指定一个停机级别,向系统表明相对于其他进程希望什么时候退出,有效的停机级别位于0-1023,默认级别为0,例如,explorer(默认系统外壳程序)它的停机级别为2,任务管理器停机级别为1,对于每个拥有顶级窗口的进程,csrss给该进程包个包含Windows消息循环的线程发送一个WM_QUERYENDSESSION消息,如果返回TRUE ,系统停机过程继续,然后Csrss给该线程发送WM_ENDSESSION,请求它退出 ,Csrss等待一定的毫秒值,默认为5000毫秒.

<2>如果该线程在超时到期之前还未退出,则csrss显示一个终止程序对话框,表明一个程序未能及时停止,让用户选择杀死进程还是取消停机

<3>一旦该进程中所有拥有窗口的线程都已经退出了,则csrss终止该进程,并转向当

前交互式会话的下一个进程,

<4>当用户停机时,许多系统进程仍然在运行,比如smss,winlogon,SCM,lsass,

<5>一旦csrss完成了向系统进程传达停机的通知,Winlogon最后调用NtShutdownSystem,从而结束停机过程,NtShutdownSystem,从而结束停机过程,NtShutdownSystem函数又调用NtSetSystemPowerState函数,来协调设备驱动程序和执行体子系统其余部分,NetSetSystemPowerState调用I/O管理器,以便给那些已经请求过停机通知的所有设备驱动程序发送停机I/O包,使得设备驱动程序有机会在Windows退出以前执行任何必要的处理,配置管理器将任何修改过的注册表刷新到磁盘上,内存管理器将所有已修改过的且包含了文件数据的页面写回到它们各自的文件中,I/O管理器会再次被调用,以便告诉文件系统驱动程序,系统正在进行停机,系统停机过程最终会在电源管理器中结束,电源管理器所执行的动作取决于用户指定的是停机,还是关闭。

6.14缓存

为缓存分配更多的虚拟内存,可以导致越少的解除映射和重新映射操作。

6.15崩溃

原因

<1>运行在内核模式下的设备驱动程序或者操作系统函数引发了一个未被处理的异常,比如内存访问违例 (企图写一个只读页面)

<2>调用一个内核支持例程,导致一次重新调度,当中断请求级别为DPC级别时等待一

个处于无信号状态的分发器对象

<3>在DPC级别的IRQL时,在由页面文件或内存映射文件中的数据上发生了一个页面错误(要求内存管理器等待一个I/O操作发生,但在DPC/Dispach级别或更高级别上不能够进行等待,因为那要求一次重新调度。

<4>一个设备驱动程序或操作系统函数显式地使系统崩溃(调用KeBugCheckEx),因为它检测到一个内部条件表明数据受到了破坏,

<5>发生硬件错误,比如机器检查或者不可屏蔽中断,

崩溃的大部分原因是第三方驱动.,KeBugCheckEx调用任何已注册的设备驱动程序错误检查回调函数,从而让驱动程序有机会停止它们的设备.

七.Windows文件系统和字符I/O

1.FAT不支持Windows安全性,FAT是惟一支持软盘的文件系统,而且出现在存储卡上.

2.在CreateFile和其他低级API的路径名参数中/也可以用,但最好还是使用\\\\

3.目录和文件是大小写不敏感,但是大小写保持

4.用于API函数参数的文件和目录名和长度可以多达255个字符,而路径名的是MAX_PATH(260)个字符,使用转义序列也可指定非常长的名称.

5.一个点和两个点也是目录名.

6.CreateFile中的属性FILE_ATTRIBUTE_READONLY 应用程序既不能写也不能删除该文件

FILE_FLAG_DELETE_ON_CLOSE 对临时文件有效,当最后一个打开的HANDLE被关闭时,Windows会删除这个文件

7.WriteFile写文件,如果HANDLE的当前位置加上写字节数超过当前文件长度,那么Windows将扩展文件的长度

八.高级文件,目录处理与注册表

8.1使用重叠结构来指定文件位置

1.RegOpenKeyEx打开一个命名的项

2.RegEnumKeyEx枚举一个打开的注册表项的子项名

3.RegCreateKeyEx 创建新项

4.RegEnumValue 枚举某个打开项下面的值名称及相关数据

5.RegSetValueEx 设置一个打开项中某个命名值的相关数据

九. 异常处理

使用_try和_catch这样的关键字

十. 内存管理,内存映射和DLL

10.1 堆

每个进程有其自已的堆,GetProcessHeap(),使用不同的堆有多种好处,

1. 内存映射文件,直接将虚拟内存空间映射到正常的文件中,比使用ReadFile和WriteFile这些文件访问函数快得多,无需缓冲区,共享内存,堆中的动态内存必须物理地分配在一个分页文件中,操作系统会控制页面在物理内存和分页文件之间移动,并且将进程的虚拟地址空间映射到分页文件中,当进程终止时,文件中的物理空间会得到释放.

2. 文件映射对象,在一个打开的文件之上映射一个有句柄的Windows内核文件映射对象,然后将文件的所有部分映射到进程的地址空间上,可以对文件映射对象命名,以便其他进程也可访问,从而实现共享内存

3. 映射文件与最好的顺序文件处理技术相比,性能改进可达3:1以上,对于超过一半物理内存的文件来说性能优势不存在

4. <1>CreateFileMapping,OpenFileMapping

<2>MapViewOfFile

<3>UnmapViewOfFile关闭映射句柄

<4>FlushViewOfFile强制将修改过的数据写入磁盘,因为映射的内存不会立刻被写入文件,再通过传统文件的访问方式(如ReadFile和WriteFile)将得不到文件的连贯性视图,

而直接通过共享内存可以保证连贯性

5. 在Win32中文件映射对于2GB-3GB的大文件不可能将整个文件映射到虚拟内存空间中,如果在映射文件中使用了带有指针的数据结构,那所有指针都将与由MapViewOfFile所返回的虚拟地址有关,解决方案就是使用基指针,另一个指针的相对实际偏移量.

LPTSTR pInFile=NULL;

DWORD _based(pInFile) *pSize;

TCHAR _based(pInFile) *pIn;

10.2动态链接库

DLL应该提供版本信息如Utility_4_0.DLL

十一.线程和调度

XP和2003加入的管理函数有

1. GetProcessIdOfThread(仅2003),从线程的句柄找出进程ID,在管理线程或与其他进程的线程交互的程序中可使用这个函数.如果需要的话,可以使用OpenProcess来获取进程句柄.

2. GetThreadIOPendingFlag,确定由句柄所标识的线程是否有任何未完成的I/O请求,如线程可能被阻塞于ReadFile操作上,返回的结果是该函数执行时的状态,在任何时候如果

目标线程完成或者初始化一个操作,则实际状态可以改变.

3. 每个线程都有一个挂起计数,SuspendThread递减挂起计数,

4. 在线程中使用C库,其并不是线程安全的,如strtok函数,

5. 多线程不只在多处理器有较好的性能,在多个磁盘驱动器或者存储系统中有其他并行能力的情况下也可提升性能.在这种情况下,对不同文件的多I/O操作可并发运行.

6. 小心使用高线程优先级和进程优先级,除非有确定的需要,否则最好不用,避免在正常的用户进程中使用实时优先级,内核总是运行准备好执行的线程中优先级最高的那一个,线程接收与其进程优先级类相关的优先级,Windows不是一个实时操作系统,使用REALTIM_PRORITY_CLASS 会妨碍其他重要线程运行.进程可以更改或确定自已的优先级,只要安全权限足够,也可更改或确定其他进程的.线程优先级会随着进程优先级自动改变,

Windows可根据线程的行为动态调整线程优先级.使用SetThreadPriorityBoost函数可以启用或禁用这一功能.

7. 如果一个运行中的线程的时间片到期但该线程没有等待操作,则执行机构将把它转移到准备好状态,执行Sleep(0)也会将线程从运行中状态转移到准备好状态.

8. 不要对父线程和子线程的执行顺序作任何假设,子线程在父线程调用CreateThread返回之前就运行完成是有可能的,反之,子线程也可能很长时间都不运行.

9. 确保子线程所需要的所有初始化在CreateThread调用之前都已完成,否则要使用线程挂起,父线程未能初始化子线程所需的数据是造成“竞态条件”的常见原因.

10. 确认每个不同的子线程都从线程参数中传递,不要假设子线程会在另一个线程之前完成

11. 任何线程在任何时间都可能被抢占或恢复

12. 不使用线程优先级来代替显式的同步

13. 确保线程有足够大的堆栈,默认的1MB通常已经足够

14. 除了显然是并发的才使用多线程以外,其他的只会增加复杂性和性能开销

15. 使用大量的线程大量的堆栈会将消耗虚拟内存空间

16. Sleep(0)会导致线程放弃时间片的剩余时间,内核将线程从运行中状态转移到准备好状态.

有在线程准备好运行的情况下,SwitchToThread函数提供另外一种线程把处理器交给另一个准备好的线程的方法.

要使用_beginthreadex来启动一个线程并为libcmt.lib创建线程特定的工作存储,使用_endthreadex替代ExitThread来终止线程,如果使用了CreateThread就会有不同线程访问并修改libcmt.lib线程安全库的正确操作.

17. 一个纤程是应用程序可调度的执行单元,而不是由内核来调度的单元,应用程序可创建大量纤程,纤程本身决定下一个要执行的是哪个纤程,纤程有的堆栈,但是完全运行于调度它们的线程的上下文中,纤程的管理完全发生在内核外面的用户空间中,可以认为纤

程是轻量级的线程,但它们之间有许多不同.纤程要在任何线程上执行,但一次不能运行于两个线程上,所以纤程不应该访问线程特定的数据如TLS..纤程不是抢占式调度的,Windows执行机构并不知道纤程的存在,纤程是在纤程DLL中管理的,完全位于用户空间中.纤程的API由7个函数构成,以如下顺序使用:

<1>线程通过调用ConvertThreadToFiber或ConvertThreadToFiberEx来启用纤程操作,而后线程就由一个单一的纤程组成,这一调用提供指向纤程数据的指针,这个指针可以像用于创建线程独特数据的线程参数的用法那样使用.

<2>应用程序可使用CreateFiber来创建更多纤程,每个纤程有起始地址,堆栈尺寸以及一个参数,每个新纤程通过地址来标识,而不是句柄.

<3>各个纤程可通过调用GetFiberData获取从CreateFiber所接收而来的自已的数据

<4>纤程可使用GetCurrentFiber获得自已的标识

<5>运行中的纤程通过调用SwitchToFiber将控制让给其他纤程,这个函数需要给出其他纤程的地址,纤程必须显式表明在线程中要运行的下一个纤程

<6>DeleteFiber函数删除现有的纤程及其所有关联的数据

<7>在XP中增加了新的函数,如ConvertFiberToThread(它释放由ConvertThreadToFiber所创建的资源),以及纤程本地存储.

<8>主从调度.一个纤程决定要运行的纤程,这个纤程总是将控制交给主纤程,点对点调

度,一个纤程决定下一个要运行的纤程,

十二.线程同步

Volatile存储可确保变量在被修改之后储存在内存中,而且在使用该变量之前永远从内存中取它来,volatile限定符通知编译器:这个变量可能在任何时间更改值,但是会影响性能,只有在需要的时候才使用它.

使用volatile的指导原则,

1. 必须是任何由并发线程访问的变量,而且至少被一个线程修改.

2. 即使是只读访问,该变量也至少被两个线程访问,并且正确的程序操作要依赖于新值能够立即对所有线程可见.

3. 互锁函数(interlocked function的参数需要volatile变量

但是使用volatile修饰符也不能确定其他处理器以特定顺序看到修改,一个处理器可能先将值保存在缓存中,而后才提交给内存

4. 使用InterlockedIncrement(&N)进行递增操作,但不可连续两次调用这个函数.

12.1 线程安全的代码

1.尽量不要使用全局变量.

2.如果一个函数被许多线程调用,而且有个线程特定的状态值,比如计数器,必须在函数调用之前保持持久,那么将它存储在全局变量中,要将状态值储存在线程专用的数据结构中.

3.避免竞态条件,如果在程序的某个点需要保持某些状态,那么可通过等待同步对象来确保状态的确得到保持

4.线程不应该更改进程环境,那样会影响所有的线程,一个线程不应该设置标准输入或输出句柄,或者改变环境变量,惟一能这样做的只有主线程.它应该在创建任何其他线程之前做这些更改,所有线程将共享相同的环境

5.所有线程共享的变量应该是静态的或者位于全局存储中,并且受到能创建内存屏障的同步或互锁机制的保护.

6.确认每个临界代码区域仅有一个入口和出口.

12.2同步

1)一个线程可通过使用WaitForSingleObject或WaitForMultipleObjects等候线程句柄的方法等待另一个进程或线程的终止.

2)文件锁专门用于同步的文件访问

3)CRITICAL_SECTION临界区对象是首选机制,永远要确认离开一个CS,如果没能这么做将导致其他线程永远等待,即使拥有该CS的线程终止,临界区拥有一个计数器,确保递归函数有用。

可以使用TryEnterCriticalSection,返回TRUE表示成功拥有该CS,返回FALSE,说明临界区代码不安全.确保要由同一个CS变量来守卫.

4)使用互锁指令访问和修改的任何变量都是volatile的,除了进入和离开时会创建内存屏障的不需要,但是临界区无法传信给其他线程,没有超时能力.

12.3互斥量

1.互斥量如果被终止的线程所放弃,会是已传信的,其他线程不会永远被阻塞.

2.互斥量等待有超时机制,而CS只能轮询

3.创建互斥量的线程可立即指定对该互斥量的拥有权,

4.CS几乎总是比互斥量快

5.原则

<1>如果WaitForSingleObject等待互斥量句柄不带超时,那么调用的线程可能永远阻塞

<2>如果线程终止或在离开CS之前终止,那么CS会处于不稳定的状态,之后的行为是不确定的

<3>互斥量粒度影响性能,大的临界区的代码区域如果长时间锁住,并发性得不到保证.

<4>锁的使用要最小化

<5>尽量以文字或表达式的形式将不变式记录到文档

12.4 信号量

1.将信号量看成是对可用资源数量的表示,可用来控制确切数量的线程醒来,但是如果请求计数递减2存在着阻塞状态

12.5 事件

当一个事件被传信之后,多个线程可从等待状态同时释放,事件可分为手动复位和自动复位两种。应该避免使用PulseEvent.会打开门并且在一个(自动复位)或所有(手工复位)等待中的线程通过这扇门之后立即关闭,

十三.锁,性能以及NT6增强

高度的锁竞争会阻碍良好的性能,而且降低是剧烈的,不会和线程数量呈线性关系,

十四.高级线程同步

1.只要存在共享变量在一个线程中更改在另一个线程中访问的情况,这个变量就是volatile的,以便确保每个线程都能在内存中对该变量做读写操作,而不是假定该变量保持在对线程特定的寄存器中,但是,不可过度使有volatile,任何函数调用或返回都将确保寄存器的存储,此外,每个同步调用都会建立起内存屏障.

2.确认不对同一个事件使用不同的锁,确认等待条件变量之前不变式成立.

3. 互锁函数如何运行取决于函数运行的处理器平台,对于x86系列的处理器 ,互锁函数会向总线发出一个硬件信号,防止其他处理器访问同一个内存地址.

高速缓存行有32或个字节组成,并且始终在第32个字节或第个字节的边界上对齐,高速缓存行的作用是为了提高CPU运行的性能。

<1>WaitForMultipleObjects函数是以原子形式运行的,它在检查内核对象状态时,其他任何线程都无法改变对象的状态,这样可以防止出现死锁.微软使用先进先出,即等待时间最长的进程得到这个对象,但是系统中的一些操作有可能改变这个特征,如果正在等待对象的线程暂停运行,那么因为无法对暂停的线程进行调度,所以系统会忘记这个线程正在等待对象,而当线程再次恢复运行时,系统则认为这个线程刚刚开始等待这个对象,因此改变了线程的等待时间特征,微软没有明确说明先进先出算法是如何实现的.在调试一个进程时,达到一个断点时,这个进程中的所有线程都会暂停运行,因此,调试一个进程会使得先进先出算法的结果难于预测.

<2>PulseEvent会使事件变为已通知状态,然后立即变为未通知状态,如果在人工重置事件上调用,等待这个事件的所有线程都可以变为可调度线程,如果在自动重置事件上调用,那么只有一个等待这个事件的线程可以变为可调度状态.如果事件发生时没有线程等待这个事件,那么这个函数不起任何作用.因为函数无法知道任何线程的状态,并且无法知道哪个线程将会看到事件发生,并变成可调度线程,所以pulseEvent并不十分有用.

<3>WM_TIMER消息属于最低优先级消息,只有进程对列中没有其他消息,才对这条消息进行检索,定时器发出的报时信息将会唤醒正在等待之中的线程.

<4>SignalObjectAndWait的优点有两个:通知一个对象等待另一个对象,可以节省很多处理时间,每次调用一个函数,使线程从用户模式代码变成内核模式代码,大约需要1000个CPU处理周期(x86平台上),不使用该函数,一个线程无法知道另一个线程何时处于等待状态,

十五.进程间通信

1. 命名管道是面对消息的,读进程可以精确地读入由写进程发送的不同长度的消息.

2. 命名管道是双向的,面对消息的.

3. 可以有多个的但具有相同名称的管道实例

4. 联网的客户可按名称访问管道.无论两个进程位于同一台机器还是不同台

5. 命名管道要比匿名管道更可取,匿名管道可以进行单向的(半双工)基于字节的IPC,对管道的写操作是在内存缓冲区中实现的.如果管道已满,也会阻塞.

6. 管道名称是大小写不敏感的,而且可以包含任何除反斜杠之外的字符,使用PeekNamePipe来确定是否有实际需要读取的消息,在不破坏原有消息的基础上读入管道中消息的任意字节,但它不会阻塞,立即返回.

7. 邮槽是一个广播机制,它与数据报类似

<1>邮槽是单向的

<2>邮槽是一对多的关系

<3>写者不知道是否对方实际接收到了消息

<4>消息长度有限

需要的操作

<1>服务器使用CreateMailslot

<2>使用ReadFile调用等待接收邮槽消息

<3>打开邮槽并使用WriteFile写入消息,

十六.套接字网络编程

http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml

查看分配的端口号

1. 套接字客户数量没有上限,但命名管道实例的数量可以有,取决于第一次对CreateNamePipe的调用.

2. 命名管道没有显示的端口号,而是通过名称来区分.

3. 两次调用消息接收函数之间,缓冲区内容和状态必须得到保持.

4. 每个数据部分以512字节为限,避免消息以碎片来发送

十七.Windows服务

服务的类型

<1>SERVICE_WIN32_OWN_PROCESS 表示Windows服务运行自已的进程

<2>SERVICE_WIN32_SHARE_PROCESS 表示Windows服务与其他服务共享一个进程,将许多服务合并到单一的进程中

<3>SERVICE_KERNEL_DRIVER 表示Windows设备驱动程序,保留系统使用

<4>SERVICE_FILE_SYSTEM_DRIVER 指定Windows文件系统驱动程序,保留系统使用

<5>SERVICE_INTERACTIVE_PROCESS 只能与两个SERVICE_WIN32_X值组合使用,但是交互服务有安全危险.

十八.异步输入/输出与完成端口

1. 多线程I/O,一个线程执行正常的同步I/O,其他线程可继续执行.

2. 重叠I/O(带等待),线程在发出读,写或其他I/O操作命令之后继续执行,当线程需要

I/O结果才能继续时,它要么等待文件句柄,要么等待ReadFile或WriteFile重叠结构中指定的一个事件.

3. 带有完成例程的重叠I/O,系统在完成I/O操作完成时调用线程内一个特定的完成例程回调函数,在XP上重叠的扩展I/O会是复杂的且极少能产生大量性能效益,

4. 重叠I/O的后果:

<1>I/O操作不会阻塞

<2>返回值为FALSE不一定会失败,因为I/O操作很可能尚未完成,在正常情况下,GetLastError()将返回ERROR_IO_PENDING ,表示没有错误.

<3>如果传送尚未完成,那么返回的已传送字节数也没有用处

<4>程序可能对单个重叠文件句柄发出多个读或写,所以,句柄的文件指针是没有意义的.必须要有另外一种方法为每个读或写指定文件的位置.

<5>程序必须要能够等待同步I/O的完成,对同一个句柄上有多个未完成的操作的情况时,必须能够确定哪个操作已经完成。I/O操作不一定按其发出的相同顺序完成

十九. Unicode

Microsoft坚定地支持Unicode,所有需要字符串的COM接口方法

Unicode对应函数

Strcat <-> wcscat

Strchr<->wcschr

Strcmp<->wcscmp

Strcpy<->wcscpy

Strlen<->wcslen

使用sizeof(szBuffer)/sizeof(TCHAR)表示缓冲区大小,使用

malloc(nCharacters*sizeof(TCHAR))

二十.内核对象

内核的对象的数据结构只能被内核访问,应用程序无法直接在内存中找到这些数据结构. 内核对象的存在时间可以比创建对象的进程长.通过使有计数来确定生命周期.安全性是指:对象管理小组中的任何成员和内核对象的创建者都拥有对这个对象的全部访问权,而其他进程或线程均无权访问该对象,如果忘记调用CloseHandle函数,有可能出现内存泄漏,除非终止运行时,系统将能确保所有内容均正确清除.

二十一.线程的基本知识

进程是不活泼的,进程从来不执行任何东西,它只是线程依存的地方,线程只有一个内核对象和栈,保留的记录很少,因此需要的内存很少,而进程使用的系统资源比线程多很多,为进

程创建一个虚拟地址空间需要许多系统资源.绝对没有道理让CPU闲置,为使CPU处于繁忙状态,可以让其执行各种工作.

1. 索引服务,创建低优先级的线程,以便于定期打开磁盘驱动器上的文件并给内容作索引.

2. 磁盘碎片整理

指令指针和栈指针寄存器是线程上下文中最重要的两个寄存器,线程总是在进程的上下文中运行,当初始化线程的内核对象时,CONTENT结构的栈指针寄存器被设置为线程栈上用来放置pfnStartAddr的地址.指令指针寄存器置为称作BaseThreadStart的未公开(和未输出)函数的地址中,该函数包含在Kernel32.dll模块中

<1>_beginthreadex函数只存在于C/C++运行时库的多线程版本中,如果链接到单线程运行时库,就会得到”未转换的外部符号”错误信息

<2>每隔20ms左右,Windows都会查看当前存在的所有线程内核对象,只有某些对象是可调度的,Windows选择一个可调度的线程内核对象,将这个内核对象加载到CPU的寄存器中,所加载的值就是上次保存在线程环境中的值,这项操作被称作上下文切换.Windows为抢占式多线程操作系统,不能保证线程能得到整个处理器,无法保证不允许其他线程运行.Windows没有被设计为实时操作系统,除挂起线程外,其他许多正在等待某些事情的发生的线程也是不可调度的,如正在运行的notepad,但不输入任何数据,那么notepad的线程就没有什么事情可做,系统不给闲置的线程分配CPU时间,当移动notepad的窗口时,系统就会自动改变notepad的线程成为可调度的线程,这并不意味notepad线程立即获得CPU时间,只是表示notepad的线程有事可做,系统会设法在某个时间(不久)对它进行调度.一个

线程可以挂起若干次也必须唤醒若干次)

<3>Sleep系统将会睡眠若干秒时间,线程能否做到在规定时间内被唤醒,取决于系统中正在进行的操作.将0传递给Sleep表示调用线程将自愿放弃剩余的时间片,迫使系统重新调度.如果不存在多个拥有相同优先级的可调度线程,系统将会调度刚刚调用Sleep的线程.

<4>SwitchToThread(),系统查看是否存在一个迫切需要CPU时间的线程,如果没有迫切需要CPU时间,这个函数立即返回,允许一个需要资源的线程强制另一个低优先级而且目前拥有该资源的线程放弃该资源.

<5>由于抢占式系统的缘故,要获得线程的运行时间使用GetThreadTimes()和GetCurrentThread().

<6>一个线程实际有两个环境,用户环境和内核环境,只有线程的用户模式环境才能被GetThreadContext函数返回,

<7>当可以调度一个优先级为31的线程时,就不会调用优先级低于31,这个称为渴求调度,当大量的 CPU时间被高优先级的线程占有时,低优先级的线程就无法运行.出现渴求的情况,不过实际上,在某一个时间段内,大多数的线程是不可调度的.

<8>在系统引导时,它会创建一个特殊的线程,成为0页线程(zero page thread),该线程的优先级为0,唯一在0优先级上运行的线程,在系统没有任何线程需要执行操作时,0页线程负责将系统中的所有空闲RAM页面置为0.

<9>Windows Exploer是在高优先级上运行的.大多数Exploer是暂停的,等待用户的按键,当低优先级线程运行时,系统会让explorer先运行.很大一部分时间,它都无事可做,不占用CPU的时间,保证用户的快速响应.

<10>尽量避免实时优先级类的使用,可能会阻止必要的磁盘I/0信息和网络信息的产生,另外,操作系统将无法及时处理键盘和鼠标的输入,只有在需要在很短的时间内响应硬件事件,或者执行某些不能中断的短期服务.

<11>进程是不能调度,只有线程才能调度,进程优先级只是一个抽象的概念.通常高优先级的线程大多时候都不应该处于可调度状态.在线程需要执行某种操作时,它能够迅速获得CPU时间,这个时候,线程应该尽可能少地执行CPU指令,并且返回睡眠状态 ,等待再次变成可调度状态.相反,低优先级的线程可以保持可调度状态,执行大量的CPU指令来进行它的操作,如果按照这个原则来做,整个系统就可以正确地对用户作出响应.

<12>线程的优先级等级,线程的优先级可能提高,SetProcessPriorityBoost函数负责告诉系统激活或停用进程中的所有线程的优先级提高功能,而SetProcessPriorityBoost则让你激活或停用单个线程的优先级提高功能.

<13>Windows使用软亲缘性,如果其他因素,它设法在线程上次运行的那个处理器上再次运行线程,让线程留在单个处理器上,有助于重复使用仍然在处理器的内存缓存数据.

二十二.内存管理

22.1无效断点分配分区

为进程地址空间设置这个分区,是为了帮助程序员捕获无效断点分配(NULL-point assignment)

如果进程中的一个线程试图读取这个分区中的数据,或者将数据写入分区,CPU就会引起非法访问,保护这个分区可以有效地帮助发现无效断点分配.

Int *pn=(int*)malloc(sizeof(int));

*pn=5;

由于禁止访问内存的这个分区,因此会发生非法访问现象,并终止这个进程的运行.

22.2 KB禁止进入分区

这个分区禁止进入,任何试图访问这个内存分区的操作都是违规的.保留的原因是为了简化操作系统的实现,将内存块的地址和长度传递给Windows函数时,在Windows函数执行操作前,内存块即可生效,

22.3物理存储器和页面文件

系统将内存页面复制到页面文件,已经将页面文件复制到内存页面的次数越多.访问硬盘的次数就越多,系统的运行也越慢(抖动(thrashing)意味着操作系统将更多的时间花费在将页面在内存中调入调出,而不是把大部分时间用于程序的运行),因此,通过增加计算机的内存,就可以减少运行应用程序时发生的次数,这样必然可以大大提高系统的运行速度,在大多数情况下,增加内存比提高处理器的运行速度更能够提高系统的运行性能.

<1>CPU处理准确对齐的数据时,它的运行效率最高,在用数据的大小对内存地址取模,结果为0时,数据是对齐的,例如,WORD类型的值应该总是从能够被2除尽的地址开始,而DWORD类型的值则应该总是从能够被4除尽的地址开始,当CPU试图读取未对齐的数据时,CPU可能产生下面的两种情况之一:它可以产生一个异常条件,也可以执行多次对齐的内存访问,以便将未对齐数据完整地读出.如果CPU多次进行内存访问,应用程序的运行速度就会降低,在最好的情况下,系统访问未对齐的数据所需的时间也是访问对齐数据所需时间的两倍,为了使应用程序获得最佳的性能,编写代码时必须将数据准确对齐.

<2>Windows内存管理的方法

(1) 虚拟内存,适合管理大型对象或结构数组

(2) 内存映射文件,适合管理大数据流以及管理在单个计算机上运行的多个进程之间

的数据共享

(3) 内存堆,适合管理大量的小对象

<3>目前为止,所有Windows的分配粒度都为KB,因此,如果需要在进程的地址空间中保留从

196692(300*65536+8192)地址开始的区域,系统将会自动将这个地址调整为KB的倍数,然后保留地址从 19660800(300*65536)开始的区域.

<4>物理RAM是一种非常宝贵的资源,应用程序只能分配尚未指明用途的RAM,不应该过多使用AWE,否则进程和其他进程就会过份地在内存与磁盘之间进行页面调度,从而严

重影响系统运行性能,如果可用的RAM数量较少,那也会对系统创建进程,线程和其他资源产生负面影响,应用程序可以使用GlobalMemoryStatusEx函数,来监控物理存储器的使用情况.

<5>尽管我们需要创建的栈大小只有1MB,栈区域的实际大小却是1MB加128KB,在Windows 98,每次为一个栈保留区域时,系统实际上所保留的区域都比所要求的区域尺寸大128KB,栈的前面有一个KB的块,捕获栈的上溢条件,栈的后面是另外一个KB的块.

22.4 内存映射文件

内存映射文件的目的

1. 系统使用内存映射来加载exe和DLL文件,可以大大节省页面文件空间以及应用程序启动运行所需的时间

2. 使用内存映射来访问磁盘的数据文件,这样不必对文件执行I/O操作,并且不必对文件内存进行缓存.

3. 使用内存映射可以在同一台计算机运行多个线程之间共享数据,Windows其它的共享方式都是基于使用内存映射来实现的.

<1>使用内存映射可以对大文件,进行内容的倒序,用户打开这个文件,并告诉系统将虚拟地址空间的一个区域进行倒序,用户告诉系统将文件的第一个字节映射到保留区域的第一个字节中,然后可以访问虚拟内存区域.最大优点,系统管理所有的文件缓存操作,不需要分配任何内存,但是内存映射文件仍然会因为电源故障之类的问题突断进程而破坏文件的数

据.

<2>注意错误的处理

HANDLE hFile=CreateFile(..);

HANDLE hMap=CreateFileMapping(hFile,..);

If(hMap==NULL) return GetLastError();

如果CreateFile调用失败,会返回INVALID_HANDLE_VALUE.但仍然可以创建文件映像成功.由于CreateFile函数运行失败的原因实在太多了,所以必须查看CreateFile的返回值,以确定是否出现了错误.

<3>堆可以用来分配一些较小的数据块,不必考虑分配粒度和页面边界之类的问题,而只处理当前的任务.但是分配内存块以及释放内存块的速度很慢,并且无法直接控制物理存储器的提交和回收.一个进程可以拥有若干个堆,在默认设置下,堆是顺序运行的,如果有多个线程对堆进行操作,堆函数必须执行额外的操作,来保证堆的安全性,在创建一个堆时可以告诉系统,只有一个线程访问这个堆,这样就不必再执行额外的代码,但是自已要负责堆的安全性.

二十三.DLL基础

一个单独的地址空间是由一个可执行模块和若干个DLL模块组成的,若干个C/C++运行时库可能会存在一个单一的地址空间中.

Void ExeFunc()

{

PVOID pv=DLLFunc();

Free(pv);

}

PVOID DllFunc()

{

Return (malloc(100));

}

如果EXE和DLL这两者或两者之链接到静态版本的C/C++运行时库,则对free的调用将会失败.如果EXE和DLL都链接到DLL版本的C/C++运行时库,则上述代码将正常工作.

<1>微软特意创建FreeLibraryAndExitThread函数的原因如下:编写一个DLL,当它初次映射到进程的地址空间,创建一个线程,当该线程执行完毕,它通过调用FreeLibrary,将DLL从进程的地址空间中解除映射,并且终止执行.然后立即调用ExitThread,但是线程分别

调用FreeLibrary和ExitThread将会产生严重的问题,调用FreeLibrary将会立即使DLL从进程的地址空间解除映射,当调用FreeLibrary返回时,包含对ExitThread调用的代码将不再可用,并且线程将不再执行,这将导致一个访问违例,以致整个进程终止运行.

<1>Dll入口函数是大小写敏感的.

<2>Dll可以使用延迟加载,在进程执行过程中,分开加载DLL,导致所有需要的DLL都映射到进程的地址空间.添加两个链接开关/Lib:DelayImp.lib /DelayLoad:MyDll.dll 在默认情况下,所调用的函数可绑定到内存地址,即系统认为该函数会在一个进程的地址处,由于创建可绑定的延时加载DLL会使可执行文件变大,因此链接程序同时也支持一个/Delay:nobind开关

<3>少数函数允许一个进程对另一个进程进行操作,大多数这类函数最初是为了调试程序和其他工具设计的,当然,任何应用程序都可以调用这些函数,Windows提供了一个称为CreateRemoteThread函数,使得可以简单地在另一个进程创建线程,

<4>注入DLL的另一个方法是替换所知道的进程将要加载的DLL,但是它不具有版本弹性.

<5>调试程序可以强制将某些代码放进被调试进程的地址空间(例如使用WriteProcessMemory函数),并且使被调试进程的主线程执行该代码,需要操作被调试线程的CONTEXT结构,编写针对特定CPU的代码,手工编写想让被调试进程执行的机器语言指令,调试程序与其被调试进程之间存在着固定的关联关系,如果调试程序终止运行,则Windows将自动地销毁被调试进程,你无法阻止该操作.

<6>如果你的进程正在派生你想要注入代码的进程,可以设置线程的指令指针以执行内存映射文件的代码,

1) 让你的进程生成被挂起的子进程

2) 从.exe模块的文件头检索主线程的起始内存地址

3) 将机器指令保存到此内存地址

4) 在该地址强制加入一些手工编写的机器指令,这些指令应当调用LoadLibrary函数来加载一个DLL

5) 恢复子进程的主线程的运行,以使该代码可以执行

6) 将初始指令重新保存到起始地址

7) 像任何事情没有发生一样,使进程从起始地址继续执行.

二十四.内联函数

只有当函数只有 10 行甚至更少时才将其定义为内联函数. 当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.

缺点:

滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。

内联函数的定义必须放在头文件中, 编译器才能在调用点内联展开定义. 然而, 实现代码理论上应该放在 .cc 文件中, 我们不希望 .h 文件中有太多实现代码, 除非在可读性和性能上有明显优势.

二十五. #include 的路径及顺序

使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: C 库, C++ 库, 其他库的 .h, 本项目内的 .h.

又如, dir/foo.cc 的主要作用是实现或测试 dir2/foo2.h 的功能, foo.cc 中包含头文件的次序如下:

1. dir2/foo2.h (优先位置, 详情如下)

2. C 系统文件

3. C++ 系统文件

4. 其他库的 .h 文件

5. 本项目内 .h 文件

二十六. 静态和全局变量

禁止使用 class 类型的静态或全局变量: 它们会导致很难发现的 bug 和不确定的构造和析构函数调用顺序. 静态生存周期的对象, 包括全局变量, 静态变量, 静态类成员变量, 以及函数静态变量, 都必须是原生数据类型 (POD : Plain Old Data): 只能是 int, char, float, 和 void, 以及 POD 类型的数组/结构体/指针. 永远不要使用函数返回值初始化静态变量; 不要在多线程代码中使用非 const 的静态变量.

不幸的是, 静态变量的构造函数, 析构函数以及初始化操作的调用顺序在 C++ 标准中未明确定义, 甚至每次编译构建都有可能会发生变化, 从而导致难以发现的 bug. 比如, 结束程序时, 某个静态变量已经被析构了, 但代码还在跑 – 其它线程很可能 – 试图访问该变量, 直接导致崩溃.

所以, 我们只允许 POD 类型的静态变量. 本条规则完全禁止 vector (使用 C 数组替代), string (使用 const char*), 及其它以任意方式包含或指向类实例的东东, 成为静态变量. 出于同样的理由, 我们不允许用函数返回值来初始化静态变量.

如果你确实需要一个 class` 类型的静态或全局变量, 可以考虑在 ``main() 函数或 pthread_once() 内初始化一个你永远不会回收的指针.

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- hids.cn 版权所有 赣ICP备2024042780号-1

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务