环境:
$ uname -r
6.1.0-13-amd64
Linux内核在使用驱动时首先要装载驱动,在装载过程中进行一些初始化动作(建立设备文件、分配内存等),在驱动程序中需提供相应函数来处理驱动初始化工作,该函数须使用module_init
宏指定;Linux系统在退出是需卸载Linux驱动,卸载过程中进行一些退出工作(删除设备文件、释放内存等),在驱动程序中需提供相应函数来处理退出工作,该函数须使用module_exit
宏指定。Linux驱动程序一般都要这两个宏指定这两个函数,所以包含这两个宏以及其所指定的两个函数的C程序可看作是Linux驱动的框架。
任何Linux驱动都需要有一个设备文件来与应用程序进行交互。建立设备文件的工作一般在上一步module_init
宏指定的函数中完成的,可以使用misc_register
函数创建设备文件;删除设备文件的工作一般在上一步module_exit
宏指定的函数中完成的,可以使用misc_deregister
函数删除设备文件。
驱动程序是自描述的,驱动程序的信息需要在驱动源代码中指定。通过MODULE_AUTHOR(作者姓名)、MODULE_LICENSE(使用的开源协议)、MODULE_ALIAS(别名)、MODULE_DESCRIPTION(驱动描述)等宏来指定与驱动相关的信息,这些宏一般写在驱动源码文件的结尾。可通过modinfo命令获取这些信息。
Linux驱动包含了很多动作,也称为事件,如“读”“写”事件,触发相应事件时Linux系统会自动调用对于驱动程序的相应回调函数。一个驱动程序不一定要指定所以的回调函数。回调函数通过相关机制进行注册。如与设备文件相关的回调函数使用misc_register函数注册。
没什么可说的,总不能注册一些空的回调函数,什么也不做吧。
Linux内核源码的编译规则是通过Makefile文件定义的,每个Linux驱动程序必须要有一个Makefile文件。
Linux驱动程序可直接编译进内核(使用obj-y编译),也可以作为模块单独编译(使用obj-m编译)。
如果将驱动编译进内核,只要Linux使用该内核,驱动程序就会自动装载。如果Linux驱动程序以模块单独存在,需要使用insmod或modprobe命令装载Linux驱动模块,使用rmmod命令卸载该模块。
// hello_world.c
#include
#include
#include
//指定license版本
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Marvin");
MODULE_DISCRIPTION("hello world");
MODULE_ALIES("hello");
//设置初始化入口函数
static int __init hello_world_init(void)
{
printk(KERN_DEBUG "hello world!!!\n");
return 0;
}
//设置出口函数
static void __exit hello_world_exit(void)
{
printk(KERN_DEBUG "goodbye world!!!\n");
}
//将上述定义的init()和exit()函数定义为模块入口/出口函数
module_init(hello_world_init);
module_exit(hello_world_exit);
上述代码就是一个设备驱动程序代码框架,这套框架主要的任务就是将内核模块中的init函数动态地注册到系统中并运行,由module_init()
和module_exit()
来实现,分别对应驱动的加载和卸载。
只是它并不做什么事,仅仅是打印两条语句而已,如果要实现某些驱动,我们就可以在init函数中进行相应的编程。
需要准备Linux头文件,一般通过
sudo apt-get install linux-headers-$(uname -r)
或者sudo yum install kernel-headers
来安装
ifneq ($(KERNELRELEASE),)
MODULE_NAME = helloworld
# 该模块需要的目标文件
# <模块名>-objs := <目标文件>.o
$(MODULE_NAME)-objs := hello_world.o
# 要生成的模块,注意模块名字不能与目标文件相同
# obj-m := <模块名>.o
obj-m := $(MODULE_NAME).o
else
KERNEL_DIR = /lib/modules/`uname -r`/build
MODULEDIR := $(shell pwd)
.PHONY: modules
default: modules
modules:
make -C $(KERNEL_DIR) M=$(MODULEDIR) modules
clean distclean:
rm -f *.o *.mod.c .*.*.cmd *.ko
rm -rf .tmp_versions
endif
编译结果会在当前目录生成helloworld.ko
文件,这个文件就是我们需要的内核模块文件了。
可以通过modinfo
命令来查看模块信息:
$ modinfo helloworld.ko
filename: /home/vm/workspace/learn_linux_driver/hello_world/helloworld.ko
alias: hello
description: hello world
author: Marvin
license: GPL
depends:
retpoline: Y
name: helloworld
vermagic: 6.1.0-13-amd64 SMP preempt mod_unload modversions
linux可加载模块makefile
linux 内核makefile总览
编译生成了内核文件,接下来就要将其加载到内核中,linux支持动态地添加和删除模块,所以我们可以直接在系统中进行加载:
sudo insmod helloworld.ko
可能遇到的问题:Why do I get “Required key not available” when install 3rd party kernel modules or after a kernel upgrade?
我们可以通过lsmod命令来检查模块是否被成功加载:
lsmod | grep helloworld
helloworld 16384 0
lsmod显示当前被加载的模块。
或者通过查看驱动程序打印的log:
dmesg | grep "hello world"
[ 30.993166] hello world!!!
同时,我们也可以卸载这个模块:
sudo rmmod hello_world.ko
同样我们也可以通过lsmod指令来查看模块是否卸载成功。
dmesg | grep "goodbye world"
[ 131.487449] goodbye world!!!
在上面实现了一个linux内核驱动程序(虽然什么也没干),接下来我们再来添加一些小功能来丰富这个驱动程序:
// hello_world_PLUS.c
#include
#include
#include
// 添加了MODULE_AUTHOR(),MODULE_DESCRIPTION(),MODULE_VERSION()等模块信息
// 添加了module_param()传入参数功能
MODULE_AUTHOR("marvin"); //作者信息
MODULE_DESCRIPTION("Linux kernel driver - hello_world PLUS!"); //模块的描述,可以使用modinfo xxx.ko指令来查看
MODULE_VERSION("0.1"); //模块版本号
//指定license版本
MODULE_LICENSE("GPL");
static char *name = "world";
module_param(name,charp,S_IRUGO); //设置加载时可传入的参数
MODULE_PARM_DESC(name,"name,type: char *,permission: S_IRUGO"); //参数描述信息
//设置初始化入口函数
static int __init hello_world_init(void)
{
printk(KERN_DEBUG "hello %s!!!\n",name);
return 0;
}
//设置出口函数
static void __exit hello_world_exit(void)
{
printk(KERN_DEBUG "goodbye %s!!!\n",name);
}
//将上述定义的init()和exit()函数定义为模块入口/出口函数
module_init(hello_world_init);
module_exit(hello_world_exit);
编译之前需要修改Makefile,将hello_world.o修改为hello_world_PLUS.o。
ifneq ($(KERNELRELEASE),)
MODULE_NAME = helloworldplus
$(MODULE_NAME)-objs := hello_world_PLUS.o
obj-m := $(MODULE_NAME).o
else
KERNEL_DIR = /lib/modules/`uname -r`/build
MODULEDIR := $(shell pwd)
.PHONY: modules
default: modules
modules:
make -C $(KERNEL_DIR) M=$(MODULEDIR) modules
clean distclean:
make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
rm -f *.o *.mod.c .*.*.cmd *.ko
rm -rf .tmp_versions
endif
在上述程序中我们添加了module_param这一选项,module_param支持三个参数:变量名,类型,以及访问权限,我们可以先试一试传入参数:
sudo insmod helloworldplus.ko name="marvin"
查看日志输出,显示:
hello marvin!!!!!
看到模块中name变量被赋值为marvin,表明参数传入成功。
然后卸载:
sudo rmmod helloworldplus
日志输出:
goodbye marvin!!!!!
在hello_world_PLUS中,我们添加了一些模块信息,可以使用modinfo来查看:
modinfo helloworldplus.ko
输出:
# modinfo helloworldplus.ko
$ sudo modinfo helloworldplus.ko
filename: /home/vm/workspace/learn_linux_driver/hello_world_plus/helloworldplus.ko
license: GPL
version: 0.1
description: Linux kernel driver - hello_world plus!
author: marvin
srcversion: 07BD424E4922972A134034F
depends:
retpoline: Y
name: helloworldplus
vermagic: 6.1.0-13-amd64 SMP preempt mod_unload modversions
parm: name:name,type: char *, permission: S_IRUGO (charp)
总结一下:
sysfs是一个文件系统,但是它并不存在于非易失性存储器上(也就是我们常说的硬盘、flash等掉电不丢失数据的存储器),而是由linux系统构建在内存中,简单来说这个文件系统将内核驱动信息展现给用户。
当我们装载helloworldplus.ko时,会在/sys/module/目录下生成一个与模块同名的目录即helloworldplus,目录里囊括了驱动程序的大部分信息,查看目录:
$ tree -a /sys/module/helloworldplus/
/sys/module/helloworldplus/
├── coresize
├── holders
├── initsize
├── initstate
├── notes
│ ├── .note.gnu.build-id
│ └── .note.Linux
├── parameters
│ └── name
├── refcnt
├── sections
│ ├── .data
│ ├── .exit.data
│ ├── .exit.text
│ ├── .gnu.linkonce.this_module
│ ├── .init.data
│ ├── .init.text
│ ├── __mcount_loc
│ ├── .note.gnu.build-id
│ ├── .note.Linux
│ ├── .orc_unwind
│ ├── .orc_unwind_ip
│ ├── __param
│ ├── .return_sites
│ ├── .rodata
│ ├── .rodata.str1.1
│ ├── .strtab
│ └── .symtab
├── srcversion
├── taint
├── uevent
└── version
5 directories, 28 files
这一部分的知识仅仅是在这里引出提一下,建立个映象,在这里就不再赘述,如果想进一步了解可以参考linux设备驱动程序–sysfs。
Linux 内核在进行模块装载时先完成模块的 CRC 值校验,再核对 vermagic 中的字符信息,linux版本:在include/generated/utsrelease.h
中定义,文件中的内容如下:#define UTS_RELEASE "6.1.0-13-amd64"
,utsrelease.h
是kernel编译后自动生成的,用户更改里面的内容不会有效果。
在init/version-timestamp.c
中,定义了kernel启动时的第一条打印信息:
/* FIXED STRINGS! Don't touch! */
const char linux_banner[] =
"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";
这里UTS_RELEASE在kernel编译时自动生成
在init/main.c的start_kernel函数中,有kernel启动的第一条打印信息,这条信息是dmesg命令打印出来:
pr_notice("%s", linux_banner);
在linux/vermagic.h
中定义有VERMAGIC_STRING
:
#define VERMAGIC_STRING \
UTS_RELEASE " " \
MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT \
MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS \
MODULE_ARCH_VERMAGIC \
MODULE_RANDSTRUCT
VERMAGIC_STRING
不仅包含内核版本号,还包含有内核使用的gcc版本,SMP与PREEMPT等配置信息。模块在编译时,我们可以看到屏幕上会显示"MODPOST"。在此阶段,VERMAGIC_STRING
会添加到模块的modinfo段。在内核源码目录下scripts\mod\modpost.c
文件中可以看到模块后续处理部分的代码。
/**
* Header for the generated file
**/
static void add_header(struct buffer *b, struct module *mod)
{
buf_printf(b, "#include \n" );
/*
* Include build-salt.h after module.h in order to
* inherit the definitions.
*/
buf_printf(b, "#define INCLUDE_VERMAGIC\n");
buf_printf(b, "#include \n" );
buf_printf(b, "#include \n" );
buf_printf(b, "#include \n" );
buf_printf(b, "#include \n" );
buf_printf(b, "#include \n" );
buf_printf(b, "\n");
buf_printf(b, "BUILD_SALT;\n");
buf_printf(b, "BUILD_LTO_INFO;\n");
buf_printf(b, "\n");
buf_printf(b, "MODULE_INFO(vermagic, VERMAGIC_STRING);\n");
buf_printf(b, "MODULE_INFO(name, KBUILD_MODNAME);\n");
buf_printf(b, "\n");
buf_printf(b, "__visible struct module __this_module\n");
buf_printf(b, "__section(\".gnu.linkonce.this_module\") = {\n");
buf_printf(b, "\t.name = KBUILD_MODNAME,\n");
if (mod->has_init)
buf_printf(b, "\t.init = init_module,\n");
if (mod->has_cleanup)
buf_printf(b, "#ifdef CONFIG_MODULE_UNLOAD\n"
"\t.exit = cleanup_module,\n"
"#endif\n");
buf_printf(b, "\t.arch = MODULE_ARCH_INIT,\n");
buf_printf(b, "};\n");
if (!external_module)
buf_printf(b, "\nMODULE_INFO(intree, \"Y\");\n");
buf_printf(b,
"\n"
"#ifdef CONFIG_RETPOLINE\n"
"MODULE_INFO(retpoline, \"Y\");\n"
"#endif\n");
if (strstarts(mod->name, "drivers/staging"))
buf_printf(b, "\nMODULE_INFO(staging, \"Y\");\n");
if (strstarts(mod->name, "tools/testing"))
buf_printf(b, "\nMODULE_INFO(test, \"Y\");\n");
}
模块编译生成后,通过modinfo mymodule.ko
命令可以查看此模块的vermagic等信息。
内核的模块装载器里保存有内核的版本信息,在装载模块时,装载器会比较所保存的内核vermagic与此模块的modinfo段里保存的vermagic信息是否一致,两者一致时,模块才能被装载。为了使两个版本一致:可以把依赖源码中的include/linux/vermagic.h
中的UTS_RELEASE
修改成与目标机器的版本一致,这样,再次编译模块就可以了。