仅供个人学习参考

最大限度利用CPU

  在计算机发展早期,CPU资源十分宝贵,如果一个CPU只能运行一个程序,那么当这个程序读写磁盘是,CPU就闲下来了,这在当时无异于暴殄天物,于是人们编写了一个监控程序,当某个程序暂时不使用CPU时,监控程序就把另一个在等待CPU资源的程序启动,使CPU得以充分利用,这种方法被称为多道程序(Multiprogramming),原始但是有效,但其有个问题就是程序的执行不分轻重缓急,调度策略太过于粗糙,当多个程序同时运行时,容易出现问题,有一些急需处理的程序可能要等到很久以后才能分配到CPU,比如当你当你点击鼠标,但是要等个十分钟才有响应,这是很难受的
  稍作改进,让程序运行模式变成一种协作模式,即程序使用一段时间的CPU后会主动让出,这种程序协作模式叫做分时系统(Time-Sharing System),
此时完整的操作系统雏形已经逐渐形成,不过这种方式也存在问题,如果此时某项程序正在进行一项耗时的计算,霸占着CPU不放,那么其他程序只能等待,整个系统看上去就好像“死机”了一样。
  于是我们再进行改进,直到现在我们熟悉的多任务(Multi-tasking)系统。操作系统接管了所有的硬件资源,并且本身运行在一个受硬件保护的级别。所有的应用程序都以进程(Process)的方式运行在比操作系统更低的级别,每个进程都有自己独立的地址空间,使得进程之间的地址空间相互隔离,每个进程根据进程优先级的高低都有机会得到CPU。但是,如果运行时间超出了一定的时间,操作系统会强制暂停该进程,将CPU资源分配给其他等待运行的进程。这种CPU分配方式称为抢占式。操作系统可以强制剥夺CPU资源并且分配给它认为目前最需要CPU的进程。如果操作系统分配给每个进程的时间都很短,即CPU在多个进程间快速地切换,就能造成了很多进程都在同时运行的假象(即伪并行(pseudoparallelism) )。

进程

操作系统中的进程是指正在执行的程序的实例。进程是计算机系统中的基本执行单位,它具有独立的内存空间和执行状态。


在网上看到一个有意思的比喻:
  想想一位会做饭的计算机科学家正在为他的女儿制作生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序、计算机科学家就是 CPU、而做蛋糕的各种原料就是输入数据。进程就是科学家阅读食谱、取来各种原料以及烘焙蛋糕等一系列动作的总和。
  现在假设科学家的儿子跑过来告诉他,说他的头被蜜蜂蜇了一下,那么此时科学家会记录出来他做蛋糕这个过程到了哪一步,然后拿出急救手册,按照上面的步骤给他儿子实施救助。这里,会涉及到进程之间的切换,科学家(CPU)会从做蛋糕(进程)切换到实施医疗救助(另一个进程)。等待伤口处理完毕后,科学家会回到刚刚记录做蛋糕的那一步,继续制作。


进程状态

在操作系统中,进程可以处于不同的状态,这些状态反映了进程在执行过程中的不同情况和条件。
常见的进程状态包括:
1.新建(New):进程刚被创建但还未被操作系统接受和分配资源。
2.就绪(Ready):进程已经准备好执行并等待分配处理器资源。
3.运行(Running):进程正在执行中,占用CPU资源。
4.阻塞(Blocked):进程因为某些原因而无法继续执行,例如等待某个事件发生、等待输入/输出完成或等待资源释放。
5.终止(Terminated):进程的执行已经结束,可能是正常结束或异常结束。

进程控制

进程控制是指操作系统对进程进行创建、终止、暂停、恢复和调度等操作的管理和控制。通过进程控制,操作系统可以有效地管理进程的执行,分配资源并确保系统的正常运行。

进程创建

操作系统允许用户或其他进程创建新的进程。进程创建的过程中,操作系统会为新进程分配资源,包括内存空间、文件描述符、进程控制块等。操作系统还会为新进程设置初始状态和上下文,并将其加入进程调度队列中等待执行。

进程终止

进程可以正常终止,也可以由于错误、异常或用户请求而被强制终止。当一个进程终止时,操作系统会回收其占用的资源,释放内存空间,并更新相应的数据结构。此外,操作系统还会通知相关进程或用户进程的终止事件。

进程暂停和恢复

操作系统可以暂停一个正在运行的进程,使其暂时停止执行。进程暂停的原因可以是等待某个事件的发生,如等待 I/O 操作完成。一旦等待的事件发生,操作系统会恢复进程的执行,使其继续运行。

进程调度

操作系统通过进程调度算法决定哪些进程获得 CPU 时间片并在运行态执行。调度算法可以根据进程的优先级、时间片轮转等策略来决定进程的执行顺序。进程调度的目标是实现公平性、高性能和响应时间的平衡。

进程间通信

进程间通信(Inter-Process Communication,IPC)是指在操作系统中,不同进程之间进行数据交换、信息传递和协作的机制。进程间通信允许进程之间共享数据、同步操作和相互通知,以实现任务的协同完成。
实现方式(常见):
1.管道(Pipe):管道是一种半双工[1]的通信机制,用于在具有亲缘关系的两个进程之间进行通信。管道有两种类型:无名管道(Anonymous Pipe)和命名管道(Named Pipe)。无名管道用于父子进程之间的通信,而命名管道允许无关进程之间的通信。
2.共享内存(Shared Memory):共享内存是一种高效的进程间通信方式,它允许多个进程访问同一块内存区域。进程可以将共享内存映射到它们的地址空间中,从而可以直接读写共享数据,而无需进行数据复制。共享内存需要使用同步机制(如信号量)来协调进程对共享数据的访问。
3.信号量(Semaphore):信号量是一种用于进程间同步的机制。它可以用来实现进程的互斥访问共享资源和进程的同步操作。信号量维护一个计数器,进程可以对信号量进行等待(阻塞)和释放(唤醒)操作,以控制对共享资源的访问。(具体后面进程互斥会提到,先留个印象
4.消息队列(Message Queue):消息队列[2]是一种基于消息的进程间通信方式。进程可以将消息发送到队列中,并由其他进程从队列中接收消息。消息队列可以实现进程之间的异步通信,进程可以按照自己的节奏发送和接收消息,而不需要进行直接的同步等待。
5.套接字(Socket):套接字是一种用于网络通信的通用接口,它也可以用于进程间通信。进程可以通过套接字进行网络通信,也可以在同一台计算机上的不同进程之间进行进程间通信。套接字提供了一种可靠的、面向连接的通信方式。

[1] 半双工(Half-Duplex)通信机制是指通信双方可以交替地发送和接收数据,但不能同时进行发送和接收。比喻起来,可以将半双工通信类比为对话中的单向讲话。当一个人在说话时,另一个人必须静默等待,然后才能回应。在这种情况下,每个人都可以发送和接收信息,但不能同时进行。相比于半双工,全双工通信则允许通信双方同时发送和接收数据。
[2] 消息队列通常由消息生产者、消息队列和消息消费者组成。简单的来说,消息生产者给出消息,消息队列存放消息,消息消费者根据需求接受消息。对应上文,进程既可以当消息生产者,也可以当消息消费者

进程互斥

进程互斥是指多个进程在访问共享资源时的一种机制,旨在防止多个进程同时访问或修改同一个共享资源,从而避免数据不一致或竞争条件的发生。在并发执行的环境中,多个进程可能会同时访问共享资源,比如共享内存区域、文件、设备等。如果没有适当的互斥机制,就有可能导致数据损坏、竞争条件和不确定性的问题。
实现(常见):
1.互斥锁(Mutex):互斥锁是最常用的进程互斥机制之一。它是一种二进制标志,且只有一个进程可以持有互斥锁,每个共享资源都有一个关联的互斥锁。在访问共享资源之前,进程需要先尝试获取互斥锁。如果互斥锁已经被其他进程持有,则当前进程会被阻塞,直到互斥锁被释放。一次只有一个进程能够持有互斥锁,确保了对共享资源的互斥访问。
2.条件变量(Condition Variable):条件变量用于进程之间的通信和同步,它允许一个进程等待某个条件满足后再继续执行。条件变量通常与互斥锁一起使用[3],确保在等待条件时不会发生竞争条件。
3.信号量(Semaphore):信号量是一种计数器,用于控制同时访问共享资源的进程数量。它可以用来实现互斥和同步。当进程需要访问共享资源时,在访问之前会尝试对信号量进行减操作,如果减操作的结果小于零,则当前进程会被阻塞,直到其他进程释放了信号量。(举个栗子:假设刚开始信号量的值为3,说明此时该共享资源允许有三个进程同时访问它,每多一个进程正在访问它,信号量的值就减一,直到其值为0,此时已经满了,若再来一个进程,进行减操作后发现结果小于0,那么该进程就会被阻塞,不让它访问)
4.读写锁(ReadWrite Lock):读写锁允许多个进程同时读取共享资源,但只允许一个进程进行写操作。这种机制适用于读取频繁、写入较少的场景,可以提高并发性能。
[3] 条件变量的使用通常遵循以下模式:(1)进程获取互斥锁。(2)进程检查某个条件是否满足,如果不满足,则调用等待操作,释放互斥锁,并进入阻塞状态。(3)其他进程在一定条件下修改共享资源,并通过通知操作唤醒等待的线程。(4)被唤醒的进程重新获取互斥锁,继续执行。

进程同步

进程同步是指多个进程之间协调和控制彼此的执行顺序,以确保它们按照预期的方式进行交互和共享资源。进程同步的目的是避免竞争条件、数据不一致和其他并发问题。在并发环境中,多个进程同时执行,彼此之间的执行顺序是不确定的。这可能会导致一些问题,比如竞态条件(Race condition)[4]、死锁(Deadlock)[5]和活锁(Livelock)[6]等。为了解决这些问题,需要使用适当的进程同步机制。
实现(常见):
1.信号量(Semaphore)
2.互斥锁(Mutex)
3.条件变量(Condition Variable)
4.读写锁(ReadWrite Lock)
5.屏障(Barrier):屏障用于确保多个进程在并发执行过程中达到某个同步点之前都会等待。当所有进程都到达屏障点后,它们才能继续执行。


可以说进程互斥是进程同步的一种特殊情况。进程同步更广泛地涵盖了进程之间协调和控制的方方面面,包括确保进程按照预期的顺序执行、等待其他进程完成某个操作、通知其他进程等。而进程互斥仅关注在多个进程访问共享资源时的互斥访问问题。


[4] 竞态条件(Race Condition)是指在多线程或多进程环境中,由于多个线程或进程之间的执行顺序不确定而导致的问题。这种不确定性可能会导致共享资源的访问和修改顺序与预期不符,从而导致程序产生不正确的结果。
举个栗子

1
2
3
4
5
6
7
8
class BankAccount:
def __init__(self):
self.balance = 0

def deposit(self, amount):
current_balance = self.balance
current_balance += amount
self.balance = current_balance

假设有两个线程同时执行以下代码

1
2
3
4
5
6
7
8
9
account = BankAccount()

# 线程1的操作
thread1:
account.deposit(100)

# 线程2的操作
thread2:
account.deposit(200)

在单线程情况下,期望的结果是账户余额增加到300。但是,由于线程1和线程2是并行执行的,存在竞态条件,可能导致以下情况发生:
线程1读取账户余额为0。
线程2读取账户余额为0。
线程1计算新余额为100。
线程2计算新余额为200。
线程1更新账户余额为100。
线程2更新账户余额为200。
在这种情况下,账户余额只增加到了200,而不是期望的300。这是因为两个线程同时读取了账户余额,进行了计算和更新操作,导致竞态条件产生。

[5] 死锁(Deadlock)是指在并发系统中,两个或多个进程或线程因互相等待对方释放资源而造成的一种僵持状态,导致它们无法继续执行下去。死锁形成需满足四个条件:
1.互斥条件(Mutual Exclusion):至少有一个资源被标记为独占性资源,同一时间只能被一个进程或线程占用。
2.占有和等待条件(Hold and Wait):进程或线程至少已经持有一个资源,同时还在等待获取其他进程或线程占用的资源。
3.不可剥夺条件(No Preemption):已经占有的资源不能被其他进程或线程强制性地剥夺,只能在自愿释放后才能让其他进程使用。
4.循环等待条件(Circular Wait):存在一个进程或线程的资源请求链,形成一个循环等待的环路。
条件2、3和4简单理解就是:现有进程abc,分别占有资源123,且a还需2才能执行,b还需3才能执行,c还需1才能执行,他们都在等待对方让出资源,然后就会陷入死循环,无限等待
预防死锁只需破坏四个条件其中之一即可

[6] 活锁(Livelock)是一种类似于死锁的情况,其中多个进程或线程在相互响应对方的动作时陷入无限循环,导致它们无法继续向前推进或完成任务。在活锁中,进程或线程一直在不停地改变自己的状态以避免冲突,但是它们无法取得进展。这种情况下,虽然进程或线程没有被阻塞,但它们却无法完成所需的工作。
简单来说,发生死锁时进程等的是对方的资源,发生活锁时进程等的是对方的动作或响应,也是一个互相等待的过程

临界区

  临界区(Critical Section)是指在多线程或多进程环境中,访问共享资源或共享数据的那部分代码区域。在临界区内,只允许一个线程或进程访问共享资源,其他线程或进程必须等待。
  临界区的目的是确保在并发访问共享资源时的数据一致性和正确性。当多个线程或进程需要同时访问共享资源时,如果不进行同步控制,就可能会导致数据竞争和错误的结果。通过将访问共享资源的代码包裹在临界区中,可以保证同一时间只有一个线程或进程可以执行临界区内的代码,从而避免竞争条件的发生。
使用临界区时,需要注意以下几点:
·互斥访问:临界区内的代码需要通过互斥机制(如互斥锁、信号量等)来确保在任意时刻只有一个线程或进程可以执行临界区内的代码。
·尽量减少临界区的长度:临界区的长度越长,其他线程或进程就需要等待更长的时间才能访问共享资源,可能导致性能下降。因此,应尽量将临界区的长度缩短,只包含必要的代码。
·避免死锁:如果多个线程或进程相互等待对方释放资源,可能导致死锁。因此,在设计临界区时,需要合理设置同步机制,避免死锁情况的发生。
·公平性:在多线程环境中,应该考虑公平性,即每个线程都有机会访问临界区,避免某个线程长时间占据临界区而导致其他线程无法访问。