Linux内核模块的强制删除-结束rmmod这类disk sleep进程


一.问题:
前些日子在工作中遇到一个文件,当rmmod一个模块的时候,在模块的exit函数中阻塞了,rmmod进程杀也杀不掉,永远呆在那里,发现它已经是D(disk sleep)状态了,真的无能为力了吗?我不相信这个事实,所以今天在稍微闲暇的时候换换脑子,终于有了解决办法。

二.分析:
解铃还须系铃人,既然是在内核中出了问题,还是需要在内核中寻找办法,解决这类问题的前提是对内核卸载模块的精确理解,流程都理解透了,害怕找不到原因吗?原因都找到了,办法肯定是有的! (这也是我从公司学习到的最重要的东西!), 我按照这个原则,查到了rmmod最终调用的代码:

asmlinkage long sys_delete_module(const char __user *name_user, unsigned int flags)   
  1. {   
  2.     ...   
  3.     if (!list_empty(&mod->modules_which_use_me)) { //0.如果其它模块依赖该模块,则不删除   
  4.         /* Other modules depend on us: get rid of them first. */  
  5.         ret = -EWOULDBLOCK;   
  6.         goto out;   
  7.     }   
  8.     ...   
  9.     if (mod->state != MODULE_STATE_LIVE) { //1.如果模块不是LIVE状态,那么就无法进行下去了,得到的结果是busy   
  10.         ...   
  11.         ret = -EBUSY;   
  12.         goto out;   
  13.     }   
  14.     ...   
  15.     if (!forced && module_refcount(mod) != 0) //2.如果引用计数不是0,则等待其为0   
  16.         wait_for_zero_refcount(mod);   
  17.     up(&module_mutex);   
  18.     mod->exit();     //3.如果在这个里面阻塞了,那就无法返回了   
  19.     down(&module_mutex);   
  20.     free_module(mod);   
  21.     ...      
  22. }  

以上注释了4处,分别解释如下:
情况0: 有其它模块依赖要卸载的模块。模块a是否依赖模块b,这个在模块a加载的时候调用resolve_symbol抉择,如果模块a的symbol在模块b中,则依赖
情况1: 只有LIVE状态的模块才能被卸载。
情况2: 引用计数在有其它模块或者内核本身引用的时候不为0,要卸载就要等待它们不引用为止。
情况3: 这个情况比较普遍,因为模块万一在使用过程中oom或者依赖它的模块oom或者模块本身写的不好有bug从而破坏了一些数据结构,那么可能造成exit函数中阻塞,最终rmmod不返回!

三.尝试一下:
     针对情况3,举一个例子来模拟:

  1. static DECLARE_COMPLETION(test_completion);   
  2. int init_module()   
  3. {   
  4.         return 0;   
  5. }   
  6. void cleanup_module( )   
  7. {   
  8.     wait_for_completion(&test_completion);   
  9. }   
  10. MODULE_LICENSE("GPL");  
编译为test.ko,最终在rmmod test的时候会阻塞,rmmod永不返回了!很显然是cleanup_module出了问题,因此再写一个模块来卸载它!在编译模块之前,首先要在/proc/kallsym中找到以下这行:
f88de380 d __this_module        [XXXX无法卸载的模块名称]
这是因为modules链表没有被导出,如果被导出的话,正确的方式应该是遍历这个链表来比对模块名称的。
以下的模块加载了以后,上述模块就可以被rmmod了:
  1. void force(void)   
  2. {   
  3. }   
  4. int __init rm_init(void)   
  5. {   
  6.         struct module *mod = (struct module*)0xf88de380;   
  7.         int i;   
  8.         int o=0;   
  9.         mod->state = MODULE_STATE_LIVE; //为了卸载能进行下去,也就是避开情况1,将模块的状态改变为LIVE   
  10.         mod->exit = force;    //由于是模块的exit导致了无法返回,则替换mod的exit。再次调用rmmod的时候会调用到sys_delete_module,最后会调用 exit回调函数,新的exit当然不会阻塞,成功返回,进而可以free掉module   
  11.         for (i = 0; i < NR_CPUS; i++) { //将引用计数归0   
  12.                 mod->ref[i].count = *(local_t *)&o;   
  13.         }   
  14.         return 0;   
  15. }   
  16. void __exit rm_exit(void)   
  17. {   
  18. }   
  19. module_init(rm_init);   
  20. module_exit(rm_exit);   
  21. MODULE_LICENSE("GPL");  

然后加载上述模块后,前面的模块就可以卸载了。

四.更深入的一些解释:
针对这个模块导致的无法卸载的问题,还有另一种方解决式,就是在别的module中complete掉这个completion,这个completion当然是无法直接得到的,还是要通过/proc/kallsyms得到这个completion的地址,然后强制转换成completion并完成它:

 
  1. int __init rm_init(void)   
  2. {   
  3.         complete((struct completion *)0xf88de36c);   
  4.         return 0;   
  5. }   
  6. void __exit rm_exit(void)   
  7. {   
  8. }   
  9. module_init(rm_init);   
  10. module_exit(rm_exit);   
  11. MODULE_LICENSE("GPL");  

当然这种方式是最好的了,否则如果使用替换exit的方式,会导致前面的那个阻塞的rmmod无法退出。你可能想在新编写的模块中调用try_to_wake_up函数,然而这也是不妥当的,因为它可能在wait_for_completion,而wait_for_completion中大量引用了已经被替换exit回调函数进而被卸载的模块数据,比如:

spin_unlock_irq(&x->wait.lock);
schedule();
spin_lock_irq(&x->wait.lock);

  • 1
  • 2
  • 下一页

相关内容