进程与线程
进程的定义、组成、组织方式、特征
定义
程序:是静态的,就是个存放在磁盘里的可执行文件,就是一系列的指令集合。
进程(Process):是动态的,是程序的一次执行过程,同一个程序多次执行会对应多个进程。
组成
PCB
PCB是进程存在的唯一标志,当进程被创建时,操作系统为其创建PCB,当进程结束时,会回收其PCB。操作系统对进程进行管理工作所需的信息都存在PCB中。
程序段、数据段
PCB 是给操作系统用的。程序段、数据段是给进程自己用的。
一个进程实体(进程映像)由PCB、程序段、数据段组成。 进程是动态的,进程实体(进程映像)是静态的。 进程实体反应了进程在某一时刻的状态(如:x++后,x=2)。
程序时如何运行的
程序段、数据段、PCB三部分组成了进程实体(进程映像)。 引入进程实体的概念后,可把进程定义为: 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
注意:PCB是进程存在的唯一标志!
组织方式
在一个系统中,通常有数十、数百乃至数千个PCB。为了能对他们加以有效的管理,应该用适当的方式把这些PCB组织起来。
注:进程的组成讨论的是一个进程内部由哪些部分构成的问题,而进程的组织讨论的是多个进程之间的组织方式问题。
-
链接方式
按照进程状态将PCB分为多个队列,操作系统持有指向各个队列的指针
-
索引方式
根据进程状态的不同,建立几张索引表,操作系统持有指向各个索引表的指针
特征
程序是静态的,进程是动态的,相比于程序,进程拥有以下特征:
小结
进程的状态与转换
状态
进程是程序的一次执行。在这个执行过程中,有的进程正在被cpu处理,有时又需要等待cpu服务,可见,进程的状态是会有变化的。为了方便对各个进程的管理,操作系统需要将进程合理地划分为几种状态。
进程状态间的转换
小结
进程控制
基本概念
什么是进程控制
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。
简化理解:反正进程控制就是要实现进程状态转换
如何实现进程控制
用原语实现
之前提到过的进程组织问题
原语的执行具有原子性,即执行过程只能一气呵成,期间不允许被中断,这种不可被中断的操作即原子操作。 可以用 “关中断指令”和“开中断指令”这两个特权指令实现原子性。
正常情况:CPU每执行完一条指令都会例行检查是否有中断信号需要处理,如果有, 则暂停运行当前这段程序,转而执行相应的中断处理程序。
进程控制相关的原语
学习技巧:进程控制会导致进程状态的转换。无论哪个原语,要做的无非三类事情:
- 更新pcb中的信息
- 修改进程状态标志
- 将运行环境保存到pcb
- 从pcb恢复运行环境
- 将pcb插入合适的队列
- 分配/回收资源
创建
终止
阻塞、唤醒
切换
小结
进程通信
顾名思义,进程通信就是指进程之间的信息交换。进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。为了保证安全,一个进程不能直接访问另 一个进程的地址空间。 但是进程之间的信息交换又是必须实现的。 为了保证进程间的安全通信,操作系统提供了一些方法。
共享存储
基于数据结构的共享
比如共享空间里只能放一个长度为10的数组。这种共享方式速度慢、 限制多,是一种低级通信方式。
基于存储区的共享
基于存储区的共享:在内存中画出一块共享存储区,数据的形式、存放位置都由进程控制, 而不是操作系统。相比之下,这种共享方式速度更快,是一种高级通信方式。
管道通信
“管道”是指用于连接读写进 、程的一个共享文件,又名pipe 文件。其实就是在内存中开辟一个大小固定的缓冲区。
-
管道只能采用半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道。
-
各进程要互斥地访问管道。
-
数据以字符流的形式写入管道,当管道写满时,写进程的write()系统调用将被阻塞,等待读进程将数据取走。当读进程将数据全部取走后,管道变空,此时读进程的read()系统调用将被阻塞。
-
如果没写满,就不允许读。如果没读空,就不允许写。
-
数据一旦被读出,就从管道中被抛弃,这就意味着读进程最多只能有一个,否则可能会有读错数据的情 况。
消息传递
进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。
直接通信方式
间接通信方式
小结
线程的概念和多线程模型
什么是线程,为什么要引入线程?
有的进程可能需要“同时”做很多事,而传统的进程只能串行地执行一系列程序。为此,引入了“线程”,来增加并发度。
可以把线程理解为“轻量级进程”。
线程是一个基本的CPU执行单元, 也是程序执行流的最小单位。 引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务(如QQ 视频、文字聊天、传文件)
引入线程后,进程只作为除CPU之外的系统资源的分配单元(如打 印机、内存地址空间等都是分配给进程的)。 线程则作为处理机调度的分配单元。
线程 = 进程 - 资源
引入线程机制后,有什么变化
类比: 切换进程运行环境:有一个不认识的人要用桌子,你需要你的书收走,他把自己的书放到桌上
同一进程内的线程切换=你的舍友要用这张书桌,可以不把桌子上的书收走。
在同一个进程中,线程的切换不会引起进程的切换,只有当一个进程中的线程切换到另一个进程中的线程时才会引起进程的切换。
线程有哪些重要的属性
TCB
线程有自己的TCB(thread control block, 和PCB很像), 只负责这条流程的信息。包括PC程序计数器的值,SP(栈指针),State状态,和寄存器的值。有不同的控制流,需要不同的寄存器来表示控制流的执行状态,每个线程有独立的这些信息,但共享一个资源。
多线程,在进程空间内有多个控制流且执行流程不一样,有各自独立的寄存器和堆栈,但共享代码段,数据段,资源。
线程的栈(基于linuxthreads-2.0.1)
线程本质上是进程中的一个执行流,我们知道,进程有代码段,线程其实就是进程代码段中的其中一段代码。线程的一种实现是作为进程来实现的。通过调用clone,新建一个进程,然后执行父进程代码段里的一个代码片段。文件、内存等信息都是共享的。因为内存是共享的,所以线程不能共享栈,否则访问栈的地址的时候,会映射到相同的物理地址,那样就会互相影响,所以每个线程会有自己独立的栈。在调用clone函数的时候会设置栈的范围。下面通过linuxthreads的代码看看线程的栈。linuxthreads里有一个__pthread_initialize函数,该函数会在main函数执行前执行。在该函数中会设置主线程的栈范围。
|
|
__pthread_initial_thread_bos 保存主线程的栈顶位置。
然后当我们第一次调用pthread_create创建线程的时候,会调用pthread_initialize_manager函数初始化manager线程。manager线程是管理其他的线程的线程。
|
|
然后调用clone函数设置manager线程的栈。
|
|
最后在函数pthread_handle_create中设置创建的线程的栈,pthread_handle_create函数是调用pthread_create函数的时候被调用的函数。
|
|
从上面代码可知,新建的线程的栈在主线程的栈顶下面(即地址小于主线程的栈顶),创建线程的时候,首先计算新线程的栈地址,然后调用mmap划出这块地址。否则访问的时候会segmentfault。最后调用clone创建进程(线程)并设置栈的栈顶和栈底位置。
|
|
内存布局如下。
从上面的栈分布我们还可以知道一个信息,即当前执行的代码属于哪个线程。
|
|
这就是linux threads获取当前线程的是方法。
线程的实现方式
主流的操作系统都提供了线程实现,java语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理。每个已经执行start()且还未结束的java.lang.Thread类的实例就代表了一个线程。java中Thread类与大部分的JavaAPI有显著的差别,它的所有关键方法都是声明为一个Native的。在java的API中Native方法往往意味着这个方法没有使用或者无法使用平台无关的手段来实现。
线程实现主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
用户级线程
用户级线程(User-Level Thread, ULT)
用户级线程由应用程序通过线程库实现。 所有的线程管理工作都由应用程序负责(包括线程切换)
用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预。 在用户看来,是有多个线程。但是在操作系统内核看来,并意识不到线程的存在。(用 户级线程对用户透明,对操作系统不透明)
可以这样理解,“用户级线程”就是“从用户视角看能看到的线程”
内核级线程
内核级线程(Kernel-Level Thread, KLT, 又称“内核支持的线程”)
内核级线程的管理工作由操作系统内核完成。 线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成。
可以这样理解,“内核级线程”就是“从操作系统内核视角看能看到的线程”
在同时支持用户级线程和内核级线程的系统中,可采用二者组合的方式:将n个用户级线程映射到m个内核级线程上( n >= m)
重点:操作系统只“看得见”内核级线程,因此只有内核级线程才是处理机分配的单位。
例如:上边这个模型中,三个用户级线程映射成两个内核级线程,在用户看来,这个进程中有三个线程。但即使该进程在一个4核处理机的计算机上运行,也最多只能被分配到两个核,最多只能有两个用户线程并行执行。
多线程模型
在同时支持用户级线程和内核级线程的系统中,由几个用户级线程映射到几个内核级线程的问题引出了“多线程模型”问题。
多对一模型
多对一模型:多个用户级线程映射到一个内核级线程。每个用户进程只对应一个内核级线程。
优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行
一对一模型
一对一模型:一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。
优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
缺点:一个用户进程会占用多个内核级线程, 线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
多对多模型
多对多模型:n个用户级线程映射到 m 个内核级线程(n >= m)。每个用户进程对应 m 个内核级线程。
克服了多对一模型并发度不高的缺点,又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。
Java线程的实现
JDK1.2之前,绿色线程——用户线程。JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。
Solaris同时支持一对一和多对多。
Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度和抢占式线程调度。
协同式线程调度,线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。
抢占式调度,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
Java线程调度就是抢占式调度。
希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。