i.MX6ULL学习笔记--驱动函数初始化

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

相关函数

上一篇:IOS缓存之NSCache缓存


下一篇:为了方便可灌入自定义方法AppendLog 比如File