Open lzh2nix opened 4 years ago
原文: https://rakyll.org/scheduler/
关于调度这块看来好多文章,就是不知道如何下手。看这种概要性设计文章还好,但是看源码分析性的文章的时候就一头污水。想来想去还是回到之前的模式 ”看文章写总结“,暂时不去关心太底层的东西。
调度的目的就是充分利用计算机的资源,从 1.1 开始 golang 的调度器就是一个 M:N 的调度器, 这里的 N 是通过 GOMAXPROCS 指定的。 G: goroutine M: 系统线程 P: process
之前一直不太理解这里为啥叫process,已经他和cpu的核数有啥关系,看了这篇文章才发现这里的 P 只是一个虚拟的概念,理解成 M 执行的上下文环境 确实更合适一点,M 执行的时候需要获得一个P。 调度规则
steal 的原则就是随机选择一个P然后偷一半的goroutine 放到local queue里
为了避免os不断的调度 thread 带来的性能损耗(上下文切换+系统调用block的中恢复),go这边在以下几种场景里会spinning thread
原文: https://morsmachine.dk/go-scheduler
关于调度器的描述性文章都差不多,基本上就是对 GMP 的再解释。
GMP 的描述可以看前面的那篇文章
当发生系统调用的时候当前的 M 会 block 在系统调用上,他持有的P会 handoff 给空闲的M。当从系统调用返回的时候,先看能不能从其他M偷一个P过来, 如果能获得一个P就将G放到这个P的local queue里,否则就将 G 放到 global queue里去。
这也是 go 需要多个os thread的原因,有些 M 处在等待任务的状态,如果某个 M 由于系统调用而block了, 立马接手该 M 持有的 P。
原文: https://go101.org/article/memory-layout.html
字节对齐这块go还是继承了c很像。语言层面并没有定义如何去对齐,都是编译器完成的。
go中个类型占用空间的大小(字节) | type | size in bytes |
---|---|---|
byte, uint8, int8 | 1 | |
uint16, int16 | 2 | |
uint32, int32, float32 | 4 | |
uint64, int64 | 8 | |
float64, complex64 | 8 | |
complex128 | 16 | |
uint, int | implementation-specific,generally 4 on 32-bit architectures, and 8 on 64-bit architectures. | |
uintptr | implementation-specific,large enough to store the uninterpreted bits of a pointer value. | 在32位系统中4字节对齐, 在64位系统中8字节对齐,字节对齐的目的就是保证struct占用的内存大小是4(32位系统)或者8(64位系统)的倍数 |
可以通过unsafe.Alignof(t)
来获取struct是几字节对齐。比如:
package main
import (
"fmt"
"unsafe"
)
type T1 struct {
a int8
b int64
c int16
}
// 8字节对齐系统中 24=(1+7+8+2+6), 其中7,2都是为了对齐而做的填充
// 8字节对齐系统中 16=(1+3+8+2+2)
func main() {
var t T1
fmt.Println(unsafe.Alignof(t))
}
原文: https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html
另外一篇介绍golang shedule的文章,个人觉得各种调度讲的最清楚的要数国人写的 https://studygolang.com/articles/26921这篇文章。
goroutine的三个状态: Waiting 等待一些被block的东西结束(syscall, mutex...) Runnable 放到GRQ/LRQ里的goroutine Executing 执行中
几个改变调度的行为:
异步的系统调用(尤其是和网络相关的)会交给network poller去完成(依赖操作系统的异步io模型), 当前的M会去执行当前P的LRQ里的其他goroutine,如果异步io完成了就继续放到P的LRQ里等等执行。
原文: https://syslog.ravelin.com/anatomy-of-a-function-call-in-go-f6fc81b80ecc
简单介绍一下golang中函数调用相关的汇编过程。每个go routine都有自己的栈空间。在每次发生函数调用是通过当前SP(stack point)-需要的空间来给调用的函数分配栈空间。函数结束之后回收栈空间
0x8(SP) = sp地址+8 的内存地址
package main
func add(x, y int64) int64 {
return x + y
}
func main() {
add(int64(1), int64(2))
}
go build -gcflags '-N -l' .
go tool objdump -s main go-assmembly
得到的汇编代码如下(只保留main和add部分的代码):
TEXT main.add(SB) /home/qx/code/golang/go-assmembly/add.go
add.go:3 0x45da40 48c744241800000000 MOVQ $0x0, 0x18(SP) // 返回值默认为0
add.go:4 0x45da49 488b442408 MOVQ 0x8(SP), AX // AX = a
add.go:4 0x45da4e 4803442410 ADDQ 0x10(SP), AX // Ax = Ax + b
add.go:4 0x45da53 4889442418 MOVQ AX, 0x18(SP) // SP+0x18 = AX
add.go:4 0x45da58 c3 RET
TEXT main.main(SB) /home/qx/code/golang/go-assmembly/add.go
add.go:7 0x45da60 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX
add.go:7 0x45da69 483b6110 CMPQ 0x10(CX), SP
add.go:7 0x45da6d 7631 JBE 0x45daa0
add.go:7 0x45da6f 4883ec20 SUBQ $0x20, SP // 开辟4 byte的空间给新的函数(2个参数+1个返回值+保存BP寄存器)
add.go:7 0x45da73 48896c2418 MOVQ BP, 0x18(SP) // 保护当前的BP寄存器
add.go:7 0x45da78 488d6c2418 LEAQ 0x18(SP), BP
add.go:8 0x45da7d 48c7042401000000 MOVQ $0x1, 0(SP) // 参数a赋值为1(已经在函数参数栈上了)
add.go:8 0x45da85 48c744240802000000 MOVQ $0x2, 0x8(SP) // 参数a赋值为2(已经在函数参数栈上了)
add.go:8 0x45da8e e8adffffff CALL main.add(SB) // 调用add
add.go:9 0x45da93 488b6c2418 MOVQ 0x18(SP), BP // 恢复BP寄存器
add.go:9 0x45da98 4883c420 ADDQ $0x20, SP // 释放栈
add.go:9 0x45da9c c3 RET
add.go:7 0x45da9d 0f1f00 NOPL 0(AX)
add.go:7 0x45daa0 e8dbaeffff CALL runtime.morestack_noctxt(SB)
add.go:7 0x45daa5 ebb9 JMP main.main(SB)
这里 CALL main.add(SB)
需要注意一下,CALL 指令回将SP寄存器会下移一个WORD(x86_64上是8byte),然后进入新函数的栈空间运行,这也是在add中main中两个参数放到SP+0, SP+8的位置,但是在add函数中从SP+8和SP+0x10中取的原因。
package main
func add10(x int64) int64 {
y := int64(10)
return x + y
}
func main() {
add10(int64(10))
}
对应的汇编代码(这里之帖了add10相关的代码)
//这里在函数add10中又会为内部变量开辟栈空间
TEXT main.add10(SB) /home/qx/code/golang/go-assmembly/add.go
add.go:3 0x45da40 4883ec10 SUBQ $0x10, SP // 为内部变量开辟栈空间(变量+BP)
add.go:3 0x45da44 48896c2408 MOVQ BP, 0x8(SP) // 保存BP
add.go:3 0x45da49 488d6c2408 LEAQ 0x8(SP), BP
add.go:3 0x45da4e 48c744242000000000 MOVQ $0x0, 0x20(SP) //初始化返回值
add.go:4 0x45da57 48c704240a000000 MOVQ $0xa, 0(SP) // y 放到SP+0处
add.go:5 0x45da5f 488b442418 MOVQ 0x18(SP), AX // AX = x(SP+0x18)
add.go:5 0x45da64 4883c00a ADDQ $0xa, AX // AX = AX+10(其实这里经过优化之后y变量直接没有用到)
add.go:5 0x45da68 4889442420 MOVQ AX, 0x20(SP) // 返回值放到 SP+0x20
add.go:5 0x45da6d 488b6c2408 MOVQ 0x8(SP), BP // 恢复 BP
add.go:5 0x45da72 4883c410 ADDQ $0x10, SP // 恢复 SP
add.go:5 0x45da76 c3 RET
对于函数调用首先在使用沾来传递函数参数,在被调用函数中如果没有局部变量就直接使用寄存器,否则需要再分配栈空间。
原文: https://www.ardanlabs.com/blog/2015/02/scheduler-tracing-in-go.html
golang 提供了强大的debug,连他的scheduler 都可以trace,而且trace又是超级方便的。
package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go work(&wg)
}
wg.Wait()
time.Sleep(3 * time.Second)
}
func work(wg *sync.WaitGroup) {
time.Sleep(time.Second)
var counter int
for i := 0; i < 1e10; i++ {
counter++
}
wg.Done()
}
只需要执行的时候加上 GOMAXPROCS=1 GODEBUG=schedtrace=1000
既可以进行trace
➜ GOMAXPROCS=8 GODEBUG=schedtrace=1000 ./sche
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0]
SCHED 1004ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=0 [50 1 13 16 0 1 6 5]
SCHED 2011ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=55 [1 9 10 1 9 0 0 7]
SCHED 3018ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=76 [1 1 5 2 3 2 2 0]
SCHED 4024ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=63 [5 2 3 5 4 3 3 4]
SCHED 5028ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=58 [1 5 5 6 6 6 5 0]
SCHED 6035ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=57 [6 7 6 7 2 7 0 0]
SCHED 7041ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=62 [0 2 0 0 3 8 8 9]
SCHED 8048ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=74 [2 3 2 1 3 1 3 3]
SCHED 9054ms: gomaxprocs=8 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=62 [6 3 1 4 2 6 5 3]
通过这个可以观察到goroutine的调度情况,以下是每个字段的解析: 进一步每个goroutine的执行情况和通过go tool trace查看https://github.com/lzh2nix/articles/issues/77#issuecomment-683261463 trace章节
原文: https://blog.gopheracademy.com/advent-2017/unsafe-pointer-and-system-calls/
正常业务代码中确实很少遇到unsafe包,所以对我来说这也是最神秘的一个包。 就和他名字一样,这里的用法都是不安全的,所以使用起来需要谨慎一点,感觉和c中的void *强转有点像。转换需要遵守以下的原则:
这篇文章作者也举了类型转换和系统调用两个例子,里面的各种转换看着还是挺复杂(感觉又回到了c中括号对齐的年代)
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
解析过程:
有了上面的例子下面这个就看起来很好理解:
// Verify that the byte slice containing a unix.Taskstats is the
// size expected by this package, so we don't blindly cast the
// byte slice into a structure of the wrong size.
const sizeofTaskstats = int(unsafe.Sizeof(unix.Taskstats{}))
if want, got := sizeofTaskstats, len(buf); want != got {
return nil, fmt.Errorf("unexpected taskstats structure size, want %d, got %d", want, got)
}
stats := *(*unix.Taskstats)(unsafe.Pointer(&buf[0]))
在发生系统调用的时候需要将go的数据结构转换成系统调用对应的类型:
// Context ID is populated by Ioctl.
var cid uint32
// Retrieve the context ID of this machine from /dev/vsock.
Ioctl(f.Fd(), unix.IOCTL_VM_SOCKETS_GET_LOCAL_CID, unsafe.Pointer(&cid))
可以同时看下ioctl的文档 这里就是把cid的地址传递给ioctl
unsafe 能使用的一个最大原因是不是 go 和c中的内存对齐原则是一样的???
目录
Go's work-stealing scheduler(2020.09.09) The Go scheduler(2020.09.09) Address Alignments in Go(2020.09.10) The Go scheduler II(2020.09.10) Anatomy of a function call in Go(2020.09.12) Scheduler Tracing in Go(2020.09.12) unsafe.Pointer and system calls(2020.09.12)