Linux设备驱动程序学习(二)——准备工作和模块概念了解

   前面是介绍Linux驱动设备程序的概念,这一部分的话才是真正开始学习Linux驱动设备程序的编写,这一部分主要是准备好Linux内核的源码树,然后通过源码树来介绍模块构造和运行,通过实例的模块程序来学习模块的装载和卸载。

设置测试系统

   准备好一个Linux内核源码树,就相当于java中准备好编译环境和编译工具,步骤为:

  1. 查看当前Linux系统的内核版本
      alex_wu@wu:~$ uname -r 
      4.8.0-36-generic        #内核版本
  1. 下载对应版本的内核源代码
    sudo apt-get install linux-source-4.8.0
  2. 切换管理员模式,解压下载的源码包
    tar jxvf linux-source-4.8.0.tar.bz2
    解压后生成一个新的目录/usr/src/linux-source-4.8.0,所有的源代码都在该目录下。 当然如果是Unbtu的话,在这个目录下本来就存在内核源码树的头文件,只有头文件也可以实现Linux驱动程序的编写,不过最好学会怎么下载和编译内核源码树,这样能够了解的深入一些。
  3. 编译内核
    按原来的kernel配置,配置kernel,根据顺序敲击以下的命令:
 sudo make oldconfig
 make(相当耗时),开始编译内核
 sudo make
 编译内核镜像
 sudo make bzImage
 安装内核模块
 make modules_install
执行结束之后,会在/lib/modules下生成新的目录/lib/modules/4.8.0-36-generic/。 
在随后的编译模块文件时,要用到这个路径下的build目录。至此,内核编译完成。可以重启一下系统。

内核模块和应用程序的区别

  • 不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模块只注册自己以便来服务将来的请求, 并且它的初始化函数立刻终止。
  • 模块初始化函数的任务是为以后调用模块的函数做准备; 模块的退出函数就在模块被卸载时调用。这种编程的方法类似于事件驱动的编程, 但是虽然不是所有的应用程序都是事件驱动的, 每个内核模块都是。
  • 另外一个主要的不同, 在事件驱动的应用程序和内核代码之间, 是退出函数: 一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.
  • 一个应用程序可以调用它没有定义的函数: 连接阶段使用合适的函数库解决了外部引用。 printf 是一个这种可调用的函数并且在 libc 里面定义。一个模块, 在另一方面, 只连接到内核, 它能够调用的唯一的函数是内核输出的那些; 没有库来连接。
  • 内核编程和应用程序编程之间的重要不同是每一个环境是如何处理错误: 在应用程序开发中段错误是无害的, 一个调试器常常用来追踪错误到源码中的问题, 而一个内核错误至少会杀掉当前进程, 如果不终止整个系统.

用户空间和内核空间

   cpu在被设计时,有保护系统软件不被应用程序破坏的功能。且这种保护功能分为不同级别,当cpu中存在多个级别时,unix通常使用最高级和最低级,即:超级用户级和用户级,也即内核空间和用户空间。

内核中的并发

常见引起并发原因:

  • linux系统中通常正在运行多个并发进程,并且可能有多个进程同时使用我们的驱动程序。
  • 大多数设备能够中断处理器,而中断处理程序异步运行,而且可能在驱动程序正试图处理其他任务时被调用。
  • linux可以运行在多处理器上,因此可能同时有多个处理器在使用该进程。

当前进程

   Current 在<asm.current.h>中定义,是一个指向struct task_struct的指针,而task_struct结构在<linux/sched.h>中定义。
   Current指针指向当前正在运行的进程;
   在open,read等系统调用的执行过程中,当前进程指的是调用这些系统调用的进程。

struct task_struct *current;
current->id :当前进程的id
current->comm. :当前进程的命令名

细节拓展

  • 如果我们需要大的结构,应该调用动态分配该结构,而不是声明大的自动变量。
  • 常见函数前加有__两个下划线,这种函数通常是接口的底层组件,实际上,双下划线是告诉程序员:谨慎使用,后果自负
  • 内核代码不支持浮点数运算。

编译和装载

编译模块

   确保安装了正确版本的编译器,模块工具,和其它必要的工具,内核文档Documentation/Changes文件列出了需要的工具版本。
   makefile解析
   obj-m:由内核构造系统使用的makefile符号,用来确定当前目录中应构造哪些模块。
   Makefile文件解析:

/*如果已经构造了KERNELRELEASE,则说明是从内核构造系统调用的,因此可以利用其内建语句。*/
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

注意: 这个 makefile 在一次典型的建立中要被读 2 次, 当从命令行中调用这个 makefile , 它注意到 KERNELRELEASE 变量没有设置. 它利用这样一个事实来定位内核源码目录, 即已安装模块目录中的符号连接指回内核源码树。如果你实际上没有运行你在为其而建立的内核, 你可以在命令行提供一个 ERNELDIR= 选项, 设置 KERNELDIR 环境变量, 或者重写 makefile 中设置 KERNELDIR 的那一行。一旦发现内核源码树, makefile 调用 default:目标, 来运行第 2 个 make 命令( 在 makefile 里参数化成 $(MAKE))象前面描述过的一样来调用内核建立系统。在第 2 次读, makefile 设置 obj-m, 并且内核的 makefile 文件完成实际的建立模块工作。当然 一个真正的 makefile 包含通常的目标类型来清除不要的文件, 安装模块等等。

装载和卸载模块

   模块建立之后, 下一步是加载到内核,insmod 完成这个工作。这个程序加载模块的代码段和数据段到内核, 接着, 执行一个类似 ld 的函数, 它连接模块中任何未解决的符号连接到内核的符号表上。
   modprobe 工具值得快速提及一下。 modprobe, 和insmod类似, 加载一个模块到内核. 它的不同在于它会查看要加载的模块, 看是否它引用了当前内核没有定义的符号。如果发现有, modprobe 在定义相关符号的当前模块搜索路径中寻找其他模块。当 modprobe 找到这些模块( 要加载模块需要的 ), 它也把它们加载到内核。如果你在这种情况下代替以使用 insmod , 命令会失败, 在系统日志文件中留下一条 " unresolved symbols "消息。
   模块可以用 rmmod工具从内核去除. 注意, 如果内核认为模块还在用( 就是说, 一个程序仍然有一个打开文件对应模块输出的设备 ), 或者内核被配置成不允许模块去除, 模块去除会失败. 可以配置内核允许"强行"去除模块, 甚至在它们看来是忙的. 如果你到了需要这选项的地步, 但是, 事情可能已经错的太严重以至于最好的动作就是重启了.

总结:
  • 只有系统调用函数的名字前边带有sys_前缀。
  • modprob区别于insmod : modprob会考虑要装载的模块是否引用了一些当前内核不存在的符号,如果是存在,modprob回查找,而insmod会失败,并在系统日志文件中记录”unresolved symbols”消息。
  • lsmod列出当前装载到内核中的所有模块。

Linux设备驱动程序的一些基本知识

头文件和代码许可

   大部分内核代码包含了许多数量的头文件来获得函数, 数据结构和变量的定义. 我们将在碰到它们时检查这些文件, 但是有几个文件对模块是特殊的, 必须出现在每一个可加载模块中. 因此, 几乎所有模块代码都有下面内容:

#include <linux/module.h> 
#include <linux/init.h> 
/*moudle.h 包含了大量加载模块需要的函数和符号的定义。你需要 init.h 来指定你的初始化和清理函数, 大部分模块还包含 moudle param.h, 使得可以在模块加载时传递参数给模块。*/

/*尽管不是严格要求的, 但是你的模块确实应当指定它的代码使用哪个许可。做到这一点只需包含一行:*/
MODULE_LICENSE: MODULE_LICENSE("GPL"); 
/*内核认识的特定许可有:
"GPL"( 适用 GNU 通用公共许可的任何版本 )
"GPL v2"( 只适用GPL版本2 )
"GPL and additional rights", "Dual BSD/GPL", "Dual MPL/GPL", 和 "Proprietary"。 
*/
 
/*可以在模块中包含的其他描述性定义有: */
MODULE_AUTHOR ( 声明谁编写了模块 )
MODULE_DESCRIPION( 一个人可读的关于模块做什么的声明 )
MODULE_VERSION ( 一个代码修订版本号;<linux/module.h> 的注释以便知道创建版本字串使用的惯例) 
MODULE_ALIAS ( 模块为人所知的另一个名字 )
MODULE_DEVICE_TABLE ( 来告知用户空间, 模块支持那些设备 )

模块初始化和关闭

   初始化函数

Static int __init initialization_function(void)
{
/*初始化代码*/
}
Module_init(initialization_function);

   初始化函数应当声明成静态的, 因为它们不会在特定文件之外可见;
   声明中的 __init 标志是一个给内核的暗示, 给定的函数只是在初始化使用。
   模块加载者在模块加载后会丢掉这个初始化函数, 使它的内存可做其他用途。
   类似的标签 (__initdata) 给只在初始化时用的数据. 使用 __init 和 __initdata 是可选的,但不要用在那些在初始化完成后还使用的函数(或者数据结构)上。可能还会遇到 __devinit 和 __devinitdata 在内核源码里; 这些只在内核没有配置支持 hotplug 设备时转换成 __init 和 _initdata。

   清除函数

Static void __exit cleanup_function(void){
/*清除代码*/
}
Module_exit(cleanup_function);

   每个重要的模块都要求有一个清理函数, 它注销接口, 在模块被去除之前返回所有资源给系统。清理函数没有返回值, 因此它被声明为 void。
   __exit 修饰符标识这个代码是只用于模块卸载( 通过使编译器把它放在特殊的 ELF 段)。和前面类似, moudle_exit 声明对于使得内核能够找到你的清理函数是必要的。如果你的模块没有定义一个清理函数, 内核不会允许它被卸载.
   注意:大部分的注册函数都带有register_前缀。

模块参数

   模块参数支持许多类型:

bool     一个布尔型( true 或者 false)(关联的变量应当是 int 类型).
invbool   invbool 类型反转bool的值, 所以真值变成 false, 反之亦然. 
charp    一个字符指针值. 内存为用户提供的字串分配, 指针因此设置. 
int        整型
long      
short      
uint      基本的变长整型值. 以 u 开头的是无符号值.
ulong 
ushort 

数组参数, 用逗号间隔的列表提供的值, 模块加载者也支持,例如要声明一个数组参数, 要调用以下的宏: 
module_param_array(name,type,num,perm); 
这里 name 是你的数组的名子(也是参数名), type 是数组元素的类型, num 是一个整型变量, perm 是通常的权限值。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值