无交换嵌入式系统的内存管理方法
Linux 内核内存溢出 (OOM) 杀手通常不会在桌面和服务器计算机上调用,因为这些环境包含足够的常驻内存和交换空间,使得 OOM 情况成为罕见事件。然而,无交换嵌入式系统通常只有很少的主内存且没有交换空间。在这样的系统中,通常不需要分配大的内存空间;然而,即使相对较小的分配也可能最终触发 OOM 杀手。
最终用户桌面应用程序的实验表明,当系统内存不足时——也就是说,即将达到 OOM 条件时——应用程序可能会由于系统缓慢而变得无响应。当物理内存即将达到 OOM 条件或完全占用时,系统性能会受到影响。系统缓慢应予以避免,因为这种行为会给最终用户带来不适。
此外,基于内核的 OOM 杀手使用的进程选择算法是为桌面和服务器计算机的需求而设计的。因此,它可能无法在无交换嵌入式系统上正常工作,因为它可能随时杀死用户可能正在交互的应用程序。
在本文中,我们提出了一种用于无交换嵌入式系统的方法,该方法采用了两种内存管理机制。第一种机制通过拒绝基于预定义的内存消耗阈值的内存分配来防止系统缓慢和 OOM 杀手激活。应仔细确定和校准此类阈值,以便优化内存使用,同时避免可能导致系统延迟和调用 OOM 杀手的大量内存消耗。我们称之为内存分配阈值 (MAT)。
第二种机制采用一个额外的阈值,称为信号阈值 (ST)。当达到此阈值时,内核会发送一个低内存信号 (LMS),用户空间应捕获该信号,从而在超过 MAT 之前触发内存释放。这两个阈值均由内核模块(低内存水位线 (LMW) 模块)实现。我们提供了一些实验结果,这些结果指出了我们的方法在优化一类嵌入式系统的内存消耗方面可能证明有用的情况。
当活动应用程序的内存需求超过系统上可用的物理内存时,系统性能会降低。在这种情况下,感知到的系统响应可能会非常缓慢。在无交换设备上,应用程序内存需求可能会经常将系统推向这种状况,因为系统内部主内存较低,并且应用程序占用整个物理内存的可能性很高。
应在此类设备上以不同的方式管理内存资源,以避免系统响应缓慢。内存分配失败机制可用于防止缓慢。防止系统缓慢使得 OOM 杀手的调用变得罕见。因此,这种机制还可以减少触发 OOM 杀手的机会,OOM 杀手的进程选择算法可能会在内存不足且没有交换空间的设备上选择意外的应用程序来杀死。
内存分配失败意味着拒绝应用程序请求的内存分配。它是根据 MAT 值执行的,该值是根据最终用户应用程序的各种用例实验设置的。MAT 应设置得足够高,以允许应用程序分配必要的内存而不影响整体系统性能,但其值应明确定义,以保证在必要时发生内存分配失败,从而防止极端的内存消耗。
然而,在发生内存分配失败之前,可以执行进程终止以释放已分配的内存。它可以由内核空间向用户空间传输 LMS 来触发,以通知应用程序释放内存。LMS 根据 ST 值分派。如图 1 所示,ST 应小于 MAT,因为 LMS 应在内存分配失败之前发生。
如果 LMS 分派成功并且内存被接收信号释放,则可以防止可能的内存分配失败。一个有用的场景可能涉及运行一些基于窗口的应用程序 A、B 和 C,它们消耗大量内存,而它们的窗口框架可以相互叠加(假设使用简单的窗口管理器环境,如 Matchbox)。假设应用程序 A 是用户在达到 MAT 时刻正在交互的应用程序,那么与其拒绝向 A 分配内存,不如尝试释放应用程序 B 和 C 分配的内存,因为这些应用程序对用户不可见。这样做将允许用户继续使用应用程序 A。
然而,对于某些应用程序用例而言,内存分配失败可能是不可避免的。例如,这种情况可能涉及单个基于窗口的应用程序,该应用程序以恒定的速率消耗内存,并且用户正在与之交互。在这种情况下,从其他应用程序释放内存可能不太理想,因为可能没有其他基于窗口的应用程序可以从中释放内存。因此,更理想的解决方案是使由有问题的应用程序请求的内存分配失败,并将其选为终止的候选者。
在我们的提议中,内核应提供两种机制来处理低内存级别极端情况下的内存管理
brk()、mmap() 和 fork() 系统调用失败:根据先前校准的 MAT 级别,拒绝内存分配请求,以防止系统缓慢和内核 OOM 杀手调用。
低内存信号:由内核发送到用户空间进程终止器的内核事件层信号,该终止器应采用基于指定的 ST 工作的进程选择算法。
使用这些机制,可以识别何时可以释放内存或何时拒绝进一步分配。只有在内存释放尝试无法成功时才应拒绝内存分配。
LMW 是一个基于 Linux 安全模块 (LSM) 框架的内核模块。它实现了一种启发式方法,用于检查物理内存消耗阈值,以便拒绝内存分配并通知用户空间释放内存。可以使用用户空间进程终止器来释放内存。低内存水位线阈值的公式如下
deny_threshold = physical_memory * deny_percentage
notify_threshold = physical_memory * notify_percentage
physical_memory 是系统的主内存,由内核全局变量 totalram_pages 表示。deny_percentage 和 notify_percentage 是可调内核参数,这些参数的值可以通过 sysctl 接口更改。这些参数绑定到 /proc 文件系统,可以使用标准命令(如 echo 和 cat)写入和读取。这些参数可以按如下方式处理
$ echo 110 > /proc/sys/vm/lowmem_deny_watermark $ echo 90 > /proc/sys/vm/lowmem_notify_watermark $ cat /proc/sys/vm/lowmem_deny_watermark 110 $ cat /proc/sys/vm/lowmem_notify_watermark 90
LWM 架构如图 2 所示。基本上,LWM 通过将 security_operations 结构中的 vm_enough_memory 函数指针字段设置为指向函数 low_vm_enough_memory() 来覆盖内核默认的过度提交行为。low_vm_enough_memory() 实现了一种基于先前描述的公式的启发式方法。将 vm_enough_memory 绑定到 low_vm_enough_memory() 允许拦截所有内存页分配请求,以便验证已提交的虚拟内存是否已达到 MAT 或 ST 水位线。清单 1 展示了 MAT 和 ST 水位线在 low_vm_enough_memory() 函数中是如何实现的。
清单 1. MAT 和 ST 水位线启发式算法
1 static int low_vm_enough_memory(long pages) 2 { 3 unsigned long committed; 4 unsigned long deny_threshold, notify_threshold; 5 int cap_sys_admin = 0; 6 7 if (cap_capable(current, CAP_SYS_ADMIN) == 0) 8 cap_sys_admin = 1; 9 10 if (deny_percentage==0||notify_percentage==0) 11 return __vm_enough_memory(pages,cap_sys_admin); 12 13 deny_threshold= 14 totalram_pages*deny_percentage/100; 15 notify_threshold= 16 totalram_pages*notify_percentage/ 100; 17 18 vm_acct_memory(pages); 19 committed = atomic_read(&vm_committed_space); 20 if (committed >= deny_threshold) { 21 enter_watermark_state(1); 22 if (cap_sys_admin) 23 return 0; 24 vm_unacct_memory(pages); 25 return -ENOMEM; 26 } else if (committed >= notify_threshold) { 27 enter_watermark_state(1); 28 return 0; 29 } 30 enter_watermark_state(0); 31 return 0; 32 }
清单 1 中的代码解释如下
第 7、8 行:验证当前进程是否具有 root 权限。
第 10、11 行:如果 MAT 或 ST 水位线为零,则执行默认的过度提交行为。
第 13-16 行:计算低内存水位线阈值。
第 18 行:页面已提交以更新 vm_committed_space 的数量。
第 19 行:获取已提交内存的数量。
第 20 行:验证已提交内存是否已达到 MAT 水位线。
第 21 行:如果已达到 MAT,则将标志状态设置为 1——state=1 表示已达到两个阈值中的任何一个(或两者)。
第 22、23 行:不拒绝 root 程序的内存分配——这些程序的分配是成功的。
第 24 行:由于已达到 MAT,因此取消提交当前已提交的页面。
第 25 行:返回无可用内存消息。
第 26 行:验证已提交内存是否已达到 ST 水位线。
第 27、28 行:将状态设置为 1,并且分配已成功。
第 30 行:将状态设置为 0(如果未达到阈值)。
第 31 行:内存分配已成功。
enter_watermark_state() 函数确定是否已达到低内存水位线条件,并最终将 LMS 发送到用户空间。全局布尔变量 lowmem_watermark_reached 标记进入或退出低内存水位线条件的状态,分别赋值为 1 或 0。每当此变量的值发生更改时,都会分派 LMS。
清单 2. 进入水位线状态的算法
1 static void enter_watermark_state(int new_state) 2 { 3 int changed = 0, r; 4 5 spin_lock(&lowmem_lock); 6 if (lowmem_watermark_reached != new_state) { 7 lowmem_watermark_reached = new_state; 8 changed = 1; 9 } 10 spin_unlock(&lowmem_lock); 11 if (changed) { 12 printk(KERN_DEBUG MY_NAME ": changed to %d\n", 13 new_state); 14 r = kobject_uevent(&kernel_subsys.kset.kobj, 15 KOBJ_CHANGE, 16 &low_watermark_attr.attr); 17 if (r < 0) 18 printk(KERN_ERR MY_NAME 19 ": kobject_uevent failed: %d\n", r); 19 } 20 }
清单 2 说明了状态是如何更改的,以及 LMS 是如何发送到用户空间的。直观地说,代码的工作原理如下
第 5 行:锁定以避免竞争条件。
第 6 行:验证新状态是否与旧状态不同。
第 7、8 行:更新 lowmem_watermark_reached 和 changed 变量。
第 10 行:解锁以离开临界区。
第 11 行:验证状态是否已更改。
第 12-16 行:记录状态已修改,并使用内核事件层机制发送信号。
第 17-19 行:如果发生错误,则记录消息。
MAT 的调整可以根据一些用例凭经验完成。此处未介绍 ST 水位线的调整,但其调整方式通常与 MAT 相同。场景中使用的应用程序应成功填满内存,从而使系统过载。这样做可能会触发系统缓慢和内核 OOM 杀死,从而确保调整 MAT 水位线的有效用例。
如前所述,最佳 MAT 值(内存分配拒绝阈值)应如此设置,以避免系统缓慢和内核 OOM 杀手执行。MAT 值以内核提交的内存百分比给出,由于 Linux 内核的内存过度提交功能,该值可能超过 100%。
基本上,在实验期间需要识别三种行为:OOM 杀手执行、内存分配拒绝和系统缓慢。实验是在具有 64MB RAM 内存和 128MB 闪存的无交换设备上进行的。闪存是用作块设备以保留数据的辅助存储。
第一个用例涉及以渐进方式达到 MAT,运行以下应用程序(按列出顺序):Web 浏览器、电子邮件客户端、用于配置系统的控制面板和图像查看器。首先,Web 浏览器加载一个网页,然后电子邮件客户端在收件箱中加载大约 360 条消息,然后打开控制面板,最后图像查看器连续加载许多图像文件(一次只将一个图像加载到内存中)。每个图像文件都比前一个图像文件逐渐增大,所有文件都只有几百 KB,但其中一个文件约为 2MB。根据不同的 MAT 值,逐步加载这些文件可能会导致不同的系统行为。表 1 说明了在此场景中改变 MAT 值时的结果。
120% 的 MAT 阈值不是一个好的选择,因为它允许 OOM 杀死发生两次,而缓慢发生三次。在此用例中,最佳 MAT 值为 111%,因为在该级别,系统能够拒绝所有内存分配,从而防止系统缓慢和内核 OOM 杀手执行。
在上述用例中,每当 OOM 杀手发生时,它总是杀死图像查看器应用程序。当图像查看器尝试加载 2MB 的大图像文件时,会发生缓慢。在实验过程中,人们感觉到 OOM 杀手总是在系统缓慢期间启动,并且通常系统缓慢非常严重,以至于等待 OOM 杀死是不可行的。
第二个用例可能尝试以更直接的方式达到 MAT 阈值。启动以下应用程序:Web 浏览器、PDF 查看器、图像查看器和控制面板。Web 浏览器加载一个网页,然后 PDF 查看器尝试加载一个 8MB 的文件,然后图像查看器加载一个 3MB 的图像文件,最后调用控制面板。
在此用例中,每当图像查看器加载图像文件时,先前加载的 8MB PDF 文件都会被卸载,因为已达到 ST 阈值,从而导致向用户空间分派信号以释放内存。观察到的行为还涉及控制面板应用程序的终止,这可以归因于由于已达到 MAT 而导致的内存分配拒绝。表 2 显示了此用例在不同 MAT 值下的实验结果。
此用例场景表明可靠的 MAT 值为 110%。当控制面板启动时,高于 110% 的值会发生缓慢。图 3 说明了 MAT 和 ST 在此用例中的行为方式。显示的内存消耗曲线是假设的,但它绝不会改变上述结果。
在实验过程中,验证计划的用例是否足以校准 MAT 值非常重要,因为可能存在不会使内存分配过载的用例。此类场景的一个示例可能是调用 Web 浏览器在后台下载 36MB 的文件,同时玩游戏。我们的实验表明,此用例在确定实际的 MAT 值方面不是很有用,因为它即使在 MAT 值为 120% 或更高的情况下也能成功运行。
为了帮助快速选择要杀死的进程以释放内存,一种有用的方法可能是将应用程序注册为可杀死或不可杀死。被认为可杀死的应用程序可以注册在称为红名单的列表中。此外,其他对于系统正确功能至关重要的应用程序(例如 X Window 系统)在任何情况下都不应被杀死,并且可以注册在称为白名单的列表中。
最终用户可以被允许选择哪些应用程序应注册在红名单或白名单上。但是,这将需要一种安全机制来确保红名单或白名单上的应用程序不会导致任何意外情况或不稳定性。如果应用程序 A 是罪魁祸首,因为它持续消耗大量内存,则它不能在白名单上。同样,如果杀死应用程序 B 会破坏整体系统功能,则它不能在红名单上。可以使用启发式方法预先选择哪些应用程序可以注册在红名单或白名单上。然后可以将预选的应用程序呈现给用户,以选择注册到相应的列表,从而提高用户友好性,同时避免因随意选择而可能产生的问题。
红名单和白名单可以在内核空间中实现,每个列表也反映在 /proc 文件系统中。ST 可用于在应更新红名单和白名单的时刻通知用户空间。之后,内核可以开始终止注册在红名单上的应用程序,以释放内存。也许可以在内核空间中使用排名启发式方法来优先处理红名单上的条目。图 4 说明了基于红名单和白名单方法的 OOM 杀手的可能架构。如果仅仅杀死红名单上的进程是不够的,那么也可以杀死未出现在白名单上的其他进程,作为确保系统稳定的最后措施。
维护一种基于在用户空间中具有一种用于选择和终止进程的启发式方法,而在内核空间中具有另一种启发式方法的机制是很有趣的,因为每个空间都可以提供可能证明对排名标准有用的不同信息。例如,在用户空间中,可以随时知道哪些基于窗口的应用程序是活动的,即最终用户可见和使用的,但在内核空间中,此类信息不易获得。因此,如果存在需要验证是否有任何基于窗口的应用程序处于活动状态的启发式方法,则应在用户空间中实现。
处理无交换嵌入式系统需要建立一种替代的内存管理方法,以防止缓慢并控制 OOM 杀手的调用和执行。基于 MAT 和 ST 的想法简单而实用,并且可以在不同的无交换嵌入式设备上进行调整,因为 LMW 内核模块提供了 /proc 和 sysctl 接口,可以根据需要从用户空间更改 MAT 和 ST 值。
可以实现其他机制,例如红名单和白名单注册列表。设计考虑与无交换嵌入式设备相关的功能的不同选择标准也很有趣。
Mauricio Lin 是位于巴西马瑙斯/巴西的诺基亚技术研究所 (INdT) 的软件工程师。自 2003 年以来,Mauricio 一直从事嵌入式系统的 Linux 内存管理工作。他还为与内存消耗分析相关的 Linux 内核的 proc-pid-smaps.patch 做出了贡献。Mauricio 获得了亚马逊联邦大学的数据处理理学学士学位。他从小练习功夫 - 武术,并在北少林风格方面毕业。可以通过 mauriciolin@gmail.com 与他联系。
Ville C. L. de Medeiros 自 1997 年以来一直对 Linux 充满热情。他最初是亚马逊联邦大学 (UFAM) 计算机科学系的实习管理员,毕业后成为该大学的网络管理员。然后,他主动将所有网络服务从基于大型机的服务转换为 Linux。他目前在巴西马瑙斯诺基亚技术研究所的 Linux 嵌入式实验室工作。可以通过 ville.medeiros@gmail.com 与他联系。
Raoni Novellino 是一位 Linux 用户和开发人员,已有两年时间,目前在巴西马瑙斯诺基亚技术研究所工作。可以通过 rnovellino@gmail.com 与他联系。
Ilias Biris 拥有爱丁堡大学人工智能博士学位,并且是 Linux 用户和开发人员已有十年时间。他在巴西马瑙斯诺基亚技术研究所的 Linux 嵌入式实验室担任项目协调员,可以通过 xyz.biris@gmail.com 与他联系。除了 Linux 之外,Ilias 还喜欢清晨的太极拳和剑道中的精彩决斗。
Edjard Mota 拥有爱丁堡大学人工智能博士学位,并且是一位狂热的 Linux 用户已有十年时间,作为开发人员已有两年时间。他目前管理巴西马瑙斯诺基亚技术研究所的 Linux 嵌入式实验室。他是一个早起的人,喜欢在一天开始时做瑜伽。可以通过 edjard.mota@gmail.com 与他联系。