Files
2026-10-07 12:21:09 +01:00

183 lines
4.7 KiB
Go

package main
import (
"encoding/binary"
"errors"
"fmt"
"os"
"strconv"
"strings"
"syscall"
"unsafe"
)
const (
sysPidfdOpen = 434 // same number on every arch
sysProcessMadvise = 440
madvPageout = 21
swapFlagPrefer = 0x8000
swapFlagPrioMask = 0x7fff
)
// swapIn faults every swapped-out page of pid back into RAM by reading it
// through /proc/pid/mem. Pages are located via /proc/pid/pagemap so only
// swapped pages are touched. Needs ptrace access to the target.
func swapIn(pid int, progress func(doneKB, totalKB uint64)) error {
maps, err := readSmaps(pid)
if err != nil {
return err
}
dir := "/proc/" + strconv.Itoa(pid)
mem, err := os.Open(dir + "/mem")
if err != nil {
return err
}
defer mem.Close()
pm, err := os.Open(dir + "/pagemap")
if err != nil {
return err
}
defer pm.Close()
page := uint64(os.Getpagesize())
var total, done uint64
for _, m := range maps {
total += m.Swap
}
const chunk = 4096 // pagemap entries per read
ents := make([]byte, chunk*8)
buf := make([]byte, 256*page)
swapped := func(i int) bool {
e := binary.LittleEndian.Uint64(ents[i*8:])
return e&(1<<62) != 0 && e&(1<<63) == 0 // in swap, not present
}
for _, m := range maps {
if m.Swap == 0 {
continue
}
if shmem(m) {
// swapped shmem pages leave no swap PTE behind (the entry lives in the
// page cache), so pagemap can't find them: just read the whole range.
for addr := m.Start; addr < m.End; addr += uint64(len(buf)) {
n := min(m.End-addr, uint64(len(buf)))
mem.ReadAt(buf[:n], int64(addr))
done += n / 1024
progress(min(done, total), total)
}
continue
}
for addr := m.Start; addr < m.End; addr += chunk * page {
n := min((m.End-addr)/page, chunk)
got, err := pm.ReadAt(ents[:n*8], int64(addr/page*8))
if got == 0 && err != nil {
break
}
n = uint64(got / 8)
for i := 0; i < int(n); {
if !swapped(i) {
i++
continue
}
j := i + 1
for j < int(n) && j-i < 256 && swapped(j) {
j++
}
mem.ReadAt(buf[:uint64(j-i)*page], int64(addr+uint64(i)*page)) // errors (guard pages etc.) are fine
done += uint64(j-i) * page / 1024
progress(done, total)
i = j
}
}
}
return nil
}
// swapOut asks the kernel to reclaim pid's resident pages right now
// (process_madvise MADV_PAGEOUT, Linux 5.10+). Anonymous pages go to swap,
// clean file-backed pages are simply dropped. Returns bytes advised.
func swapOut(pid int) (uint64, error) {
maps, err := readSmaps(pid)
if err != nil {
return 0, err
}
fd, _, e := syscall.Syscall(sysPidfdOpen, uintptr(pid), 0, 0)
if e != 0 {
return 0, fmt.Errorf("pidfd_open: %w", e)
}
defer syscall.Close(int(fd))
var advised uint64
var lastErr error
for _, m := range maps {
if m.Rss == 0 || skipPageout(m) {
continue
}
iov := struct{ base, len uintptr }{uintptr(m.Start), uintptr(m.End - m.Start)}
n, _, e := syscall.Syscall6(sysProcessMadvise, fd, uintptr(unsafe.Pointer(&iov)), 1, madvPageout, 0, 0)
if e != 0 {
lastErr = e
continue
}
advised += uint64(n)
}
if advised == 0 && lastErr != nil {
if errors.Is(lastErr, syscall.ENOSYS) {
return 0, errors.New("process_madvise unsupported (needs Linux 5.10+)")
}
return 0, fmt.Errorf("process_madvise: %w", lastErr)
}
return advised, nil
}
// ponytail: heuristic; a MAP_PRIVATE mapping of a tmpfs file is missed.
func shmem(m Mapping) bool {
if len(m.Perms) > 3 && m.Perms[3] == 's' {
return true
}
for _, p := range []string{"/dev/zero", "/dev/shm/", "/memfd:", "/SYSV"} {
if strings.HasPrefix(m.Path, p) {
return true
}
}
return false
}
func skipPageout(m Mapping) bool {
switch m.Path {
case "[vsyscall]", "[vvar]", "[vdso]":
return true
}
for _, f := range []string{" lo", " pf", " io", " ht"} { // locked, pfnmap, io, hugetlb
if strings.Contains(" "+m.Flags+" ", f+" ") {
return true
}
}
return false
}
// flushDev empties a swap device by swapoff+swapon, keeping its priority.
// Needs root and enough free RAM to hold everything currently on the device.
func flushDev(d SwapDev) error {
p, err := syscall.BytePtrFromString(d.Path)
if err != nil {
return err
}
if _, _, e := syscall.Syscall(syscall.SYS_SWAPOFF, uintptr(unsafe.Pointer(p)), 0, 0); e != 0 {
return fmt.Errorf("swapoff %s: %w", d.Path, e)
}
flags := uintptr(0)
if d.Prio >= 0 {
flags = swapFlagPrefer | uintptr(d.Prio&swapFlagPrioMask)
}
if _, _, e := syscall.Syscall(syscall.SYS_SWAPON, uintptr(unsafe.Pointer(p)), flags, 0); e != 0 {
return fmt.Errorf("swapon %s: %w (device is now OFF - re-enable manually)", d.Path, e)
}
return nil
}
func permHint(err error) string {
if errors.Is(err, os.ErrPermission) || errors.Is(err, syscall.EPERM) || errors.Is(err, syscall.EACCES) {
return " (try: sudo swaptop)"
}
return ""
}