linux驱动程序——入门-CSDN博客
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
编写linux驱动程序——HelloWorld
环境
$ uname -r
6.1.0-13-amd64
基本步骤
一、建立Linux驱动框架装载、卸载Linux驱动
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函数注册。
五、编写业务逻辑
没什么可说的总不能注册一些空的回调函数什么也不做吧。
六、编写Makefile文件
Linux内核源码的编译规则是通过Makefile文件定义的每个Linux驱动程序必须要有一个Makefile文件。
七、编译Linux驱动程序
Linux驱动程序可直接编译进内核使用obj-y编译也可以作为模块单独编译使用obj-m编译。
八、安装和卸载Linux驱动
如果将驱动编译进内核只要Linux使用该内核驱动程序就会自动装载。如果Linux驱动程序以模块单独存在需要使用insmod或modprobe命令装载Linux驱动模块使用rmmod命令卸载该模块。
hello world
// hello_world.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
//指定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支持动态地添加和删除模块所以我们可以直接在系统中进行加载
sudo insmod helloworld.ko
我们可以通过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!!!
hello_world PLUS版本
在上面实现了一个linux内核驱动程序(虽然什么也没干)接下来我们再来添加一些小功能来丰富这个驱动程序
- 添加模块信息
- 模块加载时传递参数。
// hello_world_PLUS.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
// 添加了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
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);
驱动模块的version magic信息是怎么生成的
在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 <linux/module.h>\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 <linux/build-salt.h>\n");
buf_printf(b, "#include <linux/elfnote-lto.h>\n");
buf_printf(b, "#include <linux/export-internal.h>\n");
buf_printf(b, "#include <linux/vermagic.h>\n");
buf_printf(b, "#include <linux/compiler.h>\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
修改成与目标机器的版本一致这样再次编译模块就可以了。
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |