把学习当成一种习惯
选择往往大于努力,越努力越幸运

程序、进程、线程

程序、进程

  进程的本质是正在执行的一个程序,即一个进程就是一个正在执行程序的"实例";这句话有点抽象,借助书中做蛋糕的例子会更加容易理解些 : 一位有一手好厨艺的计算机科学家正在里的厨房为他女儿烘制生日蛋糕,他有制作蛋糕的食谱,并且有所需的原料,如面粉、鸡蛋、水果、糖等;这个食谱就是程序,厨房可以说就是OS,计算机科学家是处理器(CPU),蛋糕的原料就是输入数据;进程就是计算机科学家阅读的食谱,取来各种原料以及厨房提供的工具来烘制蛋糕等一些列操作的总合.

  程序和进程是等价的吗?答案为不是等价的,可以说是先有程序再有进程 :

  • 程序 : 结合上面食谱就是程序的例子,食谱详细记录了制作蛋糕的全部过程,我们可以理解为一种制作蛋糕的办法/步骤/方案,这个所谓的方案,会告诉我们如何去做,这些步骤被人们记录在食谱里,这个食谱也就是我们所说的程序.而在计算机专业领域里,是通过键盘+某种编程语言+存储器等等来构造一个程序,程序员在开发程序前,肯定需要先构思开发一个怎样功能的程序,然后通过编码来一步一步的完成程序的开发.两者的区别就是食谱是给人看的,而程序是给操作系统"看"的.
  • 进程 : 一个进程就是一个正在执行(运行)程序的"实例",这里的"实例",指真正实际运行在操作系统上的程序,即真正在执行、干活了,而前面所说的程序,只是一种可行性的想法、方案.继续结合上面的烘制蛋糕的例子,现在计算机科学家基于食谱开始制作蛋糕了,跟着食谱提供的方案一步一步的执行,但是在这个制作过程中呢,计算机科学家接收到了一个快递电话,需要现在去拿快递,所以计算机科学家需要保存食谱的执行步骤、调料的准备步骤等等动作,停止制作蛋糕,然后去拿快递,拿完快递回来后可以基于刚刚保存的状态继续制作蛋糕,最终制作出一个蛋糕;我们这里结合例子对进程总结一下 : 它有程序(食谱)、输入(食料)、输出(蛋糕)、运行状态(停止)以及保存运行状态的信息以便从他离开时那一步继续做下去.这里还出现了另一个拿快递的进程、进程切换、继续做蛋糕.
  • 进程切换 : 现代操作系统是多道程序设计,并且是抢夺式多任务,每个进程给你一个时间片,用完就强制休眠;甚至时间片未到但更紧急的事件出现了,也会强制低优先级进程休眠,即在任意时刻仅有一个进程真正在运行;
  • 运行状态 : 结合上面烘制蛋糕过程中拿快递的例子,我们可以知道进程是有切换的,所以进程有运行状态 :
    • 运行态 : 该时刻进程实际占用CPU;
    • 就绪态 : 可运行,但是因为其他进程占用了CPU而暂时停止,等待操作系统的调度;
    • 阻塞态 : 进程因等待某一件事情(如等待I/O设备)而暂时不能运行的状态;
  • 进程控制块 : 每个进程是一个独立的实体,有其自己的进程控制块,这个进程控制块保存了进程状态的重要信息,如进程ID、堆栈指针等运行状态的信息,这样进程切换回来的时候,可以基于上次保留的状态继续执行下去,就像没有中断过一样.下图给出了所需要的大致信息 :

小结

  • 程序 : 是一种可行性的方案.
  • 进程 : 是程序真正运行起来的实例,配合着OS负责着资源的管理、调度执行、保存了运行状态信息.

线程

  线程是一种"轻量级进程",一个进程有一至多个线程(每个进程对应至少包含一个地址空间和一个控制线程),线程是在CPU上被调度执行的实体;进程与进程之间的地址空间都是隔离的,而同一进程下的线程都是共享同一进程的地址空间的.进程和线程都可用作任务的调度单位,用一句抽象的话来说,对于操作系统而言,进程就是它的调度单位,对于进程而言,线程就是它的调度单位.

  为什么有了进程还需要一个"轻量级的进程" :

  • 从资源分配处理与执行的角度看,进程作用于资源的管理,而线程则是调度执行.
  • CPU处理器为多核芯片时,对于CPU密集型的任务,性能上可能并不能带来很大的增强(上下文切换的消耗),但是对于存在着大量的计算和I\O处理,可以使多线程重叠执行,从而提高系统的执行效率;
  • 线程的创建比进程的创建更容易、更快,即开销更小.
  • 运行状态 : 线程的运行状态有运行、就绪、阻塞、终止,线程状态间的转换和进程状态间的转换是一样的.
  • 线程控制块 : 每个线程都有独立的寄存器、程序计数器、堆栈、状态等运行信息;
  • 线程切换 : 线程切换分为不同进程的切换和同一进程下的线程切换,同一进程下的线程切换又分为用户线程切换和内核线程切换 :
    • 进程切换 : 进程切换,线程肯定是要切换的,这种开销是巨大的,例如修改页表,不然为什么会引入线程;
    • 同一进程下的线程切换 :
      • 内核线程切换 : 某个占有CPU使用权的内核态线程在让出CPU使用权(时间片已用完)会在线程表中保存该线程的寄存器,然后查看表中可运行的就绪线程,并把新线程的上下文环境重新装入机器.内核线程切换,需要进入内核态,切换完成后再回到用户态,这是内核态线程切换的主要开销之一.
      • 用户线程切换 : 用户线程的切换,也是从线程表(自己在用户态上维护)中保存该线程的上下文信息,然后查看线程表中的就绪线程,并把新线程的上下文环境重新装入机器的寄存器,只要堆栈指针和程序计数器一被切换,新的线程就可以开始运行了,无需陷入内核中.

【PS : 内核线程、用户线程的可以结合下面的知识点】

小结

  • 线程是一种"轻量级进程";
  • 线程是进程的调度执行单位;
  • 线程是在CPU上被调度执行的实体.

扩展 --- POSIX线程,线程的标准

用户态线程、内核态线程

线程的实现有用户态线程和内核态线程.

内核态线程

  内核线程通过系统调用来创建、销毁,并且会在内核中维护一个系统中所有线程的线程表,线程表中保存了每个线程的寄存器、状态等其他上下文信息;当线程进入阻塞时,会以系统调用的形式实现,即陷入内核,但是内核会根据选择一个就绪状态的线程来运行.总之简单一句话 : 内核线程由操作系统来调度.

内核线程的优势 :

  • 不需要非阻塞调用,提升CPU利用率,即做到实际上的并行 : 当线程陷入阻塞时(系统调用如磁盘IO或者缺页故障等),内核自己会检查是否有其他就绪状态的线程,如果有,那么可以让出CPU,切换给另外一个线程.这是内核级线程的绝对优势.

内核线程的劣势 :

  • 昂贵的上下文切换 : 由于是操作系统管理着这些线程,所以每次进行线程切换时必须陷入内核;
  • 内存占用大 : 线程表是存放在操作系统固定的表格空间或者堆栈空间里,所以内核级线程是需要限制、优化的。

一些常用解决方案:单线程、线程池、多进程单线程、IO多路复用等.

用户态线程

  用户线程是在用户态上通过线程库创建的,对于内核是一无所知的,所以对于操作系统来说,这个进程是单线程进程;在用户态创建的线程,进程也会在用户态上维护一张线程表,跟内核的线程表是一个道理.用户线程的切换只需把新线程保存值重新装入机器的寄存器、堆栈指针和程序计数器的切换,新线程就可以投入运行.总之简单一句话 : 用户线程可以由用户态的应用程序来管理.

用户线程的优势 :

  • 无需陷入内核、上下文切换 : 只需对堆栈指针和程序计数器指令切换;
  • 支持自己定制调度任务算法 : 什么时候切换任务自己说了算;
  • 内存占用小 : 线程表由用户态维护.

用户线程的劣势 :

  • 只能利用一个CPU : 对于操作系统来说,是一个单线程进程,即只使用了一个CPU.而且由于CPU的放弃由占有CPU的线程说了算,那么如果该线程不自动放弃CPU(直到时间片用完),那么其他用户线程是不能运行的.
  • 阻塞调用导致整个系统阻塞了 : 当进行一个系统调用时,如磁盘IO,那么整个进程陷入了阻塞态了.你这个时候可能有个解决方案:那我可以封装一个非阻塞的系统调用呀,这类方案称之为包装器(jacket或wrapper),这个第二篇文章再来说.

三种线程模型

有了上面线程、用户线程、内核线程的基础概念的铺垫,再来看看线程模型.

一对一模型

  每个用户线程对应一个内核线程,该模型在一个线程执行阻塞系统调用时,能够允许另一个线程继续执行,所以对于IO密集型的场景,CPU、磁盘IO、网卡等资源利用率还是非常友好的,提供了更好的并发功能.但是多线程的使用,就代表着线程与线程之间会进行上下文切换,结合上面内核线程的劣势.

Java的内存模型就是一对一模型 :

多对一模型

  多个用户线程对应一个内核线程,该模型的线程管理是由用户空间的线程库来完成的,因此效率更高,并且高效的上下文切换和几乎无限制的线程数量.不过,如果一个线程执行阻塞系统调用,那么整个进程将会阻塞.再者,因为任一时间只有一个线程可以访问内核,所以多个用户线程不能并行运行在多处理核系统上.

多对多模型

  多个用户线程对应多个内核线程,使得库和操作系统都可以管理线程,用户线程由运行时库调度器管理,内核线程由操作系统调度器管理,可运行的用户线程由运行时库分派并标记为准备好执行的可用线程,操作系统选择用户线程并将它映射到可用内核线程.

结束语

  • 本文是为文章(OS基础:Java的并发模型(三))铺垫的基础文章.
  • 原创不易
  • 希望看完这篇文章的你有所收获!

相关参考资料


目录