从操作系统漫谈GOLang GPM模型

2023-05-16

前言

本文从操作系统谈起,简单介绍操作系统基本知识,引出进程、线程调度的基本原理和基本模型,然后从0到1设计Golang调度器,通过方案的逐步演进升级,可以了解到GPM模型设计理念。

阅读本文会了解到线程模型、线程调度,并最终理解Golang为什么设计为GPM模型,并通过简要runtime代码分析,验证在GPM模型推演过程中各种问题设想以及解决方案。

写在前面

  • 操作系统基本知识主要介绍什么是内核态与用户态?为什么要有内核态和用户态?用户态和内核态区别?从而引申出线程模型,对相关知识熟悉的同学可以跳过本章节
  • 行文中对于协程和线程的概念未做区分,用线程统一定义,因为本质上协程是用户态线程。读者知悉。

操作系统基础知识

维基给操作系统的定义是一组主管并控制计算机操作、运用和运行硬件、软件资源和提供公共服务来组织用户交互的相互关联的系统软件程序。

去掉定语可以简单理解 :操作系统(operation system,简称OS)是系统软件程序。

进一步可以理解:操作系统也是一种程序、代码,其介于计算机硬件,和上层软件之间的一种软件。

其核心职能无非五个方面:进程、存储、设备、文件、作业管理

什么是用户态和内核态?

大前提:软件程序运行需要资源(存储) 

小前提:操作系统是软件

结论: 操作系统运行需要资源

介于上述三段式结论,

  • 操作系统运行时使用的存储资源叫内核态
  • 用户(上层软件)运行时使用的存储资源叫用户态。

对于32位系统而言,其最大寻址空间为 2^32次方=4G内存。

linux系统内核态和用户态占比1:3,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

windows系统内核态和用户态占比2:2 (难道这就是windows比linux卡顿的原因?)

为什么要分为内核态和用户态?两者可以共用么?

内核态和用户态实现了权限隔离。

试想,如果上层程序可以随意访问、修改操作系统运行内存,恶意行为或者bug等异常操作可能会破坏操作系统运行环境,将会导致操作系统的崩溃,进而导致上层全部应用程序的崩溃,这对于计算机来说是灾难性的。

通过内核态和用户态区分,可以保护底层操作系统健壮性,保证系统安全运行,上层程序的bug不会破坏操作系统的运行环境。(当然,操作系统也可能存在bug,比如windows系统经常性蓝屏)

什么时候运行在内核态?什么时候运行在用户态?

程序大多数时间是运行在用户态,当程序需要操作系统完成协助时会从用户态切换到内核态。

需要协助的场景有:

系统调用:读取文件、网卡发起网络请求

异常:缺页、除以0等触发异常

中断处理:接收网卡数据等

用户态切换到内核态需要哪些操作?

以读取系统文件为例,操作系统提供一种接口(机制)供上层应用调用,上层应用程序在进行read系统调用时,会触发程序从用户态切换到内核态,执行完相关的系统调用后,再从内核态切换到用户态,继续执行上层应用程序的逻辑。

其大概流程如下:

  • 保留用户态现场(上下文、寄存器、用户栈等)
  • 复制用户态参数,用户栈切到内核栈,进入内核态
  • 额外的检查(因为内核代码对用户不信任)
  • 执行内核态代码
  • 复制内核态代码执行结果,回到用户态
  • 恢复用户态现场(上下文、寄存器、用户栈等)

结论: 从用户态切换到内核态需要“额外的”成本。

线程模型

鉴于系统用户态与内核态的存在,线程实现方式分为内核态线程和用户态线程:

内核态线程: 操作系统层面实现的线程

用户态线程:在操作系统上层应用实现的线程库。但无论如何,用户态线程如果想执行(操作系统硬件资源),都需要映射或者绑定到内核态线程。

1:1 模型

即创建一个用户态线程即创建一个内核态线程,用户态线程销毁,则内核态线程也销毁。线程不断创建与销毁,开销较大

M:1模型

用户态M个线程共用内核态一个线程,用户态线程任意一个线程阻塞,将导致内核态线程阻塞。

M:N模型

用户态M个线程映射到内核态N个线程上,如果用户态线程阻塞导致内核态线程阻塞,则可以将其他用户态线程绑定到新的内核态线程继续执行。

  • 从0到1设计Golang协程调度器

版本v1.0.0 —— G模型

按照线程模型 1:1模型,实现调度器的1.0版核心逻辑是:

  • 每次创建一个用户态线程,则同步创建一个内核态线程用于执行用户态线程。

优点

  • 将线程调度交给内核完成,语言层面无需实现调度器。

缺点

  • 创建成本:大量创建、销毁内核线程,导致不必要的系统开销。
  • 调度成本:对于短任务(执行时间小于系统内核线程调度时间片),会持续导致内核线程上下文切换,引入不必要的系统开销。

注:C++线程库则采用这种方式

版本v2.0.0 —— GM模型

按照2.1节分析可知,每次创建/销毁内核线程,成本较大,因此是否可以池化技术,将运行结束的内核线程缓存下来,用户态线程创建后直接绑定到内核线程池中获取一个线程进行运行,从而减少创建和销毁的步骤。

因此借鉴池化思想,采用M:N线程模型实现线程调度器,其核心思路:

  • 维护一个全局的任务队列,用户态每次创建线程,即放入全局任务队列中
  • 池化技术创建一批内核态线程,从全局任务队列中获取用户任务执行,执行结束or执行时间片结束后再次放入队列等待下次调度。

优点:

  • 解决方案一中持续创建内核线程导致的系统开销以及短任务执行导致的内核态线程不断上下文切换开销。

缺点

  • 因为涉及到全局队列(临界区)的读写操作,并发操作需要进行加锁、解锁操作。
  • 需要实现 用户态线程(协程)调度器

版本v3.0.0 ——PM模型

既然全局队列导致临界区的并发操作,需要加锁进行,那么是否可以每个内核线程创建一个”私有“队列,不同内核线程从”私有“队列中获取任务。上述思路可以简单理解为PM模型。

这里有两个问题:

  • 每个任务的执行时间长短不一致,可能存在P1队列中任务执行结束,P2中任务还未执行完?
    • 可能有同学说如果P1执行完以后,是不是可以从P2队列中“偷”一些G进行执行?
      • 那么问题来了,举例只是举了2个P,如果有N个P的话,P1是不是还需要从N个P中找到一个任务较多的P进行“偷窃”
  • P数量个数受限于机器核数(等于),本地队列长度有限,如果任务数过多,可能导致多个P本地队列仍然无法缓存下全部的G
    • 那是否可以将本地队列大小进行扩展?
      • 可以,但是我们可以假设程序在多数情况下,任务数是较小的, 极端情况下可能出现任务的暴涨,如果本地队列数过大,导致不必要的内存消耗。

挖坑1: 本地队列大小是多少?

版本v4.0.0——GPM模型

鉴于上述问题,以及GM模型中全局队列思路,进一步优化思路:

  • 每个P有本地队列,然后有一个全局队列,每次构建G先尝试放入P队列,如果P队列容量不足,则放入全局队列。
  • 如果P本地队列G数量为0,则尝试从全局队列中获取G执行,如果全局队列中也无可运行G,则尝试从其他P的本地队列中“偷窃”可运行的G。

挖坑2:本地队列和全局队列如何实现的?全局队列如何实现不限制大小?

挖坑3:假设P本地队列中的任务时间都较长,且数量满负荷运行,是否会导致全局队列中的G迟迟得不到调度,出现“饥饿”状态?

挖坑4:调度器如何进行G的调度?如何内核态线程M阻塞(如socket请求)会阻塞相应P的所有G执行么?

至此,从0到1 完整“设计”出GPM模型。

填坑

  • 挖坑1: 本地队列大小是多少?
type p struct {
   // Queue of runnable goroutines. Accessed without lock.
   runqhead uint32
   runqtail uint32
   runq     [256]guintptr
   // runnext, if non-nil, is a runnable G that was ready'd by
   // the current G and should be run next instead of what's in
   // runq if there's time remaining in the running G's time
   // slice. It will inherit the time left in the current time
   // slice. If a set of goroutines is locked in a
   // communicate-and-wait pattern, this schedules that set as a
   // unit and eliminates the (potentially large) scheduling
   // latency that otherwise arises from adding the ready'd
   // goroutines to the end of the run queue.
   runnext 
}

从p结构定义中可知,本地队列的数据结构是数组,其大小为256。

  
  • 挖坑2:本地队列和全局队列如何实现的?全局队列如何实现不限制大小?
type schedt struct {
// Global runnable queue.
runq gQueue
runqsize int32
}

从schedt结构体定义可知,全局队列的数据结构是链表,所以其长度不受限制。

本地队列实现方式是数组,长度为256。

挖坑3:假设P本地队列中的任务时间都较长,且数量满负荷运行,是否会导致全局队列中的G迟迟得不到调度,出现“饥饿”状态?

if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}

深度调度代码可知,每次调度本地队列中G61次后则会尝试从全局队列中获取一部分G进行执行,这样保证全局队列的G有一定机会获取调度机会。

挖坑4:调度器如何进行G的调度?如果内核态线程M阻塞(如socket请求)会阻塞相应P的所有G执行么

  • 异步系统调用 G 会和MP分离(G挂到netpoller)
  • 同步系统调用 MG 会和P分离(P另寻M),当M从系统调用返回时,不会继续执行,而是将G放到run queue。

    具体原理参考文章:golang 系统调用与阻塞处理 | 李乾坤的博客

总结

本文首先从操作系统内核态和用户态说起,介绍线程模型实现方式,进而引申出Golang调度器的设计,并从0-4版本设计和优化,逐步演化到GPM模型,在这过程中,介绍各种模型实现方式的优缺点,进而可以体会到GPM模型的设计理念,加深用户对GPM模型的理解。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从操作系统漫谈GOLang GPM模型 的相关文章

  • 服务计算hw7

    任务目标 设计一个 web 小应用 展示静态文件服务 js 请求支持 模板输出 表单处理 Filter 中间件设计等方面的能力 不需要数据库支持 基本要求 支持静态文件服务 支持简单 js 访问 提交表单 并输出一个表格 对 unknown
  • golang之跨语言ipc通信

    1 golang之跨语言ipc通信 文章目录 1 golang之跨语言ipc通信 1 1 unix domain Socket unix域套接字 介绍 1 2 IPC SOCKET通信 1 2 1 函数及地址定义介绍 1 2 2 UNIX
  • Go项目部署及所遇问题

    小聊 本次小白给大家带来Golang项目部署操作以及个人所遇问题和解决它们的方法 依然是一边实操演示一边写文稿 如遇相似问题却存有疑惑可留言 开发环境是Window 部署环境是Linux 开发工具为GoLand 部署服务器为阿里云 1 打包
  • golang: Logrus实现日志打印

    Github https github com sirupsen logrus golang标准库的日志框架非常简单 仅仅提供了print panic和fatal三个函数 对于更精细的日志级别 日志文件分割以及日志分发等方面并没有提供支持
  • Go_关键字、编译、转义字符

    关键字 关键字是指被go语言赋予了特殊含义的单词 共25个 关键字不能用于自定义名字 只能在特定语法结构中使用 break default func interface select case defer go map struct cha
  • go语言基础-----03-----流程控制、函数、值传递、引用传递、defer函数

    1 流程控制 这里只讲 for range 语句 这个关键字 主要用于遍历 用来遍历数组 slice map chan 例如 package main import fmt func main str hello world 中国 for
  • Go语言包管理(一)

    Go语言中的包 我们在使用其他语言 比如Java Python 都有类似包的概念 Go也不例外 其核心思想即为分组和模块化 人的大脑对庞大和复杂的事情很难掌控 可以对其采用分而治之的策略 使其模块化 从而更容易管理 如下是标准库中net包的
  • go-zero开发入门-API网关开发示例

    开发一个 API 网关 代理 https blog csdn net Aquester article details 134856271 中的 RPC 服务 网关完整源代码 file main go package main import
  • go-zero开发入门之gateway深入研究1

    创建一个 gateway 示例 main go package main import flag fmt gateway middleware github com zeromicro go zero core conf github co
  • go-zero 的 etcd 配置

    实现代码在 core discov config go 文件中 type EtcdConf struct Hosts string Key string ID int64 json optional User string json opt
  • go-zero开发入门-API网关鉴权开发示例

    本文是 go zero开发入门 API网关开发示例 一文的延伸 继续之前请先阅读此文 在项目根目录下创建子目录 middleware 在此目录下创建文件 auth go 内容如下 鉴权中间件 package middleware impor
  • go语言实现文件夹上传前后端代码案例

    go语言实现文件夹上传前后端代码案例 前端用于上传的测试界面 如果上传的文件夹有子文件要遍历子文件夹创建出子文件夹再进行拷贝 需要获取文件名和对应的路径 将文件的相对路径和文件对象添加到FormData中 这几行代码很关键 for let
  • 【go语言】error错误机制及自定义错误返回类型

    简介 Go 语言通过内置的 error 接口来处理错误 该接口定义如下 type error interface Error string 这意味着任何实现了 Error 方法的类型都可以作为错误类型 在 Go 中 通常使用 errors
  • go开发--操作mysql数据库

    在 Go 中访问 MySQL 数据库并进行读写操作通常需要使用第三方的 MySQL 驱动 Go 中常用的 MySQL 驱动有 github com go sql driver mysql 和 github com go xorm xorm
  • [每周一更]-(第55期):Go的interface

    参考地址 https juejin cn post 6978322067775029261 https gobyexample com interfaces https go dev tour methods 9 介绍下Go的interfa
  • Golang拼接字符串性能对比

    g o l a n g golang g o l an g
  • [每周一更]-(第55期):Go的interface

    参考地址 https juejin cn post 6978322067775029261 https gobyexample com interfaces https go dev tour methods 9 介绍下Go的interfa
  • go cannot find package “github.com/gorilla/websocket“解读

    Go无法找到包 github com gorilla websocket 的解决方案 在Go开发过程中 我们经常会依赖第三方库来简化开发工作 而使用 go get 命令安装这些库时 有时候我们可能会遇到类似于以下错误的情况 plaintex
  • 【go语言】结构体数据填充生成md错误码文件

    这里使用pongo2这个模版引擎库进行md文件渲染 GitHub flosch pongo2 Django syntax like template engine for Go package main import fmt github
  • 【go语言】AST抽象语法树详解&实践之扫描代码生成错误码文档

    背景 为了能识别出代码中抛出错误码的地址和具体的错误码值 再根据错误码文件获取到错误码的具体值和注释 方便后续的排错 这里使用AST进行语法分析获取到代码中的目标对象 一 编译过程 在开始解析代码之前先补充了解一下编译过程 编译过程是将高级

随机推荐

  • python SortedDict 遍历删除 不对

    topLevel 61 SortedDict neg 从大到小排序 for priority Id in topLevel items print 34 topLevel1 34 topLevel 将Id从topLevel中删除 topLe
  • Python字典遍历 未遍历所有元素

    不能在遍历的时候往字典中新增 删除元素 xff01 xff01 xff01 下面是我的python脚本 xff0c 它需要遍历所有具有逻辑路径和直接磁盘的物理磁盘 如果我们找到了任何逻辑路径 xff0c 那么我们得到了相应的物理磁盘 xff
  • 以太坊 事务ID txID transaction ID transaction hash怎么计算

    The transaction can then be sent to the network and will be tracked by a 256 bit transaction id This transaction can be
  • 比特币 事务ID txID transaction hash怎么计算

    A TXID Transaction ID is basically an identification number for a bitcoin transaction A TXID is always 32 bytes 64 chara
  • 使用Android studio开发jni,并实现单步调试c/c++代码

    一 环境搭建 本文讲解的是在一个现有的工程中增加JNI的支持 我们从新建一个工程说起 xff0c 本文假设你已经知道怎么设置sdk和ndk 新建工程的时候我们故意不勾选这个选项 xff0c 方便后面说明 一直默认点下一步 xff0c 直到工
  • 以太坊 分片是什么

    Ethereum Sharding An Introduction to Blockchain Sharding Alchemy Team May 18 2022 For years the question of blockchain s
  • 跨链桥——原子交换(Atomic Swaps),哈希时间锁(HTLC) 原理介绍

    什么是原子交换 xff1f xff08 Atomic swaps xff09 跨链原子交换 xff08 Atomic swaps xff09 是在两个平行链之间直接交换不同的加密货币的方法 就像用美元兑换人民币一样 xff0c 这是一个过程
  • OR-Tools|带你了解谷歌开源优化工具(Google Optimization Tools)

    转眼间暑假已经过去一大半了 xff0c 大家有没有度过一个充实的假期呢 xff1f 小编这两天可忙了 xff0c boss突然说发现了一个很有趣的开源求解器 xff1a OR Tools 经过一番了解 xff0c 小编发现它对于为解决优化问
  • 最小费用流 求解

    增广路径 匈牙利算法 二分图 https blog csdn net qq 37457202 article details 80161274 增广路径取反 xff1a 增广路上的边性质改变 xff0c 连上的变为可以连的 xff0c 可以
  • 区块链DAPP开发 以太坊智能合约框架有哪些

    一 truffle xff08 JavaScript xff09 Truffle 是一个在以太坊进行 DApp 开发的世界级开发环境 测试框架 使用 Truffle 开发有一以下优点 xff1a 内置智能合约编译 xff0c 链接 xff0
  • 区块链DAPP开发 智能合约开发工具IDE有哪些

    Remix http remix ethereum org ChainIDE https chainide cn zh CN ChainIDE提供云端编译功能 xff0c 无需繁琐的安装设置 xff0c 加速开发迭代速度 ChainIDE提
  • NFT和数字藏品的区别

    来源 xff1a 德勤 Web3 0模式分析及中国应用创新探索
  • Pycharm 增加 run 控制台缓冲行数

    找到 pycharm 安装目录的 bin 目录下 idea properties 文件 xff0c 修改 idea cycle buffer 值 xff0c 原来默认为 1024
  • python 类的定义一定要注意静态变量

    class A 静态变量 a 61 12 def init self a 成员变量 self a 61 a 静态变量通过 类名 变量名 来访问 print A a 12 成员变量通过 对象 变量名 访问的 print A 0 a 0 cla
  • python open按行读取txt 去掉\n

    加 strip 39 n 39
  • OOQP安装指南

    文章目录 1 OOQP的github链接 xff1a 2 准备工作 xff1a 3 安装OOQP xff1a 4 简单使用 xff1a 1 OOQP的github链接 xff1a ompl的官网网址为 xff1a https github
  • 海康摄像头实时显示与字符叠加详解

    1 说明 文章详细叙述了海康摄像头的两种实时显示方法 基于SDK 解码显示和基于数据流回调显示 xff0c 并且讲述了这在两种显示方法下如何往画面添加字符和图像 xff0c 最后比较了这两种方法的优劣 文章全程给以详细的程序说明 xff0c
  • Proto3序列化协议

    Proto3序列化协议 简介 对于互联网应用来说 xff0c 客户端 客户端 客户端 服务端之间需要数据的交互 xff0c 其数据传输是二进制流的方式在互联网上传输 xff0c 因为需要一种手段将数据对象编码为一种可以在网络上传输的二进制流
  • 一文读懂数据库分库分表

    阅读此文你将了解 xff1a 什么是分库分表以及为什么分库分表如何分库分表分库分表常见几种方式以及优缺点如何选择分库分表的方式 数据库常见优化方案 对于后端程序员来说 xff0c 绕不开数据库的使用与方案选型 xff0c 那么随着业务规模的
  • 从操作系统漫谈GOLang GPM模型

    前言 本文从操作系统谈起 xff0c 简单介绍操作系统基本知识 xff0c 引出进程 线程调度的基本原理和基本模型 xff0c 然后从0到1设计Golang调度器 xff0c 通过方案的逐步演进升级 xff0c 可以了解到GPM模型设计理念