QUESTION
If write(fd, buf, len) succeeds, were all len bytes written?
A successful return value is the number of bytes actually processed, not the requested length. Regular files usually make substantial progress, but a short return is normal for pipes, terminals, sockets, resource limits, and signal conditions.
A read result of 0 is EOF; -1/EAGAIN on a nonblocking fd means that no data is available right now. Treating both as 'no data' confuses connection termination with temporary backpressure.
STRUCTURE
Structure diagram
If write returns 6, the kernel consumed only the first 6 bytes. The next call must pass the remaining 10 bytes from buffer+6.
CALL PATH
Call path
The loop is not merely code that repeats a syscall; it updates byte ownership. Advance the pointer and remaining length only by the actual return value.
This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.
SOURCE COORDINATES
Linux 6.18.37 LTS source locations
Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.
| File | Function / structure | What to inspect |
|---|---|---|
| fs/read_write.c | ksys_read(), ksys_write() | Handle fd lookup, file position, and returned bytes |
| lib/iov_iter.c | copy_to_iter(), copy_from_iter() | Track progress across scatter/gather buffers |
| fs/pipe.c | pipe_read(), pipe_write() | A clear example of capacity, EOF, and EAGAIN |
COMPLETE PROGRAM
Complete runnable example
The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.
cc -std=c17 -Wall -Wextra -O2 copy_loop.c -o copy_loop01#include <errno.h>
02#include <stdio.h>
03#include <unistd.h>
04
05static int write_all(int fd, const unsigned char *buffer, size_t length)
06{
07 size_t total = 0;
08 while (total < length) {
09 ssize_t n = write(fd, buffer + total, length - total);
10 if (n > 0) {
11 total += (size_t)n;
12 continue;
13 }
14 if (n < 0 && errno == EINTR)
15 continue;
16 return -1;
17 }
18 return 0;
19}
20
21int main(void)
22{
23 unsigned char buffer[16384];
24 for (;;) {
25 ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
26 if (n > 0) {
27 if (write_all(STDOUT_FILENO, buffer, (size_t)n) < 0)
28 return 1;
29 } else if (n == 0) {
30 return 0;
31 } else if (errno != EINTR) {
32 perror("read");
33 return 1;
34 }
35 }
36}
CODE NOTES
Code notes
size_t total = 0Tracks the beginning of the unwritten range. It verifies that the ssize_t return value is positive before adding it to size_t.
buffer + totalA retry starts at the unprocessed bytes instead of sending from the beginning again. This directly prevents duplicate data.
length - totalDerives pointer movement and length reduction from the same total so the code cannot pass the end of the buffer.
else if (n == 0)This is EOF because every writer of stdin has closed. Unlike EAGAIN, waiting with poll will not make new bytes arrive on the same stream.
errno != EINTRRetries only when read made no progress before the signal. Supporting nonblocking input requires a separate branch that returns to poll/epoll on EAGAIN.
DETAILS
Detailed behavior
Blocking does not mean complete
The blocking flag means an operation may sleep when it cannot make immediate progress; it does not promise to atomically complete the full requested length. Signals, target-specific maximum transfers, and current capacity reduce the returned length.
Even a regular-file write can complete some bytes and then hit RLIMIT_FSIZE or a storage error.
Do not create a busy loop on EAGAIN
Immediately repeating the same syscall when an O_NONBLOCK fd is not ready consumes CPU. Register the event of interest with epoll/poll and retry the remaining range when readiness arrives.
Readiness is the currently observed state, not a permanent guarantee that the next syscall can never block.
Durability is separate from successful write
A write return means the kernel accepted the data; it does not mean the data is permanently stored on the medium. Place fsync/fdatasync, directory fsync, and the rename protocol according to durability requirements.
A successful network send is likewise not an acknowledgement that the peer application read the data.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
user buffer | Allocated by the caller and must remain valid until the syscall returns | base, length, processed |
iov_iter | Tracks the current segment and remaining length during kernel I/O | count, iov offset |
file position | Unless positioned I/O is used, it remains in struct file and is shared with later calls | f_pos, append mode |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| Duplicate output | The buffer is resent from the beginning after a short write | pointer/remaining log |
| CPU reaches 100% at EOF | A read result of 0 is retried | Check the read return value and poll HUP |
| CPU reaches 100% in nonblocking mode | EAGAIN busy loop | Check strace call frequency and epoll use |
LAB
Verify it yourself
- Copy a large file through a pipe and compare requested and returned lengths in strace -e read,write.
- Connect stdout to a slow reader and observe pipe backpressure and write blocking.
- Set stdout to O_NONBLOCK and implement a write_all variant that uses poll on EAGAIN.
./copy_loop < input.bin > output.binstrace -e trace=read,write ./copy_loop < input.bin > output.binPRIMARY REFERENCES