Skip to content

Commit d16ec13

Browse files
committed
runtime: scan stacks conservatively at async safe points
This adds support for scanning the stack when a goroutine is stopped at an async safe point. This is not yet lit up because asyncPreempt is not yet injected, but prepares us for that. This works by conservatively scanning the registers dumped in the frame of asyncPreempt and its parent frame, which was stopped at an asynchronous safe point. Conservative scanning works by only marking words that are pointers to valid, allocated heap objects. One complication is pointers to stack objects. In this case, we can't determine if the stack object is still "allocated" or if it was freed by an earlier GC. Hence, we need to propagate the conservative-ness of scanning stack objects: if all pointers found to a stack object were found via conservative scanning, then the stack object itself needs to be scanned conservatively, since its pointers may point to dead objects. For #10958, #24543. Change-Id: I7ff84b058c37cde3de8a982da07002eaba126fd6 Reviewed-on: https://go-review.googlesource.com/c/go/+/201761 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Cherry Zhang <[email protected]>
1 parent a3ffb0d commit d16ec13

File tree

5 files changed

+217
-32
lines changed

5 files changed

+217
-32
lines changed

src/cmd/internal/objabi/funcid.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
FuncID_gopanic
3939
FuncID_panicwrap
4040
FuncID_handleAsyncEvent
41+
FuncID_asyncPreempt
4142
FuncID_wrapper // any autogenerated code (hash/eq algorithms, method wrappers, etc.)
4243
)
4344

@@ -85,6 +86,8 @@ func GetFuncID(name, file string) FuncID {
8586
return FuncID_panicwrap
8687
case "runtime.handleAsyncEvent":
8788
return FuncID_handleAsyncEvent
89+
case "runtime.asyncPreempt":
90+
return FuncID_asyncPreempt
8891
case "runtime.deferreturn":
8992
// Don't show in the call stack (used when invoking defer functions)
9093
return FuncID_wrapper

src/runtime/mgc.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ const (
139139
_ConcurrentSweep = true
140140
_FinBlockSize = 4 * 1024
141141

142+
// debugScanConservative enables debug logging for stack
143+
// frames that are scanned conservatively.
144+
debugScanConservative = false
145+
142146
// sweepMinHeapDistance is a lower bound on the heap distance
143147
// (in bytes) reserved for concurrent sweeping between GC
144148
// cycles.

src/runtime/mgcmark.go

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -757,13 +757,17 @@ func scanstack(gp *g, gcw *gcWork) {
757757
}
758758
if gp._panic != nil {
759759
// Panics are always stack allocated.
760-
state.putPtr(uintptr(unsafe.Pointer(gp._panic)))
760+
state.putPtr(uintptr(unsafe.Pointer(gp._panic)), false)
761761
}
762762

763763
// Find and scan all reachable stack objects.
764+
//
765+
// The state's pointer queue prioritizes precise pointers over
766+
// conservative pointers so that we'll prefer scanning stack
767+
// objects precisely.
764768
state.buildIndex()
765769
for {
766-
p := state.getPtr()
770+
p, conservative := state.getPtr()
767771
if p == 0 {
768772
break
769773
}
@@ -778,7 +782,13 @@ func scanstack(gp *g, gcw *gcWork) {
778782
}
779783
obj.setType(nil) // Don't scan it again.
780784
if stackTraceDebug {
781-
println(" live stkobj at", hex(state.stack.lo+uintptr(obj.off)), "of type", t.string())
785+
printlock()
786+
print(" live stkobj at", hex(state.stack.lo+uintptr(obj.off)), "of type", t.string())
787+
if conservative {
788+
print(" (conservative)")
789+
}
790+
println()
791+
printunlock()
782792
}
783793
gcdata := t.gcdata
784794
var s *mspan
@@ -796,7 +806,12 @@ func scanstack(gp *g, gcw *gcWork) {
796806
gcdata = (*byte)(unsafe.Pointer(s.startAddr))
797807
}
798808

799-
scanblock(state.stack.lo+uintptr(obj.off), t.ptrdata, gcdata, gcw, &state)
809+
b := state.stack.lo + uintptr(obj.off)
810+
if conservative {
811+
scanConservative(b, t.ptrdata, gcdata, gcw, &state)
812+
} else {
813+
scanblock(b, t.ptrdata, gcdata, gcw, &state)
814+
}
800815

801816
if s != nil {
802817
dematerializeGCProg(s)
@@ -820,7 +835,7 @@ func scanstack(gp *g, gcw *gcWork) {
820835
x.nobj = 0
821836
putempty((*workbuf)(unsafe.Pointer(x)))
822837
}
823-
if state.buf != nil || state.freeBuf != nil {
838+
if state.buf != nil || state.cbuf != nil || state.freeBuf != nil {
824839
throw("remaining pointer buffers")
825840
}
826841
}
@@ -832,6 +847,49 @@ func scanframeworker(frame *stkframe, state *stackScanState, gcw *gcWork) {
832847
print("scanframe ", funcname(frame.fn), "\n")
833848
}
834849

850+
isAsyncPreempt := frame.fn.valid() && frame.fn.funcID == funcID_asyncPreempt
851+
if state.conservative || isAsyncPreempt {
852+
if debugScanConservative {
853+
println("conservatively scanning function", funcname(frame.fn), "at PC", hex(frame.continpc))
854+
}
855+
856+
// Conservatively scan the frame. Unlike the precise
857+
// case, this includes the outgoing argument space
858+
// since we may have stopped while this function was
859+
// setting up a call.
860+
//
861+
// TODO: We could narrow this down if the compiler
862+
// produced a single map per function of stack slots
863+
// and registers that ever contain a pointer.
864+
if frame.varp != 0 {
865+
size := frame.varp - frame.sp
866+
if size > 0 {
867+
scanConservative(frame.sp, size, nil, gcw, state)
868+
}
869+
}
870+
871+
// Scan arguments to this frame.
872+
if frame.arglen != 0 {
873+
// TODO: We could pass the entry argument map
874+
// to narrow this down further.
875+
scanConservative(frame.argp, frame.arglen, nil, gcw, state)
876+
}
877+
878+
if isAsyncPreempt {
879+
// This function's frame contained the
880+
// registers for the asynchronously stopped
881+
// parent frame. Scan the parent
882+
// conservatively.
883+
state.conservative = true
884+
} else {
885+
// We only wanted to scan those two frames
886+
// conservatively. Clear the flag for future
887+
// frames.
888+
state.conservative = false
889+
}
890+
return
891+
}
892+
835893
locals, args, objs := getStackMap(frame, &state.cache, false)
836894

837895
// Scan local variables if stack frame has been allocated.
@@ -1104,7 +1162,7 @@ func scanblock(b0, n0 uintptr, ptrmask *uint8, gcw *gcWork, stk *stackScanState)
11041162
if obj, span, objIndex := findObject(p, b, i); obj != 0 {
11051163
greyobject(obj, b, i, span, gcw, objIndex)
11061164
} else if stk != nil && p >= stk.stack.lo && p < stk.stack.hi {
1107-
stk.putPtr(p)
1165+
stk.putPtr(p, false)
11081166
}
11091167
}
11101168
}
@@ -1214,6 +1272,101 @@ func scanobject(b uintptr, gcw *gcWork) {
12141272
gcw.scanWork += int64(i)
12151273
}
12161274

1275+
// scanConservative scans block [b, b+n) conservatively, treating any
1276+
// pointer-like value in the block as a pointer.
1277+
//
1278+
// If ptrmask != nil, only words that are marked in ptrmask are
1279+
// considered as potential pointers.
1280+
//
1281+
// If state != nil, it's assumed that [b, b+n) is a block in the stack
1282+
// and may contain pointers to stack objects.
1283+
func scanConservative(b, n uintptr, ptrmask *uint8, gcw *gcWork, state *stackScanState) {
1284+
if debugScanConservative {
1285+
printlock()
1286+
print("conservatively scanning [", hex(b), ",", hex(b+n), ")\n")
1287+
hexdumpWords(b, b+n, func(p uintptr) byte {
1288+
if ptrmask != nil {
1289+
word := (p - b) / sys.PtrSize
1290+
bits := *addb(ptrmask, word/8)
1291+
if (bits>>(word%8))&1 == 0 {
1292+
return '$'
1293+
}
1294+
}
1295+
1296+
val := *(*uintptr)(unsafe.Pointer(p))
1297+
if state != nil && state.stack.lo <= val && val < state.stack.hi {
1298+
return '@'
1299+
}
1300+
1301+
span := spanOfHeap(val)
1302+
if span == nil {
1303+
return ' '
1304+
}
1305+
idx := span.objIndex(val)
1306+
if span.isFree(idx) {
1307+
return ' '
1308+
}
1309+
return '*'
1310+
})
1311+
printunlock()
1312+
}
1313+
1314+
for i := uintptr(0); i < n; i += sys.PtrSize {
1315+
if ptrmask != nil {
1316+
word := i / sys.PtrSize
1317+
bits := *addb(ptrmask, word/8)
1318+
if bits == 0 {
1319+
// Skip 8 words (the loop increment will do the 8th)
1320+
//
1321+
// This must be the first time we've
1322+
// seen this word of ptrmask, so i
1323+
// must be 8-word-aligned, but check
1324+
// our reasoning just in case.
1325+
if i%(sys.PtrSize*8) != 0 {
1326+
throw("misaligned mask")
1327+
}
1328+
i += sys.PtrSize*8 - sys.PtrSize
1329+
continue
1330+
}
1331+
if (bits>>(word%8))&1 == 0 {
1332+
continue
1333+
}
1334+
}
1335+
1336+
val := *(*uintptr)(unsafe.Pointer(b + i))
1337+
1338+
// Check if val points into the stack.
1339+
if state != nil && state.stack.lo <= val && val < state.stack.hi {
1340+
// val may point to a stack object. This
1341+
// object may be dead from last cycle and
1342+
// hence may contain pointers to unallocated
1343+
// objects, but unlike heap objects we can't
1344+
// tell if it's already dead. Hence, if all
1345+
// pointers to this object are from
1346+
// conservative scanning, we have to scan it
1347+
// defensively, too.
1348+
state.putPtr(val, true)
1349+
continue
1350+
}
1351+
1352+
// Check if val points to a heap span.
1353+
span := spanOfHeap(val)
1354+
if span == nil {
1355+
continue
1356+
}
1357+
1358+
// Check if val points to an allocated object.
1359+
idx := span.objIndex(val)
1360+
if span.isFree(idx) {
1361+
continue
1362+
}
1363+
1364+
// val points to an allocated object. Mark it.
1365+
obj := span.base() + idx*span.elemsize
1366+
greyobject(obj, b, i, span, gcw, idx)
1367+
}
1368+
}
1369+
12171370
// Shade the object if it isn't already.
12181371
// The object is not nil and known to be in the heap.
12191372
// Preemption must be disabled.

src/runtime/mgcstack.go

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,23 @@ type stackScanState struct {
175175
// stack limits
176176
stack stack
177177

178+
// conservative indicates that the next frame must be scanned conservatively.
179+
// This applies only to the innermost frame at an async safe-point.
180+
conservative bool
181+
178182
// buf contains the set of possible pointers to stack objects.
179183
// Organized as a LIFO linked list of buffers.
180184
// All buffers except possibly the head buffer are full.
181185
buf *stackWorkBuf
182186
freeBuf *stackWorkBuf // keep around one free buffer for allocation hysteresis
183187

188+
// cbuf contains conservative pointers to stack objects. If
189+
// all pointers to a stack object are obtained via
190+
// conservative scanning, then the stack object may be dead
191+
// and may contain dead pointers, so it must be scanned
192+
// defensively.
193+
cbuf *stackWorkBuf
194+
184195
// list of stack objects
185196
// Objects are in increasing address order.
186197
head *stackObjectBuf
@@ -194,17 +205,21 @@ type stackScanState struct {
194205

195206
// Add p as a potential pointer to a stack object.
196207
// p must be a stack address.
197-
func (s *stackScanState) putPtr(p uintptr) {
208+
func (s *stackScanState) putPtr(p uintptr, conservative bool) {
198209
if p < s.stack.lo || p >= s.stack.hi {
199210
throw("address not a stack address")
200211
}
201-
buf := s.buf
212+
head := &s.buf
213+
if conservative {
214+
head = &s.cbuf
215+
}
216+
buf := *head
202217
if buf == nil {
203218
// Initial setup.
204219
buf = (*stackWorkBuf)(unsafe.Pointer(getempty()))
205220
buf.nobj = 0
206221
buf.next = nil
207-
s.buf = buf
222+
*head = buf
208223
} else if buf.nobj == len(buf.obj) {
209224
if s.freeBuf != nil {
210225
buf = s.freeBuf
@@ -213,39 +228,48 @@ func (s *stackScanState) putPtr(p uintptr) {
213228
buf = (*stackWorkBuf)(unsafe.Pointer(getempty()))
214229
}
215230
buf.nobj = 0
216-
buf.next = s.buf
217-
s.buf = buf
231+
buf.next = *head
232+
*head = buf
218233
}
219234
buf.obj[buf.nobj] = p
220235
buf.nobj++
221236
}
222237

223238
// Remove and return a potential pointer to a stack object.
224239
// Returns 0 if there are no more pointers available.
225-
func (s *stackScanState) getPtr() uintptr {
226-
buf := s.buf
227-
if buf == nil {
228-
// Never had any data.
229-
return 0
230-
}
231-
if buf.nobj == 0 {
232-
if s.freeBuf != nil {
233-
// Free old freeBuf.
234-
putempty((*workbuf)(unsafe.Pointer(s.freeBuf)))
235-
}
236-
// Move buf to the freeBuf.
237-
s.freeBuf = buf
238-
buf = buf.next
239-
s.buf = buf
240+
//
241+
// This prefers non-conservative pointers so we scan stack objects
242+
// precisely if there are any non-conservative pointers to them.
243+
func (s *stackScanState) getPtr() (p uintptr, conservative bool) {
244+
for _, head := range []**stackWorkBuf{&s.buf, &s.cbuf} {
245+
buf := *head
240246
if buf == nil {
241-
// No more data.
242-
putempty((*workbuf)(unsafe.Pointer(s.freeBuf)))
243-
s.freeBuf = nil
244-
return 0
247+
// Never had any data.
248+
continue
249+
}
250+
if buf.nobj == 0 {
251+
if s.freeBuf != nil {
252+
// Free old freeBuf.
253+
putempty((*workbuf)(unsafe.Pointer(s.freeBuf)))
254+
}
255+
// Move buf to the freeBuf.
256+
s.freeBuf = buf
257+
buf = buf.next
258+
*head = buf
259+
if buf == nil {
260+
// No more data in this list.
261+
continue
262+
}
245263
}
264+
buf.nobj--
265+
return buf.obj[buf.nobj], head == &s.cbuf
266+
}
267+
// No more data in either list.
268+
if s.freeBuf != nil {
269+
putempty((*workbuf)(unsafe.Pointer(s.freeBuf)))
270+
s.freeBuf = nil
246271
}
247-
buf.nobj--
248-
return buf.obj[buf.nobj]
272+
return 0, false
249273
}
250274

251275
// addObject adds a stack object at addr of type typ to the set of stack objects.

src/runtime/symtab.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ const (
255255
funcID_gopanic
256256
funcID_panicwrap
257257
funcID_handleAsyncEvent
258+
funcID_asyncPreempt
258259
funcID_wrapper // any autogenerated code (hash/eq algorithms, method wrappers, etc.)
259260
)
260261

0 commit comments

Comments
 (0)