i.MX6ULL学习笔记--驱动函数初始化
简介
从Linux的架构来看,内核系统已经将驱动作为了一个独立的子模块。所以驱动模块可以在内核系统运行时进行加载(insmod)和卸载(remmod)的操作。
从功能上来看,驱动模块承担着将系统函数调用和硬件动作进行映射的功能。即他提供给内核系统对硬件进行操作的接口,用户在进程中,通过调用系统函数将进程进入内核空间,内核使用超级权限调用驱动接口函数,从而实现对硬件的控制!这样一来,可以将机制(怎么驱动硬件)和策略(怎么使用硬件)分开来操作。
每个模块由目标代码组成,但实际上并没有被编译成可执行的程序!
Linux系统将设备模块分为三类:字符模块(类似文件一样可以像字节流一样访问的设备)、块模块或者网络模块。
无论是哪一种模块,驱动开发的时候都必须提醒自己,驱动程序是可以重进入的,即驱动程序可能会被多个程序通过内核系统同时调用!
编译内核
值得注意的是,由于驱动模块是运行在内核空间中,所以驱动程序只能调用内核提供的函数库。例如printk函数(与printf函数有一定的区别)。
//驱动源代码,一个没有实际作用的驱动程序
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("DUAL BSD/GPL");//若不向内核声明,编译时内核会发出警告
static int hello_init(void)
{
printk(KERN_ALERT "hello,world");
return 0;
}
static int exit_init(void)
{
printk(KERN_ALERT "Goodbye");
}
module_init(hello_init);//驱动被超级用户装载后会进入这个指定的入口
module_exit(hello_exit);//驱动被超级用户卸载后会进入这个指定的入口
MODULE_LICENSE("GPL2");
MODULE_AUTHOR("xxx");
MODULE_ALIAS("xxx");
编译的过程与平常程序一样,需要借助make工具
#驱动目录下的makefile文件
KERNEL_DIR=/home/x/Linux_info/ebf-buster-linux/build_image/build
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH CROSS_COMPILE #传递参数ARCH、CROSS_COMPILE给下一级makefile文件
obj-m := helloworld.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules#在makefile中 $(CURDIR)默认是当前目录
.PHONE:clean copy
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
copy:
sudo cp *.ko /home/x/info_baidu/workspace
这段makefile文件描述了内核需要生成一个hello.ko的文件,而这个文件需要从hello.o中构造出来。为了调用内核的驱动编译makefile文件,需要利用make -C指令到指定的目录下(内核源代码编译时生成的build目录)再运行make指令。
实际上这个 ~/kernel-2.6 目录并非是固定的,他依赖得到内核源代码后编译时的配置文件,在ubuntu系统中,源代码不适用于直接编译,需要下载工具和源代码
通过指令apt install make gcc-arm-linux-gnueabihf gcc bison flex libssl-dev dpkg-dev lzop
下载工具
通过指令git clone https://gitee.com/Embedfire/ebf-buster-linux.git
下载源代码到当前目录
在源代码目录中,有一个名为 make_deb.sh 的脚本文件,其中有一句代码build_opts="${build_opts}O=build_image/build"
标明了内核的顶层makefile文件的路径。
在源代码路径下直接运行指令make
,稍作等待即可完成编译!
#顶层makefile文件
# Automatically generated by /home/x/Linux_info/ebf-buster-linux/scripts/mkmakefile: don't edit
VERSION = 4
PATCHLEVEL = 19
lastword = $(word $(words $(1)),$(1)) #读取第一个形参的最后一个字符串
makedir := $(dir $(call lastword,$(MAKEFILE_LIST))) #执行函数
ifeq ("$(origin V)", "command line")
VERBOSE := $(V)
endif
ifneq ($(VERBOSE),1)
Q := @
endif
MAKEARGS := -C /home/x/Linux_info/ebf-buster-linux
MAKEARGS += O=$(if $(patsubst /%,,$(makedir)),$(CURDIR)/)$(patsubst %/,%,$(makedir))
MAKEFLAGS += --no-print-directory
.PHONY: __sub-make $(MAKECMDGOALS)
__sub-make:
$(Q)$(MAKE) $(MAKEARGS) $(MAKECMDGOALS) #进入源代码的目录执行make
$(filter-out __sub-make, $(MAKECMDGOALS)): __sub-make
@:
验证上诉变量并不难,可以通过echo指令输出自己想知道的变量!
初始化
初始化需要用到两个头文件,详细参考附录。
//初始化函数
#include <linux/init.h>
#include <linux/module.h>
static int __init initialization_function(void)
{
printk(KERN_ALERT "hello");
}
module_init(initialization_function);//驱动被超级用户装载后会进入这个指定的入口
module_exit(xxx);//驱动被超级用户卸载后会进入这个指定的入口
初始化函数中的 __init 标志对于内核来说是一个提醒,即这段代码不会长时间存在与内存空间中,当模块初始化完成调用该函数后就可以从内存中移除。
在编写驱动的初始化时,需要时刻警惕初始化失败的情况,在内核中初始化失败的驱动程序无法进行卸载,所以需要在初始化函数中自行判断和处理。
有两种方案:
1、使用goto语句,判断到错误时,直接跳转到相应的地址,卸载已经成功安装了的部分。
2、每次成功注册的驱动都记录下来,若判断到一个错误,则采用回滚形式卸载所有成功注册的驱动。
装载和卸载
在驱动程序编译完成后,生成了*.ko文件,将该文件拷贝到需要装载的环境下。并输入指令insmod ./*.ko
终端在装载完成后,会执行初始化函数中的任务。
在终端执行insmod指令后,通过调用宏SYSCALL_DEFINE3();将*.ko的文件传递到程序空间中,然后再进行一系列的操作(ELF文件格式的变换),最后装载成功。
//来自源代码 kernel/module.s
#include <linux/syscalls.h>
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
err = may_init_module();
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
umod, len, uargs);
err = copy_module_from_user(umod, len, &info); //拷贝
if (err)
return err;
return load_module(&info, uargs, 0); //加载
}
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{
struct load_info info = { };
loff_t size;
void *hdr;
int err;
err = may_init_module();
if (err)
return err;
pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags);
if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS
|MODULE_INIT_IGNORE_VERMAGIC))
return -EINVAL;
err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX,
READING_MODULE);
if (err)
return err;
info.hdr = hdr;
info.len = size;
return load_module(&info, uargs, flags);
}
内核已经逐步更新了系统函数的表达方式,通过宏封装,将函数通过统一的通道进行访问。例如这里的module.c中,使用库<linux/syscalls.h>
//form <linux/syscalls.h>
#define SYSCALL_METADATA(sname, nb, ...)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
/*
* The asmlinkage stub is aliased to a function named __se_sys_*() which
* sign-extends 32-bit ints to longs whenever needed. The actual work is
* done within __do_sys_*().
*/
#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...) \
__diag_push(); \
__diag_ignore(GCC, 8, "-Wattribute-alias", \
"Type aliasing is used to sanitize syscall arguments");\
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(__se_sys##name)))); \
ALLOW_ERROR_INJECTION(sys##name, ERRNO); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
__diag_pop(); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */
模块参数
模块参数和全局变量有点类似,函数中通常使用变量来传递某些状态,而模块之间则使用参数。为了传递参数的需要,内核提供了相关的宏定义
//parament.c
//参数传递
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int para_i = 0;
module_param(para_i,int,0);
static bool para_b = 0;
module_param(para_b,bool,0);
static char para_c = 0;
module_param(para_c,byte,0);
static char * para_cp = 0;
module_param(para_cp ,charp,0);
static int __init param_init(void)
{
printk(KERN_ALERT "the module of param is initial!\n");
printk(KERN_ALERT "para_i = %d \n",para_i);
printk(KERN_ALERT "para_b = %d \n",para_b);
printk(KERN_ALERT "para_c = %d \n",para_c);
printk(KERN_ALERT "para_cp = %s \n",para_cp);
return 0;
}
static void __exit param_exit(void)
{
printk(KERN_ALERT "the module of param is rmmod!\n");
}
static int my_add(int,a,int b)
{
return a+b;
}
EXPORT_SYMBOL(para_i);//共享para_i
EXPORT_SYMBOL(my_add);//共享函数
static int my_sub(int,a,int b)
{
return a-b;
}
EXPORT_SYMBOL(my_sub);
module_init(param_init);//驱动被超级用户装载后会进入这个指定的入口
module_exit(param_exit);//驱动被超级用户卸载后会进入这个指定的入口
//calculation.c
//参数传递
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <calculation.h>
static int __init calculation_init(void)
{
printk(KERN_ALERT "the module of calculation is initial!\n");
printk(KERN_ALERT "para_i - 1 = %d ,para_i + 1 = %d \n",my_add(para_i,i),my_sub(para_i,1));
return 0;
}
static void __exit calculation_exit(void)
{
printk(KERN_ALERT "the module of param is rmmod!\n");
}
module_init(calculation_init);//驱动被超级用户装载后会进入这个指定的入口
module_exit(calculation_exit);//驱动被超级用户卸载后会进入这个指定的入口
在第一个模块中声明参数para_i和函数my_add();在param模块被声明后calculation模块才会被正常声明,否则会出现错误,这是由于calculation模块引用了某些外部的标识。
卸载过程则是相反的,原理和脱靴一致。
值得注意的是在声明参数para_c时使用的类型是byte。
附录
头文件
#include <linux/init.h> : 这个头文件的目的是指定初始化和清楚函数
#include <linux/module.h>:这个头文件包含有可装载模块需要的大量符号和函数定义
相关宏
module_init();
module_exit();
module_param();