先介紹一下創建線程的大致流程:在模塊的初始化函數中,調用netlink_kernel_create來注冊自己的netlink協議,然後返回,接收netlink消息的函數為fcluster_netlink_recv,真正的初始化操作是在接收到netlink報文後才做,創建線程的操作也是在fcluster_netlink_recv中,如下圖所示: 調用kernel_thread的語句為: [cpp] kernel_thread(fcluster_rcv_handoff, NULL, 0); 按照這樣的處理流程,在卸載模塊時會提示 "module is in use".通過lsmod命令查看自己的模塊,發現引用數為1(模塊引用數為0時才能卸載),但是卻沒有看到是哪個模塊在引用。經過多次的重復測試,發現問題就出現在內核線程的創建上。因為如果把kernel_thread的調用放在fcluster_init函數中,這樣模塊的引用數為0,模塊就可以卸載。現在就可以判斷,出現模塊無法卸載的原因就是調用kernel_thread創建內核線程的時機不對,或者指定的參數不對。 為了定位問題,開始在不同的情況下增加調試代碼來查找問題。 第一個測試是kernel_thread放在fcluster_init的情況下,在fcluster_rcv_handoff(內核線程的處理函數)、fcluster_init、fcluster_netlink_recv中都加了打印模塊引用計數的代碼,發現這三個地方的引用計數分別為0、1、1,在這種情況下可以卸載模塊。 第二個測試是kernel_thread放在fcluster_netlink_recv的情況下,在fcluster_rcv_handoff(內核線程的處理函數)、fcluster_init、fcluster_netlink_recv中都加了打印模塊引用計數的代碼,發現這三個地方的引用計數分別為1、1、1,在這種情況下不可以卸載模塊。 這樣就很奇怪了,為什麼fcluster_init、fcluster_netlink_recv中引用計數都為1,但是在不同的函數中創建內核線程,模塊的引用計數為不同呢?也就是說在fcluster_init中創建內核線程並返回後,模塊的引用計數會變為0,但是在fcluster_netlink_recv中創建內核線程並返回後,模塊的引用計數仍然為1,想找到問題的原因,源碼是最好的工具,所以立馬去內核源碼中看load_module函數、do_fork函數、netlink_sendmsg函數。為什麼要看幾個函數?因為fcluster_init函數的調用肯定是在模塊的加載過程中,而加載模塊的系統調用sys_init_module函數中主要是調用load_module來完成的;而kernel_thread主要是調用do_fork來創建的內核線程;netlink_sendmsg是netlink報文的發送函數,這個函數會調用到fcluster_netlink_recv函數(這個函數注冊到netlink_sock類型的netlink_rcv成員。 首先從load_module入手,通過艱苦的啃代碼過程,終於找到在load_module調用的module_unload_init中將模塊的引用計數設置為1,在初始化過程中保持對模塊的引用,防止卸載模塊的操作(這個幾乎不太可能,但是模塊的初始化函數有可能睡眠)。在load_module完成艱苦的模塊加載工作後(/* Do all the hard work */),sys_init_module中會通過do_one_initcall函數來調用模塊提供的初始化函數,所以在fcluster_init(我的模塊的初始化函數)中模塊的引用計數會為0,但是在什麼時候模塊的引用計數會變為0呢?還是在sys_init_module中,調用module_put來減少模塊的引用計數,所以如果kernel_thread的調用放在fcluster_init時,不會造成模塊的引用計數在fcluster_init返回模塊加載完成後仍為1,這個通過測試的結果也可以看出,但是還不充分,因為還不知道do_fork函數會不會因為調用時機的問題造成模塊的引用計數保持為1。 接下來開始看netlink_sendmsg函數,這個過程就比較簡單了,大致的調用流程如下圖所示: 在這個流程中並沒有對模塊引用計數加1的操作,可以判斷出在調用netlink_sendmsg之前模塊的引用計數已經為1了,那只能是在用戶層創建套接字的時候了,看看netlink_create函數,果然是在這個函數中,部分代碼如下: [cpp] if (nl_table[protocol].registered && try_module_get(nl_table[protocol].module)) module = nl_table[protocol].module; 其中的module就是我的內核模塊,具體的可以參見netlink_kernel_create函數。對我的模塊引用的釋放,要到該套接字釋放的時候才會發生,所以問題的原因就在於用戶層創建的套接字沒有被釋放,參見netlink_release函數,至此已經基本可以找到問題的原因了。 用戶層在調用socket函數創建我提供的netlink協議類型的套接字時,會調用到netlink_create函數,進而對我注冊的模塊的引用計數加1。當用戶層調用sendmsg發送netlink報文時,會調用到netlink_sendmsg,進而會調用到fcluster_netlink_recv函數,如果這時我在fcluster_netlink_recv中調用kernel_thread,會根據當前的用戶進程來創建內核線程,而當前的用戶進程打開的文件包括創建的套接字(這個套接字創建的時候會對我的模塊的引用計數加1),並且創建內核線程時指定的標志參數為0,這樣新創建的內核線程會導致對這個套接字的引用計數加1(參見copy_files函數)。當用戶層關閉套接字時,因為該套接字的引用計數不為0,這個套接字也就不會釋放,也就不會減少對我的模塊的引用計數,導致模塊無法卸載。 為了驗證我的推理,通過"lsof -p 5599"(5599為內核線程的id)來驗證: 下面是內核模塊可以卸載時的情況: 下面是內核模塊無法卸載時的情況: 從兩個圖中可以看出,無法卸載時最後多了一個sock類型的一行。 怎麼解決這個問題呢?要麼是在新創建的內核線程中關閉所有的打開的文件,要麼是在創建內核線程時指定CLONE_FILES標志,或者在內核線程的處理函數中調用daemonize函數 #define CLONE_FILES 0x00000400 /* set if open files shared between processes */ 至於為什麼調用kernel_thread時指定CLONE_FILES可以解決這個問題,參見copy_files函數。 解決這個問題我也花了不少時間,但是通過這個過程,了解了模塊的加載過程,netlink報文的發送和接收、及netlink套接字的創建、內核線程的創建等,所以還是受益匪淺啊。等有時間了,我會把load_module、netlink相關的分析整理一下,貼上來,跟大家分享一下。這個問題要描述的話很繞,很難說清。所以如果有什麼疑問的話,可以交流一下。