Text Segment Sharing After fork()

Text Segment Sharing After fork()
Topic 4 → Subtopic 2  |  Why the code segment is always shared, never copied
Topic 4
Memory Semantics
Subtopic 2
of 3
3
Code Examples

The Text Segment is Always Shared

The text segment contains the compiled machine code of your program. Unlike the data, heap, and stack, the text segment is read-only — your code doesn’t modify itself at runtime. Because it never changes, after fork() the parent and child always share the same physical code pages. No copy-on-write needed, no copy ever made.

Keywords:

text segment read-only pages mmap shared library /proc/PID/maps page permissions r-xp ELF

💡 Why the Text Segment is Permanently Shared
🔒 It’s Read-Only

The kernel maps the text segment with r-x (read + execute, no write) permissions. Any attempt to write to code pages causes a SIGSEGV. Since it can never be written, CoW is never triggered.

🌐 Identical in Parent and Child

After fork(), the child is a copy of the parent — both run the same program. Their code is identical, so there is zero reason to copy it. Sharing saves RAM proportional to the program’s code size.

📚 Shared Libraries Too

Not just your code — the text pages of libc.so, libpthread.so and every shared library are also shared across all processes that use them. This is a major memory saving system-wide.

📄 Parent & Child Share the Same Physical Code Pages

Parent Page Table
Text VA 0x400000
Text VA 0x401000
data/heap/stack (CoW)

→ Physical Frame 501 ←
→ Physical Frame 502 ←
SHARED
read-only
r-x-p

Child Page Table
Text VA 0x400000
Text VA 0x401000
data/heap/stack (CoW)

Both page tables point to the same physical frames for text. No copy ever.

💻 Code Example 1: Viewing Text Segment in /proc/PID/maps

This program reads its own memory map before and after fork to show that the text segment virtual address is identical in parent and child:

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

void print_text_mapping(const char *label)
{
    char path[64], line[256];
    FILE *f;
    snprintf(path, sizeof(path), "/proc/%d/maps", getpid());
    f = fopen(path, "r");
    if (!f) return;

    printf("\n--- %s (PID=%d) text mappings ---\n", label, getpid());
    while (fgets(line, sizeof(line), f)) {
        /* Show only executable (code) pages marked r-xp */
        if (line[3] == 'x')   /* r-xp = read+exec, private */
            printf("  %s", line);
    }
    fclose(f);
}

int main(void)
{
    print_text_mapping("Parent before fork");

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

    if (pid == 0) {
        print_text_mapping("Child after fork");
        /* Notice: same virtual address range as parent */
        _exit(0);
    }

    wait(NULL);
    return 0;
}
/* Compile: gcc -o maps_demo maps_demo.c
   Expected: parent and child show IDENTICAL r-xp addresses */
Expected output: Both parent and child show the same virtual address range for the text segment (e.g., 00400000-00401000 r-xp). The r-xp flags mean read + execute, private mapping (but shared physically).

💻 Code Example 2: Function Pointers Work Identically in Child

Because the text segment is shared (same virtual addresses), function pointers set in the parent work correctly in the child without any changes:

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

typedef int (*op_fn)(int, int);

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int sub(int a, int b) { return a - b; }

int main(void)
{
    /* Parent sets up function pointer table */
    op_fn ops[3] = { add, mul, sub };
    const char *names[3] = { "add", "mul", "sub" };

    printf("[Parent] Function pointers set up:\n");
    for (int i = 0; i < 3; i++)
        printf("  ops[%d] = %s, address = %p\n",
               i, names[i], (void*)ops[i]);

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

    if (pid == 0) {
        /* Child: function pointers still valid — same text segment */
        printf("\n[Child] Using inherited function pointers:\n");
        for (int i = 0; i < 3; i++)
            printf("  ops[%d](10, 3) = %d (address %p)\n",
                   i, ops[i](10, 3), (void*)ops[i]);
        _exit(0);
    }

    wait(NULL);
    printf("\n[Parent] Function pointers still valid after child exits:\n");
    for (int i = 0; i < 3; i++)
        printf("  ops[%d](10, 3) = %d\n", i, ops[i](10, 3));

    return 0;
}
Note: The function addresses printed by parent and child are identical because both use the same virtual address space layout (same ELF binary, same text segment VA).

💻 Code Example 3: Shared Libraries Also Share Text Pages
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <math.h>

void show_lib_mappings(const char *label)
{
    char path[64], line[256];
    FILE *f;
    snprintf(path, sizeof(path), "/proc/%d/maps", getpid());
    f = fopen(path, "r");
    if (!f) return;

    printf("\n--- %s (PID=%d) library text pages ---\n",
           label, getpid());
    while (fgets(line, sizeof(line), f)) {
        /* Show libc and libm executable mappings */
        if (line[3] == 'x' &&
            (strstr(line, "libc") || strstr(line, "libm")))
            printf("  %s", line);
    }
    fclose(f);
}

int main(void)
{
    /* Use libm to ensure it's loaded */
    double x = sqrt(2.0);
    printf("sqrt(2) = %f (forces libm load)\n\n", x);

    show_lib_mappings("Parent");

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

    if (pid == 0) {
        show_lib_mappings("Child");
        /* libc/libm text pages: same virtual addresses as parent
           AND same physical pages — shared across the whole system */
        _exit(0);
    }

    wait(NULL);
    printf("\nlibc text pages are shared across all processes.\n");
    printf("A 2 MB libc is mapped once in RAM, shared by 100 processes = only 2 MB used.\n");
    return 0;
}
/* Compile: gcc -o lib_sharing lib_sharing.c -lm */
System-wide implication: On a typical Linux system with 200 processes all using libc (~2 MB code), without sharing that would be 400 MB just for libc text pages. With text sharing, it’s 2 MB total. This is a fundamental Linux memory optimization.

🅾 Interview Questions
Q1: Is the text segment subject to copy-on-write after fork()?

No. The text segment is mapped read-only (r-x permissions). Because it can never be written, copy-on-write is never triggered for it. Parent and child permanently share the exact same physical pages of code. This applies to the program’s own code and all shared library text pages.

Q2: What page permissions does the text segment have and why?

The text segment has r-x (read + execute, no write) permissions. Read allows loading instructions. Execute allows the CPU to run code on those pages. No write permission means any attempt to modify code at runtime causes SIGSEGV. This also protects against certain code injection attacks.

Q3: How does shared library text page sharing benefit the whole system?

The text (code) pages of shared libraries like libc are mapped read-only into every process that uses them. Because they are read-only and identical, the kernel maps them to the same physical frames for all processes. 200 processes using a 2 MB libc still only need 2 MB of physical RAM for that code — not 400 MB.

Q4: What does /proc/PID/maps show for the text segment?

The text segment appears as a line with r-xp permissions (read, execute, no-write, private mapping). It shows the virtual address range and the path to the ELF binary. For shared libraries, each library has its own r-xp entry. Parent and child show identical virtual addresses.

Series Navigation
Topic 4 → Subtopic 2 of 3

← Previous Next: Memory Footprint Control → Index

Leave a Reply

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