"我们写好的一行行代码,为了让其工作起来,我们还得把它送进城(进程)里,那既然进了城里,那肯定不能胡作非为了。

城里人有城里人的规矩,城中有个专门管辖你们的城管(操作系统),人家让你休息就休息,让你工作就工作,毕竟摊位不多,每个人都要占这个摊位来工作,城里要工作的人多着去了。

所以城管为了公平起见,它使用一种策略(调度)方式,给每个人一个固定的工作时间(时间片),时间到了就会通知你去休息而换另外一个人上场工作。

另外,在休息时候你也不能偷懒,要记住工作到哪了,不然下次到你工作了,你忘记工作到哪了,那还怎么继续?

有的人,可能还进入了县城(线程)工作,这里相对轻松一些,在休息的时候,要记住的东西相对较少,而且还能共享城里的资源。"

5.1 进程、线程基础知识|小林coding (xiaolincoding.com)

OS中进程是作为资源分配与独立运行的基本单位出现的。

OS的四大特征(==并发、共享、虚拟、异步==)也都是基于进程而形成的。

所以,在OS中,进程是一个极其重要的概念。

到80年代中期,又出现了比进程更小的基本单位——线程。用它来提高程序并发执行程度,以进一步改善系统的服务质量。现代OS无一例外的引入了线程,所以,线程也是一个非常重要的概念。

2.1.1 进程描述

进程控制块PCB

PCB听起来有点熟悉,实际上还有另一个PCB

PCB(Printed Circuit Board),中文名称为印制电路板,又称印刷线路板,是重要的电子部件,是电子元器件的支撑体,是电子元器件电气相互连接的载体。由于它是采用电子印刷术制作的,故被称为“印刷”电路板

为了方便控制和管理参与并发执行的每个程序的独立运行,在操作系统中为之配置一种专门的数据结构,称为进程控制块,Process Control Block,==PCB==。

系统利用PCB来描述进程的基本信息与活动过程,进而控制和管理进程。

此时就产生了一个新的概念——**进程实体,**又称进程映像。

程序段、相关数据段PCB三部分构成了进程实体。

以下是PCB结构体的部分描述属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct task_struct {
long state; //进程运行状态:-1不可运行,0可运行,>0已停止
long priority; //进程运行优先级
long counter; //已运行时间计数器
long exit_code; //进程停止执行后退出码,其父进程需要读取
unsigned long start_code; //代码段起始地址
unsigned long end_code; //代码段长度,单位字节
unsigned long next_code; //下一条要执行代码的地址
unsigned long start_data; //数据段起始地址
unsigned long end_data; //代码段长度+数据段长度,单位字节
unsigned long next_data; //下一条要读取数据的地址
long pid; //进程标识
long father; //父进程标识
//...
};

以下是PCB结构体的部分描述属性:以上仅仅是PCB中的部分属性,PCB数据结构中共包含四大类信息:进程状态信息、处理器状态信息、进程调度信息,进程控制信息。

PCB在进程切换中的作用

系统就是根据PCB来感知进程的存在的。

我们系统运行的过程,其实就是进程不断的进行调度,切换的过程

PCB在进程切换过程中起着非常重要的作用,操作系统可以通过对PCB的修改来达到对进程的控制

PCB在在进程切换中的作用

进程进入空闲(可能是发生阻塞,比如IO),然后会把当前进程的状态保存到PCB 中,

由此来理解系统就是根据PCB来感知进程的存在的

进程定义

对于进程的定义,从不同角度可以有不同的定义,其中较典型的定义有三个:

  • 进程是程序的一次执行
  • 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
  • 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行盗源分配和调度的一个独立单位

在引入了==进程实体==慨念后,可以将进程定义为:

进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。

引发的问题

引入进程后使得作业的==并发==执行得以实现,但同时也带来了一些问题。

  • 增加了空间开销

    需要为进程创建PCB

  • 增加了时间开销

    管理、协调跟踪进程的执行需要时间
    创建、更新、回收PCB 需贾时间
    进程切换保护恢复CPU现场需要删间

  • 增加了控制难度

    协调多个进程对资源的亮争与共享增加了控制难度
    对何能由进程发的异常进行处理增加了控制难度

2.1.2 进程状态

一般说来,一个进程并不是自始至终连续不停地运行的,它与并发执行中的其他进程的执行是相互制约的。

它有时处于运行状态,有时又由于某种原因而暂停运行处于等待状态,当使它暂停的原因消失后,它又进入准备运行状态。

所以,在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。

上图中各个状态的意义:

  • 就绪状态(Ready):进程已经获取到了除处理器之外执行所需的所有资源的状态,可运行,由于其他进程处于运行状态而暂时停止运行;
  • 执行状态(Running):进程已经获取到了包括处理器在内的执行所需的所有资源的状态,该时刻进程占用 CPU;
  • 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行;
  • 创建状态(new):进程正在被创建时的状态;=> 进程缺少完整PCB与运行所需资源的状态
  • 终止状态(Exit):进程正在从系统中消失时的状态;

就绪状态,执行状态,阻塞状态,称为进程的基本状态’

比如下面的代码

1
2
3
4
5
6
7
8
9
pulbic void demo(){
Scanner sc=new Scanner(System.in);
Person A; //创建状态
A=new Person("Dan"); //就绪状态
A.age=sc.nextInt();//阻塞状态
A.eat(); //执行状态
System.out.println(A.age); //执行状态
A=null; //终止状态
}

PCB的组织方式

粗略理解的话,无论是就绪队列,还是各种阻塞队列,其队列元素是PCB

不同应用可以产生很多异类进程,一个应用可以产生很多同类进程。无论是何类进程,其会按照它们的
状态将它们组织到一起。常见的组织方式有两种:

  • 链表方式:同一状态的进程的PCB组成一个链表,所以多种状态的进程PCB组成多个链表。此时就会产生诸如就绪链表、阻塞链表等不同链表。
  • 索引表方式:所有PCB都放入同一链表,再为每种状态创建出不同的PCB索引链表,其中存放的就是该状态的所有PCB的索引。

进程的状态转换

进程的五种状态会因为资源的变化而引发状态间的转换。

  • NULL -> 创建状态:一个新进程被创建时的第一个状态;
  • 创建状态-> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
  • 就绪状态-> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
  • 运行状态-> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
  • 运行状态-> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
  • 运行状态-> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件
  • 阻塞状态-> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;

OpenFeign中的状态转换

线程的状态与进程的状态基本是一致的,控制理论也基本一样。在代码中,很多时候我们可以直接使用线程相关的方法来控制和转换线程的状态。比如在Java的Thread类中有很多方法可以控制线程的状态转换。

例如,Spring Cloud的声明式客户端OpenFeign默认使用的Ribbon做负载均衡,其不少负载均衡算法中就是通过调用Thread的yield()方法显式的将线程状态由执行态转换为了就绪态。

挂起与激活

挂起操作通过挂起原语suspend可以使进程挂起,即由活动状态变为静止状态

而激活操作通过激活原语Active可以将挂起的进程激活,即由静止状态变为活动状态。

简单的理解,挂起之后进程不会自动继续执行

进程的整个状态转变发生了一些变化。

状态转换的特点

  • 创建好的进程直接到达的是就绪态
  • 就绪态进程无法直接转换为阻塞态
  • 执行态进程被挂起后进入静止状态
  • 静止就绪态不能被直接调度
  • 静止阻塞态进程在等待的事件发生后会到静止就绪态,而不会直接到活动就绪态
  • 阻塞态只能是进程执行过程中由于等待某事件的发生而引发的
  • 进程只能是在执行时终止
  • 执行->静止就绪:对于正在执行的进程,挂起原语Suspend会使其暂停执行,变为静止就绪状态
  • 活动就绪->静止就绪:对于就绪状态的进程,挂起原语Suspend将使其暂不接受调度,变为静止就绪状态
  • 活动阻塞->静止阻塞:对于阻塞状态的进程,挂起原语Suspend会使其变为静止阻塞状态。
  • 静止就绪->活动就绪:使用激活原语Active将使静止就绪的进程变为活动就绪
  • 静止阻塞->活动阻塞:使用激活原语Active将使静止阻塞的进程变为活动阻塞
  • 静止阻塞->静止就绪:对于静止阻塞的进程,当其所等待的事件发生后,其状态变为静止就绪

如果有大量处于阻塞状态的进程,进程可能会占用着物理内存空间,显然不是我们所希望的,毕竟物理内存空间是有限的,被阻塞状态的进程占用着物理内存就一种浪费物理内存的行为。

所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。

导致进程挂起的原因不只是因为进程所使用的内存空间不在物理内存,还包括如下情况:

  • 通过 sleep 让进程间歇性挂起,其工作原理是设置一个定时器,到期后唤醒进程。
  • 用户希望挂起一个程序的执行,比如在 Linux 中用Ctrl+Z挂起进程;

2.1.3 进程控制

我们熟知了进程的状态变迁和进程的数据结构 PCB 后,再来看看进程的创建、终止、阻塞、唤醒的过程,这些过程也就是进程的控制。

进程的创建

操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源。

父子进程

在OS中,进程一般由OS内核创建,但也是允许使用一个用户进程去创建另一个进程的此时的创建进程称为父进程,被创建进程称为子进程。当然子进程还可以再创建出更多的孙子进程。这种创建与被创建关系,很自然可以形成树型层次结构例如,Unix、Linux系统。

对于具有层次结构的父子进程,在PCB中设置了专门的家族关系表项,以标明自己的父进程及所有子进程。这种父子进程间的关系十分紧密:

  • 子进程可以继承父进程的所有资源,且父进程不能拒柜绝子进程的继承权。当子进程终止时,会将其从父进程获取到的资源全部归还给父进程。
  • 父进程可以管理子进程的生命周期。例如,父进程可以终止子进程,父进程可以挂起和激活子进程,父进程在终止时会将其所有子进程全部终止。

但并不是所有OS中的这种创建与被创建关系的父子进程间就一定会形成树型层次结构

例如Windows系统。

在Windows系统中父子进程间是不存在层级结构的,所有进程间具有同等的地位。但父进程在创建子进程时会获得了子进程==句柄==,可以实现对子进程的控制。不过,这种控制句柄是可以传递的没有父子关系的进程间也可以通过控制句柄实现控制关系

Nginx、Redis、Dubbo中的父子进程

  • 在开发中,父进程fork出子进程的应用十分广泛

在Linux系统中父进程创建出子进程的过程是通过系统调用fork()完成的,所以一般称父进程创建子进程为父进程fork出子进程。

例如,Nginx中的worker进程就是由master进程fork出的,然后父子进程各司其职。当然,master进程可以 fork出多少worker进程,可以在配置文件中指定。

再如,对于Redis默认的RDB持久化,在进行bgsave持久化时,redis-server进程会 fork出一个子进程,由该子进程以异步方式负责完成持久化。而在持久化过程中,redis-server进程不会阻塞,其会继续接收和处理用户请求。

再比如,在Dubbo的forking集群容错策略中,消费者对于同一服务会fork出多个子进程(线程)去并行调用多个提供者服务器,只要有一个成功即调用结束并返回结果。通常用于实时性要求较高的读操作,但其会浪费较多的服务器资源。

进程创建事件

一个进程可以由系统内核创建,也可以由另一个进程创建。发生进程创建的典型事件有三类:

  • 用户登录:在分时系统中,用户登录后,系统内核会为该用户创建一个进程。
  • 作业调度:在多道批处理系统中,当作业调度程序调度到某作业后,会将其装入内存,并由系统内核为该作业创建一个进程。
  • 请求处理:当某进程在运行过程中提出某种请求后,系统内核或主进程将会创建一个相应的进程来处理该请求,以使主进程与子进程可以并发执行。例如,用户进程提交了一个文件打印请求后,系统内核会创建一个打印进程来处理该请求。

进程的创建过程

  • 申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息,比如进程的唯一标识等;
  • 为该进程分配运行时所必需的资源,比如内存资源;
  • 将 PCB 插入到就绪队列,等待被调度运行;

进程的终止

进程可以有3 种终止方式:正常结束、异常结束以及外界干预(信号kill掉)。

  • 正常结束:进程的任务正常执行完毕后的进程终止。当进程的任务执行完毕后,进程会发出一条终止指令,此时会产生一个中断,以通知OS进程马上终止。

  • 异常结束:进程在运行过程中发生了某种异常事件,导致进程无法继续运行。

    常见的异常事件有:

    1. 访问越界
    2. 非法指令
    3. 权限异常
    4. 运行超时
    5. 等待超时
    6. 运算异常
    7. IO异常

    以上可以引起进程的终止,但并非100%引起

  • 外界干预: 进程被外界干预终止。外界的干预主要指三个方面

    1. 用户直接终止
    2. OS终止
    3. 父进程终止

进程终止过程

当子进程被终止时,其在父进程处继承的资源应当还给父进程。而当父进程被终止时,该父进程的子进程就变为孤儿进程,会被1 号进程收养,并由1 号进程对它们完成状态收集工作。

终止进程的过程如下:

  • 根据被终止进程的PID,找出其PCB

  • 从PCB中读取其状态,若该进程处于执行态,则立即终止其执行,并置调度标志为真,用于标志该进程可被重新调度。=> 此时的进程为就绪态(获取了除了CPU之外的所有资源)

  • 从PCB中读取其子进程PID,若该进程还有子进程,则要将其所有子进程全部终止。

  • 从PCB中读取其所拥有的全部资源,并全部释放,归还给其父进程或系统。

    这个资源释放的过程主要包含两步

    1. 将其PCB赋值为null
    2. 在其父进程或系统的资源数量上增加相应释放的数量
  • 将该进程的PCB从PCB队列或链表中移出。

  • 释放该PCB空间(将其从 PCB 所在队列中删除);

进程的阻塞

引发进程阻塞的事件

进程发生阻塞是由于正在执行的进程突然发生执行条件缺失事件,进而暂停执行等待条件的满足;而阻塞进程被唤醒则是由于发生了某些事件,从而具备了执行条件,到达了执行时机。典型的引发进程阻塞的事件有以下四类:

等待资源

进程在运行过程中需要某种资源,但由于系统中的该类资源已被其它进程所使用,暂无足够的资源分配给它,那么该进程只能等待,即由运行态转为阻塞态。当其它进程释放了足够的资源时,系统会唤醒由于缺乏该资源而阻塞的进程。此时进程由阻塞态转为就绪态。

无论是临界资源还是共享资源,只要其需要的数量不够,就会阻塞。例如,打印机(临界资源)、缓存(共享资源)

等待IO完成

当进程启动某IO操作后,如果该进程必须在该IO操作完成后才能继续执行,则会先将该进程阻塞,以等待IO操作的完成。IO操作完成后,再由中断处理程序将该进程唤醒。

  • 比如redis 的RDB持久化的save方式持久化过程

再比如下面c语言代码的执行,只有在输入结束之后,才会完成主线程中的输出

1
2
3
4
5
6
7
8
#include<stdio.h>
int main(){
int a;
printf("please input a num:");
scanf("%d",&a);
printf("a = %d", a);
return0;
}
等待数据到达

对于相互合作的进程,如果一个进程专门用于处理由其它进程提供的数据的,那么在其它进程提供的数据未到达之前,该进程就处于阻塞态。当其所需要的数据全部到齐后,该进程就会被唤醒。

分布式 Barrirer 队列

等待任务到达

在某些系统中,特别是在分布式系统环境中,往往设置一些特定功能的进程,每当完成特定任务后就会把自己阻塞起来,等待新任务的到来。而当新任务到来后,会立即唤醒这些进程。

例如,MQ中的消息发送进程消息接收进程

进程阻塞过程

阻塞是进程自身的一种==主动行为==。当系统中发生进程阻塞事件后,进程自己会调用进程阻塞原语将自己阻塞。

进程阻塞原语的阻塞过程如下:

  1. 立即暂停CPU的执行,保留当前CPU的现场。
  2. 将PCB中的状态由执行态修改为阻塞态。
  3. 将PCB写入到相应阻塞原因的阻塞队列。
  4. 启动调度程序进行重新调度,即将CPU分配给另一就绪进程。
  5. 按照新进程的PCB设置CPU新的环境。

进程的唤醒

进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。

唤醒是进程的被动行为

阻塞是进程的主动行为

如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由==发现者进程==(促使执行条件满足的进程)用唤醒语句叫醒它。

唤醒进程的过程如下:

  1. 在该事件的阻塞队列中找到相应进程的 PCB;
  2. 将其从阻塞队列中移出,并置其状态为就绪状态;
  3. 把该 PCB 插入到就绪队列中,等待调度程序调度;

进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。