Skip to content

Commit 3633d2c

Browse files
committed
runtime: perform debug call injection on a new goroutine
Currently, when a debugger injects a call, that call happens on the goroutine where the debugger injected it. However, this requires significant runtime complexity that we're about to remove. To prepare for this, this CL switches to a different approach that leaves the interrupted goroutine parked and runs the debug call on a new goroutine. When the debug call returns, it resumes the original goroutine. This should be essentially transparent to debuggers. It follows the exact same call injection protocol and ensures the whole protocol executes indivisibly on a single OS thread. The only difference is that the current G and stack now change part way through the protocol. For #36365. Change-Id: I68463bfd73cbee06cfc49999606410a59dd8f653 Reviewed-on: https://go-review.googlesource.com/c/go/+/229299 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Cherry Zhang <[email protected]>
1 parent b3863fb commit 3633d2c

File tree

3 files changed

+132
-6
lines changed

3 files changed

+132
-6
lines changed

src/runtime/debugcall.go

+122-2
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,129 @@ func debugCallCheck(pc uintptr) string {
9595
return ret
9696
}
9797

98-
// debugCallWrap pushes a defer to recover from panics in debug calls
99-
// and then calls the dispatching function at PC dispatch.
98+
// debugCallWrap starts a new goroutine to run a debug call and blocks
99+
// the calling goroutine. On the goroutine, it prepares to recover
100+
// panics from the debug call, and then calls the call dispatching
101+
// function at PC dispatch.
100102
func debugCallWrap(dispatch uintptr) {
103+
var lockedm bool
104+
var lockedExt uint32
105+
callerpc := getcallerpc()
106+
gp := getg()
107+
108+
// Create a new goroutine to execute the call on. Run this on
109+
// the system stack to avoid growing our stack.
110+
systemstack(func() {
111+
var args struct {
112+
dispatch uintptr
113+
callingG *g
114+
}
115+
args.dispatch = dispatch
116+
args.callingG = gp
117+
fn := debugCallWrap1
118+
newg := newproc1(*(**funcval)(unsafe.Pointer(&fn)), unsafe.Pointer(&args), int32(unsafe.Sizeof(args)), gp, callerpc)
119+
120+
// If the current G is locked, then transfer that
121+
// locked-ness to the new goroutine.
122+
if gp.lockedm != 0 {
123+
// Save lock state to restore later.
124+
mp := gp.m
125+
if mp != gp.lockedm.ptr() {
126+
throw("inconsistent lockedm")
127+
}
128+
129+
lockedm = true
130+
lockedExt = mp.lockedExt
131+
132+
// Transfer external lock count to internal so
133+
// it can't be unlocked from the debug call.
134+
mp.lockedInt++
135+
mp.lockedExt = 0
136+
137+
mp.lockedg.set(newg)
138+
newg.lockedm.set(mp)
139+
gp.lockedm = 0
140+
}
141+
142+
// Stash newg away so we can execute it below (mcall's
143+
// closure can't capture anything).
144+
gp.schedlink.set(newg)
145+
})
146+
147+
// Switch to the new goroutine.
148+
mcall(func(gp *g) {
149+
// Get newg.
150+
newg := gp.schedlink.ptr()
151+
gp.schedlink = 0
152+
153+
// Park the calling goroutine.
154+
gp.waitreason = waitReasonDebugCall
155+
if trace.enabled {
156+
traceGoPark(traceEvGoBlock, 1)
157+
}
158+
casgstatus(gp, _Grunning, _Gwaiting)
159+
dropg()
160+
161+
// Directly execute the new goroutine. The debug
162+
// protocol will continue on the new goroutine, so
163+
// it's important we not just let the scheduler do
164+
// this or it may resume a different goroutine.
165+
execute(newg, true)
166+
})
167+
168+
// We'll resume here when the call returns.
169+
170+
// Restore locked state.
171+
if lockedm {
172+
mp := gp.m
173+
mp.lockedExt = lockedExt
174+
mp.lockedInt--
175+
mp.lockedg.set(gp)
176+
gp.lockedm.set(mp)
177+
}
178+
}
179+
180+
// debugCallWrap1 is the continuation of debugCallWrap on the callee
181+
// goroutine.
182+
func debugCallWrap1(dispatch uintptr, callingG *g) {
183+
// Dispatch call and trap panics.
184+
debugCallWrap2(dispatch)
185+
186+
// Resume the caller goroutine.
187+
getg().schedlink.set(callingG)
188+
mcall(func(gp *g) {
189+
callingG := gp.schedlink.ptr()
190+
gp.schedlink = 0
191+
192+
// Unlock this goroutine from the M if necessary. The
193+
// calling G will relock.
194+
if gp.lockedm != 0 {
195+
gp.lockedm = 0
196+
gp.m.lockedg = 0
197+
}
198+
199+
// Switch back to the calling goroutine. At some point
200+
// the scheduler will schedule us again and we'll
201+
// finish exiting.
202+
if trace.enabled {
203+
traceGoSched()
204+
}
205+
casgstatus(gp, _Grunning, _Grunnable)
206+
dropg()
207+
lock(&sched.lock)
208+
globrunqput(gp)
209+
unlock(&sched.lock)
210+
211+
if trace.enabled {
212+
traceGoUnpark(callingG, 0)
213+
}
214+
casgstatus(callingG, _Gwaiting, _Grunnable)
215+
execute(callingG, true)
216+
})
217+
}
218+
219+
func debugCallWrap2(dispatch uintptr) {
220+
// Call the dispatch function and trap panics.
101221
var dispatchF func()
102222
dispatchFV := funcval{dispatch}
103223
*(*unsafe.Pointer)(unsafe.Pointer(&dispatchF)) = noescape(unsafe.Pointer(&dispatchFV))

src/runtime/export_debug_test.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ func InjectDebugCall(gp *g, fn, args interface{}, tkill func(tid int) error, ret
4848

4949
h := new(debugCallHandler)
5050
h.gp = gp
51+
// gp may not be running right now, but we can still get the M
52+
// it will run on since it's locked.
53+
h.mp = gp.lockedm.ptr()
5154
h.fv, h.argp, h.argSize = fv, argp, argSize
5255
h.handleF = h.handle // Avoid allocating closure during signal
5356

@@ -86,6 +89,7 @@ func InjectDebugCall(gp *g, fn, args interface{}, tkill func(tid int) error, ret
8689

8790
type debugCallHandler struct {
8891
gp *g
92+
mp *m
8993
fv *funcval
9094
argp unsafe.Pointer
9195
argSize uintptr
@@ -102,8 +106,8 @@ type debugCallHandler struct {
102106
func (h *debugCallHandler) inject(info *siginfo, ctxt *sigctxt, gp2 *g) bool {
103107
switch h.gp.atomicstatus {
104108
case _Grunning:
105-
if getg().m != h.gp.m {
106-
println("trap on wrong M", getg().m, h.gp.m)
109+
if getg().m != h.mp {
110+
println("trap on wrong M", getg().m, h.mp)
107111
return false
108112
}
109113
// Push current PC on the stack.
@@ -135,8 +139,8 @@ func (h *debugCallHandler) inject(info *siginfo, ctxt *sigctxt, gp2 *g) bool {
135139

136140
func (h *debugCallHandler) handle(info *siginfo, ctxt *sigctxt, gp2 *g) bool {
137141
// Sanity check.
138-
if getg().m != h.gp.m {
139-
println("trap on wrong M", getg().m, h.gp.m)
142+
if getg().m != h.mp {
143+
println("trap on wrong M", getg().m, h.mp)
140144
return false
141145
}
142146
f := findfunc(uintptr(ctxt.rip()))

src/runtime/runtime2.go

+2
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ const (
980980
waitReasonWaitForGCCycle // "wait for GC cycle"
981981
waitReasonGCWorkerIdle // "GC worker (idle)"
982982
waitReasonPreempted // "preempted"
983+
waitReasonDebugCall // "debug call"
983984
)
984985

985986
var waitReasonStrings = [...]string{
@@ -1009,6 +1010,7 @@ var waitReasonStrings = [...]string{
10091010
waitReasonWaitForGCCycle: "wait for GC cycle",
10101011
waitReasonGCWorkerIdle: "GC worker (idle)",
10111012
waitReasonPreempted: "preempted",
1013+
waitReasonDebugCall: "debug call",
10121014
}
10131015

10141016
func (w waitReason) String() string {

0 commit comments

Comments
 (0)