「Tech初见」Linux驱动之hellodriver

免责声明

「Tech初见」系列的文章,是本人第一次接触的话题

对所谓真理的理解暂时可能还不到位,避免不了会出现令人嗤鼻的谬论

所以,看看就好,借鉴一下,别全信,也别较真。当然,文章中不正确的地方,欢迎意见评论,我会及时研判和进行下一步的纠偏

I. Motivation

在做有关嵌入式开发的时候,要跟一些特定的硬件打交道,比如,在板子上显示字符串 “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 变得臃肿

II. Solution

通常,编写驱动程序应该是需要硬件的,即是常说的板子。但是,对于一些简单(本文的 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

S1 - hello driver 的加载与卸载

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 中引用 <linux/module.h>

在明白了这套挂/卸载流程之后,就可以开始编写具体的业务逻辑了。我们想在驱动刚挂载的时候输出字符串 “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 中有定义,所以需要引用 <linux/init.h>

完整的代码如下,

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

/* 初始化函数 */
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");  /* 版本号 */

值得注意的是,需要包含 <linux/kernel.h> ,毕竟我们是在写 Linux 驱动程序

S2 - Makefile 的规则

完成 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

III. Result

在 /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 已正常卸载

IV. Evaluation

涉及驱动的头文件默认都在 /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
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值