【it168 专稿】进程、线程是Windows CE内核最基本的服务,也是内核最主要的组成部分。这两方面的知识是一个嵌入式软件开发人员必须掌握的基础知识,只有掌握了这些知识,才能够充分利用Windows CE系统内核提供的服务,使所开发的嵌入式应用软件更稳定、更健壮。
一般来说,每一种使用线程的操作系统中都存在线程死锁问题,我在Windows CE.NET程序开发中就曾碰到死锁的问题,并为此一度束手无策。本文分享我在Windows CE.NET系统开发中对线程死锁的一些认识。
一.什么是Windows CE.NET线程死锁?
(1)什么是线程
在谈到线程死锁的时候,我们首先必须了解什么是线程。Windows CE.NET是一个抢占多任务操作系统,抢占多任务又称为调度。在调度过程中,内核的调度系统包含一个当前所有进程中线程的优先级列表,并对所有的线程按优先级排列顺序。当中断发生时,调度系统重新安排所有线程的排列顺序。一个线程是一个执行单元,它控制CPU执行进程中某一段代码段。Windows CE.NET最多可支持32个进程同时运行,这是由整个系统分配给所有进程的总地址空间决定的。
Windows CE.NET操作系统具有实时性,所以调度系统必须保证高优先级线程先运行,低优先级线程在高优先级线程终止后或者阻塞时才能得到CPU时间片。而且一旦发生中断,内核会暂停低优先级线程的运行,让高优先级线程继续运行,直到终止或者阻塞。简单的说是:具有相同优先级的线程平均占有CPU时间片,当一个线程使用完了 CPU时间片或在时间片内阻塞、睡眠,那么其他相同优先级的线程才会占有时间片。
线程有五种状态,分别为运行、挂起、睡眠、阻塞、终止。Windows CE.NET不像其他Windows操作系统将进程分为不同的优先级类,Windows CE.NET只将线程分为256个优先级。0优先级最高,255最低,0到248优先级属于实时性优先级,一般分配给实时性应用程序、驱动程序、系统程序。248到 255优先级一般分配给普通应用程序线程使用,其中251优先级(THREAD_PRIORITY_NORMAL)是正常优先级,255优先级(THREAD_PRIORITY_IDLE)为空闲优先级,249优先级(THREAD_PRIORITY_HIGHEST)是高优先级。
(2)什么是线程死锁
嵌入式系统中,因系统的资源分配策略不当,最常见的是进程因资源竞争不当而产生死锁的现象。所谓死锁(Dead Lock)是指两个或两个以上的并发进程在执行过程中,如果每个进程持有某种资源而又都在等待别的进程释放它们现在保持着的资源,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法进行下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。例如,如果线程A锁住了记录1并等待记录2,而线程B锁住了记录2并等待记录1,这样两个线程就发生了死锁现象。
二. Windows CE.NET的线程死锁机制
(1)资源同步与竞争
在多数情况下,线程之间要相互通信、相互协调才能完成任务。比如,当有多个线程共同访问同一个资源时,就必须保证一个线程正读取这个资源数据的时候,其它线程不能够修改它,这就需要线程之间相互通信,了解对方的行为。因此, Windows CE.NET线程之间的通信需要同步。一般在用户模式下的同步有,如:互锁函数Interlocked(属原子访问),临界区Critical Section(保证临界区内的资源不被其它线程访问),还有其它几种内核模式下的同步:事件对象Event(线程睡眠,而内核执行等待),互斥对象Mutex(类似于临界区,但相对较慢),信标对象Semaphore(用于限制资源访问数量),消息队列MsgQueue(利用很小的内存传递消息)等。
在Windows CE.NET多任务实时系统中,当两个或更多独立线程同时访问同一资源时,就出现了资源竞争。资源竞争的影响因素是多种多样的,取决于具体的情况。资源竞争使到多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而由于线程被无限期地阻塞,因此程序就不能正常运行。一个通俗的例子:如在人行道上两个人迎面相遇,为了给对方让道,两人同时向一侧迈出一步,双方无法通过,又同时向另一侧迈出一步,这样还是无法通过。假设这种情况一直持续下去,这样就会发生“资源同步与竞争”的死锁现象。
(2)Windows CE的线程死锁原理
在Windows CE.NET共享资源的系统中,防止访问冲突极为重要。一般来说,我们可以通过锁定一个资源来防止任何其它线程访问这个资源,以避免资源竞争,但这有可能导致另一个问题:死锁。所以,导致死锁的根源在于不适当的对特定对象访问的线程管理。
正常来说,被允许执行的线程首先必须拥有对变量或对象的排他性的访问权。当线程访问对象时,线程会给访问对象加锁,而这个锁会导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。在Windows CE.NET系统中,如果一个线程不能得到需要的某个资源,它将挂起执行(阻塞),直到该资源有效为止。因此,在Windows CE系统运行过程中,各线程都将会对资源进行锁定或解锁。
但由于各线程运行和指向其资源的相对时序各不相同,有可能出现由于各个线程正在等待被其它线程保持的资源,从而导致所有线程都无法运行的情况。例如,有1台打印机和1个输入逻辑接口,有2个进程p1和p2,它们分别占用打印机和逻辑接口。当p1申请已被p2占用的逻辑接口,而p2申请已被p1占用的打印机这种情况出现时,p1和p2僵持不下,称为死锁进程。此时操作系统中虽然有死锁进程出现,但系统中其他进程仍然正常运行。如果系统中所有进程都进入死锁,即没有一个进程能前进的话,称为系统死锁,这种情况是很少见的。我们平常说的“死锁”一般指进程死锁。
线程死锁是多线程环境中一个锁定资源的问题,只有当以下四个条件同时具备时,才会发生死锁。①相互排除---每次只有一个线程可以使用某个锁定的资源;②非先占---其它线程不能强迫另一个线程释放资源;③保持并等待---线程在等待需要的其它任何资源时,保持它们已经锁定的资源;④循环等待---存在一个线程循环链,其中每个线程保持链中下一个线程所需要的资源。这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
三. Windows CE线程死锁的解决方法
(1)通过设计预防死锁
当我们理解了线程死锁的原因,尤其是产生死锁的四个必要条件后,就可以最大可能地避免、预防和解除死锁。所以,在Windows CE的系统设计、进程调度等方面应注意不让这四个必要条件成立,避免进程永久占据资源。此外,也要防止进程在处于等待状态的情况下占用资源。在系统运行过程中,对进程发出的每一个资源申请都进行动态检查,并根据检查结果决定是否分配资源,若分配后可能发生死锁,则不予分配,否则予以分配。
因此,在多任务、多线程应用程序开始编程之前就详细设计,避免线程死锁是最好的方法。例如,代码审查就是一个有效的方法之一,通过破坏四个必要条件中的一个或多个以预防系统发生线程死锁。
(2)建立资源分配图避免死锁
线程死锁产生与进程对资源的需求、进程的执行速度、资源的分配策略有关。如果无法通过设计来避免死锁,则应该建立资源分配图。通过仔细跟踪系统中的所有线程和它们锁定的共享资源,周期性地进行检查和维护资源分配图,及时发现循环等待的特征,从而识别潜在死锁。
建立资源分配图需要识别每个受保护的共享资源,以及指向其中某一资源的所有线程。可以采用这几个过程步骤:①识别所有可能阻塞的系统调用,因为每个受保护的共享资源总是有一些与访问它有关的阻塞调用。②识别出获取共享资源的阻塞调用之后,在源代码中查找它们的各次调用情况。③对于每次调用,记录下指向资源的线程名称和该资源的名称。通常调用本身将受保护的资源作为一个参数来传递,通过这种方式可以识别出所有受保护的资源以及分配资源的线程。④建立资源分配图,并检查是否有任何资源存在循环路径。
如果预先确定每一个共享资源并建立分配图是不实际或不可能的,此时可以增加一些额外的代码,以便在系统运行时检测出潜在的死锁。许多不同的算法都致力于优化这个检测过程,但本质上它们几乎都动态地建立某种资源分配图。只要有线程请求、分配或释放资源,分配图就会被修改和检测,从而能确定是否存在潜在死锁的循环路径。
(3)利用内核跟踪程序工具检测死锁
有一些工具可以用来帮助发现代码中的死锁。例如远程内核跟踪程序(Kernel Tracker),它可用于检测运行设备上的进程、线程和中断之间的交互作用关系,还可用于定位和检测死锁情况。因此,开发人员通过采用工具来对代码进行统计分析,可以识别引起竞争条件和死锁的许多原因。
(4)检测并解除死锁
检测到某个死锁之后,唯一的克服方法是强迫线程释放关键的资源。通常,这意味着中断正保持着所需资源的线程,对于某些应用,这种方法可能是无法接受的。另一个有用的解决方案是在运行时收集资源分配情况并进行事后分析处理,以确定在程序运行过程中是否有死锁情况发生。尽管这种方法并不能防止在运行时发生死锁,但它有助于在死锁出现后发现问题并进行修复。
线程死锁解除通常有如下做法:①撤销处于死锁状态的进程并收回它们的资源。具体地说,选择占用资源多的进程作为撤销对象或者选择撤销代价最小的那个进程。如果撤销一个进程不足以解除死锁,则继续再选另一个撤销对象,直到解除死锁。②资源剥夺方法。即从死锁进程中选一个进程,剥夺它的资源但不撤销它。让这些资源分配给别的死锁进程。反复做这一工作直到死锁解除。由于进程未被撤销,解除死锁的代价比较小。③进程回退。即让一个或多个进程回退到足以解除死锁的地步。它比资源剥夺法温和,因为进程回退时是自愿释放资源而不是被剥夺资源,回退方法要求Windows CE.NET系统保持更多的有关进程运行的历史信息。