2015年(2)
分类: C/C++
2015-06-16 17:07:59
半同步半异步I/O的设计模式(half sync/half async)
在半同步半异步系统体系中,操作系统内核响应设备的中断,执行异步的I/O。而用户级的应用程序进行同步的I/O。这正是“半同步半异步”这个名字的由来,这个结构满足下面的两个需要。
1) 编程实现简单
异步I/O模型中,中断随时触发输入和输出操作,编程复杂。使用异步I/O模型,当中断处理例程拥有线程控制权时,会产生非常麻烦的时序和竟争问题。而且,使用中断机制的程序要求额外的数据结构,这个数据结构用于在异步事件发生的时候保存进程上下文状态。并且,程序执行的时候,外部的事件会在不固定的时间发生,程序不容易调试。
与其相比,使用同步I/O模型的时候,I/O操作在确定的点发生,编程实现要容易的多。此外,同步I/O操作的程序会阻塞等待I/O操作的完成。进程运行上下文的活动记录自动在运行栈中保存,不必使用独立的数据结构。因此,为了让编程简单,有强烈的理由使用同步I/O模型。
2) 程序执行高效
在中断驱动的设备上,异步I/O模型运用带来了高效率。异步I/O让通信和计算同时进行。并且,因为程序运行状态的数据量相对较小,上下文切换的消耗被最小化。因此,为了提高运行的性能,也有强烈理由使用异步I/O模型。
与其相比,如果每种资源的事件(例如网卡,终端和计时器)占用一个独立的活动对象(进程或线程),一个完全同步I/O模型效率会低。每个活动对象包含多个资源(例如栈,寄存器),每种资源都会让它阻塞,等待资源事件的发生。在创建,调度,分发和终止这些独立的活动对象,会消耗更多的时间和空间。
综合同步异步的有优点,就有了半同步半异步的设计模式。这个模式中,高层使用同步I/O模型,简化编程。低层使用异步I/O模型,提高IO利用率并高效执行。half sync/half async可以很好的使得"编程复杂度"和"执行效率"之间达到一种平衡。
半同步半异步模式适合运用于如下场景:
1) 在一个系统中的进程中,系统必须响应和处理外部异步发生的事件,对于底层I/O如果为每一个外部资源的事件分派一个独立的线程同步处理I/O,任务个数无法确定并且资源利用率很低。对于上层的任务以同步方式处理I/O,实现起来简单。这时低层使用异步I/O模型,提高资源利用率和高效执行,高层使用同步I/O模型,简化编程。
2) 任务的触发响应必须在单独的控制线程中执行,而任务的处理可以在多线程中执行。底层的任务的触发响应(如网络控制器的中断处理)使用异步I/O模型,提高了执行效率。而上层的任务处理(如:数据库查询,文件传输)使用同步I/O模型,简化了编写并行程序的难度。
一般情况下,上层的任务要比下层的任务处理内容多,使用一个简单的同步处理层实现异步处理的复杂性,可以对外隐藏异步处理的细节。另外,同步层次和异步层次任务间的通信使用一个消息队列来协调。
半同步/半异步IO模型的设计可以分为三层,分别是同步任务层,消息队列层,异步任务层。
1) 同步任务层(用户级的进程)
本层的任务完成上层的I/O操作,使用同步I/O模型,处理的任务消息来自消息队列层。和异步层不同,同步层的任务使用活动对象执行,这些活动对象有自己运行栈和寄存器状态。当执行同步I/O的时候,他们会被阻塞/睡眠。
2) 消息队列层
这个层在同步任务层和异步任务层之间,提供了控制消息缓存的功能。异步任务的I/O 事件被缓存到消息队列中,同步任务层在队列中提取这些事件并处理;反之,同步任务层的执行结果消息缓存到消息队列中,异步任务层在队列中提取这些事件并处理。
3) 异步任务层
处理低层的事件,这些事件由多个外部的事件源产生(例如网卡,终端)。和同步任务不同,此层的实体是被动对象,没有自己的运行栈,要求不能被阻塞。
分层协作图展现了一个动态的过程:当源事件到达通知到异步层后,半同步半异步模式的各个组成单元协作和处理。把协作的过程分成下面三个阶段:
1) 异步阶段,通过中断机制或异步事件通知,异步任务层获取事件,并进行异步处理,最后添加任务到消息队列层;
2) 排队阶段,消息队列层的队列提供了一个同步控制机制,响应输入事件,缓存同步层和异步层之间的消息;
3) 同步阶段,同步任务层在消息队列层提取任务,并对任务进行同步处理。
同步层和异步层之间的通信使用生产者/消费者模型。理解这个模型的关键是:完成同步任务的是活动对象。他们可以在任意时刻阻塞调用read或在write。如果数据没有准备好,这些活动对象可以一直等待。相反的,异步任务层的实体是被动对象。他们不能被阻塞。这些对象被通知或外部对象触发。
半同步半异步系统优点:
1) 上层的任务被简化,这是因为不需要再面对底层的异步I/O。复杂的并发控制,中断处理和计时操作都委托给异步任务层。异步层负责处理底层的细节,如,异步系统编程的中断处理。异步任务层管理与硬件相关的组件间(如DMA,内存管理,设备寄存器)的交互;
2) 不同层可以使用不同的数据管理策略,每一层不必使用相同的并行控制方式。例如,在单线程的BSD UNIX内核中,异步任务层使用低级的机构(如修改CPU的中断级别)。与此不同,用户级的进程使用高级的同步机构(如使用信号量,消息队列,条件变量,记录锁);
3) 层间的通信被限制在消息队列层,队列层对两层之间的消息进行缓存,如果直接通信,同步和异步任务层直接访问对方的内存,就必须使用复杂的锁和时序控制;
4) 在多处理器环境中提高了性能,使用同步的I/O可以简化编程和提高性能。例如,长时间的数据传输(如从数据库中加载一个大的医学图象)使用同步I/O效率更高。因为一个处理器单独为一个线程使用,可以更有效的使用CPU的指令和数据的缓存。
半同步半异步系统缺点:
1) 跨边界导致的性能消耗,这是因为同步控制,数据拷贝和上下文切换会过度地消耗资源。在同步和异步任务层之间使用队列层传送数据的时候,这种消耗往往会发 生。特别是,许多使用半同步半异步模式的操作系统,把队列层放到用户和内核域的边界上。到跨越这个边界的时候,往往会有明显的性能消耗。例如,BSD UNIX的套接口层,在很大比例上造成了TCP/IP网络的过载;
2) 上层任务使用同步IO实现,依赖与系统接口的设计,可能导致上层无法直觉使用低层的异步I/O设备。因此,如果外部的设备支持异步的重叠的I/O方式,系统的I/O结构会防碍应用程序的高效使用。
这一节描述如何实现半同步半异步模式,系统中分成了同步任务层和异步任务层,两层之间使用队列层通信。
1) 找到消耗时间的长时任务,使用同步I/O实现他们。
使用同步I/O方式,可以让系统任务的实现简单化,而系统中经常有会时间长的任务,如执行大量数据流传输,或者是等待服务器响应的数据查询。
使用活动对象模型实现这些长时任务。活动对象拥有自己的运行栈和寄存器状态,在执行同步I/O的时候可以被阻塞。实现活动对象结构,要求一个控制线程的切换机制,需要一个地方存放和恢复线程的状态(如寄存器的值,栈指针),这些功能足够实现一个非抢占的,没有内存保护的线程机构。“用户级线程”一般实现了此类的功能(译者:操作系统都实现了这个功能)。
然而,在一个要求健壮的多任务系统中,要使用进程或线程的方式实现活动对象,需要更多的功能。这种情况下,每种线程需要自己的线程(或进程)空间,这些空间被处理器的内存管理单元(MMU)管理。当线程(或进程)切换的时候,新的地址空间信息需要加载到MMU中。切换时还需要清空高速缓存,特别是高速缓存使用虚拟地址的情况下。除了地址空间,一个操作系统进程还可能包含一个“用户ID”。这个标识告诉操作系统这个进程的访问权限和占用的系统资源。
为了防止一个单独的线程(或进程)永久的占用系统,要有抢占的机制。经常使用计时器实现,计时器周期性的产生时钟中断。在中断处理期间,操作系统检查当前的进程是否应该被抢占。如果应该被抢占,就保存当前进程的状态,加载下一个进程的状态运行。
2) 找到短时任务,使用异步I/O实现。
系统中的一些任务不能阻塞过长的时间。这些任务一般和外界事件完成一个短期的交互过程(如图形用户界面或中断驱动的网络接口器),为了提高效率和保证响应速度,这些外部事件的处理一定不能阻塞。 实现这样的短期任务,要使用被动对象模型。被动对象使用别人的控制线程(调用者的线程或者一个独立的中断栈)。因为他们不能长时间的等待,不能被阻塞。不阻塞的主要目标是保证其响应时间(如高优先级的硬件中断,如时钟中断)。
实现一个结构清晰的异步I/O框架,有多种方式。
使用反应堆模式实现多路事件的处理,反应堆模式使用一个单线程的处理循环,把多路的事件派发给多个处理者。这个模式组合了单线程处理循环的简单性和面向对象编程提供的可扩展性。反应堆模式在一个线程(或进程)中进行顺序的消息处理,常用来消除多线程同步和加锁处理的复杂性。
一个反应堆可以使用在同步或者异步的事件源上。但是它支持的事件处理者的行为要求是异步的。也就是说,为了不影响其它事件源的响应效率,事件处理者是不能阻塞的。
实现一个多级的中断处理机构。这种机构下,当更高级别的任务(如硬中断)要求处理的时候,当前的进程可以被中断。为了防止共享的状态访问时被破坏,异步层使用的数据结构必须被保护(例如提升处理器级别或使用信号量)。
例如,在一个操作系统内核中,硬件中断的服务时间很大程度上决定了对多级中断处理机构的需要。如果这个时间能够显著的减少,可以把所有的处理放到硬中断的层次上,这样就可以避免软中断的过度资源消耗。TCP/IP的实现中,就减少输入包的协议处理时间耗费,让所有的包处理过程可以在两级中断机构实现。
3) 实现一个队列层
队列层包含了一个同步控制点,缓存同步任务和异步任务之间交换的消息。在实现队列层的时候,要注意下面几点:
并行控制。如果同步任务和异步任务的执行是并行的(如使用多CPU还是硬件中断),为了避免争用,共享的队列的状态变化必须是连续的。因此,实现队列层 的时候,经常使用并行控制机制,如信号量,互斥体和条件变量等。当消息在队列中插入或删除的时候,并行控制保证队列的内部数据结构不被破坏。
层与层之间的流量控制。在队列层缓存消息,系统不能提供无限的资源。因此,必须控制同步和异步层之间传输的数据量。例如,在层与层的流控制下,避免同步层的数据量超过网络接口能够传输的极限。
同步任务可以被阻塞。因此,可以使用下面的策略:如果其排队超过一定的量,可以先让任务阻塞。当异步任务层把排队数降低到一定的水平,就可以重新唤醒同步任务继续执行。相对地,异步层的任务不能被阻塞,当处理过量的数据时,队列层要根据策略丢弃消息。这种情况下,如果使用了一个可靠的,面向连接的网络协议, 发送者最终会发现传输超时,要求重新传输数据。
数据拷贝消耗。一些系统(如BSD UNIX),把队列层放到了用户和内核之间的保护边界上。为了分离不同的保护域,一般使用拷贝数据的方法。然而,这增加了系统总线和内存的负担。当大数据量的消息传输的时候,这可能会降低很大的性能。
一种减少数据拷贝的方式:分配一个专用的内存区,这个内存区被同步任务层和异步任务层共享。这样,两层之间可以高效率的交换数据,不需要拷贝。例如,存在一个I/O子系统,使用轮询的中断机制改进连续的I/O流处理,最小化跨边界数据传送的消耗。这种方法同时提供了一个缓存管理系统,准许高效率的页映射,一个由用户进程,内核,设备使用的共享内存机构。