New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
replacing runtime.KeepAlive with runtime.Pinner #1226
Conversation
c22a8c4
to
366d952
Compare
542a9db
to
147b704
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! Left comments for some small stuff. There is one subtlety which still needs to be ironed out: we're currently not pinning the "assorted" structs, aka anything that is not passed directly to a syscall. Take a look at sys.ObjInfo
for example: we allocate a second struct that is referenced by ObjGetInfoByFdAttr. That struct also needs to be pinned recursively. I think the way to do this is to change the sys.Info.info
method to take a runtime.Pinner
as well. There might be others which need this treatment.
internal/pinning.go
Outdated
@@ -27,7 +28,8 @@ func Pin(currentPath, newPath string, fd *sys.FD) error { | |||
return fmt.Errorf("%s is not on a bpf filesystem", newPath) | |||
} | |||
|
|||
defer runtime.KeepAlive(fd) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that here runtime.KeepAlive is sufficient: we want to prevent the GC from invoking the finalizer on sys.FD, but we don't care whether the value is moved since we don't pass the pointer to sys.FD
to the kernel. Instead we pass the fd number.
internal/sys/types.go
Outdated
@@ -490,18 +491,32 @@ type BtfInfo struct { | |||
KernelBtf uint32 | |||
} | |||
|
|||
func (s *BtfInfo) Pin(p *runtime.Pinner) { | |||
p.Pin(s) | |||
p.Pin(&s.Btf) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is subtle. We don't want to pin &s.Btf
which is the address of the Btf
member of this struct. Instead we need to pin the pointer it contains, which is in s.Btf.ptr
.
How about implementing a pin function on Pointer
? func (p Pointer) Pin(*runtime.Pinner)
.
prog.go
Outdated
targetID, err := findTargetInProgram(spec.AttachTarget, spec.AttachTo, spec.Type, spec.AttachType) | ||
if err != nil { | ||
return nil, fmt.Errorf("attach %s/%s: %w", spec.Type, spec.AttachType, err) | ||
} | ||
|
||
attr.AttachBtfId = targetID | ||
attr.AttachBtfObjFd = uint32(spec.AttachTarget.FD()) | ||
defer runtime.KeepAlive(spec.AttachTarget) | ||
pinner.Pin(spec.AttachTarget) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as for the other sys.FD case: KeepAlive
is appropriate here.
internal/cmd/gentypes/main.go
Outdated
switch s.ret { | ||
case retError: | ||
fmt.Fprintf(w, "func %s(attr *%s) error { _, err := BPF(%s, unsafe.Pointer(attr), unsafe.Sizeof(*attr)); return err }\n\n", s.goType, goAttrType, s.cmd) | ||
fmt.Fprintf(w, "func %s(attr *%s) error {\n"+ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: maybe it's nicer to use a multi line string for these?
@@ -624,7 +677,14 @@ type BtfLoadAttr struct { | |||
BtfLogTrueSize uint32 | |||
} | |||
|
|||
func (s *BtfLoadAttr) Pin(p *runtime.Pinner) { | |||
p.Pin(s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is missing pins for Btf
and BtfLogBuf
?
internal/cmd/gentypes/main.go
Outdated
@@ -173,6 +175,7 @@ import ( | |||
replace(btfID, "btf_id", "attach_btf_obj_id"), | |||
replace(typeID, "attach_btf_id"), | |||
}, | |||
[]string{"XlatedProgInsns", "MapIds", "LineInfo", "FuncInfo"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worry that maintaining these by hand is going to be quite error prone. How about this:
- Split
outputPatchedStruct
intopatchStruct
andoutputStruct
or similar. - Write a new function
findPointerTypes
which takes the patched result ofpatchStruct
and iterates the members to find any whereMember.Type == pointer
. ReturnMember.Name
as[]string
as now. - Feed the patched struct and the
[]string
of the pointer types tooutputStruct
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Realized there's no distinct btf pointer type so I type asserted against btf.Int
and checked if the size == 8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True that there is no generally valid pointer, but for our purposes comparing against pointer
should work fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. Maybe I am missing something here but the replace()
function takes in a pointer which is pointer := &btf.Int{Size: 8}
and the Type
field of the Member
is an interface so type asserting against btf.Pointer
doesn't match any member.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean literally comparing the variables: Member.Type == pointer
. This asserts that the variable Member.Type matches the pointer
variable. Since both are an interface the comparison boils down to types equal && pointers equal
.
Can you figure out where the extra allocation is coming from? |
Looks like the extra allocation is coming from the Edit: In much more detail, I ran a View this document for details. |
28428f8
to
5e27b28
Compare
We currently use runtime.KeepAlive to prevent Go objects from being collected by the Go GC but in some cases this is not sufficient. This is because these objects can be moved by the GC and it becomes important to pin in situations where pointers of such objects are passed into a syscall, for example. Signed-off-by: kwakubiney <kebiney@hotmail.com>
5e27b28
to
65d2ea3
Compare
Great sleuthing, seems like this is more subtle than I initially thought! Here is the syscall implementation we end up calling: https://cs.opensource.google/go/go/+/refs/tags/go1.21.4:src/syscall/syscall_linux.go;l=65 //go:uintptrkeepalive
//go:nosplit
//go:linkname Syscall
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) { From https://go.dev/src/runtime/HACKING
So the various If Go started to move memory we'd have all hell break loose:
Overall, I'm not sure any more whether Basically, I think that we're lacking guidance from the Go team on how to future proof syscall code for moving GC. If you're up for it you could write this up on an issue on the Go bug tracker asking for documentation. (We could do draft in a Gdoc for example.) |
Thanks for the links provided and very good spot with the |
Let me know if this would be interesting to you, otherwise I'll draft something at some point. |
Interested, will draft and let you know when I am done Edit: Unfortunately won't be able to address this now. @lmb |
Benchmark comparison.
Looks like the
runtime.Pin
allocations adds some extra latency to the map operation benchmarks. See benchmarks below :Without
runtime.Pinner
With
runtime.Pinner