diff options
| -rw-r--r-- | cmd/ioworkload/scenario_mmap.go | 59 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 1 | ||||
| -rw-r--r-- | integrationtests/mmap_test.go | 40 |
3 files changed, 100 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_mmap.go b/cmd/ioworkload/scenario_mmap.go index 0d953d2..471788d 100644 --- a/cmd/ioworkload/scenario_mmap.go +++ b/cmd/ioworkload/scenario_mmap.go @@ -5,6 +5,8 @@ import ( "path/filepath" "syscall" "unsafe" + + "golang.org/x/sys/unix" ) const mremapMayMove = 1 @@ -136,3 +138,60 @@ func mmapMremapMunmap() error { } return nil } + +// mmapMemoryLock exercises the memory-locking cluster (mlock, mlock2, munlock, +// mlockall, munlockall — all FamilyMemory) so the integration suite captures +// their sys_enter_ tracepoints end-to-end. +// +// All locking is done on a single anonymous page (one page = small enough to +// stay well under RLIMIT_MEMLOCK for an unprivileged process). The raw syscall +// path is used throughout so the exact tracepoints fire: +// - mlock(addr, len) / munlock(addr, len) -> KindMem (captures addr+len) +// - mlock2(addr, len, 0) -> KindMem (no stdlib wrapper) +// - mlockall(MCL_CURRENT) / munlockall() -> KindNull +// +// Errors from mlock/mlock2/mlockall are tolerated best-effort: even when the +// kernel rejects the call (e.g. EPERM/ENOMEM if RLIMIT_MEMLOCK is too low), the +// sys_enter_ tracepoint has already fired, which is all this scenario needs. +// munlock/munlockall always succeed, so we clean up unconditionally. +func mmapMemoryLock() error { + const pageSize = 4096 + + mapped, err := syscall.Mmap(-1, 0, pageSize, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON) + if err != nil { + return fmt.Errorf("mmap: %w", err) + } + defer syscall.Munmap(mapped) + + addr := uintptr(unsafe.Pointer(&mapped[0])) + length := uintptr(len(mapped)) + + // mlock then munlock the single page (KindMem). EPERM/ENOMEM are tolerated: + // the enter tracepoint fires regardless of the eventual errno. + if _, _, errno := unix.Syscall(unix.SYS_MLOCK, addr, length, 0); errno != 0 && errno != syscall.EPERM && errno != syscall.ENOMEM { + return fmt.Errorf("mlock: %w", errno) + } + if _, _, errno := unix.Syscall(unix.SYS_MUNLOCK, addr, length, 0); errno != 0 { + return fmt.Errorf("munlock: %w", errno) + } + + // mlock2(addr, len, 0) has no stdlib/x-sys wrapper; issue the raw syscall. + if _, _, errno := unix.Syscall(unix.SYS_MLOCK2, addr, length, 0); errno != 0 && errno != syscall.EPERM && errno != syscall.ENOMEM { + return fmt.Errorf("mlock2: %w", errno) + } + // Drop any lock established by mlock2 before unmapping. + if _, _, errno := unix.Syscall(unix.SYS_MUNLOCK, addr, length, 0); errno != 0 { + return fmt.Errorf("munlock after mlock2: %w", errno) + } + + // mlockall(MCL_CURRENT) then munlockall() (KindNull). mlockall may fail with + // EPERM/ENOMEM for unprivileged callers when RLIMIT_MEMLOCK is too low; its + // enter tracepoint still fires. munlockall always succeeds. + if _, _, errno := unix.Syscall(unix.SYS_MLOCKALL, uintptr(unix.MCL_CURRENT), 0, 0); errno != 0 && errno != syscall.EPERM && errno != syscall.ENOMEM { + return fmt.Errorf("mlockall: %w", errno) + } + if _, _, errno := unix.Syscall(unix.SYS_MUNLOCKALL, 0, 0, 0); errno != 0 { + return fmt.Errorf("munlockall: %w", errno) + } + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 22f3e3c..8f2701e 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -144,6 +144,7 @@ var scenarios = map[string]func() error{ "mmap-msync-sync": mmapMsyncSync, "mmap-msync-invalid-flags": mmapMsyncInvalidFlags, "mmap-mremap-munmap": mmapMremapMunmap, + "mmap-memory-lock": mmapMemoryLock, "copy-file-range-basic": copyFileRangeBasic, "copy-file-range-bad-dst-fd": copyFileRangeBadDstFd, "truncate-basic": truncateBasic, diff --git a/integrationtests/mmap_test.go b/integrationtests/mmap_test.go index 74eb14e..ff3ad55 100644 --- a/integrationtests/mmap_test.go +++ b/integrationtests/mmap_test.go @@ -11,6 +11,11 @@ const ( var mmapTraceArgs = []string{"-trace-syscalls", "openat,write,close,mmap,msync,mremap,munmap"} +// mmapMemoryLockTraceArgs traces the memory-locking cluster. mlock/mlock2/ +// munlock are KindMem and mlockall/munlockall are KindNull; in all cases only +// the sys_enter_ tracepoint presence is asserted (return is UNCLASSIFIED). +var mmapMemoryLockTraceArgs = []string{"-trace-syscalls", "mlock,mlock2,munlock,mlockall,munlockall"} + func TestMmapBasic(t *testing.T) { runScenarioResultWithIorArgs(t, "mmap-basic", []ExpectedEvent{ { @@ -78,6 +83,41 @@ func TestMmapMremapMunmap(t *testing.T) { }, 0) } +// TestMmapMemoryLock asserts the memory-locking cluster fires its enter +// tracepoints end-to-end. mlock/mlock2/munlock (KindMem) and mlockall/ +// munlockall (KindNull) all return UNCLASSIFIED, so enter-presence is the +// correct check. mlock/mlock2/mlockall may hit EPERM/ENOMEM under a low +// RLIMIT_MEMLOCK, but the sys_enter_ tracepoint fires regardless. +func TestMmapMemoryLock(t *testing.T) { + runScenarioResultWithIorArgs(t, "mmap-memory-lock", []ExpectedEvent{ + { + Tracepoint: "enter_mlock", + Comm: "ioworkload", + MinCount: 1, + }, + { + Tracepoint: "enter_munlock", + Comm: "ioworkload", + MinCount: 1, + }, + { + Tracepoint: "enter_mlock2", + Comm: "ioworkload", + MinCount: 1, + }, + { + Tracepoint: "enter_mlockall", + Comm: "ioworkload", + MinCount: 1, + }, + { + Tracepoint: "enter_munlockall", + Comm: "ioworkload", + MinCount: 1, + }, + }, mmapMemoryLockTraceArgs) +} + func TestMmapMremapMunmapAddressSpaceBytesInParquet(t *testing.T) { h := newTestHarness(t) h.WorkloadEnv = []string{mmapWorkloadStartupEnv} |
