使用event_control监听memory cgroup的oom事件

在看containerd处理进程oom的代码时,看到了event_control这个文件,经过查阅一些资料,发现memory cgroup v1原生自带了oom的通知机制。当cgroup中的进程因申请内存被oom时,用户态可以通过编写相关代码接受到该通知并做相应的处理。

内容导读

memory cgroup v1 除了可以将进程oom消息通知到用户态外,还可以设置一个阈值,当cgroup中进程的内存使用量超过阈值时,也可以通知到用户态,这些都是通过操作memory cgroup v1的event_control文件实现的。本文将深入内核探究这一通知机制是如何实现的。

效果展示

在做详细的分析之前,先看一下实验的效果。

  1. 创建一个test的memory cgroup目录。
  2. 设置内存限额为100M。
  3. 将shell窗口1进程写入cgroup中。
  4. 在shell窗口2中运行go run oom_monitor.go开始监听oom事件。
  5. 在shell窗口1中执行dd操作,触发内核oom,杀死进程。
  6. 在shell窗口2中观察go代码的运行结果。

shell1中3次oom,shell2中确实收到了3次通知。

// shell1中创建测试的cgroup,并执行三次dd操作,触发oom
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/fs/cgroup/memory# mkdir test
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/fs/cgroup/memory# echo 104857600 > test/memory.limit_in_bytes 
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/fs/cgroup/memory# echo $$ > test/cgroup.procs 
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/fs/cgroup/memory# dd if=/dev/zero of=/root/testfile bs=101M count=1
Killed
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/fs/cgroup/memory# dd if=/dev/zero of=/root/testfile bs=101M count=1
Killed
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/fs/cgroup/memory# dd if=/dev/zero of=/root/testfile bs=101M count=1
Killed
// shell 2中监听oom事件
root@iZt4n1u8u50jg1r5n6myn2Z:~/go/src/test# go run oom_monitor.go 
eventfd : 4
fd : 4, event : 1
fd : 4, event : 1
fd : 4, event : 1

测试代码概览

测试代码使用go语言编写(由containerd的部分代码简化而来,省略错误处理),其中用到了epoll,eventfd,还操作了两个文件memory.oom_control和cgroup.event_control,细节可以参考代码中的注释。

代码中最关键的一步就是将两个fd写入了cgroup.event_control文件,一个是eventfd的fd,一个是memory.oom_control的fd。整体来看这种用法挺奇葩又挺麻烦。

  • eventfd 既可以用来做进程间通信,又可以实现用户态和内核态的通信,代码中的eventfd就是用来处理内核和用户态的通信的。即当oom事件发生时,内核会向eventfd的fd写入数据,之后用户态就可以通过epoll监听到eventfd有数据可读。
  • oom_control的fd,是用来告知内核,关注的是oom事件。
  • epoll fd,监听eventfd的事件。
// oom_monitor.go
package main

import (
        "fmt"
        "golang.org/x/sys/unix"
        "os"
        "io/ioutil"
)

func main() {
        var events [128]unix.EpollEvent
        var buf [8]byte

        // 创建epoll实例
        epollFd, _ := unix.EpollCreate1(unix.EPOLL_CLOEXEC)

        // 创建eventfd实例
        efd, _ := unix.Eventfd(0, unix.EFD_CLOEXEC)
        fmt.Printf("eventfd : %d\n", efd)

        event := unix.EpollEvent{
                                Fd:     int32(efd),
                                Events: unix.EPOLLHUP | unix.EPOLLIN | unix.EPOLLERR,}
        // 将eventfd添加到epoll中进行监听
        unix.EpollCtl(epollFd, unix.EPOLL_CTL_ADD, int(efd), &event)

        // 打开oom_control文件
        evtFile, _ := os.Open("/sys/fs/cgroup/memory/test/memory.oom_control")

        // 注册oom事件,当有oom事件时,eventfd将会有数据可读
        data := fmt.Sprintf("%d %d", efd, evtFile.Fd())
        ioutil.WriteFile("/sys/fs/cgroup/memory/test/cgroup.event_control", []byte(data), 0700)

        for {
                // 开始监听oom事件
                n, err := unix.EpollWait(epollFd, events[:], -1)
                if err == nil {
                        for i:=0; i

观测oom

可以通过cgroup下的文件和proc下的vmstat观测oom。

// 表示该主机总共发生的oom次数,可以做为一个metrics数值
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/fs/cgroup/memory/test# cat /proc/vmstat  | grep oom
oom_kill 39
// 表示该cgroup下发生的oom次数 
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/fs/cgroup/memory/test# cat memory.oom_control
oom_kill_disable 0
under_oom 0
oom_kill 4

eventfd

eventfd的知识点比较少,网上资料也足够了,由于本文用到了eventfd,这里简单对eventfd做一下介绍。eventfd2系统调用会返回一个fd,eventfd在内核维护了一个count数值,并且eventfd的fops有poll,read,write等。

SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags)
{
    return do_eventfd(count, flags);
}
// count就是eventfd维护的数值的初值
static int do_eventfd(unsigned int count, int flags)
{
    struct eventfd_ctx *ctx;
    ctx = kmalloc(sizeof(*ctx), GFP_KERNEL);
    init_waitqueue_head(&ctx->wqh);
    ctx->count = count;
    ctx->flags = flags;

    fd = anon_inode_getfd("[eventfd]", &eventfd_fops, ctx,
                  O_RDWR | (flags & EFD_SHARED_FCNTL_FLAGS));
    return fd;
}
// file_opertion中有poll方法,可以到epool中。
// read,write可以完成内核与用户态的通信,也可以用于进程间通信。
static const struct file_operations eventfd_fops = {
#ifdef CONFIG_PROC_FS
    .show_fdinfo    = eventfd_show_fdinfo,
#endif
    .release    = eventfd_release,
    .poll       = eventfd_poll,
    .read       = eventfd_read,
    .write      = eventfd_write,
    .llseek     = noop_llseek,
};

深入内核

先梳理下整个事件的流程,之后对每一步骤中内核做的事情做一下总结。

  1. 将eventfd和oomfd写入cgroup.event_control文件。
  2. 执行dd命令,触发缺页异常,因为可用内存不足,触发oom。
  3. oom被触发后,内核向eventfd写入数据。
  4. epoll监听到eventfd有可读事件,就说明内核有触发过oom。

cgroup.event_control

这里只列出4个数组成员,省略其他与本文不相关的成员,其中name表示memory cgroup中的文件名。其中read,write,show等函数就表示操作对应文件后会触发的函数。

  • limit_in_bytes就表示该memory cgroup的内存使用配额(限额),既有write函数,又有read函数,说明该文件可读可写;
  • stat表示该memory cgroup当前的内存使用情况概览,所以只有show函数;
  • cgroup.event_control表示向内核注册事件,只有write函数,说明该文件只可写;
  • oom_control表示该memory cgroup中关于oom的配置,有read,write说明该文件可查可写,读的话就是读取状态,写的话,可以表示禁用oom。
static struct cftype mem_cgroup_legacy_files[] = {
    {
        .name = "limit_in_bytes",
        .private = MEMFILE_PRIVATE(_MEM, RES_LIMIT),
        .write = mem_cgroup_write,
        .read_u64 = mem_cgroup_read_u64,
    },
    {
        .name = "stat",
        .seq_show = memcg_stat_show,
    },
    {
        .name = "cgroup.event_control",     /* XXX: for compat */
        .write = memcg_write_event_control,
        .flags = CFTYPE_NO_PREFIX | CFTYPE_WORLD_WRITABLE,
    },
    {
        .name = "oom_control",
        .seq_show = mem_cgroup_oom_control_read,
        .write_u64 = mem_cgroup_oom_control_write,
        .private = MEMFILE_PRIVATE(_OOM_TYPE, OOM_CONTROL),
    },

    { },    /* terminate */
};

经过上面的分析,将eventfd和oomfd写入cgroup.event_control文件后触发的函数是memcg_write_event_control,对该函数做一个解读,就大致可以明白内核这一套oom通知机制是如何实现的。

static ssize_t memcg_write_event_control(struct kernfs_open_file *of,
                     char *buf, size_t nbytes, loff_t off)
{
    // 从输入参数中解析出eventfd的fd
    efd = simple_strtoul(buf, &endp, 10);

    event->memcg = memcg;
    INIT_LIST_HEAD(&event->list);
    init_poll_funcptr(&event->pt, memcg_event_ptable_queue_proc);
    init_waitqueue_func_entry(&event->wait, memcg_event_wake);
    INIT_WORK(&event->remove, memcg_event_remove);

    efile = fdget(efd);
    event->eventfd = eventfd_ctx_fileget(efile.file);

    cfile = fdget(cfd);
    name = cfile.file->f_path.dentry->d_name.name;

    // 可以向内核注册的事件类型,可以有4种。
    if (!strcmp(name, "memory.usage_in_bytes")) {
        event->register_event = mem_cgroup_usage_register_event;
        event->unregister_event = mem_cgroup_usage_unregister_event;
    } else if (!strcmp(name, "memory.oom_control")) {
        event->register_event = mem_cgroup_oom_register_event;
        event->unregister_event = mem_cgroup_oom_unregister_event;
    } else if (!strcmp(name, "memory.pressure_level")) {
        event->register_event = vmpressure_register_event;
        event->unregister_event = vmpressure_unregister_event;
    } else if (!strcmp(name, "memory.memsw.usage_in_bytes")) {
        event->register_event = memsw_cgroup_usage_register_event;
        event->unregister_event = memsw_cgroup_usage_unregister_event;
    }

    // 调用上面的一个函数,将关注的事件注册到内核。
    ret = event->register_event(memcg, event->eventfd, buf);

    vfs_poll(efile.file, &event->pt);
    list_add(&event->list, &memcg->event_list);

    return nbytes;
}

本文只关注oom事件,所以需要再详细看一下mem_cgroup_oom_register_event函数的实现。

static int mem_cgroup_oom_register_event(struct mem_cgroup *memcg,
    struct eventfd_ctx *eventfd, const char *args)
{
    struct mem_cgroup_eventfd_list *event;

    event = kmalloc(sizeof(*event), GFP_KERNEL);

    event->eventfd = eventfd;
    // 这里是最关键的一步,将event加入到了oom的notify列表中。
    list_add(&event->list, &memcg->oom_notify);

    /* already in OOM ? */
    if (memcg->under_oom)
        eventfd_signal(eventfd, 1);

    return 0;
}

最重要的就是list_add那一行,将event加入到了notify的列表中,这样的话,当内核有oom事件时,就可以遍历notify列表,通知到关注者,如调用eventfd_signal,向eventfd写入数据。

static int mem_cgroup_oom_notify_cb(struct mem_cgroup *memcg)
{
    struct mem_cgroup_eventfd_list *ev;

    spin_lock(&memcg_oom_lock);

    list_for_each_entry(ev, &memcg->oom_notify, list)
        eventfd_signal(ev->eventfd, 1);

    spin_unlock(&memcg_oom_lock);
    return 0;
}

mem_cgroup_oom_notify_cb调用eventfd_signal,其中ctx->wqh表示关注者的对象,调用wake_up激活关注者注册的函数memcg_event_wake和ep_poll_callback(在将eventfd加入到epoll时会注册该函数,该函数会唤醒epoll_wait,细节参见之前的文章详解linux epoll)。

__u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n)
{
    unsigned long flags;

    spin_lock_irqsave(&ctx->wqh.lock, flags);
    if (ULLONG_MAX - ctx->count < n)
        n = ULLONG_MAX - ctx->count;
    // 将eventfd的数值加1,并激活关注者
    ctx->count += n;
    if (waitqueue_active(&ctx->wqh))
        wake_up_locked_poll(&ctx->wqh, EPOLLIN);
    spin_unlock_irqrestore(&ctx->wqh.lock, flags);

    return n;
}

oom通知机制过程总结

oom.PNG

创建eventfd是内核向用户态通知的一个“桥梁”,将oom_controlfd写入event_control表示关注的事件类型是oom,之后将eventfd加入到epoll进行监听。

当该memory cgoup内的进程dd因为内存不足,触发oom时,调用eventfd_signal,进而调用ep_poll_callback激活epoll_wait,epoll_wait返回后就表示该cgroup内有进程触发了内核的oom。

ftrace验证

设置ftrace,验证上面的分析过程是否正确。

// 关注以下函数
root@iZt4n1u8u50jg1r5n6myn2Z:/sys/kernel/debug/tracing# cat set_ftrace_filter
mem_cgroup_oom_unregister_event
mem_cgroup_oom_register_event
memcg_event_wake
memcg_event_remove
ep_poll_callback
eventfd_signal

// 将event_control写入eventfd和oomfd触发的函数堆栈
 => mem_cgroup_oom_register_event   // 向内核注册关注oom事件
 => memcg_write_event_control       
 => cgroup_file_write       // 操作cgroup文件的入口
 => kernfs_fop_write       
 => __vfs_write
 => vfs_write                 // vfs层写入口
 => ksys_write
 => __x64_sys_write  // write系统调用入口
 => do_syscall_64
 => entry_SYSCALL_64_after_hwframe

// 发生oom后的堆栈
 => ep_poll_callback   // 激活epoll_wait
 => __wake_up_common
 => __wake_up_locked_key
 => eventfd_signal       // 将eventfd维护的数值加1
 => mem_cgroup_oom_notify   // 通知oom事件的关注者
 => try_charge    // 计算内存是否充足
 => mem_cgroup_try_charge
 => mem_cgroup_try_charge_delay
 => do_anonymous_page
 => __handle_mm_fault
 => handle_mm_fault   
 => do_user_addr_fault
 => __do_page_fault
 => do_page_fault  // 处理缺页异常
 => do_async_page_fault
 => async_page_fault
 => __clear_user
 => clear_user
 => iov_iter_zero
 => read_iter_zero
 => new_sync_read
 => __vfs_read
 => vfs_read
 => ksys_read
 => __x64_sys_read
 => do_syscall_64

你可能感兴趣的:(使用event_control监听memory cgroup的oom事件)