系统中的所有的线程都必须拥有对各种系统资源的访问权,这些资源包括内存堆栈,串口,窗口,文件和许多其他资源
所谓原子访问,是指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源 对X86家族的CPU来说,互锁函数会对总线发出一个硬件信号,防止另一个CPU访问用一个内存地址
没有任何互锁函数仅仅负责对值进行读取操作(而不改变这个值)
当一个CPU从内存读取一个字节时,他不只是取出一个字节,他要取出足够的字节来填入高速缓存行。高速缓存行有32或64个字节组成(视CPU而定),而且始终在第32个字节或第64个字节的边界上对齐。高速缓存行的作用是为了提高CPU运行的性能
当线程想要访问共享资源,或者得到关于某个特殊事件的通知时,该线程必须调用一个操作系统函数
volatile类型的限定词,他告诉编译器,变量可以被应用程序本身以外的某个东西进行修改这些东西包括操作系统,硬件或者同时执行的线程等,这个限定词会告诉编译器,不要对该变量进行任何优化,并且总是重新加载来自该变量的内存单元的值
有一个关键的问题必须记住,当拥有一项可供多个线程访问的资源时,应该创建一个critical_section结构
编写需要使用共享资源的任何代码都必须封装在entercriticalsection和leavecriticalsection函数中
当线程试图进入另一个线程拥有的关键代码段时,调用线程就立即被置于等待状态,这意味着该线程必须从用户方式转入内核方式,这种转换是要付出很大代价的
线程与内核对象的同步
用户方式同步的优点是他的同步速度非常快,如果强调线程的运行速度,那么首先应该确定用户方式的线程同步机制是否合适需要
互锁函数家族只能在单值上运行,根本无法使线程进入等待状态
内核对象机制的适应性远远优于用户方式机制
内核对象机制的唯一不足之处是他的速度比较慢
当一个对象得到状态改变时,我称之为成功等待的副作用
算法是公平的,这意味着如果多个线程正在等待,那么每当对象变为已通知状态时,每个线程都应该得到他自己的被唤醒的机会
当调试一个进程时,只要达到一个断点,该进程中的所有线程均暂停运行
在所有的内核对象中,时间内核对象是个最基本的对象。包含一个使用计数,一个用于指明该事件是个自动重置的事件还是个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值
事件能够通知一个操作已经完成,有两种不同类型的事件对象,一种是人工重置的事件,另一种是自动重置的事件,当人工重置的事件得到通讯时,等该该时间的所有线程均变为可调度线程,当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变成可调度线程
当不再需要事件内核对象时,应该调用closehandle函数
一旦事件已经创建,就可以直接控制他的状态
等待计时器是在某个时间或者按规定的间隔时间发出自己的信号通知的内核对象。他们通常用来在某个时间执行某个操作
异步过程调用(APC)
定时器常常用于通信协议中
凡是称职的WINDOWS编程员都会立即将等待定时器与用户定时器(用settimer函数进行设置)进行比较
等待定时器属于内核对象,这意味着他们可以供多个线程共享,并且是安全的
当用户定时器报时的时候,只有一个线程得到通知,另一方面,多个线程可以在等待定时器上进心等待,如果定时器是个人工重置的定时器,则可以调度若干个线程
进程、线程和作业都是内核对象
当进程正在运行的时候,进程内核对象处于未通知状态,当进程终止运行的时候,他就变成为已通知状态
线程可以使自己进入等待状态,直到一个对象变为已通知状态
当进程等待的对象处于未通知状态中时,这些线程不可调度,但是一旦对象变为已通知状态,线程看到该标志变为可调度状态,并且很快恢复运行
等待函数可使线程自愿进入等待状态,知道一个特定的内核对象变为已通知状态为止
信标内核对象用于对资源进行计数。他们与所有内核对象一样,包含一个使用数量,另外包含两个带符号的32位值,一个是最大资源数量,一个是当前资源数量
使用信标,就能很好的处理对资源的监控和对线程的调度
使用信标时,不要将信标对象的使用数量与他的当前资源数量混为一谈
信标的出色之处在于他们能够以原子操作方式来执行测试和设置操作
互斥对象内核对象能够确保线程拥有对单个资源的互斥访问权,互斥对象包含一个使用数量,一个线程ID和一个递归计数器。
互斥对象属于内核对象,而关键代码段则属于用户方式对象,这意味着互斥对象的运行速度不关键代码要慢,但是这也意味着不同的进程中的多个线程能够访问单个互斥对象,并且这意味着线程在等待访问资源时可以设定一个超时值
ID用于标志系统中的哪个线程当前拥有互斥对象,递归计数器用于指明线程拥有互斥对象的次数
互斥对象用于保护由多个线程访问的内存块
通过调用一个等待函数,并传递负责保护资源的互斥对象的句柄,线程就能够获得对共享资源的访问权
与所有情况一样,对互斥内核对象进行的检查和修改都是以原子操作方式进行的
每当线程成功地等待互斥对象时,该对象的递归计数器就递增
互斥对象有一个“线程所有权”的概念,没有一种对象能够记住哪个线程成功地等待到该对象,只有互斥对象能够对此保持跟踪
系统保持对所有互斥对象和线程内核对象的跟踪,因此他能准确的知道互斥对象合适被放弃
异步设备I/O使得线程能够启动一个读操作或写操作,但是不必等待读操作或写操作完成,当初始化操作完成时,该线程可以终止自己的运行,等待系统通知他文件已经读取。设备对象可以是同步的内核对象
线程同步工具包
反复强调,关键代码段属于用户方式对象(一般情况下)
关键代码段必须包含某个内核对象,以便是线程进入有效的等待状态。关键代码段的运行速度很快,因为只有当争用该关键代码段的时候,才使用该内核对象
c++对象不能被通知,只有内核对象才能被通知,并且可以用于线程的同步
许多应用程序存在一个基本的同步问题,这个问题称为单个写入程序/多个阅读程序环境
线程池的使用
用户方式的同步机制的出色之处在于他的同步速度很快
创建多线程应用程序是非常困难的,需要会面临两个大问题,一个是要对线程的创建和撤销进行管理,另一个是要对线程对资源的访问实施同步
如何对线程个创建个撤销进行管理的问题上用线程池实现
你自己从来不调用createthread,系统会自动为你的进程创建一个线程池
当该线程处理完客户机的请求之后,该线程并不立即被撤销,他要返回线程池,这样他就可以处理已经排队的任何其他工作项目
线程池希望经常处理异步I/O请求,即每当线程将一个I/O请求排队放入设备驱动程序时,便要处理异步I/O请求
设计良好的线程池也必须设法保证线程始终都能处理各个请求
线程池不能对线程池中的线程数量规定一个上限,否则就会发生渴求或死锁现象
当使用线程池函数时,应该查找潜在的死锁条件
只有当线程不是吃梨定时器的工作项目的线程时,该线程才能进行对定时器的中断删除
如果你正在使用定时器组件的线程,不应该试图对任何定时器进行中断删除,否则就会产生死锁
线程池的定时器组件创建等待定时器,这样,他就可以给apc(异步过程调用)项目排队,而不是给对象发送通知
如果正在等待一个自动重置的事件内核对象,一旦该对象变为已通知状态,该对象就重置为他的为通知状态,并且他的工作项目将被放入队列
I/O组件的线程全部在一个I/O组件端口上等待
关闭设备会导致他的所有待处理的I/O请求立即完成,并产生一个错误代码,要做好准备,在你的回调函数中处理这种情况
纤程
WINDOWS添加了一种纤程,以便能够非常容易地将现有的nuix服务器应用程序移植到WINDOWS
nuix服务器应用程序属于单线程应用程序(由WINDOWS定义),但是他能够为多个客户程序提供服务
纤程是以用户方式反码来实现的,内核并不知到纤程,并且他们是根据用户定义的算法来调度的,纤程采用非抢占式调度方式
单线程可以包含一个或多个纤程,就内核而言,线程是抢占调度的,是正在执行的代码
除非打算创建更多的纤程以便在同一个线程运行,否则没有理由将线程转换层纤程
Windows的内存结构
操作系统使用的内存结构是理解操作系统如何运行的最重要的关键
每个进程都被赋予他自己的虚拟地址空间
对于32位进程来说,这个地址空间是4GB,因为32位指针可以拥有从0x00000000至0xffffffff
由于每个进程可以接收他自己的私有地址空间,因此当进程中的一个线程正在运行时,该线程可以访问只属于他的进程的内存,属于所有其他进程的内存则隐藏着,并且不能被正在运行的线程访问
属于操作系统本身的内存也是隐藏的,正在运行的线程无法访问
这是个虚拟地址空间,不是物理地址空间,该地址空间只是内存地址的一个范围,在你能够成功地访问数据而不会出现违规访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间
注意,当操作系统创建进程的地址空间时,需要检查一个可执行的largeadderssaware标志。对于dll,系统则忽略该标志
有时候系统能够代表你的进程来保留地址空间的区域
进程环境块(FEB)
系统也需要创建一个线程环境块(TEB),以便管理进程中当前存在的所有线程
系统规定,要求保留的地址空间区域均从分配粒度边界(目前所有平台上均为64KB)开始,但是系统本身并不受这个规定的限制
保留区域必须是CPU的页面大小的倍数
当你的程序算法不再需要访问已经保留的地址空间区域时,该区域应该被释放。这个过程称为地址空间的区域
若要使用已保留的地址空间区域,必须分配物理存储器,然后将该物理存储器映射到已保留的地址空间区域。这个过程称为提交物理存储器
物理存储器总是以页面的形式来提交的
磁盘上的文件通常称为页文件,他包含了可供所有进程使用的虚拟内存
当一个线程试图访问一个字节的内存时,CPU必须知道这个字节是在ram中还是在磁盘上
操作系统与CPU相协调,共同将ram的各个部分保存到页文件中,当运行的应用程序需要时,在将页文件的各个部分重新加载到ram
页文件增加了应用程序可以使用的ram的容量,因此页文件的使用是视情况而定的
最好将物理存储器视为存储在磁盘驱动器(通常是硬盘驱动器)上的页文件中的数据
系统的页文件的大小是确定有多少物理存储器可供应用程序使用时应该考虑的最重要的因素,ram的容量则影响非常小
要提高系统的运行性能,增加ram比提高CPU的速度所产生的效果更好
当硬盘上的一个程序的文件映像(这是个.exe文件或dll文件)用作地址空间的区域的物理存储器时,他称为内存映射文件
Microsoft不得不通过软盘来运行的映射文件,这样,安装应用程序才能正确运行
已经分配的物理存储器的各个页面可以被赋予不同的保护属性
让所有的实例共享同样的内存页面能够大大提高系统的性能,但是这要求所有实例都将该内存视为只读或只执行的内存
数据对齐并不是操作系统的内存结构的一部分,而是CPU结构的一部分
当数据大小的数据模数的内存地址是0时,数据是对齐的,例如,word值应该总是从被2除尽的地址开始,而dword值应该总是从被4除尽的地址开始
在应用程序中使用虚拟内存
WINDOWS提供3种进行内存管理的方法:
虚拟内存,最适合用来管理大型对象或结构数组
内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机 上运行的多个进程之间共享数据
内存堆栈,最适合用来管理大量的小对象
用于管理虚拟内存的函数可以用来直接保留一个地址空间区域,将物理存储器(来自页文件)提交给该区域,并且可以设置你自己的保护属性
如果保留的区域预计在很长时间内不会被释放,那么可以在尽可能搞的内存地址上保留该区域,防止其导致该区域分成碎片
当保留一个区域时,应该为该区域赋予一个已提交内存最常用的保护属性
当保留一个区域后,必须将物理存储器提交给该区域,然后才能访问该区域中包含的内存地址,系统从他的页文件中将已提交的物理存储器分配给一个区域,物理存储器总是按页面边界和页面大小的块来提交的
虚拟内存为我们提供了一种兼顾预先声明二维矩阵和实现链接表的两全其美的方法
虚拟内存技术存在的一个问题是,必须确定物理存储器在何时提交
系统总是按页面的分配粒度来提交物理存储器的
与提交物理存储器的情况一样,回收时也必须按照页面的分配粒度来进行
保护属性是与内存的整个页面相关联的,而不是赋予内存的各个字节的
地址窗口扩建(AWE),Microsoft创建awe是出于下面两个目地:允许应用程序对从来不在操作系统与磁盘之间交换的ram进行分配;允许应用程序访问的ram大于进程的地址空间
你创建的最大窗口取决于你的地址空间中可用的最大相邻空闲地址块
AWE的局限性是,映射到地址窗口的所有内存必须是可读的和可写入的
每个ram页面由操作系统赋予一个页框号
页框号本身对应用程序没有任何用处,不应该查看该数组的内容,并且肯定不应该修改该数组中的任何一个值
只有拥有页面的进程才能使用已经分配的ram页面,AWE不允许ram页面被映射到另一个进程的地址空间,因此不能在进程之间共享ram块