CodeRigor

FREE LESSON

Syscalls, short I/O & positioned files

Handle errno, EINTR, short reads/writes and EOF; use pread/pwrite without disturbing shared offsets, reject Linux append-mode traps, and test error paths deterministically.

The C++ standard library and the Unix syscall interface meet at a byte boundary. read and write return a signed count: positive progress, zero, or minus one with errno. Check that result before converting it to size_t. A successful syscall can transfer fewer bytes than requested.

State a loop can actually observe

After a positive result, keep that count and advance exactly that far. After EINTR, retry the remaining operation. An ordinary read returning zero is EOF. On a nonblocking stream, EAGAIN means no progress is available now; it is not EOF, and spinning on it wastes a core. Permanent errors stay explicit.

Short I/O advances only by actual progress
An eight-byte request returned three bytes. The next operation starts at byte three and requests the remaining five.
Short I/O advances only by actual progress
An eight-byte request returned three bytes. The next operation starts at byte three and requests the remaining five.

The build accepts injected syscall callbacks so short transfers and interrupted calls can be tested without a timing race. Its default callbacks still use real pread/pwrite on real files. This is a test seam around the operation, not a replacement for understanding its contract.

Positioned operations and shared cursors

lseek followed by read is two operations on shared cursor state. Another user of a duplicated or inherited descriptor can change that state in between. pread(fd, buffer, count, offset) supplies a position for this operation and leaves the open file description's ordinary cursor unchanged.

Positioned I/O does not move the shared cursor
pread uses its supplied offset for this operation. An ordinary read uses and advances the open file description cursor.
Positioned I/O does not move the shared cursor
pread uses its supplied offset for this operation. An ordinary read uses and advances the open file description cursor.
C++
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>

int main() {
    char name[]="/tmp/unix-check-XXXXXX"; int fd=::mkstemp(name); assert(fd>=0); ::unlink(name);
    assert(::write(fd,"abcdef",6)==6); assert(::lseek(fd,0,SEEK_SET)==0);
    assert(::lseek(fd,4,SEEK_SET)==4); char c=0; assert(::pread(fd,&c,1,1)==1);
    std::printf("%c %ld\n",c,long(::lseek(fd,0,SEEK_CUR))); ::close(fd);
}

Use the same idea for pwrite, but read platform exceptions: on Linux an O_APPEND descriptor makes pwrite append despite its explicit offset. The build rejects append mode rather than silently violating its positioned-write promise. It does not clear the flag behind another owner's back.

Range checks and zero progress

Validate a nonempty range before computing its end: size <= maximum - offset after checking offset is in range. Otherwise arithmetic can wrap before an apparently reasonable syscall argument is constructed. The teaching helpers cap each window at one MiB; this bounds allocation and keeps exercises small.

Interpret the return value first
For a nonempty request, the sign and count tell you how the loop can progress. This is a read-side table.
Interpret the return value first
For a nonempty request, the sign and count tell you how the loop can progress. This is a read-side table.
C++
#include <array>
#include <cstdio>
int main() {
    int done=0,calls=0;
    for(int n:std::array{2,1,0,8}) { ++calls; if(n==0)break; done+=n; }
    std::printf("%d %d\n",done,calls);
}

An empty request is a no-op in this API. A nonempty write returning zero must not spin forever; the wrapper reports EIO. A read that reaches EOF can still return a useful partial window. A different application might need an exact_read result distinguishing a truncated record from clean EOF; state that policy in its interface rather than hiding it in a loop.

Buffering is not durability

C stdio may buffer in user space. The kernel also caches file data. A successful write makes progress under its syscall contract, but is not a promise that a power failure cannot lose the data. This build does not add fsync or claim to measure storage speed. Error-injection tests verify the loop's accounting; ordinary fixtures verify real file contents and an unchanged shared cursor.

Before the fill, walk a sequence of EINTR, two bytes, one byte and EOF. Keep both the requested offset and the accumulated result explicit. Continue with filesystem operations, then processes.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Watch the raw Unix read example. Then apply this lesson’s positioned I/O loops, short-transfer accounting and errno policy; pread/pwrite avoid changing the shared cursor.