diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-31 09:55:02 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-31 09:55:02 +0300 |
| commit | e8948e9f52235cbbbd8af098bc624bc20e173e6c (patch) | |
| tree | 75fac81f5121cfdaac5234cca371a126be52cdb4 | |
| parent | 75d00f479d333476990ff18f7427905bb09d49f0 (diff) | |
test(aio): add io_setup end-to-end integration coverage
The classic Linux AIO family (io_setup/io_submit/io_getevents/io_cancel/
io_destroy) had no integration coverage: family_test.go exercises only
FS/Memory/IPC/Network/Process/Sched/Time, and iouring_test.go covers only
the distinct io_uring_* family. io_setup is classified KindNull/FamilyAIO,
which is correct by inspection against man 2 io_setup (nr_events is a count,
ctx_idp an output pointer, so no fd/path is captured), so the tracer itself
needed no change.
Add an ioworkload AIO scenario that drives io_setup(2)/io_destroy(2) raw
(no privileges, no libaio) plus an EINVAL variant, and integration tests
that assert ior records the enter_io_setup tracepoint end-to-end, mirroring
the existing iouring scenario/test pattern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | cmd/ioworkload/scenario_aio.go | 80 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 2 | ||||
| -rw-r--r-- | integrationtests/aio_test.go | 33 |
3 files changed, 115 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_aio.go b/cmd/ioworkload/scenario_aio.go new file mode 100644 index 0000000..389e734 --- /dev/null +++ b/cmd/ioworkload/scenario_aio.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + "runtime" + "syscall" + "unsafe" +) + +// Linux AIO (io_setup family) syscall numbers on x86_64. These are the classic +// kernel AIO interface (io_setup/io_submit/io_getevents/io_cancel/io_destroy), +// distinct from the io_uring_* family. We invoke them raw via Syscall because +// the Go standard library does not wrap them. +const ( + sysIoSetup = 206 + sysIoDestroy = 207 + + // aioMaxEvents is the nr_events count requested from io_setup(2). It is a + // plain count (NOT an fd), so the tracer must classify the enter event as + // KindNull and capture no fd/path argument. + aioMaxEvents = 32 +) + +// aioSetup exercises io_setup(2): it creates an AIO context (writing the +// context id into a userspace pointer) and then tears it down with +// io_destroy(2). io_setup needs no special privileges, so this runs end-to-end +// in the integration harness and validates that ior records the +// enter_io_setup/exit_io_setup tracepoints for the AIO family. +func aioSetup() error { + ctx, err := ioSetupContext(aioMaxEvents) + if err != nil { + return err + } + return ioDestroyContext(ctx) +} + +// aioSetupEinval calls io_setup(2) with nr_events = 0, which the kernel rejects +// with EINVAL. The syscall fails, but ior still captures the enter_io_setup +// tracepoint and an exit_io_setup return event carrying the negative errno. +func aioSetupEinval() error { + for i := 0; i < 5; i++ { + var ctx uint64 + _, _, errno := syscall.Syscall( + sysIoSetup, + 0, // nr_events = 0 -> EINVAL + uintptr(unsafe.Pointer(&ctx)), + 0, + ) + runtime.KeepAlive(ctx) + if errno == 0 { + return fmt.Errorf("expected EINVAL, but io_setup(0) succeeded") + } + } + return nil +} + +// ioSetupContext calls io_setup(2) and returns the opaque aio_context_t id. +func ioSetupContext(nrEvents uint32) (uint64, error) { + var ctx uint64 + _, _, errno := syscall.Syscall( + sysIoSetup, + uintptr(nrEvents), + uintptr(unsafe.Pointer(&ctx)), + 0, + ) + runtime.KeepAlive(ctx) + if errno != 0 { + return 0, fmt.Errorf("io_setup: %w", errno) + } + return ctx, nil +} + +// ioDestroyContext tears down an AIO context created by io_setup(2). +func ioDestroyContext(ctx uint64) error { + _, _, errno := syscall.Syscall(sysIoDestroy, uintptr(ctx), 0, 0) + if errno != 0 { + return fmt.Errorf("io_destroy: %w", errno) + } + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index b37f272..2c4737a 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -123,6 +123,8 @@ var scenarios = map[string]func() error{ "iouring-register": iouringRegister, "iouring-enter-ebadf": iouringEnterEbadf, "iouring-register-ebadf": iouringRegisterEbadf, + "aio-setup": aioSetup, + "aio-setup-einval": aioSetupEinval, } func makeTempDir(prefix string) (string, func(), error) { diff --git a/integrationtests/aio_test.go b/integrationtests/aio_test.go new file mode 100644 index 0000000..e41e854 --- /dev/null +++ b/integrationtests/aio_test.go @@ -0,0 +1,33 @@ +package integrationtests + +import "testing" + +// aioTraceArgs traces the classic Linux AIO (io_setup) family syscalls plus +// close. io_setup/io_destroy are classified KindNull (FamilyAIO): the enter +// event captures no fd/path (nr_events is a count, ctx_idp an output pointer) +// and the exit event carries the raw return value. +var aioTraceArgs = []string{"-trace-syscalls", "io_setup,io_destroy,close"} + +// TestAioSetup exercises io_setup(2) end-to-end and asserts ior records the +// enter_io_setup tracepoint for the AIO family workload. +func TestAioSetup(t *testing.T) { + runScenarioResultWithIorArgs(t, "aio-setup", []ExpectedEvent{ + { + Tracepoint: "enter_io_setup", + Comm: "ioworkload", + MinCount: 1, + }, + }, aioTraceArgs) +} + +// TestAioSetupEinval drives io_setup(2) with nr_events = 0 (EINVAL). The +// syscall fails, but ior still captures the enter_io_setup tracepoint. +func TestAioSetupEinval(t *testing.T) { + runScenarioResultWithIorArgs(t, "aio-setup-einval", []ExpectedEvent{ + { + Tracepoint: "enter_io_setup", + Comm: "ioworkload", + MinCount: 1, + }, + }, aioTraceArgs) +} |
