3 进程和线程
3.1 进程
进程的定义与组成 🔹
进程是 进程实体 的运行过程,是系统进行 资源分配和调度 的一个独立单位;
进程实体包括:PCB + 程序段 + 数据段 三部分;
PCB:进程描述信息、进程控制和管理信息、资源分配信息;
程序段:程序的代码;
数据段:运行过程中出生的各种数据;
PCB(Process Control Block) 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。可将 PCB 视为进程的大脑。
当操作系统创建一个新进程时,会为该进程分配一个唯一的进程 ID,并且为该进程创建一个对应的进程控制块。当进程执行时,PCB 中的信息会不断变化,操作系统会根据这些信息来管理和调度进程。
PCB 主要包含下面几部分的内容:进程描述信息、进程控制和管理信息、资源分配信息
- 进程的描述信息,包括进程的名称、标识符等等;
- 进程的调度信息,包括进程阻塞原因、进程状态(就绪、运行、阻塞等)、进程优先级(标识进程的重要程度)等等;
- 进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。
- 进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。
- 处理机的状态信息(由处理机的各种寄存器中的内容组成的),包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
- …
进程的特征
进程的状态与转换 ⭐️
我们一般把进程大致分为 5 种状态,这一点和线程很像!
- 创建状态(new) :进程正在被创建,尚未到就绪状态。
- 就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
- 运行状态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
- 阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
进程的组织方式
链接方式:将同一状态的PCB链接成一个队列;
索引方式:将同一状态的进程组织在一个索引表中,索引表的表项指向相应的PCB;
进程控制
-
进程的创建
申请空白 PCB; 为新进程分配所需资源; 初始化 PCB; 将 PCB 插入就绪队列;
-
进程的终止
从 PCB 集中中找到终止进程的 PCB; 若进程正在运行,立刻剥夺 CPU, 将 CPU 分配给其它进程;
-
进程的阻塞
找到要阻塞的进程对应的 PCB; 保护进程运行现场,将 PCB 状态信息设置为 阻塞态,暂时停止进程执行; 将 PCB 插入相应事件的等待队列;
-
进程的唤醒
在事件等待队列中找到PCB; 将 PCB 从等待队列移除,设置进程为就绪态; 将 PCB 插入就绪队列,等待被调度;
-
进程状态的切换
将运行环境信息存入 PCB; PCB 移入相应队列; 选择另一个进程执行,并更新其 PCB; 根据 PCB 回复新进程所需的运行环境;
进程的通信 ⭐️
- 共享存储
相互通信的进程 互斥地 共享 某些 数据结构 或 存储区,进程之间能够通过这些空间进行通信;
- 消息传递
进程之间的数据交换以 格式化的信息 为单位,将通信的数据封装在信息中,并利用操作系统提供的一组 通信命令(原语),在进程间进行信息传递,完成进程间的数据交换;
① 直接通信方式:送货上门
② 间接通信方式:快递到驿站
- 管道通信
3.2 进程与线程
-
进程(Process) 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。
-
线程(Thread) 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。
进程和线程的区别
- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
有了进程为什么还需要线程?
-
进程切换是一个开销很大的操作,线程切换的成本较低。
-
线程更轻量,一个进程可以创建多个线程。
-
多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。
而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。
-
同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核。
为什么要使用多线程?
先从总体上来说:
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
再深入到计算机底层来探讨:
- 单核时代: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
- 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
线程间的同步方式 ⭐️
线程同步是两个或多个共享关键资源的线程的并发执行。同步线程以避免关键的资源使用冲突。
下面是几种常见的线程同步的方式:
- 互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的
synchronized
关键词和各种Lock
都是这种机制。 - 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
- 信号量(Semaphore) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
- 屏障(Barrier) :屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的
CyclicBarrier
是这种机制。 - 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
3.3 进程调度
三层调度的联系与对比
进程调度的时机
进程调度的方式
调度算法 ⭐️
-
先来先服务(FCFS) 按照进程到达的先后顺序;
-
短作业优先(SJF) 服务时间最短的进程优先;
抢占式版本:最短剩余时间优先算法 SRTN; -
高响应优先(HRRN) 响应比 = (等待时间 + 要求服务时间) / 要求服务时间
-
时间片轮转调度算法(RR) 根据进程到达就绪队列的顺序,轮流调度;
-
优先级调度算法 调度时选择优先级最高的;
-
多级反馈队列调度算法
设置多级就绪队列;
各队列按照 FCFS + 时间片,时间片结束进入下一级队列队尾;
只有第 k 级队列为空时,才会为 k+1 级分配时间片;
在 Unix/Linux 系统中,子进程通常是通过 fork()系统调用创建的,该调用会创建一个新的进程,该进程是原有进程的一个副本。子进程和父进程的运行是相互独立的,它们各自拥有自己的 PCB,即使父进程结束了,子进程仍然可以继续运行。
当一个进程调用 exit()系统调用结束自己的生命时,内核会释放该进程的所有资源,包括打开的文件、占用的内存等,但是该进程对应的 PCB 依然存在于系统中。这些信息只有在父进程调用 wait()或 waitpid()系统调用时才会被释放,以便让父进程得到子进程的状态信息。
这样的设计可以让父进程在子进程结束时得到子进程的状态信息,并且可以防止出现“僵尸进程”(即子进程结束后 PCB 仍然存在但父进程无法得到状态信息的情况)。
- 僵尸进程 :子进程已经终止,但是其父进程仍在运行,且父进程没有调用 wait()或 waitpid()等系统调用来获取子进程的状态信息,释放子进程占用的资源,导致子进程的 PCB 依然存在于系统中,但无法被进一步使用。这种情况下,子进程被称为“僵尸进程”。避免僵尸进程的产生,父进程需要及时调用 wait()或 waitpid()系统调用来回收子进程。
- 孤儿进程 :一个进程的父进程已经终止或者不存在,但是该进程仍在运行。这种情况下,该进程就是孤儿进程。孤儿进程通常是由于父进程意外终止或未及时调用 wait()或 waitpid()等系统调用来回收子进程导致的。为了避免孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为 init 进程(进程号为 1),由 init 进程来回收孤儿进程的资源。
Linux 下可以使用 Top 命令查找,
zombie
值表示僵尸进程的数量,为 0 则代表没有僵尸进程。下面这个命令可以定位僵尸进程以及该僵尸进程的父进程:
ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'
3.4 同步和互斥
临界资源与临界区
一次仅允许一个进程使用的资源称为 临界资源;
对于 临界资源 的访问,必须是 互斥 进行的;每个进程中,访问临界资源的那段代码成为 临界区;
进程同步与互斥 ⭐️
进程同步 是 直接制约关系,进程互斥 是 间接制约关系。
同步:并发进程为 完成同一任务 所进行的 工作顺序协调;
互斥:并发进程为 竞争临界资源 所进行的 资源分时占用;
进程互斥软、硬件实现
3.5 死锁 🌟
死锁(Deadlock)描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
① 死锁 各进程互相等待对方的资源,导致各进程都阻塞。(资源永远不会释放)
特点:至少两个或两个以上的进程同时死锁; 处于阻塞态;
② 饥饿 长期得不到想要的资源。(资源会释放,但不会被分配)
特点:一个进程也可能出现饥饿现象; 可能阻塞态 (等 IO),也可能就绪态 (等 CPU);
③ 死循环 死锁和饥饿是由资源的不合理分配导致,死循环是由代码逻辑错误导致。
产生死锁的必要条件
① 互斥条件 只有对互斥资源的争抢才会导致死锁。
② 不可剥夺条件 进程保持的资源只能主动释放,不可强行剥夺。
③ 请求和保持条件 保证着某些资源不放的同时,请求别的资源
④ 循环等待条件 存在一种进程资源的循环等待链;
循环等待未必死锁,死锁必定循环等待。
注意 ⚠️ :这四个条件是产生死锁的 必要条件 ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
公式:资源 大于 进程个数*(每个线程需要的最大资源总数-1)
m>n*(w-1) 【m = n * (w - 1) + 1时可以避免死锁】
习题:一台计算机有8台磁带机,它们由N个进程竞争使用,每个进程可能需要3台磁带机。请问N为多少时,系统没有死锁危险总资源:8 8> n(3-1) n小于4的时候都可以
解决死锁的方法
解决死锁的方法可以从多个角度去分析,一般的情况下,有预防,避免,检测和解除四种。
- 预防 是采用某种策略,限制并发进程对资源的请求,从而使得死锁的必要条件在系统执行的任何时间上都不满足。
- 避免则是系统在分配资源时,根据资源的使用情况提前做出预测,从而避免死锁的发生
- 检测是指系统设有专门的机构,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。
- 解除 是与检测相配套的一种措施,用于将进程从死锁状态下解脱出来。
死锁的预防
思路:破坏死锁的四个必要条件 (完全没有死锁的可能)
死锁的避免
思路:避免系统进入不安全状态 (有可能死锁,避免陷入死锁)
** 银行家算法
**:在资源分配之前,先判断此次分配是否会导致系统进入 不安全状态 ,再决定是否分配资源。
- 安全状态一定不会发生死锁;
- 不安全状态可能会发生死锁;
银行家算法用一句话表达就是:当一个进程申请使用资源的时候,银行家算法 通过先 试探 分配给该进程资源,然后通过 安全性算法 判断分配后系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待,若能够进入到安全的状态,则就 真的分配资源给该进程。
死锁的避免(银行家算法)改善了 资源使用率低的问题 ,但是它要不断地检测每个进程对各类资源的占用和申请情况,以及做 安全性检查 ,需要花费较多的时间。
死锁的检测和解除
① 死锁检测算法
-
定时运行一个“死锁检测程序”,用于检测系统当前是否发生死锁;
-
资源分配图:如果最终 不能消除所有边 ,此时就是发生了 死锁 。
死锁检测步骤
- 如果进程-资源分配图中无环路,则此时系统没有发生死锁
- 如果进程-资源分配图中有环路,且每个资源类仅有一个资源,则系统中已经发生了死锁。
- 如果进程-资源分配图中有环路,且涉及到的资源类有多个资源,此时系统未必会发生死锁。如果能在进程-资源分配图中找出一个 既不阻塞又非独立的进程 ,该进程能够在有限的时间内归还占有的资源,也就是把边给消除掉了,重复此过程,直到能在有限的时间内 消除所有的边 ,则不会发生死锁,否则会发生死锁。(消除边的过程类似于 拓扑排序)
② 死锁解除算法
- 当发生死锁时,将系统从死锁状态中解脱出来;
- 如何解除:
- 资源剥夺法;从涉及死锁的一个或几个进程中抢占资源,把夺得的资源再分配给涉及死锁的进程直至死锁解除
- 终止进程法;立即结束所有进程的执行,重新启动操作系统
- 进程回退法;逐个撤销涉及死锁的进程,回收其资源直至死锁解除