Skip to content

Latest commit

 

History

History
687 lines (503 loc) · 30 KB

File metadata and controls

687 lines (503 loc) · 30 KB

二、内核内部一览

简单的操作系统(如 MS-DOS)总是在单 CPU 模式下执行,但类似 Unix 的操作系统使用双模式来有效地实现分时和资源分配与保护。在 Linux 中的任何时候,CPU 要么运行在可信的内核模式(在这里我们可以做任何我们想做的事情),要么运行在受限的用户模式(在这里某些操作是不允许的)。所有用户进程都在用户模式下执行,而核心内核本身和大多数设备驱动(在用户空间中实现的驱动除外)在内核模式下运行,因此它们可以不受限制地访问整个处理器指令集以及全部内存和输入/输出空间。

当用户模式进程需要访问外设时,它不能自己完成,而是必须通过系统调用通过设备驱动或其他内核模式代码来引导请求,系统调用在控制进程活动和管理数据交换方面起着主要作用。在本章中,我们还不会看到系统调用(它们将在第 3 章中介绍)使用 Char Drivers 进行工作,但是我们将通过直接向内核的源代码中添加新代码或使用内核模块来开始向内核中编程,这是另一种更通用的向内核添加代码的方式。

一旦我们开始编写内核代码,我们一定不要忘记,在用户模式下,每一个资源分配(CPU、RAM 等)都是由内核自动管理的(当进程死亡时,内核可以正确释放它们),在内核模式下,我们被允许独占处理器,直到我们自愿放弃 CPU 或者发生中断或异常;此外,如果没有正确释放,每个请求的资源(例如内存)都会丢失。这就是为什么正确管理 CPU 使用和释放我们请求的任何资源非常重要!

现在,是时候进行第一次内核跳转了,因此在本章中,我们将介绍以下食谱:

  • 向源中添加自定义代码
  • 使用内核消息
  • 使用内核模块
  • 使用模块参数

技术要求

在本章中,我们需要在第 1 章中已经下载的配置和构建内核配方中的内核源,安装开发系统,当然,我们还需要安装我们的交叉编译器,如第 1 章安装开发系统中的设置主机配方所示。本章使用的代码和其他文件可以从 GitHub 下载,网址为https://GitHub . com/giometti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 02

向源中添加自定义代码

作为第一步,让我们看看如何向内核源代码中添加一些简单的代码。在本食谱中,我们将简单地添加愚蠢的代码,只是为了演示它有多容易,但是在本书的后面,我们将添加更复杂的代码。

准备好

因为我们需要将代码添加到 Linux 源代码中,所以让我们进入所有源代码所在的目录。在我的系统上,我使用位于我的主目录中的Projects/ldddc/linux/路径。以下是内核源代码的样子:

$ cd Projects/ldddc/linux/
$ ls
arch        Documentation  Kbuild       mm               scripts   virt
block       drivers        Kconfig      modules.builtin  security  vmlinux
built-in.a  firmware       kernel       modules.order    sound     vmlinux.o
certs       fs             lib          Module.symvers   stNXtP40
COPYING     include        LICENSES     net System.map
CREDITS     init           MAINTAINERS  README tools
crypto      ipc            Makefile     samples usr

现在,我们需要设置环境变量ARCHCROSS_COMPILE,如下所示,以便能够交叉编译 ESPRESSObin 的代码:

$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-linux-gnu-

因此,如果我们尝试执行如下的make命令,系统应该像往常一样开始编译内核:

$ make Image dtbs modules
  CALL scripts/checksyscalls.sh
...

Note that you may avoid exporting preceding variables by just specifying them on the following command line: $ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \ Image dtbs modules

此时,内核源代码和编译环境已经准备好了。

怎么做...

让我们看看如何通过以下步骤来实现:

  1. 既然这本书谈到了设备驱动,让我们从将代码添加到 Linux 源代码的drivers目录下开始,特别是在drivers/misc中,这里有各种各样的驱动。我们应该在drivers/misc中放置一个名为dummy-code.c的文件,内容如下:
/*
 * Dummy code
 */

#include <linux/module.h>

static int __init dummy_code_init(void)
{
    printk(KERN_INFO "dummy-code loaded\n");
    return 0;
}

static void __exit dummy_code_exit(void)
{
    printk(KERN_INFO "dummy-code unloaded\n");
}

module_init(dummy_code_init);
module_exit(dummy_code_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Rodolfo Giometti");
MODULE_DESCRIPTION("Dummy code");
  1. 如果我们不把我们的新文件drivers/misc/dummy-code.c正确地插入到内核配置和构建系统中,它将没有任何作用。为此,我们必须修改drivers/misc/Kconfigdrivers/misc/Makefile文件,如下所示。必须更改以前的文件,如下所示:
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -527,4 +527,10 @@ source "drivers/misc/echo/Kconfig"
 source "drivers/misc/cxl/Kconfig"
 source "drivers/misc/ocxl/Kconfig"
 source "drivers/misc/cardreader/Kconfig"
+
+config DUMMY_CODE
+       tristate "Dummy code"
+       default n
+       ---help---
+         This module is just for demonstration purposes.
 endmenu

后者的修改如下:

--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -58,3 +58,4 @@ obj-$(CONFIG_ASPEED_LPC_SNOOP) += aspeed-lpc-snoop.o
 obj-$(CONFIG_PCI_ENDPOINT_TEST) += pci_endpoint_test.o
 obj-$(CONFIG_OCXL) += ocxl/
 obj-$(CONFIG_MISC_RTSX) += cardreader/
+obj-$(CONFIG_DUMMY_CODE) += dummy-code.o

Note that you can easily add the preceding code and whatever is needed to compile it by just using the patch command, as follows, in your main directory of Linux sources: $ patch -p1 < add_custom_code.patch

  1. 嗯,如果我们现在使用make menuconfig命令,并通过设备驱动导航到杂项设备菜单项的底部,我们应该会得到如下截图所示的内容:

在前面的截图中,我已经选择了虚拟代码条目,这样我们就可以看到最终的设置应该是什么样子。

Note that the Dummy code entry must be selected as built-in ( the * character) and not as module (the M character). Note also that, if we do not execute the make menuconfig command and we execute directly the make Image command to compile the kernel, then the building system will ask us what to do with the DUMMY_CODE setting, as shown in the following. Obviously, we have to answer yes by using the y character: $ make Image scripts/kconfig/conf --syncconfig Kconfig * * Restart config... * * * Misc devices * Analog Devices Digital Potentiometers (AD525X_DPOT) [N/m/y/?] n ... Dummy code (DUMMY_CODE) [N/m/y/?] (NEW) y

  1. 如果一切正常,那么我们执行make Image命令重新编译内核。我们应该看到我们的新文件被编译,然后被添加到内核Image文件中,如下所示:
$ make Image
scripts/kconfig/conf --syncconfig Kconfig
...
  CC drivers/misc/dummy-code.o
  AR drivers/misc/built-in.a
  AR drivers/built-in.a
...
  LD vmlinux
  SORTEX vmlinux
  SYSMAP System.map
  OBJCOPY arch/arm64/boot/Image
  1. 好了,现在我们要做的就是用刚刚重建的文件替换 microSD 上的Image文件,然后重启系统(参见第 1 章安装开发系统中的如何添加内核食谱)。

它是如何工作的...

现在,是时候看看前面的步骤是如何工作的了。在接下来的章节中,我们将更好地解释这段代码的真正作用。然而,此刻,我们应该注意到以下几点。

第一步中,注意到对module_init()module_exit()的调用,这是内核提供的 C 宏,用来告诉内核,在系统启动或关机的时候,它必须调用我们提供的函数,命名为dummy_code_init()dummy_code_exit(),它们反过来只是打印一些信息消息。

在本章的稍后部分,我们将详细了解printk()的作用以及KERN_INFO宏的含义,但是目前,我们应该只考虑它们用于在引导(或关机)期间打印消息。例如,前面的代码指示内核打印出在引导阶段某个时候加载的消息虚拟代码。

第二步中,在Makefile中,我们只是简单的告诉内核如果CONFIG_DUMMY_CODE已经被启用(也就是CONFIG_DUMMY_CODE=y,那么dummy-code.c必须被编译并插入到内核二进制(链接)中,而有了Kconfig文件,我们只是将新模块添加到内核配置系统中。

第 3 步中,我们使用make menuconfig命令来编译我们的代码。

步骤 4 中,最后,我们重新编译了内核,以便在其中添加我们的代码。

步骤 5 中,在引导期间,我们应该会看到以下内核消息:

...
loop: module loaded
dummy-code loaded
ahci-mvebu d00e0000.sata: AHCI 0001.0300 32 slots 1 ports 6 Gbps
...

请参见

  • 有关内核配置及其构建系统如何工作的更多信息,我们可以查看以下文件内核源代码中的内核文档文件:linux/Documentation/kbuild/kconfig-macro-language.txt

使用内核消息

如前所述,如果我们需要从头开始设置系统,串行控制台非常有用,但是如果我们希望在内核消息生成后立即看到它们,串行控制台也非常有用。为了生成内核消息,我们可以使用几个函数,在本食谱中,我们将了解它们以及如何在串行控制台或 SSH 连接上显示消息。

准备好了

我们的 ESPRESSObin 是生成内核消息的系统,所以我们需要一个到它的连接。通过串行控制台,这些消息一到达就自动显示,但是如果我们使用 SSH 连接,我们仍然可以通过读取特定文件来显示它们,如下命令所示:

# tail -f /var/log/kern.log

然而,串行控制台值得特别注意:事实上,在我们的示例中,当且仅当/proc/sys/kernel/printk文件中最左边的数字恰好大于 7 时,内核消息将自动显示在串行控制台上,如下所示:

# cat /proc/sys/kernel/printk
10      4       1       7

这些神奇的数字有明确的含义;特别是,第一个表示内核必须在串行控制台上显示的错误消息级别。这些级别在linux/include/linux/kern_levels.h文件中定义如下:

#define KERN_EMERG KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT KERN_SOH "1"    /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2"     /* critical conditions */
#define KERN_ERR KERN_SOH "3"      /* error conditions */
#define KERN_WARNING KERN_SOH "4"  /* warning conditions */
#define KERN_NOTICE KERN_SOH "5"   /* normal but significant condition */
#define KERN_INFO KERN_SOH "6"     /* informational */
#define KERN_DEBUG KERN_SOH "7"    /* debug-level messages */

例如,如果前一个文件的内容是 4,如下所述,只有具有KERN_EMERGKERN_ALERTKERN_CRITKERN_ERR级别的消息会自动显示在串行控制台上:

# cat /proc/sys/kernel/printk
4       4       1       7

为了允许显示所有消息、它们的子集或者不显示任何消息,我们必须使用echo命令修改/proc/sys/kernel/printk文件最左边的数字,如下例所示,我们的操作方式是完全禁用所有内核消息的打印。这是因为任何消息的优先级都不能大于 0:

 # echo 0 > /proc/sys/kernel/printk

Kernel message priorities start from 0 (the highest) and go up to 7 (the lowest)!

既然我们知道了如何显示内核消息,我们可以尝试对内核代码进行一些修改,以便对内核消息进行一些实验。

怎么做...

在前面的例子中,我们看到我们可以使用printk()函数来生成内核消息,但是为了拥有更高效的消息和紧凑可读的代码,我们可以使用其他函数来代替printk():

  1. 使用下列宏(如include/linux/printk.h文件中所定义的),如下所示:
#define pr_emerg(fmt, ...) \
        printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert(fmt, ...) \
        printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit(fmt, ...) \
        printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err(fmt, ...) \
        printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warning(fmt, ...) \
        printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn pr_warning
#define pr_notice(fmt, ...) \
        printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) \
        printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
  1. 现在,要生成内核消息,我们可以做以下工作:查看这些定义,我们可以将前面示例中的dummy_code_init()dummy_code_exit()函数重写到dummy-code.c文件中,如下所示:
static int __init dummy_code_init(void)
{
        pr_info("dummy-code loaded\n");
        return 0;
}

static void __exit dummy_code_exit(void)
{
        pr_info("dummy-code unloaded\n");
}

它是如何工作的...

如果我们仔细观察前面的打印函数(pr_info()和类似的函数),我们注意到它们也依赖于pr_fmt(fmt)参数,该参数可用于将其他有用的信息添加到我们的消息中。例如,以下定义通过添加当前模块和调用函数名来改变pr_info()生成的所有消息:

#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__

Note that the pr_fmt() macro definition must appear at the start of the file, even before the includes, to have any effect.

如果我们将这一行添加到我们的dummy-code.c中,如下面的代码块所示,内核消息将如所述发生变化:

/*
 * Dummy code
 */

#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__
#include <linux/module.h>

事实上,当pr_info()函数被执行时,输出消息,告诉我们模块已经被插入,以下面的形式轮流出现,在这里我们可以看到模块名称和调用函数名称,后面是加载消息:

dummy_code:dummy_code_init: dummy-code loaded

还有一组打印功能,但是在开始谈论它们之前,我们需要一些位于第 3 章使用设备树 中的信息,因此,目前,我们将只继续使用这些功能。

还有更多...

这里有许多内核活动,其中许多非常复杂,通常,一个内核开发人员必须处理几条消息,但并不是所有消息都有趣;所以,我们需要找到一些方法来过滤掉有趣的信息。

过滤内核消息

假设我们希望知道在引导期间检测到了哪些串行端口。我们知道我们可以使用tail命令,但是通过使用它,我们只能看到最新的消息;另一方面,我们可以使用cat命令来调用自引导以来的所有内核消息,但这是大量的信息!或者,我们可以使用以下步骤过滤内核消息:

  1. 这里,我们使用如下grep命令过滤掉uart(或UART)字符串中的行:
# cat /var/log/kern.log | grep -i uart
Feb 7 19:33:14 espressobin kernel: [ 0.000000] earlycon: ar3700_uart0 at MMIO 0x00000000d0012000 (options '')
Feb 7 19:33:14 espressobin kernel: [ 0.000000] bootconsole [ar3700_uart0] enabled
Feb 7 19:33:14 espressobin kernel: [ 0.000000] Kernel command line: console=ttyMV0,115200 earlycon=ar3700_uart,0xd0012000 loglevel=0 debug root=/dev/mmcblk0p1 rw rootwait net.ifnames=0 biosdevname=0
Feb 7 19:33:14 espressobin kernel: [ 0.289914] Serial: AMBA PL011 UART driver
Feb 7 19:33:14 espressobin kernel: [ 0.296443] mvebu-uart d0012000.serial: could not find pctldev for node /soc/internal-regs@d0000000/pinctrl@13800/uart1-pins, deferring probe
...

前面的输出也可以通过使用如下的dmesg命令获得,这是一个为此目的而设计的工具:

# dmesg | grep -i uart
[ 0.000000] earlycon: ar3700_uart0 at MMIO 0x00000000d0012000 (options '')
[ 0.000000] bootconsole [ar3700_uart0] enabled
[ 0.000000] Kernel command line: console=ttyMV0,115200 earlycon=ar3700_uart,0
xd0012000 loglevel=0 debug root=/dev/mmcblk0p1 rw rootwait net.ifnames=0 biosdev
name=0
[ 0.289914] Serial: AMBA PL011 UART driver
[ 0.296443] mvebu-uart d0012000.serial: could not find pctldev for node /soc/
internal-regs@d0000000/pinctrl@13800/uart1-pins, deferring probe
...

Note that, while cat displays everything in the log file, even very old messages from previous OS executions, dmesg displays current OS execution messages only. This is because dmesg takes kernel messages directly from the current running system via its ring buffer (that is, the buffer where all messages are stored).

  1. 另一方面,如果我们想要收集关于早期引导活动的信息,我们仍然可以将dmesg命令与head命令一起使用,以便仅显示dmesg输出的前 10 行:
# dmesg | head -10 
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
[ 0.000000] Linux version 4.18.0-dirty (giometti@giometti-VirtualBox) (gcc ve
rsion 7.3.0 (Ubuntu/Linaro 7.3.0-27ubuntu1~18.04)) #5 SMP PREEMPT Sun Jan 27 13:
33:24 CET 2019
[ 0.000000] Machine model: Globalscale Marvell ESPRESSOBin Board
[ 0.000000] earlycon: ar3700_uart0 at MMIO 0x00000000d0012000 (options '')
[ 0.000000] bootconsole [ar3700_uart0] enabled
[ 0.000000] efi: Getting EFI parameters from FDT:
[ 0.000000] efi: UEFI not found.
[ 0.000000] cma: Reserved 32 MiB at 0x000000007e000000
[ 0.000000] NUMA: No NUMA configuration found
[ 0.000000] NUMA: Faking a node at [mem 0x0000000000000000-0x000000007fffffff]
  1. 另一方面,如果我们对最后 10 行感兴趣,可以使用tail命令。事实上,我们已经看到,为了监视内核活动,我们可以使用它,如下所示:
# tail -f /var/log/kern.log

因此,要查看最后 10 行,我们可以执行以下操作:

# dmesg | tail -10 
  1. 通过添加-w选项参数,也可以对dmesg进行同样的操作,如下例所示:
# dmesg -w
  1. 通过使用-l(或--level)选项参数,dmesg命令还可以根据内核消息的级别过滤内核消息,如下所示:
# dmesg -l 3 
[ 1.687783] advk-pcie d0070000.pcie: link never came up
[ 3.153849] advk-pcie d0070000.pcie: Posted PIO Response Status: CA, 0xe00 @ 0x0
[ 3.688578] Unable to create integrity sysfs dir: -19

前面的命令显示具有KERN_ERR级别的内核消息,而下面的命令显示具有KERN_WARNING级别的消息:

# dmesg -l 4
[ 3.164121] EINJ: ACPI disabled.
[ 3.197263] cacheinfo: Unable to detect cache hierarchy for CPU 0
[ 4.572660] xenon-sdhci d00d0000.sdhci: Timing issue might occur in DDR mode
[ 5.316949] systemd-sysv-ge: 10 output lines suppressed due to ratelimiting
  1. 我们还可以组合级别,以便同时拥有KERN_ERRKERN_WARNING:
# dmesg -l 3,4
[ 1.687783] advk-pcie d0070000.pcie: link never came up
[ 3.153849] advk-pcie d0070000.pcie: Posted PIO Response Status: CA, 0xe00 @ 0x0
[ 3.164121] EINJ: ACPI disabled.
[ 3.197263] cacheinfo: Unable to detect cache hierarchy for CPU 0
[ 3.688578] Unable to create integrity sysfs dir: -19
[ 4.572660] xenon-sdhci d00d0000.sdhci: Timing issue might occur in DDR mode
[ 5.316949] systemd-sysv-ge: 10 output lines suppressed due to ratelimiting
  1. 最后,在出现大量嘈杂消息的情况下,我们可以使用以下命令要求系统清理内核环形缓冲区(存储所有内核消息的地方):
# dmesg -C

现在,如果我们再次使用dmesg,我们将只看到新生成的内核消息。

请参见

  • 关于内核消息管理的更多信息,一个很好的起点是dmesg手册页,我们可以通过执行man dmesg命令来显示该手册页。

使用内核模块

知道如何向内核添加定制代码是有用的,但是,当我们必须编写新的驱动时,将我们的代码编写为内核模块可能会更有用。事实上,通过使用一个模块,我们可以轻松修改内核代码,然后测试它,而无需每次都重新启动系统!为了测试新版本的代码,我们只需要移除然后重新插入模块(在必要的修改之后)。

在这个食谱中,我们将看看内核模块是如何被编译的,即使是在内核树之外的目录中。

准备好了

要将我们的dummy-code.c文件转换成内核模块,我们只需要更改我们的内核设置,允许编译我们的示例模块(通过在内核配置菜单中用M替换*字符)。然而,在某些情况下,将我们的驱动发布到一个与内核源完全分离的专用归档中可能会更有用。即使在这种情况下,也不需要对现有代码进行任何更改,我们将能够在内核源代码树内部甚至外部编译dummy-code.c

为了构建我们的第一个内核模块作为外部代码,我们可以安全地获取前面的dummy-code.c文件,然后将其放入一个专用目录,如下所示Makefile:

ifndef KERNEL_DIR
$(error KERNEL_DIR must be set in the command line)
endif
PWD := $(shell pwd)
ARCH ?= arm64
CROSS_COMPILE ?= aarch64-linux-gnu-

# This specifies the kernel module to be compiled
obj-m += dummy-code.o

# The default action
all: modules

# The main tasks
modules clean:
    make -C $(KERNEL_DIR) \
              ARCH=$(ARCH) \
              CROSS_COMPILE=$(CROSS_COMPILE) \
              SUBDIRS=$(PWD) $@

查看前面的代码,我们看到KERNEL_DIR变量必须在命令行上提供,指向 ESPRESSObin 以前编译的内核源代码的路径,而ARCHCROSS_COMPILE变量不是强制的,因为Makefile指定了它们(但是,在命令行上提供它们将优先)。

此外,我们应该验证insmodrmmod命令在我们的 ESPRESSObin 中可用,如下所示:

# insmod -h
Usage:
        insmod [options] filename [args]
Options:
        -V, --version show version
        -h, --help show this help

如果它们不存在,那么可以通过用通常的apt install kmod命令添加kmod包来安装它们。

怎么做...

让我们看看如何通过以下步骤来实现:

  1. dummy-code.cMakefile文件放入主机当前工作目录后,使用ls命令时应该如下所示:
$ ls
dummy-code.c  Makefile
  1. 然后,我们可以使用以下命令编译我们的模块:
$ make KERNEL_DIR=../../../linux/
make -C ../../../linux/ \
 ARCH=arm64 \
 CROSS_COMPILE=aarch64-linux-gnu- \
 SUBDIRS=/home/giometti/Projects/ldddc/github/chapter_2/module modules
make[1]: Entering directory '/home/giometti/Projects/ldddc/linux'
 CC [M] /home/giometti/Projects/ldddc/github/chapter_2/module/dummy-code.o
 Building modules, stage 2.
 MODPOST 1 modules
 CC /home/giometti/Projects/ldddc/github/chapter_2/module/dummy-code.mod.o
 LD [M] /home/giometti/Projects/ldddc/github/chapter_2/module/dummy-code.ko
make[1]: Leaving directory '/home/giometti/Projects/ldddc/linux'

可以看到,现在我们在当前的工作目录下有几个文件,其中一个名为dummy-code.ko;这是我们准备转移到 ESPRESSObin 的内核模块!

  1. 一旦模块被移动到目标系统中(例如,通过使用scp命令),我们可以使用insmod实用程序加载它,如下所示:
# insmod dummy-code.ko
  1. 现在,通过使用lsmod命令,我们可以要求系统显示所有加载的模块。在我的 ESPRESSObin 上,我只有dummy-code.ko模块,所以我的输出如图所示:
# lsmod 
Module         Size  Used by
dummy_code    16384  0

Note that the .ko postfix has been removed by the kernel module name, as the - character is replaced by _.

  1. 然后,我们可以使用rmmod命令从内核中移除我们的模块,如下所示:
# rmmod dummy_code

In case you get the following error, please verify you're running the correct Image file we got inChapter 1Installing the Development System rmmod: ERROR: ../libkmod/libkmod.c:514 lookup_builtin_file() could not open builtin file '/lib/modules/4.18.0-dirty/modules.builtin.bin'

它是如何工作的...

insmod命令只是取我们的模块插入内核;之后,执行module_init()功能。

在模块插入过程中,如果我们通过 SSH 连接,我们将在终端上什么也看不到,我们必须使用dmesg来查看内核消息(或/var/log/kern.log文件上的tail,如前所述);否则,在串行控制台上,插入模块后,我们应该会看到如下内容:

dummy_code: loading out-of-tree module taints kernel.
dummy_code:dummy_code_init: dummy-code loaded

Note that the message, loading out-of-tree module taints kernel, is just a warning and can be safely ignored for our purposes. See https://www.kernel.org/doc/html/v4.15/admin-guide/tainted-kernels.html for further information about tainted kernels.

rmmod命令执行与insmod相反的步骤,即执行module_exit()功能,然后从内核中移除模块。

请参见

  • 有关模块的更多信息,它们的手册页是一个很好的起点(命令有:man insmodman rmmodman modinfo);此外,我们可以通过阅读其手册页来查看modprobe命令(man modprobe)。

使用模块参数

在内核模块开发过程中,在模块插入过程中,而不仅仅是在编译时,通过某种方式动态设置一些变量是非常有用的。在 Linux 中,这可以通过使用内核模块的参数来实现,这些参数允许通过在insmod命令的命令行上指定参数来将参数传递给模块。

准备好了

为了展示一个例子,让我们考虑一个情况,我们有一个新的模块信息文件,module_par.c(这个文件也在我们的 GitHub 存储库中)。

怎么做...

让我们看看如何通过以下步骤来实现:

  1. 首先,让我们定义模块参数,如下所示:
static int var = 0x3f;
module_param(var, int, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(var, "an integer value");

static char *str = "default string";
module_param(str, charp, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(str, "a string value");

#define ARR_SIZE 8
static int arr[ARR_SIZE];
static int arr_count;
module_param_array(arr, int, &arr_count, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(arr, "an array of " __stringify(ARR_SIZE) " values");
  1. 然后,我们可以使用以下initexit功能:
static int __init module_par_init(void)
{
    int i;

    pr_info("loaded\n");
    pr_info("var = 0x%02x\n", var);
    pr_info("str = \"%s\"\n", str);
    pr_info("arr = ");
    for (i = 0; i < ARR_SIZE; i++)
        pr_cont("%d ", arr[i]);
    pr_cont("\n");

    return 0;
}

static void __exit module_par_exit(void)
{
    pr_info("unloaded\n");
}

module_init(module_par_init);
module_exit(module_par_exit);
  1. 最后,我们可以像往常一样添加模块描述宏:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Rodolfo Giometti");
MODULE_DESCRIPTION("Module with parameters");
MODULE_VERSION("0.1");

它是如何工作的...

像以前一样编译后,一个新文件module_par.ko应该可以加载到我们的 ESPRESSObin 中了。但是,在执行之前,让我们在上面使用modinfo实用程序,如下所示:

# modinfo module_par.ko 
filename:    /root/module_par.ko
version:     0.1
description: Module with parameters
author:      Rodolfo Giometti
license:     GPL
srcversion:  21315B65C307ABE9769814F
depends: 
name:        module_par
vermagic:    4.18.0 SMP preempt mod_unload aarch64
parm:        var:an integer value (int)
parm:        str:a string value (charp)
parm:        arr:an array of 8 values (array of int)

The modinfo command is also included in the kmod package as insmod.

正如我们在最后三行中看到的(都以parm:字符串为前缀),我们有一个模块参数列表,这些参数在代码中由module_param()module_param_array()宏定义,并用MODULE_PARM_DESC()描述。

现在,如果我们像以前一样简单地插入模块,我们会得到默认值,如下面的代码块所示:

# insmod module_par.ko 
[ 6021.345064] module_par:module_par_init: loaded
[ 6021.347028] module_par:module_par_init: var = 0x3f
[ 6021.351810] module_par:module_par_init: str = "default string"
[ 6021.357904] module_par:module_par_init: arr = 0 0 0 0 0 0 0 0

但是如果我们使用下一个命令行,我们会强制新的值:

# insmod module_par.ko var=0x01 str=\"new value\" arr='1,2,3' 
[ 6074.175964] module_par:module_par_init: loaded
[ 6074.177915] module_par:module_par_init: var = 0x01
[ 6074.184932] module_par:module_par_init: str = "new value"
[ 6074.189765] module_par:module_par_init: arr = 1 2 3 0 0 0 0 0 

Don't forget to remove the module_par module by using the rmmod module_par command before trying to reload it with new values!

最后,让我建议仔细看看下面的模块参数定义:

static int var = 0x3f;
module_param(var, int, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(var, "an integer value");

首先,我们有表示参数的变量的声明,然后我们有真实的模块参数定义(在这里我们指定类型和文件访问权限),然后我们有描述。

modinfo命令能够显示除文件访问权限之外的所有前述信息,文件访问权限是指 sysfs 文件系统中与该参数相关的文件!事实上,如果我们看一下/sys/module/module_par/parameters/目录,我们会得到以下内容:

# ls -l /sys/module/module_par/parameters/
total 0
-rw------- 1 root root 4096 Feb 1 12:46 arr
-rw------- 1 root root 4096 Feb 1 12:46 str
-rw------- 1 root root 4096 Feb 1 12:46 var

现在应该清楚S_IRUSRS_IWUSR是什么参数的意思了;它们允许模块用户(即根用户)写入这些文件,然后从中读取相应的参数。

Defines S_IRUSR and related function are defined in the following file: linux/include/uapi/linux/stat.h.

请参见