AlgoMaster Logo

Shared Libraries and Shared Pages

33 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A server runs 100 worker processes, and every worker uses the same C library. If each process required a separate physical copy of 2 MiB of library instructions, those identical bytes would consume roughly:

The processes need separate address spaces, but they do not need separate physical frames for instructions that are identical and read-only.

The operating system can map the library's code pages into every process while connecting those mappings to the same physical file-backed pages:

One copy of the code in physical memory serves all three processes, no matter how many more start.

Each process retains its own page tables and virtual addresses. What is shared is the physical storage holding common contents.

Shared libraries save storage and physical memory by letting multiple programs use one library file and share its unchanged resident pages.

Writable library state is different. Each process normally needs its own global variables and runtime data. Virtual memory combines direct read-only sharing with copy-on-write private pages to provide both efficiency and isolation.

Static and Dynamic Linking

A program can receive library code in two broad ways.

With static linking, the linker copies needed library code into the executable file:

Application source combined with a static library produces one self-contained executable.

The operating system can share that executable's read-only pages among multiple processes running the same file. However, if ten different executable files each contain their own static copy of the same library function, those bytes belong to ten file images. The system does not ordinarily treat them as one shared library mapping merely because their machine instructions happen to be identical.

With dynamic linking, the executable records dependencies on separate shared-library files:

At runtime, the dynamic loader maps those library files into the process and connects symbol references to the mapped implementations.

This has several benefits:

  • Different executables can map the same library file.
  • Read-only resident pages can be physically shared.
  • Executable files are smaller because they do not embed full library copies.
  • A compatible library update can serve newly started applications without rebuilding each executable.

Dynamic linking also introduces runtime dependencies, symbol-resolution work, version compatibility requirements, and an additional loader phase during startup.

Static and dynamic linking are build and loading choices. Physical page sharing is the virtual-memory optimization that makes dynamic libraries especially memory-efficient across processes.

What the Dynamic Loader Does

On a typical dynamically linked Linux program, the executable identifies a program interpreter, the dynamic loader.

During startup, the kernel and loader cooperate to:

  1. Map the main executable's loadable regions.
  2. Start the dynamic loader.
  3. Discover the executable's required shared libraries.
  4. Locate and map each library's loadable regions.
  5. Resolve addresses that depend on runtime placement or symbol selection.
  6. Apply permitted relocations.
  7. Run required initialization.
  8. Transfer control to the program.

The executable records library dependencies in dynamic metadata. Inspect them with:

A result can resemble:

ldd also displays dependencies on many Linux systems:

Do not run ldd casually on an untrusted executable. Depending on the system and file, dependency inspection can involve the program interpreter or behavior that should not be trusted. readelf inspects file metadata without loading the program.

Each process performs its own loading and symbol-resolution work. “Shared library” does not mean one process performs startup on behalf of every other process. It means their mappings can refer to common file contents and physical pages where semantics permit.

Multiple Mappings for One Library

A shared library file contains regions with different purposes and permissions. The loader maps them separately.

A process map can show entries resembling:

The exact layout varies, but the roles are commonly:

The file-offset column identifies which portion of the library backs each mapping.

On Linux, the trailing p in permissions such as r-xp means the mapping is private rather than MAP_SHARED. Private does not mean its current physical frame cannot be shared. An unmodified private file page can be shared physically; private means that modifications are not propagated as shared file changes.

This distinction is central:

Read-only mappings never diverge. Private writable mappings can share initial contents and split through copy-on-write when a process modifies them.

How Read-Only Library Pages Are Shared

Suppose Process A and Process B both map the same library file page:

Their virtual addresses can differ:

The kernel recognizes the same underlying file object and file offset. When those contents are resident, both page tables can point to the same physical frame.

Each PTE carries permissions appropriate to its process mapping. Each CPU still needs process-specific translation state. Sharing the data frame does not share page tables or TLB entries.

The page can be brought into memory by either process. If Process A faults first and storage supplies the page, Process B can later fault and reuse the already resident cached page. Process B still needs its own page-table mapping, but it does not need another physical copy or another storage read for those same cached bytes.

Read-only sharing needs no application lock. Because no process may modify the frame through that mapping, the contents cannot race.

Why Code Must Be Position Independent

The same library can appear at different virtual addresses in different processes. Its instructions must therefore avoid assuming one fixed load address.

Position-independent code, or PIC, uses addressing forms that continue working when the complete library moves.

Instead of embedding an absolute address such as:

PIC uses relative addressing or an indirection structure whose process-specific entries can be initialized at load time.

Conceptually:

ELF systems commonly use structures such as the Global Offset Table, or GOT, to hold resolved addresses for data and other symbols. Procedure linkage machinery performs a related role for function calls.

The instruction pages remain byte-for-byte identical and read-only across processes. Process-specific addresses live in writable private data pages.

This division preserves sharing:

A relocation that modifies an executable code page separately in every process would make that page dirty and private, destroying physical sharing. It also requires code to be writable during modification, which weakens write-versus-execute protection.

Modern shared libraries are therefore normally compiled as position-independent code:

Relocations and Sharing

A relocation tells the loader that some address-dependent value must be finalized when the program's runtime layout or symbol definitions are known.

Not every relocation harms sharing.

If a relocation updates a writable process-private table:

If it modifies a code page:

The dynamic loader tries to keep relocation writes in appropriate data regions. Some relocation-related regions can be made read-only after initialization, reducing accidental or malicious modification.

Relocation work still has a memory cost. A page containing process-specific loader data becomes private and dirty even if most of the library remains shared.

This explains why a mapped library is not 100% physically shared:

Private Writable Library Globals

Consider a shared library:

Two unrelated processes load the same library and call record_request().

They must not update one machine-wide request_count merely because the library code is shared. Each process expects ordinary global variables to belong to its own address space.

Initially, the file-derived data page can be physically shared under copy-on-write protection:

When Process A increments request_count, the write fault gives A a private frame:

When B later writes, it receives or uses its own private writable contents.

Zero-initialized library data has the same process-private semantics even though its initial bytes may come from anonymous zero-filled memory rather than stored file bytes.

Ordinary library globals are therefore not an interprocess communication mechanism. A library must use an explicitly shared mapping or another IPC mechanism when state should be common across processes.

Thread-Local Library State

A shared library can also define state with one instance per thread.

Conceptually:

The loader and threading runtime arrange thread-local storage so the same position-independent instruction can locate the current thread's instance.

This creates three distinct sharing scopes:

The word shared in “shared library” describes reusable code and file mappings. It does not imply that every variable declared by the library is shared across every caller.

Categories of Shared Pages

Shared-library code is one example of physical page sharing. Virtual memory supports several related forms.

Kind of pageWhy it can be sharedWhat happens on write?
Read-only executable or library pageIdentical file contentsWrite is invalid
Private file-backed data pageInitial contents are identicalWriter receives CoW page
Page inherited through fork()Parent and child start with same contentsWriter receives CoW page
Shared writable mappingChanges are intentionally commonOther mappers can observe write
Shared zero pageUntouched pages all read as zeroWriter receives private zero-filled page

The sharing contract determines how writes behave.

Read-only sharing is simplest because the frame never changes. CoW sharing is temporary and preserves private semantics. Shared writable memory is a communication mechanism and requires synchronization.

Two virtual mappings can share a frame while having different permissions. One process can map common data read-only while another authorized process maps it writable. The hardware checks each mapping independently.

Loading simulation...

Shared Pages and Memory Accounting

Per-process RSS includes resident pages mapped into that process, even when their frames are also mapped elsewhere.

Suppose ten processes each map and touch the same 20 MiB of library code:

The sum of RSS overstates unique physical use by roughly 180 MiB in this simplified example.

PSS divides each shared frame among its mappers:

Summed PSS is therefore closer to the physical memory represented by those mappings.

Linux exposes mapping-level accounting in /proc/<pid>/smaps. A library mapping can include fields such as:

Read-only library code commonly contributes to Shared_Clean after multiple processes map it. Writable relocated or modified pages can contribute to private fields.

Accounting categories depend on which processes currently map a page and how the kernel classifies it. A page may appear private while only one process maps it, then become shared when another process uses the same file page.

The main operational rule is:

Do not estimate unique machine memory by adding RSS across processes that share executables, libraries, or CoW pages.

Demand Paging and Warm Libraries

Mapping a shared library does not make all of its pages resident.

The process can map 10 MiB of library ranges while touching only 2 MiB of instructions and constants. The untouched pages consume virtual address range and mapping metadata but need no resident data frames.

The first process to execute a cold library path can incur major faults if storage must supply its pages. Later processes can receive minor faults for the same file pages when those contents remain cached:

The later process still incurs page-fault and page-table work. Physical sharing avoids redundant storage and frames, not every per-process setup cost.

This contributes to warm-host behavior. A newly started service can load faster when commonly used runtime and library pages are already resident because other processes used them.

It also means removing one process does not necessarily free shared library frames. Other mappings or the file cache can continue using them.

Dynamic Libraries Versus Explicit Shared Memory

A shared library and a shared-memory region solve different problems.

Loading the same .so file into two processes does not create a shared global-variable store.

If a library internally creates an explicit shared mapping, its processes can communicate through that mapping. The sharing comes from the mapping policy, not from the library being dynamically linked.

This distinction prevents a common design error: placing a global variable in a shared library and expecting every process to see one common value.

Runtime Loading with dlopen()

Linux and other Unix-like systems allow a process to load a shared library after startup:

The dynamic loader maps the library and its dependencies, applies relocations, and makes requested symbols available:

Runtime loading supports plugin systems and optional features. It also adds lifecycle concerns:

  • Symbol lookups can fail.
  • Library initialization can execute code.
  • Dependencies and versions must be compatible.
  • Function and data pointers become unsafe after the library is unloaded.
  • Multiple components can hold references to the same loaded library.

dlclose() releases one loader reference. The implementation decides when mappings can actually be removed, but application code must not continue using symbols whose lifetime has ended.

Physical page sharing works the same way as for startup-loaded libraries. If another process maps the same file pages, read-only contents can use common frames.

What Happens When a Library File Is Updated?

A running process does not automatically switch to a newly installed library file.

Package managers commonly replace a library by creating a new file and atomically renaming it over the old pathname. Existing processes retain references to the old file object through their mappings:

Both versions can coexist in physical memory until old processes exit or unload the old mapping.

On Linux, an old mapping can appear with a path marked:

The pathname entry was removed, but the mapped file object remains alive for existing users.

This is why updating a shared-library package usually requires restarting long-running services to make them use the new code.

Modifying a library file in place while processes execute it is unsafe. Running processes can observe inconsistent file-backed contents, and executable mappings can fail in difficult ways. Deployment should replace files atomically and restart dependent processes under a controlled rollout.

Inspecting Shared Libraries on Linux

List dynamic dependencies without executing the program:

Inspect the loadable segments and their permissions:

Actual library paths differ by distribution and architecture.

For a running process:

Look for repeated paths with different permissions and file offsets. The executable region commonly has r-xp, while writable data commonly has rw-p.

To inspect per-mapping physical accounting:

Find the library pathname, then compare fields such as Rss, Pss, Shared_Clean, and Private_Dirty across its mapping blocks.

Two processes can show different virtual addresses for the same library. That does not prevent physical page sharing because sharing is based on the backing file object and offset, not equal pointer values.

Ordinary user tools generally do not need to translate each page to a physical frame to establish the likely sharing relationship. File identity, offsets, permissions, and PSS-style accounting provide the useful operational view.

A Buildable Shared-Library Experiment

Create counter.c:

Build it as position-independent shared code:

Create app.c:

Link the application against the local shared library:

Run two instances:

Each process prints:

The library's function code is mapped from the same libcounter.so file and can use shared physical instruction pages. The library_counter global is private to each process, so each increments its own initial zero to one.

The /proc output can show different virtual addresses while retaining the same file offsets and library pathname.

The $ORIGIN runtime search path is convenient for this local experiment. Production library search and deployment require a deliberate security and versioning policy.

Performance Benefits and Costs

Shared libraries can reduce:

  • Executable file size
  • Duplicate physical instruction and constant pages
  • Storage reads when library pages are already resident
  • Aggregate memory for many similar processes

They also introduce:

  • Dynamic-loader startup work
  • Page-table entries in every process
  • Per-process relocation and writable data pages
  • TLB pressure for each process's library mappings
  • Runtime dependency and version-management risk
  • Potential symbol-indirection overhead

The physical data frame can be shared, but each process still needs its own virtual mapping, permissions, and translation state.

Memory savings depend on workload behavior. A library with 50 MiB of optional code may contribute only a few resident pages if most processes never call those paths. A library that performs relocations or writes throughout many data pages can create more private memory.

Static linking is not automatically memory-inefficient for multiple copies of the same executable; those processes can share that executable's file-backed code pages. Dynamic linking's distinctive sharing advantage appears across different executables that use one common library file.

Security and Correctness Boundaries

Physical sharing does not grant one process access to another process's virtual address space.

Each process has its own PTE:

The MMU enforces permissions independently. Neither process can write the code frame through those mappings.

Writable shared mappings require explicit intent. When processes communicate through one, they must define:

  • Which bytes form each object
  • How updates become atomic or synchronized
  • How initialization and versioning work
  • What happens if one process crashes mid-update

Read-only sharing avoids these coordination problems because the content is immutable through the mapping.

Dynamic loading itself executes code from another file in the process. Library search paths, file ownership, signatures or package trust, and version selection are therefore security-sensitive. A writable untrusted directory in a library search path can turn dependency resolution into code execution.

Summary

Shared libraries let different executables map one library file. Read-only instruction and constant pages can be backed by the same physical frames even when each process uses different virtual addresses. Every process still maintains its own page tables, TLB state, loader metadata, and writable memory.

Position-independent code keeps executable pages unchanged across runtime placements. Process-specific addresses and relocations are concentrated in private data structures. Ordinary writable library globals begin from common file or zero-filled contents but become private through copy-on-write; they are not interprocess shared variables.

Shared pages reduce storage I/O and aggregate physical memory, but RSS can count the same frame in many processes, so PSS and private/shared mapping fields provide a clearer accounting view. Running processes retain their mapped library version across file replacement, and explicit shared writable mappings require synchronization and consistency protocols that read-only library sharing does not.

Quiz

Shared Libraries and Shared Pages Quiz

5 quizzes