linux驱动程序——入门

编写linux驱动程序

环境:

$ uname -r
5.18.17-amd64-desktop-hwe
$ uname -a
Linux dev-PC 5.18.17-amd64-desktop-hwe #20.01.00.10 SMP PREEMPT_DYNAMIC Thu Jun 15 16:17:50 CST 2023 x86_64 GNU/Linux

背景

linux 根据设备的共同特征将设备划分为三大类型:

  1. 字符设备
    以字节为单击进行I/O传输
  2. 块设备
    以块为单位进行I/O传输
  3. 网络设备
    网络设备是一类比较特殊的设备,涉及网络协议层

在这里插入图片描述

基本步骤

一、建立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_DESCRIPTION("hello world");
// 提供别名
MODULE_ALIAS("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命令来查看模块信息:

$ sudo modinfo helloworld.ko
filename:       /home/dev/workspace/learn_linux_driver/hello_world/helloworld.ko
alias:          hello
description:    hello world
author:         Marvin
license:        GPL
srcversion:     E09CBAA95387AC8A79E4989
depends:        
retpoline:      Y
name:           helloworld
vermagic:       5.18.17-amd64-desktop-hwe 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!!!

加载模块后,系统会在/sys/module目录下面新建一个目录:

$ tree /sys/module/helloworld -a  
/sys/module/helloworld
├── coresize
├── holders
├── initsize
├── initstate
├── notes
│   ├── .note.gnu.build-id
│   └── .note.Linux
├── refcnt
├── sections
│   ├── .exit.text
│   ├── .gnu.linkonce.this_module
│   ├── .init.text
│   ├── __mcount_loc
│   ├── .note.gnu.build-id
│   ├── .note.Linux
│   ├── .return_sites
│   ├── .rodata.str1.1
│   ├── .strtab
│   └── .symtab
├── srcversion
├── taint
└── uevent

3 directories, 19 files

hello_world_plus 版本

在上面实现了一个linux内核驱动程序(虽然什么也没干),接下来我们再来添加一些小功能来丰富这个驱动程序:

  • 添加模块信息
  • 模块加载时传递参数。
// hello_world_plus.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_AUTHOR("marvin");
MODULE_DESCRIPTION("Linux kernel driver - hello_world plus!");
MODULE_VERSION("0.1");

MODULE_LICENSE("GPL");

static char *name = "world";
module_param(name, charp, S_IRUGO|S_IWUSR);
MODULE_PARM_DESC(name, "name,type: char *, permission: S_IRUGO|S_IWUSR");

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);
}

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/dev/workspace/learn_linux_driver/hello_world_plus/helloworldplus.ko
license:        GPL
version:        0.1
description:    Linux kernel driver - hello_world plus!
author:         marvin
srcversion:     28162B4946BD7DDC9A9A9CA
depends:        
retpoline:      Y
name:           helloworldplus
vermagic:       5.18.17-amd64-desktop-hwe SMP preempt mod_unload modversions 
parm:           name:name,type: char *, permission: S_IRUGO|S_IWUSR (charp)

另外,当设置参数后,加载模块后系统在/sys/module下面的模块内产生参数信息文件:

ls -lh /sys/module/helloworldplus/parameters/
总用量 0
-rw-r--r-- 1 root root 4.0K 6月  17 23:43 name

通过修改这个文件可以对模块对应的参数进行修改,但是要注意对这个参数设置的权限。

总结一下:

  • 模块加载函数:加载模块时,该函数会被自动执行,通常做一些初始化工作
  • 模块卸载函数:卸载模块时,该函数也会被自动执行,做一些清理工作
  • 模块许可声明:内核模块必须声明许可证,否则内核会发出被污染的警告
  • 模块参数:根据需要来添加,可选
  • 模块作者和描述声明:一般需要完善这些信息
  • 模块导出符号:根据需要来添加

sysfs

sysfs是一个文件系统,但是它并不存在于非易失性存储器上(也就是我们常说的硬盘、flash等掉电不丢失数据的存储器),而是由linux系统构建在内存中,简单来说这个文件系统将内核驱动信息展现给用户。

当我们装载模块时,会在/sys/module/目录下生成一个与模块同名的目录,目录里囊括了驱动程序的大部分信息,这部分信息在上面有说明。

这一部分的知识仅仅是在这里引出提一下,建立个映象,在这里就不再赘述,如果想进一步了解可以参考linux设备驱动程序--sysfs

内核版本是如何生成的:

Linux 内核在进行模块装载时先完成模块的 CRC 值校验,再核对 vermagic 中的字符信息,linux版本:在include/generated/utsrelease.h中定义,文件中的内容如下:#define UTS_RELEASE "5.18.17-amd64-desktop-hwe"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修改成与目标机器的版本一致,这样,再次编译模块就可以了。

符号共享

Linux通过EXPORT_SYMBOL()EXPORT_SYMBOL_GPL()这两个宏来提供符号共享的功能。
EXPORT_SYMBOL()把函数或者符号对全部内核代码公开,即将一个函数以符号的方式导出给内核中的其他模块使用。
EXPORT_SYMBOL_GPL()只能包含GPL许可的模块,内核核心绝大部分模块导出的符号时使用GPL这种形式。如果要使用EXPORT_SYMBOL_GPL()导出的函数,那么需要显式地通过模块声明为GPL,如MODULE_LICENSE("GPL");
在上面的hello_world_plus.c中添加下面代码后再次编译:

void test_hello_world_plus(void)
{
    printk(KERN_DEBUG "test!!!\n");
}

EXPORT_SYMBOL_GPL(test_hello_world_plus);

之后在输出目录下会产生Module.symvers文件,该文件显式了导出的符号:

$ cat Module.symvers
0x26b3d60e      test_hello_world_plus   /home/dev/workspace/learn_linux_driver/hello_world_plus/helloworldplus  EXPORT_SYMBOL_GPL

内核导出的符号表可以通过/proc/kallsyms来查看:

$ cat /proc/kallsyms | grep test_hello_world_plus
0000000000000000 r __kstrtab_test_hello_world_plus      [helloworldplus]
0000000000000000 r __kstrtabns_test_hello_world_plus    [helloworldplus]
0000000000000000 r __ksymtab_test_hello_world_plus      [helloworldplus]
0000000000000000 t test_hello_world_plus        [helloworldplus]

其中,第一列显式的是该符号在内核地址空间的地址;第二列是符号属性,t表示在text段中;第三列表示符号的字符串,也就是EXPORT导出来的符号;第四列表示哪些内核模块在使用该符号。

posted @ 2023-11-05 22:53  main_c  阅读(3)  评论(0)    收藏  举报  来源