Files
zeekayandHanzo Dev 3da05638df State the ZAP wire of every urpc payload
urpc encodes with encoding/json, which walks the payload on every call: a
realistic control.ExecArgs costs 7084ns and 10 allocations to cross. ZAP
reads a field at a constant offset out of the buffer that arrived, so the
same payload is 740ns and one allocation -- but only if the offsets are
stated, and a stated offset that is wrong is silent corruption rather than
a compile error. So state them from the types themselves.

//tools/zap reflects over the payloads at build time and writes the wire
as Go source with the offsets as constants. It lives inside Bazel because
pkg/sentry/control cannot be imported from outside it: the *_go_proto
packages it depends on have no source in the tree.

Six payloads had no layout to state. A map, an interface and a type from
another module have no offsets, so each is answered in the type:

  SaveOpts.Metadata and StartContainerArgs.InitialCgroups/Limits cross as
  lists of pairs; boot.Route.Destination restates net.IPNet as Subnet;
  OpenMountArgs restates the OCI mount fields the call actually uses;
  StartArgs.Spec and SinkConfig.Config carry their documents as the bytes
  they arrived as; MetricsRegistrationResponse carries the protocol
  buffer the same way; and seccheck's session options become a type
  instead of a map of any.

ExecArgs and MetricsExportData still have none: ExecArgs holds live
kernel objects behind an interface, and MetricsExportData reaches a
prometheus snapshot whose labels are a map.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-22 23:10:33 -07:00

739 lines
25 KiB
Go

// Copyright 2023 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 boot
import (
"fmt"
"io"
"os"
"strconv"
"strings"
time2 "time"
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/devutil"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/checkpoint"
"gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/devices/nvproxy/nvconf"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/host"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/proc"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/pgalloc"
"gvisor.dev/gvisor/pkg/sentry/state"
"gvisor.dev/gvisor/pkg/sentry/time"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/sentry/watchdog"
"gvisor.dev/gvisor/pkg/state/statefile"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/timing"
"gvisor.dev/gvisor/pkg/urpc"
"gvisor.dev/gvisor/runsc/boot/pprof"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/specutils"
"gvisor.dev/gvisor/runsc/starttime"
"gvisor.dev/gvisor/runsc/version"
)
const (
// VersionKey is the key used to save runsc version in the save metadata and compare
// it across checkpoint restore.
VersionKey = "runsc_version"
// ContainerCountKey is the key used to save number of containers in the save metadata.
ContainerCountKey = "container_count"
// ContainerSpecsKey is the key used to add and pop the container specs to the
// metadata during save/restore.
ContainerSpecsKey = "container_specs"
annotationCheckpointPrefix = "dev.gvisor.internal.checkpoint."
// annotationCheckpointPath is the path to the directory where the checkpoint files will be
// created. When present, it allows for the workload running inside to trigger a checkpoint
// without having to use the runsc CLI.
annotationCheckpointPath = annotationCheckpointPrefix + "path"
// annotationCheckpointResume indicates whether the sandbox should continue running after the
// checkpoint. Optional, defaults to false.
annotationCheckpointResume = annotationCheckpointPrefix + "resume"
// annotationCheckpointCompression is the compression to use for the checkpoint file. Optional,
// defaults to best speed compression.
annotationCheckpointCompression = annotationCheckpointPrefix + "compression"
// annotationCheckpointDirect indicates whether the checkpoint IOs should use O_DIRECT. Optional,
// defaults to false.
annotationCheckpointDirect = annotationCheckpointPrefix + "direct"
// annotationCheckpointExcludeCommittedZeroPages indicates whether the checkpoint should exclude
// committed zero pages. Optional, defaults to false.
annotationCheckpointExcludeCommittedZeroPages = annotationCheckpointPrefix + "exclude-committed-zero-pages"
// annotationCheckpointCudaCheckpointPath is the path to the cuda-checkpoint binary. It's required
// if the workload has CUDA processes.
annotationCheckpointCudaCheckpointPath = annotationCheckpointPrefix + "cuda-checkpoint-path"
// annotationCheckpointCudaCheckpointSequential indicates whether cuda-checkpoint should be run
// sequentially. Optional, defaults to false.
annotationCheckpointCudaCheckpointSequential = annotationCheckpointPrefix + "cuda-checkpoint-sequential"
// annotationCheckpointEnable indicates whether files under /proc/gvisor should be present in
// the container to allow the workload to trigger a checkpoint.
annotationCheckpointEnable = annotationCheckpointPrefix + "enable"
// annotationSaveRestoreExecArgv is the argv to use for the save/restore exec
// binary.
annotationSaveRestoreExecArgv = annotationCheckpointPrefix + "save-restore-exec-argv"
// annotationSaveRestoreExecTimeout is the timeout to use for the save/restore
// exec binary.
annotationSaveRestoreExecTimeout = annotationCheckpointPrefix + "save-restore-exec-timeout"
networkKey = "network"
)
// GetAnnotationCheckpointPath returns the checkpoint path specified in the
// container annotation. Return empty string if no annotation is specified.
func GetAnnotationCheckpointPath(conf *config.Config, spec *specs.Spec) (string, error) {
path := spec.Annotations[annotationCheckpointPath]
if len(path) != 0 {
if len(conf.TestOnlyAutosaveImagePath) != 0 {
return "", fmt.Errorf("autosave is not supported with %q annotation", annotationCheckpointPath)
}
}
return path, nil
}
// GetAnnotationCheckpointCompression returns the checkpoint compression level
// specified in the container annotation.
func GetAnnotationCheckpointCompression(spec *specs.Spec) (statefile.CompressionLevel, error) {
return statefile.CompressionLevelFromString(spec.Annotations[annotationCheckpointCompression])
}
// GetAnnotationCheckpointDirect returns true if the checkpoint is direct.
func GetAnnotationCheckpointDirect(spec *specs.Spec) bool {
return specutils.AnnotationToBool(spec, annotationCheckpointDirect)
}
// SaveAsync starts a goroutine to save the kernel. Implements kernel.Saver.
func (l *Loader) SaveAsync() (err error) {
l.mu.Lock()
defer l.mu.Unlock()
cu := cleanup.Make(func() {
// Save failed, unblock the callers as the workload will resume.
l.k.OnCheckpointAttempt(err)
})
defer cu.Clean()
// Save either not configured or already done.
if len(l.saveFDs) == 0 {
return linuxerr.ENXIO
}
o, err := saveOptsFromSpec(l.root.spec, l.saveFDs, l.saveCheckpointGofer)
if err != nil {
return err
}
// Close all FDs and set saveFDs to nil to mark that save has already
// been triggered. So further attempts to save won't reuse and corrupt the files.
for _, fd := range l.saveFDs {
_ = fd.Close()
}
l.saveFDs = nil
go func() {
_ = l.save(o)
}()
// Loader.save() takes over the responsibility of calling OnCheckpointAttempt() when
// it completes.
cu.Release()
return nil
}
// saveOptsFromSpec returns the saveOpts based on annotations from the spec. `fds` are
// no longer needed and can be closed after this is called.
func saveOptsFromSpec(spec *specs.Spec, fds []*fd.FD, useCheckpointGofer bool) (*control.SaveOpts, error) {
// Convert the FDs to files which is required by the saveOpts.
files := make([]*os.File, len(fds))
for i, fd := range fds {
var err error
files[i], err = fd.File()
if err != nil {
return nil, err
}
}
comp, err := GetAnnotationCheckpointCompression(spec)
if err != nil {
return nil, err
}
saveOpts := &control.SaveOpts{
AppMFExcludeCommittedZeroPages: specutils.AnnotationToBool(spec, annotationCheckpointExcludeCommittedZeroPages),
FilePayload: urpc.FilePayload{
Files: files,
},
Metadata: control.Pairs(comp.ToMetadata()),
HavePagesFile: len(files) > 1,
Resume: specutils.AnnotationToBool(spec, annotationCheckpointResume),
CudaCheckpointSequential: specutils.AnnotationToBool(spec, annotationCheckpointCudaCheckpointSequential),
}
if cudaPath, ok := spec.Annotations[annotationCheckpointCudaCheckpointPath]; ok {
saveOpts.CudaCheckpointPath = cudaPath
}
if useCheckpointGofer {
saveOpts.UseCheckpointGofer = true
if comp == statefile.CompressionLevelNone {
saveOpts.HavePagesFile = true
}
}
if spec.Annotations[annotationSaveRestoreExecArgv] != "" {
saveRestoreExecTimeout := control.DefaultSaveRestoreExecTimeout
if spec.Annotations[annotationSaveRestoreExecTimeout] != "" {
saveRestoreExecTimeout, err = time2.ParseDuration(spec.Annotations[annotationSaveRestoreExecTimeout])
if err != nil {
return nil, fmt.Errorf("failed to parse save-restore-exec-timeout: %w", err)
}
}
saveOpts.ExecOpts = control.SaveRestoreExecOpts{
Argv: spec.Annotations[annotationSaveRestoreExecArgv],
Timeout: saveRestoreExecTimeout,
}
}
return saveOpts, nil
}
func newProcInternalData(conf *config.Config, spec *specs.Spec) *proc.InternalData {
return &proc.InternalData{
GVisorMarkerFile: conf.GVisorMarkerFile,
OverrideProcs: procFiles(conf),
SaveTriggerEnabled: specutils.AnnotationToBool(spec, annotationCheckpointEnable),
FSCheckpointEnabled: specutils.AnnotationToBool(spec, annotationFSCheckpointEnable),
}
}
// restorer manages a restore session for a sandbox. It stores information about
// all containers and triggers the full sandbox restore after the last
// container is restored.
type restorer struct {
mu sync.Mutex
// totalContainers is the number of containers expected to be restored in
// the sandbox. Sandbox restore can only happen, after all containers have
// been restored.
totalContainers int
// containers is the list of containers restored so far.
containers []*containerInfo
// stateFile is a reader for the statefile.
stateFile io.ReadCloser
// metadata is the metadata contained in the statefile.
metadata map[string]string
// timer is the timer for the restore process.
// The `restorer` owns the timer and will end it when restore is complete.
timer *timing.Timer
// If background is true, pagesFile may continue to be read after
// restorer.restore() returns.
background bool
// mainMF is the main MemoryFile of the sandbox.
// It is created as soon as possible, and may be restored to as soon as
// the first container is restored, which is earlier than when the sandbox's
// kernel object is created.
mainMF *pgalloc.MemoryFile
// asyncMFLoader is used to load the MemoryFile pages. It handles the
// asynchronous loading of the memory pages.
asyncMFLoader *kernel.AsyncMFLoader
// deviceFile is the required to start the platform.
deviceFile *fd.FD
// cm is the container manager that is used to restore the sandbox.
cm *containerManager
// checkpointedSpecs contains the map of container specs used during
// checkpoint.
checkpointedSpecs map[string]*specs.Spec
// extractRootFsMode is true if we only want to extract the upper layer of
// a rootfs overlay.
extractRootFsMode bool
// rootFsOutputTar is the file to write the rootfs upper layer tar archive to.
rootFsOutputTar *os.File
}
// restoreSubcontainer restores a subcontainer.
func (r *restorer) restoreSubcontainer(spec *specs.Spec, conf *config.Config, l *Loader, cid string, stdioFDs, goferFDs, goferFilestoreFDs []*fd.FD, devGoferFD *fd.FD, goferMountConfs []specutils.GoferMountConf) error {
containerName := l.registerContainer(spec, cid)
info := &containerInfo{
cid: cid,
containerName: containerName,
conf: conf,
spec: spec,
stdioFDs: stdioFDs,
goferFDs: goferFDs,
devGoferFD: devGoferFD,
goferFilestoreFDs: goferFilestoreFDs,
goferMountConfs: goferMountConfs,
}
return r.restoreContainerInfo(l, info)
}
// restoreContainerInfo restores a container.
func (r *restorer) restoreContainerInfo(l *Loader, info *containerInfo) error {
r.mu.Lock()
defer r.mu.Unlock()
for _, container := range r.containers {
if container.containerName == info.containerName {
return fmt.Errorf("container %q already restored", info.containerName)
}
if container.cid == info.cid {
return fmt.Errorf("container CID %q already belongs to container %q", info.cid, container.containerName)
}
}
r.containers = append(r.containers, info)
if log.IsLogging(log.Debug) {
for i, fd := range info.stdioFDs {
log.Debugf("Restore app FD: %d host FD: %d", i, fd.FD())
}
}
log.Infof("Restored container %d of %d", len(r.containers), r.totalContainers)
r.timer.Reached(fmt.Sprintf("restore cont %d/%d", len(r.containers), r.totalContainers))
// Non-container-specific restore work:
if len(r.containers) == r.totalContainers {
// Trigger the restore if this is the last container.
if err := r.restore(l); err != nil {
return err
}
}
return nil
}
type restoreMounts struct {
fdmap map[checkpoint.ResourceID]int
mfmap map[checkpoint.ResourceID]*pgalloc.MemoryFile
sharedMfs map[string]bool
}
func (r *restoreMounts) String() string {
return fmt.Sprintf("fdmap: %#v, mfmap: %v", r.fdmap, r.mfmap)
}
func (r *restorer) restore(l *Loader) error {
log.Infof("Starting to restore %d containers", len(r.containers))
// Validate the container specs to ensure they did not meaningfully change
// between checkpoint and restore.
if err := specutils.RestoreValidateSpec(r.checkpointedSpecs, l.GetContainerSpecs(), l.root.conf); err != nil {
return fmt.Errorf("failed to handle restore spec validation: %w", err)
}
if l.root.conf.Network != config.NetworkSandbox && l.root.conf.Network != config.NetworkNone && l.root.conf.Network != config.NetworkHost {
return fmt.Errorf("checkpoint not supported when using %s networking", l.root.conf.Network)
}
// Checkpoints without the network key predate hostinet support.
savedNetwork, ok := r.metadata[networkKey]
if !ok {
savedNetwork = config.NetworkSandbox.String()
}
savedHost := savedNetwork == config.NetworkHost.String()
if restoreHost := l.root.conf.Network == config.NetworkHost; savedHost != restoreHost {
return fmt.Errorf("checkpoint created with %s networking cannot be restored with %s networking", savedNetwork, l.root.conf.Network)
}
r.timer.Reached("specs validated")
p, err := createPlatform(l.root.conf, l.root.applicationCores, r.deviceFile, l.sandboxID, r.timer, &l.pinRing)
if err != nil {
return fmt.Errorf("creating platform: %v", err)
}
// Start the old watchdog before replacing it with a new one below.
l.watchdog.Start()
// Release the kernel and replace it with a new one that will be restored into.
var oldNvidiaDriverVersion nvconf.DriverVersion
if l.k != nil {
oldNvidiaDriverVersion = l.k.NvidiaDriverVersion
l.k.Release()
}
l.k = &kernel.Kernel{
Platform: p,
}
l.k.SetMemoryFile(r.mainMF)
if l.root.conf.ProfileEnable {
// pprof.Initialize opens /proc/self/maps, so has to be called before
// installing seccomp filters.
pprof.Initialize()
}
if l.root.conf.Network == config.NetworkHost {
devFile, err := os.Open("/proc/net/dev")
if err != nil {
log.Warningf("Failed to open /proc/net/dev during restore: %v", err)
} else {
l.hostinetNetDevFile = devFile
}
snmpFile, err := os.Open("/proc/net/snmp")
if err != nil {
log.Warningf("Failed to open /proc/net/snmp during restore: %v", err)
} else {
l.hostinetNetSNMPFile = snmpFile
}
defer func() {
if l.hostinetNetDevFile != nil {
l.hostinetNetDevFile.Close()
l.hostinetNetDevFile = nil
}
if l.hostinetNetSNMPFile != nil {
l.hostinetNetSNMPFile.Close()
l.hostinetNetSNMPFile = nil
}
}()
}
// Seccomp filters have to be applied before vfs restore and before parsing
// the state file.
if err := l.installSeccompFilters(); err != nil {
return err
}
l.mu.Lock()
cu := cleanup.Make(func() {
l.mu.Unlock()
})
defer cu.Clean()
restoreMnts := restoreMounts{
fdmap: make(map[checkpoint.ResourceID]int),
mfmap: make(map[checkpoint.ResourceID]*pgalloc.MemoryFile),
sharedMfs: make(map[string]bool),
}
for _, cont := range r.containers {
// TODO(b/298078576): Need to process hints here probably
mntr := l.newContainerMounter(cont)
if err = mntr.configureRestore(&restoreMnts); err != nil {
return fmt.Errorf("configuring filesystem restore: %v", err)
}
for i, fd := range cont.stdioFDs {
key := host.MakeResourceID(cont.containerName, i)
restoreMnts.fdmap[key] = fd.Release()
}
for _, customFD := range cont.passFDs {
key := host.MakeResourceID(cont.containerName, customFD.guest)
restoreMnts.fdmap[key] = customFD.host.FD()
}
}
log.Debugf("Restore using mounts: %v", &restoreMnts)
ctx := l.k.SupervisorContext()
ctx = context.WithValues(ctx, map[any]any{
vfs.CtxRestoreFilesystemFDMap: restoreMnts.fdmap,
pgalloc.CtxMemoryFileMap: restoreMnts.mfmap,
devutil.CtxDevGoferClientProvider: l.k,
})
if r.asyncMFLoader != nil {
// Now that private memory files are known, kick off their loading in the
// background goroutine.
r.asyncMFLoader.KickoffPrivate(restoreMnts.mfmap)
}
ctx, err = r.prepareNvproxyRestoreContextLocked(ctx, l)
if err != nil {
return err
}
ctx, err = r.prepareTPURestoreContextLocked(ctx, l)
if err != nil {
return err
}
// Load the state.
clocks := time.NewCalibratedClocks(shouldEnableClockMonotonicRaw(l.root.spec, l.root.conf))
r.timer.Reached("loading kernel")
if r.extractRootFsMode {
if err := l.k.ExtractRootfsUpperLayer(ctx, r.stateFile, r.asyncMFLoader, nil, clocks, r.rootFsOutputTar); err != nil {
return fmt.Errorf("failed to extract rootfs upper layer: %w", err)
}
r.timer.Reached("rootfs upper layer extracted")
return nil
}
if err := l.k.LoadFrom(ctx, r.stateFile, r.asyncMFLoader, nil, l, clocks, &vfs.CompleteRestoreOptions{}, r.timer.Fork("kernel load")); err != nil {
return fmt.Errorf("failed to load kernel: %w", err)
}
r.timer.Reached("kernel loaded")
if oldNvidiaDriverVersion.Major() > 0 && !l.k.NvidiaDriverVersion.Equals(oldNvidiaDriverVersion) {
return fmt.Errorf("nvidia driver version changed during restore: was %v, now %v", oldNvidiaDriverVersion, l.k.NvidiaDriverVersion)
}
if r.asyncMFLoader != nil {
if r.background {
if err := r.asyncMFLoader.WaitMetadata(); err != nil {
return err
}
r.timer.Reached("MF metadata loaded")
} else {
if err := r.asyncMFLoader.Wait(); err != nil {
return err
}
r.timer.Reached("MFs loaded")
}
}
// Since we have a new kernel we also must make a new watchdog.
dogOpts := watchdog.DefaultOpts
if err := dogOpts.TaskTimeoutAction.Set(l.root.conf.WatchdogAction); err != nil {
return fmt.Errorf("setting watchdog action: %w", err)
}
dogOpts.StartupTimeout = 3 * time2.Minute // Give extra time for all containers to restore.
dog := watchdog.New(l.k, dogOpts)
// Change the loader fields to reflect the changes made when restoring.
l.watchdog.Stop()
l.watchdog = dog
l.root.procArgs = kernel.CreateProcessArgs{}
l.sandboxID = l.root.cid
// Update all tasks in the system with:
// 1. their respective new container IDs.
// 2. the new hostname and domainname.
visitedUTS := make(map[*kernel.UTSNamespace]struct{})
for _, task := range l.k.TaskSet().Root.Tasks() {
oldCid := task.ContainerID()
name := l.k.ContainerName(oldCid)
newCid, ok := l.containerIDs[name]
if !ok {
return fmt.Errorf("unable to remap task with CID %q (name: %q). Available names: %v", task.ContainerID(), name, l.containerIDs)
}
task.RestoreContainerID(newCid)
if utsns := task.UTSNamespace(); utsns != nil {
if _, ok := visitedUTS[utsns]; !ok {
visitedUTS[utsns] = struct{}{}
utsns.RestoreSpecValues(l.root.spec.Hostname, l.root.spec.Domainname)
}
}
}
// Rebuild `processes` map with containers' root process from the restored kernel.
for _, tg := range l.k.RootPIDNamespace().ThreadGroups() {
// Find all processes with no parent (root of execution), that were not started
// via a call to `exec`.
if tg.Leader().Parent() == nil && tg.Leader().Origin != kernel.OriginExec {
cid := tg.Leader().ContainerID()
proc := l.processes[execID{cid: cid}]
if proc == nil {
return fmt.Errorf("unable to find container root process with CID %q, processes: %v", cid, l.processes)
}
proc.tg = tg
}
}
// Kill all processes that have been exec'd since they cannot be properly
// restored -- the caller is no longer connected.
log.Debugf("Killing any exec session that existed previously")
for _, tg := range l.k.RootPIDNamespace().ThreadGroups() {
if tg.Leader().Origin == kernel.OriginExec {
log.Infof("Killing exec'd process, PID: %d", tg.ID())
if err := l.k.SendExternalSignalThreadGroup(tg, &linux.SignalInfo{Signo: int32(linux.SIGKILL)}); err != nil {
log.Warningf("Failed to kill exec process after restore: %v", err)
}
}
}
l.k.RestoreContainerMapping(l.containerIDs)
l.k.SetSaver(l)
l.createRemappedNvproxyDeviceFiles(ctx)
// Refresh the control server with the newly created kernel.
l.ctrl.refreshHandlers()
// Release `l.mu` before calling into callbacks.
cu.Clean()
r.timer.Reached("Starting sandbox")
if err := r.cm.onStart(); err != nil {
return fmt.Errorf("restorer.readyToStart callback failed: %w", err)
}
r.stateFile.Close()
// Transfer ownership of the `timer` to a new goroutine.
// This is because `timer.Log` blocks until all timed tasks are finished,
// but some restore tasks may still run in the background, and we don't
// want to block this function until they finish.
postRestoreTimeline := r.timer.Fork("postRestore")
go r.postRestore(l.k, postRestoreTimeline, r.timer)
r.timer = nil
return nil
}
func (r *restorer) postRestore(k *kernel.Kernel, timeline *timing.Timeline, timer *timing.Timer) {
defer timer.Log()
defer timeline.End()
timeline.Reached("scheduled")
if err := control.PostRestore(k, timeline); err != nil {
r.cm.onRestoreFailed(fmt.Errorf("post restore work failed: %w", err))
log.Warningf("Killing the sandbox after post restore work failed: %v", err)
k.Kill(linux.WaitStatusTerminationSignal(linux.SIGKILL))
return
}
timeline.Reached("post restore done")
// Now that post restore work succeeded, increment the checkpoint gen
// manually. The count was saved while the previous kernel was being saved
// and checkpoint success was unknown at that time. Now we know the had
// checkpoint succeeded. Allow the application to proceed while pages may
// keep loading in the background.
k.IncCheckpointGenOnRestore()
// Wait for page loading to complete if happening in the background.
if r.asyncMFLoader != nil {
if err := r.asyncMFLoader.Wait(); err != nil {
r.cm.onRestoreFailed(fmt.Errorf("async MemoryFile loading failed: %w", err))
log.Warningf("Killing the sandbox after MemoryFile page loading failed: %v", err)
k.Kill(linux.WaitStatusTerminationSignal(linux.SIGKILL))
return
}
}
var s Savings
if err := r.calculateCPUSavings(&s); err != nil {
log.Warningf("Failed to calculate CPU savings: %v", err)
}
if err := r.calculateWallTimeSavings(&s); err != nil {
log.Warningf("Failed to calculate walltime savings: %v", err)
}
r.cm.onRestoreDone(s)
timeline.Reached("kernel notified")
log.Infof("Restore successful")
}
// Calculate the CPU time saved for restore.
func (r *restorer) calculateCPUSavings(s *Savings) error {
t, err := state.CPUTime()
if err != nil {
return fmt.Errorf("failed to get CPU time usage for restore, err: %w", err)
}
savedTimeStr, ok := r.metadata[state.GvisorCPUUsageKey]
if !ok {
return fmt.Errorf("failed to retrieve CPU time usage from the metadata")
}
savedTime, err := time2.ParseDuration(savedTimeStr)
if err != nil {
return fmt.Errorf("cpu time usage in metadata %v is invalid, err: %w", savedTimeStr, err)
}
s.CPUTimeSaved = savedTime - t
log.Infof("CPU time saved with restore: %v ms, restore CPU time: %v ms", s.CPUTimeSaved.Milliseconds(), t.Milliseconds())
return nil
}
// Calculate the walltime saved for restore.
func (r *restorer) calculateWallTimeSavings(s *Savings) error {
savedWtStr, ok := r.metadata[state.GvisorWallTimeKey]
if !ok {
return fmt.Errorf("failed to retrieve walltime from the metadata")
}
savedWt, err := time2.ParseDuration(savedWtStr)
if err != nil {
return fmt.Errorf("walltime in metadata %v is invalid, err: %w", savedWtStr, err)
}
wt := time2.Since(starttime.Get())
s.WallTimeSaved = savedWt - wt
log.Infof("Walltime saved with restore: %v ms, restore walltime: %v ms", s.WallTimeSaved.Milliseconds(), wt.Milliseconds())
return nil
}
func (l *Loader) save(o *control.SaveOpts) (err error) {
saveOpts, err := control.ConvertToStateSaveOpts(o)
if err != nil {
return err
}
defer saveOpts.Close()
return l.saveWithOpts(saveOpts, &o.ExecOpts)
}
// saveWithOpts saves the kernel with the given options.
func (l *Loader) saveWithOpts(saveOpts *state.SaveOpts, execOpts *control.SaveRestoreExecOpts) (err error) {
defer func() {
// This closure is required to capture the final value of err.
l.k.OnCheckpointAttempt(err)
}()
if saveOpts.Metadata == nil {
saveOpts.Metadata = make(map[string]string)
}
saveOpts.Metadata[ContainerCountKey] = strconv.Itoa(l.containerCount())
// Save runsc version.
saveOpts.Metadata[VersionKey] = version.Version()
saveOpts.Metadata[networkKey] = l.root.conf.Network.String()
// Save container specs.
specsStr, err := specutils.ConvertSpecsToString(l.GetContainerSpecs())
if err != nil {
return err
}
saveOpts.Metadata[ContainerSpecsKey] = specsStr
// Save start time of the runsc process.
saveOpts.StartTime = starttime.Get()
if err := l.setNvproxyDeviceRemapMetadata(saveOpts); err != nil {
return err
}
if err := l.setTPUDeviceRemapMetadata(saveOpts); err != nil {
return err
}
state := control.State{
Kernel: l.k,
Watchdog: l.watchdog,
}
return state.SaveWithOpts(saveOpts, execOpts)
}
func procFiles(conf *config.Config) []string {
if conf.OverrideProcs == "" {
return nil
}
return strings.Split(conf.OverrideProcs, ",")
}