diff --git a/internal/amd64vm/layout.go b/internal/amd64vm/layout.go index ae5c0e4..b51ce98 100644 --- a/internal/amd64vm/layout.go +++ b/internal/amd64vm/layout.go @@ -26,6 +26,10 @@ const ( SnapshotBase = 0xd0009000 SnapshotSize = 0x1000 + BalloonBase = 0xd000a000 + BalloonSize = 0x1000 + BalloonIRQ = 10 + RootFSTag = vmruntime.RootFSTag EmulatorTag = vmruntime.EmulatorTag GuestCID = vmruntime.GuestCID diff --git a/internal/hv/kvm/balloon_linux_amd64.go b/internal/hv/kvm/balloon_linux_amd64.go new file mode 100644 index 0000000..bd459a7 --- /dev/null +++ b/internal/hv/kvm/balloon_linux_amd64.go @@ -0,0 +1,11 @@ +//go:build linux && amd64 + +package kvm + +func balloonTargetPages(mb uint64) uint32 { + pages := mb * 1024 * 1024 / 4096 + if pages > uint64(^uint32(0)) { + return ^uint32(0) + } + return uint32(pages) +} diff --git a/internal/hv/kvm/boot_linux_amd64.go b/internal/hv/kvm/boot_linux_amd64.go index f168762..23cf7c3 100644 --- a/internal/hv/kvm/boot_linux_amd64.go +++ b/internal/hv/kvm/boot_linux_amd64.go @@ -185,7 +185,7 @@ func bootToConditionWithNVMeBlock(ctx context.Context, kernel []byte, initrd []b return serialOut.String(), err } case ExitMMIO: - if err := handleBootMMIOWithPCI(vm, 0, pci, fsdevs, vsock, rng, netdev, exit.MMIO); err != nil { + if err := handleBootMMIOWithPCI(vm, 0, pci, fsdevs, vsock, rng, nil, netdev, exit.MMIO); err != nil { return serialOut.String(), err } case ExitHLT, ExitShutdown: @@ -253,11 +253,11 @@ func handleUARTIO(uart *serial.UART8250, ioExit IOExit) error { return nil } -func handleBootMMIO(vm *VM, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net, mmio MMIOExit) error { - return handleBootMMIOForVCPU(vm, 0, fsdevs, vsock, rng, netdev, mmio) +func handleBootMMIO(vm *VM, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net, mmio MMIOExit) error { + return handleBootMMIOForVCPU(vm, 0, fsdevs, vsock, rng, balloon, netdev, mmio) } -func handleBootMMIOForVCPU(vm *VM, vcpuIndex int, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net, mmio MMIOExit) error { +func handleBootMMIOForVCPU(vm *VM, vcpuIndex int, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net, mmio MMIOExit) error { for _, fsdev := range fsdevs { if fsdev == nil || !fsdev.Contains(mmio.Addr, int(mmio.Len)) { continue @@ -294,6 +294,17 @@ func handleBootMMIOForVCPU(vm *VM, vcpuIndex int, fsdevs []*virtio.FS, vsock *vi vm.CompleteVCPUMMIORead(vcpuIndex, value, mmio.Len) return nil } + if balloon != nil && balloon.Contains(mmio.Addr, int(mmio.Len)) { + if mmio.Write { + return balloon.Write(mmio.Addr, int(mmio.Len), mmioValue(mmio)) + } + value, err := balloon.Read(mmio.Addr, int(mmio.Len)) + if err != nil { + return err + } + vm.CompleteVCPUMMIORead(vcpuIndex, value, mmio.Len) + return nil + } if netdev != nil && netdev.Contains(mmio.Addr, int(mmio.Len)) { if mmio.Write { return netdev.Write(mmio.Addr, int(mmio.Len), mmioValue(mmio)) diff --git a/internal/hv/kvm/exec_linux_amd64.go b/internal/hv/kvm/exec_linux_amd64.go index b59903e..2f12536 100644 --- a/internal/hv/kvm/exec_linux_amd64.go +++ b/internal/hv/kvm/exec_linux_amd64.go @@ -28,6 +28,10 @@ func RunManagedExecWithFS(ctx context.Context, kernel []byte, initrd []byte, mem } func RunManagedExecWithFSAndNet(ctx context.Context, kernel []byte, initrd []byte, memoryMB uint64, cpus int, dmesg bool, fsdevs []*virtio.FS, netdev *virtio.Net, req client.ExecRequest) (client.ExecResponse, string, error) { + return RunManagedExecWithFSNetAndBalloon(ctx, kernel, initrd, memoryMB, 0, cpus, dmesg, fsdevs, netdev, req) +} + +func RunManagedExecWithFSNetAndBalloon(ctx context.Context, kernel []byte, initrd []byte, memoryMB uint64, balloonMB uint64, cpus int, dmesg bool, fsdevs []*virtio.FS, netdev *virtio.Net, req client.ExecRequest) (client.ExecResponse, string, error) { if len(req.Command) == 0 { return client.ExecResponse{}, "", fmt.Errorf("exec command is required") } @@ -42,6 +46,12 @@ func RunManagedExecWithFSAndNet(ctx context.Context, kernel []byte, initrd []byt vsock := virtio.NewVsock(amd64vm.VsockBase, amd64vm.VsockSize, amd64vm.VsockIRQ, vmruntime.GuestCID, backend) defer vsock.Close() rng := virtio.NewRNG(amd64vm.RNGBase, amd64vm.RNGSize, amd64vm.RNGIRQ) + balloon := virtio.NewBalloon(amd64vm.BalloonBase, amd64vm.BalloonSize, amd64vm.BalloonIRQ) + if targetPages := balloonTargetPages(balloonMB); targetPages != 0 { + if err := balloon.SetTargetPages(targetPages); err != nil { + return client.ExecResponse{}, "", fmt.Errorf("set balloon target: %w", err) + } + } connCh := make(chan virtio.VsockConn, 1) acceptErrCh := make(chan error, 1) @@ -77,6 +87,7 @@ func RunManagedExecWithFSAndNet(ctx context.Context, kernel []byte, initrd []byt } vsock.Attach(vm, vm) rng.Attach(vm, vm) + balloon.Attach(vm, vm) if netdev != nil { netdev.Attach(vm, vm) } @@ -84,6 +95,7 @@ func RunManagedExecWithFSAndNet(ctx context.Context, kernel []byte, initrd []byt extraCmdline := amd64vm.VirtioFSCommandLineArgs(fsdevs) extraCmdline = append(extraCmdline, amd64vm.VirtioMMIODeviceArg(vsock.Base, vsock.IRQ)) extraCmdline = append(extraCmdline, amd64vm.VirtioMMIODeviceArg(rng.Base, rng.IRQ)) + extraCmdline = append(extraCmdline, amd64vm.VirtioMMIODeviceArg(balloon.Base, balloon.IRQ)) if netdev != nil { extraCmdline = append(extraCmdline, amd64vm.VirtioMMIODeviceArg(netdev.Base, netdev.IRQ)) } @@ -132,7 +144,7 @@ func RunManagedExecWithFSAndNet(ctx context.Context, kernel []byte, initrd []byt runDone := make(chan struct{}) go func() { defer close(runDone) - setRunErr(runManagedExecVM(runCtx, vm, uart, fsdevs, vsock, rng, netdev, serialOut)) + setRunErr(runManagedExecVM(runCtx, vm, uart, fsdevs, vsock, rng, balloon, netdev, serialOut)) }() defer func() { cancel() @@ -196,16 +208,16 @@ func RunManagedExecWithFSAndNet(ctx context.Context, kernel []byte, initrd []byt return client.ExecResponse{ExitCode: code, Output: output, Usage: usage}, serialOut.String(), nil } -func runManagedExecVM(ctx context.Context, vm *VM, uart *serial.UART8250, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net, serialOut *vmruntime.SerialTranscript) error { - return runManagedExecVMWithSnapshot(ctx, vm, uart, fsdevs, vsock, rng, netdev, serialOut, nil) +func runManagedExecVM(ctx context.Context, vm *VM, uart *serial.UART8250, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net, serialOut *vmruntime.SerialTranscript) error { + return runManagedExecVMWithSnapshot(ctx, vm, uart, fsdevs, vsock, rng, balloon, netdev, serialOut, nil) } -func runManagedExecVMWithSnapshot(ctx context.Context, vm *VM, uart *serial.UART8250, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net, serialOut *vmruntime.SerialTranscript, snapshot *snapshotTrigger) error { +func runManagedExecVMWithSnapshot(ctx context.Context, vm *VM, uart *serial.UART8250, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net, serialOut *vmruntime.SerialTranscript, snapshot *snapshotTrigger) error { if vm != nil && len(vm.vcpus) > 1 { if snapshot != nil { return fmt.Errorf("KVM startup snapshots currently support only one vCPU") } - return runManagedExecVMMulti(ctx, vm, uart, fsdevs, vsock, rng, netdev, serialOut) + return runManagedExecVMMulti(ctx, vm, uart, fsdevs, vsock, rng, balloon, netdev, serialOut) } runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -243,7 +255,7 @@ func runManagedExecVMWithSnapshot(ctx context.Context, vm *VM, uart *serial.UART } else if handled { break } - if err := handleBootMMIO(vm, fsdevs, vsock, rng, netdev, exit.MMIO); err != nil { + if err := handleBootMMIO(vm, fsdevs, vsock, rng, balloon, netdev, exit.MMIO); err != nil { return err } case ExitShutdown: @@ -254,13 +266,13 @@ func runManagedExecVMWithSnapshot(ctx context.Context, vm *VM, uart *serial.UART pc, _ := vm.GetPC() return fmt.Errorf("unexpected exit reason %d at pc=%#x\nserial:\n%s", exit.Reason, pc, serialOut.String()) } - if err := snapshot.captureIfPending(vm, fsdevs, vsock, rng, netdev); err != nil { + if err := snapshot.captureIfPending(vm, fsdevs, vsock, rng, balloon, netdev); err != nil { return err } } } -func runManagedExecVMMulti(ctx context.Context, vm *VM, uart *serial.UART8250, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net, serialOut *vmruntime.SerialTranscript) error { +func runManagedExecVMMulti(ctx context.Context, vm *VM, uart *serial.UART8250, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net, serialOut *vmruntime.SerialTranscript) error { runCtx, cancel := context.WithCancel(ctx) defer cancel() sampler := startKVMRegisterSampler(runCtx, vm) @@ -292,7 +304,7 @@ func runManagedExecVMMulti(ctx context.Context, vm *VM, uart *serial.UART8250, f return } exitMu.Lock() - err = handleManagedExit(vm, index, uart, fsdevs, vsock, rng, netdev, serialOut, exit) + err = handleManagedExit(vm, index, uart, fsdevs, vsock, rng, balloon, netdev, serialOut, exit) exitMu.Unlock() if err != nil { reportRunErr(errCh, cancel, err) @@ -388,14 +400,14 @@ func reportRunErr(errCh chan<- error, cancel context.CancelFunc, err error) { } } -func handleManagedExit(vm *VM, vcpuIndex int, uart *serial.UART8250, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net, serialOut *vmruntime.SerialTranscript, exit Exit) error { +func handleManagedExit(vm *VM, vcpuIndex int, uart *serial.UART8250, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net, serialOut *vmruntime.SerialTranscript, exit Exit) error { switch exit.Reason { case ExitIO: if err := handleBootIO(uart, exit.IO); err != nil { return err } case ExitMMIO: - if err := handleBootMMIOForVCPU(vm, vcpuIndex, fsdevs, vsock, rng, netdev, exit.MMIO); err != nil { + if err := handleBootMMIOForVCPU(vm, vcpuIndex, fsdevs, vsock, rng, balloon, netdev, exit.MMIO); err != nil { return err } case ExitHLT: diff --git a/internal/hv/kvm/freebsd_boot_linux_amd64.go b/internal/hv/kvm/freebsd_boot_linux_amd64.go index d9fcd6d..9659c0a 100644 --- a/internal/hv/kvm/freebsd_boot_linux_amd64.go +++ b/internal/hv/kvm/freebsd_boot_linux_amd64.go @@ -130,7 +130,7 @@ func bootFreeBSDToCondition(ctx context.Context, kernel []byte, memoryMB uint64, return serialOut.String(), err } case ExitMMIO: - if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, exit.MMIO); err != nil { + if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, nil, exit.MMIO); err != nil { return serialOut.String(), fmt.Errorf("unhandled FreeBSD mmio addr=%#x len=%d write=%v: %w", exit.MMIO.Addr, exit.MMIO.Len, exit.MMIO.Write, err) } case ExitHLT: diff --git a/internal/hv/kvm/freebsd_session_linux_amd64.go b/internal/hv/kvm/freebsd_session_linux_amd64.go index eb084bd..8fd818f 100644 --- a/internal/hv/kvm/freebsd_session_linux_amd64.go +++ b/internal/hv/kvm/freebsd_session_linux_amd64.go @@ -158,7 +158,7 @@ func runFreeBSDManagedVM(ctx context.Context, vm *VM, uart *serial.UART8250, pci return err } case ExitMMIO: - if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, exit.MMIO); err != nil { + if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, nil, exit.MMIO); err != nil { return fmt.Errorf("unhandled FreeBSD mmio addr=%#x len=%d write=%v: %w", exit.MMIO.Addr, exit.MMIO.Len, exit.MMIO.Write, err) } case ExitHLT: diff --git a/internal/hv/kvm/host_linux_amd64.go b/internal/hv/kvm/host_linux_amd64.go index 29d718a..8f255cc 100644 --- a/internal/hv/kvm/host_linux_amd64.go +++ b/internal/hv/kvm/host_linux_amd64.go @@ -24,6 +24,7 @@ type LinuxManagedMachine struct { Initrd []byte FSDevices []*virtio.FS NetDevice *virtio.Net + BalloonMB uint64 SnapshotDir string RestoreSnapshot string } @@ -31,6 +32,7 @@ type LinuxManagedMachine struct { type LinuxManagedAttachments struct { FSDevices []*virtio.FS NetDevice *virtio.Net + BalloonMB uint64 SnapshotDir string RestoreSnapshot string } @@ -76,6 +78,7 @@ func (Host) startLinux(ctx context.Context, req managedhost.StartRequest, onEven Initrd: req.Artifact.Initrd, FSDevices: attachments.FSDevices, NetDevice: attachments.NetDevice, + BalloonMB: attachments.BalloonMB, SnapshotDir: strings.TrimSpace(attachments.SnapshotDir), RestoreSnapshot: strings.TrimSpace(attachments.RestoreSnapshot), }, onEvent) @@ -164,6 +167,7 @@ func (Host) StartLinuxManaged(ctx context.Context, machine LinuxManagedMachine, ManagedSessionOptions{ SnapshotDir: strings.TrimSpace(machine.SnapshotDir), RestoreSnapshot: strings.TrimSpace(machine.RestoreSnapshot), + BalloonMB: machine.BalloonMB, }, onEvent, ) diff --git a/internal/hv/kvm/netbsd_boot_linux_amd64.go b/internal/hv/kvm/netbsd_boot_linux_amd64.go index 9b475ad..cc642c5 100644 --- a/internal/hv/kvm/netbsd_boot_linux_amd64.go +++ b/internal/hv/kvm/netbsd_boot_linux_amd64.go @@ -110,7 +110,7 @@ func bootNetBSDKernelToSerial(ctx context.Context, kernel []byte, memoryMB uint6 return serialOut.String(), err } case ExitMMIO: - if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, exit.MMIO); err != nil { + if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, nil, exit.MMIO); err != nil { return serialOut.String(), fmt.Errorf("unhandled NetBSD mmio addr=%#x len=%d write=%v: %w", exit.MMIO.Addr, exit.MMIO.Len, exit.MMIO.Write, err) } case ExitHLT: diff --git a/internal/hv/kvm/netbsd_session_linux_amd64.go b/internal/hv/kvm/netbsd_session_linux_amd64.go index 50c480e..42bb073 100644 --- a/internal/hv/kvm/netbsd_session_linux_amd64.go +++ b/internal/hv/kvm/netbsd_session_linux_amd64.go @@ -162,7 +162,7 @@ func runNetBSDManagedVM(ctx context.Context, vm *VM, uart *serial.UART8250, pci return err } case ExitMMIO: - if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, exit.MMIO); err != nil { + if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, nil, exit.MMIO); err != nil { return fmt.Errorf("unhandled NetBSD mmio addr=%#x len=%d write=%v: %w", exit.MMIO.Addr, exit.MMIO.Len, exit.MMIO.Write, err) } case ExitHLT: diff --git a/internal/hv/kvm/openbsd_boot_linux_amd64.go b/internal/hv/kvm/openbsd_boot_linux_amd64.go index f228f70..53473ae 100644 --- a/internal/hv/kvm/openbsd_boot_linux_amd64.go +++ b/internal/hv/kvm/openbsd_boot_linux_amd64.go @@ -161,7 +161,7 @@ func bootOpenBSDToCondition(ctx context.Context, kernel []byte, memoryMB uint64, return serialOut.String(), err } case ExitMMIO: - if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, exit.MMIO); err != nil { + if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, nil, exit.MMIO); err != nil { return serialOut.String(), fmt.Errorf("unhandled OpenBSD mmio addr=%#x len=%d write=%v: %w", exit.MMIO.Addr, exit.MMIO.Len, exit.MMIO.Write, err) } case ExitHLT: diff --git a/internal/hv/kvm/openbsd_session_linux_amd64.go b/internal/hv/kvm/openbsd_session_linux_amd64.go index ad6ed96..8186b8f 100644 --- a/internal/hv/kvm/openbsd_session_linux_amd64.go +++ b/internal/hv/kvm/openbsd_session_linux_amd64.go @@ -171,7 +171,7 @@ func runOpenBSDManagedVM(ctx context.Context, vm *VM, uart *serial.UART8250, pci return err } case ExitMMIO: - if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, exit.MMIO); err != nil { + if err := handleBootMMIOWithPCI(vm, 0, pci, nil, nil, nil, nil, nil, exit.MMIO); err != nil { return fmt.Errorf("unhandled OpenBSD mmio addr=%#x len=%d write=%v: %w", exit.MMIO.Addr, exit.MMIO.Len, exit.MMIO.Write, err) } case ExitHLT: diff --git a/internal/hv/kvm/pci_amd64.go b/internal/hv/kvm/pci_amd64.go index 9b011ca..b7dbf7a 100644 --- a/internal/hv/kvm/pci_amd64.go +++ b/internal/hv/kvm/pci_amd64.go @@ -342,7 +342,7 @@ func handleBootIOWithPCI(uartIO func(IOExit) error, pci *PCIBus, ioExit IOExit) return uartIO(ioExit) } -func handleBootMMIOWithPCI(vm *VM, vcpuIndex int, pci *PCIBus, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net, mmio MMIOExit) error { +func handleBootMMIOWithPCI(vm *VM, vcpuIndex int, pci *PCIBus, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net, mmio MMIOExit) error { if handled, value, err := pci.HandleMMIO(mmio); handled || err != nil { if err != nil { return err @@ -352,7 +352,7 @@ func handleBootMMIOWithPCI(vm *VM, vcpuIndex int, pci *PCIBus, fsdevs []*virtio. } return nil } - return handleBootMMIOForVCPU(vm, vcpuIndex, fsdevs, vsock, rng, netdev, mmio) + return handleBootMMIOForVCPU(vm, vcpuIndex, fsdevs, vsock, rng, balloon, netdev, mmio) } func readLittleValue(data []byte) uint64 { diff --git a/internal/hv/kvm/session_linux_amd64.go b/internal/hv/kvm/session_linux_amd64.go index 4c8882d..8aa310b 100644 --- a/internal/hv/kvm/session_linux_amd64.go +++ b/internal/hv/kvm/session_linux_amd64.go @@ -38,6 +38,7 @@ type ManagedSession struct { type ManagedSessionOptions struct { SnapshotDir string RestoreSnapshot string + BalloonMB uint64 } func StartManagedSession(ctx context.Context, kernel []byte, initrd []byte, memoryMB uint64, cpus int, dmesg bool, fsdevs []*virtio.FS, onEvent func(client.BootEvent) error) (*ManagedSession, error) { @@ -55,7 +56,7 @@ func StartManagedSessionWithNetOptions(ctx context.Context, kernel []byte, initr } } if snapshotPath := strings.TrimSpace(opts.RestoreSnapshot); snapshotPath != "" { - return StartManagedSessionFromSnapshot(ctx, snapshotPath, memoryMB, dmesg, fsdevs, netdev, onEvent) + return StartManagedSessionFromSnapshot(ctx, snapshotPath, memoryMB, opts.BalloonMB, dmesg, fsdevs, netdev, onEvent) } if err := emitManagedBootStatus(onEvent, "starting VM"); err != nil { return nil, err @@ -68,6 +69,14 @@ func StartManagedSessionWithNetOptions(ctx context.Context, kernel []byte, initr vsock := virtio.NewVsock(amd64vm.VsockBase, amd64vm.VsockSize, amd64vm.VsockIRQ, vmruntime.GuestCID, backend) rng := virtio.NewRNG(amd64vm.RNGBase, amd64vm.RNGSize, amd64vm.RNGIRQ) + balloon := virtio.NewBalloon(amd64vm.BalloonBase, amd64vm.BalloonSize, amd64vm.BalloonIRQ) + if targetPages := balloonTargetPages(opts.BalloonMB); targetPages != 0 { + if err := balloon.SetTargetPages(targetPages); err != nil { + _ = listener.Close() + vsock.Close() + return nil, fmt.Errorf("set balloon target: %w", err) + } + } connCh := make(chan virtio.VsockConn, 1) acceptErrCh := make(chan error, 1) controlTranscript := vmruntime.NewSerialTranscript() @@ -112,6 +121,7 @@ func StartManagedSessionWithNetOptions(ctx context.Context, kernel []byte, initr } vsock.Attach(vm, vm) rng.Attach(vm, vm) + balloon.Attach(vm, vm) if netdev != nil { netdev.Attach(vm, vm) } @@ -119,6 +129,7 @@ func StartManagedSessionWithNetOptions(ctx context.Context, kernel []byte, initr extraCmdline := amd64vm.VirtioFSCommandLineArgs(fsdevs) extraCmdline = append(extraCmdline, amd64vm.VirtioMMIODeviceArg(vsock.Base, vsock.IRQ)) extraCmdline = append(extraCmdline, amd64vm.VirtioMMIODeviceArg(rng.Base, rng.IRQ)) + extraCmdline = append(extraCmdline, amd64vm.VirtioMMIODeviceArg(balloon.Base, balloon.IRQ)) if netdev != nil { extraCmdline = append(extraCmdline, amd64vm.VirtioMMIODeviceArg(netdev.Base, netdev.IRQ)) } @@ -151,7 +162,7 @@ func StartManagedSessionWithNetOptions(ctx context.Context, kernel []byte, initr runCtx, cancel := context.WithCancel(context.Background()) done := newSessionDone() go func() { - err := runManagedExecVMWithSnapshot(runCtx, vm, uart, fsdevs, vsock, rng, netdev, serialOut, snapshot) + err := runManagedExecVMWithSnapshot(runCtx, vm, uart, fsdevs, vsock, rng, balloon, netdev, serialOut, snapshot) closeVMWithFS(vm, fsdevs) done.finish(err) }() diff --git a/internal/hv/kvm/snapshot_linux_amd64.go b/internal/hv/kvm/snapshot_linux_amd64.go index a076820..98f7bbb 100644 --- a/internal/hv/kvm/snapshot_linux_amd64.go +++ b/internal/hv/kvm/snapshot_linux_amd64.go @@ -173,7 +173,7 @@ func (w *snapshotSerialWriter) Write(data []byte) (int, error) { return w.dst.Write(data) } -func (s *snapshotTrigger) captureIfPending(vm *VM, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net) error { +func (s *snapshotTrigger) captureIfPending(vm *VM, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net) error { if s == nil { return nil } @@ -182,7 +182,7 @@ func (s *snapshotTrigger) captureIfPending(vm *VM, fsdevs []*virtio.FS, vsock *v return nil } s.once.Do(func() { - s.err = s.capture(vm, fsdevs, vsock, rng, netdev, value) + s.err = s.capture(vm, fsdevs, vsock, rng, balloon, netdev, value) }) if s.err != nil { return fmt.Errorf("capture KVM snapshot: %w", s.err) @@ -190,7 +190,7 @@ func (s *snapshotTrigger) captureIfPending(vm *VM, fsdevs []*virtio.FS, vsock *v return nil } -func (s *snapshotTrigger) capture(vm *VM, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net, value uint64) error { +func (s *snapshotTrigger) capture(vm *VM, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net, value uint64) error { if s == nil || strings.TrimSpace(s.dir) == "" { return nil } @@ -235,7 +235,7 @@ func (s *snapshotTrigger) capture(vm *VM, fsdevs []*virtio.FS, vsock *virtio.Vso IRQChips: irqChips, PIT: pit, Clock: clock, - Devices: snapshotKVMDeviceStates(fsdevs, vsock, rng, netdev), + Devices: snapshotKVMDeviceStates(fsdevs, vsock, rng, balloon, netdev), Note: "KVM Linux checkpoint captured after guest init configured non-unique state and before vsock ready.", } data, err := json.MarshalIndent(manifest, "", " ") @@ -283,7 +283,7 @@ func allZero(data []byte) bool { return true } -func StartManagedSessionFromSnapshot(ctx context.Context, snapshotPath string, memoryMB uint64, dmesg bool, fsdevs []*virtio.FS, netdev *virtio.Net, onEvent func(client.BootEvent) error) (*ManagedSession, error) { +func StartManagedSessionFromSnapshot(ctx context.Context, snapshotPath string, memoryMB uint64, balloonMB uint64, dmesg bool, fsdevs []*virtio.FS, netdev *virtio.Net, onEvent func(client.BootEvent) error) (*ManagedSession, error) { if err := emitManagedBootStatus(onEvent, "restoring VM snapshot"); err != nil { return nil, err } @@ -303,6 +303,14 @@ func StartManagedSessionFromSnapshot(ctx context.Context, snapshotPath string, m return nil, fmt.Errorf("listen vsock control: %w", err) } vsock := virtio.NewVsock(amd64vm.VsockBase, amd64vm.VsockSize, amd64vm.VsockIRQ, vmruntime.GuestCID, backend) + balloon := virtio.NewBalloon(amd64vm.BalloonBase, amd64vm.BalloonSize, amd64vm.BalloonIRQ) + if targetPages := balloonTargetPages(balloonMB); targetPages != 0 { + if err := balloon.SetTargetPages(targetPages); err != nil { + _ = listener.Close() + vsock.Close() + return nil, fmt.Errorf("set balloon target: %w", err) + } + } connCh := make(chan virtio.VsockConn, 1) acceptErrCh := make(chan error, 1) controlTranscript := vmruntime.NewSerialTranscript() @@ -322,7 +330,7 @@ func StartManagedSessionFromSnapshot(ctx context.Context, snapshotPath string, m bootWriter = vmruntime.NewBootEventWriter(onEvent) serialWriter = bootWriter } - vm, uart, rng, serialOut, err := restoreManagedVMFromSnapshot(manifest, memPath, memoryMB, dmesg, fsdevs, vsock, netdev, serialWriter) + vm, uart, rng, serialOut, err := restoreManagedVMFromSnapshot(manifest, memPath, memoryMB, dmesg, fsdevs, vsock, balloon, netdev, serialWriter) if err != nil { _ = listener.Close() vsock.Close() @@ -335,7 +343,7 @@ func StartManagedSessionFromSnapshot(ctx context.Context, snapshotPath string, m runCtx, cancel := context.WithCancel(context.Background()) done := newSessionDone() go func() { - err := runManagedExecVM(runCtx, vm, uart, fsdevs, vsock, rng, netdev, serialOut) + err := runManagedExecVM(runCtx, vm, uart, fsdevs, vsock, rng, balloon, netdev, serialOut) closeVMWithFS(vm, fsdevs) done.finish(err) }() @@ -421,7 +429,7 @@ func StartManagedSessionFromSnapshot(ctx context.Context, snapshotPath string, m }, nil } -func restoreManagedVMFromSnapshot(manifest kvmSnapshotManifest, memPath string, memoryMB uint64, dmesg bool, fsdevs []*virtio.FS, vsock *virtio.Vsock, netdev *virtio.Net, serialWriter io.Writer) (*VM, *serial.UART8250, *virtio.RNG, *vmruntime.SerialTranscript, error) { +func restoreManagedVMFromSnapshot(manifest kvmSnapshotManifest, memPath string, memoryMB uint64, dmesg bool, fsdevs []*virtio.FS, vsock *virtio.Vsock, balloon *virtio.Balloon, netdev *virtio.Net, serialWriter io.Writer) (*VM, *serial.UART8250, *virtio.RNG, *vmruntime.SerialTranscript, error) { memorySize := amd64vm.MemorySizeBytes(memoryMB) if manifest.MemorySize != 0 && manifest.MemorySize != memorySize { return nil, nil, nil, nil, fmt.Errorf("snapshot memory size %d does not match requested VM memory %d", manifest.MemorySize, memorySize) @@ -457,10 +465,13 @@ func restoreManagedVMFromSnapshot(manifest kvmSnapshotManifest, memPath string, vsock.Attach(vm, vm) } rng.Attach(vm, vm) + if balloon != nil { + balloon.Attach(vm, vm) + } if netdev != nil { netdev.Attach(vm, vm) } - if err := restoreKVMDeviceStates(manifest.Devices, fsdevs, vsock, rng, netdev); err != nil { + if err := restoreKVMDeviceStates(manifest.Devices, fsdevs, vsock, rng, balloon, netdev); err != nil { closeVMWithFS(vm, fsdevs) return nil, nil, nil, serialOut, err } @@ -682,7 +693,7 @@ func restoreKVMIRQChips(vmFd int, chips []kvmIRQChip) error { return nil } -func snapshotKVMDeviceStates(fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net) map[string]virtio.MMIOState { +func snapshotKVMDeviceStates(fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net) map[string]virtio.MMIOState { out := map[string]virtio.MMIOState{} if rng != nil { out["rng"] = rng.SnapshotState() @@ -690,6 +701,9 @@ func snapshotKVMDeviceStates(fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virt if vsock != nil { out["vsock"] = vsock.SnapshotState() } + if balloon != nil { + out["balloon"] = balloon.SnapshotState() + } if netdev != nil { out["net"] = netdev.SnapshotState() } @@ -702,7 +716,7 @@ func snapshotKVMDeviceStates(fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virt return out } -func restoreKVMDeviceStates(states map[string]virtio.MMIOState, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, netdev *virtio.Net) error { +func restoreKVMDeviceStates(states map[string]virtio.MMIOState, fsdevs []*virtio.FS, vsock *virtio.Vsock, rng *virtio.RNG, balloon *virtio.Balloon, netdev *virtio.Net) error { if state, ok := states["rng"]; ok && rng != nil { if err := rng.RestoreState(state); err != nil { return fmt.Errorf("restore rng state: %w", err) @@ -713,6 +727,11 @@ func restoreKVMDeviceStates(states map[string]virtio.MMIOState, fsdevs []*virtio return fmt.Errorf("restore vsock state: %w", err) } } + if state, ok := states["balloon"]; ok && balloon != nil { + if err := balloon.RestoreState(state); err != nil { + return fmt.Errorf("restore balloon state: %w", err) + } + } if state, ok := states["net"]; ok && netdev != nil { if err := netdev.RestoreState(state); err != nil { return fmt.Errorf("restore net state: %w", err) diff --git a/internal/hv/kvm/vm_linux_amd64.go b/internal/hv/kvm/vm_linux_amd64.go index 71d1a9b..c0fab3b 100644 --- a/internal/hv/kvm/vm_linux_amd64.go +++ b/internal/hv/kvm/vm_linux_amd64.go @@ -566,6 +566,31 @@ func (v *VM) WriteIPA(addr uint64, data []byte) error { return nil } +func (v *VM) ReclaimGuestPage(ipa uint64) error { + return v.madviseGuestPage(ipa, unix.MADV_DONTNEED) +} + +func (v *VM) ReuseGuestPage(ipa uint64) error { + return nil +} + +func (v *VM) madviseGuestPage(ipa uint64, advice int) error { + if v == nil { + return fmt.Errorf("vm is nil") + } + const pageSize = 4096 + if ipa%pageSize != 0 { + return fmt.Errorf("guest page %#x is not page aligned", ipa) + } + v.lifecycleMu.RLock() + defer v.lifecycleMu.RUnlock() + region, off, ok := v.findMemoryRegion(ipa) + if !ok || off+pageSize > uint64(len(region)) { + return fmt.Errorf("guest page %#x: unmapped", ipa) + } + return unix.Madvise(region[off:off+pageSize], advice) +} + func (v *VM) copyFromGuest(addr uint64, out []byte) error { for len(out) > 0 { region, off, ok := v.findMemoryRegion(addr) diff --git a/internal/vm/backend_linux_amd64.go b/internal/vm/backend_linux_amd64.go index 837ef72..972e2c9 100644 --- a/internal/vm/backend_linux_amd64.go +++ b/internal/vm/backend_linux_amd64.go @@ -113,6 +113,7 @@ func (b *runtimeBackend) StartStream(ctx context.Context, req client.CreateInsta Attachments: kvm.LinuxManagedAttachments{ FSDevices: fsdevs, NetDevice: networkDevice(network), + BalloonMB: req.BalloonMB, SnapshotDir: strings.TrimSpace(req.SnapshotDir), RestoreSnapshot: strings.TrimSpace(req.RestoreSnapshot), }, @@ -230,6 +231,7 @@ func (b *runtimeBackend) StartBlankStream(ctx context.Context, req client.StartI Attachments: kvm.LinuxManagedAttachments{ FSDevices: fsdevs, NetDevice: networkDevice(network), + BalloonMB: req.BalloonMB, SnapshotDir: strings.TrimSpace(req.SnapshotDir), RestoreSnapshot: strings.TrimSpace(req.RestoreSnapshot), }, @@ -414,7 +416,7 @@ func (b *runtimeBackend) Run(ctx context.Context, req client.RunRequest) (client Cols: req.Cols, Rows: req.Rows, } - resp, serial, err := kvm.RunManagedExecWithFSAndNet(ctx, kernel, initrd, req.MemoryMB, req.CPUs, req.Dmesg, fsdevs, networkDevice(network), execReq) + resp, serial, err := kvm.RunManagedExecWithFSNetAndBalloon(ctx, kernel, initrd, req.MemoryMB, req.BalloonMB, req.CPUs, req.Dmesg, fsdevs, networkDevice(network), execReq) if err != nil && resp.Output == "" { resp.Output = serial } @@ -713,7 +715,7 @@ func linuxGuestInitConfig(modules []alpine.Module, managedExec bool, network *cl } func linuxRuntimeConfigVars(image *oci.Image, extraModules ...string) []string { - vars := []string{"CONFIG_VIRTIO_MMIO", "CONFIG_FUSE_FS", "CONFIG_VIRTIO_FS", "CONFIG_VSOCKETS", "CONFIG_VIRTIO_VSOCKETS", "CONFIG_HW_RANDOM", "CONFIG_HW_RANDOM_VIRTIO", "CONFIG_VIRTIO_NET", "CONFIG_OVERLAY_FS"} + vars := []string{"CONFIG_VIRTIO_MMIO", "CONFIG_VIRTIO_BALLOON", "CONFIG_FUSE_FS", "CONFIG_VIRTIO_FS", "CONFIG_VSOCKETS", "CONFIG_VIRTIO_VSOCKETS", "CONFIG_HW_RANDOM", "CONFIG_HW_RANDOM_VIRTIO", "CONFIG_VIRTIO_NET", "CONFIG_OVERLAY_FS"} if kvmhost.RootFSImageEnabled() { vars = append(vars, "CONFIG_BLK_DEV_LOOP") rootImageType, err := kvmhost.RootFSImageType() @@ -776,6 +778,7 @@ func linuxRuntimeExtraConfigVars(names []string) []string { func linuxRuntimeModuleMap() map[string]string { return map[string]string{ "CONFIG_VIRTIO_MMIO": "kernel/drivers/virtio/virtio_mmio.ko.gz", + "CONFIG_VIRTIO_BALLOON": "kernel/drivers/virtio/virtio_balloon.ko.gz", "CONFIG_FUSE_FS": "kernel/fs/fuse/fuse.ko.gz", "CONFIG_VIRTIO_FS": "kernel/fs/fuse/virtiofs.ko.gz", "CONFIG_VSOCKETS": "kernel/net/vmw_vsock/vsock.ko.gz", diff --git a/internal/vm/runtime_boot_test.go b/internal/vm/runtime_boot_test.go index e837d0a..d68b0e9 100644 --- a/internal/vm/runtime_boot_test.go +++ b/internal/vm/runtime_boot_test.go @@ -61,9 +61,9 @@ func TestRuntimeBootsLinuxAndRunsOneShotCommand(t *testing.T) { requireGuestOutput(t, resp.Output, "runtime-one-shot", "Linux", "machine=") } -func TestRuntimeBootsLinuxWithHVFBalloon(t *testing.T) { - if runtime.GOOS != "darwin" { - t.Skip("HVF balloon support is implemented by the Darwin backend") +func TestRuntimeBootsLinuxWithVirtioBalloon(t *testing.T) { + if runtime.GOOS != "darwin" && !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") { + t.Skip("virtio balloon support is implemented by the Darwin arm64 and Linux amd64 backends") } env := newRuntimeBootEnv(t) ctx, cancel := context.WithTimeout(context.Background(), runtimeBootTimeout())