红联Linux门户
Linux帮助

Linux如何终止D状态的进程

发布时间:2016-12-14 09:50:39来源:dog250作者:Bomb250
碰到这个问题,我第一个反应就是网搜解决方案,后来发现了自己的文章《linux内核模块的强制删除-结束rmmod这类disk sleep进程》(http://www.linuxdiyf.com/linux/26881.html),正好,碰到的也是这类问题。不过本文将介绍一种不触动内核模块本身,而是触动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里面增加条件跳转逻辑没能成功。
 
本文永久更新地址:http://www.linuxdiyf.com/linux/26882.html