linux设备驱动程序学习笔记

之前学习设备驱动写的笔记,没有好好整理,先贴上来~ 


 内核角色划分:

           进程管理,内存管理,文件系统,设备控制,网络。

 

每块可以在运行时添加到内核的代码,被称为一个模块,设备划分分类成字符模块(一个字节流来存取的设备( 如同一个文件 ), 块模块(块设备通过位于 /dev 目录的文件系统结点来存取. 一个块设备(例如一个磁盘)应该是可以驻有一个文件系统的,  块和字符设备的区别仅仅在内核在内部管理数据的方式上, 并且因此在内核/驱动的软件接口上不同.或者一个网络模块(任何网络事务都通过一个接口来进行)偶数的内核版本( 就是说, 2.6.x )是稳定的, 用来做通用的发布.奇数版本( 例如 2.7.x ), 相反, 是开发快照并且是非常短暂的


Hello world 模块

内核模块

#include<linux/init.h>

#include<linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

 static inehello_init"(void)

{       

      printk(KERN_ALERT"Hello ,World \n")

     return 0;

}

Staticvoid hello_exit(void)

{

    printk(KERN_ALERT"Goodbye, cruel world\n");

      

}

module_init(hello_init);

module_exit(hello_exit);

 

 

内核模块相比于应用程序

           不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模块只注册自己以便来服务将来的请求, 并且它的初始化函数立刻终止. 换句话说,模块初始化函数的任务是为以后调用模块的函数做准备

          外一个主要的不同, 在事件驱动的应用程序和内核代码之, 是退出函数:一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做清理工作,但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启

              个模块在内核空间运行, 而应用程序在用户空间运行. 这个概念是操作系统理论的基础

 

 

        应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 堆栈, 是用来保存函数调用历史以及所有的由当前活跃的函数创建的自动变量. 内核, 相反, 有一个非常小的堆栈;它可能小到一个, 4096 字节的页. 你的函数必须与这个内核空间调用链共享这个堆栈. 因此, 声明一个巨大的自动变量从来就不是一个好主意;如果你需要大的结构, 你应当在调用时间内动态分配.

              当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名通常是一个低层的接口组件, 应当小心使用.

给你的模块创建一个 makefile

obj-m:= hello.o

两个以上文件make

obj-m:= module.o

module-objs:= file1.o file2.o

如果你的内核源码数位于你的 ~/kernel-2.6 目录, 用来建立你的模块的 make 命令( 在包含模块源码和 makefile 的目录下键入 )会是:

make-C ~/kernel-2.6 M=`pwd` modules

这个命令开始是改变它的目录到用 -C 选项提供的目录下( 就是说, 你的内核源码目录 ). 它在那里会发现内核的顶层 makefile. 这个 M= 选项使 makefile在试图建立模块目标前, 回到你的模块源码目录. 这个目标, 依次地, 是指在obj-m 变量中发现的模块列表, 在我们的例子里设成了module.o.

 

#If KERNELRELEASE is defined, we've been invoked from the

#kernel build system and can use its language.

ifneq($(KERNELRELEASE),)

obj-m:= hello.o

#Otherwise we were called directly from the command

#line; invoke the kernel build system.

else

KERNELDIR?= /lib/modules/$(shell uname -r)/build

PWD:= $(shell pwd)

default:

$(MAKE)-C $(KERNELDIR) M=$(PWD) modules

endif

 

modprobe 工具值得快速提及一下.modprobe, 如同 insmod, 加载一个模块到内. 它的不同在于它会查看要加载的模块, 看是否它引用了当前内核没有定义的符号. 如果发现有, modprobe 在定义相关符号的当前模块搜索路径中寻找其他模. modprobe 找到这些模块( 要加载模块需要的 ), 它也把它们加载到内核如果你在这种情况下代替以使用 insmod , 命令会失败, 在系统日志文件中留下一条 " unresolved symbols "消息.

如果你的模块需要输出符号给其他模块使用,应当使用下面的宏定:

EXPORT_SYMBOL(name);

EXPORT_SYMBOL_GPL(name);

 

几乎所有模块代码都有下面内容:

#include <linux/module.h>

#include <linux/init.h>

moudle.h 包含了大量加载模块需要的函数和符号的定义. 你需要 init.h 来指定你的初始化和清理函数,大部分模块还包含 moudleparam.h,使得可以在模块加载时传递参数给模块.不是严格要求的, 但是你的模块确实应当指定它的代码使用哪个许可. 做到这一点只需包含一行 MODULE_LICENSE:

               

             MODULE_LICENSE("GPL");

 

内核认识的特定许可有, "GPL"( 适用 GNU通用公共许可的任何版本 ), "GPLv2"( 只适用 GPL 版本 2 ), "GPL and additional rights", "DualBSD/GPL","Dual MPL/GPL", 和 "Proprietary". 除非你的模块明确标识是在内核认识的一个自由许可下, 否则就假定它是私有的,

 

可以在模块中包含的其他描述性定义有

MODULE_AUTHOR ( 声明谁编写了模块 ),

MODULE_DESCRIPION( 一个人可读的关于模块做什么的声明 ),

 MODULE_VERSION(一个代码修订版本号; 看 <linux/module.h> 的注释以便知道创建版本字串使用的惯例),

MODULE_ALIAS ( 模块为人所知的另一个名子 ),

MODULE_DEVICE_TABLE ( 来告知用户空间, 模块支持那些设备 ).

 

初始化和关停

                  

staticint __init initialization_function(void)

{

/*Initialization code here */

}

module_init(initialization_function);

初始化函数应当声明成静态的, 因为它们不会在特定文件之外可见

使用 moudle_init 是强制的. 这个宏定义增加了特别的段到模块目标代码中,表明在哪里找到模块的初始化函数.

没有这个定义, 你的初始化函数不会被调.

                       

                       对某些驱动有其他的设施可以注册作为补充, 但它们的使用太特别, 所以不值得讨论它们. 它们使用堆叠技术, "内核符号表"一节中讲过. 如果你想深入探求,你可以在内核源码里查找 EXPORT_SYMBOL , 找到由不同驱动提供的入口点. 大部分注册函数以 register_ 做前缀, 因此找到它们的另外一个方法是在内核源码里查找 register_ .

 

清理函数:

每个非试验性的模块也要求有一个清理函数, 它注销接口, 在模块被去除之前返

回所有资源给系统. 这个函数定义为:

static void __exit cleanup_function(void)

{

/* Cleanup code here */

}

module_exit(cleanup_function);

                   清理函数没有返回值, 因此它被声明为 void. __exit修饰符标识这个代码是只用于模块卸载( 通过使编译器把它放在特殊的 ELF 段).如果你的模块直接建立在内核里, 或者如果你的内核配置成不允许模块卸载, 标识为__exit 的函数被简单地丢弃. 因为这个原因, 一个标识 __exit的函数只在模块卸载或者系统停止时调用; 任何别的使用是错的. 再一次, moudle_exit声明对于使得内核能够找到你的清理函数是必要的.如果你的模块没有定义一个清理函数, 内核不会允许它被卸载

 

初始化中的错误处理:

在注册内核设施时, 注册可能失败.即便最简单的动作常常需要内存分配, 分配的内存可能不可用

如果在你注册工具时发生任何错误,首先第一的事情是决定模块是否能够无论如何继续初始化它自己.

                       如果证实你的模块在一个特别类型的失败后完全不能加载, 你必须取消任何在失败前注册的动作. 内核不保留已经注册的设施的每模块注册, 因此如果初始化在某个点失败, 模块必须能自己退回所有东西.如果你无法注销你获取的东西, 内

核就被置于一个不稳定状态; 它包含了不存在的代码的内部指针.这种情况下,唯一的方法就是重启系统.

                 错误恢复有时用 goto 语句处理是最好的.下面例子代码( 使用设施注册和注销函数)在初始化在任何点失败时做得正确

int __init my_init_function(void)

{

int err;

/* registration takes a pointer and a name */

err = register_this(ptr1, "skull");

if (err)

goto fail_this;

err = register_that(ptr2, "skull");

if (err)

goto fail_that;

err = register_those(ptr3, "skull");

if (err)

goto fail_those;

return 0; /* success */

fail_those:

unregister_that(ptr2, "skull");

fail_that:

unregister_this(ptr1, "skull");

fail_this:

return err; /* propagate the error */

}

                    my_init_function 的返回值, err, 是一个错误码. 在 Linux内核里, 错误码是负数, 属于定义于<linux/errno.h> 的集合. 如果你需要产生你自己的错误码代替你从其他函数得到的返回值, 你应当包含 <linux/errno.h> 以便使用符号式的返回值, 例如 -ENODEV, -ENOMEM, 等等. 返回适当的错误码总是一个好做法,因为用户程序能够把它们转变为有意义的字串, 使用 perror 或者类似的方法

                       

void __exit my_cleanup_function(void)

{

unregister_those(ptr3, "skull");

unregister_that(ptr2, "skull");

unregister_this(ptr1, "skull");

return;

}

               使代码重复最小和所有东西流线化, 你应当做的是无论何时发生错误都从初始化里调用清理函数. 清理函数接着必须在撤销它的注册前检查每一项的状态. 以最简单的形式, 代码看起来象这样:

structsomething *item1;

structsomethingelse *item2;

intstuff_ok;

voidmy_cleanup(void)

{

if(item1)

release_thing(item1);

if(item2)

release_thing2(item2);

if(stuff_ok)

unregister_stuff();

return;

}

int__init my_init(void)

{

interr = -ENOMEM;

item1= allocate_thing(arguments);

item2= allocate_thing2(arguments2);

if(!item2 || !item2)

gotofail;

err= register_stuff(item1, item2);

if(!err)

stuff_ok= 1;

else

gotofail;

return0; /* success */

fail:

my_cleanup();

returnerr;

}

 

模块参数

                   参数的值可由 insmod 或者 modprobe 在加载时指定;后者也可以从它的配置文件(/etc/modprobe.conf)读取参数的值.这些命令在命令行里接受几类规格的值. 作为演示这种能力的一种方法,想象一个特别需要的对本章开始的"hello world"模块(称为 hellop)的改进. 我们增加 2 个参数: 一个整型值, 称为 howmany, 一个字符串称为 whom. 我们的特别多功能的模块就在加载时, 欢迎whom 不止一次, 而是 howmany 次.这样一个模块可以用这样的命令行加载

insmod hellop howmany=10 whom="Mom"

                  一旦以那样的方式加载, hellop 会说 "hello,Mom" 10 次.

但是, 在 insmod 可以修改模块参数前, 模块必须使它们可用. 参数用moudle_param 宏定义来声明, 它定义在 moduleparam.h.module_param 使用了3 个参数: 变量名,它的类型, 以及一个权限掩码用来做一个辅助的 sysfs 入

. 这个宏定义应当放在任何函数之外, 典型地是出现在源文件的前面. 因此hellop 将声明它的参数, 并如下使得对 insmod 可用:

static char *whom = "world";

static int howmany = 1;

module_param(howmany, int, S_IRUGO);

module_param(whom, charp, S_IRUGO);

模块参数支持许多类型:

bool

invbool一个布尔型( true或者 false)值(相关的变量应当是 int 类型).  invbool 类型颠倒了值, 所以真值变成 false, 反之亦然.

        charp 一个字符指针值. 内存为用户提供的字串分配, 指针因此设置.

int

long

short

uint

ulong

ushort基本的变长整型值. 以 u开头的是无符号值.数组参数, 用逗号间隔的列表提供的值,模块加载者也支持. 声明一个数组参数,

 

使用:

        module_param_array(name,type,num,perm);

这里 name是你的数组的名子(也是参数名), type 是数组元素的类型, num 是一个整型变量, perm 是通常的权限值. 如果数组参数在加载时设置, num 被设置成提供的数的个数. 模块加载者拒绝比数组能放下的多的值.

 

 

在用户空间做

 

用户空间驱动的好处在于:

         完整的 C 库可以连接. 驱动可以进行许多奇怪的任务, 不用依靠外面的程序(实现使用策略的工具程序, 常常随着驱动自身发布).

         程序员可以在驱动代码上运行常用的调试器, 而不必走调试一个运行中的内核的弯路.

          如果一个用户空间驱动挂起了, 你可简单地杀掉它. 驱动的问题不可能挂起整个系统。

              用户内存是可交换的, 不象内核内存. 一个不常使用的却有很大一个驱动

的设备不会占据别的程序可以用到的RAM, 除了在它实际在用时.

              一个精心设计的驱动程序仍然可以, 如同内核空间驱动, 允许对设备的并行存取.

               如果你必须编写一个封闭源码的驱动, 用户空间的选项使你容易避免不明朗的许可的情况和改变的内核接口带来的问题.

缺点

中断在用户空间无法用.在某些平台上有对这个限制的解决方法, 例如在IA32 体系上的vm86 系统调用.

只可能通过内存映射 /dev/mem 来使用 DMA, 而且只有特权用户可以这样.

                      存取 I/O 端口只能在调用 ioperm 或者 iopl之后. 此外, 不是所有的平台支持这些系统调用, 而存取 /dev/port 可能太慢而无效率. 这些系统调用和设备文件都要求特权用户.

       响应时间慢, 因为需要上下文切换在客户和硬件之间传递信息或动作.

                  更不好的是, 如果驱动已被交换到硬盘,响应时间会长到不可接受. 使用mlock 系统调用可能会有帮助,但是常常的你将需要锁住许多内存页, 因为一个用户空间程序依赖大量的库代码. mlock, 也, 限制在授权用户上.

      最重要的设备不能在用户空间处理, 包括但不限于, 网络接口和块设备

 

 

快速参考(总结本章内容)

             insmod modprobe   rmmod 

        #include<linux/init.h>                          必需的头文件.

         #include<linux/sched.h>                       重要的头文件这个文件包含很多驱动使用的内核 API 的定,包括睡眠函数和许多变量声明

        #include<linux/module.h>                       必需的头文件. 它必须在一个模块源码中包含

        #include<linux/version.h>                       头文件, 包含在建立的内核版本信息.

          module_init(init_function)

          module_exit(cleanup_function);

      

        _init

        _initdata

        _exit

        _exitdata

函数( __init 和 __exit)和数据 (__initdata 和 __exitdata)的标记,用在模块初始化或者清理时间. 为初始化所标识的项可能会在初始化完成后丢弃; 退出的项可能被丢弃如果内核没有配置模块卸载. 这些标记通过使相关的目标在可执行文件的特定的 ELF 节里被替换来工作.

 

 

 

           struct task_struct*current                       当前进程

           current -> pid

           current -> comn   

           obj-m                                                           一个 makefile 符号, 内核建立系统用来决定当前目录下的哪个模块应当被建立

 

        /sys/module

        /proc/modules                                              

/sys/module 是一个 sysfs 目录层次, 包含当前加载模块的信息.

/proc/moudles 是旧式的, 那种信息的单个文件版本. 其中的条目包含了

模块名, 每个模块占用的内存数量, 以及使用计数. 另外的字串追加到每

行的末尾来指定标志, 对这个模块当前是活动的.

 

LINUX_VERSION_CODE           整型宏定义, #ifdef 版本依赖有用.

EXPORT_SYMBOL(symbol);

EXPORT_SYMBOL_GPL(symbol);

         宏定义, 用来输出一个符号给内核. 2 种形式输出没有版本信息,

3 种限制输出给 GPL 许可的模块

MODULE_AUTHOR(author);

MODULE_DESCRIPTION(description);

MODULE_VERSION(version_string);

MODULE_DEVICE_TABLE(table_info);

MODULE_ALIAS(alternate_name);

放置文档在目标文件的模块中.

module_init(init_function);

module_exit(exit_function);

         宏定义, 声明一个模块的初始化和清理函数.

#include<linux/moduleparam.h>

module_param(variable,type, perm);

宏定义, 创建模块参数, 可以被用户在模块加载时调整( 或者在启动时间,

对于内嵌代码). 类型可以是 bool,charp, int, invbool, short, ushort,

uint, ulong, 或者 intarray.

#include<linux/kernel.h>

int printk(const char * fmt, ...);  printf 类似物

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值