进程是动态的,是程序的一次执行过程。同一个程序多次执行会对应多个进程。
进程是计算机科学中的一个重要概念,它是操作系统管理和调度的基本单位,用于执行程序和任务。进程可以被认为是正在运行的程序的实例,具有自己的内存空间、代码、数据和系统资源的使用。以下是有关进程的概念的详细信息:
程序执行的实例:进程是一个正在运行的程序的实例。当您启动一个应用程序或运行一个脚本时,操作系统会创建一个进程来托管该程序的执行。每个进程都有自己的执行环境,包括内存和系统资源。
资源分配单位:进程是操作系统用来管理和分配计算机资源的基本单位。这些资源包括CPU时间、内存、文件句柄、网络连接等。操作系统负责确保多个进程能够共享这些资源,同时保持相互隔离,以防止彼此干扰或冲突。
独立性:每个进程都是相互独立的实体,它们不会直接干扰或影响其他进程的执行。这种独立性是通过操作系统的管理和保护来实现的,以确保一个进程的错误或崩溃不会影响整个系统。
并发执行:操作系统可以同时管理多个进程,使它们能够在同一计算机系统上并发执行。这允许多个任务在几乎同时执行,从而提高了系统的效率和响应能力。
状态管理:每个进程可以处于不同的状态,如运行、就绪、阻塞等。操作系统负责管理这些状态并根据需要进行状态转换,以有效地利用系统资源。
通信和协作:进程之间可以通过各种通信机制进行数据交换和协作。这种通信可以在不同的进程之间传递消息、共享数据或协调任务。
进程是操作系统中的核心概念,用于管理和执行计算机系统中的程序和任务。它们具有独立性、并发性、资源分配、状态管理和通信等特点,是操作系统的重要组成部分,对于实现多任务处理和系统资源的有效利用至关重要。
进程是计算机系统中的基本执行单元,它包含多个组成部分,这些部分协同工作以执行程序或任务。包含下面主要组成部分:
程序段:进程的核心组成部分之一是程序代码,它是一系列指令或指令集,以二进制形式存储在计算机的内存中。程序代码定义了进程要执行的操作,包括算法、逻辑和计算任务。
数据段:进程需要处理的数据也是重要的组成部分。这些数据可以包括变量、数据结构、输入和输出等,它们存储在进程的内存空间中,用于程序的运行和操作。
程序计数器(Program Counter):程序计数器是一个特殊的寄存器,用于跟踪下一条要执行的指令的地址。它在程序执行期间不断更新,以指示下一步要执行的操作。
寄存器状态:进程的寄存器状态包括了一组寄存器的内容,这些寄存器存储了进程的执行上下文,如通用寄存器、栈指针、堆指针等。寄存器状态对于程序的执行和上下文切换非常重要。
堆和栈:堆和栈是进程内存的两个关键区域。堆用于动态分配内存,通常用于存储动态分配的数据结构,如对象和数组。栈用于存储函数调用和局部变量,以及控制函数的执行顺序。
进程控制块(Process Control Block,PCB):PCB 是一个数据结构,包含了操作系统管理进程所需的信息。每个进程都有一个相关的 PCB,其中包括进程状态(用一个变量state来表示进程的当前状态)、进程标识符(Process ID,进程ID)、优先级、程序计数器、寄存器状态、资源分配信息等。PCB 允许操作系统管理进程的创建、调度、暂停、恢复和终止。
打开文件表(File Descriptor Table):进程通常需要访问文件和输入/输出设备。打开文件表存储了进程打开的文件和设备的信息,包括文件描述符、文件位置等。
其他资源:进程可能会使用其他资源,如网络连接、设备访问权限、信号处理器等。这些资源也可以包含在进程的组成部分中,以便操作系统管理和跟踪它们的使用。
这些组成部分协同工作,使进程能够执行程序或任务。操作系统负责管理这些部分,确保进程能够共享系统资源、执行程序代码并与其他进程协同工作。进程的组成部分可以根据操作系统和架构的不同而有所不同,但上述元素通常包括在进程的定义中。
进程作为计算机科学中的一个基本概念,具有许多重要的特征,这些特征有助于定义和理解进程的性质和行为。以下是进程的主要特征:
动态性(Dynamic):进程是程序的一次执行过程,是动态地产生、变化和消亡的。动态性是进程最基本的特征。
并发性(Concurrency):进程允许多个进程在同一时间段内存在和执行。计算机系统通过交替执行这些进程来实现并发性,从而提高了系统的效率和响应能力。
独立性(Independence):每个进程都是独立的实体,它们不会直接干扰或影响其他进程的执行。这种独立性是通过操作系统的管理和保护来实现的,以确保一个进程的错误或崩溃不会影响整个系统。
异步性(Asynchrony):各个进程按照独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题。
结构性(Structural):每个进程都会配置一个PCB。从结构上来看,进程由程序段、数据段、PCB组成。
这些特征使进程成为操作系统的基本组成部分,允许多任务处理、资源管理和程序协作,从而实现了复杂的计算和操作。进程的特征对于操作系统的性能、可靠性和安全性都具有重要影响。
进程在其生命周期中可以经历不同的状态,操作系统通过状态转换来管理和调度进程。以下是一些常见的进程状态以及它们之间的转换:
在进程整个生命周期中,大部分是都处于三种基本状态(运行态、就绪态、阻塞态)。
进程在这些状态之间进行转换,具体的状态转换可以由操作系统的调度器和各种事件触发:
新建状态到就绪状态:当进程被创建后,它会等待操作系统将其移到就绪状态,这通常由操作系统的调度器触发。
就绪状态到运行状态:调度器会选择一个就绪状态的进程,并将其移到运行状态,开始执行进程的指令。
运行状态到就绪状态:进程可能在运行时被中断或时间片用尽,然后被移到就绪状态,等待下一次执行。
运行状态到阻塞状态:进程在等待某些事件发生时,可能会被移到阻塞状态,直到事件发生并由操作系统触发将其移到就绪状态。
阻塞状态到就绪状态:一旦进程等待的事件发生,操作系统将其移到就绪状态,以等待再次执行。
运行状态到终止状态:当进程完成任务或由于错误或其他原因需要终止时,它会被移到终止状态。
阻塞状态到终止状态:如果进程在阻塞状态中终止,操作系统会将其直接移到终止状态。
这些状态和状态转换使操作系统能够管理和调度多个进程,以有效地共享计算机资源,提高系统的效率和响应能力。状态转换通常是由操作系统的内核和调度算法自动处理的,以确保进程按照合适的顺序执行。
进程的组织方式一共有两种,一种是链式方式,另一种是索引方式。
链式方式是一种进程组织方式,通常用于操作系统中管理和调度多个进程。在链式方式中,进程以链表的形式组织,每个进程都包含指向下一个进程的指针。这种方式通常用于实现一种特定的调度策略,例如循环调度(Round Robin Scheduling)。
每个进程都包含一个指向下一个进程的指针。这些进程按照链表的方式连接在一起,形成一个环形链表或线性链表。
当一个进程的时间片用尽时,它会被移到链表中的下一个进程。这种方式使得进程在一组进程中轮流执行,以达到公平共享CPU时间的目的。
链式方式的实现通常需要操作系统维护一个进程队列,其中包含了所有等待CPU时间的进程。调度器会选择队列中的第一个进程运行,然后将其移到队列的末尾或下一个位置,以等待下一次运行。
当然,对应的阻塞状态也会有阻塞队列形成的链表。
通常用于操作系统中管理和跟踪多个进程。在索引方式中,每个进程都由一个唯一的索引或标识符引用,而不是按照线性或链式方式组织。这种方式允许快速的进程查找和访问,通常用于高效的进程调度和管理。
操作系统维护一个进程表或进程控制块(Process Control Block,PCB)表,其中包含了每个进程的索引号和相关信息。每个条目通常包含进程的状态、程序计数器、寄存器状态、资源分配信息等。
索引方式可以用于实现高效的进程调度。调度器可以根据进程的优先级或其他因素来选择下一个要执行的进程,并使用索引号直接访问该进程的信息。
进程控制是操作系统中的一个关键任务,它涉及创建、管理和监控进程的各个方面。进程控制允许操作系统有效地管理多个进程,以便它们可以共享计算机资源、协同工作并按照预期执行任务。
进程控制的主要功能是对系统中的所有进程实施有效的管理,其具有创建新进程、撤销已有进程、实现进程转换等功能。
可以使用原语来实现进程控制,因为原语的执行具有“原子性”(不可被中断的一个或一系列操作),可以一气呵成的完成。
为什么要保证进程控制一气呵成呢?
假设PCB中的变量state表示进程当前所处状态,1表示就绪态,2表示阻塞态。
可以画出上面的图。就绪状态的PCB就应该挂在就绪队列中,而阻塞状态的PCB应该挂在阻塞队列中。
如果此时进程2等待的事件发生了,则操作系统中,负责进程控制的内核程序至少需要做这样的两种事情:
此时,已经将 PCB 2 的 state 修改为 1,突然检测到一个中断信号,那么 PCB 2 的 state=1,但是它却被放在阻塞队列中。
此时就会导致 PCB 2 的state变量的值与其所处队列不匹配,会影响到操作系统进行别的管理工作。
以下是与进程控制相关的一些关键方面:
进程创建:
fork
和exec
)来创建子进程。进程终止:
wait
)来等待和监控子进程的终止状态。进程状态管理:
调度和优先级:
进程间通信(IPC):
资源管理:
进程间关系:
错误处理:
进程控制是操作系统中的一个核心功能,它负责管理、创建、终止、调度和监控进程,以确保计算机系统的有效运行。进程控制使得操作系统能够支持多任务处理、资源管理和进程间协作。
原语的执行具有原子性,即执行过程必须一步到位,期间不允许被中断。可以使用“关中断指令”和“开中断指令”这两个特权指令实现原子性。
当CPU执行了关中断指令之后,就不在例行检查中断信号,直到执行了开中断指令之后才会恢复检查。
这样,关中断、开中断之间的这些指令序列就是不可被中断的,这就实现了“原子性”。
常见的两种进程控制的原语有两种,一种是创建原语(操作系统创建一个进程时使用的原语),一种是撤销原语。
创建原语在进程创建的时候执行:
撤销原语在进程终止时执行:
阻塞原语在进程阻塞时执行:
唤醒原语在进程唤醒的时候执行:
切换原语在进程需要切换的时候执行:
其中创建原语会使进程从创建态转变为就绪态,撤销原语使进程从就绪态/阻塞态/运行态转变为终止态,最后被操作系统删除。阻塞原语将进程从运行态转变为阻塞态,唤醒原语则是将进程从阻塞态转变为就绪态。切换原语会将当前进程从运行态转变为就绪态,然后再从就绪队列中找出一个就绪态的进程转变成运行态。
需要注意的是,进程因何事情被阻塞,就应该由何事唤醒。所以阻塞原语唤醒原语必须是成对使用的。
引起进程创建的事件有: ① 用户登录:分时系统中,用户登录成功,系统会为其建立一个新的进程。
② 作业调度:多道批处理系统中,当有新的作业放入到内存中时,会为其建立一个新的进程。
③ 提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求。
④ 应用请求:由用户进程主动请求创建一个子进程。
引起进程终止的事件有:① 正常结束(进程自己请求终止。例如exit系统调用)
② 异常结束(整数除以0、非法使用特权指令等,然后被操作系统强制杀掉)
③ 外界干预:用户选择杀掉进程(Ctrl + Alt + Delete)
引起进程阻塞的事件:① 需要等待系统分配某种资源
② 需要等待相互合作的其他进程完成工作
引起进程唤醒的事件:① 等待事件发生
引起进程切换的事件:① 当前进程的时间片到
② 有更高优先级的进程到达
③ 当前进程主动阻塞
④ 当前进程终止
进程之间的通信是指在计算机操作系统中,不同的进程之间通过特定的机制来交换数据、信息或信号的过程。进程通信通常用于实现多任务、协作任务或分布式计算等需求,使不同进程能够相互协作、协调工作,从而完成更复杂的任务。
进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。出于数据安全的考虑,两个进程之间是不能随意直接进行访问的(无论是数据的读取还是写入,都是不被允许的)
虽然各个进程只能访问自己的存储空间,但是如果操作系统支持共享存储的功能,则这个进程可以申请一片共享存储区,这片共享存储区同样也可以被其他进程所共享。因此,当进程P像共享存储区存入数据时,则进程Q就可以共享这份数据。
操作系统在内存中划出一块共享存储区,数据的形式、存放位置都是由通信进程控制,而不是操作系统。这种共享方式速度很快,是一种高级通信方式。
为了避免出错,各个进程对共享空间的访问应该是互斥的。否则就会发生,多个进程向共享区的同一块区域进行写入,导致数据被覆盖等问题。各个进程可以使用操作系统提供的同步互斥工具(如P、V操作)。
假设操作系统给定的共享空间区域中,只能存放一个长度为3的数组,这种共享方式速度慢、限制多,是一种低级通信方式。
进程在对共享区域的读写操作的自由度远远没有基于存储区共享高。每次只能按照给定的数据结构的大小对数据进行读写。
进程通过发送和接收消息来进行信息交流。这种通信方式通常用于分布式系统、并行计算和多进程环境中,允许不同的进程或线程之间传递数据、命令或通知。
进程间的数据交换以格式化的消息为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。
消息的结构如下图:
在直接通信中,一个进程可以直接向另一个具体的进程发送消息,而不需要中间代理或共享数据结构。这种通信方式适用于明确的通信目标,消息发送者知道接收者的标识符或地址。
消息发送者主动地将消息发送给接收者,接收者也需要主动接收和处理消息。这种方式需要明确的发送和接收操作。
间接通信通常使用消息队列、邮箱或共享数据结构作为中介,消息发送者将消息放置在队列或邮箱中(因此间接通信方式又被称为“信箱通信方式”),而消息接收者从中获取消息。消息队列可以支持多对多通信模式。
间接通信通常支持异步通信,即发送者可以将消息放置在队列中后继续执行其他任务,而不需要等待接收者立即处理消息。
管道只能采用半双工通信,某一时间段内只能实现单向的传输。
如果要实现双向同时通信,则需要设置两个管道。
操作系统会实现各个进程互斥地访问管道。
当管道被写满时,写进程将会被阻塞,直到读进程将管道中的数据取走,即可唤醒写进程。
当管道中的数据一旦被读出之后,就会彻底消失。因此,当多个进程读同一个管道时,就可能会发生错乱。对此,通常会有两种解决方案
写进程往管道写入数据,即便管道没有被写满,只要管道没空,读进程就可以从管道读取数据。
读进程从管道读取数据,即便管道没有被读空,只要管道没满,写进程就可以往管道写入数据。
在还没有引入进程之前,系统中各个程序只能串行执行。
在引入进程之后,进程是程序的一次执行。但是这些功能显然不可能是由一个程序顺序处理就能实现的。传统的进程是程序执行流的最小单位。
有的进程可能需要同时完成很多事情,而传统的进程只能串行地执行一系列程序。为此,引入了线程的概念,来增加并发度。引入线程之后,线程成为了程序执行流的最小单位。
一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件句柄等。线程之间可以更轻松地共享数据和通信,相比于进程,线程的创建、销毁和切换开销更小,因此在多任务处理中更为高效。不仅仅在进程之间可以并发,在进程内部的各个线程之间也可以并发,进一步提升了系统的并发度。
线程的实现方式可以分为两大类:用户级线程(User-Level Threads)和内核级线程(Kernel-Level Threads)。每种方式都有其独特的特点和适用场景。
用户级线程是在应用程序中通过用户空间的库或框架来实现的线程。这些线程不依赖于操作系统内核的线程管理机制,而是由应用程序自己管理。以下是用户级线程的一些实现方式:
用户级线程的优点包括轻量级、快速线程创建和销毁,不需要切换到核心态,线程管理的系统开销小,效率高、灵活性高。
然而,当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高,多个线程不可在多核处理机上并行运行。
内核级线程是由操作系统内核直接管理的线程,每个线程对应于内核中的一个线程控制块(Thread Control Block,TCB)。以下是一些实现方式:
内核级线程的优点包括能够充分利用硬件资源、更好的并发性能和稳定性。然而,线程的创建和销毁通常比用户级线程慢,因为涉及到系统调用和内核的介入。
内核级线程的优点有:
内核级线程的缺点:
上图也被称为一对一模型:一个用户线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。
一对一模型(内核级线程):一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。
优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
多对一模型(用户级线程):多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程。
优点:用户级线程的切换在用户空间即可完成,不需要切换到内核态,线程管理的系统开销小,效率高。
缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。
注意:操作系统只能识别到内核级线程,因此只有内核级线程才是处理机分配的单位。
多对多模型:n 用户及线程映射到 m 个内核级线程( n ⩾ \geqslant ⩾ m)。每个用户进程对应 m 个内核级线程。
其克服了多对一模型并发度不高的缺点(即一个阻塞全体阻塞),又克服了一对一模型中用户进程占用太多内核级线程,开销太大的缺点。
线程的状态与转换和进程的状态与转换非常相似。线程的状态与转换主要关注阻塞、就绪、运行三种状态及其之间的转换,如下图
线程的组织和控制是多线程应用程序中的重要方面,它涉及到如何创建、启动、管理和协调线程以实现并发执行任务的目标。以下是线程的组织和控制的关键方面:
线程的创建: 创建线程是多线程应用程序的第一步。线程可以通过操作系统提供的线程库或编程语言提供的多线程API来创建。一般来说,线程的创建需要指定线程的入口函数和传递参数。此时线程会创建一个类似于进程控制块一样的线程控制块(TCB)。
线程的启动: 创建线程后,需要启动线程以开始执行。线程的启动通常由调用线程库或API中的特定函数来完成。线程启动后,它进入就绪状态并等待被调度。
线程的调度: 线程的调度由操作系统的线程调度器负责。调度器决定哪个线程在某一时刻执行,并分配 CPU 时间片给线程。线程的调度策略可以是抢占式的(即使一个线程的时间片用完,可以被剥夺CPU)或非抢占式的(直到线程主动放弃CPU控制权才会切换)。
线程的同步与互斥: 多个线程可能需要共享数据或资源,因此需要进行同步和互斥控制,以避免竞态条件和数据不一致性问题。常见的同步机制包括互斥锁、信号量、条件变量等。
线程的通信: 多个线程之间可能需要进行通信,以实现协作和数据传递。线程通信的方式包括共享内存、消息队列、管道等。通信机制依赖于线程库和编程语言。
线程的等待与唤醒: 线程可以等待某个条件满足或被唤醒。等待和唤醒通常使用条件变量或信号量来实现。线程等待特定事件或条件时,它会释放 CPU 控制权,等待其他线程唤醒它。
线程的终止: 线程可以正常完成任务后自行终止,或者被外部线程手动终止。线程的终止通常涉及资源的清理和状态的更新。
线程的控制和管理: 在多线程应用程序中,可能需要对线程进行控制和管理,例如设置线程的优先级、暂停和恢复线程、查询线程状态等。这些操作通常由线程库或API提供。
线程的错误处理: 多线程应用程序可能会面临线程崩溃、死锁和竞态条件等问题。因此,需要实现适当的错误处理机制来处理这些问题,以确保应用程序的稳定性和可靠性。
线程的组织和控制是多线程编程的关键部分,需要谨慎设计和管理,以确保线程能够协同工作并避免常见的多线程问题。正确的线程组织和控制可以提高应用程序的性能和并发能力,但也需要处理同步和互斥问题,以避免潜在的错误和竞争条件。
当有一堆任务需要进行处理,但是由于资源有限,这些事情无法同时进行处理。这就需要确定某种规则来决定处理这些任务的顺序。这就是调度。
处理机调度是计算机操作系统中的一个重要概念,它涉及到如何有效地管理和分配计算机系统中的中央处理器(CPU)资源,以便多个进程(或任务)能够共享CPU,并且系统能够高效地运行。
以下是处理机调度的概念:
进程:进程是计算机系统中正在执行的程序的实例。每个进程都有自己的代码、数据和执行环境。处理机调度的主要任务之一是决定哪个进程将在CPU上执行。
调度策略:调度策略是指决定进程在CPU上执行顺序的算法或规则。不同的调度策略可以影响系统的性能和响应时间。常见的调度策略包括先来先服务(FCFS)、最短作业优先(SJF)、轮转调度(Round Robin)和优先级调度等。
就绪队列:就绪队列是存放处于就绪状态的进程的数据结构。就绪状态的进程已准备好运行,只需等待CPU资源。处理机调度程序从就绪队列中选择下一个要执行的进程。
阻塞队列:阻塞队列是存放处于阻塞状态的进程的数据结构。阻塞状态的进程通常在等待外部事件(如I/O操作完成)时被挂起,并且不能立即执行。一旦等待的事件发生,进程将从阻塞队列移至就绪队列。
上下文切换:上下文切换是当操作系统决定切换到另一个进程时,保存当前进程的执行状态(如寄存器的内容和程序计数器的值),然后加载下一个进程的状态,以便它可以继续执行。上下文切换会引入一些开销,因此调度策略的选择应考虑最小化上下文切换的次数。
周转时间和等待时间:周转时间是一个进程从提交到完成的总时间,包括等待时间和执行时间。等待时间是进程在就绪队列中等待的时间。
响应时间:响应时间是从用户提交一个任务到系统开始执行该任务的时间间隔。快速响应时间对于交互性应用程序非常重要。
不同的计算机系统和应用场景可能需要不同的处理机调度策略,以满足性能、响应时间和资源利用率的要求。处理机调度是操作系统的一个关键组成部分,可以帮助系统有效地利用CPU资源,提高系统的性能和可用性。
高级调度,也称为长期调度或作业调度,是处理机调度中的一种重要策略,它负责从外部提交的作业队列中选择哪些作业进入系统执行。**每个作业只调入一次,调出一次。**作业调入时会建立PCB,调出时才会撤销PCB。
高级调度的主要目标是有效地利用系统资源,确保系统的吞吐量和性能。
当然内存空间是很有限的,有时无法将用户提交的作业全部放入内存中:
以下是高级调度的任务:
作业队列:高级调度管理着作业队列,这是一组等待执行的作业或任务。这些作业通常是由用户提交的,它们等待进入系统并分配给CPU。
选择作业:高级调度负责从作业队列中选择哪些作业将进入系统执行。选择的依据可以是多种因素,包括作业的优先级、资源需求、系统负载等。高级调度的目标是确保系统能够高效地运行,并且不过度拥挤。
预测系统负载:高级调度需要预测系统的负载情况,以便根据系统的当前状态来选择适当数量的作业进入执行。如果系统处于高负载状态,高级调度可能会选择较少的作业,以避免过度竞争CPU资源。
资源管理:高级调度需要考虑作业对系统资源的需求,例如内存、I/O设备等。它可能会根据系统资源的可用性来选择作业,以避免资源争用问题。
作业优先级:高级调度可以使用作业的优先级来确定哪些作业应该首先进入系统执行。通常,高优先级的作业会比低优先级的作业更早执行。
避免饥饿和死锁:高级调度需要确保所有作业都有机会进入系统执行,避免饥饿现象。此外,它还需要避免死锁情况,即系统中的作业相互等待资源,无法继续执行。
高级调度是一个重要的调度阶段,它在系统层面上决定哪些作业可以进入系统执行。它的目标是平衡系统的性能和资源利用率,确保系统在高效运行的同时不会过载。高级调度通常由操作系统的调度器(scheduler)或作业调度器(job scheduler)负责执行。与高级调度相对应的是低级调度(或进程调度),它负责在就绪队列中选择哪些进程将在CPU上执行。
中级调度,也称为内存调度,它负责管理处于内存中的进程,决定哪些进程应该暂时被置于挂起状态,以便腾出内存空间供其他进程执行。中级调度在进程管理中起到了重要的作用,帮助系统有效地利用有限的内存资源,同时确保各个进程得到公平的执行机会。
按照某种策略决定将哪个处于挂起状态的进程重新调入内存。一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高。
以下是中级调度的一些任务:
进程状态管理:中级调度负责管理进程的状态,通常将处于就绪状态的进程从内存中暂时移除,将其置于挂起状态。这些进程的上下文信息被保存到外部存储(如硬盘)中,以释放内存空间。
内存管理:中级调度协助操作系统进行内存管理。通过将一些进程置于挂起状态,系统可以释放内存,以便为新进程提供足够的内存空间。这有助于避免内存过度占用和系统的死锁问题。
挂起和恢复进程:中级调度负责挂起(暂停)进程并将其状态保存到外部存储。当需要恢复进程时,中级调度负责将进程的状态重新加载到内存中,并将其放回到就绪队列,以便它可以继续执行。
资源释放:中级调度可以协助释放进程占用的资源,如内存、I/O设备等,以便这些资源可以被其他进程使用。
进程优先级:中级调度可以根据进程的优先级来决定哪些进程应该被挂起。通常,低优先级的进程更容易被挂起,以保留内存供高优先级的进程使用。
总之,中级调度是操作系统中进程管理的一个关键部分,它帮助系统维护内存中的进程集合,并确保系统能够高效地运行。通过在进程之间切换,并根据系统资源的可用性来挂起和恢复进程,中级调度有助于平衡系统的性能和资源利用率。与中级调度相对应的是高级调度(作业调度),负责选择哪些作业可以进入系统执行,以及低级调度(进程调度),负责在就绪队列中选择哪些进程将在CPU上执行。
低级调度又称进程调度/处理机调度,按照某种策略从就绪队列中选取一个进程,将处理机分配给它。
进程调度时操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。进程调度的频率很高,一般几十毫秒就会触发一次。
低级调度,也称为进程调度或进程切换,是计算机操作系统中的一个关键组成部分,它负责从就绪队列中选择一个进程,将其加载到CPU并开始执行。低级调度控制了CPU资源的分配,以确保多个进程能够共享CPU,并决定哪个进程将在特定的时间片内执行。
以下是低级调度的任务:
进程切换:低级调度负责进程之间的切换。当一个进程的时间片用尽或它主动放弃CPU时,低级调度会选择下一个进程,将其上下文加载到CPU寄存器中,然后开始执行。
时间片管理:低级调度通常使用时间片轮转(Round Robin)或其他调度算法来管理每个进程被分配的时间片。时间片是一个小的时间段,通常在几毫秒到几十毫秒之间,它控制了每个进程在CPU上执行的时间。
上下文切换:在进行进程切换时,低级调度需要执行上下文切换操作。这包括保存当前进程的执行状态(寄存器值、程序计数器等),加载下一个进程的状态,以及切换内存映射等。
公平性和响应时间:低级调度的目标之一是确保各个进程获得公平的CPU时间,以避免某个进程占用CPU导致其他进程无法响应。同时,低级调度也需要保证系统对用户请求的快速响应时间。
调度算法:不同的操作系统可以使用不同的低级调度算法来选择下一个执行的进程。常见的算法包括轮转调度(Round Robin)、最短剩余时间优先(SRTF)、多级反馈队列等。
阻塞和唤醒:低级调度也需要处理进程的阻塞和唤醒。当一个进程在等待某些事件(如I/O操作完成)时被阻塞,低级调度会将其暂时移出执行队列。一旦等待的事件发生,进程被唤醒并重新加入队列。
总之,低级调度是操作系统中管理CPU资源分配的关键部分。它决定了哪个进程将在CPU上执行,以及每个进程执行的时间片。通过高效地执行进程切换和上下文切换操作,低级调度有助于确保系统能够高效地运行,同时平衡各个进程的执行需求。
进程调度的时机是操作系统中的一个重要决策,它决定了何时以及哪个进程将被选择执行在CPU上。
需要进行进程调度与切换的情况:
当前进程执行完毕:当一个进程执行完了它的时间片(对于轮转调度)或者它的任务(对于其他调度算法),操作系统会触发进程切换,选择下一个进程执行。
当前进程主动让出CPU:有些进程可能会自愿放弃CPU的控制权,例如,当它需要等待某个事件发生时(如等待I/O操作完成),它可以请求操作系统将CPU分配给其他就绪进程。这个过程通常是通过系统调用(如yield()
或sleep()
)来实现的。
进程被阻塞:如果一个进程在等待某个事件(如等待文件读取完成)时被挂起,操作系统会将其从CPU上移除,并将其状态置为阻塞。一旦等待的事件发生,操作系统会唤醒该进程,并将其移回就绪队列。
高级调度或中级调度:高级调度(作业调度)和中级调度(进程调度)也可以触发进程调度的时机。当新的作业或进程被提交到系统时,高级调度会决定何时将其加入到就绪队列。中级调度会管理内存中的进程,将某些进程置于挂起状态以腾出内存。这些操作都可能导致进程调度的发生。
中断和时钟中断:操作系统会周期性地接收时钟中断,这些中断可以用于触发进程调度。时钟中断发生后,操作系统可以检查当前运行进程的时间片是否已用完,如果是,则进行进程切换。
优先级调度:如果系统支持动态优先级调度,那么进程的优先级可能在运行时发生变化,当某个进程的优先级发生变化时,操作系统可能会根据新的优先级重新调度进程。
不能进行进程调度与切换的情况:
tips:
临界区为访问一段时间内只允许一个进程使用的资源的那段代码,而内核程序临界区一般是用来访问某种内核数据结构的,例如进程中的就绪队列)分为两种,一种是非剥夺调度方式,另一种是剥夺调度方式。
非剥夺调度方式:又称为非抢占式。即,只允许进程主动放弃处理机。在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态。该方式实现简单,系统开销小但是无法及时处理紧急任务,适合于早期的批处理系统。
剥夺调度方式:又称抢占方式。当一个进程正在处理机上执行时,如果有一个更重要或者更紧迫的进程需要使用处理机,则会立即暂停正在执行的进程,将处理机分配给更重要紧迫的进程。可以优先处理更紧急的进程,也可以实现让各进程按事件片轮流执行的功能(通过时钟中断)。适合分时操作系统、实时操作系统。
这三种调度层次在管理和分配系统资源方面有不同的任务和职责,但它们共同协作以确保系统能够高效地运行。下面是它们之间的对比和关系:
高级调度(作业调度):
中级调度(内存调度):
低级调度(进程调度):
三者的关系:
总结表格如下:
执行内容 | 发生地点 | 发生频率 | 对进程状态的影响 | |
---|---|---|---|---|
高级调度 (作业调度) |
按照某种规则,从后备队列中选择合适的作业将其调入内存,并为其创建进程 | 外存 => 内存 (面向作业) |
最低 | 无 => 创建态 => 就绪态 |
中级调度 (内存调度) |
按照某种规则,从挂起队列中选择合适的进程将其数据返回内存 | 外存 => 内存 (面向进程) |
中等 | 挂起态 => 就绪态 (阻塞挂起 => 阻塞态) |
低级调度 (进程调度) |
按照某种规则,从就绪队列中选择一个进程为其分配处理机 | 内存 => CPU | 最高 | 就绪态 => 运行态 |
高级、中级和低级调度是操作系统中不可或缺的组成部分,它们协同工作以管理作业和进程,确保系统能够高效地运行,并为用户提供合理的响应时间。不同的调度层次在不同的抽象级别上进行资源管理和调度,以实现整体系统的平衡和优化。
暂时调到外存中等待的进程状态为挂起状态(挂起态)。挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态。
注意“挂起”和“阻塞”的区别,两种状态都是暂时不能获得CPU的服务,但挂起态是将进程映像调到外存去了,而阻塞态下进程映像还在内存中有的操作系统会把就绪挂起、阻塞挂起分为两个挂起队列,甚至会根据阻塞原因不同再把阻塞挂起进程进一步细分为多个队列。
调度器(也称为调度程序)是操作系统的一个关键组件,负责管理和控制计算机系统中的进程和资源。调度器的主要任务是按照一定的规则和算法来分配CPU时间和其他系统资源,以使多个进程能够共享CPU,并确保系统运行平稳、高效。
以下是有关调度器的一些概念:
功能:
调度算法:
上下文切换:
多核处理器:
实时调度:
用户空间和内核空间:
在不支持内核级线程的操作系统中,调度程序的处理对象是进程:
支持内核级线程的操作系统,调度程序的处理对象是内核线程:
调度器是操作系统的一个关键组件,它负责管理和协调进程的执行,以实现多任务并发。调度器的设计和性能对于操作系统的整体性能和响应能力至关重要。
“闲逛进程” 是操作系统中的一种特殊进程,通常也称为 “空闲进程” 或 “空闲任务”。它是为了充分利用系统资源而创建的一种进程。闲逛进程的主要目的是在系统没有其他任务需要执行时,使CPU保持繁忙状态,防止 CPU 等待或闲置。
闲逛进程的特点包括:
无实际工作:闲逛进程不执行任何有用的计算任务。它不负责运行用户应用程序或执行系统任务。其唯一目的是让 CPU 保持繁忙状态。
循环执行:闲逛进程通常包含一个无限循环,其内容可能是简单的指令序列,例如空的 while 循环。这使得 CPU 在不断执行指令,而不会退出或休眠。
低优先级:闲逛进程通常具有较低的优先级,这意味着当有其他进程需要执行时,它会被立即抢占,让其他任务优先执行。
资源利用:通过占用 CPU 时间,闲逛进程可防止系统进入完全空闲状态,从而确保 CPU 和其他资源保持活跃状态。
节能和热管理:在某些系统中,闲逛进程还可以用于节能和热管理。通过让 CPU 保持繁忙状态,可以防止 CPU 进入节能模式或降低频率,从而保持 CPU 温度和能耗在合理范围内。
需要注意的是,闲逛进程的存在是为了充分利用系统资源,而不是为了执行有意义的任务。在多任务操作系统中,当没有其他进程需要执行时,闲逛进程会被调度,以避免 CPU 闲置。
不同的操作系统可能使用不同的闲逛进程实现方式,但它们都具有相似的目标:保持系统资源的有效使用和响应能力。
该评价指标分为四个部分:CPU利用率、系统吞吐量、周转时间、等待时间、响应时间。
指的是CPU忙碌的时间占总时间的比例。所以其利用率的算法为:
利用率 = 忙碌的时间 总时间 {\frac{忙碌的时间}{总时间}} 总时间忙碌的时间
衡量在单位时间内完成的进程数量。更高的吞吐量通常表示算法能够处理更多的进程。
系统吞吐量 = 总共完成作业数 花费的总时间 {\frac{总共完成作业数}{花费的总时间}} 花费的总时间总共完成作业数
进程从提交给系统开始,到作业完成为止的平均时间。较低的平均周转时间通常表示算法能够更快地完成任务。
其包含四个部分:作业在外存后备队列上等待作业调度(高级调度)的时间、进程在就绪队列上等待进程调度(低级调度)的时间、进程在CPU上 的执行时间、进程等待I/O操作完成的时间。后三项在一个作业的整个处理过程中,可能发生多次。
周转时间 = 作业完成时间 - 作业提交时间
平均周转时间 = 各作业周转时间之和 作业数 {\frac{各作业周转时间之和}{作业数}} 作业数各作业周转时间之和
带权周转时间 = 作业周转时间 作业实际运行的时间 {\frac{作业周转时间}{作业实际运行的时间}} 作业实际运行的时间作业周转时间
平均带权周转时间 = 各作业带权周期时间之和 作业数 {\frac{各作业带权周期时间之和}{作业数}} 作业数各作业带权周期时间之和
所有进程等待执行的时间。较低的平均等待时间通常表示算法能够提供更好的响应时间。等待时间越长,用户满意度越低。
对于进程来说,等待时间就是指进程建立后等待被服务的时间之和,在等待I/O完成的期间其实进程也是在被服务的,所以不计入等待时间。
对于作业来说,不仅要考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间。
与上面的指标类似,也会有一个平均等待时间来评价整体性能。
等待时间 = 周转时间 - 运行时间 (- I/O操作的时间)
如果针对纯计算型的进程(一个进程到达后要么在等待,要么在运行),则等待时间为周转时间减去运行实际那。如果既有计算、又有I/O操作的进程,则其等待时间就是周转时间减去运行时间,再减去I/O操作时间。
进程从提交到首次获得CPU执行的时间。对于交互式应用程序,低响应时间至关重要。
将从算法思想、算法规则、该调度算法是用于作业调度还是进程调度、是否为抢占式、优缺点、是否会导致饥饿现象(某进程/作业长期得不到服务)等这几方面进行分析学习。
SJF非抢占式算法:
SJF抢占式算法(SRTN最短剩余时间算法)
其中的响应比的计算公式为:响应比 = 等待时间 + 要求服务时间 要求服务时间 {\frac{等待时间+要求服务时间}{要求服务时间}} 要求服务时间等待时间+要求服务时间
当时间片大小为2时:
0时刻:0时刻只有P1到达就绪队列,让P1处理机运行一个时间片。
2时刻:2时刻时P2到达就绪队列,P1运行完一个时间片,被剥夺处理机,重新放到队尾。此时的P2排在队头,因此P2上处理机 (默认新到达的进程先进入就绪队列)
4时刻:3时刻时,P3到达,先插到就绪队尾,紧接着P2下处理机也插入到队尾。
5时刻:5时刻时,P4到达插入到就绪队尾(此时P1的时间片还没有用完,因此暂时不调度。另外,此时P1处于运行态,并不在就绪队列中)。
6时刻:6时刻时,P1的时间片用完,下处理机,重新放回就绪队尾,发生调度。
7时刻:虽然P3的时间片还没用完,但是由于P3只需要运行1个单位的时间,运行完成之后就会主动放弃处理机,因此也会发生调度。队头进程P2上处理机。
9时刻:进程P2时间片用完,并刚好运行结束,发生调度,P4上处理机。
11时刻:P4时间片用完,重新回到就绪队列,P1上处理机。
12时刻:P1运行结束,主动放弃处理机,此时就绪队列中只剩下P4,则P4上处理机。
14时刻:就绪队列为空,则此时让P4进程接着运行一个时间片。
16时刻:所有进程运行结束。
当时间片大小为5时:
从上面可以看出:
如果时间片太小,会导致进程切换过于频繁,系统会花费大量的时间来处理进程切换,从而导致实际用于进程执行的时间比例会减少。
如果时间片太大,使得每个进程都可以在一个时间片内完成,则时间片轮转调度算法就退化成先来先服务调度算法,并且会增大进程的响应时间。
一般来说,设计时间片时要让切换进程的开销占比不超过1%
非抢占式优先级调度:
抢占式优先级调度:
先来先服务、最短作业优先、最高响应比优先这三种算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心“响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。因此这三种算法一般适合用于早期的批处理系统。
比起早期的批处理操作系统来说,由于计算机造价大幅度降低,因此之后出现的交互式操作系统(如分时操作系统、实时操作系统等)更注重于系统的响应时间、公平性、平衡性等指标。而时间片轮转调度、优先级调度、多级反馈队列调度三种算法也能比较好地满足交互式系统的需求。因此这三种算法适合用于交互式系统(如UNIX使用的就是多级反馈队列调度算法)。
进程的并发性带来了异步性(各个并发的进程独立的以不可预知的速度向前推进),而有的进程则需要有次序的相互配合来完成作业,所以有了进程同步。
进程同步是指多个进程在执行过程中按照一定的顺序和协调来共享和访问共享资源,以避免竞态条件(Race Condition)和确保数据的一致性。进程同步是多进程编程中的关键问题,特别是在多进程环境下,多个进程需要协作完成某个任务时。
竞态条件(Race Condition):
竞态条件是指多个进程同时访问共享资源,导致不可预测的结果或数据不一致。竞态条件通常发生在以下情况下:多个进程试图同时读取和写入共享资源,而且执行顺序不确定。
临界资源:
临界资源可以是内存中的变量、文件、数据库、网络连接等,多个进程需要对这些资源进行读取和修改。虽然多个进程可以共享系统中的各种资源,但其中许多资源一段时间内只能为一个进程所使用,我们把一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机等。
进程同步的目标:
进程同步的主要目标是确保多个进程能够按照一定的顺序和规则访问共享资源,以避免竞态条件,并确保数据的一致性和正确性。
常见的进程同步机制:
a. 互斥锁(Mutex):互斥锁可以用来保护共享资源,只允许一个进程或线程在同一时刻访问资源。其他进程如果要访问资源,必须等待互斥锁被释放。
b. 信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量的值表示可用资源的数量,进程在访问资源前必须请求并获取信号量,然后在使用完资源后释放信号量。
c. 条件变量(Condition Variable):条件变量通常用于线程间的通信,它允许一个线程等待某个条件的发生,然后唤醒其他等待该条件的线程。条件变量可用于实现更高级的同步机制。
d. 屏障(Barrier):屏障用于确保多个进程在某个点上同时等待,然后一起继续执行。这在一些并行计算场景中非常有用。
实现进程同步的关键操作:
通过进程同步机制,可以确保多个进程按照一定的顺序访问共享资源,从而避免竞态条件和数据不一致问题,保证程序的正确性和可靠性。不同的编程语言和操作系统提供了不同的同步机制,但它们的核心概念和目标都是相似的。
因为对于临界资源的访问,需要互斥的进行。同一个时间段内只允许一个进程访问该资源。
临界资源的访问过程可以分为四个部分:
为了禁止两个进程同时进入临界区,互斥机制应遵循以下的原则:
实现进程互斥的软件实现方法有四种:单标志法、双标志先检查、双标志后检查、Peterson算法
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。即每个进程进入临界区的权限只能被另一个进程赋予。
假设turn的初值为0,即刚开始只允许0号进程进入临界区(turn 变量背后的逻辑表示“谦让”)。
若 P1 先上处理机运行,则代码会被一直卡在 ⑤。直到 P1 的时间片用完,发生调度,切换到 P0 上处理机运行。但代码 ① 不会卡住 P0,即 P0 可以正常访问临界区,在 P0 访问临界区期间即时切换回 P1 ,P1依然会卡在 ⑤。只有 P0 在退出区将 turn 改为1后,P1 才能进入临界区。
单标志法存在的主要问题是,违背了“空闲让进”原则。
算法思想:设置一个布尔数组 flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如“flag[0] = true”意味着0号进程 P0 现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志 flag[i] 设为 true,之后开始访问临界区。
双标志先检查法主要的问题是,违背了“忙则等待”原则。原因在于,进入区的“检查”和“上锁”两个处理不是原子操作。在“检查”后,“上锁”前可能发生进程切换。
算法思想:是双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”,但是这两个操作又无法一气呵成,因此会导致两个进程同时进入临界区的情况。因此,认门又想到先“上锁”后“检查”的方法,来避免上述问题。
双标志后检查法虽然解决了“忙则等待”的问题,但是又违背了“空闲让进”和“有限等待”原则,会因各个进程都长期无法访问临界资源而产生饥饿现象(两个进程都争着想要进入临界区,但是都上锁了,谁也不让谁,最后谁都无法进入临界区)。
算法思想:结合双标志法、单标志法的思想。如果双方都争着想进入临界区,那可以让进程尝试“谦让”的方式,做一个有“礼貌”的进程。
bool flag[2]; // 表示进入临界区意愿的数组,初始化都是false
int turn = 0; // turn 表示优先让哪个进程进入临界区
// P0进程
flag[0] = true; // 表示本线程想进入临界区
turn = 1; // 可以优先让对方线程进入临界区
while(flag[1] && turn == 1); // 对方想进入临界区且自己“谦让”,则本线程循环等待
critical section;
flag[0] = false; // 访问完临界区,表示自己已经不想访问临界区
remainder section;
// P1进程
flag[1] = true;
turn = 0;
while(flag[0] && turn == 0);
critical section;
flag[1] = false;
remainder section;
Peterson 算法用软件方式解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则。
进程互斥的硬件实现方法有三种:中断屏蔽方法、TestAndSet(TS指令/TSL指令)、Swap(XCHG指令)
利用“开/关中断指令”实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况)
优点:简单、高效
缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)。
简称TS指令,也称为TestAndLock指令,或TSL指令。
TSL指令是用硬件实现的,执行过程不允许被中断,只能一气呵成。
若刚开始的lock是false,则TSL返回的old值为false,while 循环条件不满足,直接跳过循环,进入临界区。若刚开始lock是 true,则执行执行TLS后old返回的值为 true,while循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行“解锁”。
相比软件实现方式,TSL 指令把“上锁”和“检查”操作用硬件的方式变成原子操作。
优点:实现简单,无需像软件实现方式那样严格检查是否有逻辑漏洞;适合于多处理机环境。
缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。
又称 Exchange 指令,简称 XCHG 指令。
Swap 指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成(原子操作)。
逻辑上 Swap 与 TSL 并无太大的区别,都是先记录下此时临界区是否已经被上锁(记录在 old 变量上),再将上锁标记 lock 设置为 true,最后检查 old,如果 old为false则说明之前没有别的进程对临界区上锁,则可以跳出循环,进入临界区。
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境。
缺点:不满足“让权等待”原则,暂时无法进入临界区的进程占用CPU并循环执行TSL指令,从而导致“忙等”。
互斥锁(Mutex,全名为 Mutual Lock)是一种用于多线程或多进程编程中的同步机制,它用于确保在任何给定时刻只有一个线程或进程能够访问临界资源,从而防止竞态条件和数据不一致性问题。互斥锁提供了互斥性,确保临界区内的代码只能被一个线程或进程执行,其他线程或进程必须等待锁的释放。
互斥锁的基本操作包括两个主要函数:锁定(Lock)和解锁(Unlock)。
每个互斥锁都有一个布尔变量 available,表示锁是否可用。如果锁是可用的,调用 acquire() 会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。
acquire() 或 release() 的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现。
互斥锁的主要缺点是忙等,当有一个进程在临界区中,任何其他进程在进入临界区时必须连续循环调用 acquire()。当多个进程共享同一CPU时,就浪费了CPU周期。因此,互斥锁通常用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行。
需要连续循环忙等的互斥锁,都可称为自旋锁(spin lock),如 TSL 指令、swap 指令、单标志法。
特性:
信号量(Semaphore)是一种用于进程或线程同步的抽象数据类型,用于管理共享资源的访问。信号量可以用于控制多个进程或线程之间的并发访问,以确保对临界资源的互斥访问,或者控制并发任务的执行顺序。信号量的主要目的是解决竞态条件、死锁和资源管理等问题。
信号量其实就是一个变量(即一个整数,又或者是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量。
原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。软件解决方案的主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区、退出区的操作都用“原语”来实现,使这些操作都能“一气呵成”就能避免问题。
一对原语:wait(S)原语和signal(S)原语,可以把原语理解为自己所写的函数,wait与signal为函数名,括号中的信号量S其实就是函数调用时传入的一个参数。
wait、signal原语常简称P、V操作(荷兰语:proberen 和 verhogen)。因此也常将wait(S)、signal(S)两个操作分别写为P(S)、V(S)
信号量通常具有以下两个基本操作:
信号量有两种主要类型:二进制信号量和计数信号量。
用一个整数型的变量作为信号量,用来表示系统中系统中某种资源的数量。
与普通整数变量的区别:对信号量的操作只有三种,即 初始化、P操作、V操作。
“检查”和“上锁”一气呵成,避免了并发、异步导致的问题。
存在的问题:不满足“让权等待”原则,会发生“忙等”
整形信号量的缺陷是存在“忙等”问题,因此人们又提出“记录型信号量”,即用记录型数据结构表示信号量。
如果剩余资源数不够,使用block原语使进程从运行态进入阻塞态,并把其挂到信号量S的等待队列(即阻塞队列)中。
释放资源后,若还有别的进程在等待这种资源,则使用wakeup原语唤醒等待队列中的一个进程,该进程从阻塞态变为就绪态。
注意:对于不同的临界资源需要设置不同的互斥信号量。
P、V操作必须成对出现。缺少P(mutex)就不能保证临界资源的互斥访问。缺少V(mutex) 会导致资源永不释放,等待进程永不被唤醒。
若先执行到V(S)操作,则S++ 后S=1。之后当执行到P(S)操作时,由于S=1,表示有可用资源,会执行S–,S的值变回0, P2进程不会执行block原语,而是继续往下执行代码4。
若先执行到P(S)操作,由于S=0,S-- 后S=-1,表示此时没有可用资源,因此P操作中会执行block原语,主动请求阻塞。之后当执行完代码2,继而执行V(S)操作,S++, 使S变回0,由于此时有进程在该信号量对应的阻塞队列中,因此会在V操作中执行wakeup原语,唤醒P2进程。这样P2就可以继续执行代码4了
其实每一对前驱关系都是一个进程同步的问题:
所以实现的步骤/顺序为:
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品(可以代指某种数据资源)放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。
生产者、消费者共享一个初始化为空、大小为n的缓冲区(假设n为5)
只有缓冲区未满时,生产者才能把产品放入缓冲区,否则必须要等待。
只有缓冲区非空时,消费者才能从中取出产品,否则必须等待。
缓冲区是临界资源,各个进程之间必须互斥地访问(成为互斥关系)。因为如果为非互斥关系,可能两个生产者进程随机选中缓冲区中的同一块区域进行写入(如上图),则会致使数据覆盖,导致数据丢失。
注意:实现互斥的P操作一定要在实现同步的P操作之后,否则会造成生产者和消费者循环等待被对方唤醒,出现“死锁”情况。而V操作不会导致进程阻塞,因此两个V操作顺序可以交换。
吸烟者问题(The Smoker’s Problem)是一个经典的同步问题,用于展示多进程或多线程之间的协作和同步。问题的描述如下:
在一个场景中,有三个吸烟者(Smoker)和一个代理者(Agent)。每个吸烟者都有以下两个特征:
每个吸烟者都有一个不同的烟草配料,例如,一个吸烟者有烟草、另一个有纸、第三个有火柴。
每个吸烟者都需要这三种配料才能开始吸烟。
供应者的任务是:
供应者周期性地生成两种随机的烟草配料,并将它们放在一个桌子上。
供应者等待一个吸烟者拿起这两种烟草配料并开始吸烟。
一旦一个吸烟者开始吸烟,供应者会再次生成两种随机的烟草配料,等待下一个吸烟者。
吸烟者的任务是:
每个吸烟者需要等待桌子上的两种烟草配料中有自己缺少的那种。
一旦满足条件,吸烟者会拿起两种烟草配料,开始吸烟。
吸烟者吸烟结束后,需要通知代理者,代理者再生成两种新的烟草配料。
吸烟者问题的目标是通过协调代理者和吸烟者的行为,确保每个吸烟者都能够获取到自己所需的两种烟草配料,然后开始吸烟。
同步关系(前V后P):
桌上有组合一 => 第一个抽烟者取走东西
桌上有组合二 => 第二个抽烟者取走东西
桌上有组合三 => 第三个抽烟者取走东西
供应者:
解决吸烟者问题通常涉及使用同步机制,如互斥锁、条件变量、信号量等,以确保代理者和吸烟者之间的正确协作。不同的编程语言和操作系统可以有不同的实现方式,但问题的本质是协作和同步。这个问题对于理解多进程或多线程编程中的同步概念非常有用。
读者-写者问题(Reader-Writer Problem)是一个经典的同步问题,用于展示多进程或多线程之间的协作和同步。问题的描述如下:
在一个共享资源(通常是一个数据结构,如数据库、文件等)上,有多个读者和写者同时访问。读者是只读操作,写者是写操作。问题的目标是确保在读者和写者之间的并发访问时不会引发数据不一致性、竞态条件和死锁等问题。
具体来说,读者-写者问题有以下要求:
多个读者可以同时访问共享资源,但不能同时写入。
如果有写者在写入,其他读者和写者都必须等待写者完成。
多个写者不能同时写入,且当写者在写入时,其他读者和写者都必须等待写者完成。
解决读者-写者问题通常需要使用同步机制来确保正确的访问顺序和互斥性。一种常见的解决方法包括使用互斥锁(Mutex)和信号量(Semaphore)。以下是一个通用的解决方案:
使用两个互斥锁:一个用于控制读者的访问,另一个用于控制写者的访问。
使用一个计数信号量来跟踪当前有多少读者在访问共享资源。
写者在写入前必须获得写者互斥锁,并且在写入结束后释放它。
读者在读取前必须获得读者互斥锁,然后在读取结束后释放它。
在读者和写者访问共享资源前,需要对读者数量进行计数信号量的P操作,以确保写者不会在读者存在时写入。
在读者和写者访问共享资源后,需要对读者数量进行计数信号量的V操作,以通知其他读者或写者可以访问。
这个通用解决方案确保了读者和写者之间的正确协作,防止了数据不一致性和竞态条件。需要注意的是,具体的实现可能因编程语言、操作系统或应用场景而异,但核心原则是相似的。读者-写者问题对于理解同步和互斥概念以及多线程或多进程编程中的并发问题非常有用。
哲学家进餐问题(Dining Philosophers Problem)是一个经典的多进程同步问题,用于展示多个进程之间的并发互斥和资源分配问题。问题的场景描述如下:
有五位哲学家坐在一张圆形餐桌周围,每位哲学家都在思考和吃饭。在每个哲学家之间放置一只叉子,共有五只叉子。每位哲学家的行为如下:
哲学家可以思考,此时他不需要叉子。
哲学家可以尝试进餐,但需要两只相邻叉子,一只放在左侧,另一只放在右侧。
问题的目标是设计一个算法,确保哲学家在思考和进餐之间的正确协作,以避免死锁(所有哲学家都在等待叉子)和资源争夺(多个哲学家尝试同时获取叉子)等问题。
解决哲学家进餐问题的一种经典方法是使用互斥锁和条件变量。以下是一个通用的解决方案:
为每个叉子创建一个互斥锁,以确保每次只能有一个哲学家拿起或放下叉子。
为每位哲学家创建一个状态变量,表示他们的状态(思考、饥饿或进餐)。
当哲学家想要进餐时,他必须获取两只相邻叉子的锁。如果其中一只叉子已被其他哲学家占用,他需要等待。
如果哲学家成功获取两只相邻叉子的锁,他可以开始进餐,并将状态变为进餐状态。
进餐结束后,哲学家将放下叉子并将状态变回思考状态,然后通知左右两侧的哲学家,以便他们有机会进餐。
哲学家进入饥饿状态时,他会尝试获取叉子,如果成功获取,他进入进餐状态,否则他将等待。
这个解决方案确保了哲学家之间的正确协作,避免了死锁和资源争夺问题。需要注意的是,具体的实现方式可能因编程语言、操作系统或应用场景而异,但核心原则是相似的。哲学家进餐问题对于理解并发编程中的互斥和条件同步非常有用。
信号量机制存在的问题是编写程序困难、容易出错。
管程是一种特殊的软件模块(面向对象中的类),有这些部分组成:
管程的基本特征:
由编译器负责实现各进程互斥地进入管程中的过程。
引入管程的目的:更加方便地实现进程互斥和同步。
需要在管程中定义共享数据(如生产者消费者问题的缓冲区)
需要在管程中定义用于访问这些共享数据的“入口”-- -其实就是一些函数(如生产者消费者问题中,可以定义一一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)
只有通过这些特定的“入口”才能访问共享数据
管程中有很多“入口”,但是每次只能开放其中一个“入口”,并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。注意:这种, 互斥特性是由编译器负责实现的,程序员不用关心)
可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出“入口”);可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。
死锁(Deadlock)指的是在多个进程或线程之间,每个进程都在等待某个资源被释放,而同时又持有其他资源,导致它们都无法继续执行下去的状态。这种情况下,系统进入了一种僵局,无法自行恢复,需要通过干预来解决。
死锁通常涉及多个资源,例如内存、文件、打印机、网络连接等,不同进程或线程试图获取这些资源,但由于资源之间的竞争条件,它们陷入了相互等待的状态。
死锁的产生是因为以下四个条件同时满足(只要其中任何一个条件不满足,死锁都不会发生):
互斥条件(Mutual Exclusion):每个资源只能被一个进程或线程占用,其他进程或线程必须等待资源释放。
占有和等待条件(Hold and Wait):进程或线程至少持有一个资源,并且在等待获取其他资源。
不可抢占条件(No Preemption):资源不能被强制抢占,只能在进程或线程自愿释放后才能被其他进程或线程获取。
循环等待条件(Circular Wait):存在一个进程或线程的资源请求链,形成一个循环,每个进程或线程都在等待下一个资源,直到最后一个又在等待第一个进程或线程释放资源。
注意:发生死锁时一定有循环等待,但是发生循环等待时未必死锁(即循环等待时死锁的必要不充分条件)。如果同类资源数大于1,即使有循环等待,也未必发生死锁。但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件。
为了避免死锁的发生,通常采用以下策略:
资源分配策略:确保系统在分配资源时不会导致死锁。这可以通过先分配所有必要资源,然后才允许进程或线程执行,或者采用其他资源分配算法来实现。
超时和重试:在等待资源时,设定一个超时限制,如果超过限制还未获取资源,则释放已经占有的资源,并尝试重新获取。
死锁检测和恢复:定期检测系统是否进入了死锁状态,如果检测到死锁,采取措施来中断其中一个或多个进程,以解除死锁。
避免死锁:通过仔细设计进程或线程的资源请求顺序,以避免循环等待条件的发生(使用银行家算法)。
使用信号量、互斥锁等同步机制:正确使用同步机制可以减少死锁的发生。
死锁:各进程互相等待对方手里的资源,导致各进程都发生阻塞,无法向前推进的现象。
饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象。例如:在SPF(短进程优先)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而导致长进程“饥饿”。
死循环:某进程执行过程中一直无法跳出某个循环的现象。有时是因为程序逻辑bug导致的,有时则是程序员故意设计的。
预防死锁只需要让形成死锁的四个条件中的任何一个不成立即可。
**互斥条件 **:只有对必须互斥使用的资源的争抢才会导致死锁。
破坏互斥条件以预防死锁,有以下几种方法:
值得注意的是,破坏互斥条件可能会引入性能和复杂性方面的问题,因为多个实体同时访问资源可能会导致冲突和竞争条件。因此,在考虑破坏互斥条件时,需要权衡系统性能和死锁预防的需求。
此外,应该根据具体的应用场景和系统需求来选择合适的死锁预防策略。死锁预防是系统设计中的一个重要方面,需要综合考虑多个因素,以确保系统能够在高效性和可靠性之间取得平衡。
不抢占条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
要破坏不抢占条件以预防死锁,可以考虑以下方法:
该策略的缺点:
占用和等待:进程已经保持了至少一个资源,但是又提出了新的资源请求,而该资源又被其他进程占用,此时请求进程被阻塞,但又对自己已有的资源保持不放。
要破坏占有和等待条件以预防死锁,可以考虑下面的方法:
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已经获取的资源都同时被下一个进程所请求。
以下是一些破坏循环等待条件的方法:
资源排序:为系统中的资源分配一个全局唯一的编号或标识符,并要求进程按照一定的顺序请求资源。这样可以减少循环等待的可能性,因为资源的分配顺序变得可预测。
资源申请策略:采用资源分配策略,要求进程只能同时申请一类资源,而不是多种资源。这可以减少资源之间的交叉等待,从而减少死锁的概率。
资源分级:将资源分为不同的等级或层次,要求进程只能获取相同或更低级别的资源。这样可以确保资源请求不会形成循环等待。
资源组合:将多个资源组合成一个资源组,进程只能请求整个资源组而不是单独的资源。这可以降低循环等待的可能性,因为资源组的请求是原子的。
超时机制:引入超时机制,当一个进程请求资源时,如果在一定时间内无法获得资源,就会释放已经占有的资源,以避免死锁。这需要注意超时值的设置,以确保不会误解资源的真实可用性。
银行家算法:银行家算法是一种死锁避免策略,它使用资源分配和进程请求的信息来判断是否分配资源会导致死锁。只有在分配资源不会导致死锁的情况下才进行分配。
资源释放:要求进程在完成任务后立即释放占有的资源,而不是等待其他资源。这可以减少资源持有时间,减少循环等待的机会。
该策略的第一种方法的缺点:
安全序列是一组进程的执行顺序,使得系统不会进入死锁状态。在安全序列中,每个进程都按照它的资源请求顺序逐一获取资源,执行完任务后释放资源。在整个执行过程中,没有出现资源竞争导致的死锁,系统能够正常运行并顺利完成进程。
如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
如果系统处于安全状态,就一定不会 发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
银行家算法(Banker’s Algorithm)是一种死锁避免算法,用于管理多个进程对有限资源的竞争和分配,以避免死锁的发生。这个算法最初由荷兰计算机科学家 Edsger W. Dijkstra 于1965年提出。原本该算法是 Dijkstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用于操作系统中。
银行家算法的详细解释:
1. 进程和资源: 银行家算法涉及到多个进程和多个资源。每个进程都需要一定数量的不同类型的资源来执行任务,而这些资源可能是有限的。
2. 资源的种类: 每种资源都有一个固定的数量,表示为"总数"。这些资源可以是CPU时间、内存、文件句柄、打印机等。
3. 进程的资源需求: 每个进程在开始执行之前必须指定其所需的各种资源数量。这些需求被表示为一个向量,称为最大需求(Maximum Demand)向量。
4. 系统资源状态: 银行家算法维护一个当前资源状态的向量,表示为可用资源(Available Resources)向量。这个向量的每个元素代表相应资源的可用数量。
5. 安全状态: 在银行家算法中,系统会定期检查是否存在安全状态。一个系统被认为处于安全状态,如果存在一种资源分配序列,使得所有进程都能够成功完成,而不会导致死锁。
6. 分配资源的规则: 银行家算法遵循以下规则来分配资源:
7. 安全性检查: 在每次资源分配之后,系统会检查是否存在安全状态。安全性检查通常涉及模拟进程执行和资源释放,以验证是否存在一种资源分配序列,使得所有进程都能够成功完成而不会导致死锁。
8. 如果系统处于安全状态: 如果安全性检查确定系统处于安全状态,那么资源分配是合法的,进程可以继续执行。否则,系统将等待资源,直到安全状态恢复。
银行家算法的主要目标是确保资源分配不会导致死锁。它通过以下步骤来实现这一目标:
初始化:
请求资源:
分配资源:
安全性检查:
资源释放:
循环:
通过这些步骤,银行家算法可以确保资源的合理分配,从而预防死锁的发生。如果系统进入不安全状态,算法会等待,直到安全状态恢复。请注意,这是一个高层次的描述,实际实现可能涉及更多细节和复杂性。
以下是银行家算法的简单示例,用于模拟资源分配和安全性检查。这个示例假设有n个进程和m种资源。
import java.util.Scanner;
public class BankersAlgorithm {
private int[][] max; // 最大需求矩阵
private int[][] allocation; // 分配矩阵
private int[][] need; // 需求矩阵
private int[] available; // 可用资源向量
private int n; // 进程数量
private int m; // 资源种类数量
public BankersAlgorithm(int n, int m) {
this.n = n;
this.m = m;
max = new int[n][m];
allocation = new int[n][m];
need = new int[n][m];
available = new int[m];
}
// 初始化资源矩阵
public void initialize() {
Scanner scanner = new Scanner(System.in);
System.out.println("输入每种资源的总数:");
for (int i = 0; i < m; i++) {
available[i] = scanner.nextInt();
}
System.out.println("输入每个进程的最大需求矩阵:");
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
max[i][j] = scanner.nextInt();
}
}
System.out.println("输入每个进程的已分配矩阵:");
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
allocation[i][j] = scanner.nextInt();
need[i][j] = max[i][j] - allocation[i][j];
}
}
}
// 检查是否存在安全序列
public boolean isSafe() {
int[] work = new int[m];
boolean[] finish = new boolean[n];
// 初始化work向量
for (int i = 0; i < m; i++) {
work[i] = available[i];
}
int count = 0;
int[] safeSequence = new int[n];
while (count < n) {
boolean found = false;
for (int i = 0; i < n; i++) {
if (!finish[i]) {
boolean canAllocate = true;
for (int j = 0; j < m; j++) {
if (need[i][j] > work[j]) {
canAllocate = false;
break;
}
}
if (canAllocate) {
for (int j = 0; j < m; j++) {
work[j] += allocation[i][j];
}
safeSequence[count] = i;
finish[i] = true;
count++;
found = true;
}
}
}
if (!found) {
break; // 没有找到可分配的进程,不安全
}
}
if (count == n) {
System.out.println("存在安全序列:");
for (int i = 0; i < n; i++) {
System.out.print("P" + safeSequence[i]);
if (i < n - 1) {
System.out.print(" -> ");
}
}
System.out.println();
return true;
} else {
System.out.println("不存在安全序列,系统处于不安全状态。");
return false;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("输入进程数量(n):");
int n = scanner.nextInt();
System.out.println("输入资源种类数量(m):");
int m = scanner.nextInt();
BankersAlgorithm banker = new BankersAlgorithm(n, m);
banker.initialize();
banker.isSafe();
}
}
这段代码实现了银行家算法的主要功能。用户需要输入每种资源的总数、每个进程的最大需求矩阵以及每个进程的已分配矩阵。然后,程序会检查是否存在安全序列,并在存在安全序列时输出安全序列。如果不存在安全序列,它会提示系统处于不安全状态。
银行家算法的主要优点是它可以防止死锁并确保资源的合理分配。然而,它也有一些限制,如必须知道进程的最大资源需求和资源总量等信息,这在某些情况下可能不容易获得。此外,它假设进程的资源需求是静态的,这在某些实际情况下可能不成立。
如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法:
① 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。
② 死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。
为了能对系统是否已经发生了死锁进行检测,必须用某种数据结构来保存资源的请求和分配信息。提供一种算法,利用所给的信息来检测系统是否已经进入死锁状态。
下面是一个简单的资源分配图:
如果系统中剩余的可用资源数足够满足进程的需求,那么这个进程暂时是不会阻塞的,可以顺利地执行下去。
如果这个进程执行结束了把资源归还系统,就可以使某些正在等待资源的进程被激活,并且顺利地执行下去。
对应的,这些被激活的进程执行完之后又会归还一些资源,这样可能又会激活另外一些阻塞的进程。
如果按照上述过程分析,最终能消除所有的边,则称这个图为可完全简化的。此时一定没有发生死锁(即相当于能找到一个安全序列)。相反,如果最终不能消除所有边,则此时就发生了死锁(最终还连着边的哪些进程就是处于死锁状态的进程)。
死锁的解除是指采取措施来终止已经发生的死锁,以使系统能够恢复正常运行。死锁解除的方法有多种,以下是一些常见的死锁解除策略:
进程终止:强制终止一个或多个导致死锁的进程,以释放它们持有的资源。通常,选择终止哪个进程需要谨慎考虑,可能会选择影响最小的进程或优先级较低的进程。
资源抢占:将资源从一个或多个进程中抢占,然后将它们分配给其他进程。这需要确保被抢占的进程能够在适当的时候重新获得资源,以避免数据不一致或其他问题。
进程回退:当一个进程请求无法满足时,它可以释放一些已经占用的资源,然后等待一段时间后再次请求。这可以减少资源争用,有助于解除死锁。
等待超时:为资源请求设置超时限制,如果一个进程在等待资源的时间超过限制,就放弃资源请求并释放已占用的资源。这有助于防止进程陷入无限等待状态。
资源回收:引入一种机制,允许系统回收长时间未被使用的资源,然后将这些资源重新分配给其他进程。这可以减少资源的浪费和持有时间。
全局回滚:如果死锁解除的方法较复杂,可以选择执行全局回滚,即将系统恢复到某个先前的快照状态,然后重新开始执行进程。这种方法会丢失一些进程的进度,但可以解除死锁。
如何决定该处理哪个进程?
死锁解除的具体方法取决于系统的要求和资源管理策略,需要谨慎考虑,以确保解除死锁后系统可以继续正常运行。此外,死锁解除通常需要监控和检测死锁的存在,以及选择合适的解除策略。死锁解除通常是在死锁检测之后执行的,以确保系统能够尽早恢复正常运行。