当前位置:编程学习 > C/C++ >>

内核模块中错误调用kernel_thread创建内核线程导致模块无法卸载

 先介绍一下创建线程的大致流程:在模块的初始化函数中,调用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相关的分析整理一下,贴上来,跟大家分享一下。这个问题要描述的话很绕,很难说清。所以如果有什么疑问的话,可以交流一下。
 
补充:软件开发 , C++ ,
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,