MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

饥饿进程长时间得不到服务的原因探究

2021-11-306.7k 阅读

进程调度基础

在深入探讨饥饿进程长时间得不到服务的原因之前,我们先来回顾一下操作系统中进程调度的基本概念。进程调度是操作系统内核的重要功能之一,它负责在多个就绪进程中选择一个合适的进程来占用 CPU 资源,以实现多任务并发执行。

进程调度算法

常见的进程调度算法有多种,每种算法都有其独特的设计目标和实现方式。

  1. 先来先服务(FCFS, First - Come, First - Served):这是一种简单直观的调度算法,按照进程进入就绪队列的先后顺序来分配 CPU。先进入队列的进程先获得 CPU 资源,直到它完成任务或主动放弃 CPU。例如,假设有三个进程 P1、P2、P3 依次进入就绪队列,P1 先被调度执行,只有当 P1 执行完毕后,P2 才会获得 CPU 执行,P2 执行完毕后 P3 才开始执行。这种算法的优点是实现简单,公平性直观,但对于 CPU 密集型和 I/O 密集型混合的进程环境,可能导致 I/O 设备长时间空闲,因为 CPU 密集型进程会占用较长时间的 CPU 资源。

以下是一个简单的 FCFS 调度算法的 Python 代码示例:

class Process:
    def __init__(self, pid, arrival_time, burst_time):
        self.pid = pid
        self.arrival_time = arrival_time
        self.burst_time = burst_time


def fcfs(processes):
    processes.sort(key=lambda x: x.arrival_time)
    total_waiting_time = 0
    total_turnaround_time = 0
    current_time = 0
    for process in processes:
        if current_time < process.arrival_time:
            current_time = process.arrival_time
        waiting_time = current_time - process.arrival_time
        turnaround_time = waiting_time + process.burst_time
        total_waiting_time += waiting_time
        total_turnaround_time += turnaround_time
        current_time += process.burst_time
        print(f"Process {process.pid}: Waiting Time = {waiting_time}, Turnaround Time = {turnaround_time}")
    avg_waiting_time = total_waiting_time / len(processes)
    avg_turnaround_time = total_turnaround_time / len(processes)
    print(f"Average Waiting Time = {avg_waiting_time}")
    print(f"Average Turnaround Time = {avg_turnaround_time}")


# 示例数据
processes = [
    Process(1, 0, 24),
    Process(2, 0, 3),
    Process(3, 0, 3)
]
fcfs(processes)
  1. 短作业优先(SJF, Shortest Job First):该算法优先调度预计执行时间最短的进程。它旨在最小化平均周转时间,提高系统的整体效率。例如,在一组进程中,如果有一个进程的预计执行时间远小于其他进程,SJF 算法会优先让这个进程执行。但 SJF 算法的缺点是需要预先知道每个进程的执行时间,这在实际情况中往往很难准确获取。而且,如果不断有短作业进入系统,长作业可能会面临饥饿问题。

  2. 优先级调度:为每个进程分配一个优先级,调度程序优先选择优先级最高的进程执行。优先级可以基于多种因素设定,如进程的类型(系统进程通常优先级较高)、进程的资源需求、进程的紧急程度等。然而,如果高优先级进程持续不断地进入系统,低优先级进程可能会长时间得不到执行,从而导致饥饿。

  3. 时间片轮转(Round - Robin):将 CPU 的时间划分成固定大小的时间片,每个进程轮流在一个时间片内执行。当时间片用完后,即使进程尚未执行完毕,也会被剥夺 CPU 并重新回到就绪队列末尾等待下一次调度。这种算法保证了每个进程都能在一定时间内获得 CPU 执行机会,适用于交互式系统,能提供较好的响应时间。但如果时间片设置不合理,可能会导致系统开销增大或进程响应延迟。例如,如果时间片过长,可能会退化为 FCFS 算法;如果时间片过短,进程上下文切换过于频繁,会消耗大量 CPU 资源用于上下文切换操作。

饥饿现象概述

饥饿进程是指在系统中长时间等待 CPU 资源却始终得不到调度执行的进程。这种现象违背了进程调度的公平性原则,严重影响了系统的整体性能和用户体验。

饥饿的直观表现

从用户角度来看,当一个应用程序长时间处于无响应状态,例如一个视频编辑软件在渲染视频时,进度条长时间不动,看似程序“卡死”,这很可能就是该进程陷入了饥饿状态。在系统层面,通过系统监控工具可以观察到某些进程在就绪队列中等待的时间异常长,而 CPU 却一直在为其他进程服务。

饥饿与死锁的区别

虽然饥饿和死锁都表现为进程无法继续推进,但它们有着本质的区别。死锁是指多个进程由于竞争资源而形成一种互相等待的僵局,所有涉及死锁的进程都无法向前推进,并且这种状态会一直持续下去,除非通过外力(如管理员手动干预或系统重启)来打破。而饥饿进程是因为调度算法的不合理或系统资源分配不均衡等原因,导致某个或某些进程长期得不到 CPU 资源,但系统中其他进程仍在正常运行,并非所有进程都陷入停滞。例如,在一个基于优先级调度的系统中,低优先级进程可能会饥饿,但高优先级进程依然能正常执行;而死锁情况下,涉及死锁的所有进程都无法执行。

导致饥饿进程长时间得不到服务的原因分析

调度算法相关原因

  1. 固定优先级调度算法的缺陷:在固定优先级调度系统中,如果高优先级进程源源不断地进入系统,低优先级进程就可能永远没有机会执行。例如,在一个实时操作系统中,实时任务(如处理传感器数据的任务)通常被赋予较高优先级,而一些后台任务(如系统日志整理任务)优先级较低。如果实时任务持续不断地产生,后台任务可能会一直处于等待状态,从而导致饥饿。

假设我们有一个简单的固定优先级调度的模拟代码(Python):

class Process:
    def __init__(self, pid, priority, burst_time):
        self.pid = pid
        self.priority = priority
        self.burst_time = burst_time


def priority_scheduling(processes):
    processes.sort(key=lambda x: x.priority, reverse=True)
    total_waiting_time = 0
    total_turnaround_time = 0
    current_time = 0
    for process in processes:
        waiting_time = current_time
        turnaround_time = waiting_time + process.burst_time
        total_waiting_time += waiting_time
        total_turnaround_time += turnaround_time
        current_time += process.burst_time
        print(f"Process {process.pid}: Waiting Time = {waiting_time}, Turnaround Time = {turnaround_time}")
    avg_waiting_time = total_waiting_time / len(processes)
    avg_turnaround_time = total_turnaround_time / len(processes)
    print(f"Average Waiting Time = {avg_waiting_time}")
    print(f"Average Turnaround Time = {avg_turnaround_time}")


# 示例数据,高优先级进程不断
processes = [
    Process(1, 1, 10),
    Process(2, 1, 10),
    Process(3, 2, 10)
]
priority_scheduling(processes)

在上述代码中,如果持续有优先级为 1 的进程进入,优先级为 2 的进程就可能饥饿。

  1. 短作业优先算法的饥饿风险:虽然 SJF 算法能有效降低平均周转时间,但它对长作业不太友好。如果系统中不断有短作业到达,长作业可能会被一直推迟执行。例如,在一个云计算环境中,用户提交的作业大小各不相同。如果短作业频繁提交,那些需要长时间计算的大数据处理作业可能会因为 SJF 算法而长时间等待,最终导致饥饿。

资源分配相关原因

  1. 资源分配策略不合理:操作系统在分配资源时,如果采用的策略过于偏向某些进程,可能会导致其他进程饥饿。例如,在内存分配中,如果总是优先满足高优先级进程的内存需求,低优先级进程可能因为无法获得足够的内存而无法执行。假设一个系统中有两个进程 P1 和 P2,P1 是高优先级进程且内存需求大,P2 是低优先级进程。如果系统采用一种简单的“优先满足高优先级”的内存分配策略,当系统内存紧张时,P2 可能一直无法获得足够的内存,即使它的其他资源需求都已满足,也只能处于等待状态,最终导致饥饿。

  2. 资源竞争与瓶颈:当多个进程竞争有限的关键资源时,如果某个进程长期占据这些资源,其他进程可能会因为资源不足而饥饿。例如,在一个多线程的数据库应用程序中,多个线程可能竞争数据库连接资源。如果一个线程长时间占用数据库连接进行复杂的查询操作,其他需要连接数据库执行简单操作的线程可能会因为无法获取连接而长时间等待,进而导致相关进程饥饿。

系统负载相关原因

  1. 高系统负载下的饥饿:当系统负载过高时,大量进程同时竞争 CPU 资源。在这种情况下,如果调度算法不能很好地适应高负载环境,就容易出现饥饿现象。例如,在一个 Web 服务器上,当同时有大量用户请求访问时,系统负载急剧上升。如果采用简单的 FCFS 调度算法,新到达的请求对应的进程可能会因为前面有大量长时间运行的进程而等待很长时间,导致用户体验变差,甚至可能出现某些请求对应的进程饥饿。

  2. 不均衡的负载分布:即使系统整体负载在可承受范围内,但如果负载分布不均衡,也可能导致部分进程饥饿。例如,在一个多核处理器系统中,如果所有的计算密集型任务都集中在某一个或几个核心上,而其他核心相对空闲,那么在繁忙核心上等待的进程可能会因为长时间无法获得 CPU 时间而饥饿,尽管系统还有其他空闲资源。

系统设计与配置相关原因

  1. 缺乏饥饿检测与预防机制:如果操作系统没有内置的饥饿检测和预防机制,饥饿进程可能会一直存在而不被发现和处理。例如,一些简单的嵌入式操作系统可能为了追求系统的简洁性,没有实现复杂的饥饿检测逻辑。在这种系统中,如果出现进程饥饿现象,只能通过人工排查,这在实际应用中可能会导致严重的问题,尤其是对于一些对可靠性要求较高的系统。

  2. 系统参数配置不当:操作系统的一些参数配置对进程调度和资源分配有重要影响。例如,时间片轮转算法中的时间片大小,如果设置得过大,在高负载情况下,新到达的进程可能需要等待很长时间才能获得执行机会,容易导致饥饿;如果设置得过小,上下文切换开销过大,也会影响系统性能,间接增加进程饥饿的可能性。又如,在优先级调度中,优先级的划分范围和调整策略如果设置不合理,可能会导致部分进程始终处于低优先级状态,从而引发饥饿。

饥饿进程对系统的影响

性能方面

  1. 系统资源利用率降低:饥饿进程虽然没有执行,但它们依然占用着系统资源,如内存空间、打开的文件描述符等。这些资源不能被有效利用,导致系统整体资源利用率下降。例如,一个饥饿的大数据处理进程可能已经分配了大量内存用于数据存储,但由于长时间得不到 CPU 执行,这些内存处于闲置状态,而其他需要内存的进程可能因为内存不足而无法高效运行。

  2. 系统吞吐量下降:由于饥饿进程无法正常推进,系统中完成的任务数量减少,从而导致系统吞吐量下降。例如,在一个生产环境中,若有多个任务需要按顺序处理,其中一个任务对应的进程饥饿,那么后续依赖该任务结果的其他任务也无法执行,整个生产流程的产出就会降低。

用户体验方面

  1. 应用程序无响应:从用户角度看,饥饿进程对应的应用程序会表现为无响应状态。比如,用户在使用文字处理软件时,突然软件界面冻结,无法进行任何操作,这可能是因为负责处理用户输入和显示更新的进程陷入了饥饿,导致用户体验极差。

  2. 任务执行延迟:对于一些有时间限制的任务,如在线支付过程中的订单处理任务,如果相关进程饥饿,会导致订单处理延迟,可能引发用户不满,甚至造成经济损失。

缓解饥饿进程问题的方法

调度算法改进

  1. 动态优先级调度:为了避免固定优先级调度算法导致的饥饿问题,可以采用动态优先级调度。在这种调度方式下,进程的优先级不是固定不变的,而是随着时间或其他因素动态调整。例如,随着进程等待时间的增加,逐渐提高其优先级,这样可以保证长时间等待的进程最终能够获得执行机会。在 Linux 内核的 CFS(完全公平调度器)中,就采用了类似的思想,通过对进程的运行时间等因素进行评估,动态调整进程的调度优先级。

  2. 多级反馈队列调度:多级反馈队列调度算法结合了多种调度算法的优点。系统设置多个就绪队列,每个队列有不同的优先级,高优先级队列的时间片较短,低优先级队列的时间片较长。新进程首先进入最高优先级队列,在该队列中按照时间片轮转调度。如果进程在一个时间片内未执行完,则降低其优先级,放入下一级队列。这样既能保证短作业快速执行,又能避免长作业饥饿。例如,在一个操作系统中,设置了三个优先级队列 Q1、Q2、Q3,Q1 优先级最高,时间片为 10ms;Q2 次之,时间片为 20ms;Q3 优先级最低,时间片为 50ms。新进程先进入 Q1,若在 10ms 内未执行完则进入 Q2,以此类推。

资源分配优化

  1. 公平资源分配算法:在资源分配方面,采用公平资源分配算法,确保每个进程都能获得合理的资源份额。例如,在内存分配中,可以采用一种基于进程需求比例的分配算法,而不是简单地优先满足高优先级进程。假设系统中有两个进程 P1 和 P2,P1 需求内存为 100MB,P2 需求内存为 200MB,系统总可用内存为 300MB。按照公平分配原则,P1 应获得 100MB,P2 应获得 200MB,而不是只考虑优先级而忽略需求比例。

  2. 资源预分配与预留:对于一些关键资源,可以采用预分配或预留的方式,保证重要进程或可能面临饥饿的进程能够获得资源。例如,在一个多媒体播放系统中,为了保证音频和视频播放的流畅性,可以预先为播放进程预留一定的 CPU 时间和内存资源,防止它们因为其他进程的资源竞争而饥饿。

系统监控与调整

  1. 饥饿检测机制:操作系统应实现有效的饥饿检测机制,通过监控进程在就绪队列中的等待时间、执行频率等指标,及时发现饥饿进程。例如,可以设定一个阈值,当某个进程在就绪队列中的等待时间超过该阈值时,判定该进程可能饥饿,并采取相应措施。在 Windows 操作系统中,任务管理器可以显示进程的一些基本信息,虽然没有直接的饥饿检测功能,但通过观察进程的 CPU 使用率、内存占用等长时间无变化的情况,管理员可以人工判断是否存在饥饿进程。

  2. 系统参数动态调整:根据系统负载和进程运行情况,动态调整操作系统的相关参数。例如,在高负载情况下,适当减小时间片轮转算法中的时间片大小,以提高系统的响应速度,减少新进程的等待时间;在低负载时,适当增大时间片大小,降低上下文切换开销,提高系统整体性能。

应用程序层面的优化

  1. 合理的任务分解与优先级设置:应用程序开发者在设计应用程序时,应合理分解任务,并为不同任务设置合适的优先级。例如,在一个图形渲染软件中,用户交互相关的任务(如鼠标点击响应)应设置较高优先级,而后台渲染任务可以设置相对较低的优先级。这样可以保证用户操作的即时响应,同时也能使渲染任务在系统资源允许的情况下逐步推进,避免因优先级设置不当导致某些任务饥饿。

  2. 资源管理与复用:应用程序应合理管理和复用资源,减少对系统资源的不必要竞争。例如,在一个多线程的网络应用程序中,线程之间可以共享一些网络连接资源,而不是每个线程都独立申请新的连接,从而降低资源竞争的程度,减少进程饥饿的可能性。

通过对饥饿进程长时间得不到服务的原因进行深入分析,并采取相应的缓解措施,可以有效提高操作系统的性能和稳定性,为用户提供更好的使用体验。在实际的操作系统设计和应用开发中,需要综合考虑各种因素,不断优化进程调度和资源分配策略,以避免饥饿现象的发生。