[整理] - Relational Engine之UMS Internals
SQL Server 6.5使用Windows的调度处理管理多线程,和其它Windows应用程序一样,它使用Windows标准API,没有用到任何隐藏API,这使得 SQL Server的工作线程同其它多线程Windows程序完全一样,没有任何特殊的优先级,同等的被Windows调度。在SQL Server 7.0之后,需要具有处理几千并发用户的能力,而Windows的线程调度方法,使得SQL Server在多线程处理上效率受到制约,因此SQL Team决定在SQL Server 7.0中加入自己的线程调度方法。在这种情况下,SQL Server的UMS诞生了。
抢占式(Preemptive Scheduling)与协作式(Cooperative Scheduling)
在任务调度方面,Windows 95、98使用协作式,Windows NT系列、2000等,使用抢占式方式。协作式方式下,操作系统将处理器分配给某个进程之后,处理器完全由进程自己控制,包括在何时释放以供其它进程使用等。这样如果当前占有处理器的进程崩溃,并且不能将处理器释放出来,此时别的任何进程都无法获得处理器使用权,整个操作系统就崩溃了。因此在Win 95、98下,经常会死机,系统没有任何响应,只有重启系统才可以解决。抢占式方式下,处理器完全在操作系统的进程调度控制之下。操作系统将处理器在时间上分成一个个时间片(Slice),每次将一个时间片分配给某个进程,如果这个时间片结束,不管工作进程有没有完成自己的工作,操作系统都会将这个进程挂起,将随后的时间片按照进程优先级顺序,分配给其它的进程。这种方式,只要操作系统内核没有Bug,硬件驱动不宕机,就永远也不会导致系统崩溃,因为只要被分配的时间片结束,操作系统的进程调度就取得了处理器的控制权。例如Win 2000之后使用抢占式方式进行进程调度,比较少出现应用程序导致操作系统死机无响应的情况发生。但是这种方式下,为了实现任意时刻都可以将某进程挂起,将其它进程唤醒,这种频繁的在进程间进行切换,尤其是当进程间需要通讯、协作时,需要很多额外的机制和开销,使得处理器的整体利用率降低。
SQL Server使用多线程工作,在抢占式方式下,各个并行线程(当然这里的并行是指对应用而言,对用户角度而言)的执行在时间顺序上应用程序是无法把握的,完全由操作系统调度管理。并行的多线程之间的数据共享、线程间的通讯、协作等,都需要使用各种机制予以解决。这些机制将导致SQL Server这种处理量大、实时性要求相当高的应用效率降低。为了避免上述情况,SQL Server的UMS使用协作式方式管理自己的多线程。
UMS怎样调度
在操作系统的抢占式调度策略下,SQL Server如何避免自己的多线程被操作系统调度?在这方面,SQL Server使用一种欺骗方式。Windows认为,某些线程是非活动状态的,它们处于永久的等待状态,对这种线程,Windows不会考虑对之进行调度。SQL Server利用了这个机制,UMS确保任意时刻只有一个活动状态的工作线程,其它线程都处于永久的等待状态,这样,操作系统就不会考虑对SQL Server的多线程进行调度,多线程的调度完全在UMS的控制之下(注意,当前活动的SQL Server工作线程还是受Windows调度,只是在这种机制下,Windows认为SQL Server是单线程应用程序)。所有的SQL Server线程,都有一个相关的事件对象,通过传入INFINITE参数调用相关事件对象的WaitForSingleObject方法,可以使这个线程进入永久的等待状态;而通过给相关事件对象信号,可以唤醒这个线程而进入执行状态。活动线程的工作结束时,会使自己进入到永久的等待状态,并给后续线程相关事件对象信号,唤醒后续线程进入活动状态。SQL Server使用这种机制,保证在每个处理器上,只有一个当前的活动线程。
SQL Server比操作系统更清楚自己的各个线程主要是在执行什么任务,各个任务之间应该怎样协作,应该怎样才能最大的发挥系统的处理效率,因此在UMS的调度下,SQL Server多线程的效率能够得到有效的提升。
UMS管理调度进程的这一套机制,叫做UMS Scheduler(UMS 调度器)。SQL Server启动时,为每个处理器会创建一个UMS scheduler。例如4个处理器的环境中,SQL Server启动时可能创建4个UMS scheduler,如果最大工作线程为255,则每个UMS scheduler最大可负责调度的线程数为64。
当然,SQL Server的UMS并不是完全的重写、替代Windows的调度算法,UMS只是根据自己的需要,对SQL Server线程进行调度,对异步I/O进行更有效的管理。相当于基于Windows的线程管理,实现一个SQL Server用户定义的多线程调度器。UMS可以使用线程模式,也可以使用Windows NT 4.0中引入的fiber模式,线程和fiber的基本管理,都是使用Window的实现。
UMS调度器
每个UMS调度器包含五个组成部分:a worker list, a runnable list, a waiter list, an I/O list, and a timer list。
The Worker List
Worker,指SQL Server工作进程,即SQL Server的threads或fibers。worker list中的workers为当前可用的工作进程。
The Connection Process
每一个client的connection都被分配给某个UMS调度器。分配方式很简单,当有一个新的连接请求时,哪一个UMS调度器的connection最少,新的connection就被分配给这个UMS调度器。一旦这个connection被分配,就一直受指定的UMS调度器管理,就算这个UMS调度器当前很忙,系统中有其它空闲的调度器也不例外。
举个例子,如果服务器有2个CPU,应用程序建立了4个connection,可能出现的情况是,分配给CPU A的UMS调度器的两个connection都需要90%的使用CPU,而分配给CPU B的UMS调度器的两个connection只需要10%的使用CPU。这种情况下,你会发现其中一个CPU一直处于非常高的使用率,而另外的CPU却基本一直在空闲状态。SQL Server无法处理这种负载均衡,我们只能在应用程序的connection层面来均衡负载。
Connection被分配给某个调度器之后,如果Worker List中有可用的worker,将取一个来执行这个connection的请求;如果Worder List中没有可用worker,并且系统的worker数还没有到达最大worker数的设置值,调度器会新建一个worker以执行请求;如果已经达到最大worker数,则这个请求将被放入waiter list中,按FIFO的方式等待分配可用worker。
Work Requests
每一个work requests将交给某个worker来执行。一个worker在执行某个任务时,在这个任务完成之前,这个worker不会接受其它的work requests。处理完当前work request之后,这个worker将接受下一个work request,或者是进入空闲循环中等待下一个work request。一个work request自始至终都是由某个worker完成,这样的处理方式,可以使各个worker之间并不需要进行频繁切换以进行通讯。
一个新的work request被提交之后,如果当前有worker正在执行任务,则会分配另外一个worker给这个请求,并进入runnable list中等待执行。并发请求相当多时,SQL Server很容易就达到最大worker数,这种情况下,新的connection只有在其它的worker空闲下来的时候才能被接受。
如果SQL Server工作在线程模式下,一个worker在空闲15分钟之后将被销毁,当然,系统会保证一个最小worker数量。
The "Runnable" List
Runnable list中的workers是那些已经被指派了某个work request,等待执行的工作进程队列。
Runnabel list中的workers处于永久的等待状态,不受windows调度,只有在相关事件对象接收到信号之后才变成活动状态,从而接受windows调度。那么由谁向这些workers发送信号呢?
从前面操作系统协作式调度方法介绍可以了解,协作式方式下,如果某个任务需要耗费很长的时间,那这段时间内计算机不能做任何其它事情,只能等待这个进程处理结束。要解决这种现象很简单,即每个进程在处理时,中间插入一些其它调用,例如系统中断、操作系统方法等,这样,调度器就可以截取这些调用,完成一些必要的事情,然后再将控制权交回给这个进程。UMS中提供了很多这种调用方法,当前在执行的worker,将根据自己的运行状况,在必要的时候调用UMS提供的方法,以暂时将控制权释放出来,供其它的worker完成一些必要的操作。这些UMS方法被调用时,都会检查runnable list中的workers,给其中某个worker相关事件对象一个信号,以唤醒这个worker开始执行。
虽然SQL Server启动后会创建UMS调度器线程,其实事实上UMS工作过程中并没有一个专门的线程来负责整体的调度,所有的调度控制,都是通过UMS提供的方法,由各个 worker自行调用,在这种机制之下实现全局的调度任务。
因此,如果要回答前面的问题,处于永久等待状态的那些worker,可能会被某一个执行中的worker通过调用UMS方法而唤醒(在耗时比较长的工作过程中,或者是当前工作结束时),继而这个worker 执行过程中或者执行完毕之后,也会调用UMS方法,再去唤醒其它的worker。
The Waiter List
Waiter list中的workers列表,是那些等待某个资源的workers。当某个UMS worker请求一个资源,而这个资源被其它worker占用时,这个worker将被放入waiter list中。占有资源的worker在释放资源的时候,会负责检查waiter list,将等待这个资源的worker转移到runnable list中。之后,当前工作的worker执行完毕,或者中途调用UMS方法释放控制的时候,被转移到runnable list中的这个worker可能会被唤醒开始执行。例如前面提到的,如果SQL Server已经达到最大工作进程数量,这时如果再有新的work request被提交,这些work request将被放入waiter list中,因为当前已经没有可用的worker分配给它们。
The I/O List
I/O list中维护UMS异步I/O请求对象列表。
Windows 98/ME不支持异步I/O,Windows NT系列开始支持。异步I/O通过使用Asynchronous Procedure Calls (APCs)实现,APCs使程序和系统组件在特定线程的上下文中执行代码(即操作系统的内核模式和用户模式下),在不同进程的地址空间内传输数据。在异步I/O中,I/O线程异步的从磁盘读取数据到内存中,结束之后,通过APCs,将读取的数据传给发出异步I/O请求的线程,并让I/O请求线程处理后续任务。一个线程在请求异步I/O操作时,需要做异步I/O初始化,创建并设置一个OVERLAPPED结构,将这个结构传给 ReadFile/ReadFileEx或者WriteFile/WriteFileEx函数。异步操作时,I/O线程将结构内部的状态成员设置为 STATUS_PENDING,以表明异步I/O操作正在进行中,I/O请求线程通过调用HasOverlappedIoCompleted可以查询异步 I/O操作是否结束。OVERLAPPED结构中包含一个APC函数(在异步I/O初始化时创建并设置到结构OVERLAPPED中),这个函数的作用是将I/O操作的结果和相关状态信息,从系统内存空间拷贝到I/O请求线程的虚拟地址空间。异步I/O操作结束后,I/O请求线程查询到 HasOverlappedIoCompleted为true,则调用APC函数,将读取的数据和状态信息转到自己的虚拟地址空间中来,以进行后续处理。
UMS异步I/O请求对象中封装了I/O请求信息,以及OVERLAPPED结构等。
UMS接收到I/O请求时,如果是Win98/ME,将初始化一个同步I/O操作;如果是Windows NT系列,将初始化一个异步I/O操作。之后调用操作系统异步I/O方法,把异步I/O请求对象放入I/O list中。任何worker在结束或者临时调用UMS方法中断时,都要检查I/O list。它遍历I/O list,用异步I/O请求对象的OVERLAPPED成员作为参数,调用HasOverlappedIoCompleted方法。如果某个异步I/O操作已经结束,则将这个请求对象从I/O list中移除,并调用这个异步I/O请求结束的APC函数。从这里可以看到,UMS的异步I/O操作与操作系统不同之处在于,对操作系统,在异步I/O 操作过程中,由I/O请求线程监控其状态,I/O操作结束后,仍然是I/O请求线程来调用APC函数;而对于UMS,异步I/O操作期间,是由当前执行的worker负责对状态进行监控,I/O操作完成后,也是检测到结束信号的worker,通过UMS方法来调用APC函数,处理异步I/O请求的后续操作。如果是在Win98/ME环境中,因为是同步I/O操作,I/O请求对象不会被放入I/O list中,I/O请求线程调用Win32 I/O API,在I/O API返回之后,立即调用I/O请求结束函数。
The Timer List
Timer list维护记时请求。例如某个worker需要等待某个resource一个指定的时间,则这个worker将被放入到timer list中。当前工作进程调用UMS方法中断时,在检查处理完I/O list之后,接着检查timer list。如果其中某个请求已经超过指定的等待时间,工作进程从timer list中移除这个worker,将它放入到runnable list中。
The Idle Loop
当前工作进程结束后调用UMS方法,如果runnable list、I/O list都为空,timer list为空或者其中的请求还没有到达执行时间,则进入idle loop。
Idle loop的一种工作方式:UMS调度器被创建的时候,同时会创建一个windows事件对象,并赋给这个UMS调度器。如果当前工作进程需要进入idle loop,它将使用UMS 调度器的事件对象,和一个时间间隔值,调用WaitForSingleObject方法。这样在这个时间间隔到达的时候,操作系统就会唤醒 UMS调度器的事件对象进入执行状态,这样UMS又开始按照UMS机制进行各项检查。
另外一种方式,是异步I/O操作结束后,也可以将UMS调度器从idle loop中唤醒,执行异步I/O的结束APC函数。因此在idle loop期间,要么是异步I/O操作结束,要么是等待的时间间隔到达,这两个事件任意一个发生时,都会将UMS调度器从idle loop中唤醒。
UMS的这种idle loop方式,使得在空闲时间UMS不占用任何CPU资源。
Going Preemtive
UMS在某些时候需要使用操作系统的抢占式方式工作,例如扩展存储过程。UMS协作式方式下,worker会在特定的点或者时间间隔之后,调用UMS方法,以使SQL Server平稳的运行。而对于扩展存储过程的开发,无法使它按照这种要求来工作,ODS API中也并没有提供使用UMS机制的相关函数,因此,扩展存储过程的执行是在操作系统的调度之下,不受UMS管理。执行扩展存储过程的worker被 UMS忽略,因此,扩展存储过程执行时,同一个处理器上会运行多个UMS worker。
如果扩展存储过程开发不当,致命的错误可能会导致SQL Server进程崩溃。
另外,Linked server、分布式查询等处理,和扩展存储过程一样,在操作系统的调度管理下执行。
Fiber Mode
Windows NT系列支持fiber mode。Fiber可以看作是一些轻量级的线程,fiber模式下,多个fiber共用一个线程,这种处理方式,一方面利用了多线程的优点,另一方面也在一定程度上避免了线程上下文间切换的一些额外开销。
SQL Server工作在fiber模式下时,一个worker就是一个fiber,因此多个worker会共用一个线程。这种模式下的某些操作与线程模式会不一样,例如扩展存储过程、使用linked server等。因为fiber模式下多个worker共用一个线程,而扩展存储过程等执行时需要一个单独的线程,并由windows进行调度,如果仍使用正常的fiber模式来处理,则执行扩展存储过程等处理时,该线程相关的其它fiber会被block。因此fiber模式下,SQL Server使用隐藏的调度器(用DBCC SQLPERF无法查询到的调度器)单独的处理扩展存储过程等操作。
隐藏调度器
象备份、恢复等操作,需要进行大量的异步I/O操作,操作所需时间长、耗用资源大。如果UMS在正常的机制下来处理这些操作,将会影响这些UMS调度器相关的所有worker的性能。
SQL Server创建了一些隐藏调度器,专门用于处理这些类型的操作。这样,SQL Server在进行这些操作时,可以由操作系统在多个处理器之间进行协调,从而避免对个别UMS调度器的block。
DBCC SQLPERF(umsstats)
未公开的命令,用来查看UMS调度器相关信息,包括相关调度器总的用户数、workers、runnable list中的workers数量,等等。
参考
Inside the SQL Server 2000 User Mode Scheduler
Microsoft Windows Internals - Fouth Edition
抢占式(Preemptive Scheduling)与协作式(Cooperative Scheduling)
在任务调度方面,Windows 95、98使用协作式,Windows NT系列、2000等,使用抢占式方式。协作式方式下,操作系统将处理器分配给某个进程之后,处理器完全由进程自己控制,包括在何时释放以供其它进程使用等。这样如果当前占有处理器的进程崩溃,并且不能将处理器释放出来,此时别的任何进程都无法获得处理器使用权,整个操作系统就崩溃了。因此在Win 95、98下,经常会死机,系统没有任何响应,只有重启系统才可以解决。抢占式方式下,处理器完全在操作系统的进程调度控制之下。操作系统将处理器在时间上分成一个个时间片(Slice),每次将一个时间片分配给某个进程,如果这个时间片结束,不管工作进程有没有完成自己的工作,操作系统都会将这个进程挂起,将随后的时间片按照进程优先级顺序,分配给其它的进程。这种方式,只要操作系统内核没有Bug,硬件驱动不宕机,就永远也不会导致系统崩溃,因为只要被分配的时间片结束,操作系统的进程调度就取得了处理器的控制权。例如Win 2000之后使用抢占式方式进行进程调度,比较少出现应用程序导致操作系统死机无响应的情况发生。但是这种方式下,为了实现任意时刻都可以将某进程挂起,将其它进程唤醒,这种频繁的在进程间进行切换,尤其是当进程间需要通讯、协作时,需要很多额外的机制和开销,使得处理器的整体利用率降低。
SQL Server使用多线程工作,在抢占式方式下,各个并行线程(当然这里的并行是指对应用而言,对用户角度而言)的执行在时间顺序上应用程序是无法把握的,完全由操作系统调度管理。并行的多线程之间的数据共享、线程间的通讯、协作等,都需要使用各种机制予以解决。这些机制将导致SQL Server这种处理量大、实时性要求相当高的应用效率降低。为了避免上述情况,SQL Server的UMS使用协作式方式管理自己的多线程。
UMS怎样调度
在操作系统的抢占式调度策略下,SQL Server如何避免自己的多线程被操作系统调度?在这方面,SQL Server使用一种欺骗方式。Windows认为,某些线程是非活动状态的,它们处于永久的等待状态,对这种线程,Windows不会考虑对之进行调度。SQL Server利用了这个机制,UMS确保任意时刻只有一个活动状态的工作线程,其它线程都处于永久的等待状态,这样,操作系统就不会考虑对SQL Server的多线程进行调度,多线程的调度完全在UMS的控制之下(注意,当前活动的SQL Server工作线程还是受Windows调度,只是在这种机制下,Windows认为SQL Server是单线程应用程序)。所有的SQL Server线程,都有一个相关的事件对象,通过传入INFINITE参数调用相关事件对象的WaitForSingleObject方法,可以使这个线程进入永久的等待状态;而通过给相关事件对象信号,可以唤醒这个线程而进入执行状态。活动线程的工作结束时,会使自己进入到永久的等待状态,并给后续线程相关事件对象信号,唤醒后续线程进入活动状态。SQL Server使用这种机制,保证在每个处理器上,只有一个当前的活动线程。
SQL Server比操作系统更清楚自己的各个线程主要是在执行什么任务,各个任务之间应该怎样协作,应该怎样才能最大的发挥系统的处理效率,因此在UMS的调度下,SQL Server多线程的效率能够得到有效的提升。
UMS管理调度进程的这一套机制,叫做UMS Scheduler(UMS 调度器)。SQL Server启动时,为每个处理器会创建一个UMS scheduler。例如4个处理器的环境中,SQL Server启动时可能创建4个UMS scheduler,如果最大工作线程为255,则每个UMS scheduler最大可负责调度的线程数为64。
当然,SQL Server的UMS并不是完全的重写、替代Windows的调度算法,UMS只是根据自己的需要,对SQL Server线程进行调度,对异步I/O进行更有效的管理。相当于基于Windows的线程管理,实现一个SQL Server用户定义的多线程调度器。UMS可以使用线程模式,也可以使用Windows NT 4.0中引入的fiber模式,线程和fiber的基本管理,都是使用Window的实现。
UMS调度器
每个UMS调度器包含五个组成部分:a worker list, a runnable list, a waiter list, an I/O list, and a timer list。
The Worker List
Worker,指SQL Server工作进程,即SQL Server的threads或fibers。worker list中的workers为当前可用的工作进程。
The Connection Process
每一个client的connection都被分配给某个UMS调度器。分配方式很简单,当有一个新的连接请求时,哪一个UMS调度器的connection最少,新的connection就被分配给这个UMS调度器。一旦这个connection被分配,就一直受指定的UMS调度器管理,就算这个UMS调度器当前很忙,系统中有其它空闲的调度器也不例外。
举个例子,如果服务器有2个CPU,应用程序建立了4个connection,可能出现的情况是,分配给CPU A的UMS调度器的两个connection都需要90%的使用CPU,而分配给CPU B的UMS调度器的两个connection只需要10%的使用CPU。这种情况下,你会发现其中一个CPU一直处于非常高的使用率,而另外的CPU却基本一直在空闲状态。SQL Server无法处理这种负载均衡,我们只能在应用程序的connection层面来均衡负载。
Connection被分配给某个调度器之后,如果Worker List中有可用的worker,将取一个来执行这个connection的请求;如果Worder List中没有可用worker,并且系统的worker数还没有到达最大worker数的设置值,调度器会新建一个worker以执行请求;如果已经达到最大worker数,则这个请求将被放入waiter list中,按FIFO的方式等待分配可用worker。
Work Requests
每一个work requests将交给某个worker来执行。一个worker在执行某个任务时,在这个任务完成之前,这个worker不会接受其它的work requests。处理完当前work request之后,这个worker将接受下一个work request,或者是进入空闲循环中等待下一个work request。一个work request自始至终都是由某个worker完成,这样的处理方式,可以使各个worker之间并不需要进行频繁切换以进行通讯。
一个新的work request被提交之后,如果当前有worker正在执行任务,则会分配另外一个worker给这个请求,并进入runnable list中等待执行。并发请求相当多时,SQL Server很容易就达到最大worker数,这种情况下,新的connection只有在其它的worker空闲下来的时候才能被接受。
如果SQL Server工作在线程模式下,一个worker在空闲15分钟之后将被销毁,当然,系统会保证一个最小worker数量。
The "Runnable" List
Runnable list中的workers是那些已经被指派了某个work request,等待执行的工作进程队列。
Runnabel list中的workers处于永久的等待状态,不受windows调度,只有在相关事件对象接收到信号之后才变成活动状态,从而接受windows调度。那么由谁向这些workers发送信号呢?
从前面操作系统协作式调度方法介绍可以了解,协作式方式下,如果某个任务需要耗费很长的时间,那这段时间内计算机不能做任何其它事情,只能等待这个进程处理结束。要解决这种现象很简单,即每个进程在处理时,中间插入一些其它调用,例如系统中断、操作系统方法等,这样,调度器就可以截取这些调用,完成一些必要的事情,然后再将控制权交回给这个进程。UMS中提供了很多这种调用方法,当前在执行的worker,将根据自己的运行状况,在必要的时候调用UMS提供的方法,以暂时将控制权释放出来,供其它的worker完成一些必要的操作。这些UMS方法被调用时,都会检查runnable list中的workers,给其中某个worker相关事件对象一个信号,以唤醒这个worker开始执行。
虽然SQL Server启动后会创建UMS调度器线程,其实事实上UMS工作过程中并没有一个专门的线程来负责整体的调度,所有的调度控制,都是通过UMS提供的方法,由各个 worker自行调用,在这种机制之下实现全局的调度任务。
因此,如果要回答前面的问题,处于永久等待状态的那些worker,可能会被某一个执行中的worker通过调用UMS方法而唤醒(在耗时比较长的工作过程中,或者是当前工作结束时),继而这个worker 执行过程中或者执行完毕之后,也会调用UMS方法,再去唤醒其它的worker。
The Waiter List
Waiter list中的workers列表,是那些等待某个资源的workers。当某个UMS worker请求一个资源,而这个资源被其它worker占用时,这个worker将被放入waiter list中。占有资源的worker在释放资源的时候,会负责检查waiter list,将等待这个资源的worker转移到runnable list中。之后,当前工作的worker执行完毕,或者中途调用UMS方法释放控制的时候,被转移到runnable list中的这个worker可能会被唤醒开始执行。例如前面提到的,如果SQL Server已经达到最大工作进程数量,这时如果再有新的work request被提交,这些work request将被放入waiter list中,因为当前已经没有可用的worker分配给它们。
The I/O List
I/O list中维护UMS异步I/O请求对象列表。
Windows 98/ME不支持异步I/O,Windows NT系列开始支持。异步I/O通过使用Asynchronous Procedure Calls (APCs)实现,APCs使程序和系统组件在特定线程的上下文中执行代码(即操作系统的内核模式和用户模式下),在不同进程的地址空间内传输数据。在异步I/O中,I/O线程异步的从磁盘读取数据到内存中,结束之后,通过APCs,将读取的数据传给发出异步I/O请求的线程,并让I/O请求线程处理后续任务。一个线程在请求异步I/O操作时,需要做异步I/O初始化,创建并设置一个OVERLAPPED结构,将这个结构传给 ReadFile/ReadFileEx或者WriteFile/WriteFileEx函数。异步操作时,I/O线程将结构内部的状态成员设置为 STATUS_PENDING,以表明异步I/O操作正在进行中,I/O请求线程通过调用HasOverlappedIoCompleted可以查询异步 I/O操作是否结束。OVERLAPPED结构中包含一个APC函数(在异步I/O初始化时创建并设置到结构OVERLAPPED中),这个函数的作用是将I/O操作的结果和相关状态信息,从系统内存空间拷贝到I/O请求线程的虚拟地址空间。异步I/O操作结束后,I/O请求线程查询到 HasOverlappedIoCompleted为true,则调用APC函数,将读取的数据和状态信息转到自己的虚拟地址空间中来,以进行后续处理。
UMS异步I/O请求对象中封装了I/O请求信息,以及OVERLAPPED结构等。
UMS接收到I/O请求时,如果是Win98/ME,将初始化一个同步I/O操作;如果是Windows NT系列,将初始化一个异步I/O操作。之后调用操作系统异步I/O方法,把异步I/O请求对象放入I/O list中。任何worker在结束或者临时调用UMS方法中断时,都要检查I/O list。它遍历I/O list,用异步I/O请求对象的OVERLAPPED成员作为参数,调用HasOverlappedIoCompleted方法。如果某个异步I/O操作已经结束,则将这个请求对象从I/O list中移除,并调用这个异步I/O请求结束的APC函数。从这里可以看到,UMS的异步I/O操作与操作系统不同之处在于,对操作系统,在异步I/O 操作过程中,由I/O请求线程监控其状态,I/O操作结束后,仍然是I/O请求线程来调用APC函数;而对于UMS,异步I/O操作期间,是由当前执行的worker负责对状态进行监控,I/O操作完成后,也是检测到结束信号的worker,通过UMS方法来调用APC函数,处理异步I/O请求的后续操作。如果是在Win98/ME环境中,因为是同步I/O操作,I/O请求对象不会被放入I/O list中,I/O请求线程调用Win32 I/O API,在I/O API返回之后,立即调用I/O请求结束函数。
The Timer List
Timer list维护记时请求。例如某个worker需要等待某个resource一个指定的时间,则这个worker将被放入到timer list中。当前工作进程调用UMS方法中断时,在检查处理完I/O list之后,接着检查timer list。如果其中某个请求已经超过指定的等待时间,工作进程从timer list中移除这个worker,将它放入到runnable list中。
The Idle Loop
当前工作进程结束后调用UMS方法,如果runnable list、I/O list都为空,timer list为空或者其中的请求还没有到达执行时间,则进入idle loop。
Idle loop的一种工作方式:UMS调度器被创建的时候,同时会创建一个windows事件对象,并赋给这个UMS调度器。如果当前工作进程需要进入idle loop,它将使用UMS 调度器的事件对象,和一个时间间隔值,调用WaitForSingleObject方法。这样在这个时间间隔到达的时候,操作系统就会唤醒 UMS调度器的事件对象进入执行状态,这样UMS又开始按照UMS机制进行各项检查。
另外一种方式,是异步I/O操作结束后,也可以将UMS调度器从idle loop中唤醒,执行异步I/O的结束APC函数。因此在idle loop期间,要么是异步I/O操作结束,要么是等待的时间间隔到达,这两个事件任意一个发生时,都会将UMS调度器从idle loop中唤醒。
UMS的这种idle loop方式,使得在空闲时间UMS不占用任何CPU资源。
Going Preemtive
UMS在某些时候需要使用操作系统的抢占式方式工作,例如扩展存储过程。UMS协作式方式下,worker会在特定的点或者时间间隔之后,调用UMS方法,以使SQL Server平稳的运行。而对于扩展存储过程的开发,无法使它按照这种要求来工作,ODS API中也并没有提供使用UMS机制的相关函数,因此,扩展存储过程的执行是在操作系统的调度之下,不受UMS管理。执行扩展存储过程的worker被 UMS忽略,因此,扩展存储过程执行时,同一个处理器上会运行多个UMS worker。
如果扩展存储过程开发不当,致命的错误可能会导致SQL Server进程崩溃。
另外,Linked server、分布式查询等处理,和扩展存储过程一样,在操作系统的调度管理下执行。
Fiber Mode
Windows NT系列支持fiber mode。Fiber可以看作是一些轻量级的线程,fiber模式下,多个fiber共用一个线程,这种处理方式,一方面利用了多线程的优点,另一方面也在一定程度上避免了线程上下文间切换的一些额外开销。
SQL Server工作在fiber模式下时,一个worker就是一个fiber,因此多个worker会共用一个线程。这种模式下的某些操作与线程模式会不一样,例如扩展存储过程、使用linked server等。因为fiber模式下多个worker共用一个线程,而扩展存储过程等执行时需要一个单独的线程,并由windows进行调度,如果仍使用正常的fiber模式来处理,则执行扩展存储过程等处理时,该线程相关的其它fiber会被block。因此fiber模式下,SQL Server使用隐藏的调度器(用DBCC SQLPERF无法查询到的调度器)单独的处理扩展存储过程等操作。
隐藏调度器
象备份、恢复等操作,需要进行大量的异步I/O操作,操作所需时间长、耗用资源大。如果UMS在正常的机制下来处理这些操作,将会影响这些UMS调度器相关的所有worker的性能。
SQL Server创建了一些隐藏调度器,专门用于处理这些类型的操作。这样,SQL Server在进行这些操作时,可以由操作系统在多个处理器之间进行协调,从而避免对个别UMS调度器的block。
DBCC SQLPERF(umsstats)
未公开的命令,用来查看UMS调度器相关信息,包括相关调度器总的用户数、workers、runnable list中的workers数量,等等。
参考
Inside the SQL Server 2000 User Mode Scheduler
Microsoft Windows Internals - Fouth Edition