1、前趋图和程序执行
前趋图,就是一个有向无环图,也记为DAG,用于描述进程之间执行的先后顺序。每个节点具有的权重表示该节点所含有的程序数量或者执行时间。
程序在顺序执行时的特征:
- 顺序性
- 封闭性:
即程序运行时独占全机资源,资源的状态(除初始状态外)只有本程序才能改变它 - 可再现性:
不论它是从头到尾不停顿地执行,还是"停停走走"地执行,都可获得相同的结果
程序在并发执行时的特征:
- 间断性
- 失去封闭性
- 不可再现性
2、进程
进程的各种定义
(1) 进程是程序的一次执行。
(2) 进程是可以和别的程序并发执行的计算
(3) 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
(4) 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
其实进程跟程序最重要的区别就是,进程拥有进程控制块(PCB)结构
PCB是操作系统中用于管理和描述一个进程的关键数据结构,它包含了操作系统管理和调度该进程所需的全部信息,包括标识信息、状态信息、控制信息和资源信息。当进程被创建时,操作系统会为其分配一个PCB;当进程结束时,其PCB会被回收。
进程的特征
- 动态性
- 并发性
- 独立性
- 异步性
1)进程的状态
一般情况下,进程在生命周期中有以下三种基本状态:就绪(Ready)状态、执行(Running)状态、阻塞(Block)状态。这三个都非常常见,不多描述。
特殊一点的,还有创建状态:就是进程申请空白PCB并填入信息,分配资源的过程。若此时所需资源无法得到满足,也就是内存满了之类的,进程就会处于创建状态。
终止状态:进程自然结束退出或者是操作系统kill掉等特殊情况下,进行终止状态,操作系统中会保留状态码和一些统计数据等供其他进程收集,收集完毕后就会将其PCB清零返还系统。
2)挂起操作
基于以下需要时,进程需要引入挂起操作:
- 终端用户的需要
- 父进程请求
- 负载均衡调节的需要
- 操作系统的需要
通过挂起原语Suspend和激活原语Active来进行切换进程状态。
进程控制
- 创建新进程
- 终止已完成的进程
- 将因发生异常情况而无法继续运行的进程置于阻塞状态
- 负责进程运行中的状态转换等功能
1)操作系统内核
支撑功能:中断处理、时钟管理、原语操作
2)进程的创建
OS允许一个进程创建另一个进程,也叫做父子进程,子进程还可以继续创建孙进程。为了方便描述,引入了一个有向树作为进程图。
OS启动时先运行0号内核进程(idle),即PID=0;然后创建1号进程(init)和2号内核进程,所有普通进程 均由1号进程创建,所有内核进程由2号进程创建。
3)引起创建进程的事件
典型事件有四类:
- 用户登录
- 作业调度
- 提供服务
- 应用请求
4)进程的创建流程
总结一下进程创建的整体流程:
- 调用进程创建原语Creat,申请空白PCB,获取唯一数字标识符并从PCB集合中索取一个空白PCB
- 为新进程分配其运行所需的资源,包括内存、文件、I/O设备和CPU时间等
- 初始化PCB
- 如果进程就绪能容纳就加入进程就绪队列
5)进程的终止
会引起进程终止的事件:正常结束、异常结束、外界干预
终止过程:
- 调用进程终止原语,根据标识符检索出该进程的PCB,然后读出该进程的状态。
- 若被终止进程处于执行状态,则立即终止执行并置调度标志为真,用于指示该进程被终止后应重新进行调度。
- 如果还有子孙进程,则所有子孙进程都要终止
- 将该进程所有资源都归还给父进程或者归还给系统
- 将被终止进程PCB从队列中移出,等待其他程序搜集信息
6)进程的阻塞与唤醒
会引起进程阻塞或唤醒的事件:
- 向系统请求共享资源失败
- 等待某种操作的完成
- 新数据尚未到达
- 等待新任务的到达
进程阻塞过程:
调用阻塞原语block,将PCB状态改为阻塞并将PCB插入阻塞队列。转调度程序进行重新调度,将处理机分配给另一就绪进程,并进行切换
进程唤醒过程:
有关操作的进程调用唤醒原语wakeup,将等待该事件的进程唤醒,也就是将阻塞过程倒过来一遍。
进程管理中的数据结构
1)进程控制块PCB作用:
- 作为独立运行基本单位的标志
- 能实现间断性运行方式
- 提供进程管理所需要的信息
- 提供进程调度所需要的信息
- 实现与其他进程的同步与通信
2)进程控制块中的信息
进程标识符:用于唯一标识一个进程,有两种标识符:外部标识符(显示名称),内部标识符(PID)
处理机状态(CPU上下文):处理机的各种寄存器中的内容组成
进程调度信息:进程状态、进程优先级、进程调度所需的其他信息、事件
进程控制信息:程序和数据的地址、进程同步和通信机制、资源清单、链接指针(本进程所在队列中下一进程PCB的首地址)
3)进程控制块的组织方式
线性方式,适用于进程数目不多的系统。

链接方式:根据进程不同的状态来区分组成条链,然后按照进程优先级将PCB从高到低排列

索引方式:根据进程不同的状态建立几张索引表,通过记录PCB在PCB表中的地址来进行指针调用

3、线程
引入线程的目的是减少进程在并发执行时需要进程切换所付出的时空开销,提高资源利用率和系统吞吐量
简单来说就是将进程的两个属性分开,由OS分别处理调度和拥有资源。
线程运行的状态
- 执行状态:线程获得了处理机正在运行
- 就绪状态:线程具备了各种执行条件,只须获得CPU便可立即执行
- 阻塞状态:线程在执行过程中因某件事受阻而处于暂停状态
线程控制块TCB
跟进程有进程控制块PCB一样,每个线程也有一个线程控制块TCB。
通常在多线程OS中的进程都包含了多个线程,所以多线程OS中的进程有以下属性:
- 进程是一个可拥有资源的基本单位
- 多个线程可并发执行
- 进程已经不是可执行的实体
线程的实现方式
用户级线程(User-Level Threads, ULT)
- 管理主体 :由用户空间的线程库(如POSIX的
pthread库)管理和调度,操作系统内核对此无感知。 - 切换开销:线程切换在用户态完成,无需内核介入,开销极小。
- 阻塞问题:若一个线程因系统调用阻塞,则整个进程的所有线程均被阻塞。
- 并行性:无法利用多核处理器,内核仍以进程为单位分配CPU时间片。
- 实现示例:
python
# Python的threading库即为用户级线程实现
import threading
def worker():
print("Thread running")
t = threading.Thread(target=worker)
t.start()
内核支持线程(Kernel-Supported Threads, KST)
- 管理主体:由操作系统内核直接管理,每个线程在内核有独立数据结构。
- 切换开销:线程切换需陷入内核态,开销较大。
- 阻塞问题:单线程阻塞不影响其他线程执行。
- 并行性:支持多核并行调度,可真正实现并发执行。
- 实现示例:
c
// Linux内核线程创建(简化示例)
#include <linux/kthread.h>
int pthread_create(pthread_t *thread, pthread_addr_t *arr, void*(*start_rtn(void *), void *arg));
struct task_struct *thread = kthread_run(thread_func, NULL, "my_thread");
映射关系
| 模型 | 特点 |
|---|---|
| 多对一 | 多个ULT映射到单个KST,易阻塞但高效 |
| 一对一 | 每个ULT绑定独立KST,资源消耗大但并发强 |
| 多对多 | 动态映射,平衡效率与并发(如Solaris的LWP) |

性能对比
- 创建速度:ULT创建比KST快约100倍
- 切换速度:ULT切换比KST快约10倍
- 系统调用:ULT阻塞影响整个进程,KST仅影响当前线程
通过理解这两种线程模型的差异,开发者可根据应用场景选择最优方案:计算密集型任务适合KST,I/O密集型任务可优先考虑ULT。
线程的创建和终止
利用函数或者系统调用来实现新线程创建,提供线程主程序的入口指针、堆栈大小、优先级等。
线程终止:
大部分OS中,线程终止后不会立即释放它占有的资源,只有当进程中其他的线程执行了分离函数后,被终止的线程才与资源分离。
4、互斥和同步
同步的概念
协调多个进程的执行顺序,确保它们在特定点上等待或继续,以避免冲突。例如,生产者-消费者问题中,生产者生产数据后,消费者才能消费。
1)两种形式的制约关系
- 间接相互制约关系:竞争使用临界资源
- 直接相互制约关系:进程间相互合作
2)临界资源(Critical Resouce)
打印机、磁带机等,属于临界资源,进程间应该用互斥方式实现对资源的共享
3)临界区(Critical section)
对于硬件临界资源和软件临界资源,每个进程中访问临界资源的那段代码称为临界区。
4)同步机制应遵循的规则
- 空闲让进
- 忙则等待
- 有限等待
- 让权等待
硬件同步机制
- 硬件同步机制依赖于CPU提供的原子指令(atomic instructions),这些指令在单个机器周期内完成,不可被中断。这确保了操作的原子性(atomicity),即操作要么完全执行,要么不执行,不会出现部分执行的情况。
- 目的:通过硬件直接实现简单的同步原语(如锁),减少软件开销,提高性能。常见的硬件同步机制包括测试并设置(Test and Set)、交换(Swap)和比较并交换(Compare and Swap)等。
1)测试并设置(Test and Set)
-
这是一个原子指令,通常由硬件实现(如x86架构的
XCHG指令)。 -
工作原理:它操作一个共享的布尔变量(例如,表示锁的状态)。指令测试变量的当前值,如果为0(表示锁空闲),则设置为1(表示锁被占用),并返回旧值;如果为1(表示锁已被占用),则直接返回1。
-
数学表示:令共享锁变量为lock,初始值为0。Test and Set指令定义为: $$TS(lock) = \begin{cases} \text{返回 } 0 \text{ 并设置 } lock = 1, & \text{如果 } lock = 0 \ \text{返回 } 1, & \text{如果 } lock = 1 \end{cases}$$
-
在互斥中的应用:进程在进入临界区前调用Test and Set。如果返回0,则获得锁,进入临界区;否则,等待或重试。退出临界区时,将lock重置为0。
-
示例伪代码:
plaintext
// 共享变量
boolean lock = 0;
// 进程代码
while (TS(lock) == 1) {
// 忙等待(busy waiting),直到锁空闲
}
// 进入临界区
// ... 访问共享资源 ...
// 退出临界区
lock = 0; // 释放锁
2)关中断
-
关中断是实现互斥的最简单的方法之一。在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断,有效地保证了互斥。
-
关中断方法的缺点:① 滥用关中断权力可能导致严重后果;② 关中断时间过长,会影响系统效率,限制了处理器交叉执行程序的能力;③ 不适用于多CPU系统,因为在一个处理器上关中断并不能防止进程在其它处理器上执行相同的临界段代码。
3)使用 swap 指令实现进程互斥
在并发编程中,swap 指令是一种原子操作,用于交换两个变量的值。它可以用来实现简单的互斥锁机制,确保多个进程不会同时进入临界区。具体实现时,我们使用一个共享变量(如锁变量)来表示锁的状态:值为 0 表示锁空闲,值为 1 表示锁已被占用。进程通过 swap 指令尝试获取锁,如果失败则忙等(busy-wait),直到锁可用。
以下是使用 swap 指令实现进程互斥的伪代码。伪代码基于一个假设的原子 swap 函数,它原子地交换两个变量的值并返回第一个变量的旧值。
plaintext
// 共享变量,表示锁状态:0 表示空闲,1 表示占用
共享变量 lock = 0
// 进程尝试获取锁的函数
函数 获取锁():
本地变量 temp = 1
当 swap(lock, temp) != 0: // 原子交换:如果 lock 旧值为 1,则锁被占用
忙等 // 进程等待,直到锁可用
结束当
// 成功获取锁,进入临界区
// 进程释放锁的函数
函数 释放锁():
lock = 0 // 直接设置锁为 0,表示空闲
代码解释
- 获取锁过程 :进程调用
获取锁()函数时,首先设置本地变量temp为 1。然后,使用原子swap指令交换lock和temp的值。swap操作返回lock的旧值:- 如果旧值为 0,表示锁空闲,进程成功获取锁,进入临界区。
- 如果旧值为 1,表示锁被占用,进程进入忙等循环(例如空循环),不断重试 swap 操作,直到锁可用。
- 释放锁过程 :进程在退出临界区后调用
释放锁()函数,直接将lock设置为 0,表示锁已释放。 - 原子性 :
swap指令是原子的,确保在多个进程并发访问时不会出现竞态条件。 - 忙等缺点:这种方法基于忙等,可能浪费 CPU 资源,但在简单系统或硬件支持场景下有效。
这种实现是互斥锁的一种基本形式,适用于低级并发控制场景。在实际系统中,可能还需要考虑优化或避免忙等。