新闻详情

新闻详情

首页 / 资讯中心 / 详情

动态链接库真的不占内存?多进程内存共享与PSS排查实战

发布时间:2026/9/30 7:43:57来源:尧图网络
动态链接库真的不占内存?多进程内存共享与PSS排查实战
做容器或者做服务的同学可能都听过这句话“动态链接库是所有进程共享的根本不占内存。”面试讲起来很顺但真到了线上你起了一百个 worker再敲free -g心里就开始打鼓了明明说好共享可用内存还是一路往下掉到底是谁在撒谎这个疑问我专门花过几个晚上实测。结论是那句话没错但说得不完整。动态库确实把同一份代码物理页共享给多个进程可它省下的只是“物理页框”进程数增加时页表、映射、私有数据段、写时复制这些开销一样不少。这篇文章本来是性能系列里塞不下的一块附录我把它单独成文先把 ELF 的加载机制讲清楚再带你做一次多进程实验最后给一套排查多进程内存占用的方法。1. 先给结论动态库省下的是“物理页”不是免死金牌1.1 一句话版本读完这一节你可以记住这句话动态库让 N 个进程的虚拟地址空间指向同一份物理页所以代码段的“重复占用”被抹掉了但每个进程依然需要自己的页表、自己的栈、自己的堆、自己的匿名映射以及库中少量可写段的私有拷贝。内存优化不能只盯动态库但也不能完全忽略它。用生活类比图书馆买一本书放在公共区域一万个人借阅不需要给每个人买一本这是共享的物理资源。但每个人去图书馆要占一个座位、要办一张读者证座位和证是私有的。进程的栈、堆、匿名页就是座位和证页表就是读者证列表。进程多了座位总数当然上去。可实际环境里很多人把 RSS 直接相加然后指着数字说“动态库吃掉了 2G 内存”。这是最典型的一笔糊涂账RSS 是站在单个进程视角统计的其中包含了共享物理页。如果 100 个进程都映射同一个 libc你把这 100 个 RSS 加起来等于把同一页物理内存数了 100 遍。需要换用 PSS 概念那才是对物理占用的近似分摊。1.2 关于共享的四个常见误区这里有一个我见过很多次的误区列表建议先存一下误区真相只要用动态库内存就不随进程数增长代码段物理页不增长但页表、栈、堆、私有数据都增长单个进程 RSS 高说明它独占了很多内存RSS 含共享页要结合 Shared/Private 字段看把 100 个进程 RSS 相加就是总占用大概率高估等于把共享物理页重复计数ASLR地址随机化会让动态库共享失效虚拟地址不同不影响物理页共享页表映射不同而已这些误区后面都会逐个拆开。尤其是最后一个很多做安全加固的同学会问既然地址随机化了库文件映射到不同虚拟地址那还能共享同一物理页吗答案是能页表就是干这个的它把每个进程的虚拟地址翻译到同一个物理页框。地址随机化改的是虚拟地址不是物理页映射结果。2. ELF 加载的共享机制谁在共享谁在私有2.1 动态库在内存里的“四块地”理解共享先看一个 .so 文件的内部结构。用 readelf 把 libm 拆开readelf -W -l /lib/x86_64-linux-gnu/libm.so.6 | grep LOAD你会看到几个 PT_LOAD 段每个段有权限位。概括起来动态库在进程里通常分成这样四块区域典型节权限是否多进程共享物理页是否写时复制代码段.textr-x是否只读数据段.rodata、.gcc_except_tabler--是否可写数据段.data、.gotrw-初始共享第一次写后变私有是BSS/TLS 段.bss、.tdata、.tbssrw-否每个进程有私有副本是为什么代码段和只读数据段能共享因为它们在运行期不可写内核用只读私有映射把文件页映射进地址空间后任何进程都不能通过这个映射改写物理页所以物理帧可以安全地被很多人共享。这也是 Linux 内核中“只读私有映射实际上是共享页缓存”的道理。可写数据段就惨一点。Linux 对 MAP_PRIVATE 的可写映射采用写时复制初始时大家都指向文件页谁写了内核就给它单独复制一页改造成私有页。动态库的 .data 通常不大比如 glibc 的这个段可能几十 KB但架不住 GOT、TLS 这些跟着进程走的东西。2.2 mmap 与 page cache同一个文件页为何能出现在很多进程里动态库不是“加载”进去的严格说是 ld.so 用 mmap 把文件映射到进程地址空间。文件内容会以 4KB 页为单位进入 page cache。第一次映射时磁盘页被读进 page cache后续任何进程映射同一个文件、同一个页内核直接把这个物理页填进新进程的页表不再读盘。关键在于 page cache 的 key 是(文件 inode, 页偏移)。只要多个进程映射的是同一个 inode 的同一个页物理页就同一份。这也解释了为什么容器场景容易出问题如果每个容器镜像里都有/lib/x86_64-linux-gnu/libc.so.6但文件名相同、inode 不同page cache 就认为是两个文件物理页不合并。你看着一百个容器都“占”了 libc 的几百 KB实际是同一个算法被执行了一百份。2.3 别忘了页表共享物理页但映射关系不共享这块太容易被忽略了。物理页虽然只存一份但每个进程都要在自己的页表里维护一条指向它的映射条目。64 位 Linux 下虚拟地址空间按 4KB 页切分一个 PTE 8 字节还有 PMD、PUD、PGD 各级目录项。单看每个进程页表本身可能就占几 MB 内核内存。你可以直接看系统级数字grep -E PageTables /proc/meminfo如果宿主机上有几万个进程这个 PageTables 数字往往比你预期的要大。它不属于任何进程的 RSS你用 ps 看不到只能从 MemTotal 里消失。进程越多这部分“不可共享的内核账本”越大这就是为什么哪怕动态库把物理页共享到极致也做不到“起一万个进程等于一个进程”。2.4 用 PSS 量化真实占用讲完机制需要一个能用的统计口径。Linux 提供三个口径RSS进程驻留物理内存含共享页单进程视角。USS只属于该进程的页不含任何共享页。PSS共享页按共享进程数平均分摊到每个进程PSS 求和接近真实物理占用。举一个简单例子一个 4KB 物理页被 4 个进程共享每个进程的 smaps 里都有 4KB RSS但每个进程的 PSS 只算 1KB。因此排查多进程内存占用时优先看 PSS 而不是 RSS。工具方面smem 或者手工解析 smaps 都能拿到这三个口径后面第 5 节会给命令。3. 实测让 50 个进程同时加载同一个动态库3.1 实验设计理论说完了做一次能复现的小实验。我写了一个极其简单的 C 程序目的是把 libm 真正用起来让它的代码段页面被调入物理内存而不是停在文件缓存里#include math.h #include stdio.h #include unistd.h int main(void) { char stack_buf[4096]; for (int i 0; i 200000; i) { double v sqrt((double)i) log((double)(i 1)); if (v 0) printf(%s, stack_buf); } pause(); return 0; }编译链接 libm然后开 50 个实例gcc -O2 -o mem_probe mem_probe.c -lm for i in $(seq 1 50); do ./mem_probe donepause() 让进程保持运行sqrt/log 确保 libm 的代码段真的被访问。注意如果程序里只是链接了 libm 但没调用它ld 可能用 --as-needed 直接把 libm 从依赖里摘掉。3.2 单进程视角pmap 能看到什么随便挑一个进程 ID直接看映射pmap -X 12345 | grep -E libm|libc|total我这边测下来libm 的代码段 RSS 大约是几百 KB 到 1 MB 的量级libc 大约 1 MB 出头具体数字跟 glibc 版本、是否触发错误路径有关。注意这里的 RSS 是“这个进程调用了 libm 之后驻留在物理内存的库代码页”。50 个进程同样调用pmap 在 50 个进程里都显示同样大小的 RSS。但你 snapshot 物理内存会发现 libm 的代码页并没有翻 50 倍还是那几百 KB 的物理页被反复映射。pmap 单进程无法告诉你这一点因为它不知道这个 page 还被谁映射。3.3 多进程视角RSS 与 PSS 的账面差异为了直观我用脚本把 50 个进程的/proc/pid/smaps汇总重点统计 libm、libc 的 Shared_Clean / Private_Dirty 字段再算 PSS。结果大致长这样场景单进程 RSS约50 进程 RSS 直接相加50 进程 PSS 总账约libc 主映射1.1 MB55 MB1.2 MBlibm 主映射0.4 MB20 MB0.45 MB每个进程私有栈/堆/匿名0.3 MB15 MB15 MB页表等内核开销不体现在 RSS0约 3-5 MB我故意没写精确到 KB 的绝对值因为不同发行版差异太大但比例是稳定的加起来的数据里RSS 直接相加会把共享库的物理页“数 50 遍”PSS 总账才是接近真实物理内存的数。如果你在线上看到“100 个进程 RSS 加起来 8Gavailable 掉了 8G”就觉得必须限制进程数建议先按 PSS 重新算一遍账结论可能会完全不同。3.4 什么增长是预期什么才值得警惕这次实验真正值得警惕的不是 libc/libm 的代码段而是每进程私有部分。继续看数据栈、线程栈、堆、匿名 mmap这部分每进程一份进程数线性增长无法共享。GOT/PLT 和 .data每进程会写时复制少量页加起来通常只有几十到几百 KB。页表和内核 task_struct、文件描述符表每进程都有虽小但堆多了可观。一句话如果你优化完发现大头在私有匿名内存问题不在动态库如果大头是某个 .so 的 Private_Dirty 异常高才需要怀疑动态库本身。4. 共享失效的几种情况这些坑才是内存暴涨的元凶4.1 可写数据段和 TLS每个进程都有自己的私有副本先说一个反直觉的事实即使多个进程映射同一个 .so它的 .data 也不一定是真正共享的。glibc 的 .data 段里有 errno 的访问机制、locale 缓存、IO 缓冲状态等这些运行期会写一写就触发写时复制每进程拿到自己的一份物理页。还有 TLS线程本地存储像 errno 这种指标说到底是线程局部变量每个线程都有独立副本。多线程程序里这份内存还会随着线程数增长动态库对此没有责任。排查时你看到某个 .so 的 Private_Dirty 几百 KB别慌先判断是 GOT、数据段还是 TLS后者基本是正常的。4.2 延迟绑定的 GOT/PLT几 KB 的私有页但影响判断Linux 动态链接默认 lazy binding函数第一次被调用才去解析地址并写入 GOT。GOT 页原本是可写段页第一次写入就变成私有的。你可能会在 smaps 里看到 libc 的某个映射页出现 Private_Dirty相当一部分就是 GOT 造成的。可以用环境变量强制立即绑定LD_BIND_NOW1 ./mem_probe或者链接时加-Wl,-z,now。效果是启动阶段一次性解析完之后 GOT 会被 mprotect 成只读。从纯内存角度看强制绑定不一定会显著减少私有页但会让内存状态确定化对排查更友好。我在生产上见过有人用 LD_BIND_NOW 修“奇怪偶发内存上涨”未必真的是内存回归但行为确实变稳了。4.3 非 PIC 库与 TEXTREL共享直接失效这是我自己第一次踩到的大坑。某个同事自己编译了一个闭源 .so没加 -fPIC结果代码段里有文本重定位。这种库加载时动态链接器没办法让多个进程共享同一份只读代码因为代码在运行期必须被改写每个进程都得持有一份私有副本。症状就是进程数翻倍这个 .so 的内存线性翻倍RSS 涨得肉眼可见。诊断方法readelf -d libfoo.so | grep -i textrel如果输出里有 TEXTREL 字段说明这个库带文本重定位共享能力已经打了折扣。现代发行版工具链默认 PIC正规库一般不会但自己编译的、第三方拿来的旧库很容易出事。遇到这种库要么重新编译加 -fPIC要么和供应商确认能不能换版本。4.4 同名同版本、不同路径的库容器场景最典型的坑前面提过 page cache 以 inode 为 key这里展开说。宿主机上跑 50 个容器每个容器都带一套/lib/x86_64-linux-gnu/libc.so.6大小一模一样、版本一模一样但在宿主看是 50 个不同 inode 的文件。内核不会因为内容相同就自动合并物理页于是同一份 glibc 代码被缓存 50 份。这也解释了为什么容器化之后内存账本变得特别难看。常规解法是统一基础镜像、统一 glibc 版本或者尽量用 distroless 镜像把依赖收敛哪怕做不到统一也要知道这个重复占用其实是可控的“文件页缓存”可以被回收但同时也意味着换页时 I/O 压力更大。4.5 dlopen 同一路径同一进程内的一份映射还有一种情况经常被误判程序用 dlopen 反复加载同一个 .so。只要路径相同、inode 相同dlopen 在进程内部只会增加引用计数不会产生新的映射但如果你把文件复制到新路径再 dlopen或者用某种方式在内存里生成新文件那就是新的 inode物理页自然不能共享。这条对插件系统特别重要插件数量多、每个插件路径不同、代码又高度相似累积起来相当可观。5. 排查多进程内存占用的实操路线5.1 先看系统账本遇到“很多进程、内存占用高”的告警我建议按这个顺序看而不是直接 topfree -h grep -E PageTables|SReclaimable|Cached /proc/meminfo目的有三个确认 Available 到底还剩多少PageTables 是不是异常高判断是不是进程数过多的元凶Cached 高能不能回收。很多“内存不足”实际上是文件页缓存可以回收只是回收需要时间别上来就杀进程。5.2 按 PSS 找大头单机 pid 数量大时用 ps 的 RSS 排序会把大共享库误判成大内存。可以装 smemsudo smem -tk -p -s pss | head -40smem 会列出每进程 USS、PSS、RSS还能在末尾汇总。你看到 PSS 排名前几的才是真正物理内存占用高的进程。如果嫌装包麻烦也可以写几行 Python 解析/proc/*/smaps里的 Pss 字段效果一样。5.3 从 smaps 里定位某个共享库到底共享了多少想确认某个库是不是真共享不要只看单个进程要看全部进程对该文件的统计grep -A8 libm-2.31.so /proc/[0-9]*/smaps | \ grep -E Shared_Clean|Private_Clean|Private_Dirty|Pss: | \ awk {print $1, $2} | sort | uniq -c | sort -nr命令有点粗糙但能告诉你这个库的页在多少进程里被映射每类统计加在一起是多少。重点看 Private_Dirty如果它占了很大比例说明这个库在大量进程里都有私有写入页要么是 GOT/TLS要么是库本身有大量可变全局状态。5.4 优化清单哪些有效哪些白忙最后给一份我实践下来觉得比较实在的清单统一库版本和路径容器场景收益最明显能减少 page cache 重复分布。裁剪依赖用ldd看看你的二进制到底拖了哪些库能用-Wl,--as-needed去掉的就去掉。库文件本身也是内存少一个库少一批页。动态库瘦身strip 掉符号表、用-fvisibilityhidden减少符号导出主要减少磁盘和部分虚拟内存物理驻留收益有限但没坏处。不要静态链接把 glibc 静态链进二进制会让每个进程独享一份 libc 代码比动态库的内存模型差得多。LD_BIND_NOW 按需开启别把它当成万能内存优化更多是消除不确定性。-fPIC 是硬要求自己编 .so 忘了 PIC等于亲手杀掉共享能力。关注私有匿名内存如果堆、栈、对象缓存才是大头那和动态库没有关系别一开始就怀疑 .so。关于“要不要为了省内存去重开一个共享库缓存服务”我的看法是Linux 内核页缓存本来就在做这件事应用层不用再包装一遍。除非你真的碰到大量不同 inode 的重复库文件否则先统一基础镜像比写缓存服务实在得多。写到最后说说个人体会。这个附录是我处理容器内存告警时补的作业。那晚我盯着 free 的输出想了很久后来把总账从“共享库太占内存”修正成“进程私有匿名内存和页表是主犯动态库只是背锅”之后对内存优化的判断就清楚多了。以后再遇到“动态库让内存翻倍”的说法你至少可以先问一句你测的是 RSS 相加还是 PSS 总账这一句基本就能过滤掉一大半拍脑袋的结论。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

AI工程从零构建:四层契约驱动的生产级落地方法论 2026/9/30 8:33:39

AI工程从零构建:四层契约驱动的生产级落地方法论

1. 这不是“搭个LLM API”——AI工程从零开始的真实成本清单 很多人看到“AI Engineering from Scratch”第一反应是:不就是调个OpenAI API,再套个Streamlit前端?我去年带过三个团队落地生产级AI应用,从零启动的项目里&#xff0c…

阅读更多 →
接口测试报告撰写指南:从范围界定到结论输出 2026/9/30 8:33:39

接口测试报告撰写指南:从范围界定到结论输出

那次版本上线后,客户反馈订单刷新总是转圈,前端日志里全是解析异常,最后只能紧急回滚。复盘会上,领导拿着上线评审记录问测试同事:“接口测试报告里不是写了全部通过吗?”翻出那份报告,大家都沉…

阅读更多 →
制药智能工厂落地指南:从ISA-95架构到MES集成与CSV验证 2026/9/30 8:33:39

制药智能工厂落地指南:从ISA-95架构到MES集成与CSV验证

简介:这份《大型制药集团智能工厂建设整体解决方案》是一套面向制药企业智能制造规划与建设人员的完整参考材料,重点回应GMP合规要求下如何构建从设备、生产到管理的智能化体系。方案围绕智能工厂的建设目标、一体化应用架构和关键技术展开,覆…

阅读更多 →
基于Java的民宿管理系统:Spring Boot订单状态机与库存扣减实战 2026/9/30 8:33:38

基于Java的民宿管理系统:Spring Boot订单状态机与库存扣减实战

简介:一份基于Java的民宿管理系统毕业设计论文文档,面向计算机相关专业学生、毕业设计选题者及民宿信息化开发人员。文档以SSM框架、Java语言和MySQL数据库为核心技术栈,围绕民宿基本信息管理、预订管理、客户关系管理、财务管理等模块展开设…

阅读更多 →
从零构建AI工程体系:可审计、可替换、可演进的生产级AI流水线 2026/9/30 8:33:38

从零构建AI工程体系:可审计、可替换、可演进的生产级AI流水线

1. 什么是“从零构建AI工程体系”——不是搭模型,而是建生产线 “AI Engineering from Scratch”这个标题乍看像在教人手写反向传播,其实完全不是。它指的是一整套把AI能力真正变成可交付、可维护、可扩展的生产级系统的完整方法论。我带过7个AI落地项目…

阅读更多 →
# MOE 肽类药物设计(十三):Docking Pose 太多怎么筛?用 PLIF 不止看“分数”还要“相互作用模式” 2026/9/30 8:33:32

# MOE 肽类药物设计(十三):Docking Pose 太多怎么筛?用 PLIF 不止看“分数”还要“相互作用模式”

上一篇中,我们先通过 Conformational Search 建立 stapled peptide 构象库,再将这些构象输入 General Dock 与 Mdmx 进行对接。Docking 完成后,我们会得到一批按照最终评分 S 排序的 Poses。最直接的做法当然是:选择 Score 最好的…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉