Go 程序的启动流程【1/2】

Go 程序的启动流程

本文将以一个简单的 HelloWorld 程序为例,探究 Go 程序的启动流程

package mainfunc main() {_ = "Hello World"
}

入口

我们先通过 go build . 将代码编译成可执行文件,众所周知,我们在一个 shell 中执行可执行文件时,shell 进程会启动一个子进程,并在子进程中通过 execve 系统调用启动加载器,加载器会读取可执行文件头部信息中的入口点(entry point) 字段,该字段告诉加载器应该从什么地方开始执行该可执行文件,这便是我们要找的程序入口。

不同环境的可执行文件格式不同,查看入口点的方法也不同,以最常见的 Linux 系统的 TLF 格式为例,

先将代码交叉编译成 TLS 格式

GOOS=linux GOARCH=amd64 go build -gcflags="-N -l" -o main

使用 objdump -f 便可以得到入口点地址为 0x0000000000453860

➭ objdump -f main                                       main:	file format elf64-x86-64
architecture: x86_64
start address: 0x0000000000453860

使用 -t 列出符号表并检索入口地址就可以得到 go 程序的入口地址 _rt0_amd64_linux

➭ objdump -t main | grep 453860
0000000000453860 g     F .text	0000000000000005 _rt0_amd64_linux

如果你是 Mac 系统,可执行文件格式为 Mach-O 想要查看这种文件的入口点信息,可以使用 otool 工具查看,Mach-O 中,这个信息被放在 Load Command 中,使用命令:

➭ otool -l main | awk '/entryoff/ { printf "0x%x\n", $2 }'
0x4fe80

得到入口点偏移地址为 0x4fe80 同样检索符号表可以得到入口函数 _rt0_arm64_darwin

➭ otool -t -V helloworld | grep -C 1 "4fe80"
__rt0_arm64_darwin:
000000010004fe80        adrp    x2, 0 ; 0x10004f000
000000010004fe84        add     x2, x2, #0x160

不管是 AMD 的 _rt0_amd64_linux 还是 ARM 的 _rt0_arm64_darwin 在简单的移动 argc 和 argv 之后,都会跳转到 rt0_go 函数,这就是引导 Go 程序的入口点,它主要干以下几件事:

  1. 初始化 TLS
  2. 初始化 g0 和 m0 并相互绑定
  3. 初始化命令行参数,程序执行路径
  4. 操作系统初始化
  5. 调度器初始化
  6. 启动新的 groutine 执行 main 函数

后续内容以 ARM 架构的 Mac 系统为例

初始化 TLS

TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0SUB	$32, RSPMOVW	R0, 8(RSP) // argcMOVD	R1, 16(RSP) // argv#ifdef TLS_darwin// Initialize TLS.MOVD	ZR, g // clear g, make sure it's not junk.SUB	$32, RSPMRS_TPIDR_R0AND	$~7, R0MOVD	R0, 16(RSP)             // arg2: TLS baseMOVD	$runtime·tls_g(SB), R2MOVD	R2, 8(RSP)              // arg1: &tlsgBL	·tlsinit(SB)ADD	$32, RSP
#endif

rt0_go 中会先将寄存器中的 argc 和 argv 移动到栈上,之后进入初始化 TLS 的过程。

TLS(Thread Local Storage,线程本地存储)被用来保存那些只对本线程可见的全局变量,go 会将正在系统线程上运行的 groutine 保存在 TLS 中,在实现上,一般会在内存中开辟一块专门的区域用来保存 TLS 数据,然后使用某个 CPU 寄存器保存这块内存的起始地址(AMD 架构下一般使用段寄存器 FD, 而 ARM 有一个专门的 TPIDR 寄存器来保存),线程被操作系统调度时,会保存和恢复寄存器的值,这样在任何情况下,根据寄存器中的起始地址,我们一定可以拿到专属于该线程的全局数据。

在 c++ 中,可以使用 __thread 将变量声明为 “Pre-Thread” 的

POSIX 提供了一组 API1 来操作 TLS:

// 存储 每线程 的变量
int pthread_setspecific(pthread_key_t key, const void *value);// 获取 每线程 的变量
void *pthread_getspecific(pthread_key_t key);// 生成一个 每线程 变量的 key
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

它的内部结构类似一个 uintptr 的数组,每一位都保存一个指向真实数据地址的指针,而根据 pthread_key 则可以为宜定位到该数组中的某一位。

接下来我们逐行分析这段汇编:

MOVD ZR g

这一行的作用是将 g 清空,避免脏数据。

  • ZR 是一个 62 bit 零寄存器的代称,它总是返回 0。

  • g 是 go 官方对 ARM 架构下 R10 寄存器的一个代称,可能因为他们总是使用 R10 来保存 g 的指针,因此在 go 的汇编中,直接使用 R10 是无效的,只能使用 g 指代.1

这一句翻译一下就是清空 R10 寄存器。

SUB	$32, RSP

栈指针 RSP 向下移 32 字节,为后面的函数调用申请栈空间。

MRS_TPIDR_R0
AND	$~7, R0

这实际上是一个宏,定义在 tls_arm64.h 中, 作用是读 TPIDR 寄存器的值到 R0, TPIDR 寄存器中保存的就是 TLS 的基地址:

  • #define MRS_TPIDR_R0 WORD $0xd53bd040 // MRS TPIDR_EL0, R0
    

下面的 AND 是 ARM 的特殊规定,要求基地址低 3 位必须为 0,

MOVD	R0, 16(RSP)             // arg2: TLS base
MOVD	$runtime·tls_g(SB), R2
MOVD	R2, 8(RSP)              // arg1: &tls
BL	·tlsinit(SB)

这四行的作用是调用 tlsinit 函数为 tls_g 赋值。

func tlsinit(tlsg *uintptr, tlsbase *[_PTHREAD_KEYS_MAX]uintptr) {}

这个函数接受两个参数:

  • tlsg: tlsg 的指针,来自 runtime·tls_g(SB) 这是一个全局的变量
  • tlsbase: 从寄存器中取出的 TLS 基地址

它会调用 POSIX API 在 TLS 中临时保存一个魔法值,之后遍历 TLS 空间找到这个保存这个魔法值的槽的偏移,并将其赋值给 runtime·tls_g

上面说过,POSIX 的 TLS 实现类似于一个数组,这里 runtime·tls_g 中保存的就是 g 在那个数组中的地址偏移,这样寄存器中的 TLS 基地址加上这里的地址偏移就可以定位到 g 的地址了。

ADD	$32, RSP

最后函数调用结束,释放栈。

总结一下

相对而言 ARM 初始化 TLS 的过程是比较简单的,总体就做了一件事:为 runtime·tls_g 赋值

这个全局变量保存了 g 在 tls 中的偏移,根据 tls 基地址加上偏移,我们就可以定位到保存在 TLS 中的 g 了

初始化 g0 m0

我们知道 g 是 go 对 groutine 的抽象,m 是对系统线程的抽象。

而 m0 描述的就是主线程,运行中主线程上的第一个 groutine 就是 g0.

m0 和 g0 是两个全局变量,引导时会使用汇编为这两个变量中的一些字段赋初值:

// runtime/proc.go
var (m0           mg0           g
)
MOVD	$runtime·g0(SB), g
MOVD	RSP, R7
MOVD	$(-64*1024)(R7), R0
MOVD	R0, g_stackguard0(g)
MOVD	R0, g_stackguard1(g)
MOVD	R0, (g_stack+stack_lo)(g)
MOVD	R7, (g_stack+stack_hi)(g)

这里是在初始化 g0 的栈帧:


type stack struct {lo uintptr // gorutine 栈低地址hi uintptr // groutine 栈高地址
}type g struct {stack       stack   // 标识栈的边界stackguard0 uintptr // 同于栈增长stackguard1 uintptr // TODO
}

执行完上述代码后,程序的栈空间应该如下:

        high addr┌───────────┐│           ││           ││           │├───────────┤│  argv 8   │├───────────┤
RSP   │  argc 8   │
─────►├───────────┤◄─────▲   │           │    g0.stack.ho│   │           ││   │           ││   │           ││   │           ││   │           ││   │           │
64kb  │  g0 stack ││   │           ││   │           ││   │           ││   │           ││   │           ││   │           ││   │           │     g0.stackguard1▼   │           │     g0.stackguard0
─────►├───────────┤◄─────
R7    │           │     g0.stack.lo│           ││           ││           ││           │└───────────┘low addr

初始化栈后会调用 save_g 正式将 g0 的地址存入 TLS:

nocgo:BL	runtime·save_g(SB)
TEXT runtime·save_g(SB),NOSPLIT,$0MRS_TPIDR_R0MOVD	runtime·tls_g(SB), R27MOVD	g, (R0)(R27)

和上面一样 R0 是从寄存器中获得的 TLS 基地址,R27 是来自 tls_g 的 TLS 偏移,通过 R0+R27 的间接寻址得到的就是 g0 在 TLS 中的位置

接着会调整 stackguard0 和 stackguard1 以插入栈保护区(StackGuard 默认是 928 字节)

MOVD	(g_stack+stack_lo)(g), R0
ADD	$const__StackGuard, R0
MOVD	R0, g_stackguard0(g)
MOVD	R0, g_stackguard1(g)

接下来会绑定 g0 和 m0:

	MOVD	$runtime·m0(SB), R0MOVD	g, m_g0(R0) // m0.g0 = g0MOVD	R0, g_m(g)  // g0.m = m0
type m struct {g0      *g
}

m 的 g0 字段保存了当前正在我上面执行的是哪个 groutine

而 g 结构中也有一个 m 字段用来反向查找当前 groutine 在哪个 m 上执行

总结

这一段主要做了三件事:

  1. 为 g0 申请栈空间,32 KB,其中有 928 字节保护区
  2. 将 g0 地址保存到 TLS 中
  3. 将 m0 和 g0 双向绑定

这时的内存布局为:

        high addr┌───────────┐│           ││           ││           │├───────────┤│  argv 8   │├───────────┤
RSP   │  argc 8   │
─────►├───────────┤◄─────▲   │           │    g0.stack.ho│   │           ││   │           ││   │           ││   │           ││   │           ││   │           │
64kb  │  g0 stack ││   │           ││   │           ││   │           ││   │           ││   │           │       g0.stackguard0│   │───────────│◄───── g0.stackguard1│   │ 928 byte  │     ▼   │           │  栈保护区
─────►├───────────┤◄─────
R7    │           │     g0.stack.lo│           ││           ││           ││           │└───────────┘low addr

args/osinit

TLS 初始化结束后会调用 args 对命令行参数进行初始化:

MOVW	8(RSP), R0	// copy argc
MOVW	R0, -8(RSP)
MOVD	16(RSP), R0		// copy argv
MOVD	R0, 0(RSP)
BL	runtime·args(SB)BL	runtime·osinit(SB)
BL	runtime·schedinit(SB)
var (argc int32argv **byte
)//go:linkname executablePath os.executablePath
var executablePath stringfunc args(c int32, v **byte) {argc = cargv = vsysargs(c, v)
}

值得注意的是这一步只是调用 sysargs 初始化了 executablePath 全局变量,它保存的是当前可执行文件的绝对路径,在代码中,你可以使用 os.Executable() 获取到该值。

argv 参数在程序中可以使用 os.Argv 获取,返回的是一个 []string 不过这一步并没有初始化这个值,它与 os.GetEnv() 所返回的环境变量的值一起会在调度器初始化时通过 goargsgoenvs 函数初始化。

实际上命令行参数 argv 和环境变量 envv 都是按顺序保存在栈上的(v **byte)这一步将它赋值给了全局变量 argv, 它的结构如下:

┌───────────────┬───┬────────────────────┬───┬───────────────────────────┬───┐
│               │ n │                    │ n │                           │ n │
│               │ u │                    │ u │                           │ u │
│   argv        │ l │     envv           │ l │    executablePath         │ l │
│               │ l │                    │ l │                           │ l │
│               │   │                    │   │                           │   │
└───────────────┴───┴────────────────────┴───┴───────────────────────────┴───┘

程序参数被初始化后,就会通过 osinit 初始化 cpu 数量和物理内存页大小,这两个变量在初始化内存时会被经常用到。

func osinit() {ncpu = getncpu()physPageSize = getPageSize()
}

schedinit

在获取了 CPU 和 内存的必要参数后,Go 开始调用 schedinit 初始化包括 内存分配器,Goroutine 调度器,垃圾回收器等语言层面的组件。

函数开始会对用到的锁调用 lockInit,但除非你设置 GOEXPERIMENT=staticlockranking 开启 static lock ranking 功能,否则他们都是空函数,不会做任何事情;static lock ranking 的作用是在运行时 mutex 出现死锁时报告错误,具体可以参考提交 comment 信息.

这里大部分是一些初始化函数,用来初始化堆栈分配,gc 等模块的关键数据结构,值得注意的有下面几个地方:

  1. 这里设置了 sched.maxmcount=1000

  2. mcommoninit 中将当前 m 即 m0 的 id 设置成了 -1

     mcommoninit(_g_.m, -1)
    
  3. 并且为其新初始化了一个 g 挂在 gsignal 上,实际上这个 groutine 是专门用来处理操作系统信号的,有意思的是这个 g 的栈空间是 32k 大于普通 g 的 8k

     func mpreinit(mp *m) {mp.gsignal = malg(32 * 1024) // OS X wants >= 8Kmp.gsignal.m = mp}
    
  4. mcommoninit 还将当前 m 插入了全局 m 链表 allm 的表头,事实上每个新建的 m 都会被加入到这个链表的头部,每个 m 有一个 alllink 字段指向该链表(注意:现在还在初始化,allm 本身还是 nil,所以这里直接将 m0 原子地赋值给了 allm)

     mp.alllink = allmatomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
    
  5. 调用 goargsgoenvs 初始化命令后参数和环境变量,我们在上一节中已经讲过了

  6. procresize 中根据 GOMAXPROCS 和 cpu 数量初始化所有 p:

     procs := ncpuif n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {procs = n}if procresize(procs) != nil {throw("unknown runnable goroutine during bootstrap")}
    

    这里会根据 GOMAXPROCS 和 cpu 数量 直接将 nprocs 个 p 全部初始化好并放入 allp 列表中并将 m0 和第 0 个 p(我们称为 p0)绑定:

     _g_.m.p = 0p := allp[0]p.m = 0p.status = _Pidleacquirep(p)func acquirep(_p_ *p) {wirep(_p_)// ...}func wirep(_p_ *p) {_g_ := getg()// ..._g_.m.p.set(_p_)_p_.m.set(_g_.m)_p_.status = _Prunning}
    

    可以留意一下 p 的状态流转:
    在刚被初始化好还没有与 m 绑定时,状态是 _Pidle 绑定后,状态流转为 _Prunning
    最后,所有空闲的 p(现在是 除 p0 外的所有p)放入 _Pidle 列表:

     var runnablePs *pfor i := nprocs - 1; i >= 0; i-- {p := allp[i]if _g_.m.p.ptr() == p {continue}p.status = _Pidleif runqempty(p) {pidleput(p) // 加入 _Pidle list } else {p.m.set(mget())p.link.set(runnablePs)runnablePs = p}}
    

总结一下,这一步主要是初始化了一些关键数据结构的字段,除此之外最重要的就是将 m0 加入了全局 m 链表,并切初始化了所有 p 并实现了 m0 和 p0 的绑定,至此 g0 m0 p0 全部如下图绑定和初始化结束,下一步就是启动新的 groutine 去执行 main 函数了:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

newproc

// create a new goroutine to start program
MOVD	$runtime·mainPC(SB), R0		// entry
SUB	$16, RSP
MOVD	R0, 8(RSP) // arg
MOVD	$0, 0(RSP) // dummy LR
BL	runtime·newproc(SB)
ADD	$16, RSP

这一步的作用是通过 newproc 创建一个新的 goroutine 去执行 main 函数, 它接受一个函数指针作为参数:

func newproc(fn *funcval) {gp := getg() // 这里是 g0pc := getcallerpc()systemstack(func() {newg := newproc1(fn, gp, pc)pp := getg().m.p.ptr() // p0runqput(pp, newg, true)if mainStarted {wakep()}})
}

systemstack 用于切换到系统栈执行,有关系统栈,signal 和用户栈可以参考下面 细节/stack 一节,我们这里本来就在系统栈上.

newproc1 创建一个新的 g

细节

为了逻辑通顺,上面有些细节没有介绍,放在了这里

stack

https://go.dev/src/runtime/HACKING

Every non-dead G has a user stack associated with it, which is what user Go code executes on. User stacks start small (e.g., 2K) and grow or shrink dynamically.

每个非死亡的 G(goroutine)都关联有一个用户栈(user stack),这是用户Go代码执行的地方。用户栈起始时很小(例如2KB),并且可以动态增长或缩小。

Every M has a system stack associated with it (also known as the M’s “g0” stack because it’s implemented as a stub G) and, on Unix platforms, a signal stack (also known as the M’s “gsignal” stack). System and signal stacks cannot grow, but are large enough to execute runtime and cgo code (8K in a pure Go binary; system-allocated in a cgo binary).

每个M(OS线程)都关联有一个系统栈(system stack),也被称为M的“g0”栈,因为它是作为一个存根G(stub G)实现的。在Unix平台上,M还有一个信号栈(signal stack),也被称为M的“gsignal”栈。系统栈和信号栈不能增长,但足够大以执行运行时和cgo代码(在纯Go二进制文件中为8KB;在cgo二进制文件中由系统分配)。

Runtime code often temporarily switches to the system stack using systemstack, mcall, or asmcgocall to perform tasks that must not be preempted, that must not grow the user stack, or that switch user goroutines. Code running on the system stack is implicitly non-preemptible and the garbage collector does not scan system stacks. While running on the system stack, the current user stack is not used for execution.

运行时代码经常使用 systemstack、mcall或asmcgocall临时切换到系统栈来执行不能被抢占的任务,或者不能增长用户栈的任务,或者需要切换用户goroutine的任务。在系统栈上运行的代码隐式地不可抢占,垃圾回收器不会扫描系统栈。在系统栈上运行时,当前的用户栈不用于执行。

文档描述的很清楚,在处理一些特殊任务时,需要切换到系统栈,这里我们简单看一下 systemstack 的实现,这是一个汇编函数:

参考

记录

  1. getg 是一个伪指令 https://groups.google.com/g/golang-nuts/c/KgPOzaMylHo/m/zX0GosvnAQAJ

  2. 所有的 m 通过 m.alllink 连了一个链表,有一个 allm 指针指向这个链表的头部,每次创建新的 m 都会插入到链表头部


  1. https://pubs.opengroup.org/onlinepubs/009695399/functions/pthread_key_create.html ↩︎ ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/797268.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CLoVe:在对比视觉语言模型中编码组合语言

CLoVe:在对比视觉语言模型中编码组合语言 摘要引言相关工作CLoVe: A Framework to Increase Compositionality in Contrastive VLMsSynthetic CaptionsHard NegativesModel Patching CLoVe: Encoding Compositional Language inContrastive Vision-Language Models 摘要 近年来…

记一次安服薅洞实战

记一次为数不多但还算有点收获的一次实战(平时摸鱼来着...),大致任务是对某某市某*院进行次漏洞收集和外网资产梳理且是有授权的(其实是甲方不大清楚自己外网有多少资产),漏洞质量要求还挺高。emmm本来是打…

大米自动化生产线设备:现代粮食加工的核心力量

随着科技的不断进步和粮食加工行业的快速发展,大米自动化生产线设备在现代粮食加工中的地位愈发重要。这些设备不仅大大提高了生产效率,还保证了产品的质量和安全,成为了现代粮食加工行业不可或缺的核心力量。 一、自动化生产线设备助力效率提…

【面试经典150 | 动态规划】交错字符串

文章目录 写在前面Tag题目来源解题思路方法一:动态规划 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法,两到三天更新一篇文章,欢迎催更…… 专栏内容以分析题目为主,并附带一些对于本题涉及到的数据结构等内容进行…

一些Java面试题

1、 Java语言有哪些特点 1、简单易学、有丰富的类库 2、面向对象(Java最重要的特性,让程序耦合度更低,内聚性更高) 3、与平台无关性(JVM是Java跨平台使用的根本) 4、可靠安全 5、支持多线程 2、面向对象和…

1.8.3 卷积神经网络近年来在结构设计上的主要发展和变迁——GoogleNet/inception-v1

1.8.3 卷积神经网络近年来在结构设计上的主要发展和变迁——GoogleNet/ inception-v1 前情回顾: 1.8.1 卷积神经网络近年来在结构设计上的主要发展和变迁——AlexNet 1.8.2 卷积神经网络近年来在结构设计上的主要发展和变迁——VGGNet GoogleNet问题 在VGGNet简单堆…

(2024)Ubuntu源码安装多个版本的opencv并切换使用

本人工作会用到x86_64的opencv和aarch64的opencv,所以写下来备忘自用 一、源码编译安装 依赖库安装: sudo apt-get install build-essential libgtk2.0-dev libgtk-3-dev libavcodec-dev libavformat-dev libjpeg-dev libswscale-dev libtiff5-dev o…

卷积神经网络实战

构建卷积神经网络 卷积网络中的输入和层与传统神经网络有些区别,需重新设计,训练模块基本一致 1.首先读取数据 - 分别构建训练集和测试集(验证集) - DataLoader来迭代取数据 # 定义超参数 input_size 28 #图像的总尺寸28*28…

【代码】二分法求最小值

仅适用于以下情况:区间内单调或者最多一个极小值 代码 以[0,pi]内的三角函数为例 clc clear close allx0:pi/1000:pi; ytest(x); figure() plot(x,y,.)cutnum100;x1x(1); x2x(end); error_max10^-1000;%能接受的误差上限 for i1:cutnum%这里cutnum是取值上限num(…

电池二次利用走向可持续大循环周期的潜力和挑战(第一篇)

一、背景 当前,气候变化是全球可持续发展面临的重大挑战。缓解气候变化最具挑战性的目标是在本世纪中期实现碳中和(排放量低到足以被自然系统安全吸收),其中电动汽车(EV)的引入是一项关键举措。电动汽车在…

对代理模式的理解

目录 一、前言二、案例1 代码2 自定义代理类【静态代理】2.1 一个接口多个实现,到底注入哪个依赖呢?2.1.1 Primary注解2.1.2 Resource注解(指定name属性)2.1.3 Qualifier注解 2.2 面向接口编程2.3 如果没接口咋办呢?2.…

阿里巴巴中国站获得1688商品详情 API:如何通过API接口批量获取价格、标题、图片、库存等数据

在数字化时代,数据的重要性不言而喻。对于电商从业者来说,获取商品详情数据是提升业务效率和用户体验的关键。阿里巴巴中国站作为电商行业的巨头,提供了丰富的API接口,方便开发者们批量获取商品信息。本文将详细叙述如何通过阿里巴…

C语言——详解字符函数和字符串函数(二)

Hi,铁子们好呀!之前博主给大家简单地介绍了部分字符和字符串函数,那么这次,博主将会把这些字符串函数给大家依次讲完! 今天讲的具体内容如下: 文章目录 6.strcmp函数的使用及模拟实现6.1 strcmp函数介绍和基本使用6.1.1 strcmp函…

总结:微信小程序中跨组件的通信、状态管理的方案

在微信小程序中实现跨组件通信和状态管理,有以下几种主要方案: 事件机制 通过事件机制可以实现父子组件、兄弟组件的通信。 示例: 父组件向子组件传递数据: 父组件: <child binddata"handleChildData" /> 子组件: Component({..., methods: { handleChildData(…

Linux网卡与IP地址:通往网络世界的通行证

在探索Linux网卡和IP地址的关系之前&#xff0c;我们得先理解Linux网卡是怎么工作的。想象一下&#xff0c;每台计算机都是一个世界&#x1f30e;&#xff0c;而网卡就是连接这些世界的门户&#x1f6aa;。网卡的工作就是接收和发送数据包&#xff0c;就像邮差&#x1f4ec;递送…

RabbitMQ3.13.0起支持MQTT5.0协议及MQTT5.0特性功能列表

RabbitMQ3.13.0起支持MQTT5.0协议及MQTT5.0特性功能列表 文章目录 RabbitMQ3.13.0起支持MQTT5.0协议及MQTT5.0特性功能列表1. MQTT概览2. MQTT 5.0 特性1. 特性概要2. Docker中安装RabbitMQ及启用MQTT5.0协议 3. MQTT 5.0 功能列表1. 消息过期1. 描述2. 举例3. 实现 2. 订阅标识…

洛谷 1126.机器人搬重物

思路&#xff1a;BFS 这道BFS可谓是细节爆炸&#xff0c;对于编程能力和判断条件的能力的考察非常之大。 对于这道题&#xff0c;我们还需要额外考虑一些因素&#xff0c;那就是对于障碍物的考虑和机器人方位的考虑。 首先我们看第一个问题&#xff0c;就是对于障碍物的考虑…

【洛谷】P9236 [蓝桥杯 2023 省 A] 异或和之和

题目链接 P9236 [蓝桥杯 2023 省 A] 异或和之和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 思路 1. 暴力求解 直接枚举出所有子数组&#xff0c;求每个子数组的异或和&#xff0c;再对所有的异或和求和 枚举所有子数组的时间复杂度为O&#xff08;N^2&#xff09;&…

Qt+OpenGL-part3

1-4EBO画矩形_哔哩哔哩_bilibili 可以绘制两个三角形来组成一个矩形&#xff08;OpenGL主要处理三角形&#xff09; 直接画两个三角形&#xff1a; #include "openglwidget.h" #include <QDebug>unsigned int VBO,VAO; unsigned int shaderProgram;//顶点着…

Leetcode 215. 数组中的第K个最大元素

心路历程&#xff1a; 这道题本质上是排序不完全的过程&#xff0c;而且这道题有bug&#xff0c;直接用python的排序算法其实就能AC。 可以按照快排排到找到k-1个large元素的思维去做&#xff0c;不过这道题需要考虑空间复杂度&#xff0c;所以需要用指针快排。 其实也可以考虑…