目录

🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂 捐赠一下吧 🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂

BiscuitOS


OOM 使用手册

本节基于丰富的实例案例来介绍 OOM 的使用,实践案例已经在 BiscuitOS 适配,开发者可以参考部署流程在 BiscuitOS 直接实践案例:


内核空间触发一次 OOM

系统在分配物理内存时,如果只分配一个物理页,那么物理页来自 PCP 分配器,如果是多个物理页,那么物理页来自 Buddy 分配。对于 PCP 分配器如果没有可用的物理页时,其会从 Buddy 分配器一次性申请一批空闲的单个物理页进行维护,当 PCP 分配器无法从 Buddy 分配器中分配内存时也会触发 OOM; 同理 Buddy 分配器通过 free_area 链表在每个 Zone 上都维护一定数量的空闲页,如果 free_area 链表无法提供可用的物理页时页会触发 OOM。本节用于介绍如果在内核空间触发一次 OOM, BiscuitOS 提供了使用案例,其在 BiscuitOS 上部署逻辑如下:

cd BiscuitOS/
# KERNEL_VERSION: 内核版本字段 e.g. 5.0
# ARCHITECTURE: 架构字段 e.g x86_64 or i386
make linux-${KERNEL_VERSION}-${ARCHITECTURE}_defconfig
make menuconfig

  [*] Package --->
      [*] OOM: Out-of-memory mechanism --->
          [*]  Trigger OOM on Kernel Space --->

make
cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-OOM-kernel-default/
# 下载案例源码
make download

BiscuitOS-OOM-kernel-default Gitee Source Code

BiscuitOS 独立模块部署手册

案例源码通过一个内核模块进行展示,在模块的初始化函数 BiscuitOS_init() 中,87 行通过 register_oom_notifier() 函数向 OOM 的通知链中注册了一个钩子函数,钩子函数在 68-80 行进行构建,其核心是 struct notifier_block 数据结构,其 notifier_call 指向了 BiscuitOS_oom_notifier(), 可以看出当系统发生 OOM 的时候,BiscuitOS_oom_notifier() 函数就会将 BiscuitOS_oom_stop 变量置 1. 在模块的 90 行通过调用 caculate_order_free_pages() 函数可以计算 Buddy 分配器中大于且等于 order 大小的空闲物理页数量,其实现位于 24-45 行,caculate_order_free_pages() 函数的逻辑是通过在所有的 NUMA NODE 上遍历所有维护物理页的 Zone,其 free_area 链表上记录可用物理页的数量. 在模块 96 行通过调用 cost_pages() 函数将不断申请可用物理页,直到触发 OOM 为止。cost_pages() 函数的实现位于 47-66 行,其逻辑通过在 while 循环和 alloc_pages() 函数不同申请指定 order 的物理页,如果物理页申请失败,或者发现 BiscuitOS_oom_stop 为 1 的时候就会提供物理页的申请。如果 OS_oom_stop 为 1,那么说明系统发生了一次 OOM,那么函数通过 __free_pages() 函数释放指定数量的物理页回 Buddy 分配器,并将 OS_oom_stop 设置为 0 以此等待下一次 OOM 的发生, 函数 63 行通过参数 release 限定了当发生 OOM 时需要释放多少物理页回 Buddy 分配器,目前支持 PAGE_OOM_BACKUP 和 PAGE_OOM_NOBACKUP 两种模式. 回到模块 100 行,当触发一次 OOM 之后,函数调用 unregister_oom_notifier() 函数解除 OOM 的监听,至此模块功能完结. 案例代码很精简,那么接下来使用如下命令在 BiscuitOS 上实践案例代码:

cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-OOM-kernel-default/
# 编译源码
make
# 安装驱动
make install
# Rootfs 打包
make pack
# 运行 BiscuitOS
make run

# BiscuitOS 运行之后安装驱动模块
insmod /lib/modules/$(uname -r)/extra/BiscuitOS-OOM-kernel-default.ko

# 查看系统物理内存使用情况
cat /proc/buddyinfo

可以看到当安装模块之后立即触发了一次 OOM,从 OOM 的打印的消息可以看出发生 OOM 的位置,以及物理内存的使用情况,最后通过 “/proc/buddyinfo” 查看物理内存的使用情况,可以看出 PAGE_ALLOC_COSTLY_ORDER 也就是 order 3 之后的物理内存已经被消耗殆尽, 模块也只触发了一次 OOM.


用户空间触发一次 OOM

Cgroup 可以用来限制进程组的资源使用,其中可以限制进程组的物理内存使用,当进程组中的进程使用的物理内存达到一定值之后,Cgroup 可以根据策略要么将进程组的部分内存 swap 出去,要么直接调用 OOM killer 杀死进程组中的进程, 由于使用了 MEMCG 的功能,那么必须打开内核的 CONFIG_MEMCG 宏。本节用于介绍 Cgroup 调用 OOM killer 杀死一个进程,因此实现用户空间触发 OOM 的条件, BiscuitOS 提供了使用案例,其在 BiscuitOS 上部署逻辑如下:

cd BiscuitOS/
# KERNEL_VERSION: 内核版本字段 e.g. 5.0
# ARCHITECTURE: 架构字段 e.g x86_64 or i386
make linux-${KERNEL_VERSION}-${ARCHITECTURE}_defconfig
make menuconfig

  [*] Package --->
      [*] OOM: Out-of-memory mechanism --->
          [*]  Trigger OOM on Userspace --->

make
cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-OOM-userspace-default/
# 下载案例源码
make download

BiscuitOS-OOM-userspace-default Gitee Source Code

BiscuitOS 独立模块部署手册

案例源码通过一个应用程序进行讲解,在应用程序 25 行使用 while 循环,在每次循环里程序在 27 行调用 malloc() 函数分配长度为 REGION_SIZE 的内存块,这里将 REGION_SIZE 设置为 128KiB,主要考虑到 Cgroup 对物理内存的感知最小是 128KiB。程序接着在 29 行调用 memset() 函数对虚拟内存进行写操作,这里主要是触发缺页以此分配物理内存,最后睡眠 1s 之后进行下一次循环。程序很简单,就是不停的新增内存,那么接下来使用如下命令在 BiscuitOS 上实践案例代码:

cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-OOM-userspace-default/
# 编译源码
make
# 安装驱动
make install
# Rootfs 打包
make pack
# 运行 BiscuitOS
make run

在运行应用程序之前,需要对 Cgroup 进行配置,以此用于限制进程使用物理内存的上限,因此从上图可以看到,系统起来之后使用 mount 命令在 “/sys/fs/cgroup” 目录下挂载一个 memory 相关的 cgroup 文件系统,挂载成功之后建立一个子目录用于限制案例进程的资源,可以直接在 “/sys/fs/cgroup/memory” 目录下直接创建目录,例如上面的 BiscuitOS 目录就是一个新的资源限制组:

接下来是后台运行进程 BiscuitOS-OOM-userspace-default,并使用 pidof 获得进程的 PID,接着将 PID 写入到资源组的 “cgroup.procs” 成员,这样进程就被 Cgroup 管理起来,接下来设置进程最大能够使用 512KiB 的物理内存,如果内存超过 512KiB 之后,Cgroup 会调用 OOM Killer 直接杀下进程。如上图可以看到进程触发了 OOM 并被杀死。另外如果当进程达到 Cgroup 限制的最大值之后,如果不想被 OOM 杀死,那么可以通过设置进程组的 “memory.oom_control”, 将其设置为 1 即可:

从上图的实践结果来看,当向 “memory.oom_control” 写入之后,运行程序并且程序使用的物理内存达到 512KiB 之后,进程将停止运行并等待有物理内存释放,此时可用查看 “memory.failcnt” 查看进程已经重试了多少次,最后可以查看 “memory.oom_control” 节点,发现 under_oom 被设置为 1,那么表示进程组中有一个进程本来应该被 OOM Killer 杀死的,但最后没有被杀死.