前言:
- 本文是纯技术细节文档,不涉及PSI整体机制等内容。
- 本文以bug为驱动,写下调查跟踪思路,和定位方法。
一、简介
PSI = Pressure Stall Information,由facebook提出的新特性,目的是结合cgroup用于监控内核三大项数据memory/cpu/io。
必备的关键词:
- trigger:当用户对/proc/pressure/{cpu, io, mem}三个文件任意一个发起poll监听,内核则会创建trigger,因此trigger可以有多个。
- psi poll work:即为[psimon]进程,是指的内核psi模块的用于监听poll事件的守护进程,内核中代码是用group->poll_work来表示。此进程只有一个,只有当trigger数量减为0时,此进程才能被销毁。
二、背景
在内核源码树中的Documentation/accounting/psi.txt文件介绍了PSI的基本概念,并给出了如何监控(monitor)PSI文件。给出的代码思路如下:通过poll函数发起对/proc/pressure/io(psi.txt是mem)文件的监听,如果内核中有事件接受到,则用户态返回接受数值,正常情况是能够“event triggered"。
但是,当第一次poll监听程序发起后正常退出,第二次以及后面更多次再重新启动poll监听,会发现总是无法得到内核的事件返回值。这个就是很明显的bug,我们要动手开始去解决!
三、PSI原理分析
内核态关键函数:
- poll对接函数:psi_fop_poll()->psi_trigger_poll();此函数是用户态发起poll()系统调用后,会有psi此函数对接,检查在此poll之前的时间内是否有事件发生,如果有则设置相应事件signal,如果无则通过poll_wait()让其等待。所以用户每次监听io/mem/cpu的任一文件,都会引发此对接函数的调用,根据已有的trigger判断事件监听情况。总结,这里只做一次’询问‘而已,并不操作psi事件。
- psi poll work创建:psi_trigger_create();通过此函数创建和初始化trigger,并在内核创建work,通过kthread_init_delayed_work()函数初始化交由kthread管理,即为[psimon]守护进程,在内核态接受调度负责设置psi事件。
- psi poll work销毁:psi_trigger_destroy();销毁psi_fop_poll()时候创建的trigger,如果是最后一个trigger,则销毁psi_trigger_create()阶段创建的work。
- psi poll work入口:psi_poll_work();是内核守护进程[psimon]的入口调用函数。在psi poll work创建过程中,交给kthreadd管理。
疑问点:此psi poll work何时调度?
- psi的状态切换:psi_task_change();由enqueue_task()/dequeue_task()进程进入和退出引发调用,此函数会调用psi_schedule_poll_work()函数来让psi poll work加入queue队列然后执行,首先它会判断一个标志poll_scheduled是否为0,如果为0,那么就将poll_work进程通过kthread_queue_delayed_work()函数放入队列中延迟执行,并且设置poll_scheduled标志为1;如果为1,那么将不放入到队列中,不设置poll_scheduled。这么看来,psi poll work是有可能不能够得到运行机会的。
疑问点:poll_scheduled何时设置?
- poll_scheduled设置点:1)在函数最初group_init()初始化时候会设置poll_scheduled为0;2)在入口函数中会设置poll_scheduled为0,因为进入到入口函数说明此psi poll work已经在运行了;
四、定位问题
重新捋一遍执行过程,
- 首先内核创建psi poll work,并不执行,也未放在run的队列中。
- 用户态发起poll系统调用,内核判断poll_scheduled标志位是0还是1,如果0,则调度work。
- 触发成功poll事件,销毁trigger,销毁[psimon]守护进程。
- 第二次创建work,同上。
- 用户态发起系统调用,此时,poll_scheduled为1,无论[psi的状态切换]如何变化,内核都需要判断此标志位是否为0,否则不加入kthread queue。
- 用户态再次发起系统调用,同上,不调度。这样就一直处于死状态。
因此,寻找为什么poll_scheduled为1,又究竟何时变为了1,顺藤摸瓜撸完源码后,答案很清楚:在destroy销毁后poll_scheduled并未重新设置(reinitialize)为0。
解决方案在psi_trigger_destroy()函数的[psimon]销毁操作后加入atomic_set(&group->poll_scheduled, 0);即可。
五、后记
有些感想,
- 需要对kthreadd机制的了解。如果不熟悉则会容易跑偏,认为是不是因为kthreadd的驱动进程运行部分有问题。
- 需要对poll()机制的熟悉。除了用户态的poll()调用以外,还是要熟悉内核态如何构建一个poll监听机制。
- 需要做充分且必要的实验。每一步的操作都会带来不同的细微的差别,需要总结结果来大胆猜测。