此前在《WSL2下Ubuntu22.04使用Qemu搭建虚拟Vexpress-A9开发板》系列文章中,我们已建立好Linux最小系统的运行环境,并将其成功移植到了由Qemu模拟的arm32开发板上。接下来将介绍如何基于上述环境进行驱动开发。
本节主要带各位读者了解Linux内核驱动的基本架构,并在WSL的Ubuntu22.04子系统下实现基于x86操作系统的简易Linux驱动“HelloWorld”。
Linux驱动框架
#include <linux/module.h> //包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义,如:MODULE_LICENSE
/*init初始化函数在模块被插入进内核时调用,主要作用为驱动功能做好预备工作
被称为模块的入口函数
__init的作用 :
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text"))) 实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init mydriver_init(void)
{
……………………
return 0;
}
/*exit退出函数在模块从内核中被移除时调用,主要作用做些init函数的反操作
被称为模块的出口函数
__exit的作用:
1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text"))) 实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit mydriver_exit(void)
{
……………………
}
/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2" "GPL and additional rights" "Dual BSD/GPL" "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用
其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:
myhello:module license 'unspecified' taints kernel
Disabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");
/*
module_init 宏
1. 用法:module_init(模块入口函数名)
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(mydriver_init);
/*
module_exit宏
1.用法:module_exit(模块出口函数名)
2.动态加载的模块在卸载时,对应函数被调用
3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(mydriver_exit);
以上就是Linux操作系统中驱动程序的基本框架,主要包涵三个部分,一个是模块入口部分,另一个是模块出口部分,最后是源码许可证协议声明。驱动程序中只要存在这三个部分,就能被内核所认可,编译通过,且可以加载运行,但仅有这几个模块是不够的,后续章节将详细讲解驱动代码的其他重要组成部分,在本小节先做一个HelloWorld代码并尝试让它跑起来。
Hello World
进入WorkSpace,并创建一个新的文件夹Drivers,用于存放以后开发的所有驱动代码,同时所有代码共享同一个Makefile。
makedir -p /home/workspace/drivers
cd /home/workspace/drivers
vim myhello.c
在myhello.c驱动文件中填入以下内容:
/*
/home/workspace/drivers/myhello.c
*/
#include <linux/module.h>
#include <linux/kernel.h>
int __init myhello_init(void)
{
printk("##############################################\n");
printk("##############################################\n");
printk("##############################################\n");
printk("################ Hello World! ################\n");
printk("##############################################\n");
printk("##############################################\n");
printk("##############################################\n");
return 0;
}
void __exit myhello_exit(void)
{
printk("myhello will exit\n");
}
MODULE_LICENSE("GPL");
module_init(myhello_init);
module_exit(myhello_exit);
接下来新建一个Makefile:
vim Makefile
在Makefile文件中输入以下内容:
ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= /home/workspace/linux-5.10.186
OBJECTDIR ?= /home/workspace/objects/vexpress-v2p-ca9
ROOTFS ?= /sync/rootfs
CROSS_COMPILE ?= arm-linux-gnueabi-
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.o *.ko .*.cmd *.mod* modules.order Module.symvers .tmp_versions
else
obj-m += myhello.o
endif
宿主机运行环境搭建
如果是完整的Ubuntu操作系统,则可以省掉这个步骤,直接对上述代码进行make操作,编译驱动代码。但如果Ubuntu操作系统不完整,或者是属于WSL这种微软特供版子系统,则需要进行其他操作,才能确保驱动编译过程中不会出现环境问题。
首先查看操作系统内核版本:
uname -r
如果是WSL子系统,则显示结果和上图差不多,如果不是微软特供版的内核,则可以输入以下命令查看内核编译环境是否完整:
ls /lib/modules/$(uname -r)
如果显示文件夹下存在build、kernel、source三个子文件夹,则说明内核编译环境基本完整,可以尝试对驱动代码进行编译,如果以上命令提示文件夹不存在之类的情况,则尝试按照下列步骤进行操作。
通用内核
如果输入uname -r显示的不是微软特供版,而是类似于4.15.0-162-generic之类的格式,则说明操作系统采用的是Linux通用内核,输入以下指令可以直接安装编译环境。
sudo apt install linux-headers-$(uname -r)
WSL内核
如果输入uname -r显示的是类似5.15.90.1-microsoft-standard-WSL2的格式,则说明操作系统采用的是微软特供版内核,需要下载源码单独进行编译安装。
进入 Releases · microsoft/WSL2-Linux-Kernel · GitHub 网站上下载对应uname -r显示的内核源码,然后执行下述步骤。
安装依赖
sudo apt install libelf-dev build-essential pkg-config bison build-essential flex libssl-dev libelf-dev bc dwarves
将内核源码拷贝到workspace(其中,/.../为源码下载到本地的路径)
sudo cp /.../WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1.tar.gz /home/workspace
进入workspace并解压
cd /home/workspace
tar -xvf WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1.tar.gz
进入源码目录并编译安装
cd WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1
cp Microsoft/config-wsl .config
执行以下指令进行编译安装
sudo make scripts
sudo make modules -j$(nproc)
sudo make modules_install
安装成功后提示如下图所示
其中有个错误no binutils support,忽略即可,目前不需要直接编译x86架构的32位程序。
编译加载x86架构驱动
编译驱动
cd /home/workspace/drivers
sudo make
加载驱动
sudo insmod myhello.ko
任意终端输入dmesg查看内核打印信息:
移除驱动
sudo rmmod myhello
任意终端输入dmesg查看内核打印信息:
可以注意到,加载内核的过程中出现loading out-of-tree module taints kernel.提示,意思是加载的树外模块污染了内核。这是我们没有把此驱动模块加入到Kconfig树导致的,即make menuconfig的配置选项中没有此驱动,此时这个驱动模块仍能正常加载和使用。如果仅仅用于学习和验证驱动,则可以忽略此提示,但如果是要求正式写入操作系统的驱动,则需要在Kconfig树中加入驱动信息,这样操作可以直接在menuconfig中选择是否加载驱动,同时也不会出现loading out-of-tree module taints kernel.提示了。文章来源:https://www.toymoban.com/news/detail-755521.html
下一节将介绍如何在Linux内核源码内添加驱动,并在操作系统启动时静态加载驱动代码。文章来源地址https://www.toymoban.com/news/detail-755521.html
到了这里,关于Qemu虚拟arm开发板驱动开发详解(一)——驱动基本架构的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!