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.
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:
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.
On a typical dynamically linked Linux program, the executable identifies a program interpreter, the dynamic loader.
During startup, the kernel and loader cooperate to:
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.
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.
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.
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:
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:
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.
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.
Shared-library code is one example of physical page sharing. Virtual memory supports several related forms.
| Kind of page | Why it can be shared | What happens on write? |
|---|---|---|
| Read-only executable or library page | Identical file contents | Write is invalid |
| Private file-backed data page | Initial contents are identical | Writer receives CoW page |
Page inherited through fork() | Parent and child start with same contents | Writer receives CoW page |
| Shared writable mapping | Changes are intentionally common | Other mappers can observe write |
| Shared zero page | Untouched pages all read as zero | Writer 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...
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.
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.
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.
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:
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.
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.
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.
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.
Shared libraries can reduce:
They also introduce:
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.
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:
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.
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.
5 quizzes