关于做笔记
首次学习LDD3只是通读,只知晓主体概念,且未做笔记和实践,对其中细节不求甚解。随着时间逝去,原有概念逐渐淡化,重拾LDD3后有种温故而知新的感觉。孔子《论语》: 学而不思则罔 思而不学则殆。我的理解是,思既为实践。在实践中,理解概括所学,而后举一反三。 LDD3是本实践性很强的书,此次重温LDD3也是本着实践精神而来:先实践再理解概括。实践则为按照书中例程亲自操作演练;理解概括则为将书中知识点与实践过程经验总结,加深知识点的理解,并做笔记以备后忘;
关于Hello world的笔记
LDD3被很多人拜读,不少同学读完后做读书笔记并在网络中与大家共享。每人笔记内容层次不同,描述方式及风格也各具特色: 或概要或详细,或点或面,或引例或自举。 “Hello workd”在程序员中是个比较经典短句,学习各种(一门语言,一种程序框架等等)都以“Hello world”做为开始例程。LDD3的“Hello workd” 主要展现了linux下编写驱动的基础框架和驱动的基本特性,而无实质性的内容,因此本篇笔记将记录部分要点。
Makefile文件及编译过程
LDD3的例程可以无需加入到内核编译树中(kbuild编译体系,不知这么描述是否正确),在任何目录下编译。主要依赖于内核新的编译系统kbuild和GNU make的扩展语法。
-
ifeq ($(KERNELRELEASE),)
-
-
# Assume the source tree is where the running kernel was built
-
# You should set KERNELDIR in the environment if it's elsewhere
-
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
-
# The current directory is passed to sub-makes as argument
-
PWD := $(shell pwd)
-
-
modules:
-
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
-
-
modules_install:
-
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
-
-
clean:
-
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
-
-
.PHONY: modules modules_install clean
-
-
else
-
# called from kernel build system: just declare what our modules are
-
obj-m := hello.o hellop.o seq.o jit.o jiq.o sleepy.o complete.o \
-
silly.o faulty.o kdatasize.o kdataalign.o
-
endif
输入make命令编译,变量KERNELRELEASE未设置,makefile有效部分是2~19行。行5设置内核编译树目录变量,行7设置当前目录变量。此makefile的默认目标为modules, 接下来执行其规则:调用GNU make,通过GNU make扩展选项 -C 指定内核编译树目录 (内核编译树目录: 可以是Linux源码目录,也可以是 kernel-devel 安装后Linux编译目录: 包含头文件, makefile, 库及kbuild等),并读取目录中的Makefile 作为makefile继续,这样间接地引入到kbuild编译体系;M 指定了外部模块编译的目录, 而此次调用make的目标即为: modules(kbuild中定义),因此kbuild会再次读取当前目录下的Makefile文件。
第二次读取时,变量KERNELRELEASE已设置(kbuild中设置),因此makefile有效部分是22~23行。此两行为kbuild编译体系定义了编译对象目标obj-m,此时才开始编译“Hello world”。
此makefile中定义obj-m的对象包括多个驱动模块,对象*.o文件;每个*.o文件只对应一个*.c源码文件。但是通常驱动程序包括多个源码文件,那么前面的写法就需要改进下。如果模块包括多个源码文件,则需要在下面添加: modname-objs := src1.c src2.c .... ,其中modname为模块名称,且源码中不能出现modname.c的源码文件。
知道了整个驱动编译流程,借助于kbuild编译体系,外部驱动makefile构成基本模板化。因此编写驱动的makefile比较简单,也能以此makefile文件为模板,编写自己的makefile。
驱动的构造与析构
Linux虽然是用C和汇编编写,但是她的设计处处体现着面向对象的思想:接口抽象,数据及其接口的封装,甚至有多态和继承设计元素。每个驱动在内核看来就是一个对象,实际编写驱动时也会将驱动注册到系统中,LDD3的“设备驱动模型”章节有详细描述。既然说到了对象,那就会有生命的开始和结束;因此内核要求驱动必须提供两个函数:构造函数与析构函数,并将这两个函数声明给内核知道。
-
static int __init hello_init(void)
-
{
-
printk(KERN_ALERT "Hello, world\n");
-
return 0;
-
}
-
-
static void __exit hello_exit(void)
-
{
-
printk(KERN_ALERT "Goodbye, cruel world\n");
-
}
-
-
module_init(hello_init);
-
module_exit(hello_exit)
“Hello world” 例程比较简单,但是它足够地展示编写一个驱动所必须的元素。行1~5定义了构造函数,行7~10定义了析构函数,他们的实现很简单:向这个残酷的世界打声招呼,然后离开。行12~13将构造和析构函数声明给内核,让内核知道他们的存在,以便内核调用它们。构造函数和析构函数的原型如代码所示,但他们与普通的函数有点小小的区别:在返回值类型和函数名称直接多了: __init或者__exit。这两个东东是什么,有什么用?先来看看他们的定义,下面省去了无关代码:
-
#ifndef _LINUX_INIT_H
-
#define _LINUX_INIT_H
-
-
#include <linux/compiler.h>
-
-
......
-
-
#define __init __section(.init.text) __cold notrace
-
......
-
......
-
#define __exit __section(.exit.text) __exitused __cold
-
......
-
......
-
-
#ifndef __ASSEMBLY__
-
/*
-
* Used for initialization calls..
-
*/
-
typedef int (*initcall_t)(void);
-
typedef void (*exitcall_t)(void);
-
......
-
-
#endif
-
-
#ifndef MODULE
-
-
#ifndef __ASSEMBLY__
-
-
#define __define_initcall(level,fn,id) \
-
static initcall_t __initcall_##fn##id __used \
-
__attribute__((__section__(".initcall" level ".init"))) = fn
-
......
-
......
-
#define device_initcall(fn) __define_initcall("6",fn,6)
-
......
-
......
-
#define __initcall(fn) device_initcall(fn)
-
-
#define __exitcall(fn) \
-
static exitcall_t __exitcall_##fn __exit_call = fn
-
......
-
......
-
-
#endif /* __ASSEMBLY__ */
-
-
-
#define module_init(x) __initcall(x);
-
-
#define module_exit(x) __exitcall(x);
-
-
#else /* MODULE */
-
-
/* Each module must use one module_init(). */
-
#define module_init(initfn) \
-
static inline initcall_t __inittest(void) \
-
{ return initfn; } \
-
int init_module(void) __attribute__((alias(#initfn)));
-
-
/* This is only required if you want to be unloadable. */
-
#define module_exit(exitfn) \
-
static inline exitcall_t __exittest(void) \
-
{ return exitfn; } \
-
void cleanup_module(void) __attribute__((alias(#exitfn)));
-
-
.......
-
.......
-
-
#endif /* _LINUX_INIT_H */
由行8, 11可知__init和__exit 是两个宏,扩展为GCC的__section(*****)。当对象定义带有此扩展时,表明想把被定义的对象在链接时放入到特殊段中。 因此构造函数将被放入到".init.text"段,而析构函数放入到".exit.text"段,这么做的原因:".exit.text"段向内核标示此为模块初始化时的代码段,内核可以在构造函数返回后,释放构造函数所占的内存空间。而".exit.text"段向内核标示此为模块卸载时调用的代码段,其他正常代码段,不能调用此段内的代码,否则有可能出错。因此,在其他代码段内不能调用有 __init或__exit修饰的函数。
其实,构造函数与析构函数没有__init和__exit两个宏修饰,也无大碍。因此,有其他地方涉及到这两个函数的代码,可以不加此修饰。
同时在行19~20看到了构造函数与析构函数的原型定义, 行37~40 与行54~63看到了将构造函数与析构函数声明给内核的方法定义,只是一个对应于内核加载,一个对应于动态加载。
当把驱动选为编进内核时,会定义两个函数指标变量分别存储两个函数的地址。构造函数变量链接后存放在".initcall6.init"断中,内核在启动时会调用此段内的函数;析构函数变量比较简单就定义为一个局部静态变量,因为驱动被选为build-in的方式后不卸载的,直到和内核共亡。
当把驱动选为动态加载时,构造与析构函数没有定义存储的变量,而是对对他们分别创建了别名: init_module和cleanup_module。因为模块加载时,不可能知道每个模块构造函数与析构函数名称,因此创建别名,而后统一调用。行55~56与行61~62实际没啥作用,仅仅是在编译时对宏的参数做检测,保证函数原型正确。这类宏参数检测技巧,在Linux用的比较多。
编写构造与析构函数函数是有一点需要注意: 驱动加载/卸载时的竞争。通常驱动初始化时,会向内核注册一些组件或者功能接口,以向内核提供一定的功能。一旦注册成功返回后,内核可能会立刻调用它,此时驱动的构造函数可能还没返回。因此在向内核暴露驱动的功能前,务必保证此功能所需资源准备完毕。同样地,驱动在卸载时,内核也有可能正在使用某个功能。如果不处理而直接返回,有可能导致内核出错。
驱动参数
Linux驱动在加载时,支持指定参数,以改变驱动功能及特性等。不但方便驱动调试,实际运用也非常有效。本质上来说,每个驱动的参数都会额外定义一个静态数据结构变量用来处理参数的读取与设置,并将此变量放入到模块文件的“__param”段中,内核在加载时和搜索此段中数据,并与传入参数对比,从而设置参数值。Linux内核支持常用的参数类型,如: int, long, char*等,并且为每种类型定义了两个函数:读取与设置,此函数由内核提供。当然了,也可以自定义自己的参数类型,那就需要添加对应的函数,具体可参考:moduleparam.h。
阅读(2343) | 评论(0) | 转发(0) |