Upgrading Shared Libraries Install new library versions while programs are running

 

Chapter 41 – Part 3 of 3
Upgrading Shared Libraries
Install new library versions while programs are running — zero-downtime live upgrades on Linux
3
Code Examples
10
Interview Q&A
~20
min read

The Beauty of Shared Library Upgrades

One of the most powerful features of shared libraries is the ability to install a new version while existing programs continue to run using the old version. This is a fundamental advantage over static libraries.

When a program is already running, its process holds an open file reference to the old .so file. Replacing the file on disk does not affect the running process. The new version is only picked up by programs launched after the upgrade. This gives you zero-downtime library upgrades — no need to stop long-running servers or daemons.

What Happens During a Live Upgrade
Time Running Program (OLD) New Program Launched (NEW)
Before upgrade Uses libdemo.so.1.0.1 (loaded into its address space)
During upgrade Still using 1.0.1 — inode still open. Unaffected.
After upgrade Still using 1.0.1 — no change Loads libdemo.so.1 → libdemo.so.1.0.2 — gets new version!

Key Terms in This Tutorial
minor version upgrade major version upgrade live upgrade zero downtime symlink update ldconfig inode mmap running process

1. Minor Version Upgrade — Step by Step

A minor version upgrade is a compatible change — bug fixes, performance improvements, or new functions. Programs using the old version continue to work with the new version without recompiling.

1
Write and compile the updated library code — the soname stays the same (e.g., libdemo.so.1)
2
Build the new .so with the updated real name — e.g., libdemo.so.1.0.2 (bumped patch/minor)
3
Copy the new .so to the library directory — e.g., /usr/lib/ or /usr/local/lib/
4
Run ldconfig — it automatically updates the soname symlink to point to the new file
5
Done! Already-running programs continue using the old file. New programs get the new version.

Code Example 1 — Full Minor Version Upgrade

/* ===== BEFORE UPGRADE: libdemo.so.1.0.1 ===== */

/* mod1.c — version 1.0.1 */
#include <stdio.h>
#include <string.h>

int add_numbers(int a, int b) {
    return a + b;
}

/* Bug: this had an off-by-one in certain conditions */
int count_chars(const char *s, char c) {
    int count = 0;
    while (*s) {
        if (*s == c) count++;
        s++;
    }
    return count;  /* was: count - 1; (the bug) */
}

void hello(void) {
    printf("libdemo v1.0.1\n");
}
/* ===== AFTER UPGRADE: libdemo.so.1.0.2 ===== */

/* mod1.c — version 1.0.2: bug fixed in count_chars */
#include <stdio.h>
#include <string.h>

int add_numbers(int a, int b) {
    return a + b;          /* unchanged */
}

/* Bug fixed! Now returns correct count */
int count_chars(const char *s, char c) {
    int count = 0;
    while (*s) {
        if (*s == c) count++;
        s++;
    }
    return count;  /* fixed */
}

/* New function added — compatible */
int subtract_numbers(int a, int b) {
    return a - b;
}

void hello(void) {
    printf("libdemo v1.0.2 (bug fixed!)\n");
}
# === BUILD THE NEW VERSION ===

gcc -g -c -fPIC -Wall mod1.c mod2.c

# soname stays libdemo.so.1 — same major version, compatible change
gcc -g -shared -Wl,-soname,libdemo.so.1 \
    -o libdemo.so.1.0.2 \
    mod1.o mod2.o

# === INSTALL ===
sudo cp libdemo.so.1.0.2 /usr/local/lib/

# Check before ldconfig
ls -l /usr/local/lib/libdemo*
# lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.1  ← still pointing to old
# -rwxr-xr-x libdemo.so.1.0.1
# -rwxr-xr-x libdemo.so.1.0.2

# === RUN LDCONFIG ===
sudo ldconfig -v | grep libdemo
# libdemo.so.1 -> libdemo.so.1.0.2   (changed) ← NOW points to new!

# Check after ldconfig
ls -l /usr/local/lib/libdemo*
# lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.2  ← updated!
# -rwxr-xr-x libdemo.so.1.0.1                  ← old file still exists
# -rwxr-xr-x libdemo.so.1.0.2                  ← new file

# === WHAT HAPPENS TO RUNNING PROGRAMS ===
# Programs already running have their own mmap of libdemo.so.1.0.1
# That file (inode) stays valid until all processes close it
# Running programs: still use 1.0.1 (unaffected by symlink change)
# New programs: load libdemo.so.1 -> libdemo.so.1.0.2 -> get the fix!
Cleanup tip: After verifying the new version works, you can delete the old file: sudo rm /usr/local/lib/libdemo.so.1.0.1. Already-running programs still hold a reference to the inode, so they keep working until they exit. Once all processes using the old file have exited, the OS frees the inode.

2. Major Version Upgrade — Step by Step

A major version upgrade introduces incompatible changes. Old programs must NOT use the new version. Both versions must coexist on the system simultaneously.

1
Write the new library with a new soname — e.g., libdemo.so.2 instead of libdemo.so.1
2
Build with the new real name — e.g., libdemo.so.2.0.0
3
Install alongside the old version — do NOT remove the old libdemo.so.1.x.y file
4
Run ldconfig — creates libdemo.so.2 → libdemo.so.2.0.0 while keeping libdemo.so.1 intact
5
Update the linker name symlinkln -sf libdemo.so.2 libdemo.so so new compilations use v2
6
Old programs still load v1 — their ELF says they need libdemo.so.1, which still exists

Coexistence of v1 and v2
File in /usr/lib/ Type Used By
libdemo.so.1.0.2 Real file (v1 latest) Old programs at runtime
libdemo.so.1 → libdemo.so.1.0.2 Soname symlink Dynamic linker resolves old programs
libdemo.so.2.0.0 Real file (v2) New programs at runtime
libdemo.so.2 → libdemo.so.2.0.0 Soname symlink Dynamic linker resolves new programs
libdemo.so → libdemo.so.2 Linker name symlink gcc -ldemo at compile time (new code gets v2)

Code Example 2 — Full Major Version Upgrade

# === CURRENT STATE: libdemo v1 is installed ===
ls -l /usr/lib/libdemo*
# lrwxrwxrwx libdemo.so   -> libdemo.so.1
# lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.2
# -rwxr-xr-x libdemo.so.1.0.2

# === BUILD v2 (INCOMPATIBLE — new soname) ===
# Assume mod1_v2.c and mod2_v2.c have breaking changes

gcc -g -c -fPIC -Wall mod1_v2.c mod2_v2.c

# NOTE: soname is libdemo.so.2 ← DIFFERENT from before
gcc -g -shared -Wl,-soname,libdemo.so.2 \
    -o libdemo.so.2.0.0 \
    mod1_v2.o mod2_v2.o

# Verify the embedded soname
readelf -d libdemo.so.2.0.0 | grep SONAME
# (SONAME) Library soname: [libdemo.so.2]

# === INSTALL (alongside v1 — do NOT remove v1!) ===
sudo cp libdemo.so.2.0.0 /usr/lib/

# === RUN LDCONFIG ===
sudo ldconfig -v | grep libdemo
#   libdemo.so.1 -> libdemo.so.1.0.2    ← v1 unchanged
#   libdemo.so.2 -> libdemo.so.2.0.0    ← v2 newly created

# Verify all files
ls -l /usr/lib/libdemo*
# lrwxrwxrwx libdemo.so   -> libdemo.so.1          ← still v1 linker name
# lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.2
# -rwxr-xr-x libdemo.so.1.0.2
# lrwxrwxrwx libdemo.so.2 -> libdemo.so.2.0.0      ← new soname created!
# -rwxr-xr-x libdemo.so.2.0.0

# === UPDATE LINKER NAME TO POINT TO v2 ===
# (so that new compilations use v2)
sudo ln -sf libdemo.so.2 /usr/lib/libdemo.so

ls -l /usr/lib/libdemo.so
# lrwxrwxrwx libdemo.so -> libdemo.so.2   ← now points to v2!
# === VERIFY OLD AND NEW PROGRAMS ===

# Old program (compiled against v1):
ldd old_program | grep libdemo
# libdemo.so.1 => /usr/lib/libdemo.so.1.0.2

# It still works! libdemo.so.1 exists and points to v1 code.

# Compile new program (uses v2 API):
gcc -o new_program new_main.c -ldemo -L/usr/lib
# -ldemo finds /usr/lib/libdemo.so -> libdemo.so.2

ldd new_program | grep libdemo
# libdemo.so.2 => /usr/lib/libdemo.so.2.0.0

# New program uses v2!
./new_program

Code Example 3 — Installing a New 2.x Minor Version

Once you have v2 installed, later minor releases (2.0.1, 2.1.0) follow the same minor upgrade process as before — ldconfig keeps the linker name up to date automatically.

# Current state: libdemo.so.2.0.0 is installed
ls -l /usr/lib/libdemo*
# lrwxrwxrwx libdemo.so   -> libdemo.so.2
# lrwxrwxrwx libdemo.so.2 -> libdemo.so.2.0.0
# -rwxr-xr-x libdemo.so.2.0.0

# === BUILD 2.0.1 — compatible fix within v2 ===
gcc -g -c -fPIC -Wall mod1_v2.c mod2_v2.c

# soname is still libdemo.so.2 — still v2 major
gcc -g -shared -Wl,-soname,libdemo.so.2 \
    -o libdemo.so.2.0.1 \
    mod1_v2.o mod2_v2.o

# Install
sudo mv libdemo.so.2.0.1 /usr/lib/

# Run ldconfig
sudo ldconfig -v | grep libdemo
#   libdemo.so.2 -> libdemo.so.2.0.1   (changed) ← soname updated!

# Verify
ls -l /usr/lib/libdemo*
# lrwxrwxrwx libdemo.so   -> libdemo.so.2
# lrwxrwxrwx libdemo.so.2 -> libdemo.so.2.0.1    ← updated by ldconfig
# -rwxr-xr-x libdemo.so.2.0.0
# -rwxr-xr-x libdemo.so.2.0.1

# Notice: libdemo.so -> libdemo.so.2 -> libdemo.so.2.0.1
# The linker name chain is automatically "up to date" because:
# libdemo.so points to the soname, and ldconfig updated the soname.
# So the linker name is also effectively updated — no manual step needed!
Key insight: When the linker name points to the soname (libdemo.so → libdemo.so.2), and ldconfig updates the soname (libdemo.so.2 → libdemo.so.2.0.1), the linker name chain is automatically up to date for free. This is another reason why the two-level symlink design (linker name → soname → real file) is elegant.

4. Why Running Programs Survive — The inode Mechanism

Understanding why live upgrades work requires understanding how Linux handles file deletion and inode reference counts.

What Happens to the Old File
Before upgrade
Filesystem:
libdemo.so.1.0.1 → inode #1234

Running Process:
mmap → inode #1234
(ref count = 1 directory entry + 1 process = 2)

After upgrade (new file installed, ldconfig run)
Filesystem:
libdemo.so.1 → libdemo.so.1.0.2 (symlink updated)
libdemo.so.1.0.2 → inode #5678 (new file)
libdemo.so.1.0.1 → inode #1234 (still on disk!)

Running Process:
mmap still holds inode #1234 (ref count = 1 process)
Process continues working with old code!

New Process:
loads libdemo.so.1 → libdemo.so.1.0.2 → inode #5678

When the old process exits, inode #1234 ref count drops to 0 and the OS frees it (even if the directory entry was deleted). The old file is not truly deleted from disk until all processes holding it close it.
# === Demonstration: old process survives upgrade ===

# Terminal 1: Start a long-running process using the old library
./long_running_server &
SERVER_PID=$!
echo "Server PID: $SERVER_PID"

# Check which library version it loaded
cat /proc/$SERVER_PID/maps | grep libdemo
# 7f8abc000000-7f8abc001000 r-xp ... /usr/lib/libdemo.so.1.0.1

# Terminal 2: Install new version
sudo cp libdemo.so.1.0.2 /usr/lib/
sudo ldconfig -v | grep libdemo
# libdemo.so.1 -> libdemo.so.1.0.2  (changed)

# Check if server is still running — it is!
kill -0 $SERVER_PID && echo "Server still alive!"

# Server still references the OLD inode
cat /proc/$SERVER_PID/maps | grep libdemo
# 7f8abc000000-7f8abc001000 r-xp ... /usr/lib/libdemo.so.1.0.1
# ← STILL using 1.0.1 even though symlink now points to 1.0.2

# Start a NEW server instance
./long_running_server &
NEW_PID=$!

# New server gets the new version
cat /proc/$NEW_PID/maps | grep libdemo
# ... /usr/lib/libdemo.so.1.0.2  ← gets new version!

5. Minor vs Major Upgrade — Summary

Aspect Minor Version Upgrade Major Version Upgrade
Example 1.0.1 → 1.0.2 1.x.y → 2.0.0
Soname changes? No — same soname (libdemo.so.1) Yes — new soname (libdemo.so.2)
Old programs recompile? No — they automatically get the fix Optional — they keep using v1
Keep old .so file? Optional (safe to delete after all processes restart) YES — must keep v1 files for old programs
ldconfig updates symlink? Yes — libdemo.so.1 → new file Yes — creates new libdemo.so.2
Linker name update? Automatic (via soname chain) Manual: ln -sf libdemo.so.2 libdemo.so
Running programs affected? No — live upgrade is transparent No — they continue with old version

Interview Questions & Answers

Q1. Can you upgrade a shared library while programs using it are running? Explain how.
Yes. When a program loads a shared library, the dynamic linker maps the library file’s inode into the process’s address space. The process holds a reference to that inode. If the library file on disk is replaced (or the symlink updated), the running process is not affected — it continues using the old inode already mapped. New programs launched after the upgrade will find the new symlink pointing to the new library file and load the new version. This is why Linux shared library upgrades are zero-downtime for running processes.
Q2. What is the minimum set of steps to perform a minor version upgrade of a shared library?
(1) Compile the updated source with -fPIC.
(2) Link with the same soname as before: gcc -shared -Wl,-soname,libXYZ.so.N -o libXYZ.so.N.M+1 ...
(3) Copy the new .so to the library directory.
(4) Run ldconfig — it automatically updates the soname symlink.
That’s it. The linker name symlink does not need manual updating because it points to the soname, which ldconfig just updated.
Q3. After a minor version upgrade, do you need to restart running programs to get the bug fix?
Yes, to get the bug fix, a program must be restarted. Running processes have the old library code already mapped in memory — they will not pick up the new code until they exit and are relaunched. The new version is loaded only at program startup. Long-running servers typically need a graceful restart (e.g., systemctl restart service) to benefit from a library bug fix. This is a conscious design trade-off: stability of running processes vs. getting fixes immediately.
Q4. Why does the linker name symlink NOT need to be manually updated after a minor version upgrade?
Because the linker name symlink points to the soname symlink (e.g., libdemo.so → libdemo.so.1), and ldconfig updates the soname symlink to point to the new real file (e.g., libdemo.so.1 → libdemo.so.1.0.2). So the full resolution chain is: libdemo.so → libdemo.so.1 → libdemo.so.1.0.2. The linker name still points to the soname, and the soname now points to the latest minor version. No manual update needed. However, for a major version upgrade, you must manually update the linker name to point to the new soname (e.g., libdemo.so → libdemo.so.2).
Q5. After a major version upgrade, why must you NOT delete the old version’s files?
Programs compiled against the old major version have the old soname (e.g., libdemo.so.1) embedded in their ELF file as a required library. When they run, the dynamic linker looks up this soname. If the old library and its soname symlink are deleted, the old programs will fail to start with “cannot open shared object file” errors. The old files must remain until all programs using them have been recompiled against the new version and redeployed, or until the old programs are no longer needed.
Q6. How do you verify which version of a shared library a running process is using?
Read /proc/<PID>/maps: cat /proc/1234/maps | grep libdemo. This shows memory-mapped regions and includes the full path of each mapped file. You’ll see something like /usr/lib/libdemo.so.1.0.1 — the actual real name of the file currently in use, not the symlink. This tells you exactly which version the running process has loaded.
Q7. What is the role of the dynamic linker (ld-linux.so) in library loading?
The dynamic linker (ld-linux.so.2 or ld-linux-x86-64.so.2) is itself a shared library that is invoked by the kernel when starting a dynamically-linked program. It reads the program’s ELF dynamic section to find which libraries are needed (by soname), looks them up in /etc/ld.so.cache and search paths, loads them into the process’s address space using mmap(), resolves symbol references (relocations), and then transfers control to the program’s main(). All of this happens before any application code runs.
Q8. What command confirms that ldconfig has the new library in its cache after installation?
ldconfig -p | grep libdemo. The -p option prints the current contents of /etc/ld.so.cache. After a successful ldconfig run, you should see an entry like: libdemo.so.1 (libc6,x86-64) => /usr/lib/libdemo.so.1.0.2. This confirms the cache is aware of the new file and the dynamic linker will find it.
Q9. Describe the complete chain of symlinks from a compiler -l flag to the actual .so file on disk.
At compile time: gcc -ldemo → linker searches for libdemo.so → resolves to libdemo.so → libdemo.so.2 (linker name symlink). The linker records the soname libdemo.so.2 (not the full path) in the executable’s ELF dynamic section.

At run time: dynamic linker reads ELF — needs libdemo.so.2 → looks in /etc/ld.so.cache → finds /usr/lib/libdemo.so.2.0.0 → mmaps the real file into process memory. The soname symlink (libdemo.so.2 → libdemo.so.2.0.0) is just for directory browsing; the cache stores the real path directly.

Q10. What is the advantage of shared libraries over static libraries for embedded systems and drivers?
Memory savings: Multiple processes share one in-memory copy of the library code (read-only pages are shared via the MMU). On a system with 10 processes using the same library, static linking would replicate the code 10 times in RAM; shared libraries use it once.
Live patching: Security fixes can be deployed by replacing the .so file and restarting services — without rebuilding every binary.
Modular driver architecture: Drivers and protocol stacks (like BlueZ for BLE) expose a shared library interface. Applications link against it and get automatic improvements when the library is updated.
Reduced flash footprint: In embedded Linux systems with multiple applications, shared libraries save significant flash storage compared to statically linked equivalents.

Series Complete! 🎉

You have covered ldconfig, ABI compatibility rules, and live library upgrades — the full picture of Linux shared library management.

← Back to Chapter Index Review Part 2

Leave a Reply

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