【操作系统学习笔记】进程与线程(一)

250 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

注:本文为个人操作系统的学习笔记,如有错误,还请各位大神指出,谢谢!

进程的含义?如何理解进程?

  • 进程是操作系统中最核心的概念,是对正在运行中的程序的一种抽象,本质上指的是正在运行中的程序,它拥有独立的地址空间,上面几乎存放着所有与运行这个程序有关的信息,如可执行程序,程序的数据以及程序的堆栈。

  • 进程表:与一个进程相关的所有信息,除该进程自身地址空间的内容外均放在了操作系统的一张表——进程表中。其物理结构为数组或链表,每个进程都要单独占据一项。

  • 别的角度:用某种方法把相关资源集中在一起。(因为进程有相应的独立地址空间)

关于进程模型

基于两大独立概念

资源分组处理和执行

多道程序设计:

真正的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)

关于线程模型

  • 单个进程中的多个线程共享进程中的资源: 地址空间、全局变量、打开文件集、子进程、即将发生的定时器、信号与信号处理程序、账户信息(即进程的属性)

  • 每个线程自己的内容: 程序计数器、寄存器、堆栈、状态(即线程的属性)

    为什么每个线程需要有自己的堆栈? 因为通常每个线程会调用不同的过程,有各自不同的执行历史,所以需要堆栈来保存中间状态

  • 有两种模型:如图所示:

image.png

分别是一个进程中有一个线程,然后多进程;一个进程中有多个线程。 若线程之间关系不大,应用前者;若线程间合作密切,应用后者。

实现线程

用户态

实现方式

把整个线程包放在用户态中,内核对线程包一无所知。

  • 可以用函数库实现线程。
  • 用户空间管理进程时,每个进程需要有其专用的线程表,该表由运行时系统管理,记录各个线程的属性,并在其状态切换完成时更新启动该线程所需的信息(类比内核中的进程表存放进程信息)

优点

  • 用户级线程包可以在不支持线程的操作系统上实现。
  • 借助线程表,线程切换可以在几条指令内完成,至少比陷入内核快一个数量级。
  • 无需陷入内核、进行上下文切换、对内存高速缓存进行刷新。
  • 允许每个进程有专属的调度算法。

缺点

  • 阻塞系统调用难实现

    • 解决方案1:将系统调用全改为非阻塞的
      • 缺点:需要修改操作系统,与可在现有操作系统直接运行的初衷背离
    • 解决方案2:提前预警,并避免使用可能会引发阻塞的调用 在系统调用周围从事检查的代码称为包装器(jacket或wrapper)
  • 缺页中断问题(原理也和阻塞调用有关)

    • 什么是缺页中断? 简单理解就是,并非所有程序都一次性放在内存中。当程序调用或跳转到某个内存中没有存储的指令时,会发生页面故障(缺页),操作系统需要从磁盘上取回这个指令及其相关指令;在对目标指令进行定位的过程中,相关进程会被阻塞(中断当前执行程序转而去执行定位读入操作)
    • 为什么用户级线程的缺页中断需引起重视? 因为页面故障很可能只由一个线程引起,其他线程可以正常运行。但由于内核不知道线程的存在,会将整个进程阻塞,这极大降低效率。
  • 线程的永久运行问题 线程无法像进程一样通过时钟中断实现轮换,当线程包中的一个线程开始运行时,其他线程就无法执行,这种情况有可能一直持续。(虽然线程退出机制里有thread_yield,但其是基于自愿原则)

    • 可能的解决方案及其争议:让运行时系统定期请求时钟信号(中断)(可能会扰乱时钟)
  • 必要性问题

    • 用户级线程一般是为应用程序服务。多线程的目标场景是经常发生线程阻塞的应用。如果本身是CPU密集型或者少有阻塞,并不需要用到多线程。
    • 但是用户级多线程由于是在用户态,一旦发生内核陷入,原有的线程被阻塞,要借助内核的力量的话,时间开销本身也很大。 (简单理解就是:用户级线程比较 表面 ,没出事的时候作用不大,出了事光指望它是不行的,效率还低,但是使用多线程的初衷是希望出了事能快快解决)

内核态

缺点:系统调用代价比较大。

  • 内核有用来记录系统中所有线程的 线程表。进行线程的创建或撤销时,通过一个系统调用,对线程表更新来完成。
  • 在内核中创建或撤销线程的开销大,所以会有回收线程的概念。即将撤销的线程标记不可运行的,但不改变其内部数据结构,下次可以通过改变标志位使其重新运行。
  • 内核线程不需要任何新的、非阻塞的系统调用。缺页中断问题在内核级别可得到方便解决。

内核线程无法解决所有问题

例如一个多线程进程创建新进程,新进程是该拥有单一线程,还是该复制原进程的所有线程?答案是视情况而定,调用exec是前者,继续执行当前程序是后者。

混合实现

多路复用

image.png

参考书籍

《现代操作系统》 Andrew S.Tanenbaum,Herbert Bos著,陈向群,马洪兵等译