有没有一种方法可以找出是什么在使用Linux内核模块?

82

假设我加载了一个内核模块并用 lsmod 命令列出了已加载的模块,我可以得到该模块的“使用计数”(即引用该模块的其他模块数量)。但是,有没有办法找出究竟是哪个模块在使用这个模块呢?

问题在于我正在开发的一个模块坚持认为它的使用计数为1,因此我无法使用 rmmod 命令卸载它,但它的“by”列是空的。这意味着每次我想要重新编译和重新加载该模块时,都必须重启机器(或者至少我找不到其他方法来卸载它)。


在哪些方面?什麼代碼?什麼模塊?什麼用戶?什麼程序?不過我稍微感覺這不是與編程有關:) 還是很有趣的。 - Johannes Schaub - litb
2
嗯,这确实与编程有关,因为我在询问是因为我正在编写内核模块。 - mipadi
请澄清问题,以展示您正在尝试解决的编程问题。 - Norman Ramsey
15
这个问题对我来说非常清晰,Norman:他该如何找出是什么原因阻止rmmod删除他的实验模块?他该如何避免每次编译新版本时都必须重新启动系统? - Die in Sente
7个回答

54

实际上,似乎有一种方法可以列出声称拥有模块/驱动程序的进程 - 但是,我没有看到它被广告(除了Linux内核文档外),所以我在这里记录下我的笔记:

首先,非常感谢@haggai_e的回答;指向函数try_module_gettry_module_put作为管理使用计数(refcount)的负责人的指针是允许我跟踪该过程的关键。

进一步寻找在线上的信息时,我不知何故偶然发现了文章Linux-Kernel Archive: [PATCH 1/2] tracing: Reduce overhead of module tracepoints;最终指向内核中存在的一个设施,即“跟踪”(我猜测),其文档位于目录Documentation/trace-Linux kernel source tree中。特别是,两个文件解释了跟踪设施:events.txtftrace.txt

但是,在运行中的Linux系统中还有一个简短的“跟踪mini-HOWTO”,位于/sys/kernel/debug/tracing/README中(请参见我真的很累,人们总是说没有文档……);请注意,在内核源树中,此文件实际上是由文件kernel/trace/trace.c生成的。我在Ubuntu natty上测试过这个,需要注意的是,由于/sys是由root拥有的,因此您必须使用sudo读取此文件,例如sudo cat

sudo less /sys/kernel/debug/tracing/README

......对于几乎所有在 /sys 下进行的操作都适用,这里将对其进行描述。


首先,这是一个简单的最小模块/驱动程序代码(我从参考资源中整理而来),它仅创建一个 /proc/testmod-sample 文件节点,在读取时返回字符串 "This is testmod.";这是 testmod.c

/*
https://github.com/spotify/linux/blob/master/samples/tracepoints/tracepoint-sample.c
https://www.linux.com/learn/linux-training/37985-the-kernel-newbie-corner-kernel-debugging-using-proc-qsequenceq-files-part-1
*/

#include <linux/module.h>
#include <linux/sched.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h> // for sequence files

struct proc_dir_entry *pentry_sample;

char *defaultOutput = "This is testmod.";


static int my_show(struct seq_file *m, void *v)
{
  seq_printf(m, "%s\n", defaultOutput);
  return 0;
}

static int my_open(struct inode *inode, struct file *file)
{
  return single_open(file, my_show, NULL);
}

static const struct file_operations mark_ops = {
  .owner    = THIS_MODULE,
  .open = my_open,
  .read = seq_read,
  .llseek   = seq_lseek,
  .release  = single_release,
};


static int __init sample_init(void)
{
  printk(KERN_ALERT "sample init\n");
  pentry_sample = proc_create(
    "testmod-sample", 0444, NULL, &mark_ops);
  if (!pentry_sample)
    return -EPERM;
  return 0;
}

static void __exit sample_exit(void)
{
    printk(KERN_ALERT "sample exit\n");
    remove_proc_entry("testmod-sample", NULL);
}

module_init(sample_init);
module_exit(sample_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mathieu Desnoyers et al.");
MODULE_DESCRIPTION("based on Tracepoint sample");

可以使用以下Makefile来构建此模块(将其放置在与testmod.c相同的目录中,然后在该目录中运行make):

CONFIG_MODULE_FORCE_UNLOAD=y
# for oprofile
DEBUG_INFO=y
EXTRA_CFLAGS=-g -O0

obj-m += testmod.o

# mind the tab characters needed at start here:
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

当构建此模块/驱动程序时,输出的是一个内核对象文件testmod.ko


此时,我们可以准备与try_module_gettry_module_put有关的事件跟踪,它们在/sys/kernel/debug/tracing/events/module中:

$ sudo ls /sys/kernel/debug/tracing/events/module
enable  filter  module_free  module_get  module_load  module_put  module_request

请注意,在我的系统上,默认情况下启用跟踪功能:

$ sudo cat /sys/kernel/debug/tracing/tracing_enabled
1

...然而,模块跟踪(具体来说)并不是:

$ sudo cat /sys/kernel/debug/tracing/events/module/enable
0

我们首先需要创建一个过滤器,能够响应module_getmodule_put等事件,但只针对testmod模块。为此,我们首先要检查事件的格式:

$ sudo cat /sys/kernel/debug/tracing/events/module/module_put/format
name: module_put
ID: 312
format:
...
    field:__data_loc char[] name;   offset:20;  size:4; signed:1;

print fmt: "%s call_site=%pf refcnt=%d", __get_str(name), (void *)REC->ip, REC->refcnt

这里可以看到有一个名为name的字段,它保存了驱动程序的名称,我们可以根据它进行过滤。要创建一个过滤器,只需将过滤字符串echo到相应的文件中即可:

sudo bash -c "echo name == testmod > /sys/kernel/debug/tracing/events/module/filter"

首先请注意,由于我们必须调用sudo,因此我们必须将整个echo重定向作为sudo命令的参数包含在一个bash中。其次,请注意,由于我们写入了“父”module/filter,而不是特定事件(例如module/module_put/filter等),因此此过滤器将应用于列在module目录下的所有“子级”事件。

最后,我们启用模块跟踪:

sudo bash -c "echo 1 > /sys/kernel/debug/tracing/events/module/enable"

从现在开始,我们可以阅读跟踪日志文件;对我而言,通过阅读阻塞的“管道化”版本的跟踪文件来进行阅读是有效的 - 就像这样:

sudo cat /sys/kernel/debug/tracing/trace_pipe | tee tracelog.txt

在这个阶段,日志中将不会看到任何内容 - 因此现在是加载(并使用和删除)驱动程序的时候了(在与trace_pipe被读取的终端不同的终端中):

$ sudo insmod ./testmod.ko
$ cat /proc/testmod-sample 
This is testmod.
$ sudo rmmod testmod

如果我们返回到正在读取trace_pipe的终端,我们应该能看到如下内容:

# tracer: nop
#
#           TASK-PID    CPU#    TIMESTAMP  FUNCTION
#              | |       |          |         |
          insmod-21137 [001] 28038.101509: module_load: testmod
          insmod-21137 [001] 28038.103904: module_put: testmod call_site=sys_init_module refcnt=2
           rmmod-21354 [000] 28080.244448: module_free: testmod

那基本上就是我们针对testmod驱动程序所能获得的全部内容了——引用计数仅在加载(insmod)或卸载(rmmod)驱动程序时更改,而不会在通过cat读取时更改。 因此,我们可以在终端中使用CTRL+C中断从trace_pipe读取的内容;要完全停止跟踪:

sudo bash -c "echo 0 > /sys/kernel/debug/tracing/tracing_enabled"

需要注意的是,大多数示例都是读取文件/sys/kernel/debug/tracing/trace,而不是这里的trace_pipe。然而,一个问题是这个文件不是用来“管道传输”的(所以你不应该对这个trace文件运行tail -f),相反,你应该在每次操作之后重新读取trace。在第一次insmod之后,从cat命令读取tracetrace_pipe会得到相同的输出。但是,在rmmod之后,读取trace文件会给出如下结果:

   <...>-21137 [001] 28038.101509: module_load: testmod
   <...>-21137 [001] 28038.103904: module_put: testmod call_site=sys_init_module refcnt=2
   rmmod-21354 [000] 28080.244448: module_free: testmod

...这意味着,在此时刻,insmod已经退出很久了,因此在进程列表中不存在 - 因此无法通过记录的进程ID(PID)找到它 - 因此我们得到一个空的<...>作为进程名称。因此,在这种情况下最好记录(通过tee)从trace_pipe运行的输出。另外,请注意,要清除/重置/擦除trace文件,只需将0写入其中:

sudo bash -c "echo 0 > /sys/kernel/debug/tracing/trace"

如果这看起来有些违反直觉,要注意的是trace是一个特殊文件,并且无论如何都会报告文件大小为零:

$ sudo ls -la /sys/kernel/debug/tracing/trace
-rw-r--r-- 1 root root 0 2013-03-19 06:39 /sys/kernel/debug/tracing/trace

即使它是"完整的",也要截取。

最后,请注意,如果我们没有实现过滤器,我们将获得运行系统上所有模块调用的日志-这将记录任何调用(包括后台)到grep等的日志,因为这些使用binfmt_misc模块:

...
  tr-6232  [001] 25149.815373: module_put: binfmt_misc call_site=search_binary_handler refcnt=133194
..
  grep-6231  [001] 25149.816923: module_put: binfmt_misc call_site=search_binary_handler refcnt=133196
..
  cut-6233  [000] 25149.817842: module_put: binfmt_misc call_site=search_binary_handler refcnt=129669
..
  sudo-6234  [001] 25150.289519: module_put: binfmt_misc call_site=search_binary_handler refcnt=133198
..
  tail-6235  [000] 25150.316002: module_put: binfmt_misc call_site=search_binary_handler refcnt=129671

...这会增加相当大的开销(在日志数据量和生成所需的处理时间方面)。


在查找时,我偶然发现了Debugging Linux Kernel by Ftrace PDF,其中提到了一个名为trace-cmd的工具,它基本上与上面的工具类似,但通过更简单的命令行界面来实现。还有一个名为trace-cmd的“前端阅读器”GUI,称为KernelShark;这两个工具也可以通过sudo apt-get install trace-cmd kernelshark在Debian/Ubuntu存储库中获取。这些工具可能是上述过程的替代方法。

最后,我想指出的是,虽然上面的testmod示例并没有真正展示多个声明的上下文用法,但我已经使用相同的跟踪过程发现我正在编写的USB模块在插入USB设备时被pulseaudio重复声明 - 因此该过程似乎适用于此类用例。


5
感谢您的评论,@RichardHansen - 问题是“有没有一种方法可以确定哪个模块正在使用”,您可以在模块跟踪中看到,例如rmmod-21354tr-6232(进程名称 - 进程ID)正在执行 module_put,也就是说,它们正在更改模块的引用计数 - 也就是说,这些进程正在“使用”该模块。因此,我认为它正好回答了OP所问的问题...干杯! - sdaau
1
“我非常疲倦”的链接已经损坏。 - Hi-Angel

8

4

你所得到的只是一个列出哪些模块依赖于其他模块的列表(在lsmod中的“使用者”列)。如果要知道为什么加载了该模块,它是否仍然需要,以及如果卸载它和所有依赖它的模块会导致什么问题,就不能编写程序进行判断。


3
你可以尝试使用 lsof 或者 fuser 命令。

1
我应该如何使用lsof来实现这个功能? - Vadim Kotov
2
这并不是通用的解决方案,但如果驱动程序在/dev中创建设备,则可以通过lsof列出它。 - SergeyM

2
如果您使用rmmod而没有使用--force选项,它将告诉您哪个模块正在被使用。例如:
$ lsmod | grep firewire
firewire_ohci          24695  0 
firewire_core          50151  1 firewire_ohci
crc_itu_t               1717  1 firewire_core

$ sudo modprobe -r firewire-core
FATAL: Module firewire_core is in use.

$ sudo rmmod firewire_core
ERROR: Module firewire_core is in use by firewire_ohci

$ sudo modprobe -r firewire-ohci
$ sudo modprobe -r firewire-core
$ lsmod | grep firewire
$

5
一般来说,这是不正确的:我在我的机器上运行了以下命令: $ lsmod | grep snd ,结果如下:snd_seq 47263 1 snd_timer 19130 1 snd_seq snd_seq_device 5100 1 snd_seq ...因此snd_seq被某些东西所占用(引用计数为1),但不能确定原因,因为它后面的列为空,没有其他模块特别声明占用它(但也许如果从启动过程开始跟踪内核,就可以找出原因,我猜测)。 - sdaau
6
只有当lsmod在“used by”列中也显示一些内容时,这才有效;rmmod在显示依赖关系方面并没有比lsmod更多的逻辑。 - dannysauer

0
尝试使用kgdb并在您的模块上设置断点。

-4

对于那些急于弄清楚为什么无法重新加载模块的人,我通过以下方式解决了这个问题:

  • 使用“modinfo”获取当前使用模块的路径
  • 删除该路径下的模块(rm -rf)
  • 将要加载的新模块复制到原路径中
  • 输入“modprobe DRIVER_NAME.ko”。

2
这个回答实际上并没有回答所问的问题。 - kelnos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接