process_madvise: memory pressure hints across process boundaries
Published by RodHat · Updated

Classic madvise(2) has one limitation: it only works on memory you own. If you are a container runtime, a checkpoint tool, or a memory advisor daemon watching another process from the outside, you can see its mappings in /proc/<pid>/maps but you cannot hint the kernel to do anything about them. Until Linux 5.10, your options were to signal the process and hope it calls madvise itself, or use ptrace to inject a syscall and feel terrible about it.
process_madvise(2) fills this gap. A short list of the madvise(2) advice values, and the first argument is a pidfd and the address ranges are an iovec referencing virtual addresses inside the target process.
#include <sys/mman.h>
#include <sys/uio.h>
#include <sys/syscall.h>
#include <unistd.h>
ssize_t process_madvise(int pidfd,
const struct iovec *iovec, size_t vlen,
int advice, unsigned int flags);
As of Linux 5.10, no flags are defined; pass 0.
Getting the pidfd
int pidfd = pidfd_open(target_pid, 0);
if (pidfd == -1) err(1, "pidfd_open");
pidfd_open(2) landed in Linux 5.3. If you are spawning the process yourself, clone(2) with CLONE_PIDFD stores the fd directly. The pidfd post covers both paths.
Using a pidfd rather than a raw PID means the call is atomic with respect to PID reuse. If the target exits between pidfd_open and process_madvise, you get ESRCH rather than accidentally advising whatever process inherited the PID next.
The advice values that matter
MADV_PAGEOUT is the reason this syscall exists. It triggers immediate reclaim: anonymous pages go to swap, file-backed pages get cleaned and returned to the page cache. The target does not pause; it faults pages back on next access. The cost is real if the process is actively running, but it shrinks the memory footprint you need to serialize during checkpoint.
MADV_COLD is cheaper. It moves the named pages toward the cold end of the LRU without forcing reclaim. Android’s Low Memory Killer Daemon uses both: MADV_COLD on background processes to age their pages out of the hot tier, then MADV_PAGEOUT when the system is under real pressure and a process has been selected as the next kill target. It reclaims before killing so other processes get the benefit of those freed pages immediately rather than waiting for the kernel to collect the corpse.
That’s most of the menu. Against another process the kernel accepts exactly four: MADV_COLD, MADV_PAGEOUT, MADV_WILLNEED, and MADV_COLLAPSE. MADV_FREE and MADV_DONTNEED are not on it, and for good reason: letting one process discard another’s memory contents is how you get a very confusing bug report. (Since 6.13, any advice is allowed when the pidfd is your own process, which is just madvise with extra steps.) The FREE/DONTNEED distinction for your own memory is in the MADV_FREE RSS post.
Building the iovec from /proc/pid/maps
You do not know the target’s virtual layout from your own address space. Parse it from /proc/<pid>/maps. For most uses you want anonymous private mappings (the [anon] entries and unlabeled rwxp regions):
/* one range: 512 MB anonymous mapping at 0x7f0000000000 */
struct iovec ranges[1];
ranges[0].iov_base = (void *)0x7f0000000000;
ranges[0].iov_len = 512UL * 1024 * 1024;
ssize_t advised = process_madvise(pidfd, ranges, 1, MADV_PAGEOUT, 0);
if (advised < 0) err(1, "process_madvise");
printf("advised %zd bytes\n", advised);
The return value is the number of bytes successfully advised, not a boolean. If an entry in the iovec points at an invalid region in the target, the kernel stops there and returns the byte count advised so far. No -1, no errno, just a smaller number than you asked for. When you pass multiple ranges, compare the return against the sum of your iov_lens; that’s the only way you’ll know.
Permissions
Two gates, both mandatory since 5.12. First, a PTRACE_MODE_READ_FSCREDS ptrace access check on the target, the same check that guards reading /proc/<pid>/maps: same UID and a dumpable target, or CAP_SYS_PTRACE, subject to whatever Yama is set to. Second, the caller needs CAP_SYS_NICE, period, because forcing another process’s pages out has performance consequences the kernel won’t hand to just anyone. Same UID alone gets you EPERM. (5.10 and 5.11 used the stricter PTRACE_MODE_ATTACH_FSCREDS check instead and didn’t ask for CAP_SYS_NICE.)
Pre-checkpoint use
CRIU serializes process memory by reading /proc/<pid>/mem and /proc/<pid>/pagemap. Before calling into CRIU, running process_madvise with MADV_PAGEOUT over the target’s anonymous mappings moves dirty pages to swap and cleans file-backed pages. CRIU then reads through pagemap to find present pages and serializes them. The image is smaller, the restore is faster, and the network transfer window for post-copy migration shrinks proportionally. QEMU does a version of this for live migration: drain dirty memory before the final freeze so the post-copy phase has less to pull over the wire during resume.
The process_madvise(2) man page is sparse. The original kernel commit (by Minchan Kim, December 2020) has the rationale. For a production example of iovec construction, the LMKD source in AOSP under system/memory/lmkd/ shows how Android builds the range list from /proc/<pid>/maps before calling process_madvise on each doomed process.