为什么 Linux 要使用写入时复制(COW)?从动态链接库内存共享讲清楚设计原则

在分析 Linux 内存时,很多工程师都会看到类似现象:

  • 一个进程加载了 20MB 的动态库
  • 启动 10 个相同进程
  • 系统内存却没有增加 200MB,而只增加了很少一部分

这是 Linux 内存管理中一个非常核心的机制:写入时复制(Copy-On-Write, COW)

但很多人有几个疑问:

  • 动态库不是每个进程都要用吗?为什么不分别加载?
  • 为什么不是一开始就分开,而是“写的时候才复制”?
  • 动态库哪些内存共享,哪些不共享?
  • 这个机制背后的设计原则是什么?

本文从工程视角,一步一步讲清楚。🧠


一、先理解:进程的虚拟内存结构

一个进程的虚拟内存典型结构:

虚拟内存空间

+------------------+
| code segment     | 代码段(.text)
+------------------+
| rodata segment   | 只读数据段
+------------------+
| data segment     | 已初始化全局变量
+------------------+
| bss segment      | 未初始化全局变量
+------------------+
| heap             | malloc 分配
+------------------+
| mmap region      | 动态库映射
+------------------+
| stack            |
+------------------+

动态库(.so)通过 mmap 加载到 mmap region。


二、动态库加载后,哪些内存是共享的?

假设动态库 libexample.so 大小为 2MB,它的内部结构是:

libexample.so

.text     1.5MB   ← 代码段(只读)
.rodata   0.3MB   ← 常量段(只读)
.data     0.1MB   ← 可写全局变量
.bss      0.1MB   ← 可写全局变量

关键区别:

是否共享 原因
.text ✅ 共享 只读,不会修改
.rodata ✅ 共享 只读
.data ❌ 私有(COW) 可能修改
.bss ❌ 私有(COW) 可能修改

也就是说:

  • 2MB 动态库中,大约 1.8MB 是共享的
  • 只有 0.2MB 可能变成私有

这就是内存节省的来源。


三、关键机制:写入时复制(COW)

Linux 并不会一开始就复制 data/bss,而是:

先共享,写的时候再复制

流程如下:

进程A加载 libexample.so

进程B加载 libexample.so

共享物理页

进程A修改变量

触发 page fault

复制物理页

进程A使用新页

进程B继续使用旧页

关键点:

  • 修改之前:共享同一个物理页
  • 修改之后:复制一个新页

这就是 Copy-On-Write。


四、为什么不一开始就复制?

这是一个非常关键的设计问题。

假设:

动态库:

代码段 1.5MB
数据段 0.5MB

启动 100 个进程。


方案 A:立即复制

每进程占用:

代码段 1.5MB
数据段 0.5MB

总内存:

100 × 2MB = 200MB

方案 B:COW(Linux 实际方案,对比最坏情况与典型情况)

假设只有 10% 页被修改:

共享:

代码段:
1.5MB

数据段初始共享部分(尚未被写入的页):
0.45MB

私有部分(被实际写入后触发 COW 的页):
100 × 0.05MB = 5MB

总内存:

1.95MB + 5MB ≈ 7MB

对比:

方案 内存
立即复制 200MB
COW 7MB

节省:

97%

这是巨大的差异。🚀


五、更重要的原则:延迟分配(Lazy Allocation)

COW 体现了 Linux 核心设计哲学:

不要为“可能发生”的事情付出成本,只为“已经发生”的事情付出成本

即:

不是:

可能修改 → 立即复制

而是:

真正修改 → 才复制

这是典型的 Lazy 策略。

广泛存在于:

  • fork()
  • mmap()
  • 动态库加载
  • 匿名内存

六、fork() 是 COW 的经典应用

fork 时:

pid = fork();

不会复制整个进程内存。

而是:

parent process

child process

共享所有物理页

parent write

child write

复制页

这使 fork 非常快。

否则 fork 会非常慢。


七、动态库如何影响多个进程内存占用

假设:

libc.so   3MB
libstdc++ 2MB
libQt     20MB

总:

25MB

启动 50 个进程:

如果没有共享:

25MB × 50 = 1250MB

实际:

≈ 25MB + 少量私有页

这就是为什么 Linux 可以运行大量进程。


八、从物理页角度看共享关系

物理内存

虚拟空间

进程A text

进程B text

物理页 0x1234

多个虚拟页 → 同一个物理页。


九、总结:COW 的本质设计原则

核心思想可以总结为一句话:

先共享,按需复制

背后体现三个关键设计原则:


原则 1:最大化共享

只读数据共享:

  • 动态库代码段
  • 常量段

减少内存占用。


原则 2:延迟成本(Lazy)

不是:

可能修改 → 复制

而是:

真正修改 → 才复制

避免无意义开销。


原则 3:用 page fault 驱动资源分配

复制由 page fault 触发:

write
  ↓
page fault
  ↓
kernel copy page
  ↓
resume execution

完全自动。


十、一句话理解 COW(工程师版本)

可以用一句非常工程化的话总结:

Linux 默认假设:绝大多数内存不会被修改,所以先共享,只有真的写时才复制。

这使得:

  • fork 很快
  • 动态库几乎零成本共享
  • 系统能运行更多进程
  • 内存利用率大幅提升

十一、实践验证

可以用这个命令观察:

pmap -x <pid>

或:

smem -r

查看:

  • shared
  • private
  • USS
  • PSS

你会发现:

动态库大部分是 shared。


十二、最终总结图

动态库加载

只读段

可写段

共享物理页

共享物理页

共享物理页

进程写入

page fault

复制页

私有页


#总结

COW 本质不是优化技巧,而是 Linux 内存管理的基础设计原则:

共享是默认,复制是例外。

这也是 Linux 能高效运行大量进程的根本原因。🚀
在这里插入图片描述

Logo

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

更多推荐