「Tech初见」系列的文章,是本人第一次接触的话题
对所谓真理的理解暂时可能还不到位,避免不了会出现令人嗤鼻的谬论
所以,看看就好,借鉴一下,别全信,也别较真。当然,文章中不正确的地方,欢迎意见评论,我会及时研判和进行下一步的纠偏
在做有关嵌入式开发的时候,要跟一些特定的硬件打交道,比如,在板子上显示字符串 “hello driver”。在 Linux 中,要想完成这个操作,只能通过调用 kernel 的设备驱动程序来实现,简称驱动
在对应的设备驱动程序中,完成向板子输出字符串 “hello driver” 的业务逻辑即可。不妨,往通用方向联想,编写驱动程序,它的本质(业务逻辑)应该是和在 Visual Studio 中编写 32 位应用程序相同,只是平台变了
编译的这套流程在应用程序中,被 Visual Studio 全包揽了;而在 Linux 下编写驱动,要通过 Makefile 才能完成编译,生成 kernel modules
而且 C 语言的标准库函数在 Linux 中是不能被使用的,要用 Linux 的方言,比如 C 标准的 printf()
,在驱动中要更替成 printk()
另外,我对于生成的 kernel modules 的理解,应该就是一个 kernel 的插件,它能控制特定的硬件,同时又独立与 kernel 之外。一句话概括,即扩展了 kernel 的功能,又不至于使 kernel 变得臃肿
通常,编写驱动程序应该是需要硬件的,即是常说的板子。但是,对于一些简单(本文的 hello_driver )的驱动程序,可以没有板子,有 Linux OS 即可,OS 可以模拟出在实际硬件上的效果
好,下面就来实现一下设备驱动程序显示字符串 “hello driver” 的业务逻辑。本文的驱动程序是在腾讯云服务器上完成的,首先,在 /home/lighthouse
目录下创建 test-linuxdriver 文件夹,专事于驱动程序
在其中,为第一个 hello 驱动程序创建专属 workspace ,即是 hello-driver 文件夹
在 /home/lighthouse/test-linuxdriver/hello-driver
路径下,开始 hello driver 之旅
其中,我想推广的一种命名法则,也是 Linux 的命名规则。如果文件夹的名字是由多个单词构成,则用连字符 -
隔开,例如 hello-driver 文件夹;应用在文件上,则用下划线 _
隔开,例如 hello_driver.c
在 I. Motivation 中讲过 kernel modules 的作用,即扩展 kernel 的功能。我们都知道,Linux 的外围设备都是要先挂载至 kernel ,然后才能正常工作的。最常见的莫如外接 U 盘插入,需要键入 mount
命令挂载,之后资源管理器方能访问到其中的数据
同样,我们编写的设备驱动程序也是需要挂载至 kernel 的,只有挂载上了,才能正常驱动硬件;同理,在硬件结束工作后,需要卸载 module ,好比拔 U 盘
这其中的挂载对应在驱动中,即是,
module_init();
这是个宏,可以理解成初始化函数。卸载的写法如下,
module_exit();
卸载 module 时会调用。在 usr/include/linux/module.h
中可以查看这两个宏的定义。另外,还有一些描述性的宏定义,比如,
/* 描述性定义 */
MODULE_LICENSE("GPL"); /* 许可协议 */
MODULE_AUTHOR("jeffrey wood"); /* 作者 */
MODULE_VERSION("V0.1"); /* 版本号 */
在驱动程序中也是必不可少的,特别是许可协议。想要使用上面的几个宏,需要在 hello_driver.c 中引用
在明白了这套挂/卸载流程之后,就可以开始编写具体的业务逻辑了。我们想在驱动刚挂载的时候输出字符串 “hello driver” ,在卸载时也能给我们一些提示信息,即输出字符串 “goodbye driver” 。于是,初始化函数可以写成,
/* 初始化函数 */
static int __init hello_init(void)
{
printk(KERN_INFO "hello driver\n");
return 0;
}
其中的业务逻辑只是简简单单地输出提示信息,KERN_INFO
是一个宏,它告诉 kernel 紧跟其后的字符串是一个常规级别的信息
hello_init()
返回值是 int
且为 static 函数,这是 Linux driver 规定的,暂时我没办法解答;同理,卸载函数的写法也很简单,
/* 卸载函数 */
static void __exit hello_exit(void)
{
printk(KERN_INFO "goodbye driver\n");
}
注意,它是不用返回值的。前者需要加上宏 __init
,后者需要宏 __exit
。这两个宏在 usr/include/linux/init.h
中有定义,所以需要引用
完整的代码如下,
#include
#include
#include
/* 初始化函数 */
static int __init hello_init(void)
{
printk(KERN_INFO "hello driver\n");
return 0;
}
/* 卸载函数 */
static void __exit hello_exit(void)
{
printk(KERN_INFO "goodbye driver\n");
}
/* 用宏来指定初始化函数和卸载函数 */
module_init(hello_init);
module_exit(hello_exit);
/* 描述性定义 */
MODULE_LICENSE("GPL"); /* 许可协议 */
MODULE_AUTHOR("jeffrey wood"); /* 作者 */
MODULE_VERSION("V0.1"); /* 版本号 */
值得注意的是,需要包含
完成 hello driver 的业务逻辑之后,就可以开始编写 Makefile 了。这是个非常简单的 demo ,可以说此 Makefile 是一个原形模版,
# Linux kernel 文件所在路径
# 使用 $(shell uname -r) 获取现在使用的 kernel 版本号
KDIR := /lib/modules/$(shell uname -r)/build
# 获取当前目录
PWD:=$(shell pwd)
# 编译 hello_driver, 生成的 hello_driver.o 源于 hello_driver.c
obj-m := hello_driver.o
# 编译成模块,下面的语句都是模版,
# 大意是将 PWD 下编写的驱动文件移至 KDIR 目录中,联合编译,生成 modules
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
写法规定,通过定义 KDIR
变量,获取 Linux kernel 文件的所在路径。/lib/modules/5.4.0-126-generic/build
是我的机器上的路径,其中的 5.4.0-126-generic
是 kernel 的版本号。因为版本号可能会变化,所以在 Makefile 中将其提炼出来,用 $(shell uname -r)
表示
$(shell pwd)
获取当前目录,方便之后的,
make -C $(KDIR) M=$(PWD) modules
将当前目录下的所有文件,包括 hello_driver.c 和 Makefile ,迁移到 kernel 文件目录,进行联合编译
这里有个常识,lib 代表着其下存放着库文件,lib 是 library 的缩写,库文件无论是在应用程序还是 anywhere ,都是开发必不可少的源码。一般,我们会将一些常用的基础功能提炼出来,打包生成较为集中的库文件,这样我们在开发的时候,就不必重复造轮子了
其中的 -C
应该是指将当前目录下的文件迁移到 Linux kernel 目录下。以及 obj-m
是要与 hello_driver.c 同名的,这样才能定位到我们编写的驱动程序。需要从 hello_driver.c 生成 hello_driver.o ,然后再生成驱动程序 hello_driver.ko
在 /home/lighthouse/test-linuxdriver/hello-driver 目录下,键入 make
命令编译程序,
make -C /lib/modules/5.4.0-126-generic/build M=/home/lighthouse/test-linuxdriver/hello-driver modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-126-generic'
CC [M] /home/lighthouse/test-linuxdriver/hello-driver/hello_driver.o
Building modules, stage 2.
MODPOST 1 modules
CC [M] /home/lighthouse/test-linuxdriver/hello-driver/hello_driver.mod.o
LD [M] /home/lighthouse/test-linuxdriver/hello-driver/hello_driver.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-126-generic'
一般来说,正常是这样的,最后生成了 .ko 文件,这就是我们需要的驱动。键入,
sudo insmod hello_driver.ko
挂载驱动程序,键入 dmesg
查看此时 OS 的调试信息,
[9923653.182991] hello driver
这是正常显示了输入提示信息,期间也可以通过 lsmod
查看当前 OS 的驱动程序状态。键入,
sudo rmmod hello_driver
卸载驱动程序,同样键入 dmesg
查看,
[9923808.884171] goodbye driver
表示 hello_driver 已正常卸载
涉及驱动的头文件默认都在 /usr/include/linux 下,我们可以在其中找到函数 or 宏等相关的定义
/usr/include/linux 目录下的文件可能不是最全的,会缺失少数文件,比如我在 ubuntu 20.04 kernel 5.4.0-126 下编写驱动,/usr/include/linux 缺失 init.h ,导致 module_init
之类的宏加载不出来
这时,可以进入 /usr/src/linux-headers-5.4.0-126-generic/include/linux 中,将目录下的 init.h 拷贝到 /usr/include/linux,这样就可以补全缺失
其中的 /usr/src/linux-headers-5.4.0-126-generic/include/linux 是存放 Linux 源码的地方,一般都是最全的。如果 /usr/include/linux 中缺少文件,可以来此处寻找。使用 cp
拷贝,一步到位,
$cd /usr/src/linux-headers-5.4.0-126-generic/include/linux
$sudo cp -r * /usr/include/linux