在看containerd处理进程oom的代码时,看到了event_control这个文件,经过查阅一些资料,发现memory cgroup v1原生自带了oom的通知机制。当cgroup中的进程因申请内存被oom时,用户态可以通过编写相关代码接受到该通知并做相应的处理。
内容导读
memory cgroup v1 除了可以将进程oom消息通知到用户态外,还可以设置一个阈值,当cgroup中进程的内存使用量超过阈值时,也可以通知到用户态,这些都是通过操作memory cgroup v1的event_control文件实现的。本文将深入内核探究这一通知机制是如何实现的。
效果展示
在做详细的分析之前,先看一下实验的效果。
- 创建一个test的memory cgroup目录。
- 设置内存限额为100M。
- 将shell窗口1进程写入cgroup中。
- 在shell窗口2中运行go run oom_monitor.go开始监听oom事件。
- 在shell窗口1中执行dd操作,触发内核oom,杀死进程。
- 在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,
};
深入内核
先梳理下整个事件的流程,之后对每一步骤中内核做的事情做一下总结。
- 将eventfd和oomfd写入cgroup.event_control文件。
- 执行dd命令,触发缺页异常,因为可用内存不足,触发oom。
- oom被触发后,内核向eventfd写入数据。
- 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通知机制过程总结
创建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