使用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<n; i++ {
                                fmt.Printf("fd : %d, event : %d\n", events[i].Fd, events[i].Events)
                                // 消费掉eventfd的数据
                                unix.Read(int(events[i].Fd), buf[:])
                        }
                }
        }

        unix.Close(epollFd)
        unix.Close(int(evtFile.Fd()))
}

观测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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容