diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-01 10:31:53 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-01 10:31:53 +0300 |
| commit | 8549884d1d957821b75dfbd5a4ff746667095f17 (patch) | |
| tree | c486c6643a86beceafd428a7e8b2d312162426f1 | |
| parent | 1b59bbe42c13a2a60667dff51dc02e0c350434d7 (diff) | |
test(integration): add pwritev/pwritev2 retbytes coverage
Add write-side positional vectored coverage to close the remaining gap
in the pwrite64-family byte-accounting validation. The retbytes/readwrite
integration suite already exercised pwrite64 (scalar) and the read-side
preadv/preadv2, but the WRITE_CLASSIFIED byte attribution for the
vectored positional writes pwritev/pwritev2 was only covered by unit
tests, not end-to-end.
New ioworkload scenarios:
- readwrite-pwritev: issues pwritev (syscall.SYS_PWRITEV) writing a known
two-iovec payload at offset 0 to a temp file.
- readwrite-pwritev2: issues pwritev2 via the explicit syscall number
(328 amd64 / 287 arm64, mirroring preadv2SyscallNr) with offset 0 and
no flags.
New integration tests assert enter_pwritev/enter_pwritev2 fired and that
the attributed retbytes equal the exact iovec total, validating
WRITE_CLASSIFIED end-to-end. Both pass as root.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | cmd/ioworkload/scenario_readwrite.go | 87 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 2 | ||||
| -rw-r--r-- | integrationtests/readwrite_test.go | 39 |
3 files changed, 128 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_readwrite.go b/cmd/ioworkload/scenario_readwrite.go index 02fab53..04ee86f 100644 --- a/cmd/ioworkload/scenario_readwrite.go +++ b/cmd/ioworkload/scenario_readwrite.go @@ -183,6 +183,93 @@ func readwritePwrite() error { return nil } +// readwritePwritev opens a file and writes data via pwritev (positional +// vectored write). pwritev returns the number of bytes written and is +// WRITE_CLASSIFIED, so the scenario writes a known iovec total to validate +// end-to-end byte attribution for the positional vectored write variant. +func readwritePwritev() error { + dir, cleanup, err := makeTempDir("readwrite-pwritev") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "pwritevfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer syscall.Close(fd) + + buf1 := []byte("pwritev ") + buf2 := []byte("test data") + iovs := []syscall.Iovec{ + {Base: &buf1[0], Len: uint64(len(buf1))}, + {Base: &buf2[0], Len: uint64(len(buf2))}, + } + // pwritev(fd, iov, iovcnt, offset) writes at the given offset (0) without + // changing the file position. + _, _, errno := syscall.Syscall6(syscall.SYS_PWRITEV, uintptr(fd), uintptr(unsafe.Pointer(&iovs[0])), uintptr(len(iovs)), 0, 0, 0) + runtime.KeepAlive(buf1) + runtime.KeepAlive(buf2) + if errno != 0 { + return fmt.Errorf("pwritev: %w", errno) + } + return nil +} + +// readwritePwritev2 opens a file and writes data via pwritev2 (positional +// vectored write with flags). Like pwritev it returns the bytes written and is +// WRITE_CLASSIFIED; the scenario writes a known iovec total to validate +// end-to-end byte attribution. +func readwritePwritev2() error { + dir, cleanup, err := makeTempDir("readwrite-pwritev2") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "pwritev2file.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer syscall.Close(fd) + + buf1 := []byte("pwritev2 ") + buf2 := []byte("test data") + iovs := []syscall.Iovec{ + {Base: &buf1[0], Len: uint64(len(buf1))}, + {Base: &buf2[0], Len: uint64(len(buf2))}, + } + nr, err := pwritev2SyscallNr(runtime.GOARCH) + if err != nil { + return err + } + // pwritev2(fd, iov, iovcnt, pos_l, pos_h, flags): offset 0, no flags. + _, _, errno := syscall.Syscall6(nr, uintptr(fd), uintptr(unsafe.Pointer(&iovs[0])), uintptr(len(iovs)), 0, 0, 0) + runtime.KeepAlive(buf1) + runtime.KeepAlive(buf2) + if errno != 0 { + return fmt.Errorf("pwritev2: %w", errno) + } + return nil +} + +// pwritev2SyscallNr returns the pwritev2(2) syscall number for the given GOARCH. +// Go's syscall package lacks a SYS_PWRITEV2 constant, so the architecture +// numbers are provided explicitly (mirroring preadv2SyscallNr). +func pwritev2SyscallNr(arch string) (uintptr, error) { + switch arch { + case "amd64": + return 328, nil + case "arm64": + return 287, nil + default: + return 0, fmt.Errorf("pwritev2 syscall number not defined for GOARCH=%s", arch) + } +} + // readwriteReadv opens a file, writes data, then reads it back via readv. func readwriteReadv() error { dir, cleanup, err := makeTempDir("readwrite-readv") diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 000750a..597d0e8 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -21,6 +21,8 @@ var scenarios = map[string]func() error{ "readwrite-preadv": readwritePreadv, "readwrite-preadv2": readwritePreadv2, "readwrite-pwrite": readwritePwrite, + "readwrite-pwritev": readwritePwritev, + "readwrite-pwritev2": readwritePwritev2, "readwrite-readv": readwriteReadv, "readwrite-writev": readwriteWritev, "readwrite-wronly-read": readwriteWronlyRead, diff --git a/integrationtests/readwrite_test.go b/integrationtests/readwrite_test.go index c1efbb4..f4f528a 100644 --- a/integrationtests/readwrite_test.go +++ b/integrationtests/readwrite_test.go @@ -66,6 +66,45 @@ func TestReadwritePwrite(t *testing.T) { }, 1) } +func TestReadwritePwritev(t *testing.T) { + // pwritev is the positional vectored write sibling of pwrite64/writev and is + // WRITE_CLASSIFIED, so a successful write must attribute the iovec total + // (sum of both buffers) end-to-end. + const payloadLen = uint64(len("pwritev ") + len("test data")) + result, _ := runScenarioResult(t, "readwrite-pwritev", []ExpectedEvent{ + { + PathContains: "pwritevfile.txt", + Tracepoint: "enter_pwritev", + Comm: "ioworkload", + MinCount: 1, + }, + }) + assertEventBytesAtLeast(t, result, ExpectedEvent{ + PathContains: "pwritevfile.txt", + Tracepoint: "enter_pwritev", + Comm: "ioworkload", + }, payloadLen) +} + +func TestReadwritePwritev2(t *testing.T) { + // pwritev2 is the positional vectored write variant with flags; like pwritev + // it is WRITE_CLASSIFIED and must attribute the iovec total end-to-end. + const payloadLen = uint64(len("pwritev2 ") + len("test data")) + result, _ := runScenarioResult(t, "readwrite-pwritev2", []ExpectedEvent{ + { + PathContains: "pwritev2file.txt", + Tracepoint: "enter_pwritev2", + Comm: "ioworkload", + MinCount: 1, + }, + }) + assertEventBytesAtLeast(t, result, ExpectedEvent{ + PathContains: "pwritev2file.txt", + Tracepoint: "enter_pwritev2", + Comm: "ioworkload", + }, payloadLen) +} + func TestReadwritePreadv(t *testing.T) { // preadv is the positional vectored read sibling of pread64/readv and is // READ_CLASSIFIED, so a successful read must attribute the payload bytes |
