Exercises — File operations — open, read, write, seek, close
Before we start, one shared picture. A file is a row of numbered mailboxes (bytes). The offset
is a little arrow that says "the next read/write starts here". Byte positions count from 0.

L1 — Recognition
Q1.1
open("notes.txt", O_RDONLY) succeeds. What is the smallest possible value it can return in a
fresh program, and why not 0?
Recall Solution
WHAT: open returns the smallest unused file descriptor.
WHY not 0: in a normal program fds 0, 1, 2 are already taken by stdin, stdout,
stderr. So the smallest unused one is 3.
Answer: 3.
Q1.2
read(fd, buf, 512) returns 0. Is this an error? What does it mean?
Recall Solution
Not an error. A return of 0 means end of file (EOF) — there are no more bytes at the current
offset. Errors return -1 (with errno set). So 0 = "cleanly finished", -1 = "broke".
Q1.3
You call open("new.txt", O_WRONLY) and the file does not exist. What does open return?
Recall Solution
It returns ==-1== and sets errno = ENOENT. Without O_CREAT, open will not create a
missing file — O_WRONLY only describes how you want to access it, not whether to make it.
L2 — Application
Q2.1
A file is exactly 20 bytes long. You run lseek(fd, -4, SEEK_END). What offset results, and
which byte indices will the following read(fd, buf, 4) cover?
Recall Solution
WHAT SEEK_END does: it computes .
The offset arrow lands on byte 16. A read of 4 bytes then covers indices 16, 17, 18, 19 —
exactly the last four bytes — and stops at EOF.

Q2.2
Offset starts at 0. You call read(fd, buf, 10) and it returns 10, then
lseek(fd, 5, SEEK_CUR). What is the new offset?
Recall Solution
Step 1: read of 10 bytes advances the offset automatically by its return value: .
Step 2: SEEK_CUR means .
Answer: 15. (Bytes 10,11,12,13,14 were skipped, never read.)
Q2.3
lseek(fd, 0, SEEK_CUR) — what is this idiom used for?
Recall Solution
It adds 0 to the current offset, so it changes nothing but returns the current offset.
It's the standard trick to ask "where am I right now?" without moving. WHY it works: lseek
always returns newpos, and here .
L3 — Analysis
Q3.1
A 20-byte file. You do lseek(fd, 25, SEEK_SET) then write(fd, "AB", 2). What is the new file
size, and what lives in bytes 20–24?
Recall Solution
Step 1 (seek): SEEK_SET sets offset directly: — that is past the
current 20-byte end. This is allowed.
Step 2 (write): writing 2 bytes at offset 25 puts 'A' at 25 and 'B' at 26. New size
= last written index + 1 = .
The gap (bytes 20–24): never written → the kernel treats them as implicit zero bytes. This
is a sparse file — a "hole" that reads back as \0 and may occupy no disk blocks.

Q3.2
Two processes both open log.txt with O_WRONLY | O_APPEND and each write an 8-byte line
"concurrently". Explain why neither line gets clobbered, using the offset model.
Recall Solution
WHY normally dangerous: without O_APPEND, each process has its own offset. Both could
compute "write at offset 40", so one overwrites the other.
WHY O_APPEND fixes it: the flag makes each write atomically do "seek-to-EOF then
write" as a single indivisible kernel step. Process A seeks to 40, writes 8 (EOF now 48); process
B's write then seeks to 48, writes 8 (EOF now 56). Because the seek+write can't be interrupted,
the two offsets can never collide → both lines survive, back to back.
Q3.3
read(fd, buf, 100) returns 40. Which slots of buf are safe to use, and what's in the rest?
Recall Solution
Only buf[0] .. buf[39] (the first 40 bytes, the return value) hold real file data. Slots
buf[40] .. buf[99] are untouched — still whatever garbage was there before. So you must
use n (=40), never count (=100). Writing 100 out would leak 60 bytes of junk.
L4 — Synthesis
Q4.1
Write a robust loop that writes an entire count-byte buffer, correctly handling short writes.
Then: if count = 4096 and each write call happens to return 1500, how many write calls run?
Recall Solution
ssize_t total = 0, n;
while (total < count) {
n = write(fd, buf + total, count - total);
if (n < 0) break; // real error → bail
total += n; // advance past bytes already written
}WHY the loop: a single write may transfer fewer than requested (disk pressure, signals).
Production code never assumes one call finishes the job.
Count of calls for 4096 bytes, 1500 per call:
- Call 1: writes 1500 → total 1500 (remaining 2596)
- Call 2: writes 1500 → total 3000 (remaining 1096)
- Call 3: asks for 1096; a short write caps at... but only 1096 remain, so it writes 1096 → total 4096, loop ends.
Answer: 3 calls.
Q4.2
Design (in pseudo-C) a copy_first_k(src, dst, k) that copies the first k bytes of src into a
new file dst, closing everything. Handle the case where src has fewer than k bytes.
Recall Solution
int in = open(src, O_RDONLY);
if (in < 0) return -1; // src missing → -1
int out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);// create/empty dst
if (out < 0) { close(in); return -1; }
char buf[4096];
size_t remaining = k;
while (remaining > 0) {
size_t want = remaining < sizeof buf ? remaining : sizeof buf;
ssize_t n = read(in, buf, want);
if (n <= 0) break; // n==0 EOF (src shorter than k) → stop; n<0 error
ssize_t w = 0;
while (w < n) { // robust write of exactly the bytes we read
ssize_t m = write(out, buf + w, n - w);
if (m < 0) { close(in); close(out); return -1; }
w += m;
}
remaining -= n;
}
close(in); close(out);
return 0;KEY design points: loop on read's return n (not k); stop cleanly on n == 0 (EOF)
when src is short; nest a short-write loop; O_TRUNC empties any old dst; close both fds.
L5 — Mastery
Q5.1
Your long-running server does open/read/close per request, but a bug skips close when a
request errors. ulimit -n is 1024, and 1 in 8 requests errors. After roughly how many
requests does the next open start failing, and with what error?
Recall Solution
WHAT is scarce: the per-process file-descriptor table has a hard cap (ulimit -n = 1024).
Three fds (0,1,2) are pre-occupied, leaving 1024 - 3 = 1021 usable slots.
WHY leaks accumulate: every errored request leaks exactly one fd (never returned). Leaks per
request .
WHEN it breaks: it fails once the 1021 free slots are all leaked:
Around the 8168th request the next open returns ==-1== with errno = EMFILE
("too many open files"). Fix: close on every path, including error paths.
Q5.2
A file is 20 bytes. Predict the exact offset after this sequence:
lseek(fd, 8, SEEK_SET) → read(fd, buf, 5) (returns 5) → lseek(fd, -3, SEEK_CUR) →
lseek(fd, 0, SEEK_END). Give the offset after each step.
Recall Solution
Track the arrow step by step:
SEEK_SET 8→ offset .read 5(returns 5) → offset advances: .SEEK_CUR -3→ .SEEK_END 0→ .
Offsets: 8 → 13 → 10 → 20. Final answer 20 (EOF).
Q5.3
Explain, using only the offset + reference-count model, why after int fd2 = dup(fd); calling
lseek(fd2, 0, SEEK_SET) also affects reads on fd, while opening the same file twice with two
separate open calls does not share an offset. (Conceptual — no numbers.)
Recall Solution
dup: copies the descriptor, so fd and fd2 point to the same open file description —
the same offset arrow. Move it via fd2 and fd sees the move too (one arrow, two names).
Two opens: each open allocates a separate open file description, each with its own
offset arrow. Moving one leaves the other untouched.
Reference count: the description is freed only when all fds referring to it are closed; the
count is why one stray dup'd fd keeps the file "alive" until every copy is closed.
Recall Feynman recap
Every question on this page is really "where is the arrow, and how many claim-tickets point to
it?" read/write shove the arrow forward by their return value; lseek teleports it with
simple arithmetic (SET=absolute, CUR=relative, END=from size); close gives back one ticket
and only tidies up when the last ticket is gone.
Related: File descriptor table · System calls and the kernel boundary · ulimit and resource limits · Buffered I/O (fopen/fread) · File system & inodes · Pipes and sockets.