Home Linux内核与模块数据交互方式总结
Post
Cancel

Linux内核与模块数据交互方式总结


1. 通过Linux内核与模块之间的接口

定义静态全局变量,通过Linux内核与模块之间的接口,将全局变量的地址传递给kernel

以ixgbe驱动为例:模块中定义了static全局变量 ixgbe_driver:

static struct pci_driver ixgbe_driver = {
	.name     = ixgbe_driver_name,
	.id_table = ixgbe_pci_tbl,
	.probe    = ixgbe_probe,
	.remove   = __devexit_p(ixgbe_remove),
#ifdef CONFIG_PM
#ifndef USE_LEGACY_PM_SUPPORT
	.driver = {
		.pm = &ixgbe_pm_ops,
	},
#else
	.suspend  = ixgbe_suspend,
	.resume   = ixgbe_resume,
#endif /* USE_LEGACY_PM_SUPPORT */
#endif
#ifndef USE_REBOOT_NOTIFIER
	.shutdown = ixgbe_shutdown,
#endif
#ifdef HAVE_SRIOV_CONFIGURE
	.sriov_configure = ixgbe_pci_sriov_configure,
#endif
#ifdef HAVE_PCI_ERS
	.err_handler = &ixgbe_err_handler
#endif
};

在module_init中,通过pci_register_driver向PCI subsystem注册该驱动,将上述全局变量的地址传给subsystem。

pci_register_driver实际上是一个宏,调用的真正函数为__pci_register_driver:

#define pci_register_driver(driver)
			__pci_register_driver(driver, THIS_MODULE, KBUILD_MODNAME)

在该函数中,初始化driver的一系列字段,并向总线注册device_driver:

int __pci_register_driver(struct pci_driver *drv, struct module *owner,
			  const char *mod_name)
{
	/* initialize common driver fields */
	drv->driver.name = drv->name;
	drv->driver.bus = &pci_bus_type;
	drv->driver.owner = owner;
	drv->driver.mod_name = mod_name;
	drv->driver.groups = drv->groups;
	drv->driver.dev_groups = drv->dev_groups;

	spin_lock_init(&drv->dynids.lock);
	INIT_LIST_HEAD(&drv->dynids.list);

	/* register with core */
	return driver_register(&drv->driver);
}
EXPORT_SYMBOL(__pci_register_driver);

2.向kernel传递一个函数指针,通过函数指针来修改模块内部的数据

在上述ixgbe_driver的定义中,将probe字段设置为ixgbe_probe函数,其定义如下:

static int __devinit ixgbe_probe(struct pci_dev *pdev,
				 const struct pci_device_id __always_unused *ent)

3. EXPORT_SYMBOL/EXPORT_SYMBOL_GPT导出全局变量

二者的区别在于后者仅支持MODULE_LICENSE为GPT的模块

默认情况下,模块与模块之间、模块与内核之间的全局变量是相互独立的,只有通过EXPORT_SYMBOL将模块导出才能对其他模块或内核可见。

首先介绍一下Linux kallsyms:内核符号表,其中会列出所有的Linux内核中的导出符号,在用户态下可以通过/proc/kallsyms访问,此时由于内核保护,看到的地址为0x0000000000000000,在root模式下可以看到真实地址。启用kallsyms需要编译内核时设置CONFIG_KALLSYMS为y。

Linux模块在编译时符号的查找顺序:

  1. 在本模块中符号表中,寻找符号(函数或变量实现)

  2. 在内核全局符号表中寻找

  3. 在模块目录下的Module.symvers文件中寻找

下面我们编写两个模块对EXPORT_SYMBOL的作用加以说明。

在通过内核模块定义一个名为_exported_symbol的全局变量前,首先查看kallsyms,此时并不存在这一符号:

image-20230302172342530.png

创建一个新的内核模块,并定义:

//mod.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

int _exported_symbol = 0;
// EXPORT_SYMBOL(_exported_symbol);

...

编译并插入该模块,查看内核符号表,发现出现了地址在0xffffffffc09c0380,类型为b的名为_exported_symbol的符号:

image-20230302172442426.png

而编译产生的Module.symvers为空。在另一模块中通过extern链接此符号,并将上述Module.symvers拷贝到该目录下:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

extern int _exported_symbol;
...

编译报错,该模块无法访问_exported_symbol这一符号:

image-20230302172736039.png

当在第一个模块中加入EXPORT_SYMBOL(_exported_symbol)这一语句,编译并重新插入后,再次查看kallsyms,发现其类型变为B:

image-20230302172926489.png

其编译产生的Module.symvers为:

image-20230303093159389.png

此时再次编译模块2,成功编译。

在不插入模块1的情况下插入模块2,会出现Unknown symbol in module,原因在于此时内核全局符号表中不存在该符号:

image-20230303093521233.png

先插入模块1,再插入模块2,通过dmesg查看_exported_symbol的变化,此时模块1中定义的exported_symbol成功被模块2使用并修改:

image-20230303093818063.png

将mod1中的EXPORT_SYMBOL注释掉,插入模块1,此时全局符号表中的符号类型为b,插入模块2报错,无法访问模块1中的符号:

image-20230303095036846.png

原因在于,模块类型为小写字母b表示局部引用,定义在BSS,只能在模块内访问。模块类型为大写字母B表示全局引用,可以在模块外访问,其他类型类似。

Reference

1.intersvyaz/ixgbe: ixgbe driver mirror (github.com)

2.Inter-module communication in Linux kernel - Stack Overflow

3.c - How to call exported kernel module functions from another module? - Stack Overflow

This post is licensed under CC BY 4.0 by the author.