File Descriptors After fork()

File Descriptors After fork()
Topic 3 → Subtopic 1  |  How open files are shared between parent and child
Topic 3
File Sharing
Subtopic 1
of 3
3
Code Examples

File Descriptors Are Duplicated During fork()

When fork() is called, the child gets its own copy of the file descriptor table. But here’s the important part: these copies still refer to the same underlying open file descriptions in the kernel. This means the parent and child share things like the file offset and open file status flags for any files that were open before the fork.

Keywords:

file descriptor open file description file offset file status flags O_APPEND dup() fcntl() lseek()

📄 The Three-Layer File System Model

Linux file I/O has three layers. Understanding them is the key to understanding what is shared and what is separate after fork():

Parent FD Table
fd 0 (stdin)
fd 1 (stdout)
fd 2 (stderr)
fd 3 (file.txt) →

Open File Table (Kernel)
OFT entry for stdin
OFT entry for stdout
OFT entry for stderr
OFT entry (file.txt)
offset, flags, ref_count

Inode (disk)
stdin inode
stdout inode
stderr inode
file.txt inode

After fork(): Child’s fd 3 also points to the same OFT entry as parent’s fd 3.
Both share: file offset • O_APPEND flag • access mode

🔂 What is Shared via the Open File Description
Property Shared? Effect
File offset ✓ YES If child seeks to position 1000, parent sees offset=1000 too
Open file status flags (O_APPEND etc.) ✓ YES If child sets O_APPEND, parent’s fd also has O_APPEND
Access mode (O_RDONLY/O_RDWR) ✓ YES Read-only files stay read-only for both
File descriptor flags (close-on-exec) ✗ NO Each process has its own close-on-exec flag per fd

💻 Code Example 1: Sharing File Offset and O_APPEND (from TLPI)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>

int main(void)
{
    int fd, flags;
    char template[] = "/tmp/testXXXXXX";

    /* Disable buffering so printf output appears immediately */
    setbuf(stdout, NULL);

    /* Create a temporary file */
    fd = mkstemp(template);
    if (fd == -1) { perror("mkstemp"); exit(1); }

    printf("File offset before fork(): %lld\n",
           (long long) lseek(fd, 0, SEEK_CUR));

    flags = fcntl(fd, F_GETFL);
    printf("O_APPEND flag before fork(): %s\n",
           (flags & O_APPEND) ? "on" : "off");

    switch (fork()) {
    case -1:
        perror("fork"); exit(1);

    case 0:
        /* CHILD: change file offset and set O_APPEND */
        if (lseek(fd, 1000, SEEK_SET) == -1) {
            perror("lseek"); exit(1);
        }
        flags = fcntl(fd, F_GETFL);
        flags |= O_APPEND;
        if (fcntl(fd, F_SETFL, flags) == -1) {
            perror("fcntl F_SETFL"); exit(1);
        }
        _exit(EXIT_SUCCESS);

    default:
        /* PARENT: wait, then check if it sees child's changes */
        if (wait(NULL) == -1) { perror("wait"); exit(1); }

        printf("Child has exited\n");

        /* Offset should now be 1000 */
        printf("File offset in parent: %lld\n",
               (long long) lseek(fd, 0, SEEK_CUR));

        /* O_APPEND should now be on */
        flags = fcntl(fd, F_GETFL);
        printf("O_APPEND flag in parent: %s\n",
               (flags & O_APPEND) ? "on" : "off");
        exit(EXIT_SUCCESS);
    }
}
Expected output:
File offset before fork(): 0
O_APPEND flag before fork(): off
Child has exited
File offset in parent: 1000
O_APPEND flag in parent: on

💻 Code Example 2: Parent and Child Writing to the Same File
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>

int main(void)
{
    int fd;
    char parent_msg[] = "Parent line\n";
    char child_msg[]  = "Child  line\n";

    /* Open file BEFORE fork: both share the offset */
    fd = open("/tmp/shared_output.txt",
              O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); exit(1); }

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* Child writes first */
        write(fd, child_msg, strlen(child_msg));
        close(fd);
        _exit(0);
    }

    /* Parent waits then writes */
    wait(NULL);
    write(fd, parent_msg, strlen(parent_msg));
    close(fd);

    /* Show the file contents */
    printf("Contents of /tmp/shared_output.txt:\n");
    system("cat /tmp/shared_output.txt");
    return 0;
}
Because offset is shared: Child writes at offset 0, advances offset to 12. Parent sees offset=12 and writes “Parent line” right after — no overlap, no data loss. This is the same mechanism the shell uses to redirect output.

💻 Code Example 3: stdout (fd 1) is Shared After fork()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    /* IMPORTANT: disable stdio buffering so writes are immediate */
    setbuf(stdout, NULL);

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* Child: stdout fd is inherited and shared */
        printf("[Child ] Writing to stdout (fd=%d)\n",
               STDOUT_FILENO);
        write(STDOUT_FILENO, "[Child ] Raw write\n", 19);
        _exit(0);
    }

    wait(NULL);
    printf("[Parent] Writing to stdout (fd=%d)\n",
           STDOUT_FILENO);
    write(STDOUT_FILENO, "[Parent] Raw write\n", 19);

    return 0;
}
Key takeaway: After fork(), both parent and child write to the same terminal because they share the same stdout open file description. Always disable stdio buffering (setbuf(stdout, NULL)) in programs with fork() to avoid output being mixed up.

🅾 Interview Questions
Q1: What is the difference between a file descriptor and an open file description?

A file descriptor is an integer index (per-process) into the file descriptor table. An open file description is a kernel structure (in the open file table) that stores the file offset, status flags, and access mode. Multiple file descriptors (in same or different processes) can refer to the same open file description.

Q2: After fork(), if the child changes the file offset, does the parent see it?

Yes. The file offset is stored in the open file description (kernel level), which is shared between parent and child. Any lseek() or read/write that advances the offset in the child is immediately visible to the parent through its file descriptor.

Q3: Why should you call setbuf(stdout, NULL) before fork()?

stdio uses buffering. If stdout is line-buffered or block-buffered, data sitting in the buffer gets copied to the child’s memory during fork(). Both parent and child may flush that same data to the terminal, causing duplicate output. Disabling buffering ensures each write goes directly to the kernel and is not duplicated.

Q4: What is the close-on-exec flag and how does it relate to fork()?

The close-on-exec flag (FD_CLOEXEC) causes a file descriptor to be automatically closed when the process calls exec(). It is NOT shared via the open file description — each process has its own flag per fd. You set it with fcntl(fd, F_SETFD, FD_CLOEXEC) or via O_CLOEXEC in open().

Q5: How does shared file offset prevent output interleaving when writing to a file?

If parent and child both have the same fd pointing to the same open file description, their writes advance the shared file offset atomically. Each write() is atomic for small sizes (under PIPE_BUF or for O_APPEND). With O_APPEND, both processes always write to the current end of file, preventing overwriting each other’s data.

Series Navigation
Topic 3 → Subtopic 1 of 3

← Previous Next: Shared Offset & Flags → Index

Leave a Reply

Your email address will not be published. Required fields are marked *