几个关于 Go Runtime 的问题
问题:go 的 runtime 如何实现?(这个实在不理解)
追问:runtime 这个进程是运行在哪儿的?
追问:每个 go 进程都会有一个吗还是共用一个?(应该是共用一个)
追问:如果你运行了 go 进程,在 linux 系统里查看进程能看到 runtime 进程吗?
问题:go 的 runtime 如何实现?(这个实在不理解)
追问:runtime 这个进程是运行在哪儿的?
追问:每个 go 进程都会有一个吗还是共用一个?(应该是共用一个)
追问:如果你运行了 go 进程,在 linux 系统里查看进程能看到 runtime 进程吗?
C 的可以共享,也可以静态链接进去(
go 的就不行了,每个都自己带一份
runtime 运行在 OS 线程上,比如 pthread 。参见 https://github.com/golang/gofrontend/blob/master/libgo/runtime/go-libmain.c#L208
go 进程应该是 goroutine 吧。这个真不好回答。有很多个,但至少有一个 runtime.main,至于最多有多少个,如果 golang 实现者考虑过这个问题的话,会有一个可以设置的参数的,可以用环境变量 GOMAXPROCS 限制 OS 线程数。
比如:一个只有 http.ListenAndServe 的 pprof go 程序,有 3 个 goroutine,5 个 OS 线程。//go version go1.15.7 darwin/amd64
linux 里系统查看 go 进程是指 top 用查看 go 程序吧。top 查看到的是那个“线程池”,用 pprof 可以查看 goroutine 。
参见 https://golang.google.cn/pkg/net/http/pprof/
go runtime,严格来讲就是以 golang 下 以 goroutine 结合管道通信的并发模型,这个并发模型是基于 stackfull 的协程模型,相反的是 stackless 的协程模型,而传统其它语言可能是基于线程池以及锁的并发模型 ,有兴趣可以看 stackfull 跟 stackless 的区别,前者会占用空间,后者对内存空间友好,但是现在是 2021 年了,谁家老板买不起 2T 内存? golang 选的是 stackfull 协程模型,当然这里比较简单,下面会详细介绍,有兴趣深入还是根据我的关键字去找书看。
然后 stackfull 的本质就是 每个协程有自己的栈幁,具体我没有关注过 goroutine 的栈幁结构,但是从我观察 goroutine 进入内核态的汇编代码,做了一次 go 的调用约定 到 amd64 fastcall 的调用约定转换,基本上可以确认 golang 内部的函数调用约定以及栈幁结构 应该是平台无关化的,另外 golang 貌似是不支持二进制 lib 编译的,是不是因为内部大版本之间的 ABI 是否从来未稳定过。
然后弄这个 stackfull 的协程模型是因为 golang 这个语言的野心很大,它希望能把操作系统内核态任务调度这个事情全权交给应用态的 go 调度系统来作,而之前所有的语言 包括 C++ Java C 都是将调度模型交给操作系统提供的线程抽象,这一点上 golang 是一个伟大的进步 配合管道跟非阻塞式 IO 以及 epoll 调用,可以在用户态实现一个无锁且按需调度的调度系统,可以说 go 它是专门为后端服务设计的,当然从语法上来看,目前不支持泛型,我暂时没有加大对 go 这门语言的投入,但是它这个设计理念是好的,那就是带有运行时的语言应该从操作系统那里拿回原本属于我们应用开发人员的调度功能。
这里可以提出两点
1. 管道模型为什么优于锁 monitor ? 传统操作系统内核提供的监视器锁 存在惊群的问题,这是很难避免的,因为操作系统并不知道你要唤醒多少个线程,当然你也可以指定唤醒的数量,这里就要做到很精细化的锁唤醒操作,例如 Java 里面的 blockingQueue 当队列满了的时候 操作的线程会去 唤醒因为读等待的线程而不是去唤醒因为写等待的线程,当队列空了的时候,当前操作线程就会去唤醒因为写等待的线程而不是去唤醒因为读等待的线程,而之前我所说的这些操作,都需要对并发编程有很高的理解,你才能设计出一个多线程并发且线程安全的队列,这一点对开发人员要求很高,而且从因为监视器锁 Java 的线程会频繁进入内核 开销很大。
但如果使用管道,其实从 golang 调度器来看,当你想从一个管道读数据而挂起的时候,其实 golang 调度器只要把当前线程的几个寄存器换掉就能把对 CPU 的控制权转移到另外一个协程上,这中间无需进入内核调度,而且 golang 还可以做一些优化,把对 CPU 的控制器优先配给需要写这个管道的协程,或者把对同一个管道读写的协程都分配到一个 CPU 上,这样一来 golang 使用管道可以实现完全无锁化的协程之间的通信,但是从编写 golang 的协程代码的人来看,他的大脑负担就会少很多。
我分享的 可能广度 深度都有,
可能很多人 根本不知道 ABI monitor 监视器 栈幁 fastcall stackfuul stackless 这种东西,
你如果计算机体系结构 都了解的话,就不会产生这种疑惑了
go 进程应该是 goroutine 吧。这个真不好回答。有很多个,但至少有一个 runtime.main,至于最多有多少个,如果 golang 实现者考虑过这个问题的话,会有一个可以设置的参数的,可以用环境变量 GOMAXPROCS 限制 OS 线程数。”
————————–
请问上述内容,是不是可以认为你觉得是一个 OS 线程就有一个 runtime 呢?
而且关于 Runtime 的创建及运行过程,Google 上面几乎也搜索不到,都是在将 GMP 的调度模型等等的内容,并没有涉及到最开始的部分内容。如果你觉得可以了解到的一些博客,请推荐一下啊,谢谢大佬了。
movl $4096, %rdi
call runtime.alloc # 调用 runtime 中的垃圾回收函数
movl $1, -4($rax)
movl $2, -8($ax)
…
var a []int // 申请一个动态数组
a = append(a, 22222222)
a = append(a, 33333333)
类似这样的 golang 代码会转成上面你的汇编,另外汇编是可以调用 c 函数的,只要按照 c 调用约定就可以了。runtime.alloc 无非就是调用 c 的 malloc 函数申请一个 4096 字节的空间,然后返回指针地址给 %rax(调用约定)。
—–go tool compile -S main.go 虽然没看太懂,但是大概是这样
0x0046 00070 (main.go:5) CALL runtime.growslice(SB) #调用 runtime 函数得到一段堆内存空间
0x004b 00075 (main.go:5) MOVQ 40(SP), AX
0x0050 00080 (main.go:5) MOVQ 48(SP), CX
0x0055 00085 (main.go:5) MOVQ 56(SP), DX
0x005a 00090 (main.go:5) MOVQ $22222222, (AX) # 根据指针偏移写入到堆内存
0x0061 00097 (main.go:6) LEAQ 2(CX), BX
0x0065 00101 (main.go:6) CMPQ DX, BX
0x0068 00104 (main.go:6) JCS 125
0x006a 00106 (main.go:6) MOVQ $33333333, 8(AX)(CX*8) # 根据指针偏移写入到堆内存
所以 runtime 和你自己写的 golang 代码并没啥太大的区别。用户代码调用 runtime 中的帮助函数,函数执行完了返回。 用户代码调用 import 中的函数,函数执行完了就返回了。都一样的呗。
我说不好回答时,既要考虑你想要问什么,又要考虑我知道什么,还要考虑篇幅等。程序没有进内存前只是函数,你问“运行”当然是进内存后了,这些 runtime 函数都被 OS 线程调用执行,所以我回到 runtime 跑在 OS 线程上。但是有可能,你是想问 runtime 是在哪里调用的。不是你的函数调用 runtime,而是 runtime 调用你的函数,先执行 runtime,runtime 系统调用创建 OS 线程后,再调用 main.main 中你的函数。
gccgo 的 runtime 另当别论。
i 就 i 了吧,每个人对 runtime 这个概念理解完全不一样,但是我们一般网上交流 runtime 无非是讨论 程序语言编译后的二进制文件在运行时提供的方方面面的功能,例如 程序的栈幁 调度模式(抢占 /非抢占) 调度单位 调度策略 内存回收策略 变量的生命周期 语言元信息的反射功能, 这都算作语言的 runtime,我不知道你所说的 runtime 是什么 runtime