4.2.32 · D5Operating Systems
Question bank — File operations — open, read, write, seek, close
Before you start, meet the three actors that every question here leans on — see the picture below, then read the labels.

Recall The three actors (unfold if rusty)
- Actor A — the file descriptor. A file descriptor is a small non-negative integer — a claim ticket indexing your process's File descriptor table. In the figure it is the numbered slot on the left.
- Actor B — the open file description. Behind that ticket sits the open file description,
whose most important field is the offset (the byte position where the next
read/writelands). It is the middle box in the figure. - Actor C — the kernel boundary. Every real transfer of bytes crosses the System calls and the kernel boundary — you ask the kernel, you never touch the disk directly. It is the dashed line separating your program from the disk.

The next two figures make the two trickiest edge cases visual — the sparse file hole and the atomicity of O_APPEND. Refer back to them when those questions appear.


True or false — justify
Everything below is a Statement ::: Verdict + why. Decide first, then reveal.
read returning 0 signals an error you must handle like -1.
False.
0 means EOF (end of file) — a normal, successful "nothing left" result; only -1 is an error. Treating EOF as error makes copy loops quit or crash early.After read(fd, buf, 100) you may safely inspect all 100 bytes of buf.
False. Only the first
n bytes (the return value, see figure s02) are valid; the tail may be uninitialised garbage. Always bound your use by n, never by count.lseek is slow because it physically repositions the disk read head.
False.
lseek only updates the in-kernel offset integer — no bytes move, no head moves. It is essentially free; the actual head movement (if any) happens lazily on the next read/write.Opening the same file twice gives you two fds that share one offset.
False. Two separate
open calls create two independent open file descriptions, each with its own offset. Reading through one does not advance the other. (Sharing only happens via dup/fork.)A single write(fd, buf, count) always writes exactly count bytes to a regular file.
False. A short write can occur (disk full, an interrupting signal, or a pipe/socket buffer being full), so
n < count is legal. Production code loops until total == count.O_WRONLY alone will create the file if it does not exist.
False. Without O_CREAT an
open on a missing file fails with -1 and errno = ENOENT. O_WRONLY only chooses access mode, not creation.The mode argument (e.g. 0644) sets permissions on every open.
False.
mode is consulted only when the file is actually created (i.e. O_CREAT was given and the file was absent). On an existing file it is ignored entirely.close(fd) guarantees your data is safely on the physical disk.
Mostly false.
close flushes kernel buffers toward storage and frees the description, but the OS may still hold data in its write-back cache. Only fsync guarantees durability to the platter.stdin, stdout, stderr are language features of C, not real fds.
False. They are ordinary file descriptors 0, 1, 2 that the shell wires up before your program starts.
write(1, ...) and printf ultimately hit the same fd 1.A read on a fd opened with O_NONBLOCK blocks until data arrives.
False. The whole point of
O_NONBLOCK is not to block: if no data is ready it returns -1 with errno = EAGAIN (a.k.a. EWOULDBLOCK), telling you to try again later.Spot the error
Each line describes buggy code or reasoning; name the flaw and the fix.
char buf[100]; read(fd, buf, 100); write(1, buf, 100); — copy the first 100 bytes.
Bug: it writes
100 instead of the return value n. On a short/last read, bytes n..99 are garbage. Fix: capture n = read(...) and write(1, buf, n).while (read(fd, buf, N) > 0) write(out, buf, N); — copy a whole file.
Bug: the last read usually returns
n < N, but the loop writes a fixed N, appending garbage. Fix: store n and write n bytes each iteration.if (open("x", O_RDONLY) < 0) perror("open"); then later using fd.
Bug: the fd was never saved to a variable, so it is lost; the later use references an undefined/stale
fd. Fix: int fd = open(...) and test fd < 0 on that variable.fd = open("log", O_WRONLY|O_APPEND); lseek(fd, 0, SEEK_SET); write(fd, s, k); — write at the start.
Bug: with O_APPEND every
write atomically re-seeks to EOF (figure s04), so the preceding lseek is silently undone. Fix: drop O_APPEND if you truly need positioned writes.A program opens thousands of files in a loop and never calls close.
Bug: it leaks descriptors; the per-process table fills and new
open calls fail with EMFILE. Fix: close each fd when done, respecting ulimit and resource limits.int n = write(fd, buf, count); /* assume done */ in a network-writing tool.
Bug: for Pipes and sockets a short write is common, so assuming completion drops bytes. Fix: loop
while (total < count) advancing buf + total.fd = open(...); ... ; close(fd); read(fd, buf, 10); — one more read for safety.
Bug: using an fd after
close is a use-after-close; the integer may have been reassigned to a different file by a later open. Fix: never touch an fd past its close.On a non-blocking socket: n = read(fd, buf, N); if (n < 0) die("read failed");.
Bug: with
O_NONBLOCK, n == -1 with errno == EAGAIN/EWOULDBLOCK is not a failure — it just means "nothing ready yet." Fix: treat EAGAIN as retry-later, only truly die on other errnos.Why questions
Reasoning drills — the answers explain the design, not just the fact.
Why does open return the smallest unused fd rather than a random number?
It makes shell redirection deterministic: after
close(1), the next open reliably grabs fd 1, letting shells wire stdout to a file predictably.Why does the kernel keep the offset in the open file description, not in your process memory?
Because the offset must survive across many
read/write calls and stay consistent under sharing (dup, fork); the kernel owns it so no user-space bug can corrupt the file's position.Why does read from a pipe often return fewer bytes than requested even when not at EOF?
A pipe delivers only what the writer has produced so far;
read returns immediately with whatever is ready rather than blocking for the full count, which is exactly what makes streaming responsive.Why does O_APPEND guarantee two concurrent loggers don't clobber each other?
Each
write under O_APPEND atomically seeks to the current EOF and writes in one indivisible step (figure s04), so no other writer can slip in between the seek and the write.Why is write to a full disk a short write rather than an outright -1?
The kernel writes as many bytes as fit and reports that count; only if zero bytes could be written does it return
-1 with ENOSPC. This lets your loop make partial progress and react precisely.Why must you cross the System calls and the kernel boundary for read/write at all?
The disk is shared, protected hardware; only the kernel may issue device I/O and enforce permissions, so user code must request the transfer instead of performing it.
Why does Buffered I/O (fopen/fread) exist if read/write already work?
Each raw syscall crosses the kernel boundary (costly); a user-space buffer batches many small
fread/fwrite calls into few large read/write syscalls, cutting overhead dramatically.Why would you ever open a fd with O_NONBLOCK if it forces you to handle EAGAIN?
So a single thread can juggle many slow connections without freezing on any one: a non-blocking
read returns instantly, letting an event loop (select/poll/epoll) service whichever fd is actually ready.Edge cases
Boundary situations the parent hinted at — reason through each.
lseek to a position far past EOF, then write one byte — what is created?
A sparse file with a "hole" (figure s03): the gap reads back as zero bytes but consumes no real disk blocks until written. The file's reported size grows even though allocated storage does not.
lseek(fd, -10, SEEK_SET) — seek to a negative absolute position.
It fails with
-1 and errno = EINVAL: a byte offset can never be negative, so SEEK_SET with -10 is meaningless. (Contrast SEEK_END with -4, which is fine because size + (-4) is still non-negative.)read on a file whose offset is already at EOF — what happens?
It returns
0 (EOF) immediately, transferring nothing and leaving the offset unchanged; no error, just "nothing left."read(fd, buf, 0) — a zero-count read.
It returns
0 and does nothing — but this 0 is the no-op meaning, not EOF, so a robust copy loop should not blindly treat every 0 as end-of-file if it might request zero bytes.read on an O_NONBLOCK socket with no data yet ready.
It returns
-1 with errno = EAGAIN/EWOULDBLOCK — "would have blocked, so I didn't." This is a normal signal to come back later, not an error to abort on.lseek on a pipe or socket fd.
It fails with
-1 and errno = ESPIPE: Pipes and sockets are inherently sequential streams with no addressable byte positions, so random access is undefined.Two fds from dup — reading through one, then the other.
They share the same open file description and hence one offset; a
read on the first advances the position seen by the second. (Contrast this with two separate open calls, which do not share.)close a descriptor that another dup'd fd still points to.
The open file description survives because its reference count is still positive; the kernel only flushes and frees it when the last fd referring to it is closed.
write returns -1 with errno = EINTR mid-transfer.
A signal interrupted the call before any bytes moved; the correct response is to retry the
write, not to abort — the operation simply hasn't started yet.Opening a file that exists with O_CREAT | O_EXCL.
The
open fails with -1 and errno = EEXIST. O_EXCL demands the file be newly created, giving you an atomic "create only if absent" — the basis of safe lock files.