Linux Kernel Shared Memory 剖析

本文介绍Linux 2.6.32内核引入的Kernel Shared Memory (KSM)机制,该机制通过合并虚拟机间相同的内存页面来提高内存利用效率。文章详细探讨了KSM的工作原理、配置及监控方法。

看到这篇文章的时候,感觉只有两个字:郁闷。还在Linux-2.6.29的时候,就看是看Linux kernel virtual machine,那时候就感觉多个VM肯定有很多的内存是共享的,完全可以合并,没想到Linux的发展是这么的快,linux-2.6.32就正式的支持了ksm机制,实现了共享内存的合并,而且还是专门针对虚拟机的,真是让我抓狂,都是可恶的项目,全是在浪费时间。下面转载ksm相关的一篇文章,学习之余做个记录,已备毕业设计只需。

 

简介: 作为一个系统管理程序(hypervisor),Linux® 有几个创新,2.6.32 内核中一个有趣的变化是 Kernel Shared Memory (KSM)。KSM 允许这个系统管理程序通过合并内存页面来增加并发虚拟机的数量。本文探索 KSM 背后的理念(比如存储去耦合)、KSM 的实现、以及如何管理 KSM。

服务器虚拟化

虚拟化技术从上世纪 60 年代开始出现,经由 IBM® System/360® 大型机得以流行。50 年过后,虚拟化技术取得了跨越式发展,使得多个操作系统和应用程序共享一个服务器成为可能。这一特殊用途(称为服务器虚拟化)正在演变为数据中心,因为单个物理机能够用于托管 10 个(一般情况)或更多虚拟机(VM),如图 1 所示。这种虚拟化使基础设施更动态、更省电、(因而也)更经济。

 

页面都是相同的。假如操作系统和应用程序代码以及常量数据在 VMs 之间相同,那么这个特点就很有用。当页面惟一时,它们可以被合并,从而释放内存,供其他应用程序使用。图 2 演示了内存共享,并展示了在内容相同的 VMs 之间共享页面时更多可用闲置内存的好处。
图 2. 跨 VMs 的内存共享
 

特性命名

本文描述的特性非常新;因此,其名称经历了一些变化。您将发现这个 Linux 内核特性称为 Kernel Shared MemoryKernel Samepage Merging

您很快就会发现,尽管 Linux 中的内存共享在虚拟环境中有优势(KSM 最初设计用于基于内核的虚拟机),但它在非虚拟环境中仍然有用。事实上,KSM 甚至在嵌入式 Linux 系统中也有用处,表明了这种方法的灵活性。下面,我们将探索这种 Linux 内存共享方法,以及如何使用该方法提高服务器的内存密度,从而增加其托管其他应用程序或 VMs 的能力。

其他技术支持

存储技术中的一个称为去耦合(de-duplication)的最新进展是 Linux 和其他系统管理程序中的内存共享的先驱。去耦合这种技术通过删除冗余数据(基于数据块,或者基于更大的数据片段,比如文件)来减少已存储的数据。公共数据片段被合并(以一种 copy-on-write [CoW] 方式),释放空间供其他用途。使用这种方法,存储成本更低,最终需要的存储器也更少。鉴于当前的数据增长速度,这个功能显得非常重要。

KSM 操作

KSM 作为内核中的守护进程(称为 ksmd)存在,它定期执行页面扫描,识别副本页面并合并副本,释放这些页面以供它用。KSM 执行上述操作的过程对用户透明。例如,副本页面被合并(然后被标记为只读),但是,如果这个页面的其中一个用户由于某种原因更改该页面,该用户将(以 CoW 方式)收到自己的副本。可以在内核源代码 ./mm/ksm.c 中找到 KSM 内核模块的完整实现。

KSM 依赖高级应用程序来提供指导,根据该指导确定合并的候选内存区域。尽管 KSM 可以只扫描系统中的匿名页面,但这将浪费 CPU 和内存资源(考虑到管理页面合并进程所需的空间)。因此,应用程序可以注册可能包含副本页面的虚拟区域。

KSM 应用程序编程接口(API)通过 madvise 系统调用(见清单 1)和一个新的建议参数(advice parameter)MADV_MERGEABLE(表明已定义的区域可以合并)来实现。可以通过 MADV_UNMERGEABLE 参数(立即从一个区域取消合并任何已合并页面)从可合并状态删除一个区域。注意,通过 madvise 来删除一个页面区域可能会导致一个 EAGAIN 错误,因为该操作可能会在取消合并过程中耗尽内存,从而可能会导致更大的麻烦(内存不足情况)。


清单 1. madvise 系统调用

				
#include <sys/mman.h>

int madvise( void *start, size_t length, int advice );

一旦某个区域被定义为 “可合并”,KSM 将把该区域添加到它的工作内存列表。启用 KSM 时,它将搜索相同的页面,以写保护的 CoW 方式保留一个页面,释放另一个页面以供它用。

KSM 使用的方法与内存去耦合中使用的方法不同。在传统的去耦合中,对象被散列化,然后使用散列值进行初始相似性检查。当散列值一致时,下一步是进行一个实际对象比较(本例中是一个内存比较),以便正式确定这些对象是否一致。KSM 在它的第一个实现中采用这种方法,但后来开发了一种更直观的方法来简化它。

在当前的 KSM 中,页面通过两个 “红-黑” 树管理,其中一个 “红-黑” 树是临时的。第一个树称为不稳定树,用于存储还不能理解为稳定的新页面。换句话说,作为合并候选对象的页面(在一段时间内没有变化)存储在这个不稳定树中。不稳定树中的页面不是写保护的。第二个树称为稳定树,存储那些已经发现是稳定的且通过 KSM 合并的页面。为确定一个页面是否是稳定页面,KSM 使用了一个简单的 32 位校验和(checksum)。当一个页面被扫描时,它的校验和被计算且与该页面存储在一起。在一次后续扫描中,如果新计算的校验和不等于此前计算的校验和,则该页面正在更改,因此不是一个合格的合并候选对象。

使用 KSM 进程处理一个单一的页面时,第一步是检查是否能够在稳定树中发现该页面。搜索稳定树的过程很有趣,因为每个页面都被视为一个非常大的数字(页面的内容)。一个 memcmp(内存比较)操作将在该页面和相关节点的页面上执行。如果 memcmp 返回 0,则页面相同,发现一个匹配值。反之,如果 memcmp 返回 -1,则表示候选页面小于当前节点的页面;如果返回 1,则表示候选页面大于当前节点的页面。尽管比较 4KB 的页面似乎是相当重量级的比较,但是在多数情况下,一旦发现一个差异,memcmp 将提前结束。请参见图 3 查看这个过程的视觉呈现。


图 3. 搜索树中的页面的搜索过程


 

如果候选页面位于稳定树中,则该页面被合并,候选页面被释放。有关代码位于 ksm.c/stable_tree_search()(称为 ksm.c/cmp_and_merge_page())中。反之,如果没有发现候选页面,则应转到不稳定树(参见 ksm.c/unstable_tree_search())。

在不稳定树中搜索时,第一步是重新计算页面上的校验和。如果该值与原始校验和不同,则本次扫描的后续搜索将抛弃这个页面(因为它更改了,不值得跟踪)。如果校验和没有更改,则会搜索不稳定树以寻找候选页面。不稳定树的处理与稳定树的处理有一些不同。第一,如果搜索代码没有在不稳定树中发现页面,则在不稳定树中为该页面添加一个新节点。但是如果在不稳定树中发现了页面,则合并该页面,然后将该节点迁移到稳定树中。

当扫描完成(通过 ksm.c/ksm_do_scan() 执行)时,稳定树被保存下来,但不稳定树则被删除并在下一次扫描时重新构建。这个过程大大简化了工作,因为不稳定树的组织方式可以根据页面的变化而变化(还记得不稳定树中的页面不是写保护的吗?)。由于稳定树中的所有页面都是写保护的,因此当一个页面试图被写入时将生成一个页面故障,从而允许 CoW 进程为写入程序取消页面合并(请参见 ksm.c/break_cow())。稳定树中的孤立页面将在稍后被删除(除非该页面的两个或更多用户存在,表明该页面还在被共享)。

如前所述,KSM 使用 “红-黑” 树来管理页面,以支持快速查询。实际上,Linux 包含了一些 “红-黑” 树作为一个可重用的数据结构,可以广泛使用它们。“红-黑” 树还可以被 Completely Fair Scheduler (CFS) 使用,以便按时间顺序存储任务。您可以在 ./lib/rbtree.c 中找到 “红-黑” 树的这个实现。

KSM 配置和监控

KSM 的管理和监控通过 sysfs(位于根 /sys/kernel/mm/ksm)执行。在这个 sysfs 子目录中,您将发现一些文件,有些用于控制,其他的用于监控。

第一个文件 run 用于启用和禁用 KSM 的页面合并。默认情况下,KSM 被禁用(0),但可以通过将一个 1 写入这个文件来启用 KSM 守护进程(例如,echo 1 > sys/kernel/mm/ksm/run)。通过写入一个 0,可以从运行状态禁用这个守护进程(但是保留合并页面的当前集合)。另外,通过写入一个 2,可以从运行状态(1)停止 KSM 并请求取消合并所有合并页面。

KSM 运行时,可以通过 3 个参数(sysfs 中的文件)来控制它。sleep_millisecs 文件定义执行另一次页面扫描前 ksmd 休眠的毫秒数。max_kernel_pages 文件定义 ksmd 可以使用的最大页面数(默认值是可用内存的 25%,但可以写入一个 0 来指定为无限)。最后,pages_to_scan 文件定义一次给定扫描中可以扫描的页面数。任何用户都可以查看这些文件,但是用户必须拥有根权限才能修改它们。

还有 5 个通过 sysfs 导出的可监控文件(均为只读),它们表明 ksmd 的运行情况和效果。full_scans 文件表明已经执行的全区域扫描的次数。剩下的 4 个文件表明 KSM 的页面级统计数据:

  • pages_shared:KSM 正在使用的不可交换的内核页面的数量。
  • pages_sharing:一个内存存储指示。
  • pages_unshared:为合并而重复检查的惟一页面的数量。
  • pages_volatile:频繁改变的页面的数量。

KSM 作者定义:较高的 pages_sharing/pages_shared 比率表明高效的页面共享(反之则表明资源浪费)。

结束语

Linux 并不是使用页面共享来改进内存效率的惟一系统管理程序,但是它的独特之处在于将其实现为一个操作系统特性。VMware 的 ESX 服务器系统管理程序将这个特性命名为 Transparent Page Sharing (TPS),而 XEN 将其称为 Memory CoW。不管采用哪种名称和实现,这个特性都提供了更好的内存利用率,从而允许操作系统(KVM 的系统管理程序)过量使用内存,支持更多的应用程序或 VM。 您可以在最新的 2.6.32 Linux 内核中发现 KSM — 以及其他很多有趣的特性。

<think>我们正在分析"Shared memory buffer overrun! Stalling"错误。该错误通常发生在使用共享内存进行进程间通信(IPC)时,写入速度超过读取速度,导致缓冲区溢出。 ### 错误定义 - **共享内存缓冲区溢出(Shared Memory Buffer Overrun)**:生产者进程向共享内存写入数据的速度持续高于消费者进程读取数据的速度,导致缓冲区被填满,后续数据无法写入。 - **停滞(Stalling)**:系统为避免数据丢失而采取的自我保护措施——暂停生产者写入操作,等待缓冲区空间释放。 ### 根本原因分析 根据引用[2]对缓冲区下溢的定义,我们可以类比得出缓冲区溢出的核心矛盾:**生产与消费速率不匹配**。具体原因包括: | 原因类别 | 典型场景 | 后果 | |-------------------------|--------------------------------------------------------------------------|-------------------------------| | **消费者性能瓶颈** | 消费者进程因CPU/IO阻塞处理变慢 | 数据堆积在缓冲区 | | **生产者过量写入** | 突发流量超过设计容量 | 缓冲区快速饱 | | **缓冲区配置过小** | 共享内存区域分配空间不足 | 频繁触发溢出保护 | | **同步机制失效** | 锁竞争激烈或通知机制延迟 | 消费者未能及时唤醒 | ### 诊断步骤 1. **监控缓冲区使用率** 使用 `ipcs -mu` 检查共享内存段的使用情况: ```bash # 示例输出关键字段 segments allocated = 45 pages allocated = 16384 # 总页数 pages resident = 16000 # 实际使用页数 pages percent = 97% # 使用率 ``` 当`percent`持续>95%时风险极高。 2. **分析进程状态** 通过 `top` 或 `pidstat` 检查消费者进程状态: ```bash pidstat -p <consumer_pid> 1 # 每秒采样消费者资源使用 ``` 关注: - **%wait**:等待IO的CPU时间占比(高则可能IO瓶颈) - **%sys**:内核态CPU时间(高则系统调用过多) 3. **追踪系统调用** 使用 `strace` 观察消费者阻塞点: ```bash strace -p <consumer_pid> -T -e poll,select,read ``` 若频繁出现`read(..., <未就绪>) = ?`且耗时>10ms,表明可能在等待数据源。 ### 解决方案 #### 应急措施:缓解当前阻塞 ```c // 生产者端增加超时与重试逻辑 if (write(shm_fd, data, data_len) == -1) { if (errno == EAGAIN) { // 缓冲区满 usleep(1000); // 等待1ms后重试 retry_count++; } } ``` #### 长期优化 1. **动态调整缓冲区** 实现弹性共享内存池,根据负载自动扩容: ```c size_t calculate_new_size(size_t current) { return current * 1.5; // 按1.5倍增长 } ``` 2. **消费者性能提升** - **批处理**:单次读取多个数据块减少系统调用 ```python # 消费者伪代码 while True: chunks = read_up_to(1024) # 单次最多读1024块 process_batch(chunks) ``` - **异步IO**:使用 `epoll` 或 `io_uring` 提升处理效率 3. **流量控制(Backpressure)** 生产者根据缓冲区状态调节写入速率: ```mermaid graph LR A[生产者] -->|写入数据| B[共享内存] B --> C{使用率>80%?} C -->|是| D[降低生产者速率] C -->|否| E[正常速率] ``` 4. **配置调优** 增加系统级共享内存上限(需root权限): ```bash sysctl -w kernel.shmmax=4294967296 # 4GB sysctl -w kernel.shmall=1048576 # 总页数 ``` ### 预防机制 - **监控报警**:实时检测共享内存使用率,超过阈值触发告警 - **压力测试**:模拟峰值流量验证缓冲区设计容量 - **优雅降级**:在持续过载时丢弃非关键数据 > **关键结论**:该错误本质是生产者-消费者模型中的**资源协调失效**。通过速率控制、弹性缓冲消费端优化可系统性解决[^2][^3]。 --- ### § 相关问题 1. 如何设计一个支持动态扩容的共享内存环形缓冲区? 2. Linux 中有哪些高效的多进程同步机制? 3. 如何利用 Perfetto 分析共享内存的性能瓶颈? </think> ### Shared Memory Buffer Overrun! Stalling 分析及解决方案 #### 1. **问题本质** "Shared memory buffer overrun! Stalling" 表示**共享内存缓冲区溢出导致的数据流停滞**。核心矛盾是: - **生产者写入速率** > **消费者读取速率** - 当共享内存缓冲区被完全填满时,系统为避免数据丢失会强制暂停生产者进程(Stalling) > 该错误本质是**生产者-消费者模型中的资源协调失效**(引用[2]的缓冲区下溢概念反向场景)。 #### 2. **根本原因分析** | 原因类型 | 典型场景 | 检测指标 | |------------------|--------------------------------------------------------------------------|------------------------------| | **消费者阻塞** | 消费者进程因I/O等待、死锁或CPU过载无法及时读取数据 | 消费者进程`%wait` > 30% | | **生产者过载** | 突发流量或设计缺陷导致写入速率超过缓冲区容量 | 缓冲区填充速率 > 清空速率 | | **缓冲区过小** | 共享内存空间配置不足,无法容纳正常业务波动 | 缓冲区使用率持续 > 90% | | **同步机制失效** | 无锁设计竞争或信号量/互斥锁使用错误,导致消费者唤醒延迟 | `futex`系统调用耗时异常 | #### 3. **诊断步骤** **步骤1:监控缓冲区状态** ```bash # Linux共享内存监控 $ watch -n 1 "ipcs -m | grep -A 1 '共享内存键值'" # 关键指标:nattch(连接进程数), bytes(使用量) ``` **步骤2:分析进程状态** ```bash $ pidstat -p 消费者PID 1 # 检查消费者资源使用 $ strace -p 生产者PID -e write # 跟踪写入阻塞点 ``` **步骤3:检测同步瓶颈** ```bash $ perf record -e synchronize:rcu_* -g -p 消费者PID $ perf report # 查看锁竞争热点 ``` #### 4. **解决方案** **A. 紧急恢复(已发生Stalling时)** ```c // 生产者增加非阻塞写入重试逻辑 if (shm_write(data) == BUFFER_FULL) { usleep(1000); // 暂停1ms避免忙等待 if (retry_count++ > MAX_RETRY) { drop_data(data); // 实施降级策略 } } ``` **B. 架构优化** 1. **动态缓冲区扩容** ```c void resize_buffer(size_t new_size) { shmctl(SHM_RESIZE, new_size); // Linux系统调用 sem_post(&resize_sem); // 通知所有进程 } ``` 2. **消费者性能提升** - 批处理读取:单次读取多个数据块减少上下文切换 - 异步I/O:使用`io_uring`(Linux)或`kqueue`(BSD)减少阻塞 3. **流量控制** ```mermaid graph LR A[生产者] -->|写入请求| B[令牌桶] B -->|令牌可用| C[共享内存] B -->|无令牌| D[等待/丢弃] ``` 4. **配置调优** ```bash # 调整Linux共享内存限制 sysctl -w kernel.shmmax=4294967296 # 4GB缓冲区 sysctl -w kernel.shmall=1048576 # 总页数 ``` #### 5. **预防措施** - **监控预警**:当缓冲区使用率 > 75% 触发告警 - **压力测试**:使用`fio`工具模拟峰值流量 - **优雅降级**:实现数据重要性分级,过载时丢弃低优先级数据 - **无锁环形缓冲区**:采用DPDK/rte_ring等高性能方案(适用实时系统) > **关键原则**:通过**速率控制+弹性缓冲+消费优化**实现流平衡(引用[3]的内存管理理念延伸)[^2][^3]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值