前言
要更好的掌握Android系统,必然绕不过去的就是 Kernel 层的驱动程序。
所谓驱动程序,就是一个与硬件交互的程序,他既可以支撑硬件之间的交互,也可以支撑硬件与软件之间的交互。
Linux Kernel驱动程序大体上分为三类:
- 字符设备
- 块设备
- 网络设备
简单来说,字符设备可以像文件一样操作,正常情况下无法随机寻址。而块设备可以当做是比较特殊的字符设备,不同的是块设备传输单位是块,比如硬盘操作就是以块为单位进行操作。而网络设备则是使用套接字通信。
由于内核和驱动程序运行在内核态,所以驱动程序无法使用位于用户态实现的C库函数(但内核本身有提供一些库),另外驱动程序和内核缺乏用户态程序一样的内存保护机制,一旦越界,很有可能导致整个kernel崩溃,所以务必注意。
驱动程序结构
一个典型的驱动程序至少有如下几个部分:
-
模块头文件
<linux/module.h> <linux/init.h>
这几乎是驱动程序必须使用的,其包含了大量宏以及模块加载释放等函数定义 -
模块加载函数
提供一个返回值为
int
的函数作为加载函数,并且使用module_init
加载 -
模块卸载函数
提供一个返回值为
void
的函数作为退出函数,并且使用module_exit
来取消加载 -
模块许可声明
如果你的模块没有使用
MODULE_LICENSE
声明权限,你的模块将无法通过编译,要注意的是内核中有很多GPL
协议的部分,一旦使用你的模块就必须同样使用这个协议,因为该协议有传染性。
一个HelloWorld模块
下面我们开始尝试一下编写自己的HelloWorld,按照结构,至少拥有这几部分:
#include <linux/init.h>
#include <linux/module.h>
static int helloworld_init(void)
{
printk(KERN_INFO "Hello World, Module loaded. \n");
return 0;
}
static void helloworld_exit(void)
{
printk(KERN_INFO "Goodbye world, Module unloaded. \n ");
}
module_init(helloworld_init); //入口函数
module_exit(helloworld_exit); //出口函数
MODULE_LICENSE("GPL"); //协议声明
MODULE_AUTHOR("GARFIELD"); //作者
MODULE_DESCRIPTION("Just a hello world!"); //描述,可不写
这样一来,一个基本的框架就有了,然后就是编译了。
编译模块驱动
由于我的平台是RK3399 Android 7.1,为了不扰乱原本的模块目录,因此我这里新建一个目录,并修改 Makefile
和 Kconfig
文件。
首先新建一个目录 hello_drvier
到 kernel/drivers/char
中,当然这里你随意。
然后我将这个.c文件放到 kernel/drivers/char/hello_driver
目录中,然后为其编写一个 Kconfig
cd kernel/drivers/char/hello_driver
touch Kconfig
gedit Kconfig
# "Kconfig" 加入如下内容
config HELLOWORLD
tristate "Hello world Modules"
help
Just a hello world modules
touch Makefile
gedit Makefile
# "Makefile" 加入如下内容
obj-$(CONFIG_HELLOWORLD) += helloworld.o
然后,还需要在上一层目录的 Makefile
中的最后一行加入对该目录的包含,否则就不会进去编译啦。
## "kernel/drivers/char/Makefile"
obj-$(CONFIG_HELLOWORLD) += hello_driver/
完成以上内容添加后,开始设置内核,打开该模块。
make menuconfig
选中后按 M
让其按模块重新编译内核 make -j8 ARCH=arm64
,就可以得到ko驱动啦。
测试驱动
由于驱动按模块编译,所以不需要重新刷kernel到板,只要把ko文件push上去执行就可以了。
adb push helloworkd.ko /system/lib/modules/
adb shell insmod /system/lib/modules/helloworld.ko
adb shell rmmod /system/lib/modules/helloworkd.ko
执行后可以看到驱动入口函数和出口函数都被调用,打印了信息:
[ 3310.624502] Hello World, Module loaded.
[ 3393.958614] Goodbye world, Module unloaded.
单独编译驱动模块
由于上述方法编译还是比较麻烦,所以可以通过编写make文件来手动编译模块。
因为我是跨平台的,所以要配置两个目录,一个是kernel的目录,另一个是交叉编译器的目录,但是如果你不是跨平台的话后者是可以忽略的
我的驱动是单独放在的 hello_driver
目录里,所以要将上面编写过的 Makefile
改动如下。
## KERNELRELEASE 是Kernel顶层Makefile定义的,如果没有执行过,则会指定Kernel目录及交叉编译编译器目录,执行独立编译。
## 若定义过,实际上等于是kernel在全编,正常执行obj-m即可
## 若在该目录直接执行的Make,则走else分支单独编译
ifneq ($(KERNELRELEASE),)
obj-m += helloworld.o
else
KDIR := /home/garfield/Firefly339/kernel
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64 CROSS_COMPILE=/home/garfield/Firefly339/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers modul*
endif
/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers modul*
endif