前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >从进程开始了解GMP模型

从进程开始了解GMP模型

作者头像
leobhao
发布于 2024-04-01 00:33:04
发布于 2024-04-01 00:33:04
1890
举报
文章被收录于专栏:涓流涓流

进程和线程

进程是操作系统分配资源(CPU、内存、文件)、调度任务和执行的一个基本单位。它拥有独立的内存空间、已分配的资源和独立的执行上下文。 线程是CPU调度的基本单位,同一进程内的线程共享了进程的资源和内存空间。

系统将内存分为两个区域:

  • 内核空间(Kernal Space): 内核空间是指用于运行操作系统内核、驱动程序等低级系统组件的特殊内存区域。在这个区域中,代码拥有对硬件的完全访问权限,包括传感器、内存、CPU 等。
  • 用户空间(User Space):用户空间指的是应用程序和相关库运行的内存区域。在这个区域中,代码受到了更严格的保护措施,以防止对硬件的不当访问。用户空间中的应用程序必须通过系统调用(System Call)与内核空间进行交互,以获得内核提供的服务,如内存分配、输入/输出操作等。

由于内核态具有非常高的权限, 用户空间的代码被限制在一个局部的内存空间, 也就是用户态(User Mode), 内核空间的代码能够访问所有的内存, 在内核空间的程序也被称为内核态(Kenranl Mode)

用户态切换内核态: 内核程序执行在内核态(Kernal Mode),用户程序执行在用户态(User Mode)。当发生系统调用时,用户态的程序发起系统调用。因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序。内核程序开始执行,也就是开始处理系统调.用。内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。

用户线程、内核线程和轻量级进程(LWP)

如果进程想要创造更多的线程,就需要思考一件事情,这个线程创建在用户态还是内核态。

很多同学会以为: 用户态的进程创建用户态的线程,内核态的进程创建内核态的线程。其实不是, 进程可以通过 API 创建用户态的线程,也可以通过系统调用创建内核态的线程。

用户态线程

用户态线程(User Level Thread):是一种完全由用户空间库(如线程库 Posix Pthreads)管理的线程实现。这种线程实现与操作系统内核相互独立,操作系统无法直接感知和调度这些线程。纯用户态线程仅在用户级别进行调度、上下文切换和同步操作,没有内核级别的干预。 优点:

  • 管理开销小:创建、销毁不需要系统调用。
  • 切换成本低:用户空间程序可以自己维护,不需要走操作系统调度。

缺点:

  • 与内核协作成本高:比如这种线程完全是用户空间程序在管理,当它进行 I/O 的时候,无法利用到内核的优势,需要频繁进行用户态到内核态的切换。
  • 线程间协作成本高:设想两个线程需要通信,通信需要 I/O,I/O 需要系统调用,因此用户态线程需要支付额外的系统调用成本。
  • 无法利用多核优势:比如操作系统调度的仍然是这个线程所属的进程,所以无论每次一个进程有多少用户态的线程,都只能并发执行一个线程,因此一个进程的多个线程无法利用多核的优势。
  • 操作系统无法针对线程调度进行优化:当一个进程的一个用户态线程阻塞(Block)了,操作系统无法及时发现和处理阻塞问题,它不会更换执行其他线程,从而造成资源浪费。
内核态线程

内核态线程(Kernel Level Thread):这种线程执行在内核态,可以通过系统调用创造一个内核级线程。

优点:

  • 可以利用多核 CPU 优势:内核拥有较高权限,因此可以在多个 CPU 核心上执行内核线程。
  • 操作系统级优化:内核中的线程操作 I/O 不需要进行系统调用;一个内核线程阻塞了,可以立即让另一个执行。

缺点:

  • 创建成本高:创建的时候需要系统调用,也就是切换到内核态。
  • 扩展性差:由一个内核程序管理,不可能数量太多。
  • 切换成本较高:切换的时候,也同样存在需要内核操作,需要切换内核态
LWP(轻量级进程)

首先理解一个概念 内核态调度实体(Kernel-level scheduling entity):是指在操作系统内核空间运行、被操作系统内核调度器管理的任务执行单位。在这种情况下,调度实体通常指的是线程(如内核线程)或轻量级进程(LWP)。

LWP 是 Linux (Solaris)系统中内核态调度实体的一种实现, 和内核线程都是以 task_struct 结构体表示, 具体两者的区别:

  • 内核线程(Kernel Thread):内核线程是运行在内核空间的线程,直接被内核管理,用于处理核心系统任务,如中断处理、设备驱动等。内核线程并不与用户空间的程序相关联,而只服务于操作系统内核。
  • 轻量级进程(LWP):Linux 中的 LWP 可以与 POSIX 线程 (Pthread 或用户线程) 联系起来。LWPs 由操作系统内核管理,它们运行在用户空间,可以执行用户程序。在 Linux 中,每个 LWP 对应一个唯一的内核态调度实体,有着独立的上下文切换信息。

另外LWP运行在用户态还是内核态呢?

轻量级进程(LWP)通常是用户空间线程(如 POSIX 线程)和内核态调度实体之间的桥梁。从这个角度来看,LWP 既有用户态特性,又与内核态调度实体关联。但整体上说,LWP 更多地处于内核态。

在用户态,LWP 与用户级线程(如 POSIX 线程)关联,它们共享代码、数据以及操作系统资源。然而,在内核中,每个 LWP 都关联着一个唯一的内核态调度实体,负责管理 LWP 的状态、资源和调度信息等。当用户线程需要进行系统调用时,操作系统接管控制权并将执行切换到内核态。

在 Linux 系统中, POSIX 线程和 LWP 的实现已经高度集成,LWP 可以直接与内核态调度实体关联,由操作系统内核进行调度和管理。因此,在这种上下文中,LWP 主要运行在内核态,负责管理和调度用户线程与内核态调度实体之间的映射关系。

线程模型

线程简单理解,就是要执行一段程序。程序不会自发的执行,需要操作系统进行调度。

如果要让一个拥有多个线程的用户态进程去执行, 最先想到的就是让一个内核去执行这个进程, 毕竟内核才能真正的调度CPU资源。这里的内核实际上是指的内核调度实体(KSE,Kernel Scheduling Entity)。内核调度实体 KSE 就是指可以被操作系统内核调度器调度的对象实体, 也可以简单的理解为内核线程。

这样所有的线程都需要自己去调度, 相当于在进程的主线程中实现分时算法调度每一个线程,也就是所有线程都用操作系统分配给主线程的时间片段执行。这种做法,相当于操作系统调度进程的主线程;进程的主线程进行二级调度,调度自己内部的线程。

这样操作劣势非常明显,比如无法利用多核优势,每个线程调度分配到的时间较少,而且这种线程在阻塞场景下会直接交出整个进程的执行权限。

用户态线程创建成本低,问题明显,不可以利用多核。内核态线程,创建成本高,可以利用多核,切换速度慢

因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。这样,用户线程与内核调度实体 KSE 之间的对应关系可以就形成了三种主要的线程模型:

  • 用户级线程模型
  • 内核级线程模型
  • 两级线程模型(或者混合线程模型)
多对一(Many to One)

也称为 用户级线程模型

用户线程与内核线程 KSE 是多对一(N : 1)的映射模型,多个用户线程的一般从属于单个进程并且多线程的调度是由用户自己的线程库来完成,线程的创建、销毁以及多线程之间的协调等操作都是由用户自己的线程库来负责而无须借助系统调用来实现。一个进程中所有创建的线程都只和同一个 KSE 在运行时动态绑定,也就是说,操作系统只知道用户进程而对其中的线程是无感知的,内核的所有调度都是基于用户进程。这种用户级线程调度都是基于基于合作式调度,而不是抢占式调度。这意味着当前运行的线程需要主动放弃执行权,以便其他线程获得 CPU 时间。由于不存在 CPU 时钟中断,线程无法在执行过程中被中断(抢占)。

这种模型相较于内核调度不需要让 CPU 在用户态和内核态之间切换,这种实现方式相比内核级线程可以做的很轻量级,对系统资源的消耗会小很多,因此可以创建的线程数量与上下文切换所花费的代价也会小得多。但是这个模型有个致命的缺陷:并不能做到真正意义上的并发,假设在某个用户进程上的某个用户线程因为一个阻塞调用(比如 I/O 阻塞)而被 CPU 给中断(抢占式调度)了,那么该进程内的所有线程都被阻塞(因为单个用户进程内的线程自调度是没有 CPU 时钟中断的,从而没有轮转调度),整个进程被挂起。

因为在用户级线程模型下,一个 CPU 关联运行的是整个用户进程,进程内的子线程绑定到 CPU 执行是由用户进程调度的,内部线程对 CPU 是不可见的,此时可以理解为 CPU 的调度单位是用户进程。所以很多的协程库会把自己一些阻塞的操作重新封装为完全的非阻塞形式,然后在以前要阻塞的点上,主动让出自己,并通过某种方式通知或唤醒其他待执行的用户线程在该 KSE 上运行,从而避免了内核调度器由于 KSE 阻塞而做上下文切换,这样整个进程也不会被阻塞了。

一对一(One to One)

也称为 内核级线程模型

用户线程与内核线程 KSE 是一对一(1 : 1)的映射模型,也就是每一个用户线程绑定一个实际的内核线程,而线程的调度则完全交付给操作系统内核去做。

大部分编程语言的线程库(比如 Java 的 java.lang.Thread、C++11 的 std::thread 等等)都是对操作系统的线程(内核级线程)的一层封装,创建出来的每个线程与一个独立的 KSE 静态绑定,因此其调度完全由操作系统内核调度器去做,也就是说,一个进程里创建出来的多个线程每一个都绑定一个 KSE。这种模型的优势和劣势同样明显:优势是实现简单,直接借助操作系统内核的线程以及调度器,所以 CPU 可以快速切换调度线程,于是多个线程可以同时运行,因此相较于用户级线程模型它真正做到了并行处理;但它的劣势是,由于直接借助了操作系统内核来创建、销毁和以及多个线程之间的上下文切换和调度,因此资源成本大幅上涨,且对性能影响很大

多对多(Many To Many)

也称为 两级线程模型

两级线程模型充分吸收前两种线程模型的优点且尽量规避它们的缺点。在此模型下,用户线程与内核 KSE 是多对多(N : M)的映射模型:首先,区别于用户级线程模型,两级线程模型中的一个进程可以与多个内核线程 KSE 关联,也就是说一个进程内的多个线程可以分别绑定一个自己的 KSE,这点和内核级线程模型相似;其次,又区别于内核级线程模型,它的进程里的线程并不与 KSE 唯一绑定,而是可以多个用户线程映射到同一个 KSE,当某个 KSE 因为其绑定的线程的阻塞操作被内核调度出 CPU 时,其关联的进程中其余用户线程可以重新与其他 KSE 绑定运行。所以,两级线程模型既不是用户级线程模型那种完全靠自己调度的也不是内核级线程模型完全靠操作系统调度的,而是中间态(自身调度与系统调度协同工作)。由 用户调度器实现用户线程到 KSE 的调度,内核调度器实现 KSE 到 CPU 上的调度。

Go 语言中的 runtime 调度器就是采用的这种实现方案,实现了 Goroutine 与 KSE 之间的动态关联。

GO GMP 调度模型

GO 采取 GMP来解决传统内核级线程的创建、切换、销毁开销大的问题, 其中:

  • G(Goroutine): 用户态、轻量级的协程,一个 G 代表了对一段需要被执行的 Go 语言程序的封装;每个 Goroutine 都有自己独立的栈存放自己程序的运行状态上下文;分配的栈大小 2KB,可以按需扩缩容
  • M(Machine): 代表了内核线程 OS Thread 的抽象,CPU调度的基本单元;在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础,M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。
  • P(Processor): 代表一个虚拟的处理器,它维护一个局部的可运行的 G 队列,可以通过 CAS 的方式无锁访问,工作线程 M 优先使用自己的局部运行队列中的 G,只有必要时才会去访问全局运行队列,这大大减少了锁冲突,提高了大量 G 的并发性。每个 G 要想真正运行起来,首先需要被分配一个 P。 对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量),P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256。

整体如下图所示:

Goroutine 调度器负责将 G 高效的调度到 M 上去执行, 核心思想是:

  1. 尽可能复用线程 M, 避免频繁的线程创建和销毁;
  2. 利用多核并行能力, 限制同时运行(不包含阻塞)的 M 线程数为 N,N 等于 CPU 的核心数目,这里通过设置 P 处理器的个数为 GOMAXPROCS 来保证,GOMAXPROCS 一般为 CPU 核数,因为 M 和 P 是一一绑定的,没有找到 P 的 M 会放入空闲 M 列表,没有找到 M 的 P 也会放入空闲 P 列表;
  3. Work Stealing 任务窃取机制:M 优先执行其所绑定的 P 的本地队列的 G,如果 P 为空,即没有其他 G 可以执行,它(这里指的是 M, 由 go 调度器来实现)会先尝试从 Global 队列寻找 G 来执行,如果 Global 队列为空,它会随机挑选另外一个 P,从它的队列里中拿走一半的 G 到自己的队列中执行。由于 P 是一个双端队列, 执行任务的时候关联的 M 从一端获取任务,而其他 M 在窃取任务时从另一端获取。这种设计可以降低锁竞争风险,从而提高调度性能。
  4. Hand Off 交接机制:如果 P 关联的 M 阻塞(例如等待 I/O 完成),会将 M 上 P 解绑并交给其他空闲的 M 执行,如果空闲 M 的数量不足,可以根据实际需要创建新的 M
  5. goroutine 调度器协作式抢占: 由于Go 语言没有实现传统的抢占式时间片调度,因此,协作式抢占是一种在并发执行一组 goroutines 时获得平衡和公平性的方法。 goroutine 可能在以下被阻塞的情况下主动让出时间片让另一个 goroutine 执行: (1) blocking syscall (for example opening a file) (2) network input (3) channel operations (4) primitives in the sync package

总的来说可以分为两种情况:

  • 用户态阻塞/唤醒 当 goroutine 因为 channel 操作或者 network I/O 而阻塞时(实际上 golang 已经用 netpoller 实现了 goroutine 网络 I/O 阻塞不会导致 M 被阻塞,仅阻塞 G),对应的 G 会被放置到某个 wait 队列(如 channel 的 waitq),该 G 的状态由 _Gruning 变为 _Gwaitting ,而 M 会跳过该 G 尝试获取并执行下一个 G,如果此时没有 runnable 的 G 供 M 运行,那么 M 将解绑 P,并进入 sleep 状态;当阻塞的 G 被另一端的 G2 唤醒时(比如 channel 的可读/写通知),G 被标记为 runnable,尝试加入 G2 所在 P 的 runnext,然后再是 P 的 Local 队列和 Global 队列。
  • 系统调用阻塞 当 G 被阻塞在某个系统调用上时,此时 G 会阻塞在 _Gsyscall 状态,M 也处于 block on syscall 状态,此时的 M 可被抢占调度:执行该 G 的 M 会与 P 解绑,而 P 则尝试与其它 idle 的 M 绑定,继续执行其它 G。如果没有其它 idle 的 M,但 P 的 Local 队列中仍然有 G 需要执行,则创建一个新的 M;当系统调用完成后,G 会重新尝试获取一个 idle 的 P 进入它的 Local 队列恢复执行,如果没有 idle 的 P,G 会被标记为 runnable 加入到 Global 队列。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-6-5,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
golang的线程模型——GMP模型
内核线程(Kernel-Level Thread ,KLT) 轻量级进程(Light Weight Process,LWP):轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程
benny
2021/08/09
1.9K0
【深度知识】GO语言的goroutine并发原理和调度机制
Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。
辉哥
2019/08/05
1.8K0
【深度知识】GO语言的goroutine并发原理和调度机制
Go的CSP并发模型实现:M, P, G
最近抽空研究、整理了一下Golang调度机制,学习了其他大牛的文章。把自己的理解写下来。如有错误,请指正!!!
sunsky
2020/08/20
1.5K0
进程、线程与协程傻傻分不清?一文带你吃透!
欢迎来到操作系统系列,依然采用图解 + 大白话的形式来讲解,让小白也能看懂,帮助大家快速科普入门
9号同学
2021/07/07
9360
进程、线程与协程傻傻分不清?一文带你吃透!
Golang 语言的 goroutine 调度器模型 GPM
Golang 语言与其他编程语言之间比较,最大的亮点就是 goroutine,使 Golang 语言天生支持并发,可以高效使用 CPU 的多个核心,而并发执行需要一个调度器来协调。
frank.
2021/01/22
1.3K0
线程模型的理解
Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,一次系统调用可以实现用户态和内核态的切换
liddytang
2021/02/25
3.6K2
Linux下的进程类别(内核线程、轻量级进程和用户进程)--Linux进程的管理与调度(四)
虽然我们在区分Linux进程类别, 但是我还是想说Linux下只有一种类型的进程,那就是task_struct,当然我也想说linux其实也没有线程的概念, 只是将那些与其他进程共享资源的进程称之为线程。
233333
2018/09/14
6.8K1
Linux下的进程类别(内核线程、轻量级进程和用户进程)--Linux进程的管理与调度(四)
Java与线程
并发不一定要依赖多线程(如PHP的多进程并发),但在Java中谈论并发,大多数都与线程脱不开关系
JavaEdge
2018/05/16
2.6K1
关于Go并发编程,你不得不知的“左膀右臂”——并发与通道!
导语 | 并发编程,可以说一直都是开发者们关注最多的主题之一。而Golang作为一个出道就自带“高并发”光环的编程语言,其并发编程的实现原理肯定是值得我们深入探究的。本文主要介绍Goroutine和channel的实现。 Go并发编程模型在底层是由操作系统所提供的线程库支撑的,这里先简要介绍一下线程实现模型的相关概念。 一、线程的实现模型 线程的实现模型主要有3个,分别是:用户级线程模型、内核级线程模型和两级线程模型。它们之间最大的差异在于用户线程与内核调度实体(KSE)之间的对应关系上。内核调度实体就是可
腾讯云开发者
2022/01/26
5910
GoLang GPM模型
goroutine 是什么?通常 goroutine 会被当做 coroutine(协程)的 golang 实现,但实际上,goroutine 并非传统意义上的协程,现在主流的线程模型分三种:内核级线程模型、用户级线程模型和两级线程模型(也称混合型线程模型),传统的协程库属于用户级线程模型,而 goroutine 和它的 Go Scheduler 在底层实现上其实是属于两级线程模型。
阿珍
2025/03/05
850
GoLang GPM模型
如何使用 Go 更好地开发并发程序,纯干货!
Go 语言的并发特性是其一大亮点,今天我们来带着大家一起看看如何使用 Go 更好地开发并发程序?
aoho求索
2021/03/16
5710
如何使用 Go 更好地开发并发程序,纯干货!
Golang GMP模型
早期操作系统是单进程的,只能顺序执行进程,如果进程需要IO,必须要等IO结束才能继续运行,造成了严重的CPU资源的浪费。
月梦@剑心
2023/08/31
4590
Golang GMP模型
协程实现原理
大家好,我是易安!今天我们来探讨一个问题,Go 协程的实现原理。此“协程”非彼”携程“。
架构狂人
2023/08/16
4320
协程实现原理
前端性能监控(RUM)接入层服务高并发优化实践(二)——并发模型原理
张翔 腾讯高级前端开发工程师、腾讯云前端性能监控(RUM)核心开发。主要负责前端性能监控系统中的上报服务层模块的设计与实现。 前言 往期我们介绍了 前端性能监控 (RUM) 接入层服务高并发优化实践 ,我们针对缓存模型进行重新设计与优化,相信不少用户也感受到了 RUM 前所未有的流畅。 腾讯云前端性能监控(RUM) 系统中,接入层服务时刻承受着平均上百万 QPS 的上报请求,所以对于服务端的性能要求是极其高。接着往期 前端性能监控 (RUM) 接入层服务高并发优化实践 说的缓存模型,本次我们将带为您潜入 G
腾讯云可观测平台
2022/08/26
6300
前端性能监控(RUM)接入层服务高并发优化实践(二)——并发模型原理
go进阶(1) -深入理解goroutine并发运行机制
并发指的是同时进行多个任务的程序,Web处理请求,读写处理操作,I/O操作都可以充分利用并发增长处理速度,随着网络的普及,并发操作逐渐不可或缺 
黄规速
2023/02/27
4.5K0
go进阶(1) -深入理解goroutine并发运行机制
操作系统之进程管理(上),研究再多高并发,都不如啃一下操作系统进程!!!
由图可知程序会先由编译器编译成机器指令,运行之前先把程序放入内存,在内存中创建一个进程实体。一个进程实体(进程映像)由PCB、程序段、数据段组成。然后CPU从内存中取出指令,来运行程序。
阿甘的码路
2022/09/22
4980
操作系统之进程管理(上),研究再多高并发,都不如啃一下操作系统进程!!!
进程和线程基础知识全家桶,30 张图一套带走
我们写好的一行行代码,为了让其工作起来,我们还得把它送进城(进程)里,那既然进了城里,那肯定不能胡作非为了。
小林coding
2020/07/14
3980
进程和线程基础知识全家桶,30 张图一套带走
Golang 协程/线程/进程 区别以及 GMP 详解
**进程** 每个进程都有自己的独立内存空间,拥有自己独立的地址空间、独立的堆和栈,既不共享堆,亦不共享栈。一个程序至少有一个进程,一个进程至少有一个线程。进程切换只发生在内核态。
码仔janrs.com
2023/06/01
7300
推荐阅读
相关推荐
golang的线程模型——GMP模型
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档