mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
301 lines
9.8 KiB
Go
301 lines
9.8 KiB
Go
// 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 kernel
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
"gvisor.dev/gvisor/pkg/abi/linux"
|
|
"gvisor.dev/gvisor/pkg/cleanup"
|
|
"gvisor.dev/gvisor/pkg/context"
|
|
"gvisor.dev/gvisor/pkg/hostarch"
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/sentry/checkpoint"
|
|
"gvisor.dev/gvisor/pkg/sentry/fscheckpoint"
|
|
fspb "gvisor.dev/gvisor/pkg/sentry/fscheckpoint/fscheckpoint_proto_go_proto"
|
|
"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
|
|
"gvisor.dev/gvisor/pkg/sentry/pgalloc"
|
|
"gvisor.dev/gvisor/pkg/sentry/state/stateio"
|
|
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
|
"gvisor.dev/gvisor/pkg/sync"
|
|
)
|
|
|
|
// FSSaveOpts holds options to Kernel.FSSave.
|
|
type FSSaveOpts struct {
|
|
// These correspond to files specified by the fscheckpoint package, and are
|
|
// all required.
|
|
ManifestFile io.WriteCloser
|
|
MultiTarFile io.WriteCloser
|
|
PagesMetadataFile io.WriteCloser
|
|
PagesFile stateio.AsyncWriter
|
|
|
|
// RunscVersion is the runsc binary version.
|
|
RunscVersion string
|
|
|
|
// If ExitAfterSaving is true, all processes exit with status 0 before
|
|
// FSSave returns, whether or not it returns a non-nil error.
|
|
ExitAfterSaving bool
|
|
|
|
// Paths is the list of paths inside the containers to save.
|
|
// If empty, defaults to a single path "/" for all containers.
|
|
Paths []checkpoint.ResourceID
|
|
}
|
|
|
|
// FSSave collects a filesystem checkpoint as specified by the fscheckpoint
|
|
// package. FSSave takes ownership of resources in opts.
|
|
func (k *Kernel) FSSave(ctx context.Context, opts *FSSaveOpts) error {
|
|
defer func() {
|
|
if opts.ManifestFile != nil {
|
|
opts.ManifestFile.Close()
|
|
opts.ManifestFile = nil
|
|
}
|
|
if opts.MultiTarFile != nil {
|
|
opts.MultiTarFile.Close()
|
|
opts.MultiTarFile = nil
|
|
}
|
|
if opts.PagesMetadataFile != nil {
|
|
opts.PagesMetadataFile.Close()
|
|
opts.PagesMetadataFile = nil
|
|
}
|
|
if opts.PagesFile != nil {
|
|
opts.PagesFile.Close()
|
|
opts.PagesFile = nil
|
|
}
|
|
}()
|
|
paths := opts.Paths
|
|
if len(paths) == 0 {
|
|
paths = []checkpoint.ResourceID{{Path: "/"}}
|
|
}
|
|
pathsMap := make(map[checkpoint.ResourceID]struct{}, len(paths))
|
|
for _, p := range paths {
|
|
pathsMap[p] = struct{}{}
|
|
}
|
|
|
|
k.Pause()
|
|
defer k.Unpause()
|
|
if opts.ExitAfterSaving {
|
|
defer k.Kill(linux.WaitStatusExit(0)) // consistent with sentry/state.SaveOpts.Save
|
|
}
|
|
err := k.quiescePausedAnd(ctx, func() error {
|
|
var (
|
|
asyncPageSaveWg sync.WaitGroup
|
|
asyncPageSaveErr error
|
|
)
|
|
asyncPageSaveWg.Add(1)
|
|
apfs, err := pgalloc.StartAsyncPagesFileSave(opts.PagesFile /* transfers ownership */, func(err error) {
|
|
defer asyncPageSaveWg.Done()
|
|
asyncPageSaveErr = err
|
|
})
|
|
opts.PagesFile = nil
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start async page saving: %w", err)
|
|
}
|
|
asyncPageSaveCleanup := cleanup.Make(func() {
|
|
apfs.MemoryFilesDone()
|
|
asyncPageSaveWg.Wait()
|
|
})
|
|
defer asyncPageSaveCleanup.Clean()
|
|
mfOpts := pgalloc.SaveOpts{
|
|
PagesFile: apfs,
|
|
}
|
|
|
|
manifest := &fspb.Manifest{
|
|
Version: 1,
|
|
RunscVersion: opts.RunscVersion,
|
|
PageSize: hostarch.PageSize,
|
|
Endian: hostarch.EndianString(),
|
|
}
|
|
type tmpfsAndPrivateMemoryFile struct {
|
|
fs *vfs.Filesystem
|
|
mf *pgalloc.MemoryFile
|
|
}
|
|
resourceIDs := make(map[checkpoint.ResourceID]tmpfsAndPrivateMemoryFile)
|
|
multiTarWriter := &countingWriter{w: opts.MultiTarFile}
|
|
pagesMetadataWriter := &countingWriter{w: opts.PagesMetadataFile}
|
|
prevTarOffset := uint64(0)
|
|
prevPagesMetadataOffset := uint64(0)
|
|
prevPagesOffset := uint64(0)
|
|
fss := k.vfs.GetFilesystems()
|
|
defer func() {
|
|
for _, fs := range fss {
|
|
fs.DecRef(ctx)
|
|
}
|
|
}()
|
|
// TODO: NOLINT - fss is obtained by iterating a map, so its order -
|
|
// and thus the order in which filesystems will be saved - is
|
|
// effectively random. pgalloc.MemoryFile async page loading biases
|
|
// toward MemoryFiles stored at lower offsets in the pages file. We
|
|
// should save both filesystems and MemoryFiles in the order that they
|
|
// were created, which is most likely to be the order in which they are
|
|
// created after restore; in particular, this will be the case when the
|
|
// same Kubernetes Pod spec is reused after restore.
|
|
for _, fs := range fss {
|
|
mf := tmpfs.MemoryFileOf(fs)
|
|
if mf == nil {
|
|
continue
|
|
}
|
|
resourceID := mf.ResourceID()
|
|
// Exclude tmpfs filesystems backed by the main MemoryFile (which
|
|
// have no ResourceID) since we currently have no way to save only
|
|
// the contents of the main MemoryFile owned by a checkpointed
|
|
// filesystem, and don't want to save all application memory.
|
|
//
|
|
// TODO: NOLINT - Support checkpointing tmpfs filesystems backed
|
|
// by the main MemoryFile by implementing the ability to save a
|
|
// subset of a pgalloc.MemoryFile, and using it to save only the
|
|
// subset of each MemoryFile used by any checkpointed tmpfs
|
|
// filesystem. This would also avoid saving MemoryFile pages that
|
|
// are referenced by e.g. a previous MM.Pin() but no longer owned
|
|
// by a regularFile; in such cases, the holder of the extra
|
|
// reference won't be restored by filesystem checkpointing,
|
|
// causing the referenced pages to be leaked. (As of this
|
|
// writing, this leak is unlikely to be an issue in practice,
|
|
// since the primary user of MM.Pin() is nvproxy, and the Nvidia
|
|
// driver appears to reject pinning mappings of disk-backed
|
|
// files.)
|
|
if !resourceID.Ok() {
|
|
continue
|
|
}
|
|
|
|
matched := false
|
|
if _, ok := pathsMap[resourceID]; ok {
|
|
matched = true
|
|
} else if _, ok := pathsMap[checkpoint.ResourceID{Path: resourceID.Path}]; ok {
|
|
matched = true
|
|
} else if _, ok := pathsMap[checkpoint.ResourceID{ContainerName: resourceID.ContainerName, Path: fscheckpoint.AllTmpfsPath}]; ok {
|
|
matched = true
|
|
} else if _, ok := pathsMap[checkpoint.ResourceID{Path: fscheckpoint.AllTmpfsPath}]; ok {
|
|
matched = true
|
|
}
|
|
|
|
if !matched {
|
|
continue
|
|
}
|
|
if old, ok := resourceIDs[resourceID]; ok {
|
|
return fmt.Errorf("pgalloc.MemoryFile restore ID %q is used by both tmpfs filesystem %p (MemoryFile %p) and tmpfs filesystem %p (MemoryFile %p)", resourceID, old.fs, old.mf, fs, mf)
|
|
}
|
|
resourceIDs[resourceID] = tmpfsAndPrivateMemoryFile{fs, mf}
|
|
if log.IsLogging(log.Debug) {
|
|
log.Debugf("Filesystem checkpoint saving tmpfs with resourceID %s", resourceID)
|
|
}
|
|
if err := mf.SaveTo(ctx, pagesMetadataWriter, &mfOpts); err != nil {
|
|
return fmt.Errorf("failed to save MemoryFile with resourceID %s: %w", resourceID, err)
|
|
}
|
|
manifest.MemoryFiles = append(manifest.MemoryFiles, &fspb.MemoryFile{
|
|
ResourceId: toProtoResourceID(resourceID),
|
|
PagesMetadataStart: prevPagesMetadataOffset,
|
|
PagesMetadataEnd: pagesMetadataWriter.count,
|
|
PagesStart: prevPagesOffset,
|
|
})
|
|
prevPagesMetadataOffset = pagesMetadataWriter.count
|
|
prevPagesOffset = apfs.PagesFileOffset()
|
|
if err := tmpfs.FSCheckpointWrite(ctx, fs, multiTarWriter); err != nil {
|
|
return fmt.Errorf("failed to write tmpfs with resourceID %s to multi-tar file: %w", resourceID, err)
|
|
}
|
|
manifest.Tmpfs = append(manifest.Tmpfs, &fspb.Tmpfs{
|
|
ResourceId: toProtoResourceID(resourceID),
|
|
TarStart: prevTarOffset,
|
|
TarEnd: multiTarWriter.count,
|
|
})
|
|
prevTarOffset = multiTarWriter.count
|
|
}
|
|
if len(resourceIDs) == 0 {
|
|
return fmt.Errorf("no checkpointable filesystems")
|
|
}
|
|
apfs.MemoryFilesDone()
|
|
bytes, err := proto.Marshal(manifest)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal manifest: %w", err)
|
|
}
|
|
if _, err := opts.ManifestFile.Write(bytes); err != nil {
|
|
return fmt.Errorf("failed to write manifest file: %w", err)
|
|
}
|
|
// Close other writers while MemoryFile saving is in progress to
|
|
// overlap their latencies.
|
|
err = opts.ManifestFile.Close()
|
|
opts.ManifestFile = nil
|
|
if err != nil {
|
|
return fmt.Errorf("failed to close manifest file: %w", err)
|
|
}
|
|
err = opts.MultiTarFile.Close()
|
|
opts.MultiTarFile = nil
|
|
if err != nil {
|
|
return fmt.Errorf("failed to close multi-tar file: %w", err)
|
|
}
|
|
err = opts.PagesMetadataFile.Close()
|
|
opts.PagesMetadataFile = nil
|
|
if err != nil {
|
|
return fmt.Errorf("failed to close pages metadata file: %w", err)
|
|
}
|
|
// Finally wait for MemoryFile saving to complete.
|
|
asyncPageSaveCleanup.Release()()
|
|
if asyncPageSaveErr != nil {
|
|
return fmt.Errorf("failed to save MemoryFile pages: %w", asyncPageSaveErr)
|
|
}
|
|
return nil
|
|
})
|
|
k.fsSaveMu.Lock()
|
|
defer k.fsSaveMu.Unlock()
|
|
for _, c := range k.fsSaveWaiters {
|
|
c <- err
|
|
}
|
|
k.fsSaveWaiters = nil
|
|
return err
|
|
}
|
|
|
|
type countingWriter struct {
|
|
w io.Writer
|
|
count uint64
|
|
}
|
|
|
|
// Write implements io.Writer.Write.
|
|
func (cw *countingWriter) Write(src []byte) (int, error) {
|
|
n, err := cw.w.Write(src)
|
|
cw.count += uint64(n)
|
|
return n, err
|
|
}
|
|
|
|
// WaitForFSSave waits for a call to k.FSSave() to complete, then returns the
|
|
// error returned by that call.
|
|
//
|
|
// This API is difficult to use without races, but is consistent with
|
|
// k.WaitForCheckpoint().
|
|
func (k *Kernel) WaitForFSSave() error {
|
|
c := make(chan error, 1)
|
|
k.fsSaveMu.Lock()
|
|
k.fsSaveWaiters = append(k.fsSaveWaiters, c)
|
|
k.fsSaveMu.Unlock()
|
|
return <-c
|
|
}
|
|
|
|
// SignalAllFSSaveWaiters signals all FS save waiters with err.
|
|
func (k *Kernel) SignalAllFSSaveWaiters(err error) {
|
|
k.fsSaveMu.Lock()
|
|
defer k.fsSaveMu.Unlock()
|
|
for _, c := range k.fsSaveWaiters {
|
|
c <- err
|
|
}
|
|
k.fsSaveWaiters = nil
|
|
}
|
|
|
|
func toProtoResourceID(id checkpoint.ResourceID) *fspb.ResourceID {
|
|
return &fspb.ResourceID{
|
|
ContainerName: id.ContainerName,
|
|
Path: id.Path,
|
|
}
|
|
}
|