linux 设备驱动程序 (2) —— 构造和运行模块

来源:互联网 发布:宠物用品软件排行 编辑:程序博客网 时间:2024/06/05 06:23
 一 设置测试系统
在2.6内核中构造模块,需要系统中中配置并构造好内核树,先前的版本只需要有一套内核头文件。
2.6内核的模块要和内核源码树中的目标文件连接,可得到一个更加健壮的模块加载器。

二 HelloWord模块
用到的宏
module_init
module_exit
分别制定了模块和被加载或卸载时内核调用的函数
MODULE_LICENSE(" " ) 高速内核采用的自由许可证,如果没有,模块装载时会产生抱怨

printk 类似c库的printf 在内核中模块不能依赖于c库,模块装载后可访问内核的公用符号(包括函数和变量)

KERN_ALERT 定义消息的优先级 只是个字符串 如<1> 消息的显示位置依赖内核版本,klogd的版本和配置。

三 核心模块与应用程序的对比
模块退出时必须撤销初始化函数所作的一切
内核中只能调用作为内核一部分的函数,大多数相关头文件保存在include/linux和include/asm目录中,其他子目录中保存有和特定内核子系统相关的头文件。
调试方式不同和应用程序不同。

四 用户控件和内核空间
内核模块运行在内核空间,应用程序运行在用户空间
操作系统的作用是为应用程序提供一个对计算机硬件的一致视图。
操作系统负责程序的独立操作并保护资源不受非法访问。只有cpu能够保护系统软件不受应用程序破坏时不受应用程序破坏时才能完成!
在cpu中实现不同的操作模式,不同的级别具有不同的功能,在较低的级别中禁止某些操作。程序代码只能通过有限数目的门来从一个级别切换到另一个级别。unix系统使用两个级别,在x86中使用最高和最低两个级别。
unix中在最高级别(也称超级用户态)可运行所有操作
而应用程序运行在最低级别(用户态),处理器控制着对硬件的直接访问以及对内存的非授权访问
两个级别有自己的内存映射,也即自己的地址空间
当应用程序执行系统调用或被硬件中断刮起,将切换到内核空间,执行系统调用的内核代码运行在进程上下文中,因此能够访问进程地址空间的所有数据。处理中断的内核代码和进程是异步的,与任何一个特定进程无关。

模块的两类任务:
1 模块中某些函数作为系统调用的一部分
2 其它函数负责中断处理

五 内核中的并发
内核代码必须是可重入的。
要时刻考虑并发问题

六 当前进程
内核代码可通过全局想current获得当前进程
current在<asm/current.h>中定义,struct task_struct的指针。
task_struct在<linux/sched.h>

在2.6中 current不再是一个全局变量,为了支持smp系统,将指针隐藏在内核栈中。
只要引用<linux/sched.h>头文件即可引用当前进程。
current->comm
是当前进程所执行的程序文件的基本名称,如果有必要会剪裁到15个字符以内。

六 其他一些细节
内核栈很小,只有4096字节,要使用大的结构,需要动态分配
带有__的函数名的函数,通常是接口的底层组件,需要谨慎使用
内核代码不能实现浮点预算,内核不需要浮点运算,因为属于额外的开销

七 编译和装载

2.4内核只需要内核头文件,即可编译

2.6内核需要通过内核源码中的makefile来构建

编译模块需要内核源码树,并且是编译过的,因为编译的模块需要和内核源码中的一些目标文件进行链接,从而得到稳定的模块装载器。
如果源码版本或者配置与当前运行的内核不同可能造成编译出来的模块无法在当前内核上运行
Makefile中最简单只需要
obj-m := hello.o
编译命令
make -C /usr/src/linux-2.6.*/ M=`pwd` modules
M=指定构造的模块的源码目录

有一种复杂的

ifneq ($(KERNELRELEASE), )
   obj
-m := hello.o
else
   KERNELDIR 
?= /lib/modules/$(shell uname -r)/build
   PWD :
= $(shell pwd)
default:
    $(MAKE) 
-C $(KERNELDIR) M=$(PWD) modules

endif

 

如果KERNELRELEASE定义了,说明是通过内核系统构造

如果没有则设置变量,然后通过内核系统构造


因此该Makefile会运行两次


 

八 装载和卸载模块

使用insmod将模块装入内核,使用内核符号表解析模块中未解析的符号

内核不会修改模块,仅修改内存中的副本

insmod可以接受命令行参数,可提高模块的可配置性

insmod以来kernel/module.c中的一个系统调用 sys_init_module,它给模块分配内核内存(vmalloc), 然后将模块复制到内存区域,解析符号表,最后调用模块初始化函数

只有系统调用的名字前带有sys_前缀

modprobe也可以装载模块,他会考虑模块的依赖关系,insmod则会强制插入,并返回错误

rmmod 模块正在被使用时禁止移除模块(配置内核人可以强制移除)

lsmod列出当前模块,通过读取/proc/modules来实现


有关以装载的模块信息可到/sys/module下找到

九 版本依赖

在缺少modversions的情况下,模块代码必须针对每个内核重新编译

模块和特定内核版本定义的数据结构和函数原型紧密关联

内核不会假定一个模块的是正确的内核版本。

模块构造过程中会和vermagic.o链接,该文件包含大量有关内核的信息,包括版本,编译器版本以及一些重要配置变量,这些信息用来装载时进行兼容性检测


跨不同版本请使用#ifdef来构造编译代码,linux/version.h中有相关定义,该头文件自动包含于linux/module.h

并定义了如下宏

UTS_RELEASE 内核版本的字符串

LINUX_VERSION_CODE 内核版本的二进制表示

KERNEL_VERSION(major, minor, release) 用来创建内核版本的二进制表示

十 平台依赖

驱动程序最好考虑肯定能的不同处理器的变种

十一内核符号表

公共内核符号表中包含了所有的全局内核项(函数和变量)的地址

模块被装入后,它所有到处的符号都会变成内核符号表的一部分

通常,模块只需实现自己的功能,无需导出符号

模块层叠技术

模块导出符号应该使用一下的宏
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
_GPL版本导出的符号只能被GPL许可证下的模块使用
符号必须在全局部分导出,因为上面的宏被扩展为一个特殊的变量声明,而该变量必须是全局的
该变量将在模块可执行文件的特殊部分保存,内核通过该段找出导出变量

十二预备知识
linux/module.h 包含可装载模块需要的符号和函数定义
linux/init.h 目的是为了初始化和清除函数
linux/moduleparam.h 可以在装载模块时向模块传递参数

内核能识别的许可证 GPL, GPLv2, GPL and additional rights, Dual BSD/PGL, Dual MPL/GPL, Propretary(专有),如果没有使用以上协议,则视为专有

十三初始化和关闭
static int __init init_function(void) { }
__init 用来标记该函数是初始化函数,当初始化完成后,就将该函数占用的内存释放。不要再初始化以后再使用该函数

grep EXPORT_SYMBOL 可以找到不同驱动程序的入口点
大部分注册函数名称带有register_ 前缀

十四 清除函数
static void __exit cleanup_function(void) { }
__exit 标识该函数值用于模块卸载 如果内核不支持卸载模块,则__exit的函数被丢弃(编译器将该函数放在特殊的ELF段中)
如果未定义清楚函数,内核则不允许卸载该模块

十五 初始化过程中的错误处理
在某个注册失败时应该尽可能通过降低功能来继续运转
初始化失败后,应该自行销毁已注册的设施
内核中尝试用goto语句来处理

<linux/errno.h> 错误编码

十六 模块装载竞争
为完成内部初始化之前,不要注册任何设施,内核可能在初始化期间调用模块

十七 模块参数
insmod hellop howmany=10

参数必须使用module_param宏来声明 定义在moduleparam.h
static int howmany = 1;
module_param(变量名称, 类型, sysfs访问许可)
支持的类型 bool invbool charp int long short uint ulong ushort
访问许可定义在<linux/stat.h>
模块参数会在/sys/module 中出现

十八 用户空间驱动程序
X11就是用户空间驱动程序
用户空间不能使用中断 (ia32上可使用vm86)
响应时间很慢,只是因为客户端和硬件之间传输数据和动作需要上下文切换


原创粉丝点击