Race Conditions After fork()

Race Conditions After fork()
Topic 6 → Subtopic 1  |  Why indeterminate execution order causes bugs
Topic 6
Race Conditions
Subtopic 1
of 2
3
Code Examples

The Problem: Who Runs First?

After fork(), both parent and child are placed in the kernel’s run queue. The scheduler decides which one runs first — and this decision is indeterminate. It can change between runs, between machines, under different load conditions. Any program that assumes a specific ordering between parent and child has a race condition.

Keywords:

race condition scheduler indeterminate order time slice context switch non-deterministic output interleaving synchronization

🔃 What is a Race Condition After fork()?

A race condition occurs when the outcome of a program depends on the relative timing of two or more concurrent operations. After fork(), if parent and child both access shared resources (files, terminals, pipes) without synchronization, their output order becomes unpredictable.

Run 1: Parent first
Parent: step A
Parent: step B
Child: step A
Child: step B
Run 2: Child first
Child: step A
Parent: step A
Child: step B
Parent: step B
Run 3: Interleaved
Parent: step A
Child: step A
Parent: step B
Child: step B

All three outputs above come from the same program. If your program’s correctness depends on any specific ordering, it has a race condition.

💻 Code Example 1: Classic Race Condition Demo (Output Varies)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    int num_children = (argc > 1) ? atoi(argv[1]) : 3;
    int i;

    setbuf(stdout, NULL);   /* disable buffering */

    for (i = 0; i < num_children; i++) {
        switch (fork()) {
        case -1:
            perror("fork"); exit(1);
        case 0:
            /* Child: print its number and exit */
            printf("%d ", i);
            _exit(EXIT_SUCCESS);
        default:
            break;
        }
    }

    /* Parent: wait for all children */
    for (i = 0; i < num_children; i++)
        wait(NULL);

    /* Parent also prints */
    printf("\n");

    return 0;
}
/* Run: ./race 5
   Output changes every run!
   e.g.: 2 0 1 3 4     (one run)
         0 2 3 1 4     (next run)
         1 0 4 2 3     (next run)          */
Try it: Run this program multiple times — you’ll see different orderings of the numbers each time. This proves the non-deterministic scheduling after fork().

💻 Code Example 2: Race Condition on a Shared File
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

#define FILE_PATH "/tmp/race_test.txt"

void write_lines(const char *prefix, int count)
{
    int fd = open(FILE_PATH, O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (fd == -1) { perror("open"); return; }

    char buf[64];
    for (int i = 0; i < count; i++) {
        int len = snprintf(buf, sizeof(buf),
                           "%s line %d\n", prefix, i+1);
        write(fd, buf, len);
        /* Without usleep: scheduling decides interleaving.
           With usleep: forces more interleaving for demo. */
        usleep(100);
    }
    close(fd);
}

int main(void)
{
    /* Clear the file */
    int fd = open(FILE_PATH, O_WRONLY|O_CREAT|O_TRUNC, 0644);
    close(fd);

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

    if (pid == 0) {
        write_lines("CHILD ", 5);
        _exit(0);
    }

    write_lines("PARENT", 5);
    wait(NULL);

    printf("File contents (order varies each run):\n");
    system("cat " FILE_PATH);
    return 0;
}
Observation: The file contents vary between runs. Sometimes all PARENT lines come first, sometimes they interleave with CHILD lines. This is a race condition. Fix: use O_APPEND (for ordering) or file locking (for atomic blocks of lines).

💻 Code Example 3: Why sleep() Does NOT Fix Race Conditions
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    setbuf(stdout, NULL);

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

    if (pid == 0) {
        /* Anti-pattern: using sleep to "ensure" child runs after parent */
        sleep(1);  /* Does this guarantee parent ran first? NO! */
        printf("[Child ] I ran after 1 second\n");
        _exit(0);
    }

    /* Anti-pattern: parent prints without synchronization */
    printf("[Parent] I ran immediately\n");

    wait(NULL);
    return 0;
}
/* This LOOKS ordered, but:
   - What if the system is loaded and parent is preempted before printf?
   - What if sleep() returns early due to a signal?
   - sleep() is NOT a synchronization mechanism
   The correct fix: use signals, pipes, semaphores, or mutexes. */
Anti-pattern: Using sleep() to enforce ordering is wrong. sleep(1) is a hint to the scheduler, not a guarantee. Under load, a sleeping process may not wake on time; a non-sleeping process may be preempted. The correct solution is explicit synchronization with signals or pipes (see Topic 7).

🅾 Interview Questions
Q1: What is a race condition in the context of fork()?

A race condition occurs when the program’s outcome depends on the scheduling order of parent and child processes after fork(). Since the kernel scheduler decides which process runs next (indeterminate), programs that assume a specific ordering between parent and child will produce unpredictable results.

Q2: Can you use sleep() to prevent race conditions after fork()?

No. sleep() is not a synchronization mechanism. It tells the scheduler to not schedule this process for N seconds, but there is no guarantee about other processes’ execution during that time. Under different load conditions, the sleep may not achieve the desired ordering. Use explicit synchronization: signals, pipes, semaphores, or mutexes.

Q3: What mechanisms can you use to synchronize parent and child after fork()?

(1) Signals: Parent sends SIGUSR1 to child when ready; child uses sigsuspend() to wait. (2) Pipes: Parent writes a byte when ready; child blocks on read() until byte arrives. (3) Semaphores: POSIX semaphores or System V semaphores. (4) Shared memory + mutex: Using pthread_mutex in shared memory region.

Series Navigation
Topic 6 → Subtopic 1 of 2

← Previous Next: Scheduling Order → Index

Leave a Reply

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