目录(每节开头都可以跳过本节,享受极致体验)

  • 前言
  • 一、编译生成模块
    • 方法一(推荐)
    • 方法二
      • 步骤一:进入内核树目录
      • 步骤二:编辑Makefile
      • 步骤三:编辑Kconfig
      • 步骤四:生成.config
      • 步骤五:开始make
  • 二、模块的加载和使用
    • 步骤一:插入模块
    • 步骤二:查询模块的`主编号(major number)`
    • 步骤三:创建系统节点
  • 三、使用模块
  • 参考链接


前言

注:以下全部方法已经过测试。测试环境:

  1. 系统:Ubuntu 20.04 LTS
  2. 内核:5.11.0-27-generic5.11.0-41-generic

编写好了驱动程序(.c文件)后,下一步是编译、加载和运行。这里讲述如何实现这个操作。

这里默认已经构建好了Linux内核树。如何查询电脑中有无内核树,或是构建新的内核树的具体步骤请参照这篇文章:【超详细】Linux内核树的构建。


一、编译生成模块

(点击跳过本章节)

这里介绍两种方法,任选一种即可。

  1. 方法一:在任意目录下编译模块,也是推荐方法。
  2. 方法二:在内核树目录下编译模块,步骤较为繁琐。

方法一(推荐)

随便进入一个文件夹,将驱动代码放进这个目录,比如helloworld.c

然后,创建一个Makefile,文件内容如下

obj-m += helloworld.o 
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules

(!!注意make前必须得是Tab缩进符。)

代码解释:

  1. obj-m += <文件>:将指定的文件(需要是以.o结尾)设为编译时以模块形式编译(这里并没有开始编译,make那行才是真正的编译命令)
  2. make -C <目录>:跳转到指定的目录,读取那里的Makefile
  3. M=<目录> :跳转到指定的目录,执行先前读取的Makefile
  4. 总结:先读取内核树下的Makefile,再读取我们写的源码,然后结合二者执行make modules命令

最后,在源代码和Makefile所在目录下运行make,将在相同目录下生成模块helloworld.ko

方法二

(点击跳过方法二)

假设我们的内核树所在的目录为/lib/modules/5.11.22/build。下面的步骤里都默认这个目录为根目录。如,/drivers实际上是/lib/modules/5.11.22/build/drivers

步骤一:进入内核树目录

进入内核树所在根目录

cd /lib/modules/5.11.22/build

(其实这个build文件夹是个链接,真实目录可以进入文件夹后用pwd查看,或者在上一级文件夹用ll查看)

将编写的驱动的源代码(比如helloworld.c)放在内核树根目录的/drivers/char文件夹下。

步骤二:编辑Makefile

编辑在同一个目录下的Makefile,在末尾添加

obj-$(CONFIG_HELLO_WORLD)	+= helloworld.o

helloworld.o在这里指定的是编译时寻找的文件。由于示例源代码名为helloworld.c.c文件将来生成的.o文件就是Makefile中需要指明的目标文件了。)

步骤三:编辑Kconfig

仍然在/drivers/char目录下,编辑Kconfig文件(这是make的配置文件,相当于一般软件菜单栏里的选项功能)。

在文末endmenu之前添加

config HELLO_WORLD
	tristate "HELLO WORLD"

注意,config后面的名称需要和之前Makefile里的对应,对应规则是Makefile中的名称去掉CONFIG_前缀。

tristate意思是告知这个模块可以有三种选项:ynm,分别对应编译时包括该模块不包括该模块使用模块化特性(祥见下图红框中的黄色高亮部分,更多细节请看参考链接1)。

!!!注意:

  1. 一定要找到endmenu所在位置(可能在文件中间,vi/vim用户可以在命令模式输入/endmenu后回车进行搜索),在之前添加,否则不会显示在menuconfig里。
  2. tristate前面的是Tab键而非空格。Makefile中,空格和制表符的含义有所不同。

步骤四:生成.config

回到根目录(即前提中所说的/lib/modules/5.11.22/build)。

cd ../..

设置menuconfig (这里可以在末尾添加-j <最大并发数>来提高效率,如6核CPU可以是-j 12

make menuconfig

在出现的菜单中进入Device Drivers子目录

然后进入Character drivers子目录

这时在目录最下面应该会出现一条入口
< > HELLO WORLD (NEW)

将光标移到其上,按M键将当前驱动视为可装卸的模块。

然后,通过连按两次Esc来返回上一页,直到退出UI

退出前会提示是否保存更改,选择Yes即可。

(注:也可以不进行menuconfig这一步,直接运行make后,在显示到helloworld.ko时,会询问驱动类型[n/y/m],输入m后回车即可)


步骤五:开始make

回到内核树所在根目录(如,这里是/lib/modules/5.11.22/build),开始编译我们的驱动。

运行

make

如果报错,说明驱动的源代码有bug。按照报错的具体提示进行debug,然后重新运行make即可。
(在vi/vim中输入:set number可以开启行号显示)

持续这个过程直至成功。

成功后我们来检查一下是否生成了编译好的驱动程序。

首先,进入内核树的根目录下的/drivers/char目录(我这里的完整路径是/lib/modules/5.11.22/build/drivers/char)。

运行ls命令后,在打印的文件清单中找到编译成功的.ko结尾的驱动程序,就算成功了。



注:快速找到目标文件的方法

  1. ls -t可以按照更改时间对文件清单进行排序,最新修改的文件位于清单的开头
  2. ls | grep *,ko只会打印清单中以.ko结尾的所有文件。(想了解更多请查询grep命令的用法)

二、模块的加载和使用

(点击跳过本章节)

步骤一:插入模块

在第一步生成的helloworld.ko文件所在的目录下,利用insmod命令,将我们的模块插入到当前操作系统中

sudo insmod helloworld.ko

现在我们来检查一下是否成功载入模块。

使用lsmod命令。它会打印所有已经载入的模块(也可通过grep命令快速找到我们的模块:lsmod | grep hello

```
$ lsmod
Module                  Size  Used by
helloworld             16384  0
isofs                  49152  1
vboxvideo              36864  0
...					   ...    ...

```

删除的方法也很简单,用rmmod即可:

sudo rmmod helloworld.ko

步骤二:查询模块的主编号(major number)

cat /proc/devices会在打印所有已经载入的模块的同时,显示模块的主编号(major number)(如,下面的情况中,我们的helloworld模块的主编号是237)。

(这里显示的模块名称,应该取决于源代码中alloc_chrdev_region()的 第四个参数 所指定的模块名称。)

$ cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
... ...
237 hello_world
... ...

注意:

  1. 如果没有找到自己的驱动的话,可以参照下面的解释:

    不是所有设备驱动都会有用到设备号的,除非驱动里有实现字符设备驱动接口(cdev). 而且还有可能是用了平台设备驱动模型,需要平台设备匹配上才会触发注册设备号等字符设备的操作
    (摘自:https://bbs.csdn/topics/392191032)

  2. 如果发现成功载入了模块,但是程序中的printk本应该在载入模块成功时打印消息,却没打印,那么可能是系统不支持printk直接打印到终端。具体的原理及解决方法见这篇文章

步骤三:创建系统节点

最后,如果想要在应用程序中使用这个模块(通常是通过/dev/<设备名称>来调用),需要先用mknod命令创建设备文件:mknod /dev/<自定义设备名称> <模块类型> <模块主编号> <模块次编号>

比如

sudo mknod /dev/helloworld c <主编号> <次编号>

(注:删除时使用rm -f /dev/helloworld命令即可)

参数解释:

  1. /dev/helloworld:这里,/dev后面的“helloworld”是我为创建出的设备文件起的名字。该名字与之前的步骤没有依赖关系,可以任意更换,只是在后续程序中使用该设备文件的时候,需要与这里起的名字相同(如open("/dev/helloworld", O_RDONLY);
  2. c:代表创建的是字符型设备(char device)
  3. <主编号>:我的模块的主编号(major number),可以在源代码中用MAJOR()函数得到,也可通过cat /proc/devices命令来找到
  4. <次编号>:模块的次编号(minor number),通常是1(详见这篇文章:linux设备驱动第三版 - 主次编号)

成功后,输入

ls /dev

就应该可以找到创建好的设备文件并且在应用程序的代码里使用了

(注:嫌输出太长可以用ls /dev | grep hello命令过滤输出)。

此外,移除节点的方法是rm -f命令:

sudo rm -f /dev/helloworld.c

三、使用模块

(还跳?都已经到文章结尾了)

使用Linux系统自带的openread等函数,就能够使用我们的驱动了。
如:

int buf[100];
int fd = open("/dev/helloworld", O_RDONLY);
read(fd, buf, 99);

参考链接

浅谈Kconfig、Makefile、.config之间的关系 - cnblog

如何让 printk 打印到终端 - Xav Pan

Linux lsmod 命令 - 菜鸟教程

更多推荐

【Linux】驱动模块的 编译与加载