动态链接库的内存是共享的吗?深入理解 .so 如何影响进程内存占用

在分析 Linux 进程内存时,你可能经常会看到这样的现象:

$ pmap -x 1234 | grep libc
00007f... r-xp  1500K  ...  libc.so
00007f... rw-p    32K  ...  libc.so

或者:

$ smem -p 1234
PID   RSS   PSS   USS   Command
1234  20M   8M    5M    app

一个进程明明加载了十几个 .so 文件,RSS 看起来很大,但 PSS 却小很多。

这时一个关键问题就出现了:

动态链接库的内存到底是共享的,还是每个进程独占的?

答案是:

动态库的代码段是共享的,数据段通常是每个进程独占的。

本文将从内存结构、虚拟内存机制和 Copy-on-Write 原理三个层面,彻底解释这个问题。


一、动态库加载后在内存中的真实结构

当一个进程加载 .so 文件时,Linux 会将其映射到进程虚拟地址空间。

结构如下:

进程B

进程A

.text 代码段
只读可执行 r-x
共享

.rodata 常量段
只读 r--
共享

.data 全局变量
可读写 rw-
私有

.bss 未初始化变量
rw-
私有

.text 代码段
共享

.rodata 常量段
共享

.data
私有

.bss
私有

物理内存 text

物理内存 rodata

可以看到:

是否共享 原因
.text 共享 只读,不会修改
.rodata 共享 只读
.data 私有 可写
.bss 私有 可写

核心决定因素只有一个:

是否可写


二、为什么代码段可以共享?

代码段包含机器指令,例如:

printf("hello");

编译后变成:

call printf

这些指令:

  • 不会修改
  • 所有进程执行逻辑相同

因此 Linux 可以安全共享:

进程A

物理代码页

进程B

进程C

优势:

  • 节省物理内存
  • 提高 cache 命中率

这也是动态链接库节省内存的核心原因。


三、为什么全局变量必须私有?

看一个例子:

动态库:

// libexample.so

int counter = 0;

void inc() {
    counter++;
}

如果多个进程共享这块内存:

进程A

counter

进程B

会发生:

进程A: counter++
进程B: counter++

结果:

  • 进程之间互相影响
  • 状态混乱
  • 程序错误

这违反了 Linux 的基本原则:

进程地址空间必须隔离

所以必须私有。


四、Linux 如何实现“初始共享,写时私有”

Linux 使用的是:

Copy-on-Write (COW)

流程如下:

第一步:初始共享

磁盘数据页

进程A

进程B

此时:

物理页共享

节省内存。


第二步:进程写入时复制

当进程A修改变量:

counter++

Linux 执行:

复制物理页

变成:

共享页

进程B

私有页

进程A

进程A得到私有副本。

进程B仍共享原页。

这就是:

写时复制(Copy-on-Write)


五、从 mmap 权限理解本质

Linux 使用 mmap 加载动态库:

代码段:

mmap(..., PROT_READ | PROT_EXEC, MAP_SHARED)

只读共享。

数据段:

mmap(..., PROT_READ | PROT_WRITE, MAP_PRIVATE)

私有映射。

关键区别:

映射类型 是否共享
MAP_SHARED 共享
MAP_PRIVATE 私有(COW)

这就是根本原因。


六、通过 smaps 可以直接验证

查看:

cat /proc/<pid>/smaps

示例:

7f... libc.so

r-xp Shared_Clean: 1500 kB
rw-p Private_Dirty: 32 kB

说明:

代码段:

Shared

数据段:

Private

七、完整生命周期图

进程B

进程A

PageCache

磁盘

写入

libexample.so

text pages

data pages

A_text

A_data

B_text

B_data

私有副本


八、这也是 RSS 和 PSS 差异的根本原因

RSS 包含:

共享代码 + 私有数据

但共享代码实际只占用一份物理内存。

PSS 会按比例分摊:

真实占用 ≈ 私有内存 + 共享内存 / 进程数

例如:

libc.so text = 2MB
10 个进程使用

RSS 每个进程 = 2MB
PSS 每个进程 = 200KB

九、真实系统示例

pmap -x <pid>

示例:

Address    Kbytes  RSS   Dirty  Mode   Mapping

libc.so
r-xp       1500    1500     0    shared
rw-p         32      32    32    private

可以看到:

绝大多数共享,

少量私有。


十、为什么动态链接库可以节省大量内存

假设:

libc.so = 2MB
100 个进程

如果不共享:

200MB

实际:

≈ 2MB + 少量私有数据

节省 99% 内存。


十一、最终总结(工程视角)

动态库内存结构:

私有部分

.data 全局变量
私有

.bss 未初始化变量
私有

共享部分

.text 代码
共享

.rodata 常量
共享

结论:

共享:

  • 代码段
  • 常量段

私有:

  • 全局变量
  • 静态变量

实现机制:

只读段 → 共享映射

可写段 → MAP_PRIVATE + Copy-on-Write

十二、对性能分析的实际意义

这解释了为什么:

不要用 RSS 判断真实内存占用,

而应该用:

smem

或:

/proc/pid/smaps

重点关注:

PSS
USS
Private_Dirty

而不是简单的 RSS。


在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐