Files
Etienne Perot d11f1e9bb7 Defer release of slow-to-release FDs via disabled io_uring ring.
`runsc` creates some FDs that are slow to release (`AF_PACKET` sockets for
networking, KVM FDs for the KVM platform).
By releasing them asynchronously, we free up the sandbox process memory
earlier during teardown, allowing better density of sandboxes to exist.

Also allows faster sandbox recycling for systems like Substrate which spawn
a new sandbox as soon as one exits.

This uses an `io_uring` hack to move the release work to a kworker thread.
The ring is held by the gofer (not the Sentry), is disabled
(`IORING_SETUP_R_DISABLED`), and is unusable anyway due to seccomp filters.

Includes a test to verify this kernel implementation detail.

Benchmarks:

```
$ sudo runsc do --platform=kvm --network=sandbox

        │    before      │   after                              │
        │    sec/op      │   sec/op     vs base                 │
RunscDo     459.5m ± 6%     432.5m ± 4%  -5.88% (p=0.001 n=30)

           │     before       │   after                               │
           │     sec/op       │   sec/op     vs base                  │
OCICreate       155.8m ± 5%      155.0m ± 7%        ~ (p=0.623 n=30)
OCIDelete       8.838m ± 4%      7.713m ± 9%  -12.73% (p=0.000 n=30)
OCIExit        80.511m ± 6%      2.015m ± 7%  -97.50% (p=0.000 n=30)
OCIReady        170.6m ± 6%      171.0m ± 7%        ~ (p=0.708 n=30)
OCIStart        15.51m ± 3%      15.97m ± 5%        ~ (p=0.223 n=30)
OCITotal        261.0m ± 5%      180.8m ± 6%  -30.73% (p=0.000 n=30)
```
2026-08-20 18:27:54 -07:00

259 lines
7.0 KiB
Go

// Copyright 2018 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 kvm provides a kvm-based implementation of the platform interface.
package kvm
import (
"fmt"
"golang.org/x/sys/unix"
pkgcontext "gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/ring0"
"gvisor.dev/gvisor/pkg/ring0/pagetables"
"gvisor.dev/gvisor/pkg/sentry/hostmm"
"gvisor.dev/gvisor/pkg/sentry/platform"
"gvisor.dev/gvisor/pkg/sync"
)
// userMemoryRegion is a region of physical memory.
//
// This mirrors kvm_memory_region.
type userMemoryRegion struct {
slot uint32
flags uint32
guestPhysAddr uint64
memorySize uint64
userspaceAddr uint64
}
// runData is the run structure. This may be mapped for synchronous register
// access (although that doesn't appear to be supported by my kernel at least).
//
// This mirrors kvm_run.
type runData struct {
requestInterruptWindow uint8
_ [7]uint8
exitReason uint32
readyForInterruptInjection uint8
ifFlag uint8
_ [2]uint8
cr8 uint64
apicBase uint64
// This is the union data for exits. Interpretation depends entirely on
// the exitReason above (see vCPU code for more information).
data [32]uint64
}
// KVM represents a lightweight VM context.
type KVM struct {
// KVM never changes mm_structs.
platform.UseHostProcessMemoryBarrier
// machine is the backing VM.
machine *machine
}
var (
globalOnce sync.Once
globalErr error
)
// OpenDevice opens the KVM device and returns the File.
// If the devicePath is empty, it will default to /dev/kvm.
func OpenDevice(devicePath string) (*fd.FD, error) {
if devicePath == "" {
devicePath = "/dev/kvm"
}
f, err := fd.Open(devicePath, unix.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("error opening KVM device file (%s): %v", devicePath, err)
}
return f, nil
}
// New returns a new KVM-based implementation of the platform interface.
func New(deviceFile *fd.FD, config Config) (*KVM, error) {
if hostarch.PageSize != 4096 {
return nil, fmt.Errorf("KVM platform does not support %dK page size", hostarch.PageSize/1024)
}
mbCh := hostmm.Probe(true)
fd := deviceFile.FD()
// Ensure global initialization is done.
initIoeventfdMMIO()
globalOnce.Do(func() {
globalErr = updateGlobalOnce(int(fd))
})
if globalErr != nil {
return nil, globalErr
}
config.StartupTimer.Reached("kvm global state initialized")
// Create a new VM fd.
var (
vm uintptr
errno unix.Errno
)
for {
vm, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(fd), KVM_CREATE_VM, 0)
if errno == unix.EINTR {
continue
}
if errno != 0 {
return nil, fmt.Errorf("creating VM: %v", errno)
}
break
}
// We are done with the device file.
deviceFile.Close()
config.StartupTimer.Reached("kvm VM created")
// `kvm_destroy_vm` costs tens of milliseconds. Do that async.
config.PinRing.Add(int(vm))
// Create a VM context.
machine, err := newMachine(int(vm), &config)
if err != nil {
return nil, err
}
config.StartupTimer.Reached("kvm machine created")
config.StartupTimer.Reached("waiting for membarrier")
memBarrier := <-mbCh
config.StartupTimer.Reached("host membarrier probed")
return &KVM{
UseHostProcessMemoryBarrier: platform.UseHostProcessMemoryBarrier{MemBarrier: memBarrier},
machine: machine,
}, nil
}
// SupportsAddressSpaceIO implements platform.Platform.SupportsAddressSpaceIO.
func (*KVM) SupportsAddressSpaceIO() bool {
return false
}
// MapUnit implements platform.Platform.MapUnit.
func (*KVM) MapUnit() uint64 {
// We greedily creates PTEs in MapFile, so extremely large mappings can
// be expensive. Not _that_ expensive since we allow super pages, but
// even though can get out of hand if you're creating multi-terabyte
// mappings. For this reason, we limit mappings to an arbitrary 16MB.
return 16 << 20
}
// MinUserAddress returns the lowest available address.
func (*KVM) MinUserAddress() hostarch.Addr {
return hostarch.PageSize
}
// MaxUserAddress returns the first address that may not be used.
func (*KVM) MaxUserAddress() hostarch.Addr {
return hostarch.Addr(ring0.MaximumUserAddress)
}
// NewAddressSpace returns a new pagetable root.
func (k *KVM) NewAddressSpace() (platform.AddressSpace, error) {
// Allocate page tables and install system mappings.
pageTables := pagetables.NewWithUpper(newAllocator(), k.machine.upperSharedPageTables, ring0.KernelStartAddress)
// Return the new address space.
return &addressSpace{
machine: k.machine,
pageTables: pageTables,
dirtySet: k.machine.newDirtySet(),
}, nil
}
// ConcurrencyCount implements platform.Platform.ConcurrencyCount.
// KVM can't run more than maxVCPUs contexts concurrently.
func (k *KVM) ConcurrencyCount() int {
return k.machine.maxVCPUs
}
// HasCPUNumbers implements platform.Platform.HasCPUNumbers.
func (k *KVM) HasCPUNumbers() bool {
return k.machine.useCPUNums
}
// NumCPUs implements platform.Platform.NumCPUs.
func (k *KVM) NumCPUs() int {
if !k.HasCPUNumbers() {
panic("platform is not configured to use CPU numbers")
}
return k.machine.maxVCPUs
}
// DetectsCPUPreemption implements platform.Platform.DetectsCPUPreemption.
func (*KVM) DetectsCPUPreemption() bool {
return true
}
// PreemptAllCPUs implements platform.Platform.PreemptAllCPUs.
func (k *KVM) PreemptAllCPUs() error {
for _, c := range k.machine.vCPUsByID {
c.lastCtx.Store(nil)
c.BounceToHost()
}
return nil
}
// PreemptCPU implements platform.Platform.PreemptCPU.
func (k *KVM) PreemptCPU(cpu int32) error {
c := k.machine.vCPUsByID[cpu]
c.lastCtx.Store(nil)
c.BounceToHost()
return nil
}
// NewContext returns an interruptible context.
func (k *KVM) NewContext(pkgcontext.Context) platform.Context {
return &platformContext{
machine: k.machine,
}
}
type constructor struct{}
func (*constructor) New(opts platform.Options) (platform.Platform, error) {
log.Infof("UseCPUNums: %v", opts.UseCPUNums)
return New(opts.DeviceFile, Config{
ApplicationCores: opts.ApplicationCores,
UseCPUNums: opts.UseCPUNums,
StartupTimer: opts.StartupTimer,
PinRing: opts.PinRing,
})
}
func (*constructor) OpenDevice(devicePath string) (*fd.FD, error) {
return OpenDevice(devicePath)
}
// Flags implements platform.Constructor.Flags().
func (*constructor) Requirements() platform.Requirements {
return platform.Requirements{}
}
func init() {
platform.Register("kvm", &constructor{})
}