183 lines
4.7 KiB
Go
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 ""
|
|
}
|