什么是进程?

从概念上来说:进程是计算机顺序执行的程序的过程,它是系统资源分配的最小单位。从我个人理解来说它类似于一个容器,里面有很多的资源(比如内存、打开的文件描述符),里面也有空间去运行程序的指令,更重要的是有隔离属性,支持了系统中多并发活动。

什么是线程? 线程是进程当中的一条执行流程。同一个进程内多个线程之间可以资源,但每个线程各自都有一套独立的寄存器和栈,来确保其控制流是相对独立的。

  • 房子比作进程 房子实际上是一个容器,具有某些属性(例如 占地面积、卧室数量等)。 这样来看,房子真的不会主动自己做任何事情——它是一个被动的对象。 这实际上就是一个过程。

  • 人比作线程 住在房子里的人是活跃的对象——他们是使用各个房间的人,看电视,做饭,拿走 淋浴,等等。

进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位

并行与并发

并行(parallel):并行-行字,有左右结构, 是并行的 并发(concurrency):下图右边的结构

1
2
3
4
5
6
7
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░▒█████████████████████████████████▒░░░░░░░░░░░▒▓▓▓▓▓░░░░░░░░░░░░▓▓▓▓▓▒░░░░░░░░░░░░░░░░░
░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░▒█████████████████████████████████▒░░░░░░░░░░░▒░░░░░▒▓▓▓▓▓░░░░░░░░░░░░▓▓▓▓▓▒░░░░░░░░░░░
░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░▒░░░░░░░░░░░▓████▓░░░░░░░░░░░▒████▓░░░░░░
░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

1、在单核计算机里,有一个资源是无法被多个程序并行使用的:cpu。

没有操作系统的情况下,一个程序一直独占着全都cpu。

如果要有两个任务来共享同一个CPU,程序员就需要仔细地为程序安排好运行计划–某时刻cpu和由程序A来独享,下一时刻cpu由程序B来独享

而这种安排计划后来成为OS的核心组件,被单独名命为“scheduler”,即“调度器”,它关心的只是怎样把单个cpu的运行拆分成一段一段的“运行片”,轮流分给不同的程序去使用,而在宏观上,因为分配切换的速度极快,就制造出多程序并行在一个cpu上的假象。

2、在单核计算机里,有一个资源可以被多个程序共用,然而会引出麻烦:内存。

在一个只有调度器,没有内存管理组件的操作系统上,程序员需要手工为每个程序安排运行的空间 – 程序A使用物理地址0x00-0xff,程序B使用物理地址0x100-0x1ff,等等。

然而这样做有个很大的问题:每个程序都要协调商量好怎样使用同一个内存上的不同空间,软件系统和硬件系统千差万别,使这种定制的方案没有可行性。 为了解决这个麻烦,计算机系统引入了“虚拟地址”的概念,从三方面入手来做:

2.1、硬件上,CPU增加了一个专门的模块叫MMU,负责转换虚拟地址和物理地址。 2.2、操作系统上,操作系统增加了另一个核心组件:memory management,即内存管理模块,它管理物理内存、虚拟内存相关的一系列事务。 2.3、应用程序上,发明了一个叫做【进程】的模型,(注意)每个进程都用【完全一样的】虚拟地址空间,然而经由操作系统和硬件MMU协作,映射到不同的物理地址空间上。不同的【进程】,都有各自独立的物理内存空间,不用一些特殊手段,是无法访问别的进程的物理内存的。

3、现在,不同的应用程序,可以不关心底层的物理内存分配,也不关心CPU的协调共享了。然而还有一个问题存在:有一些程序,想要共享CPU,【并且还要共享同样的物理内存】,这时候,一个叫【线程】的模型就出现了,它们被包裹在进程里面,在调度器的管理下共享CPu,拥有同样的虚拟地址空间,同时也共享同一个物理地址空间,然而,它们无法越过包裹自己的进程,去访问别一个进程的物理地址空间。

4、进程之间怎样共享同一个物理地址空间呢?不同的系统方法各异,符合posix规范的操作系统都提供了一个接口,叫mmap,可以把一个物理地址空间映射到不同的进程中,由不同的进程来共享。


  • 我们先从线程讲起,无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。
    • 而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;
      • 内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源.

      • 用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。

        • 我们现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程
        • 操作系统本身内核态的线程(简称KSE),还是有区别的。

线程模型的实现,可以分为以下几种方式:

  • [M:1] 如图所示,多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。

用户级线程模型

  • [1:1] 这种模型直接调用操作系统的内核线程,所有线程的创建、终止、切换、同步等操作,都由内核来完成。C++就是这种。

  • [M:N] 这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。这种模型的实现非常复杂,和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。

—

Go语言的线程模型就是一种特殊的两级线程模型。暂且叫它“MPG”模型吧。

  • Go线程实现模型MPG
    • M指的是Machine,一个M直接关联了一个内核线程。
    • P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。
    • G指的是Goroutine,其实本质上也是一种轻量级的线程。

以上这个图讲的是两个线程(内核线程)的情况。

一个M会对应一个内核线程, 一个M也会连接一个上下文P,

一个上下文P相当于一个“处理器”, 一个上下文连接一个或者多个Goroutine。

  • P(Processor)的数量是在启动时被设置为环境变量GOMAXPROCS的值,
    • 或者通过运行时调用函数runtime.GOMAXPROCS()进行设置。
  • Processor数量固定意味着任意时刻只有固定数量的线程在运行go代码。Goroutine中就是我们要执行并发的代码。
    • 图中P正在执行的Goroutine为蓝色的;处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列runqueues

抛弃P(Processor)

  • 你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让Goroutine的runqueues挂到M上呢?答案是不行,需要上下文的目的,是让我们可以直接放开其他线程,当遇到内核线程阻塞的时候。

  • 一个很简单的例子就是系统调用sysall,一个线程肯定不能同时执行代码和系统调用被阻塞,这个时候,此线程M需要放弃当前的上下文环境P,以便可以让其他的Goroutine被调度执行。

go

如上图左图所示,M0中的G0执行了syscall,然后就创建了一个M1(也有可能本身就存在,没创建),(转向右图)然后M0丢弃了P,等待syscall的返回值,M1接受了P,将·继续执行Goroutine队列中的其他Goroutine。

当系统调用syscall结束后,M0会“偷”一个上下文,如果不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,然后自己放到线程池或者转入休眠状态。全局runqueue是各个P在运行完自己的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。

均衡的分配工作

按照以上的说法,上下文P会定期的检查全局的goroutine 队列中的goroutine,以便自己在消费掉自身Goroutine队列的时候有事可做。假如全局goroutine队列中的goroutine也没了呢?就从其他运行的中的P的runqueue里偷。

每个P中的Goroutine不同导致他们运行的效率和时间也不同,在一个有很多P和M的环境中,不能让一个P跑完自身的Goroutine就没事可做了,因为或许其他的P有很长的goroutine队列要跑,得需要均衡。 该如何解决呢?

Go的做法倒也直接,从其他P中偷一半!

线程与进程的区别

线程与进程的比较如下:

  • 所属单位不同:进程是资源(包括内存、打开的文件等)分配的单位,线程是CPU调度的单位;
  • 所拥有的资源不同:进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
  • 时间/空间开销不同:线程能减少并发执行的时间和空间开销;

对于,线程相比进程能减少开销,体现在:

  • 创建/终止:线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;线程的终止时间比进程快,因为线程释放的资源相比进程少很多。
  • 切换:同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
  • 交互:由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;

进程状态

20220729223445

在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存

挂起状态:描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态

僵尸进程/孤儿进程/守护进程

reference:

http://morsmachine.dk/go-scheduler?spm=a2c4e.10696291.0.0.1b3a19a47lE8HL

Go: Goroutine, OS Thread and CPU Management