How fork() Works Internally

How fork() Works Internally
Topic 2 → Subtopic 1  |  What the kernel does when you call fork()
Topic 2
fork() Basics
Subtopic 1
of 4
3
Code Examples

What Really Happens Inside fork()?

When your program calls fork(), the Linux kernel does a lot of work in the background. It doesn’t just blindly copy everything — it’s smart about memory. Understanding what happens internally helps you write better, bug-free programs that use processes correctly.

Keywords:

kernel process descriptor page table copy-on-write virtual memory text segment (shared) indeterminate scheduling CPU time slice

⚙ Step-by-Step: What the Kernel Does During fork()
1
Allocate new process descriptor (task_struct)
Kernel creates a new task_struct for the child and assigns it a new PID.
2
Copy parent’s process attributes
Most fields copied: signal handlers, open file descriptors, credentials (UID/GID), working directory, etc.
3
Share the text (code) segment
The child’s page table entries for the text segment point to the same physical pages as the parent. Marked read-only — no copy needed.
4
Mark data/heap/stack pages as copy-on-write
Child’s page table entries point to parent’s pages, but both are marked read-only. Actual copying deferred until a write happens.
5
Set return values and schedule
Parent’s fork() returns child PID. Child’s fork() returns 0. Both are placed in the run queue. Kernel decides which runs first.

📋 What the Child Inherits vs What is Unique
🔄 Child INHERITS (copy of parent’s)
Stack, data, heap segments (copy-on-write)
Open file descriptors
File offset positions
Signal handlers
Signal mask
Process group ID
Controlling terminal
Real UID, GID
Working directory (cwd)
Root directory
Resource limits (ulimit)
Environment variables
Program text (shared, read-only)
🔴 Child gets its OWN (unique)
Process ID (PID)
Parent Process ID (PPID = parent’s PID)
Its own copy of pending signals
File locks are NOT inherited
CPU time counters reset to 0
Memory locks (mlock) not inherited
Semaphore adjustments (semadj) reset
Timers (alarm, setitimer) reset
Asynchronous I/O not inherited

💻 Code Example 1: Proving the Child Inherits from Parent

This shows the child inherits the working directory and environment:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    char cwd[256];
    pid_t pid;

    /* Parent changes directory */
    if (chdir("/tmp") == -1) { perror("chdir"); exit(1); }
    getcwd(cwd, sizeof(cwd));
    printf("[Parent] CWD = %s\n", cwd);

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

    if (pid == 0) {
        /* Child checks its own cwd */
        getcwd(cwd, sizeof(cwd));
        printf("[Child PID=%d] CWD inherited = %s\n",
               getpid(), cwd);

        /* Child changes to a DIFFERENT directory */
        chdir("/var");
        getcwd(cwd, sizeof(cwd));
        printf("[Child] After chdir: CWD = %s\n", cwd);
        exit(0);
    }

    wait(NULL);

    /* Parent's cwd is NOT changed by child's chdir */
    getcwd(cwd, sizeof(cwd));
    printf("[Parent] After child exits: CWD = %s\n", cwd);

    return 0;
}
Output confirms: Child inherits /tmp, changes to /var, but parent still sees /tmp. They have independent CWD copies.

💻 Code Example 2: Signal Handlers Are Inherited
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void sigint_handler(int sig)
{
    printf("[PID=%d] Caught SIGINT (Ctrl+C)\n", getpid());
}

int main(void)
{
    /* Install SIGINT handler in parent */
    signal(SIGINT, sigint_handler);

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

    if (pid == 0) {
        /* Child inherits the SIGINT handler */
        printf("[Child PID=%d] Handler inherited. "
               "Sending SIGINT to myself...\n", getpid());
        raise(SIGINT);  /* send signal to self */
        printf("[Child] After signal handler returned.\n");
        exit(0);
    }

    wait(NULL);

    /* Parent can also reset child's handlers after fork if needed */
    printf("[Parent] Done.\n");
    return 0;
}
Important: The child inherits the parent’s SIGINT handler. After exec(), all signal handlers reset to defaults (SIG_DFL) because the old handler code is gone.

💻 Code Example 3: What Is NOT Inherited — File Locks
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(void)
{
    int fd;
    struct flock lock;

    /* Open a file and take an exclusive write lock */
    fd = open("/tmp/test_lock.txt",
              O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); exit(1); }

    lock.l_type   = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start  = 0;
    lock.l_len    = 0;  /* lock whole file */

    if (fcntl(fd, F_SETLK, &lock) == -1) {
        perror("fcntl lock"); exit(1);
    }
    printf("[Parent] File locked.\n");

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

    if (pid == 0) {
        /* Child does NOT inherit the file lock.
           File locks belong to the process, not the fd. */
        printf("[Child PID=%d] File lock is NOT inherited.\n",
               getpid());
        /* Child can acquire its own lock if needed */
        exit(0);
    }

    wait(NULL);

    /* Unlock */
    lock.l_type = F_UNLCK;
    fcntl(fd, F_SETLK, &lock);
    close(fd);

    printf("[Parent] Lock released.\n");
    return 0;
}

🅾 Interview Questions
Q1: Does fork() copy the entire memory of the parent?

Not immediately. Linux uses copy-on-write (CoW). The text segment is shared (read-only). Data, heap, and stack page table entries point to the same physical pages, but are marked read-only. A physical copy is made only when either process tries to write to that page.

Q2: Does the child inherit the parent’s signal handlers?

Yes, after fork() the child inherits all signal dispositions. However, after exec(), all signal handlers reset to their default actions (SIG_DFL), because the handler code belongs to the old program that no longer exists.

Q3: Are file locks inherited by the child after fork()?

No. POSIX file locks (fcntl F_SETLK) are associated with a process, not a file descriptor. The child does NOT inherit the parent’s file locks. The child starts with no file locks, even if it inherits the same open file descriptors.

Q4: What happens to pending timers (alarm) after fork()?

The child does NOT inherit pending alarms. The alarm clock is reset to zero for the child. The parent’s alarm continues unaffected. Interval timers (setitimer) are also not inherited by the child.

Q5: After fork(), which process runs first — parent or child?

It is indeterminate and depends on the kernel scheduler and system load. Linux 2.6.32+ runs the parent first by default (configurable via /proc/sys/kernel/sched_child_runs_first). Programs must never rely on a specific execution order — use synchronization if order matters.

Series Navigation
Topic 2 → Subtopic 1 of 4

← Previous Next: fork() Return Values → Index

Leave a Reply

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