Skip to content

fork · read · lseek

The cursor your child moves

fork copies the descriptor table and shares the descriptions underneath it. The child's read moves the parent's cursor, and nothing in the source says so.

The table is copied. What it points at is not

After fork, both processes have their own descriptor tables. The child can close its fd 3 without touching the parent's, and it can dup2 over it, and none of that is visible on the other side.

Underneath, they point at the same descriptions. Which means the same cursors. A child that reads four bytes has moved the parent's position four bytes, and the parent finds out the next time it reads — or never, and reads from the wrong place for the rest of its life.

This is the single most surprising consequence of fork, and there is no syntax anywhere to warn you about it.

Two processes appending to one log this way is fine. Two processes reading a file this way silently splits it between them, and each one thinks it read the whole thing.

Try this
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
  char buf[4];
  int fd = open("/work/data", O_RDONLY);

  pid_t pid = fork();
  if (pid == 0) {
    read(fd, buf, 4);
    _exit(0);
  }

  int status;
  waitpid(pid, &status, 0);
  return lseek(fd, 0, SEEK_CUR);
}

Watch: The parent never reads. Where is its cursor at the end, and why?

fork-shares-the-offset

`O_APPEND` is not a property of the write

O_APPEND lives on the description, and it means the seek-to-the-end happens inside the write, atomically. Two processes appending to one log never overwrite each other, however they are scheduled.

The tempting alternative — lseek to the end, then write — is two calls with a gap between them, and something else can append in that gap. It works every time you test it and fails under load, which is the worst kind of bug there is.

Try this
#include <fcntl.h>
#include <unistd.h>

int main(void) {
  int a = open("/work/log", O_WRONLY|O_CREAT|O_APPEND, 0644);
  int b = open("/work/log", O_WRONLY|O_CREAT|O_APPEND, 0644);
  write(a, "one\n", 4);
  write(b, "two\n", 4);
  close(a);
  close(b);
  return 0;
}

Watch: Two descriptions with two cursors, and nothing lost. The flag is on the description, not the write.

fork-shares-the-offset

Trace

Run one of the programs on the left.

Everything you do here stays in this browser.Part of liter8.sh