进程的定义
什么是进程?
其实进程就是运行着的程序,是一个指令序列。
进程实体:进程实体由三个部分组成,程序段、数据段、PCB。
- 程序段:保存的是程序代码本身。
- 数据段:保存的是程序运行的过程中运行所需要的数据(如:变量)。
- PCB:进程控制块,用来描述进程的各种信息。

我们把进程实体简称位进程。
所谓的创建进程,实质上是创建进程实体中的PCB
撤销进程,实质上是撤销进程实体中的PCB
PCB是进程存在的唯一标志。
严格来说,进程实体和进程是不一样的。进程实体是静态的,进程是动态的。
进程控制块PCB
在操作系统中,是用进程控制块(process control block,PCB)数据结构来描述进程的。
PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失
PCB包含的信息:
进程描述信息
- 进程标识符【PID】:标识各个进程,每个进程都有一个并且唯一的标识符;【可以理解位进程的ID】
- 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
进程控制和管理信息
- 进程当前状态:如 new、ready(就绪态)、running(运行态)、waiting 或 blocked(阻塞态) 等;
- 进程优先级:进程抢占 CPU 时的优先级;
资源分配清单【即该进程被分配了哪些系统资源】
- 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。
处理机相关信息
CPU 中各个寄存器的值。
当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
进程的组织
在一个系统中,通常由数十、数百乃至数千个PCB。为了对他们加以有效的管理,应当用适当的方式把这些PCB组织起来。
进程的组织方式:
- 链接方式
- 索引方式
链接方式
PCB通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。
比如:
- 将所有处于就绪状态的进程链在一起,称为就绪队列;【通常会把优先级高的进程放在队头】
- 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列;【很多操作系统会根据阻塞原因不同,再分为多个队列】
- 对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。

索引方式
它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。
它和链接方式很类似

进程的状态与转换
进程的状态由五种:
运行状态:占用CPU,并在CPU上运行。【多核CPU,可以有多个程序处于运行态,单核CPU,只能由一个程序处于运行态】
就绪状态:已经具备的运行条件,但由于没有空闲的CPU,而暂时不能运行【万事具备,只欠CPU】
阻塞状态:因等待某一事件而暂时不能运行。【比如:等待用户输入,等待读磁盘操作】
创建状态:程序正在被创建,操作系统为进程分配资源,初始化PCB
终止状态:进程正在从系统撤销,操作系统会回收进程拥有的资源、撤销PCB。
其中,运行状态、就绪状态和阻塞状态是三种基本状态。
状态转换
运行态 —–> 阻塞态是一种进程自身做出的主动行为
阻塞态 —–> 就绪态不是进程自身能控制的,是一种被动行为
注意:
- 不能由阻塞态直接转换为运行态
- 不能由就绪态直接转换为阻塞态
进程的控制
进程控制的主要功能是对系统中的所有进程实施有效的管理,他具有以下功能:
- 创建新进程
- 撤销已有进程
- 实现进程状态转换等
其实,进程控制就是实现进程的状态转换
创建进程
创建进程,进程会从创建态,转换为,就绪态【创建态 —-> 就绪态】

过程如下:
- 申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息,比如进程的唯一标识等;
- 为该进程分配运行时所必需的资源,比如内存资源;
- 将 PCB 插入到就绪队列,等待被调度运行;
阻塞进程
当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。

过程如下:
- 找到将要被阻塞进程标识号对应的 PCB
- 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
- 将该 PCB 插入到阻塞队列中去;
唤醒进程
进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。
【不可能自己叫醒自己】
如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。

- 在该事件的阻塞队列中找到相应进程的 PCB;【如果是等待的是资源分配,则还需要为进程分配资源】
- 将其从阻塞队列中移出,并置其状态为就绪状态;
- 把该 PCB 插入到就绪队列中,等待调度程序调度;
进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句
终止进程

进程可以有 3 种终止方式:
- 正常结束
- 异常结束【除0,数组越界····】
- 外界干预(信号
kill
掉)。
当子进程被终止时,其在父进程处继承的资源应当还给父进程。而当父进程被终止时,该父进程的子进程就变为孤儿进程,会被 1 号进程收养,并由 1 号进程对它们完成状态收集工作。
终止进程的过程如下:
- 查找需要终止的进程的 PCB;
- 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
- 如果其还有子进程,则应将该进程的子进程交给 1 号进程接管;
- 将该进程所拥有的全部资源都归还给操作系统;
- 将其从 PCB 所在队列中删除;
原语
进程控制的实现是使用原语实现的
原语的特点是:执行期间不允许中断,只能一气呵成。【这种不可被中断的操作即原子操作】
原语是一种特殊的程序,采用 “关中断指令” 和 “开中断指令“实现。

关/开中断指令的权限非常大,必然只允许在核心态下执行。
进程通信
进程是分配系统资源的单位,因此各进程拥有的内存地址空间相互独立。

进程1可以访问进程1的地址空间,进程2可以访问进程2的地址空间,但进程1无法访问进程2的地址空间。如果可以,这是非常危险,进程1就可以篡改进程2的数据,是非常不安全的。
为了保证安全,一个进程是不能直接访问另一个进程的地址空间的。
但是进程之间的信息交换又是必须实现的。为了保证进程间的安全通信,操作系统提供了一些方法。
比如:你可以将浏览器的照片通过QQ分享给你的朋友。
进程通信可以使用以下三种方式实现
- 共享存储
- 消息传递
- 管道通信
管道通信
所谓的管道,是指用于连接读写进程的一个共享文件,又名pipe文件。其实就是在内存中开辟一个大小固定的缓冲区。
管道只能采用半双工通信【单向传输】。如果要实现全双工通信,则需要设置两个管道。
各进程要互斥地访问管道【进程1向管道写数据的时候,进程2是不可以读数据的】
通信过程:
- 数据以字符流的形式写入管道,当管道写满时,写进程的
write()
系统调用将被阻塞,等待读进程将数据取走。 - 当读进程将数据全部取走后,管道变空,此时读进程的
read()
系统调用将被阻塞。
注意:
如果没有写满,是不允许读的。如果没有读空,是不允许写的。
数据一旦被读出,就从管道中被抛弃,这就意味着读进程最多只能由一个,否则可能会有读错数据的情况。
管道这种通信方式效率低,不适合进程间频繁地交换数据
消息队列
进程间的数据交换以格式化的消息为单位。进程通过操作系统提供的 ”发送消息/接收消息“ 两个原语进行数据交换。
一个格式化的消息分为:消息头 和 消息体 两部分
每个进程会有一个消息缓冲队列,消息队列是保存在内核中的消息链表
通信过程:
如果由另外一个进程向此进程发送消息,这个进程会首先创建好一个格式化的消息体。
消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。
这个消息体会通过发送原语发送给目标进程。
这个消息就会挂到目标进程的消息缓存队列的队尾。
目标进程会通过接收原语依次把这些队列的消息一个一个取走,处理。
如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列和管道的比较
- 生命周期
- 消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在
- 管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。
- 数据格式
- 每个消息体都是固定大小的存储块
- 管道是无格式的字节流数据
消息队列的不足
消息队列这种模型,两个进程之间的通信就像平时发邮件一样,你来一封,我回一封,可以频繁沟通了。
但邮件的通信方式存在不足的地方有两点,一是通信不及时,二是附件也有大小限制,这同样也是消息队列通信不足的点。
消息队列不适合比较大数据的传输
因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销
进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程
另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
共享内存
消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。
两个进程不能直接访问对方的进程的地址空间。所以操作系统会为两个进程开辟一块共享空间。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。
这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
需要注意的是:两个进程对共享空间的访问必须的**互斥**的
当进程A向内存写数据时,进程B是不允许访问共享空间的。
并发与并行
单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发。
并发是指能处理多个同时事件,并行是指同时处理两个事件。
单核CPU只能并发处理进程,多核CPU才能并行处理多个进程。

线程
为什么要引入线程
还没有引入线程之前,系统中各个程序只能串行执行。
如果我们打开一个QQ程序,
加入没有引入线程:我们要么进行视频聊天,要么进行文字聊天,而且只能和一个人。
在引入线程之后:我们课进行视频聊天的同时和其他人进行文字聊天。
有的进程可能需要”同时“处理很多事,而传统的进程只能串行地执行一系列程序代码。为此,引入线程来增加并发度。
传统的进程是执行流的最小单位 ,CPU会轮流执行这些进程。

而引入线程之后,CPU调度的最小单位不再是进程,而是线程。CPU会用一些算法,轮流执行这些线程。

引入线程后,线程成为了程序执行流的最小单位
我们可以把线程理解为“轻量级进程”
引入线程之后,不仅进程之间可以并发执行,进程间的各个线程之间也可以并发的执行。从而进一步提高了并发度。
进程只作为除CPU之后的系统资源分配单元。(如打印机、内存地址等都是分配给进程的)
线程是进程当中的一条执行流程。
同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
线程和进程的比较
比较如下:
- 进程是资源分配(包括内存、打开的文件等)的单位,线程是 CPU 调度的单位;
- 【资源方面】进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;【线程几乎不拥有系统资源】
- 【状态方面】线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;【状态方面,和进程状态差不多类似】
- 【开销方面】线程能减少并发执行的时间和空间开销;
为什么会减少并发执行的时间和空间开销呢?
线程的创建时间比进程快
- 进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,
- 线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
线程的终止时间比进程快
- 线程释放的资源相比进程少很多
同一个进程内的线程切换比进程切换快
- 对于同一个进程的线程之间的切换,线程具有相同的地址空间(虚拟内存共享),这意味着在切换的时候不需要切换页表。
- 而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
同一进程的各线程之间的数据交互效率更高
同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了。
【由于共享内存地址空间,同一进程中的线程通信甚至无需系统干预】
同一进程内的线程切换,不需要切换进程环境,系统开销小。
但是不同进程的线程切换,则需要切换进程,系统开销大。
线程的实现方式
线程的实现方式基本有两种:
- 用户级线程:在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
- 内核级线程:在内核中实现的线程,是由内核管理的线程;
如何理解用户级线程呢?
可以回想一下,进程是如何实现的呢?也就是说进程是如何控制的呢?
进程的控制是由原语实现的,而原语是一段特殊的程序,其中包含 开/关中断 。而开关中断是特权指令,必须在核心态下执行。
所以,进程的控制是由操作系统来实现的。
而用户级线程是用函数库来实现线程。所有的线程管理工作都由函数库来管理。
为什么要有用户级线程呢?
很久很久之前,线程的概念是出现了,但操作系统厂商可不能直接就去修改操作系统的内核,因为对他们来说,稳定性是最重要的。贸然把未经验证的东西加入内核,出问题了怎么办?所以想要验证线程的可用性,得另想办法。
所以,当初 用户级线程的出现是为了验证线程的稳定性等功能。
那些研究人员就编写了一个关于线程的函数库,用函数库来实现线程!
这个函数库实现了线程,还能进行管理。
比如创建线程、终止线程等功能放在了这个线程库内,用户就可以通过调用这些函数来实现所需要的功能。
但是!
刚刚我们说的线程库,是位于用户空间的,操作系统内核对这个库一无所知,所以从内核的角度看,它还是按正常的方式管理。也就是说操作系统眼里还是只有进程,用线程库写的一个多线程进程,只能一次在一个 CPU 核心上运行。
由于操作系统只能看到进程的存在,那如果某一个线程阻塞了。在操作系统眼里,是进程阻塞了,那么整个进程就会进入阻塞态,在阻塞操作结束前,这个进程都无法得到 CPU 资源。那就相当于,所有的线程都被阻塞了。
对于内核级线程,在用户看来,是有多个线程。但是在操作系统看来,并意识不到线程的存在。

内核级线程
内核级线程的管理工作由操作系统内核完成。线程调度、切换等工作都由内核负责,因此内核级的切换必然需要在核心态下才能完成。
内核级线程的实现,使得操作系统内核知道线程的存在,就可以像调度多个进程一样,把这些线程放在好几个 CPU 核心上,就能做到实际上的并行了。
假如线程 A 阻塞了,与他同属一个进程的线程也不会被阻塞。这是内核级线程的绝对优势。

用户线程和内核线程的对应关系
有的操作系统中只支持用户级线程,有的操作系统只支持内核级线程。有的操作系统两种都支持。
在同时支持用户级线程和内核级线程的系统中,可采用二者组合的方式:将n个用户级线程映射到m个内核级线程上。(n>=m)
需要注意的是:
操作系统只看得见内核级线程,所以内核级线程才是处理机分配的单位。
由几个用户级线程映射到几个内核级线程的问题引出了“多线程模型”问题。
多对一模型
多对一模型:多个用户级线程映射到一个内核级线程。每个用户进程只对应一个内核级线程。

优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。
缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行
一对一模型
一对一模型:一个用户及线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。

优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
多对多模型
多对多模型:n个用户及线程映射到m个内核级线程(n >= m) 。每个用户进程对应m个内核级线程。

克服了多对一模型并发度不高的缺点,又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。
总结用户线程和内核线程
用户线程:
即使多核CPU,多线程也不能并行执行。
- 优点:
- 每个进程都需要有它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
- 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快;
- 缺点:
- 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了。【一个阻塞,全部阻塞】
- 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的【无法中断线程运行】
- 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;
- 优点:
内核线程:
在多核CPU中,多线程可以并行执行。
- 优点:
- 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;【一个阻塞,其他不影响】
- 分配给线程,多线程的进程获得更多的 CPU 运行时间;
- 缺点:
- 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如 PCB 和 TCB;
- 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大;【内核态和用户态需要进行转换。】
- 优点:
参考:
https://xiaolincoding.com/os/4_process/process_base.html
王道操作系统
用户级线程和内核级线程,你分得清吗? - 知乎 (zhihu.com)
__END__