一.問題:
前些日子在工作中遇到一個文件,當rmmod一個模塊的時候,在模塊的exit函數中阻塞了,rmmod進程殺也殺不掉,永遠呆在那裏,發現它已經是D(disk sleep)狀態了,真的無能爲力了嗎?我不相信這個事實,所以今天在稍微閒暇的時候換換腦子,終於有了解決辦法。
二.分析:
解鈴還須繫鈴人,既然是在內核中出了問題,還是需要在內核中尋找辦法,解決這類問題的前提是對內核卸載模塊的精確理解,流程都理解透了,害怕找不到原因嗎?原因都找到了,辦法肯定是有的! (這也是我從公司學習到的最重要的東西!), 我按照這個原則,查到了rmmod最終調用的代碼:
- asmlinkage long sys_delete_module(const char __user *name_user, unsigned int flags)
- {
- ...
- if (!list_empty(&mod->modules_which_use_me)) { //0.如果其它模塊依賴該模塊,則不刪除
- /* Other modules depend on us: get rid of them first. */
- ret = -EWOULDBLOCK;
- goto out;
- }
- ...
- if (mod->state != MODULE_STATE_LIVE) { //1.如果模塊不是LIVE狀態,那麼就無法進行下去了,得到的結果是busy
- ...
- ret = -EBUSY;
- goto out;
- }
- ...
- if (!forced && module_refcount(mod) != 0) //2.如果引用計數不是0,則等待其爲0
- wait_for_zero_refcount(mod);
- up(&module_mutex);
- mod->exit(); //3.如果在這個裏面阻塞了,那就無法返回了
- down(&module_mutex);
- free_module(mod);
- ...
- }
以上註釋了4處,分別解釋如下:
情況0: 有其它模塊依賴要卸載的模塊。模塊a是否依賴模塊b,這個在模塊a加載的時候調用resolve_symbol抉擇,如果模塊a的symbol在模塊b中,則依賴
情況1: 只有LIVE狀態的模塊才能被卸載。
情況2: 引用計數在有其它模塊或者內核本身引用的時候不爲0,要卸載就要等待它們不引用爲止。
情況3: 這個情況比較普遍,因爲模塊萬一在使用過程中oom或者依賴它的模塊oom或者模塊本身寫的不好有bug從而破壞了一些數據結構,那麼可能造成exit函數中阻塞,最終rmmod不返回!
三.嘗試一下:
針對情況3,舉一個例子來模擬:
- static DECLARE_COMPLETION(test_completion);
- int init_module()
- {
- return 0;
- }
- void cleanup_module( )
- {
- wait_for_completion(&test_completion);
- }
- MODULE_LICENSE("GPL");
編譯爲test.ko,最終在rmmod test的時候會阻塞,rmmod永不返回了!很顯然是cleanup_module出了問題,因此再寫一個模塊來卸載它!在編譯模塊之前,首先要在/proc/kallsym中找到以下這行:
f88de380 d __this_module [XXXX無法卸載的模塊名稱]
這是因爲modules鏈表沒有被導出,如果被導出的話,正確的方式應該是遍歷這個鏈表來比對模塊名稱的。
以下的模塊加載了以後,上述模塊就可以被rmmod了:
- void force(void)
- {
- }
- int __init rm_init(void)
- {
- struct module *mod = (struct module*)0xf88de380;
- int i;
- int o=0;
- mod->state = MODULE_STATE_LIVE; //爲了卸載能進行下去,也就是避開情況1,將模塊的狀態改變爲LIVE
- mod->exit = force; //由於是模塊的exit導致了無法返回,則替換mod的exit。再次調用rmmod的時候會調用到sys_delete_module,最後會調用 exit回調函數,新的exit當然不會阻塞,成功返回,進而可以free掉module
- for (i = 0; i < NR_CPUS; i++) { //將引用計數歸0
- mod->ref[i].count = *(local_t *)&o;
- }
- return 0;
- }
- void __exit rm_exit(void)
- {
- }
- module_init(rm_init);
- module_exit(rm_exit);
- MODULE_LICENSE("GPL");
然後加載上述模塊後,前面的模塊就可以卸載了。
四.更深入的一些解釋:
針對這個模塊導致的無法卸載的問題,還有另一種方解決式,就是在別的module中complete掉這個completion,這個completion當然是無法直接得到的,還是要通過/proc/kallsyms得到這個completion的地址,然後強制轉換成completion並完成它:
- int __init rm_init(void)
- {
- complete((struct completion *)0xf88de36c);
- return 0;
- }
- void __exit rm_exit(void)
- {
- }
- module_init(rm_init);
- module_exit(rm_exit);
- 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);
其中x就是那個模塊裏面的,既然x已經沒有了(使用替換exit的方式已經成功卸載了模塊,模塊被free,x當然就不復存在了),剛剛被喚醒運行的rmmod就會oops,但是不管怎樣,一個進程的oops一般是沒有問題的,因此還是可以幹掉它的,這種oops一般不會破壞其它的內核數據,一般都是由於引用已經被free的指針引起的(當然還真的可能有危險情況哦...)。 既然知道這些rmmod都是阻塞在睡眠裏面,那麼我們只需要強制喚醒它們就可以了,至於說被喚醒後oops了怎麼辦?由內核處理啦,或者聽天由命!因此考慮以下的代碼:
- int (*try)(task_t * p, unsigned int state, int sync);
- int __init rm_init(void){
- struct task_struct *tsk = find_task_by_pid(28792); //28792爲一個阻塞的rmmod進程,這個模塊使用上述的替換exit的方式已經被重新rmmod卸載了,然而第一次的那個rmmod仍然阻塞在哪裏,沒有睡再去喚醒它了。
- try=0xc011a460;
- (*try)(tsk, TASK_INTERRUPTIBLE|TASK_UNINTERRUPTIBLE, 0); //我們喚醒它,至於它醒了之後幹什麼,隨便吧!
- return 0;
- }
- void __exit rm_exit(void){
- }
- module_init(rm_init);
- module_exit(rm_exit);
- MODULE_LICENSE("GPL");
然後再ps -e一下,基本沒有那個rmmod進程了。一個[State: D (disk sleep)]的進程這樣完蛋了。
以上代碼基本都是硬編碼的地址以及進程號,真正的代碼應該使用參數來傳遞這些信息,就會比較方便了!
既然模塊結構都可以拿到,它的任意字段就可以被任意賦值,哪裏出了問題就重新賦值哪裏!既然內核空間都進入了,導不導出符號就不是根本問題了,就算沒有procfs的kallsym,也一樣能搞定,因爲你能控制整個內存!
五.防刪除:
我們可以在自己的模塊初始化的時候將其引用計數設置成一個比較大的數,或者設置一下別的模塊結構體字段,防治被rmmod,然而別人也可以再寫一個模塊把這些字段設置回去,簡單的使用上述方式就可以幹掉你的防刪除模塊,這是一個矛與盾的問題,關鍵是,別讓人擁有root權限。
六.總結:
代碼都拿到手了,流程還看不懂嗎?流程都懂了,還怕定位不到問題嗎?問題都定位了,還能解決不了嗎?只要沒有人爲因素,事實上任何技術問題都是能解決的( 這是我從公司學習到的最重要的東西 ! ),所謂的不可能只是規範上的規定或者說既然你誤操作了或者你的代碼有bug,與其說去按照上述方式搞定它,還不如不搞定它,而是改正你自己的錯誤!
解決模塊由於阻塞而無法刪除問題有下面的過程:
1.寫一個模塊替換exit函數,且設置引用計數爲0,狀態爲LIVE,然後rmmod;
2.強制try_to_wake_up那個rmmod進程,注意不能使用wake_up,因爲隊列可能已經不在了,而應該直接喚醒task_struct;
3.聽天由命!
附:內核缺頁
在do_page_fault中,如果缺頁發生在內核空間,最終OOPS的話,會調用die:
die("Oops", regs, error_code);
在die中,如果沒有處在中斷以及沒有設置panic-on-oops的話,最終將以SIGSEGV退出當前進程:
if (in_interrupt())
panic("Fatal exception in interrupt");
if (panic_on_oops) {
printk(KERN_EMERG "Fatal exception: panic in 5 seconds/n");
set_cu rrent_state(TASK_UNINTERRUPTIBLE);
schedule_timeout(5 * HZ);
panic("Fatal exception");
}
do_exit(SIGSEGV);
這樣,如果喚醒睡眠在模塊exit中的rmmod,顯然在被喚醒之後,檢測變量會導致缺頁(由於變量已經被free了),因此會進入die("Oops"...),最終退出rmmod進程,這個也是很合理的哦!因此上述的清理D狀態的進程還是可以用的。