【愚公系列】软考高级-架构设计师 017-进程管理
🏆 作者简介,愚公搬代码
🏆《头衔》:华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。
🏆《近期荣誉》:2022年度博客之星TOP2,2023年度博客之星TOP2,2022年华为云十佳博主,2023年华为云十佳博主等。
🏆《博客内容》:.NET、Java、Python、Go、Node、前端、IOS、Android、鸿蒙、Linux、物联网、网络安全、大数据、人工智能、U3D游戏、小程序等相关领域知识。
🏆🎉欢迎 👍点赞✍评论⭐收藏
🚀前言
进程管理是操作系统中一个核心的功能,负责创建、调度、同步和终止进程。一个进程基本上是一个程序的执行实例,包含了程序的代码和其活动的数据以及执行历史的状态。有效的进程管理对于确保系统的稳定性、效率和公平性至关重要。
🚀一、进程管理
🔎1.同步与互斥
🦋1.1 互斥
进程互斥是指在多进程环境中,防止多个进程同时访问某个共享资源或执行某个特定的代码区段的机制。这种机制确保在同一时刻,只有一个进程能够访问到关键的资源或执行关键的任务,从而避免数据的不一致性和竞态条件。
进程互斥的重要性
在没有适当的互斥机制的情况下,如果多个进程同时修改同一个数据,可能会导致数据损坏或系统行为不可预测。例如,当两个进程同时更新同一个银行账户的余额时,如果没有适当的同步,最终的账户余额可能不正确。
实现进程互斥的方法
操作系统和编程语言通常提供了多种机制来实现进程间的互斥,包括:
-
互斥锁(Mutex): 互斥锁是一种保护共享资源的常见同步机制。当一个进程需要访问共享资源时,它首先尝试锁定互斥锁。如果锁已被另一个进程持有,则该进程将等待(阻塞)直到锁变为可用。一旦获取了锁,该进程可以安全地访问资源,使用完毕后需要释放锁。
-
信号量(Semaphores): 信号量是一种更通用的同步机制,可以用于互斥和协调多个进程的执行。在互斥使用场景中,通常初始化为1的信号量可以作为二元信号量或互斥锁使用。
示例:银行账户操作
假设有两个进程,一个是存款进程,另一个是取款进程,它们都需要访问同一个银行账户的余额。
- 存款进程:读取账户余额,增加一个特定金额,然后保存新的余额。
- 取款进程:读取账户余额,减去一个特定金额,然后保存新的余额。
为了保证这两个进程不会同时修改账户余额,我们可以使用互斥锁来实现互斥:
import threading
# 创建一个互斥锁
mutex = threading.Lock()
def deposit(account, amount):
with mutex:
current_balance = account.balance
new_balance = current_balance + amount
account.balance = new_balance
print(f"Deposited {amount}; New Balance = {account.balance}")
def withdraw(account, amount):
with mutex:
if account.balance >= amount:
current_balance = account.balance
new_balance = current_balance - amount
account.balance = new_balance
print(f"Withdrew {amount}; New Balance = {account.balance}")
else:
print("Insufficient funds")
class BankAccount:
def __init__(self, initial_balance=0):
self.balance = initial_balance
# 示例
account = BankAccount(100)
# 假设这些函数在不同进程中调用
deposit(account, 50)
withdraw(account, 70)
在这个例子中,无论存款还是取款操作,我们都通过mutex
确保了每次只有一个操作可以修改账户余额,防止了因并发访问而可能出现的错误余额计算。这种方式保证了银行账户余额的正确性和一致性。
🦋1.2 临界资源
临界资源是指在多任务或多进程环境中,多个任务或进程都需要访问但同时只能被一个任务或进程使用的资源。这类资源如果不进行适当的管理和保护,同时访问它们的多个进程可能会导致资源冲突、数据不一致或系统行为异常。
为什么需要关注临界资源
在并发编程中,正确管理临界资源是保证程序正确执行的关键。如果多个进程或线程不受控制地同时访问临界资源,可能会引发竞态条件,即最终结果依赖于进程或线程执行的精确时序。这通常会导致程序出错,而且这类错误往往难以调试和修复,因为它们在不同的运行时可能表现不一样。
临界资源的例子
-
全局变量:在多线程程序中,全局变量可以被所有线程访问和修改。如果没有适当的同步机制,同时对这些变量的读写操作可能导致未定义的行为或数据损坏。
-
文件和数据库:多个进程或线程可能需要读写同一个文件或数据库条目。如果不加锁控制,同时写入操作可能覆盖彼此的数据,导致数据丢失或损坏。
-
硬件设备:如打印机或其他I/O设备,多个进程可能需要使用同一设备。如果不对这些设备访问进行适当同步,可能会导致命令交叉或设备状态混乱。
示例:文件写入
考虑一个系统日志文件,多个应用程序可能需要写入日志到同一文件。如果没有合适的同步机制,日志条目可能会交织在一起,从而损坏日志文件的结构。
import threading
# 用于文件写入的锁
lock = threading.Lock()
def log_to_file(message):
with lock:
with open("system.log", "a") as file:
file.write(message + "\n")
# 示例用法
thread1 = threading.Thread(target=log_to_file, args=("Application A error",))
thread2 = threading.Thread(target=log_to_file, args=("Application B error",))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
在这个例子中,我们使用了一个锁(lock
)来确保在任何给定时刻只有一个线程可以写入文件。这防止了来自不同应用程序的日志消息相互覆盖或交叉,确保日志文件的完整性和可读性。
管理临界资源的常见策略
- 互斥锁(Mutex):用于保护资源,确保每次只有一个线程或进程可以访问资源。
- 信号量(Semaphores):可以用于限制对资源的访问,控制同时访问资源的线程或进程数目。
- 条件变量:允许线程在特定条件下挂起执行并等待资源变得可用。
- 读写锁:允许多个读操作同时进行,但写操作会独占资源。
🦋1.3 互斥信号量
互斥信号量是一种用于进程或线程同步的机制,确保多个进程或线程中只有一个能够访问临界资源。互斥信号量通常是一个变量,其值限制了可同时进入临界区的线程数目。在互斥的应用中,这个值被初始化为1,这意味着在任何时刻只允许一个线程进入临界区。
互斥信号量的工作原理:
-
等待(Wait)操作:一个线程在进入临界区之前执行等待操作。如果信号量的值大于0,信号量的值减1,线程进入临界区。如果信号量的值为0,线程进入等待状态,直到信号量值变为正。
-
释放(Signal)操作:当线程离开临界区时执行释放操作,信号量的值增加1。如果有其他线程正在等待这个信号量,它们中的一个将被唤醒并允许进入临界区。
示例:使用互斥信号量同步两个线程
假设有两个线程,分别执行不同的任务,但它们需要共享访问一个打印机(临界资源)。我们可以使用互斥信号量来确保在任何时刻只有一个线程可以使用打印机。
这里是一个简单的Python代码示例,展示了如何使用线程模块中的信号量来同步线程访问:
import threading
import time
# 创建一个互斥信号量
mutex = threading.Semaphore(1)
# 一个简单的函数,模拟打印任务
def printer_task(document):
mutex.acquire()
print(f"Printing: {document}")
time.sleep(2) # 模拟打印需要一些时间
print("Printing finished.")
mutex.release()
# 创建线程
thread1 = threading.Thread(target=printer_task, args=("Document1",))
thread2 = threading.Thread(target=printer_task, args=("Document2",))
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
在这个例子中,mutex
是一个被初始化为1的信号量,保证了printer_task
函数在同一时间内只能由一个线程执行。当一个线程开始执行打印任务时,它首先需要获得信号量(通过acquire()
方法),在打印结束后释放信号量(通过release()
方法)。如果信号量已经被占用(值为0),其他尝试访问打印机的线程将会阻塞,直到信号量被释放。
使用互斥信号量的优点:
- 简单有效:互斥信号量是一种简单有效的同步机制,尤其适用于控制对单个资源的访问。
- 避免死锁:合理使用互斥信号量可以帮助避免死锁,尤其是在每个临界区外只使用一个信号量的情况下。
互斥信号量是并发编程中一个非常重要的工具,帮助开发者在多线程和多进程环境中安全地管理对临界资源的访问。
🦋1.4 同步
进程同步是操作系统中的一个机制,用于协调在多进程环境中运行的进程,以确保它们以有序和一致的方式访问共享资源或执行相关任务。这种同步主要是为了避免竞态条件、确保数据一致性并防止诸如死锁之类的问题。
为什么需要进程同步
在多进程系统中,进程通常需要共享某些资源(如内存、文件等),或者在执行时需要相互通信。如果没有适当的同步,进程间的互相干扰可能导致资源冲突、数据损坏或系统行为不可预测。
进程同步的常用方法
-
信号量(Semaphore):一种广泛使用的同步工具,用于控制对共享资源的访问。信号量的值表示可用资源的数量。当进程需要资源时,它会执行等待(P)操作,这会减少信号量的值。当进程释放资源时,它执行信号(V)操作,这会增加信号量的值。
-
互斥锁(Mutex):一种保证在任何时刻只允许一个进程或线程访问共享资源的同步机制。互斥锁可以看作是只允许一个资源使用者的信号量。
-
条件变量:通常与互斥锁一起使用,允许进程在某些条件尚未满足时阻塞自身,直到其他进程改变条件并通知条件变量解除阻塞。
-
管道(Pipes)和消息队列:这些通信机制允许进程以同步方式交换数据,是进程间通信的一部分,也有助于同步操作。
示例:使用信号量实现进程同步
假设有一个系统中有三个进程:生产者、消费者和协调器。生产者生成数据,消费者处理数据,协调器控制数据流向,以确保消费者不会在没有数据的情况下运行(即避免空消费),生产者在缓冲区满时停止生产(避免溢出)。
这里可以使用两个信号量:一个表示空闲(可用于生产的空间),另一个表示满的(可用于消费的数据项)。初始化时,空闲信号量的值设为缓冲区的大小,满信号量的值设为0。
from threading import Thread, Semaphore
import time
# 信号量初始化
empty = Semaphore(10) # 假设缓冲区大小为10
full = Semaphore(0)
buffer = []
def producer():
global buffer
for i in range(20):
empty.acquire() # 等待空闲空间
buffer.append(i) # 生产数据
print(f"Produced {i}")
full.release() # 增加可消费的数据项
time.sleep(1)
def consumer():
global buffer
for i in range(20):
full.acquire() # 等待数据
data = buffer.pop(0) # 消费数据
print(f"Consumed {data}")
empty.release() # 增加空闲空间
time.sleep(1.5)
# 创建并启动线程
t1 = Thread(target=producer)
t2 = Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()
在这个例子中,empty
和 full
信号量确保生产者和消费者可以协调对共享缓冲区buffer
的访问。生产者在添加数据前必须确保有空间(empty
),而消费者在取数据前必须确保有数据可取(full
)。
🦋1.5 同步信号量
同步信号量是一种在多进程或多线程环境中用来控制不同执行流之间同步的机制。与互斥信号量主要用于实现互斥(即,防止多个进程或线程同时访问共享资源)不同,同步信号量主要用来协调进程或线程的执行顺序,确保它们在某些关键操作或条件满足前后能够正确地协同工作。
工作原理
同步信号量通常用来解决生产者-消费者问题,其中生产者和消费者需要协调它们对共享资源(如缓冲区)的访问。同步信号量主要用于:
- 控制资源的使用顺序,确保特定资源/任务在其依赖资源/任务完成后才开始。
- 控制执行流之间的协作,使得一些行为必须在其他行为之后发生。
基本操作
同步信号量包含两个主要操作:
- Wait(等待或P操作):用于请求资源。如果信号量的值大于零,表示资源可用,进程或线程可以继续执行,并将信号量的值减一。如果信号量的值为零,进程或线程将被阻塞,直到信号量的值大于零。
- Signal(信号或V操作):用于释放资源或通知其他进程/线程可以继续执行。执行此操作会将信号量的值加一。如果有其他进程或线程因等待这个信号量而被阻塞,它们中的一个将被唤醒。
例子:使用同步信号量解决生产者-消费者问题
假设有一个固定大小的缓冲区,生产者向缓冲区中放入数据,消费者从缓冲区中取出数据。为了确保消费者不会在缓冲区空时试图取出数据,以及生产者不会在缓冲区满时试图放入数据,我们可以使用两个信号量:一个用于表示空闲的槽位数(可以生产的数量),另一个用于表示已填充的槽位数(可以消费的数量)。
以下是使用Python的threading
模块实现的示例:
import threading
# 初始化信号量,空槽位信号量为缓冲区大小,满槽位信号量为0
empty_slots = threading.Semaphore(10)
filled_slots = threading.Semaphore(0)
buffer = []
def producer():
global buffer
for i in range(20):
empty_slots.acquire() # 等待空槽位
buffer.append(i) # 生产数据
print(f"Produced {i}")
filled_slots.release() # 增加已填充的槽位数
def consumer():
global buffer
for i in range(20):
filled_slots.acquire() # 等待已填充的槽位
data = buffer.pop(0) # 消费数据
print(f"Consumed {data}")
empty_slots.release() # 增加空槽位数
# 创建并启动生产者和消费者线程
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
consumer_thread.join()
在这个例子中,empty_slots
信号量控制了可以放入缓冲区的项目数,而filled_slots
信号量控制了可以从缓冲区取出的项目数。这种方式确保生产者不会在缓冲区已满时继续放入数据,消费者也不会在缓冲区为空时尝试取出数据,从而协调了生产者和消费者之间的同步。
🔎2.信号量
P操作:申请资源,S=S-1,若S>=0,则执行P操作的进程继续执行;若S<0,则置该进程为阻塞状态
(因为无可用资源),并将其插入阻塞队列。
V操作:释放资源,S=S+1,若S>0,代表此时资源有空余,没有阻塞的进程,则该进程继续执行;若
S<=0,代表此时线程在被阻塞,所以需要从阻塞状态唤醒一个进程,并将其插入就绪队列(此时因为缺
少资源被P操作阻塞的进程可以继续执行),然后执行V操作的进程继续。
🔎3.生产者和消费者
🦋3.1 概念
经典问题:生产者和消费者的问题
三个信号量:互斥信号量S0(仓库独立使用权),同步信号量S1(仓库空闲位置),同步信号量S2(仓库商
品个数)。
生产者流程:
生产一个商品S
P(S0)
P(S1)
将商品放入仓库中
V(S2)
V(S0)
消费者流程:
P(S0)
P(S2)
取出一个商品
V(S1)
V(S0)
🦋3.2 练习
🔎4.死锁
死锁是操作系统中的一个常见问题,特别是在多任务和并发环境中。死锁发生时,两个或多个进程因为相互竞争资源而无法继续执行,每个进程都在等待其他进程释放它所需要的资源。
🦋4.1 死锁的四个必要条件
要发生死锁,以下四个条件必须同时满足:
- 互斥条件:至少有一个资源必须处于非共享模式,也就是说,一次只有一个进程可以使用资源。如果其他进程请求该资源,请求者只能等待,直到资源被释放。
- 持有和等待条件:一个进程至少持有一个资源,并且等待获取其他进程持有的资源。
- 非抢占条件:资源不能被抢占,也就是说,资源不能从一个进程中强制移除,只能由持有它的进程显式释放。
- 循环等待条件:发生死锁时,必须存在一个进程—资源的循环链,其中每个进程都在等待下一个进程持有的资源。
🦋4.3 死锁的处理策略
对死锁的处理可以分为以下几种策略:
-
死锁预防:通过破坏导致死锁的四个必要条件中的至少一个来预防死锁的发生。例如,可以通过实施资源一次性分配策略(破坏持有和等待条件)或只允许在没有其他资源请求的情况下请求资源(破坏循环等待条件)。
-
死锁避免:在死锁预防的基础上,更为动态地处理资源分配问题。系统在资源分配之前检查此次分配是否可能导致系统进入不安全状态(即可能导致死锁的状态),使用算法(如银行家算法)来确保系统始终处于安全状态。
-
死锁检测和恢复:在这种策略中,系统不尝试预防或避免死锁,而是允许死锁发生,然后通过某种方式检测它并采取措施解决。死锁检测通常通过维护和分析资源分配图来进行。一旦检测到死锁,可以通过杀死进程、回滚操作或逐步撤销进程的资源来解决。
死锁计算问题:系统内有n个进程,每个进程都需要R个资源,那么其发生死锁的最大资源数为n*(R-1)。其不发生死锁的最小资源数为n*(R-1)+1。
🦋4.4 死锁的示例
考虑两个进程P1和P2,以及两种资源R1和R2。P1持有R1并请求R2,P2持有R2并请求R1。如果每个进程都不释放其持有的资源,他们将永远等待对方释放资源,从而陷入死锁。
死锁是一个复杂且需要仔细处理的问题,操作系统的设计必须仔细考虑如何最小化死锁的可能性并有效地管理资源。
🦋4.6 练习
🔎5.线程
引入线程概念后,传统的进程模型在操作系统中得到了扩展和精细化。在这个上下文中,进程和线程的角色和功能区别如下:
🦋5.1 进程
-
资源拥有者:在多任务操作系统中,进程是系统资源(如内存空间、打开的文件和外部设备等)的拥有单位。一个进程拥有一套完整的私有虚拟地址空间,这使得它在运行过程中具有独立性和保护性。
-
执行环境:进程提供了一个包含所有必要状态信息的执行环境,以支持一个或多个线程的执行。这包括代码、系统资源以及进程级别的数据。
🦋5.2 线程
-
调度的基本单位:线程是进程中的实际运行单位,它具有自己的执行状态,包括程序计数器、寄存器集合和栈,但与同一进程的其他线程共享内存和资源。线程的引入使得操作系统的调度更加高效,因为线程间的切换开销小于进程间的切换。
-
资源共享:线程共享其父进程的资源和地址空间,包括文件描述符、全局变量和打开的文件等。这种共享机制使得线程间的通信和数据交换更加方便和高效。
-
独有资源:尽管线程共享大部分进程资源,但每个线程有自己的独立栈(用于存储局部变量和跟踪函数调用)、线程局部存储(TLS)和独立的执行序列。
🦋5.3 线程和进程的区别
- 效率和开销:线程的创建、销毁和切换的开销远小于进程,因为线程共享其所属进程的资源。
- 数据共享与通信:线程之间由于共享内存和资源,数据共享和通信更加容易,但这也需要适当的同步机制以防止竞态条件。
- 独立性:相比线程,进程之间拥有更高的独立性。系统或进程故障通常不会影响到其他进程,而线程的错误可能会影响同一进程中的其他线程。
通过引入线程,现代操作系统能够更有效地利用多核处理器的能力,提高系统的并发性和响应速度。同时,设计和开发多线程程序时必须考虑同步、死锁和并发控制等问题,以确保程序的正确性和性能。
🚀感谢:给读者的一封信
亲爱的读者,
我在这篇文章中投入了大量的心血和时间,希望为您提供有价值的内容。这篇文章包含了深入的研究和个人经验,我相信这些信息对您非常有帮助。
如果您觉得这篇文章对您有所帮助,我诚恳地请求您考虑赞赏1元钱的支持。这个金额不会对您的财务状况造成负担,但它会对我继续创作高质量的内容产生积极的影响。
我之所以写这篇文章,是因为我热爱分享有用的知识和见解。您的支持将帮助我继续这个使命,也鼓励我花更多的时间和精力创作更多有价值的内容。
如果您愿意支持我的创作,请扫描下面二维码,您的支持将不胜感激。同时,如果您有任何反馈或建议,也欢迎与我分享。
再次感谢您的阅读和支持!
最诚挚的问候, “愚公搬代码”
- 点赞
- 收藏
- 关注作者
评论(0)