文章目录
最近在复习操作系统,顺便总结一下进程相关知识,参考书籍:《操作系统-精髓与设计原理》,希望对大家有所帮助。
一、什么是进程
进程从字面意思理解就是运行中的程序,是对应用程序运行状态的封装,一个应用程序的启动到关闭过程对应着一个进程的出生到死亡的过程,从进程中可以获取到应用程序运行的相关信息。进程是操作系统调度和执行的基本单位。而线程是存在于进程中一条执行路径,是CPU进行调度和资源分配的最小单位。
进程由两个基本部分组成:程序代码块和数据集。
进程的描述信息存储在对应的PCB(进程控制块)当中。
二、线程和进程的区别
- 线程只拥有启动所需的最小资源,一个进程中至少有一个以上的线程,线程又被称为轻量级进程。
- 线程的资源和地址空间都取自进程的进程映象
- 线程拥有线程上下文,线程的上下文保存了当前线程所指向代码的PC计数器、一个数据栈、处理器状态和私有的一些数据。
- 线程是CPU调度的最小单位,是进程中的一条执行路径,是资源分配的最小单位。
进程是操作系统资源分配的单位,而线程是CPU调度和资源分配的最小单位,线程所需的资源都来自于对应的进程,而线程只含有基本的堆栈。
协程存在于线程中,它和线程的区别是:
1.协程是轻量级线程,所需的上下文切换开销比线程小很多
2.协程的调度存在于用户态中,而线程的调度存在于内核态中
3.协程比线程能更好地实现并行程序
三、进程的特征
- 动态性:进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征。
- 并发性:指多个进程实体,同存于内存中,能在一段时间内同时运行,并发性是进程的重要特征,同时也是操作系统的重要特征。引入进程的目的就是为了使程序能与其他进程的程序并发执行,以提高资源利用率。
- 独立性:指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。凡未建立PCB的程序都不能作为一个独立的单位参与运行。
- 异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按各自独立的、 不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此,在操作系统中必须配置相应的进程同步机制。
- 结构性:每个进程都配置一个PCB对其进行描述。从结构上看,进程实体是由程序段、数据段和进程控制段三部分组成的。
四、进程的状态
4.1、两状态模型
两状态模型是进程状态的最基础模型,即将进程视作两个状态:运行态和未运行态,未运行态的进程进入到排队队列中,等待由分派器选择并设置为运行态,而运行态的进程将会被设置为未运行态,放入排队队列中。
4.2、五状态模型
两状态模型中的待执行队列采用先进先出的方式(不考虑优先级)从未运行的进程中选择下一个被执行的进程。但是这种模型是不合适的。当队列中有正在等待事件的被阻塞的进程时,这个进程可能会因为不能执行而再次被放回队尾,等到他解除阻塞时,他可能排在了新加入进程的后面,所以这种策略不会每次选择在队列中存在时间最久的进程。为解决这种情况的一种比较自然的方法是将未运行状态分成两个状态:就绪态 和 阻塞态。另外还可以增加两个有用的状态,新建态和退出态。
- 新建态:创建执行一个进程。
- 就绪态:操作系统开始接纳进程。
- 运行态:操作系统选择一个处于就绪态的进程运行。
- 阻塞态:进程请求了操作系统一个资源,但是操作系统无法立即返回该资源,如IO操作,那么该线程就会发生堵塞,去执行获取资源的操作(联想到NIO)
- 退出态:进程终止。
4.3、七状态模型
七状态模型是现代操作系统常用的进程状态模型,新增了就绪挂起态及阻塞挂起态。
挂起指的是将暂时无需运行的进程从内存中换入到磁盘中存储,以便腾出内存空间。当需要时再从磁盘中换出到内存之中。
TIP:线程的状态相较于进程为什么多出了一个wait状态?wait状态和阻塞状态的区别是什么?
线程的wait状态主要是描述线程被调用wait方法后,在某个资源监视器中等待,直至被唤醒的状态;而阻塞状态描述的是线程由于获取不到锁资源,而等待排队获取锁的现象。
TIP:在进程空闲时或阻塞时,会被挂起。所谓的挂起是将内存中的进程存储到磁盘中,等到可以执行时,再将进程从磁盘中调出到内存中。
五、用户级线程、内核级线程、混合型线程的区别
5.1、用户级线程(ULT,user level thread)
用户级线程指的是通过线程库来实现线程的调度,线程库运行在用户空间中,不依赖于内核的实现,所以用户级线程(又被称为协程)可以做到对内核无感知,内核不会参与用户级线程的调度和控制,操作系统仍对进程进行直接控制(Golang协程的实现)。
用户级线程的优点
- 用户级线程上下文切换在用户空间完成,无需借助内核,所以不用进行内核态转化,效率高
- 用户级线程与具体操作系统无关,只依赖于线程库的实现
- 用户级线程可以根据自身需要实现相应的调度算法,而无需受操作系统控制
用户级线程的缺点
- 操作系统侧以进程为调度单位,当线程阻塞时,该进程内所有线程都阻塞
- 由于不依赖于操作系统实现,无法利用多核CPU的优势
5.2、内核级线程(KST,kernel support thread)
内核级线程依赖于操作系统的线程实现,每个内核级线程都对应着操作系统进程内部的线程实现,线程的调度和控制依赖于操作系统内核的线程,通常操作系统对外提供相应的内核线程操作API供程序使用。操作系统内核可以感知到线程的存在和操作。
内核级线程的优点
- 借助操作系统的实现,可利用CPU多核处理器的优势实现并发执行
- 一个进程内的线程被阻塞后,其他线程仍然可以继续执行
内核级线程的缺点
线程上下文切换需要借助于操作系统内核,存在两次用户态和内核态的转化,效率较低。
5.3、混合型线程
即将用户级线程和内核支持线程两种方式进行组合,在Solaris 中,用户创建的多个用户级线程被映射到一些内核线程上(多对多的线程映射),内核线程的数目可能少于用户级线程的数目,内核级线程的数目决定了该进程的并发度。
六、进程控制块
为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。
PCB一般包括:
- 程序ID(PID、进程句柄):它是唯一的,一个进程都必须对应一个PID。PID一般是整形数字
- 特征信息:一般分系统进程、用户进程、或者内核进程等
- 进程状态:运行、就绪、阻塞,表示进程现的运行情况
- 优先级:表示获得CPU控制权的优先级大小
- 通信信息:进程之间的通信关系的反映,由于操作系统会提供通信信道
- 现场保护区:保护阻塞的进程用
- 资源需求、分配控制信息
- 进程实体信息,指明程序路径和名称,进程数据在物理内存还是在交换分区(分页)中
- 其他信息:工作单位,工作区,文件信息等 [1]
七、进程映象
进程映象包括了PCB及代码数据信息:
其中,除了PCB外的数据类型为.bss、.data、.text,stack,heap:
- .bss->.bss段(段式储存)
存储没有被初始化的全局变量 - .data->数据段
已经被初始化的全局变量 - .text->代码段
存储可执行的指令 - stack
存储局部变量、函数、参数等数据,由操作系统释放 - heap
存储数据的实例,由编译器或虚拟机释放
八、进程间通信方式
8.1、管道(pipe)
用于父子进程和兄弟进程之间的通信。命名管道可以用于任意进程之间的通信。
- 管道只支持读与写操作
- 管道是半双工的,即同一时间,可以A写B读,B写A读
8.2、消息队列(message)
例如kafka等。
8.3、信号量(sempahore)
可作为同步机制以及简单的数据通信。
TIP:kill -9和control c的方式结束进程,实质上都是通过向该进程发生特定的信号。
8.4、共享数据区(share data)
开辟一块共享的数据区让多个进程可以相互访问这段区域的数据,实现进程的通信。这是进程间通信最快速的一种方式。
8.5、套接字(socket)
使用套接字来实现不同主机上的进程之间的通信,但是需要网络支持。
九、如何实现进程同步
9.1、临界区
访问临界资源的那段代码被称为临界区。可以在进程每次进入临界区之前检查互斥检查。
9.2、同步锁和互斥锁
通过为资源加锁,使得同一时刻只能有一个进程获取到资源,其他进行则同步等待。
9.3、信号量
通过信号量来限制获取到临界资源的进程数量。
9.4、管程
管程封装了互斥操作(信号量),同一时刻只可有一个进程使用管程,其他进程则在管程中阻塞。
TIP:进程同步的经典问题
- 哲学家就餐问题
- 读者-写者问题
十、死锁
10.1、什么是死锁
死锁指的是两个或多个进程在等待某个进程持有的资源,但该资源无法被释放掉,从而造成了无限时间的等待。死锁就是两个或多个进程无期限的等待。
10.2、死锁的必要条件
1. 互斥条件:同一个资源在同一时间只可被一个进程使用。
2. 请求与保持条件:一个进程获取到一个资源时,在使用过程中对该资源持续地占用。
3. 不可剥夺条件:一个进程在使用一个资源时,在其使用完之前不可强行剥夺。
4. 循环等待条件:多个进程之间形成形成一种头尾相连循环等待资源的关系。
以上是进程或者线程发生死锁的四个必备条件,当其中任意一个条件不符合时,都不会发生死锁。
10.3、死锁代码
public class DeadLockDemo {
private static Object a = new Object();
private static Object b = new Object();
public static void main(String[] args) {
// 死锁死条件:
// 1。互斥条件:一个资源同一时刻只可被一个线程使用
// 2。请求与保持条件:一个线程获取到资源后会保持使用
// 3。不可剥夺条件:一个线程使用资源使用完之前,不可被剥夺使用权
// 4。循环等待条件:多个线程之间形成了头尾相连的循环等待资源的情况
Thread t1 = new Thread() {
@Override
public void run() {
// 先获取a资源并锁住(1。互斥条件)
synchronized (a) {
// 3。不可剥夺条件
try {
// 2。请求与保持条件
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再请求获取b资源
// 4。循环等待条件
synchronized (b) {
System.out.println("Get");
}
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
// 先获取b资源并锁住
synchronized (b) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再请求获取a资源
synchronized (a) {
System.out.println("Get");
}
}
}
};
t1.start();
t2.start();
}
}
Golang死锁Demo:
package main
import (
"fmt"
"sync"
"time"
)
var (
a interface{}
b interface{}
mutex1 sync.Mutex
mutex2 sync.Mutex
)
func main() {
// 死锁发生的死条件:
// 1。互斥条件:一个资源同时刻只可被一个线程使用
// 2。请求与保持条件:一个线程程可以持续地使用资源
// 3。不可剥夺条件:在一个线程使用完资源之前不可被强行剥夺使用权
// 4。循环等待条件:多个线程之间形成了头尾相连的循环等待资源的情况
type value struct {
mu sync.Mutex
value int
}
var wg sync.WaitGroup
printSum := func(v1, v2 *value) {
defer wg.Done()
v1.mu.Lock() //1
defer v1.mu.Unlock() //2
time.Sleep(2 * time.Second) //3
v2.mu.Lock()
defer v2.mu.Unlock()
fmt.Printf("sum=%v\n", v1.value+v2.value)
}
var a, b value
wg.Add(2)
go printSum(&a, &b)
go printSum(&b, &a)
wg.Wait()
}
10.4、死锁的预防
死锁的预防即不要形成死锁的四条件:1.互斥条件 2.请求与保持条件 3.不可剥夺条件 4.循环等待条件
10.5、死锁的避免-银行家算法和资源分配图算法
死锁避免的基本思想是动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。资源分配图算法和银行家算法是两种经典的死锁避免的算法,其可以确保系统始终处于安全状态。其中,资源分配图算法应用场景为每种资源类型只有一个实例(申请边,分配边,需求边,不形成环才允许分配),而银行家算法应用于每种资源类型可以有多个实例的场景。
- 银行家算法
https://blog.csdn.net/cout_sev/article/details/24980627 - 资源分配图算法
https://blog.csdn.net/ai977313677/article/details/72780203
10.6、死锁的解除
死锁解除的常用两种方法为进程终止和资源抢占。所谓进程终止是指简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;所谓资源抢占是指从一个或多个死锁进程那里抢占一个或多个资源
10.7、进程饥饿
进程饥饿,即为Starvation,指当等待时间给进程推进和响应带来明显影响称为进程饥饿。当饥饿到一定程度的进程在等待到即使完成也无实际意义的时候称为饥饿死亡。
如果一个线程因为 CPU 时间全部被其他线程抢走而得不到 CPU 运行时间,这种状态被称之为“饥饿”。而该线程被“饥饿致死”正是因为它得不到 CPU 运行时间的机会。解决饥饿的方案被称之为“公平性” – 即所有线程均能公平地获得运行机会。
10.8、进程活锁
活锁是相对于死锁的概念,指的是进程在抢占资源的过程中不断改变自身的状态,但却无法获取到资源,以至于发生动态切换的锁定状态。
10.9、判断是否存在死锁-联合进程分析图
假设进程P获取锁和释放锁的顺序为:
获得A
获得B
释放A
释放B
进程Q获取和释放锁的顺序为:
获得B
获得A
释放B
释放A
那么画出联合进程分析图:
将两者共同需要A锁和B锁的区域描绘出来,发现Deadlock inevitable
区域,也就是死锁不可避免区域,此时会发生死锁。
下图中就不会发生死锁:
10.10、鸵鸟策略
把头埋在沙子里,假装根本没发生问题。
因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。
当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。
十一、进程派生-fork
for(i=0;i<2;i++){
pid_t fpid=fork();//执行完毕,i=0,fpid=0
if(fpid**0)
printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid);
else
printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);
}
return 0;
一个进程调用fork()函数后,系统先给新的进程复制父进程的资源,然后分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
fork后会产生一个fpid,fpid可以为负数、0、正数;
- 负数:说明创建子进程错误
- 0:说明创建子进程成功,此处返回的的子进程
- 正数:说明创建子进程成功,此处返回的是子进程的pid
打印进程树,左子节点为当前父节点,右子节点为fork出来的子进程
十一、系统调用和系统中断
11.1、系统内核调用
当一个进程在用户态需要使用到系统函数的功能时,将发生系统调用函数,将用户态转化为内核态,进入操作系统内部,由操作系统来完成这一段操作。
用户态和内核态实质就是指的CPU的运行等级变化,当CPU运行在Ring3级时,CPU处于低等级的用户态,不能执行特殊的函数以及数据区域,而CPU等级处于Ring0级时,处于高等级的内核态,此时可以执行所有的内核调用函数以及数据区。
系统内核调用过程:
1.为系统调用设置参数,系统调用号压入寄存器
2.产生80中断, 由用户态切换到内核态
3.保护现场将程序运行相关信息压栈,执行系统调用处理程序 system_call
4.根据系统调用表找系统调用号,将系统函数返回值压入寄存器
5.从内核态回到用户态,恢复现场,程序从寄存器中获得返回值
以下是Linux中常见的系统调用函数:
11.2、系统中断
系统中断是指CPU对系统发生的某个事件做出的一种反应,CPU暂停正在执行的程序,保留现场后自动地转去执行相应的处理程序,处理完该事件后再返回断点继续执行被“打断”的程序。如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等
A请求中断B,那么B会暂停正在执行的程序,然后保留现场,去执行A的处理程序,完成之后返回B的断点继续执行。
11.3、函数调用
函数调用指的是应用程序之间或者各个系统间的调用,不会经过内核态,而是处于用户态级别
1.异常中断
操作系统内部发生异常,导致了异常中断
2.系统陷入
在用户态中执行系统调用而陷入内核态
十二、进程创建的过程
- 为新进程分配一个唯一的进程标识符
- 为进程分配内存空间
- 初始化进程控制块(PCB)
- 添加其到就绪队列中等待调度
- 其他额外的附加操作