Trình điều phối Go sử dụng một luồng giám sát đặc biệt để xử lý việc chiếm quyền các goroutine chạy quá thời gian quy định. Bài viết này sẽ phân tích chi tiết cách thức thực hiện cơ chế này thông qua mã nguồn.
Luồng giám sát sysmon
Trong quá trình khởi tạo chương trình chính, hệ thống sẽ tạo ra một luồng giám sát tên là sysmon. Luồng này hoạt động độc lập trên ngăn xếp hệ thống và không cần liên kết với bất kỳ P nào:
func main() {
// ...
if GOARCH != "wasm" {
systemstack(func() {
newm(sysmon, nil, -1)
})
}
// ...
}
Hàm sysmon thực hiện chu kỳ giám sát định kỳ, trong đó gọi hàm retake để kiểm tra và xử lý các trường hợp cần chiếm quyền:
func sysmon() {
idle := 0
delay := uint32(0)
for {
if idle == 0 {
delay = 20
} else if idle > 50 {
delay *= 2
}
if delay > 10*1000 {
delay = 10 * 1000
}
usleep(delay)
if retake(now) != 0 {
idle = 0
} else {
idle++
}
}
}
Xử lý chiếm quyền goroutine chạy lâu
Hàm retake chịu trách nhiệm chính trong việc phát hiện và xử lý các goroutine chạy vượt quá thời gian cho phép:
func retake(now int64) uint32 {
n := 0
lock(&allpLock)
for i := 0; i < len(allp); i++ {
pp := allp[i]
if pp == nil {
continue
}
pd := &pp.sysmontick
s := pp.status
if s == _Prunning || s == _Psyscall {
t := int64(pp.schedtick)
if int64(pd.schedtick) != t {
pd.schedtick = uint32(t)
pd.schedwhen = now
} else if pd.schedwhen+forcePreemptNS <= now {
preemptone(pp)
sysretake = true
}
}
}
unlock(&allpLock)
return uint32(n)
}
Khi phát hiện P đang chạy goroutine vượt quá ngưỡng 10ms, hàm preemptone sẽ được gọi để thực hiện chiếm quyền:
func preemptone(pp *p) bool {
mp := pp.m.ptr()
if mp == nil || mp == getg().m {
return false
}
gp := mp.curg
if gp == nil || gp == mp.g0 {
return false
}
gp.preempt = true
gp.stackguard0 = stackPreempt
if preemptMSupported && debug.asyncpreemptoff == 0 {
pp.preempt = true
preemptM(mp)
}
return true
}
Cơ chế kiểm tra và chuyển đổi ngữ cảnh
Khi goroutine thực hiện lời gọi hàm tiếp theo, quá trình kiểm tra ngăn xếp sẽ kích hoạt cơ chế chiếm quyền:
TEXT main.gpm(SB)
cmp rsp, qword ptr [r14+0x10]
jbe 0x462350
// ... mã thực thi bình thường
call $runtime.morestack_noctxt
Vì stackguard0 đã được thiết lập thành giá trị stackPreempt (một số rất lớn), nên lệnh so sánh luôn thất bại và dẫn đến việc gọi hàm mở rộng ngăn xếp morestack_noctxt:
TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
MOVL $0, DX
JMP runtime·morestack(SB)
func newstack() {
thisg := getg()
gp := thisg.m.curg
stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
preempt := stackguard0 == stackPreempt
if preempt {
if !canPreemptM(thisg.m) {
gp.stackguard0 = gp.stack.lo + stackGuard
gogo(&gp.sched)
}
}
if preempt {
gopreempt_m(gp)
}
}
Cuối cùng, hàm gopreempt_m sẽ thực hiện chuyển đổi ngữ cảnh hoàn chỉnh:
func gopreempt_m(gp *g) {
goschedImpl(gp)
}
func goschedImpl(gp *g) {
status := readgstatus(gp)
casgstatus(gp, _Grunning, _Grunnable)
dropg()
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock)
schedule()
}
Quá trình này chuyển trạng thái goroutine từ đang chạy sang sẵn sàng, giải phóng mối liên kết giữa goroutine và thread, sau đó đưa goroutine vào hàng đợi toàn cục trước khi tiến hành lịch trình mới.