2018-05-25
Some notes on Go's runtime.KeepAlive()
function
I was recently reading go101's "Type-Unsafe Pointers" (via)
and ran across a usage of an interesting new runtime
package function, runtime.KeepAlive()
. I was initially puzzled
by how it was used, and then me being me I had to poke into how it
worked.
What runtime.KeepAlive()
does is that it keeps a variable 'alive',
which means that it (and what it refers to) will not be garbage
collected and any finalizers it has won't be run.
The documentation has
an example of its use. My initial confusion was why the use of
runtime.KeepAlive()
was so late in the code; I had sort of expected
it to be used early, like finalizers are set, but then I realized
what it is really doing. In short, runtime.KeepAlive()
is using
the variable. A variable is obviously alive right up to the end of
its last use, so if you use a variable late, Go must keep it alive
all the way there.
At one level, there's nothing magical about runtime.KeepAlive
;
any use of the variable would do to keep it alive. At another level
there is an important bit of magic about runtime.KeepAlive
, which
is that Go guarantees that this use of your variable will not be
cleverly optimized away because the compiler can see that nothing
actually really depends on your 'use'. There are various other ways
of using a variable, but even reasonably clever ones are vulnerable
to compiler optimization and aggressively clever ones have the
downside that they may accidentally defeat Go's reasonably clever
escape analysis, forcing what would otherwise
be a local stack variable to be allocated on the heap instead.
The other special magic trick in runtime.KeepAlive()
is in how
it's implemented, which is that it doesn't actually do anything.
In particular, it doesn't make a function call. Instead, much
like unsafe.Pointer
, it's a compiler
builtin, set up in ssa.go.
When your code uses runtime.KeepAlive()
, the Go compiler just
sets up a OpKeepAlive
SSA thing and then the rest of the compiler
knows that this is a use of the variable and keeps it alive through
to that point.
(Reading this ssa.go initialization function was interesting.
Unsurprisingly, it turns out that there are a number of nominal
package function calls that are mapped directly to instructions
that will be placed inline in your code, like math.Sqrt
. Some
of these are platform-dependent, including a bunch of math.bits
.)
That runtime.KeepAlive
is special magic has one direct consequence,
which is that you can't take its address. If you try, Go will report:
./tst.go:20:22: cannot take the address of runtime.KeepAlive
I don't know if Go will too-cleverly optimize away a function that
only exists to call runtime.KeepAlive
, but hopefully you're never
going to need to call runtime.KeepAlive
indirectly.
PS: Although it's tempting to say that one should never need to
call runtime.KeepAlive
on a stack allocated local variable
(including arguments) because the stack isn't cleaned up until the
function returns, I think that this is a dangerous assumption. The
compiler could be sufficiently clever to either reuse a stack slot
for two different variables with non-overlapping lifetimes or simply
tell garbage collection that it's done with something (for example
by overwriting the pointer to the object with nil
).
There's real reasons for Linux to replace ifconfig, netstat, et al
One of the ongoing system administration controversies in Linux is
that there is an ongoing effort to obsolete the old, cross-Unix
standard network administration and diagnosis commands of ifconfig
,
netstat
and the like and replace them with fresh new Linux specific
things like ss
and the ip
suite. Old sysadmins are generally grumpy about this; they consider
it yet another sign of Linux's 'not invented here' attitude that
sees Linux breaking from well-established Unix norms to go its own
way. Although I'm an old sysadmin myself, I don't have this reaction.
Instead, I think
that it might be both sensible and honest for Linux to go off in
this direction. There are two reasons for this, one ostensible and
one subtle.
The ostensible surface issue is that the current code for netstat
,
ifconfig
, and so on operates in an inefficient way. Per various
people,
netstat
et al operate by reading various files in /proc
, and
doing this is not the most efficient thing in the world (either on
the kernel side or on netstat's side). You won't notice this on a
small system, but apparently there are real impacts on large ones. Modern commands
like ss
and ip
use Linux's netlink sockets, which are much more
efficient. In theory netstat
, ifconfig
, and company could be
rewritten to use netlink too; in practice this doesn't seem to have
happened and there may be political issues involving different groups
of developers with different opinions on which way to go.
(Netstat and ifconfig are part of net-tools, while ss
and ip
are part of iproute2.)
However, the deeper issue is the interface that netstat, ifconfig,
and company present to users. In practice, these commands are caught
between two masters. On the one hand, the information the tools
present and the questions they let us ask are deeply intertwined
with how the kernel itself does networking, and in general the tools
are very much supposed to report the kernel's reality. On the other
hand, the users expect netstat
, ifconfig
and so on to have their
traditional interface (in terms of output, command line arguments,
and so on); any number of scripts and tools fish things out of
ifconfig
output, for example. As the Linux kernel has changed how
it does networking, this has presented things like ifconfig
with
a deep conflict; their traditional output is no longer necessarily
an accurate representation of reality.
For instance, here is ifconfig
output for a network interface on
one of my machines:
; ifconfig -a [...] em0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 128.100.3.XX netmask 255.255.255.0 broadcast 128.100.3.255 inet6 fe80::6245:cbff:fea0:e8dd prefixlen 64 scopeid 0x20<link> ether 60:45:cb:a0:e8:dd txqueuelen 1000 (Ethernet) [...]
There are no other 'em0:...
' devices reported by ifconfig
, which
is unfortunate because this output from ifconfig
is not really an
accurate picture of reality:
; ip -4 addr show em0 [...] inet 128.100.3.XX/24 brd 128.100.3.255 scope global em0 valid_lft forever preferred_lft forever inet 128.100.3.YY/24 brd 128.100.3.255 scope global secondary em0 valid_lft forever preferred_lft forever
This interface has an IP alias, set up through systemd's networkd. Perhaps there once was a day when all IP
aliases on Linux had to be set up through additional alias interfaces,
which ifconfig
would show, but these days each interface can have
multiple IPs and directly setting them this way is the modern
approach.
This issue presents programs like ifconfig
with an unappealing
choice. They can maintain their traditional output, which is now
sometimes a lie but which keeps people's scripts working, or they
can change the output to better match reality and probably break
some scripts. It's likely to be the case that the more they change
their output (and arguments and so on) to match the kernel's current
reality, the more they will break scripts and tools built on top
of them. And some people will argue that those scripts and tools
that would break are already broken, just differently; if you're
parsing ifconfig
output on my machine to generate a list of all
of the local IP addresses, you're already wrong.
(If you try to keep the current interface while lying as little as
possible, you wind up having arguments about what to lie about and
how. If you can only list one IPv4 address per interface in ifconfig
,
how do you decide which one?)
In a sense, deprecating programs like ifconfig
and netstat
that
have wound up with interfaces that are inaccurate but hard to change
is the honest approach. Their interfaces can't be fixed without
significant amounts of pain and they still work okay for many
systems, so just let them be while encouraging people to switch to
other tools that can be more honest.
(This elaborates on an old tweet of mine.)
PS: I believe that the kernel interfaces that ifconfig
and so on
currently use to get this information are bound by backwards
compatibility issues themselves, so getting ifconfig
to even know
that it was being inaccurate here would probably take code changes.