在百忙中抽出点时间帮温州皮鞋厂老板解决一个杀掉D进程的问题,虽然最终线上的机器被老板蹂躏挂了,我也没帮上什么实质性的忙,还是写点记录,以备查阅。
碰到这个问题,我第一个反应就是网搜解决方案,后来发现了自己的文章《 linux内核模块的强制删除-结束rmmod这类disk sleep进程》,正好,老板碰到的也是这类问题。不过本文将介绍一种不触动内核模块本身,而是触动D进程的方案。
声明一下,本文介绍的方法并非常规方法,如果出现了D进程,正常的做法,除了满足它或者重启机器之外,别无其它捷径。
先看一下D进程的成因。
所谓的D进程,就是不可中断的进程,即便你唤醒它,它还是要睡在那里,直到它等待的资源到位,典型的一个D进程的代码情景是:
static void wait_for_zero_refcount(struct module *mod)
{
/* Since we might sleep for some time, release the mutex first */
mutex_unlock(&module_mutex);
for (;;) {
DEBUGP("Looking at refcount...\n");
// 设置为不可中断。
set_current_state(TASK_UNINTERRUPTIBLE);
// 除非引用计数为0,否则不退出循环。
if (module_refcount(mod) == 0)
break;
// 等待期间,出让CPU资源。
schedule();
}
current->state = TASK_RUNNING;
mutex_lock(&module_mutex);
}
几乎所有的D进程在D状态期间都符合上述代码里的场景,明确了这个场景之后,你就应该知道下面的办法并不奏效的原因了:
// 新编写一个模块,在init函数中更改D进程状态,然后唤醒它。
static int __init mymm(void)
{
struct task_struct *p;
if (pid > 0) {
for_each_process(p) {
if (task_pid_vnr(p) == pid) {
// 企图更改其状态为”可中断状态“,然后kill之!
set_task_state(p, TASK_INTERRUPTIBLE);
// 然则一旦被wakeup,根据上述D场景,又会进入那个死结!
wake_up_process(p);
break;
}
}
}
return -ENOMEM;
}
这是一种典型的头痛医头脚痛医脚的方案,你不是状态为D吗?我就把你的状态改成不是D。然而,如果了解了D进程本质,就知道这是没用的。那么怎么办呢?
上述的D进程场景中,很显然,D进程在等待module的refcount变成0,正如我之前的文章中描述的那种方法,编写一个模块找到出问题的module,将其refcount设置为0即可,然而不同的D进程可能在等待不同的资源,100类D进程就有100种解决的办法。
下面的办法用来把D进程本身带出死结怪圈:
void exit_task1()
{
// 这个有点猛,仅为测试。
// emergency_restart();
// TODO:这里要做的事情非常多,类似ret_from_fork那样,要执行schedule_tail后处理之类的事情。
// 首先,你必须preempt_enable_no_resched,不然会锁死,其次,还要考虑schedule_tail的逻辑。
}
static int __init mymm(void)
{
struct task_struct *p;
if (pid > 0) {
for_each_process(p) {
if (task_pid_vnr(p) == pid) {
// 企图更改其状态为”可中断状态“,然后kill之!
set_task_state(p, TASK_INTERRUPTIBLE);
// 修改D进程被切换前保存的PC指针,待它被唤醒后,将其引入别处,脱离那个死循环怪圈。
p->thread.ip = (unsigned long)exit_task1;
// 一旦被唤醒,执行流将开始执行exit_task1。
wake_up_process(p);
break;
}
}
}
return -ENOMEM;
}
以上方案的关键在于修改p->thread.ip的值。如果没有意外,不在运行状态(即current不是它)的进程其p->thread.ip值就是schedule中__switch_to后面的指令地址,意思是其被切换回来后要执行的地址,但是fork新进程时除外,新进程由于没有谁将其切换出,也就不存在切换入的情况了,因此对于fork出来的新进程而言,要手工帮它制造一个被切换出的现场,待其被切换入的时候执行,这个就是ret_from_fork。
受到ret_from_fork的启发,其实我们可以更改任何进程的p->thread.ip指针,从而将其从原来的执行绪中拉出,就好像穿越虫洞一样进入另一个时空。
最后,要说明的是,exit_task1是一个非常复杂的函数,不是想象的那样直接调用do_exit就完事的。它要把schedule函数中从__switch_to冒出来以后一直到结束的逻辑全部执行一遍。另外,要注意的是,上述的代码都是基于32位系统的,如果是64位系统,就会比较麻烦,因为64位的话,不能采用修改p->thread.ip的方法,它完全是另外一套机制。如果想在64系统将D进程拉出死循环,需要动态HOOK switch_to的二进制指令(你看,64位情况下,ret_from_fork是在__switch_to汇编里直接条件跳转的,copy_thread只是设置了一个TIF_FORK标志),学着64位ret_from_fork的样子,在__my_switch_to里面增加条件跳转逻辑,我尝试了大半个晚上就panic了大半个晚上,没能成功...