Preallocate host kernel FD table before Sentry boot.

This avoids an expensive RCU stall during the Sentry boot process, when
the Linux-side FD table needs to be expanded to accommodate the Sentry's
512-sized FD table.

The kernel imposes an RCU stall when this occurs for
multithreaded programs (which all Go programs are), but not
single-threaded ones. Hence the introduction of a minimal C binary that
does the magic. There is some storytime explanation in the C source
file itself.

Even though this is an extra `exec` in the hot startup path, the
benefits are obvious:

```
runsc do /bin/true:
          │       A (baseline)       │           B (prewarmer)           │
  RunscDo             202.8m ± 3%        147.1m ± 5%  -27.45% (n=100)

docker run --rm alpine true:
                   │   A (baseline)   │        B (prewarmer)              │
  DockerRunStartup     778.9m ± 2%      738.0m ± 2%  -5.25% (p=0.000 n=100)

OCi operations ("total" = create + start + wait + delete):
            │  A (baseline)  │        B (prewarmer)               │
  OCICreate    145.6m ± 5%     102.3m ± 8%  -29.71% (n=100)
  OCIReady     165.8m ± 4%     122.6m ± 6%  -26.10% (n=100)
  OCITotal     178.6m ± 3%     138.3m ± 5%  -22.59% (n=100)
```

I also reduced the Sentry's FD table to 256 rather than 512, which saves
one or two pages of host kernel memory per sandbox instance.

Also threw in a test to make sure the C program remains
single-page-sized to avoid potential unintended bloating down the line.
This commit is contained in:
Etienne Perot
2026-08-11 17:40:26 -07:00
parent df407c89df
commit c2beb05272
9 changed files with 573 additions and 4 deletions
+11 -1
View File
@@ -27,6 +27,7 @@ import (
"github.com/moby/sys/capability"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/context"
@@ -492,7 +493,14 @@ const (
// startingStdioFD is the starting stdioFD number used during sandbox
// start and restore. This makes sure the stdioFDs are always the same
// on initial start and on restore.
startingStdioFD = 256
// It must be above the highest FD number that donation can assign at boot
// (donated FDs are numbered densely from 3, and should ideally be such
// that adding 2 to it (i.e. index stderr gets) does *not* cross a
// power of 2. See `//runsc/prewarmer/prewarmer.c` for some fun Linux
// kernel lore that explains why.
// LINT.IfChange
startingStdioFD = 253
// LINT.ThenChange(../prewarmer/prewarmer.c)
// containerSpecsKey is the key used to add and pop the container specs to the
// kernel during save/restore.
@@ -610,6 +618,7 @@ func New(args Args) (*Loader, error) {
// used.
newfd := startingStdioFD
remapStart := gtime.Now()
for _, stdioFD := range args.StdioFDs {
// Check that newfd is unused to avoid clobbering over it.
if _, err := unix.FcntlInt(uintptr(newfd), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) {
@@ -627,6 +636,7 @@ func New(args Args) (*Loader, error) {
_ = unix.Close(stdioFD)
newfd++
}
log.Infof("Remapped stdio FDs to [%d, %d] in %v", startingStdioFD, newfd-1, gtime.Since(remapStart)) // Load-bearing log message! See `//runsc/prewarmer/prewarmer_speedup_test.go` which parses this log line.
for _, goferFD := range args.GoferFDs {
l.root.goferFDs = append(l.root.goferFDs, fd.New(goferFD))
}
+4 -1
View File
@@ -73,6 +73,7 @@ const (
checkpointGoferName = "checkpointgofer"
gvisorSentryName = "gvisor_sentry"
gvisorSentryPluginStackName = "gvisor_sentry_plugin_stack"
prewarmerName = "gvisor-sentry-prewarmer"
)
// binDirName is the name of the directory holding sidecar binaries.
@@ -217,10 +218,12 @@ var (
// network stack linked in.
// Only present in plugin-enabled installations, so not part of `All`.
GvisorSentryPluginStack = Binary{Name: gvisorSentryPluginStackName}
// GvisorSentryPrewarmer is the C binary that runs ahead of `GvisorSentry`.
GvisorSentryPrewarmer = Binary{Name: prewarmerName}
)
// All lists every sidecar present in a standard installation.
var All = []*Binary{&MetricServer, &CheckpointGofer, &GvisorSentry}
var All = []*Binary{&MetricServer, &CheckpointGofer, &GvisorSentry, &GvisorSentryPrewarmer}
// Options is the set of options used to execute a sidecar binary.
type Options struct {
+58
View File
@@ -0,0 +1,58 @@
load("//tools:defs.bzl", "cc_binary", "go_binary", "go_test")
package(
default_applicable_licenses = ["//:license"],
licenses = ["notice"],
)
# gvisor-sentry-prewarmer must remain a statically-linked, freestanding (no libc),
# single-threaded C binary that fits in a single 4KiB page.
# Do NOT rewrite it in Go; it must be single-threaded.
# Read its comments as to why.
cc_binary(
name = "gvisor-sentry-prewarmer",
srcs = ["prewarmer.c"],
copts = [
"-Os",
"-ffreestanding",
"-fno-stack-protector",
"-fno-asynchronous-unwind-tables",
"-fno-unwind-tables",
],
linkopts = [
"-static",
"-nostdlib",
"-nostartfiles",
"-Wl,--build-id=none",
"-Wl,--gc-sections",
"-Wl,-s",
],
visibility = ["//visibility:public"],
)
go_test(
name = "prewarmer_single_page_test",
size = "small",
srcs = ["prewarmer_single_page_test.go"],
data = [":gvisor-sentry-prewarmer"],
deps = ["//pkg/test/testutil"],
)
go_binary(
name = "fdtable_expander",
srcs = ["fdtable_expander.go"],
pure = True,
deps = ["@org_golang_x_sys//unix:go_default_library"],
)
go_test(
name = "prewarmer_speedup_test",
size = "small",
srcs = ["prewarmer_speedup_test.go"],
data = [
":fdtable_expander",
":gvisor-sentry-prewarmer",
"//:release",
],
deps = ["//pkg/test/testutil"],
)
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Binary `fdtable_expander` successively forces the kernel to expand this
// process's FD table through the sizes that `gvisor-sentry-prewarmer` is
// meant to have already paid for:
// - 64 slots (the kernel default, should be a no-op but who knows)
// - 128
// - 256
// See `prewarmer.c` for the underlying kernel details that make this worth
// testing.
package main
import (
"fmt"
"os"
"golang.org/x/sys/unix"
)
func main() {
for _, target := range []int{63, 127, 255} {
if err := unix.Dup3(0, target, unix.O_CLOEXEC); err != nil {
fmt.Fprintf(os.Stderr, "fdtable_expander: dup3(0, %d) failed: %v\n", target, err)
os.Exit(1)
}
}
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// gvisor-sentry-prewarmer grows its host file descriptor table, then execs
// the binary at argv[1], handing it argv[2:] as its argv (so argv[2] is the
// target's argv[0]). It runs just before `runsc boot` (the Sentry) does, and
// is meant to be invoked as such, i.e.:
// `gvisor-sentry-prewarmer /path/to/gvisor_sentry runsc-sandbox <flags...> boot <flags...>`
//
// You may be asking: wtf, wat, but why? Excellent questions.
// Sit down for some deep kernel lore.
//
// We hold the following truths to be not self-evident, but true nonetheless:
//
// - FDs in Linux are reused, serially by index, but some system calls allow
// the userspace program to pick an FD number, so the table can be sparse.
// - The Linux kernel backs a process's FD table as an array with sizes that
// are always powers of 2.
// - When the table needs to grow, it grows to the smallest power of 2 that
// accommodates the largest FD number that the program needs.
// - When a program `exec`s, its FD table size is maintained, even when the FDs
// would be closed-on-exec. (Yes, this implies the FD table never shrinks as
// the genealogy of a process tree goes deeper.)
// - When the table needs to grow, if the program is a multi-threaded program,
// the kernel waits for a full RCU grace period (which is >10ms, ouch).
// However, if the process is *not* multi-threaded, there is no such wait!
// - All Go programs are multi-threaded from the kernel's perspective, because
// the Go runtime spawns off some threads on startup before `main()` is even
// called.
// - gVisor has this funny behavior of needing to segment its FD table range
// so that the stdin/stdout/stderr FDs are always remapped to some high
// indexes (see `startingStdioFD` in `//runsc/boot/loader.go`).
//
// You can see where this is going. We need a single-threaded program that
// runs before `runsc boot` does that inflates its FD table to be large enough
// so that `runsc boot` never hits the FD table expansion RCU grace period
// that the kernel would hit it with otherwise.
// That's what this prewarmer program does.
// It's written in C with very minimal dependencies and fitting in a single
// page (4KiB) because it runs on the hot path of gVisor startup. And,
// despite its existence, it makes gVisor startup faster.
#include <asm/unistd.h> // __NR_* syscall numbers for the target arch.
#include <linux/fcntl.h> // F_DUPFD_CLOEXEC, O_* flags, AT_FDCWD.
// The FD number that the boot process remaps its first stdio FD to.
// Must match `startingStdioFD` in `runsc/boot/loader.go`.
// LINT.IfChange
#define STARTING_STDIO_FD 253
// LINT.ThenChange(../boot/loader.go)
// The minimum FD number that the prewarmed fdtable must accommodate.
// Since `startingStdioFD` is mapped to stdin, then `startingStdioFD+1` is
// mapped to stdout, and `startingStdioFD+2` is mapped to stderr.
// We choose this to be just under what would cause the FD table to expand to
// its next power-of-2 size. Here, STARTING_STDIO_FD + 2 = 253 + 2 = 255,
// so the host kernel FD table is of size 256. If we were to bump this any
// further, we would needlessly waste 50% of the host kernel memory dedicated
// to the FD table (which isn't that much, but I write this because the
// previous value of this constant was committing this exact wastefulness).
#define PREWARM_MIN_FD (STARTING_STDIO_FD + 2)
// Raw 3-argument syscall function.
#if defined(__x86_64__)
static long sys3(long nr, long a0, long a1, long a2) {
long ret;
__asm__ volatile("syscall"
: "=a"(ret)
: "a"(nr), "D"(a0), "S"(a1), "d"(a2)
: "rcx", "r11", "memory");
return ret;
}
#elif defined(__aarch64__)
static long sys3(long nr, long a0, long a1, long a2) {
register long x8 __asm__("x8") = nr;
register long x0 __asm__("x0") = a0;
register long x1 __asm__("x1") = a1;
register long x2 __asm__("x2") = a2;
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2) : "memory", "cc");
return x0;
}
#else
#error "unsupported architecture"
#endif
static __attribute__((noreturn)) void sys_exit(long code) {
for (;;) {
sys3(__NR_exit_group, code, 0, 0);
}
}
// Basic stderr logging function. Look ma, no libc.
static void write_stderr(const char* s) {
long len = 0;
while (s[len] != '\0') {
len++;
}
sys3(__NR_write, 2, (long)s, len);
}
// Forces the host kernel to grow the FD table to allow the FD with index
// `PREWARM_MIN_FD` to exist without needing later FD table expansion.
static void prewarm_fdtable(void) {
// F_DUPFD_CLOEXEC atomically picks a free FD >= PREWARM_MIN_FD, so this
// cannot clobber any inherited (donated) FD.
long fd = sys3(__NR_fcntl, 0, F_DUPFD_CLOEXEC, PREWARM_MIN_FD);
if (fd < 0) {
// FD 0 may not be open. Fall back to duplicating an FD we open ourselves.
long base = sys3(__NR_openat, AT_FDCWD, (long)"/", O_RDONLY | O_CLOEXEC);
if (base < 0) {
// OK, whatever is going on is weird. We will hit the RCU stall, but not
// worth failing on.
write_stderr("gvisor-sentry-prewarmer: failed to prewarm the FD table. This slows down gVisor startup.\n");
return;
}
fd = sys3(__NR_fcntl, base, F_DUPFD_CLOEXEC, PREWARM_MIN_FD);
sys3(__NR_close, base, 0, 0);
}
if (fd >= 0) {
sys3(__NR_close, fd, 0, 0);
}
}
// Called by `_start` with a pointer to the initial process stack, which per
// the Linux ABI holds: `argc`, `argv[0..argc-1]`, NULL, `envp`, NULL.
__attribute__((noreturn, used)) void prewarmer_main(long* stack) {
long argc = stack[0];
char** argv = (char**)(stack + 1);
char** envp = argv + argc + 1;
if (argc < 3) {
write_stderr(
"gvisor-sentry-prewarmer: usage: gvisor-sentry-prewarmer <binary> <argv[0]> [argv[1:]...]\n"
"Do not run this by hand; it is exec'd by runsc ahead of the sandbox "
"process.\n");
sys_exit(1);
}
prewarm_fdtable();
// The argv array is NULL-terminated, so &argv[2] is a valid argv.
sys3(__NR_execve, (long)argv[1], (long)&argv[2], (long)envp);
write_stderr("gvisor-sentry-prewarmer: exec failed: ");
write_stderr(argv[1]);
write_stderr("\n");
sys_exit(127);
}
// Process entry point. Hand the initial stack pointer to `prewarmer_main`.
#if defined(__x86_64__)
__asm__(
".globl _start\n"
".type _start, @function\n"
"_start:\n"
" movq %rsp, %rdi\n"
" call prewarmer_main\n"
" ud2\n");
#elif defined(__aarch64__)
__asm__(
".globl _start\n"
".type _start, @function\n"
"_start:\n"
" mov x0, sp\n"
" bl prewarmer_main\n"
" brk #0\n");
#endif
@@ -0,0 +1,44 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package prewarmer_test verifies size constraints on the gvisor-sentry-prewarmer
// sidecar binary.
package prewarmer_test
import (
"os"
"testing"
"gvisor.dev/gvisor/pkg/test/testutil"
)
// TestPrewarmerFitsInOnePage verifies that the compiled gvisor-sentry-prewarmer
// binary fits in a single 4KiB page.
// This ensures its faulting cost remains as small as we can make it,
// given its trivial nature and the fact that it's on the hot-path
// of gVisor startup.
func TestPrewarmerFitsInOnePage(t *testing.T) {
const maxSize = 4096
path, err := testutil.FindFile("runsc/prewarmer/gvisor-sentry-prewarmer")
if err != nil {
t.Fatalf("cannot find gvisor-sentry-prewarmer binary: %v", err)
}
st, err := os.Stat(path)
if err != nil {
t.Fatalf("cannot stat %q: %v", path, err)
}
if st.Size() > maxSize {
t.Errorf("gvisor-sentry-prewarmer is %d bytes, which does not fit in a single %d-byte page", st.Size(), maxSize)
}
}
+231
View File
@@ -0,0 +1,231 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prewarmer_test
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"testing"
"time"
"gvisor.dev/gvisor/pkg/test/testutil"
)
// highestOpenFD returns the highest FD number currently open in this
// process.
func highestOpenFD(t *testing.T) int {
t.Helper()
entries, err := os.ReadDir("/proc/self/fd")
if err != nil {
t.Fatalf("cannot list /proc/self/fd: %v", err)
}
highest := -1
for _, e := range entries {
fd, err := strconv.Atoi(e.Name())
if err != nil {
continue
}
if fd > highest {
highest = fd
}
}
if highest == -1 {
t.Fatalf("cannot determine highest open FD")
}
return highest
}
// timedRun runs argv[0] with the given arguments and returns its wall time.
func timedRun(t *testing.T, argv ...string) time.Duration {
t.Helper()
cmd := exec.Command(argv[0], argv[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
start := time.Now()
if err := cmd.Run(); err != nil {
t.Fatalf("%v failed: %v", argv, err)
}
return time.Since(start)
}
func p50(durations []time.Duration) time.Duration {
sorted := append([]time.Duration(nil), durations...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
return sorted[len(sorted)/2]
}
// TestPrewarmerSpeedsUpFDTableExpansion verifies the assumptions about Linux
// kernel implementation details made in `prewarmer.c` which make it
// worthwhile.
// If this test fails, then either the Linux kernel or the Go runtime
// may have changed, and it is time to consider retiring `prewarmer.c`.
func TestPrewarmerSpeedsUpFDTableExpansion(t *testing.T) {
prewarmer, err := testutil.FindFile("runsc/prewarmer/gvisor-sentry-prewarmer")
if err != nil {
t.Fatalf("cannot find gvisor-sentry-prewarmer binary: %v", err)
}
expander, err := testutil.FindFile("runsc/prewarmer/fdtable_expander")
if err != nil {
t.Fatalf("cannot find fdtable_expander binary: %v", err)
}
// The children we'll spawn inherit their FD table size from the test
// process, so if we already have an FD table large enough, the prewarmer
// will be a no-op. We cannot ask the kernel to shrink the FD table, so
// all we can do is skip the test in such a case.
if fd := highestOpenFD(t); fd >= 64 {
t.Skipf("test process has FD %d open, so child processes inherit a pre-expanded FD table; this environment cannot exercise the expansion the prewarmer exists to avoid", fd)
}
// One untimed run with both binaries involved just to warm the page cache:
timedRun(t, prewarmer, expander, "fdtable_expander")
const runs = 32 // Number of runs to do.
var alone, prewarmed []time.Duration
for i := 0; i < runs; i++ {
alone = append(alone, timedRun(t, expander))
}
for i := 0; i < runs; i++ {
prewarmed = append(prewarmed, timedRun(t, prewarmer, expander, "fdtable_expander"))
}
p50Alone, p50Prewarmed := p50(alone), p50(prewarmed)
t.Logf("fdtable_expander alone: p50 = %v over %d runs", p50Alone, runs)
t.Logf("fdtable_expander with prewarmer: p50 = %v over %d runs", p50Prewarmed, runs)
if p50Prewarmed >= p50Alone {
t.Errorf("prewarming did not speed up FD table expansion: p50 with prewarmer (%v) >= p50 without (%v); either the kernel no longer stalls multi-threaded FD table expansion (and the prewarmer should be deleted), or the prewarmer is broken", p50Prewarmed, p50Alone)
}
}
// remapLogRegexp matches the timing log line emitted by the Sentry when it
// remaps its stdio FDs to high FD numbers in `loader.go`.
// Used to parse the duration out of the logs.
var remapLogRegexp = regexp.MustCompile(`Remapped stdio FDs to \[\d+, \d+\] in (\S+)`)
// runscDo runs `runsc --rootless do /bin/true` with the given sidecar
// directory and returns the stdio remap duration parsed from the Sentry's
// logs (and the whole logs too).
func runscDo(t *testing.T, runsc, sidecarDir, rootDir string) (time.Duration, string, error) {
t.Helper()
cmd := exec.Command(runsc,
"--rootless",
"--network=none",
"--root="+rootDir,
"--debug-log=/dev/stderr",
"do", "/bin/true")
cmd.Env = append(os.Environ(), "GVISOR_SIDECAR_BINARIES_DIR="+sidecarDir)
var output bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = &output
if err := cmd.Run(); err != nil {
return 0, output.String(), fmt.Errorf("runsc do failed: %w", err)
}
m := remapLogRegexp.FindStringSubmatch(output.String())
if m == nil {
return 0, output.String(), fmt.Errorf("no stdio remap timing line in runsc debug log")
}
d, err := time.ParseDuration(m[1])
if err != nil {
return 0, output.String(), fmt.Errorf("cannot parse stdio remap duration %q: %w", m[1], err)
}
return d, output.String(), nil
}
// TestPrewarmerSpeedsUpRealBoot verifies that the Linux kernel implementation
// details that the `prewarmer.c` program takes advantage of actually help
// with gVisor's startup latency in practice.
// It runs `runsc --rootless --network=none do /bin/true` and extracts the FD
// remap timing from the logs, and verifies that this is faster with the
// prewarmer in the startup path vs without the prewarmer.
func TestPrewarmerSpeedsUpRealBoot(t *testing.T) {
runsc, err := testutil.FindFile("release/runsc")
if err != nil {
t.Fatalf("cannot find runsc binary in the release layout: %v", err)
}
releaseBinDir, err := testutil.FindFile("release/gvisor-bin")
if err != nil {
t.Fatalf("cannot find gvisor-bin directory in the release layout: %v", err)
}
// Create a writable symlink-based mirror of the gvisor-bin directory,
// so that we can delete the prewarmer binary from it later.
tmp := t.TempDir()
sidecarDir := filepath.Join(tmp, "gvisor-bin")
if err := os.Mkdir(sidecarDir, 0755); err != nil {
t.Fatalf("cannot create sidecar directory: %v", err)
}
entries, err := os.ReadDir(releaseBinDir)
if err != nil {
t.Fatalf("cannot list %q: %v", releaseBinDir, err)
}
prewarmerCopy := ""
for _, e := range entries {
src := filepath.Join(releaseBinDir, e.Name())
dst := filepath.Join(sidecarDir, e.Name())
if err := os.Symlink(src, dst); err != nil {
t.Fatalf("cannot symlink %q into sidecar directory: %v", src, err)
}
if e.Name() == "gvisor-sentry-prewarmer" {
prewarmerCopy = dst
}
}
if prewarmerCopy == "" {
t.Fatalf("release gvisor-bin directory %q does not contain gvisor-sentry-prewarmer; the release is missing the prewarmer sidecar", releaseBinDir)
}
// One untimed boot to warm everything up; if this one cannot run at all,
// that is the environment's limitation, not the prewarmer's.
if _, output, err := runscDo(t, runsc, sidecarDir, filepath.Join(tmp, "root-prewarmed")); err != nil {
t.Skipf("cannot run rootless sandboxes in this environment: %v; output:\n%s", err, output)
}
const runs = 8 // Number of runs to do per side.
var prewarmed []time.Duration
for i := 0; i < runs; i++ {
d, output, err := runscDo(t, runsc, sidecarDir, filepath.Join(tmp, "root-prewarmed"))
if err != nil {
t.Fatalf("runsc do with the prewarmer failed: %v; output:\n%s", err, output)
}
prewarmed = append(prewarmed, d)
}
if err := os.Remove(prewarmerCopy); err != nil {
t.Fatalf("cannot delete prewarmer from sidecar directory: %v", err)
}
var unprewarmed []time.Duration
for i := 0; i < runs; i++ {
d, output, err := runscDo(t, runsc, sidecarDir, filepath.Join(tmp, "root-unprewarmed"))
if err != nil {
t.Fatalf("runsc do without the prewarmer failed: %v; output:\n%s", err, output)
}
unprewarmed = append(unprewarmed, d)
}
p50Prewarmed, p50Unprewarmed := p50(prewarmed), p50(unprewarmed)
t.Logf("stdio remap with prewarmer: p50 = %v over %d boots", p50Prewarmed, runs)
t.Logf("stdio remap without prewarmer: p50 = %v over %d boots", p50Unprewarmed, runs)
if p50Prewarmed >= p50Unprewarmed {
t.Errorf("prewarming did not speed up the Sentry's stdio remap: p50 with prewarmer (%v) >= p50 without (%v); either the kernel no longer stalls multi-threaded FD table expansion (and the prewarmer should be deleted), or the prewarmer is broken", p50Prewarmed, p50Unprewarmed)
}
}
+11 -2
View File
@@ -964,10 +964,19 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn
Setsid: true,
}
// Set Args[0] to make easier to spot the sandbox process. Otherwise it's
// shown as `exe`.
// Set Args[0] to make easier to spot the sandbox process.
cmd.Args[0] = "runsc-sandbox"
// If the prewarmer sidecar is available, exec it ahead of the boot binary.
// Its argv is `gvisor-prewarmer <binary> <argv[0]> [argv[1:]...]`.
if p, err := gvisorbinaries.GvisorSentryPrewarmer.Path(); err == nil {
log.Infof("Sidecar %q found: prepending Sentry boot command with %s", gvisorbinaries.GvisorSentryPrewarmer.Name, p)
cmd.Args = append([]string{p, cmd.Path}, cmd.Args[0:]...)
cmd.Path = p
} else {
log.Warningf("Sidecar %q not found or usable (%v). This slows down gVisor startup significantly.", gvisorbinaries.GvisorSentryPrewarmer.Name, err)
}
// Transfer FDs that need to be present before the "boot" command.
// Start at 3 because 0, 1, and 2 are taken by stdin/out/err.
nextFD := donations.Transfer(cmd, 3)
+1
View File
@@ -6,6 +6,7 @@ SIDECARS = {
"//runsc/checkpointgofer:checkpointgofer_binary": "checkpointgofer",
"//runsc/cmd/metricserver:runsc-metric-server": "runsc-metric-server",
"//runsc/cmd/sentry:gvisor_sentry": "gvisor_sentry",
"//runsc/prewarmer:gvisor-sentry-prewarmer": "gvisor-sentry-prewarmer",
}
def _single_file(target):