问题:go 的 runtime 如何实现?(这个实在不理解)
追问:runtime 这个进程是运行在哪儿的?
追问:每个 go 进程都会有一个吗还是共用一个?(应该是共用一个)
追问:如果你运行了 go 进程,在 linux 系统里查看进程能看到 runtime 进程吗?
1
saberlong 2021-02-04 23:09:16 +08:00 via Android
golang 的 runtime 源码在 src 可以直接找到。真正的 main 函数其实是在 runtime 里。然后你可以看到启动的步骤,包括不限于启动 g0 协程和启动 gc 工作。
每个 go 进程是自己独立的,和虚拟机区分开来。没有独立的 runtime 进程。 简单的讲,你可以理解成 c 语言加入了协程和 gc 的框架。由于有 gc,所以不能直接使用系统分配内存的函数,都是通过这个框架提供的函数来分配。 |
2
Jirajine 2021-02-04 23:11:50 +08:00 via Android
runtime 就是你编译出的程序中你自己的代码以外的部分。
比如一个 c 程序,不会直接执行 main,而是执行_start (多数平台),这个函数会先进行一些栈空间等初始化,然后再调用你的 main 函数。这个函数就是由 c 的 runtime 提供。 go 这样的语言 runtime 做的事情更多,初始化线程池、调度器、gc 、unwind 等,然后再把你的 main 函数作为一个 goroutine 启动。 |
3
saberlong 2021-02-04 23:12:34 +08:00 via Android
入口代码在 src/runtime/proc.go 。有个 func main()
|
4
PureWhiteWu 2021-02-04 23:13:15 +08:00 2
别的不说,如果这是面试,如果你都回复不知道 go runtime 怎么实现了,还追着问下面的三个问题,那这面试官妥妥 sb 。
|
5
1011 2021-02-04 23:15:57 +08:00 1
runtime 只是一个名字而已,本质只是一个“库”,go 程序编译时它将和我们编写的代码一同编译为一个可执行文件
golang.org/doc/faq#runtime |
6
codehz 2021-02-04 23:37:31 +08:00
C 也有 runtime 呢,应该去想想 crt 怎么做的(别说人家叫 libc 就不算 crt 了)
C++也有 runtime,叫 libstdc++/libc++/vcruntime C 的可以共享,也可以静态链接进去( go 的就不行了,每个都自己带一份 |
7
henglinli 2021-02-05 02:57:24 +08:00 1
golang 的 runtime 其实是个线程池,用 work-steal 调度算法调度 goroutine 。golang 的实现已知的有 go gccgo llgo,gccgo 也是 go 的官方实现就是下面的 gofrontend,llgo 已经完了。参见 https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#heading=h.nfb3viti0vlt
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/ |
8
baiyi 2021-02-05 08:49:00 +08:00
runtime 不是完全独立于你程序的一个进程。相反,你写的程序是作为 goroutine 在 runtime 中的调度器中运行。当然 runtime 也不只有调度器,还有其他 GC 什么的程序在运行。
|
9
taowen 2021-02-05 08:55:10 +08:00
https://talkgo.org/t/topic/31 讲解 golang 的协程调度实现
|
10
lewis89 2021-02-05 10:04:40 +08:00 5
@henglinli #7 我补充一点吧
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 的协程代码的人来看,他的大脑负担就会少很多。 |
11
lewis89 2021-02-05 10:04:54 +08:00
第二点 我待会下午补充
|
12
lewis89 2021-02-05 10:06:30 +08:00
对于 C10K 问题
|
13
Dongxiem OP 谢谢以上大佬们的回复
|
15
lewis89 2021-02-05 13:08:36 +08:00
@love2020 #14 没事,多搜索就好了..
我分享的 可能广度 深度都有, 可能很多人 根本不知道 ABI monitor 监视器 栈幁 fastcall stackfuul stackless 这种东西, 你如果计算机体系结构 都了解的话,就不会产生这种疑惑了 |
16
1011 2021-02-05 13:19:03 +08:00 via Android
|
17
lewis89 2021-02-05 13:29:16 +08:00 1
@1011 #16 其实也不是优雅的问题,以前线程的通信,肯定使用监视器锁去做,这样 调度 同步都是内核来完成的,应用态的语言包括我们应用程序员都是没有改造权限的,golang 做的一个事情就是把原本让内核干的事情,都夺回来来让应用层自己来干,因为操作系统其实 提供像信号这种中断机制,就类似 CPU 提供给操作系统的时钟中断一样,也就是 go 调度器 干了原本归内核的事情.. 干了之后 我们就可以自己优化策略,或者用其它的抽象 例如管道 来替代锁这种抽象
|
18
Dongxiem OP @taowen 夜读这一期是讲了调度模型,但是关于 runtime 的具体还是没有涉及到,比如上面几个问题似乎在这个视频当中找不到答案。
|
20
Dongxiem OP @henglinli
“runtime 运行在 OS 线程上,比如 pthread 。参见 https://github.com/golang/gofrontend/blob/master/libgo/runtime/go-libmain.c#L208 go 进程应该是 goroutine 吧。这个真不好回答。有很多个,但至少有一个 runtime.main,至于最多有多少个,如果 golang 实现者考虑过这个问题的话,会有一个可以设置的参数的,可以用环境变量 GOMAXPROCS 限制 OS 线程数。” -------------------------- 请问上述内容,是不是可以认为你觉得是一个 OS 线程就有一个 runtime 呢? |
21
Dongxiem OP @lewis89 是的,关于 runtime 包含的东西很多,比如你说的 channel 、调度模型、垃圾回收等,但是我还是觉得 Runtime 最开始的几个内容还是不太清晰,如 runtime 究竟运行在哪里呢?如果按照 @henglinli 的说法,是不是在 GMP 模型中 runtime 运行在每个 M 系统线程上?每个 M 都承载着一个 runtime 进行并发调度?还是说 runtime 只是运行在一个初始的系统线程 M0 上,然后再进行指挥调度所有的线程、协程等;
而且关于 Runtime 的创建及运行过程,Google 上面几乎也搜索不到,都是在将 GMP 的调度模型等等的内容,并没有涉及到最开始的部分内容。如果你觉得可以了解到的一些博客,请推荐一下啊,谢谢大佬了。 |
22
lewis89 2021-02-05 14:04:23 +08:00
@Dongxiem #21 这个你要了解 runtime 每个细节的话,只能去看源代码了, 我只是提供一些大致上的思路,因为很多东西道理是一样的,例如垃圾回收 肯定要暂停 goroutine 维持 变量之间的引用关系的一致性,不然你一边在修改变量引用关系 又进行垃圾回收 是不现实的..
|
23
weiwenhao 2021-02-05 14:07:34 +08:00
runtime.collector()
|
24
xbh1794970183564 2021-02-05 14:09:14 +08:00 via Android 1
老哥头像能发一张吗。。
|
25
weiwenhao 2021-02-05 14:12:53 +08:00
没看过源码,但是看过一个垃圾回收的教程,我觉得应该会有类似这样的实现(上面还没打完就发出去了也不能删除)
movl $4096, %rdi call runtime.alloc # 调用 runtime 中的垃圾回收函数 movl $1, -4($rax) movl $2, -8($ax) ... |
27
Dongxiem OP @lewis89 是的,这个问题的本意就是了解 runtime 的初始化、运行等底层原理,你回答的 GMP 、CSP 、GC 等内容也是 runtime 的原理,但是还是没能解决到上面一开始的几个问题啊。
|
28
Dongxiem OP @xbh1794970183564 这不知道要咋发啊。
|
29
weiwenhao 2021-02-05 14:22:51 +08:00
@weiwenhao
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) # 根据指针偏移写入到堆内存 |
30
lewis89 2021-02-05 14:24:48 +08:00 1
@Dongxiem #27 运行时的原理?我不知道你提出的问题 具体是什么,代码无非是运行在 ring3 用户态上,基本上所有操作都是要用操作系统 提供的 API,初始化? 初始化就更简单了 malloc 一个连续内存地址 就能当做协程的栈帧使用,栈幁只是一个逻辑的概念。
|
31
xbh1794970183564 2021-02-05 14:26:38 +08:00 via Android
@Dongxiem 有原链接吗。。
|
32
lewis89 2021-02-05 14:27:13 +08:00 1
@Dongxiem #27 然后调度代码的话 每一个 M 都是有自己的 线程栈幁的,调度的时候 有调度队列,不同的 M 可以去其它 M 的队列里面去偷 goroutine 执行,这可以理解吗?因为暂停 goroutine 的话 只要保存 goroutine 的协程栈幁就好了..
|
33
lewis89 2021-02-05 14:33:06 +08:00 1
@Dongxiem #27 我觉得你可能对 M 这个抽象的概念没理解清楚,每个 M 实际上就是一个操作系统线程,它自己有自己的线程栈幁,然后它拿到 goroutine 的时候 可以把 寄存器保存起来,然后把 goroutine 的协程上下文等寄存器变量换上去,然后把对 CPU 的控制权 交给 goroutine (在汇编层面上 可能就是 JMP 到 goroutine 的代码 恢复 goroutine 之前被暂停时的上下文状态) .. 然后 goroutine 运行到一定时候 例如调用方法 或者 使用网络 IO 它会自动把对 CPU (线程)的控制权 让给 M 的调度代码.. 或者 goroutine 长期占用线程不放,这个时候会有操作系统提供的信号中断机制 强行迫使 goroutine 放弃对 CPU 的控制权,然后代码会跳回到调度器 重新进行调度..
|
34
lewis89 2021-02-05 14:39:02 +08:00 1
@Dongxiem #21 你如果要完全理解 goroutine 代码 调度器代码 谁控制了线程( CPU )控制权的话,最好去读 X86 汇编跟抢占式调度原理,因为无论协程 线程都是抽象的概念,只要能保存其上下文 例如线程只要操作系统保存 RSP RBP IP 寄存器 CPU 就能在线程之间 不断来回切换,另外操作系统在进程之间切换 可能还要修改 CR3 寄存器 因为每个进程的内存地址空间不一样,但是对于协程来讲,我不清楚协程内部是如何维护栈幁的,但是原理肯定是相同的,无非是保存当前运行状态的寄存器跟各种上下文的东西 以便切换回来的时候 使用..
|
35
Dongxiem OP @xbh1794970183564 去这里找: https://medium.com/a-journey-with-go,里面很多 go 的配图,包你喜欢。
|
36
xbh1794970183564 2021-02-05 14:42:00 +08:00 via Android
@Dongxiem 感谢
|
37
lewis89 2021-02-05 14:46:07 +08:00 1
@weiwenhao #29 这个要看是用 引用计数 还是 可达性分析,如果是引用计数的话 那就是跟 python 一样,每个对象有一个引用计数器,当它变成 0 的时候 这样就能回收,如果是可达性分析,那就要判断当前程序运行时这个状态哪些对象是不能被回收的,例如我 A->B->C 三个方法调用 C 方法在执行磁盘 IO,此时 A 跟 B 的临时变量引用或者间接引用的堆上面的内存空间 你是没法标记回收的,如果标记回收了 C 方法从内核返回回来 A 跟 B 发现自己的临时变量或者对象都不见了..那就出问题了,还有一些全局变量也是不能被回收的,因为全局变量的生命周期可能跨越整个程序的运行周期。
|
40
weiwenhao 2021-02-05 15:19:07 +08:00 1
对于静态语言而言 runtime 其实就是一个帮助函数库( runtime.go )。
golang 用户代码编译成汇编,然后链接这个帮助函数库(runtime.go)。 最后得到一整个完整的二进制可执行文件。 所以 runtime 和你自己写的 golang 代码并没啥太大的区别。用户代码调用 runtime 中的帮助函数,函数执行完了返回。 用户代码调用 import 中的函数,函数执行完了就返回了。都一样的呗。 |
43
lewis89 2021-02-05 15:34:26 +08:00
@love2020 #42 没有什么深不可测的.. 你如果去读一下 linux 0.12 的代码 基本上都懂了.. 微信群..
|
44
henglinli 2021-02-05 15:47:31 +08:00 via iPhone
@Dongxiem 不是。
runtime 其实是 runtime dependence 运行时依赖。 你写了一个空的 main.main 函数,编译器套件隐式链接的那一部分就是 runtime,恰好这一部分也是 runtime 包。 在函数层面上,runtime 包里面的函数都是 runtime ;在操作系统层面上,你看到的 go 程序执行的线程都是 runtime 包里面的函数通过系统调用创建的。 你写的代码只是被 runtime.mian 调用而已,当你编译链接出一个 golang 编写的程序,runtime 包被隐式的链接进去,不需要你在代码里 import,因为所以的其他代码都依赖 runtime 包。 我说不好回答时,既要考虑你想要问什么,又要考虑我知道什么,还要考虑篇幅等。程序没有进内存前只是函数,你问“运行”当然是进内存后了,这些 runtime 函数都被 OS 线程调用执行,所以我回到 runtime 跑在 OS 线程上。但是有可能,你是想问 runtime 是在哪里调用的。不是你的函数调用 runtime,而是 runtime 调用你的函数,先执行 runtime,runtime 系统调用创建 OS 线程后,再调用 main.main 中你的函数。 gccgo 的 runtime 另当别论。 |
45
byte10 2021-02-05 15:52:42 +08:00
@1011 有啥优雅,又不能创建线程,死坑货 go 。我都明知道这行代码是阻塞的,它就是不让我创建线程跑,在协程跑,浪费时间。
|
46
janxin 2021-02-05 15:54:01 +08:00
这面试官以前用 Java 的?....
|
47
qieqie 2021-02-05 15:57:03 +08:00 1
明明主楼问的 runtime,歪到并发模型还能歪这么多楼。这里就是 i 知乎吗,i 了 i 了。
|
53
lewis89 2021-02-05 19:42:23 +08:00
@byte10 #45 为什么要创建线程,而且使用读写磁盘文件这种阻塞式 IO 的话,goroutine 封装了 syscall,会自动分离 M 跟 G,自动创建线程去应付这种阻塞式 IO,不比你手动去创建线程强 100 倍?
|
54
lewis89 2021-02-05 19:49:55 +08:00
@qieqie #47
i 就 i 了吧,每个人对 runtime 这个概念理解完全不一样,但是我们一般网上交流 runtime 无非是讨论 程序语言编译后的二进制文件在运行时提供的方方面面的功能,例如 程序的栈幁 调度模式(抢占 /非抢占) 调度单位 调度策略 内存回收策略 变量的生命周期 语言元信息的反射功能, 这都算作语言的 runtime,我不知道你所说的 runtime 是什么 runtime |