Initial Version
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user