本文首发于稀土掘金。该平台的作者 逐光而行 也是本人。
注:本文为个人操作系统的学习笔记,如有错误,还请各位大神指出,谢谢!
进程的含义?如何理解进程?
- 进程是操作系统中最核心的概念,是对正在运行中的程序的一种抽象,本质上指的是正在运行中的程序,它拥有独立的地址空间,上面几乎存放着所有与运行这个程序有关的信息,如可执行程序,程序的数据以及程序的堆栈。
- 进程表:与一个进程相关的所有信息,除该进程自身地址空间的内容外均放在了操作系统的一张表——进程表中。其物理结构为数组或链表,每个进程都要单独占据一项。
- 别的角度:用某种方法把相关资源集中在一起。(因为进程有相应的独立地址空间)
关于进程模型
基于两大独立概念
资源分组处理和执行
多道程序设计:
真正的cpu在不同进程间高速来回切换,看上去就像每个进程都拥有自己虚拟的cpu。
- 注意:在对进程编程时绝不应对时序有任何想当然的假设。
- 如果一个程序运行了两遍,则算作两个进程。
cpu利用率=1-p^n
p表示一个进程等待I/O操作的时间与其停留在内存中时间之比
n为进程个数,也为多道程序设计的道数。
更好的模型是从 概率论(因为现实中n个进程可能同时等待IO) 和 排队论(因为单CPU中进程不是独立的) 出发构建。
进程的创建
时机(按权限大小排序)
- 系统初始化
- 批处理作业的初始化(见于批处理系统)
- 正在运行的程序执行了创建进程的系统调用。
- 用户请求创建一个新进程。
(类)UNIX系统与Windows系统的进程创建方式对比
- UNIX系统中,fork用于创建新进程。
Windows系统中,(书中指win32)CreateProcess有两大作用;
- 创建进程
- 将正确的程序装入新的进程
图为我个人对两者创建区别的理解(结合书中其他表述)
进程的终止
自愿
- 正常退出
- 出错退出
非自愿
- 严重错误
- 被其他进程杀死
UNIX和windows的进程层次结构对比
- UNIX下,所有进程都属于以init为根的一棵树。进程及其所有子进程及其后裔共同组成一个进程组。
- windows中没有层次的概念,所有的进程地位都相同。
线程的含义?为什么需要线程?
本质上是将一个进程再细分。(因为不同进程拥有不同的地址空间和资源,一个进程的崩溃不会影响到另一个进程;而同一进程中的线程共享该进程的资源,且一个线程的崩溃会导致整个进程崩溃。这说明线程本身是进程中不可分割的一部分)
- 正如上面所提,进程模型无法满足"使并行实体拥有共享同一个地址空间和所有可用数据"的要求,而现实中这种需求是真实而迫切的。
- 线程比进程更轻量级,更容易(更快)创建和撤销。
- 在多cpu系统中,多线程使得使得真正意义上的并行实现有了可能。(这里其实需要线程共享内存这一特性)
关于进程与线程
可以这么理解:进程用于把资源集中到一起,而线程则是cpu上被调度执行的实体。
关于线程的运作过程
线程的创建
进程从当前某个线程开始,该线程可通过调用一个库函数创建新的线程。
线程的优先级
有时线程间会存在父子关系,但大多数情况下,所有的线程都是平等的。
线程的退出
- 可通过调用库函数退出,之后该线程不再可调度。
- 调用thread_yield,允许线程自动放弃CPU从而让另一个线程运行。
(原因:线程库无法像对进程一样利用时钟中断强制线程让出CPU)
关于线程模型
- 单个进程中的多个线程共享进程中的资源:
地址空间、全局变量、打开文件集、子进程、即将发生的定时器、信号与信号处理程序、账户信息(即进程的属性)
- 每个线程自己的内容:
程序计数器、寄存器、堆栈、状态(即线程的属性)
为什么每个线程需要有自己的堆栈?
因为通常每个线程会调用不同的过程,有各自不同的执行历史,所以需要堆栈来保存中间状态
- 有两种模型:如图所示:
分别是一个进程中有一个线程,然后多进程;一个进程中有多个线程。
若线程之间关系不大,应用前者;若线程间合作密切,应用后者。
实现线程
用户态
实现方式
把整个线程包放在用户态中,内核对线程包一无所知。
- 可以用函数库实现线程。
- 用户空间管理进程时,每个进程需要有其专用的线程表,该表由运行时系统管理,记录各个线程的属性,并在其状态切换完成时更新启动该线程所需的信息(类比内核中的进程表存放进程信息)
优点
- 用户级线程包可以在不支持线程的操作系统上实现。
- 借助线程表,线程切换可以在几条指令内完成,至少比陷入内核快一个数量级。
- 无需陷入内核、进行上下文切换、对内存高速缓存进行刷新。
- 允许每个进程有专属的调度算法。
缺点
阻塞系统调用难实现
解决方案1:将系统调用全改为非阻塞的
- 缺点:需要修改操作系统,与可在现有操作系统直接运行的初衷背离
- 解决方案2:提前预警,并避免使用可能会引发阻塞的调用
在系统调用周围从事检查的代码称为包装器(jacket或wrapper)
缺页中断问题(原理也和阻塞调用有关)
- 什么是缺页中断?
简单理解就是,并非所有程序都一次性放在内存中。当程序调用或跳转到某个内存中没有存储的指令时,会发生页面故障(缺页),操作系统需要从磁盘上取回这个指令及其相关指令;在对目标指令进行定位的过程中,相关进程会被阻塞(中断当前执行程序转而去执行定位读入操作)
- 为什么用户级线程的缺页中断需引起重视?
因为页面故障很可能只由一个线程引起,其他线程可以正常运行。但由于内核不知道线程的存在,会将整个进程阻塞,这极大降低效率。
- 线程的永久运行问题
线程无法像进程一样通过时钟中断实现轮换,当线程包中的一个线程开始运行时,其他线程就无法执行,这种情况有可能一直持续。(虽然线程退出机制里有thread_yield,但其是基于自愿原则)
- 可能的解决方案及其争议:让运行时系统定期请求时钟信号(中断)(可能会扰乱时钟)
必要性问题
- 用户级线程一般是为应用程序服务。多线程的目标场景是经常发生线程阻塞的应用。如果本身是CPU密集型或者少有阻塞,并不需要用到多线程。
- 但是用户级多线程由于是在用户态,一旦发生内核陷入,原有的线程被阻塞,要借助内核的力量的话,时间开销本身也很大。
(简单理解就是:用户级线程比较 表面 ,没出事的时候作用不大,出了事光指望它是不行的,效率还低,但是使用多线程的初衷是希望出了事能快快解决)
内核态
缺点:系统调用代价比较大。
- 内核有用来记录系统中所有线程的 线程表。进行线程的创建或撤销时,通过一个系统调用,对线程表更新来完成。
- 在内核中创建或撤销线程的开销大,所以会有回收线程的概念。即将撤销的线程标记为 不可运行的,但不改变其内部数据结构,下次可以通过改变标志位使其重新运行。
- 内核线程不需要任何新的、非阻塞的系统调用。缺页中断问题在内核级别可得到方便解决。
内核线程无法解决所有问题
例如一个多线程进程创建新进程,新进程是该拥有单一线程,还是该复制原进程的所有线程?答案是视情况而定,调用exec是前者,继续执行当前程序是后者。
混合实现
多路复用
参考书籍
《现代操作系统》 Andrew S.Tanenbaum,Herbert Bos著,陈向群,马洪兵等译