Skip to content
academia.sh

Lesson 01 / 15

File Descriptors

Whether a pointer refers to the same memory location is measured by equality; whether a file descriptor shares the same open file description is measured by offset behavior instead — the descriptor itself is an integer and says nothing on its own.

Published Updated

In this lesson

  • distinguishes the four classes (copy, shared, handoff, unobserved) through fifteen observations in a single run of the core
  • shows through offset behavior that a descriptor duplicated with dup shares the same open file description
  • explains why a separate open call keeps a separate offset, without looking at the two descriptors' numeric values
Contents

The previous course asked who would free a block: when a pointer referred to a record, which side owed the obligation to release it. This course asks a different question. When one side changes something, does the other side see it? fork copies memory, dup shares an open file, a thread shares the address space, a pipe carries bytes across once. Each mechanism answers this question differently, and throughout the course the answer has four fixed names: copy, shared, handoff, unobserved. This lesson shows all four in a single run, then looks a little closer at the first of them — file descriptors.

Who Sees It: A Full Run of the Core

The measurement is built from two parts. observe.h and observe.c are a small unit that records a change’s name, its class, and whether the other side saw it. The counts live only inside observe.c; no lesson touches them from the outside.

// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}

The third unit, who_sees.c, measures each of fifteen small changes with these two functions. This lesson runs the whole core; the remaining fourteen lessons will each take only their own slice, not rewrite this file.

// who_sees.c — fifteen changes, one question for each: who sees it
#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "observe.h"

/* 1. File descriptor: a descriptor duplicated with dup shares the offset */
static void descriptor(void) {
    int a = open("who_sees.txt", O_CREAT | O_RDWR | O_TRUNC, 0600);
    if (write(a, "abcdef", 6) != 6) {
        return;
    }
    int b = dup(a);
    lseek(a, 2, SEEK_SET);
    observe("offset shared through dup", SHARED, lseek(b, 0, SEEK_CUR) == 2);
    close(b);
    close(a);
    unlink("who_sees.txt");
}

/* 2. Process: the variable the child writes does not change in the parent */
static void process(void) {
    int value = 0;
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        value = 1;
        _exit(value);
    }
    int status = 0;
    waitpid(p, &status, 0);
    observe("child's variable after fork", COPY, value == 1);
}

/* 3. Signal: the flag the handler raises reaches the main flow once */
static volatile sig_atomic_t flag = 0;
static void handler(int s) {
    (void)s;
    flag = 1;
}
static void signals(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);
    raise(SIGUSR1);
    observe("flag raised by the handler", HANDOFF, flag == 1);
}

/* 4. Pipe: the value the child writes is carried to the parent once */
static void pipes(void) {
    int ends[2];
    if (pipe(ends) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        int value = 42;
        close(ends[0]);
        ssize_t n = write(ends[1], &value, sizeof value);
        _exit(n == (ssize_t)sizeof value ? 0 : 1);
    }
    close(ends[1]);
    int received = 0;
    ssize_t n = read(ends[0], &received, sizeof received);
    waitpid(p, NULL, 0);
    close(ends[0]);
    observe("value written to the pipe", HANDOFF, n == (ssize_t)sizeof received && received == 42);
}

/* 5. Thread: a counter set under the lock reaches the main thread */
static int counter = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static void *increment(void *arg) {
    (void)arg;
    pthread_mutex_lock(&lock);
    counter = 7;
    pthread_mutex_unlock(&lock);
    return NULL;
}
static void thread(void) {
    pthread_t t;
    pthread_create(&t, NULL, increment, NULL);
    pthread_join(t, NULL);
    pthread_mutex_lock(&lock);
    int seen = (counter == 7);
    pthread_mutex_unlock(&lock);
    observe("counter written by the thread", SHARED, seen);
}

/* 6. Linked structure: a change made through a second pointer shows in the list */
struct Node {
    int value;
    struct Node *next;
};
static void linked(void) {
    struct Node c = { 3, NULL };
    struct Node b = { 2, &c };
    struct Node a = { 1, &b };
    struct Node *alias = &b;
    alias->value = 20;
    int total = 0;
    for (struct Node *d = &a; d != NULL; d = d->next) {
        total += d->value;
    }
    observe("node changed through an alias", SHARED, total == 24);
}

/* 7. Ring buffer: an item put in reaches the taking side once */
enum { CAPACITY = 4 };
struct Ring {
    int items[CAPACITY];
    int head;
    int count;
};
static int ring_put(struct Ring *r, int item) {
    if (r->count == CAPACITY) {
        return 0;
    }
    r->items[(r->head + r->count) % CAPACITY] = item;
    r->count++;
    return 1;
}
static int ring_take(struct Ring *r, int *item) {
    if (r->count == 0) {
        return 0;
    }
    *item = r->items[r->head];
    r->head = (r->head + 1) % CAPACITY;
    r->count--;
    return 1;
}
static void ring(void) {
    struct Ring r = { { 0 }, 0, 0 };
    int taken = 0;
    ring_put(&r, 5);
    int first = ring_take(&r, &taken) && taken == 5;
    int second = ring_take(&r, &taken);
    observe("item put in the ring", HANDOFF, first && !second);
}

/* 8. Hash table: the table keeps its own copy of the key */
struct Entry {
    char key[16];
    int value;
};
static void hashing(void) {
    char buffer[16] = "apple";
    struct Entry e;
    strcpy(e.key, buffer);
    e.value = 1;
    strcpy(buffer, "pear");
    observe("key changed after insertion", COPY, strcmp(e.key, "pear") == 0);
}

/* 9. Object-like design: two users holding the same object */
struct Account {
    int balance;
};
static void account_deposit(struct Account *a, int amount) {
    a->balance += amount;
}
static int account_balance(const struct Account *a) {
    return a->balance;
}
static void object(void) {
    struct Account account = { 0 };
    struct Account *first = &account;
    const struct Account *second = &account;
    account_deposit(first, 50);
    observe("deposit made by one user", SHARED, account_balance(second) == 50);
}

/* 10. Cleanup: a buffer that is never flushed vanishes with _exit */
static void cleanup(void) {
    int ends[2];
    if (pipe(ends) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        close(ends[0]);
        FILE *stream = fdopen(ends[1], "w");
        if (stream == NULL) {
            _exit(1);
        }
        setvbuf(stream, NULL, _IOFBF, 256);
        fputs("line left in the buffer\n", stream);
        _exit(0);
    }
    close(ends[1]);
    char incoming[64];
    ssize_t n = read(ends[0], incoming, sizeof incoming);
    waitpid(p, NULL, 0);
    close(ends[0]);
    observe("buffer lost on exit, not flushed", UNOBSERVED, n > 0);
}

/* 11. Build system (model): a unit with no listed dependency is not rebuilt */
struct Unit {
    int depends_on_header;
    int built_version;
};
static void build_unit(struct Unit *u, int header_version, int header_time, int object_time) {
    if (u->depends_on_header && header_time > object_time) {
        u->built_version = header_version;
    }
}
static void build(void) {
    struct Unit unit = { 0, 1 };
    build_unit(&unit, 2, 20, 10);
    observe("header with no listed dependency", COPY, unit.built_version == 2);
}

/* 12. Library (model): static linking takes a copy of the code */
struct Library {
    int version;
};
struct Program {
    int embedded_version;
    const struct Library *linked;
};
static void library(void) {
    struct Library lib = { 1 };
    struct Program static_build = { lib.version, NULL };
    lib.version = 2;
    observe("update to a static library", COPY, static_build.embedded_version == 2);
}

/* 13. Test: a change made by a test in its own process does not reach the next */
static int shared_state = 0;
static void testing(void) {
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        shared_state = 9;
        _exit(0);
    }
    waitpid(p, NULL, 0);
    observe("change made by an isolated test", COPY, shared_state == 9);
}

/* 14. Debugger (model): the value between two stops */
static void debugger(void) {
    int values[3] = { 1, 2, 3 };
    int seen_values[2];
    seen_values[0] = values[0]; /* first stop */
    seen_values[1] = values[2]; /* second stop */
    observe("value between two stops", UNOBSERVED,
            seen_values[0] == 2 || seen_values[1] == 2);
}

/* 15. Optimization (model): a write that no read ever saw */
struct Step {
    char op;
    int value;
};
static void optimization(void) {
    struct Step flow[] = { { 'w', 1 }, { 'w', 2 }, { 'r', 0 } };
    int readers = 0;
    int last_write = -1;
    for (int i = 0; i < 3; i++) {
        if (flow[i].op == 'w') {
            last_write = i;
        } else if (last_write == 0) {
            readers++;
        }
    }
    observe("write overwritten before any read", UNOBSERVED, readers > 0);
}

int main(void) {
    descriptor();
    process();
    signals();
    pipes();
    thread();
    linked();
    ring();
    hashing();
    object();
    cleanup();
    build();
    library();
    testing();
    debugger();
    optimization();
    observe_summary();
    return 0;
}
offset shared through dup          saw      shared
child's variable after fork        missed   copy
flag raised by the handler         saw      handoff
value written to the pipe          saw      handoff
counter written by the thread      saw      shared
node changed through an alias      saw      shared
item put in the ring               saw      handoff
key changed after insertion        missed   copy
deposit made by one user           saw      shared
buffer lost on exit, not flushed   missed   unobserved
header with no listed dependency   missed   copy
update to a static library         missed   copy
change made by an isolated test    missed   copy
value between two stops            missed   unobserved
write overwritten before any read  missed   unobserved
classes: copy 5, shared 4, handoff 3, unobserved 3
reached 7, not reached 8

SI1. None of the fifteen lines carries a MISMATCH mark: every observation matches its expected class. Five changes are copy, four shared, three handoff, three unobserved — and seven reach the other side while eight do not. This fifteen-line list is a map for the rest of the course: each lesson takes one line from here and goes deeper. This lesson’s share is the first line: offset shared through dup.

The Offset Shared Through dup

SI2. The descriptor function opens a file with the descriptor a, writes six bytes, then derives a second descriptor — b — with dup(a). The offset is moved through a; the question measured is whether the offset read through b also moved. The output says saw, meaning it did. The reason is not in the descriptor numbers but in the three-layer structure the operating system keeps: every process keeps its own descriptor table, every descriptor points at an open file description, and every open file description carries an offset. Every time open is called it creates a new open file description — and with it a new offset. dup, on the other hand, does not open a new description; it hands back a second descriptor pointing at the existing one. a and b are different integers, but they point at the same description; when one moves the offset, the other reads that same offset.

Had we opened the same file with open twice instead — a once, then the same path again as c — the situation would differ. Two separate open file descriptions would be created, each keeping its own offset; moving the offset through a would leave c at zero. Sharing a description is not established by open but by deriving a descriptor that points at the same description: dup, dup2, or descriptors inherited during fork. who_sees.c shows this distinction with dup; the next lesson carries the same inheritance forward with fork, but there the subject is not a shared offset, it is copied memory.

The Bounding Measurement

A descriptor is only a small integer; it says nothing on its own. Whether two descriptors look at the same description or at two separate ones can never be told by looking at their numeric values. The only reliable measurement is the behavior of the offset the description carries: if the offset changed through one is also read through the other, the description is shared; if not, there are two separate descriptions. That is why who_sees.c reads the offset value with lseek(b, 0, SEEK_CUR) and never prints the descriptor number anywhere — the descriptor number depends on the target and the run, the offset behavior does not.

Summary

  • The four classes that stay fixed throughout the course — copy, shared, handoff, unobserved — all answer one question: does the side that did not make the change see it.
  • The core’s fifteen observations split into five copy, four shared, three handoff, three unobserved; seven reach the other side, eight do not.
  • A descriptor derived with dup does not open a new open file description; it opens a second path to the existing one, so it shares the offset.
  • A separate open call always creates a separate open file description, and therefore a separate offset — even when it looks at the same file.
  • This lesson’s class is shared: because the two descriptors hold the same description, when one changes it, the other sees it too.

Next Step

The next lesson asks the same question of fork. dup made a description shared; fork does not share memory, it copies it — the child’s variable stays in its own copy, the parent never sees it.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close