您的位置:首页 > 运维架构 > Linux

Linux设备驱动程序学习(2)-构造和运行模块

2013-07-31 11:36 393 查看
一、内核开发的特点(摘自《LINUX内核设计与实现(第三版)》):

<1> 内核编程时既不能访问C库也不能访问标准C头文件。

<2> 内核编程必须使用GNU C

<3> 内核没有内存保护机制

<4> 不要轻易在内核中使用浮点数

<5> 内核给每个进程只有一个很小的定长堆栈(内核栈)

<6> 同步和并发

<7> 要考虑可移植性

二、内核功能划分



内核可以划分为如下几部分:

进程管理

  内核进程管理活动就是在单个或者多个CPU上面实现各个进程的抽象

内存管理

  用来管理内存的策略是决定一个系统性能的一个关键因素

文件系统

  不同的文件系统,即在物理介质上组织数据的方式不同

设备控制

  几乎每一个系统操作最终都会映射到物理设备上

网络功能

  大部分网络操作和具体进程无关

三、设备驱动程序的作用

1、 驱动程序的作用在于提供机制,而不是策略。

“需要提供什么功能”(机制);“如何使用这些功能”(策略)。不同的环境通常需要不同的方式来使用硬件,我们应尽量做到让驱动程序不带策略。驱动程序应该处理如何使硬件可用的问题,而将怎样使用硬件的问题留给上层应用程序。

2、总的来说,驱动程序的设计主要还是综合考虑下面三个方面的因素:提供给用户

尽量多的选项、编写驱动程序要占用的时间以及尽量保持程序简单而不至于错误丛生。

四、设备驱动的分类

字符设备

字符设备是能够像字节流(类似文件)一样被访问的设备,字符设备驱动程序通常至少要实现open、close、read和write系统调用。大多数字符设备是一个只能顺序访问的数据通道。

块设备

块设备上能够容纳文件系统。进行I/O操作时,块设备每次只能传输一个或多个完整的块。

网络接口

任何网络事物都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。通常,接口是硬件设备,但也可能是个纯软件设备,比如回环(loopback)接口。UNIX访问网络接口的方法仍然是给他们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核使用一套和数据包传输相关的函数而不是read、write等。

五、内核模块基本结构

利用Linux设备驱动程序的第一个例程:Hello World模块学习内核模块的结构。




View
Code

1 #include <linux/init.h>
2 #include <linux/module.h>
3
4 static int __init hello_init(void)
5 {
6     printk(KERN_ALERT"Hello World\n");
7     return 0;
8 }
9
10 static void __exit hello_exit(void)
11 {
12     printk(KERN_ALERT"Goodbye, cruel world\n");
13 }
14
15 module_init(hello_init);
16 module_exit(hello_exit);
17
18 MODULE_LICENSE("GPL");


1、 所有模块都包含以下两个头文件:




View
Code

1 #include <linux/init.h>
2 #include <linux/module.h>


module.h包含有可装载模块所需要的大量符号和函数定义。

包含init.h的目的是指定初始化和清除函数。

2、尽管不是严格要求,但模块应该指定代码所使用的许可证。




View
Code

1 MODULE_LICENSE("GPL");


内核能够识别的许可证有:




View
Code

1 “GPL”(任一版本的GNU通用公共许可证)
2 “GPL v2”(GPL版本2)
3 “GPL and additional rights”(GPL及附加权利)
4 “Dual BSD/GPL”(BSD/GPL双重许可证)
5 “Dual MPL/GPL”(MPL/GPL双重许可证)
6 “Proprietary”(专有)


如果一个模块没有显示地标记为上述内核可识别的许可证,则会被假定是专有的,而内核装载这种模块就会被“污染”。

另外,可在内核中包含其他描述性定义:




View
Code

1 MODULE_AUTHOR(); (描述模块作者)
2 MODULE_DESCRIPTION(); (说明模块用途)
3 MODULE_VERSION(); (代码修订号)
4 MODULE_ALIAS(); (模块别名)
5 MODULE_DEVICE_TABLE(); (用来告诉用户空间模块所支持的设备)


上述MODULE_声明可出现在源文件中源代码函数以外的任何地方,新近的内核编码习惯是将这些声明放在文件的最后

3、初始化和关闭

初始化函数的实际定义通常如下:




View
Code

1 static int __init initialization_function(void)
2 {
3     /* 这里是初始化代码 */
4 }
5 module_init(initialization_function);


__init和__initdata表明该函数仅在初始化期间使用。

__devinit和__devinitdata,只有在内核未被配置为支持可热插拔设备的情况下,才会被翻译为__init和__initdata

清除函数的实际定义通常如下:




View
Code

1 static void __exit cleanup_function(void)
2 {
3     /* 这里是清除代码 */
4 }
5 module_exit(cleanup_function);


被标记为__exit的函数只能在模块被卸载或者系统关闭时被调用。

如果一个模块未定义清除函数,则内核不允许卸载该模块。

4、Makefile分析




View
Code

1 ifneq ($(KERNELRELEASE),)
2
3 obj-m := hello.o
4
5 else
6
7 KDIR := /root/work/ldd3/kernel/linux-2.6.32.2/linux-2.6.32.2
8 all:
9     make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-linux-
10 clean:
11     rm -rf *.ko *.o *.mod.o *.mod.c *.symvers modul* .*.mod.o.cmd .tmp_versions .Makefile.swp .*.ko.cmd .*.o.cmd
12
13 endif


obj-m := hello.o

代表了我们要构造的模块名称为hello.ko,make会在当前目录下找到hello.c文件进行编译。如果hello.o是由两个源文件生成(比如file1.c和file2.c),则正确的makefile可如下编写:
obj-m := hello.o
hello-objs := file1.o file2.o
make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-linux-

-C $(KDIR) 指定了内核源代码的位置,其中保存有内核的顶层makefile文件。

M=$(PWD) 指定了模块源代码的位置

modules 目标指向obj-m变量中设定的模块。

5、宿主机上编译模块




View
Code

root@ycz-virtual-machine:~/work/ldd3/2/helloworld# ls
hello.c  Makefile
root@ycz-virtual-machine:~/work/ldd3/2/helloworld# make
make -C /root/work/ldd3/kernel/linux-2.6.32.2/linux-2.6.32.2 M=/root/work/ldd3/2/helloworld modules ARCH=arm CROSS_COMPILE=arm-linux-
make[1]: 正在进入目录 `/root/work/ldd3/kernel/linux-2.6.32.2/linux-2.6.32.2'
CC [M]  /root/work/ldd3/2/helloworld/hello.o
Building modules, stage 2.
MODPOST 1 modules
CC      /root/work/ldd3/2/helloworld/hello.mod.o
LD [M]  /root/work/ldd3/2/helloworld/hello.ko
make[1]:正在离开目录 `/root/work/ldd3/kernel/linux-2.6.32.2/linux-2.6.32.2'
root@ycz-virtual-machine:~/work/ldd3/2/helloworld#


6、在开发板上操作




View
Code

[root@FriendlyARM /]# cd udisk/ldd3/2/
[root@FriendlyARM 2]# ls
hello.ko
[root@FriendlyARM 2]# insmod hello.ko
Hello World
[root@FriendlyARM 2]# lsmod
hello 509 0 - Live 0xbf006000
[root@FriendlyARM 2]# rmmod hello
Goodbye, cruel world
rmmod: module 'hello' not found
[root@FriendlyARM 2]# lsmod
[root@FriendlyARM 2]#


insmod: 加载(insmod hello.ko)

rmmod: 卸载(rmmod hello)

lsmod: 查看

modprobe: 加载(modprobe hello)。从标准的已安装内核模块目录中搜索需要装入的模块。

六、内核模块参数

内核允许对驱动程序指定参数,而这些参数可在装载驱动程序模块时改变。这些参数的值可以在运行insmod或modprobe命令装载模块时赋值。但是所有的模块参数都应该给定一个默认值。在insmod改变模块参数之前,模块必须让这些参数对insmod命令可见。

1、一般参数须使用module_param宏来声明:

module_param(name, type, perm);

该宏必须放在任何函数之外,通常是在源文件的头部。

2、模块也支持数组参数,用module_param_array宏声明:

module_param_array(name, type, num, perm);

name: 数组名称即参数名称

type: 数组元素类型

num: 整型指针,如果在装载时设置数组参数,则num会被设置为用户提供的值得个数。模块加载器会拒绝接受超过数组大小的值。

perm: 访问许可值,即用于sysfs入口项的访问许可掩码

3、内核支持的模块参数类型(type)

bool

invbool

布尔值(取true或false),bool和invbool取值相反。

charp

字符指针值。内核会为用户提供的字符串分配内存,并相应的设置指针。

int

long

short

uint

ulong

ushort

以u开头的用于无符号值。

4、 访问许可值(perm)

这个值用来控制谁能够访问sysfs中对模块参数的表述。如果prem设置为0,就不会应对应的sysfs入口项。如果一个参数通过sysfs而被修改,则如同模块修改了该参数的值一样,但是内核不会以任何方式通知模块,大多数情况下,不应该让模块参数可写。常用的设置值:

S_IRUGO: 任何人均可读取该参数,但不能修改。

S_IRUGO | S_IWUSR: 允许root用户修改该参数。

实验代码:




View
Code

 1 #include <linux/init.h>
2 #include <linux/module.h>
3
4 static char *name = "ycz9999";
5 static int age = 22;
6 static int param_array[] = {0,0,0,0};
7 static int param_array_nr;
8
9 module_param(age, int, S_IRUGO);
10 module_param(name, charp, S_IRUGO);
11 module_param_array(param_array, int, ¶m_array_nr, S_IRUGO);
12
13 static int __init hello_init(void)
14 {
15 int i;
16
17 printk(KERN_ALERT" Name: %s\n", name);
18 printk(KERN_ALERT" Age: %d\n", age);
19
20 for(i = 0; i < param_array_nr; i++)
21 {
22 printk(KERN_ALERT" param_array[%d] = %d\n", i, param_array[i]);
23 }
24
25 return 0;
26 }
27
28 static void __exit hello_exit(void)
29 {
30 printk(KERN_ALERT" Module Exit!\n");
31 }
32
33 module_init(hello_init);
34 module_exit(hello_exit);
35
36 MODULE_DESCRIPTION("hello_linux test module");
37 MODULE_ALIAS("hello_param");
38 MODULE_VERSION("v1.0");
39 MODULE_AUTHOR("ycz9999");
40 MODULE_LICENSE("Dual BSD/GPL");


开发板上实验现象:




View
Code

[root@FriendlyARM 2]# ls
hello.ko  param.ko
[root@FriendlyARM 2]# insmod param.ko  param_array=1,2,3,4
Name: ycz9999
Age: 22
param_array[0] = 1
param_array[1] = 2
param_array[2] = 3
param_array[3] = 4
[root@FriendlyARM 2]# lsmod
param 689 0 - Live 0xbf066000
[root@FriendlyARM 2]# rmmod param
Module Exit!
rmmod: module 'param' not found
[root@FriendlyARM 2]# insmod param.ko  param_array=1,2,3
Name: ycz9999
Age: 22
param_array[0] = 1
param_array[1] = 2
param_array[2] = 3
[root@FriendlyARM 2]# lsmod
param 689 0 - Live 0xbf06c000
[root@FriendlyARM 2]# rmmod param
Module Exit!
rmmod: module 'param' not found
[root@FriendlyARM 2]# insmod param.ko  name=hello age=10 param_array=1,2,3,4
Name: hello
Age: 10
param_array[0] = 1
param_array[1] = 2
param_array[2] = 3
param_array[3] = 4
[root@FriendlyARM 2]# lsmod
param 689 0 - Live 0xbf072000
[root@FriendlyARM 2]# rmmod param
Module Exit!
rmmod: module 'param' not found
[root@FriendlyARM 2]# insmod param.ko  name=hello age=10 param_array=1,2,3,4,5
param_array: can only take 4 arguments
param: `1' invalid for parameter `param_array'
insmod: cannot insert 'param.ko': invalid parameter
[root@FriendlyARM 2]# lsmod
[root@FriendlyARM 2]#


七、内核符号表

公共符号表(/proc/kallsyms)中包含了所有的全局内核项(即函数和变量)的地址,这是实现模块化驱动程序必需的。当模块被装入内核后,它所导出的任何符号都会变成内核符号表的一部分。在通常情况下,模块只需实现自己的功能,而无需导出任何符号。但是,如果其他模块需要从某个模块中获得好处时,我们也可以导出符号。

新模块可以使用由我们自己的模块导出的符号,这样,我们可以在其他模块基础上层叠新的模块。模块层叠技术在复杂的项目中非常有用。modprobe是处理层叠模块时的一个实用工具。如同insmod,modprobe也是加载一个模块到内核。它的不同之处在于,它会根据文件/lib/modules/<$version>/modules.dep来查看要加载的模块,看它是否还依赖于其他模块,如果是,modeprobe会首先找到这些模块,把它们加载到内核。然而,从当前目录装入自己的模块时仍需使用insmod,因为modprobe只能从标准的已安装内核模块目录中搜索需要装入的模块。

如果需要向其他模块导出符号,则应该使用下面的宏:

EXPORT_SYMBOL(name);

EXPORT_SYMBOL_GPL(name);

_GPL使得要导出的模块符号只能被GPL许可证下的模块使用。符号必须在模块文件的全局部分导出,不能在函数中导出。

实验代码:

calculate.c




View
Code

 1 #include <linux/init.h>
2 #include <linux/module.h>
3
4 int add_integar(int a, int b)
5 {
6 return a+b;
7 }
8
9 int sub_integar(int a, int b)
10 {
11 return a-b;
12 }
13
14 static int __init sym_init()
15 {
16 return 0;
17 }
18
19 static void __exit sym_exit()
20 {
21
22 }
23
24 module_init(sym_init);
25 module_exit(sym_exit);
26
27 EXPORT_SYMBOL(add_integar);
28 EXPORT_SYMBOL(sub_integar);
29
30
31 MODULE_LICENSE("GPL");


hello.c




View
Code

 1 #include <linux/init.h>
2 #include <linux/module.h>
3
4 extern int add_integar(int a, int b);
5 extern int sub_integar(int a, int b);
6
7 static int __init hello_init()
8 {
9 int res_add = add_integar(1, 2);
10 printk(KERN_ALERT"res_add = %d\n", res_add);
11 return 0;
12 }
13
14 static void __exit hello_exit()
15 {
16 int res_sub = sub_integar(2, 1);
17 printk(KERN_ALERT"res_sub = %d\n", res_sub);
18 }
19
20 module_init(hello_init);
21 module_exit(hello_exit);
22
23
24 MODULE_LICENSE("GPL");
25 MODULE_AUTHOR("ycz9999");
26 MODULE_DESCRIPTION("Hello World Module");
27 MODULE_ALIAS("a simplest module");


开发板上实验现象:

calculate.c向内核中导出符号,hello.c通过extern关键字引用内核符号。

实验时,要注意卸载模块时的先后顺序,因为calculate.ko导出的符号被hello.ko引用,因此,一定要先卸载calculate.ko,然后再卸载hello.ko。装载也是一个道理。




View
Cod

[root@FriendlyARM symbol]# ls
calculate.ko  hello.ko
[root@FriendlyARM symbol]# insmod hello.ko
hello: Unknown symbol add_integar
hello: Unknown symbol sub_integar
insmod: cannot insert 'hello.ko': unknown symbol in module or invalid parameter
[root@FriendlyARM symbol]# lsmod
[root@FriendlyARM symbol]# insmod calculate.ko
[root@FriendlyARM symbol]# insmod hello.ko
res_add = 3
[root@FriendlyARM symbol]# lsmod
hello 521 0 - Live 0xbf02a000
calculate 584 1 hello, Live 0xbf024000
[root@FriendlyARM symbol]# cat /proc/kallsyms >> tmp
[root@FriendlyARM symbol]# vi tmp
- tmp 1/24838 0%
:/add_*
- tmp 1/24838 0%
- tmp 24838/24838 100%
:q
- tmp 24838/24838 100%
c0442308 R __stop___param
c0443000 A _etext
bf02a000 t hello_exit   [hello]
bf02a000 t $a   [hello]
bf02a028 t $d   [hello]
bf02a000 t cleanup_module       [hello]
bf024000 t $a   [calculate]
bf024028 t sym_exit     [calculate]
bf024028 t $a   [calculate]
bf024028 t cleanup_module       [calculate]
bf024000 T add_integar  [calculate]
bf024014 T sub_integar  [calculate]
~
~
~
~
~
~
~
~
~
~
~
[root@FriendlyARM symbol]# rmmod calculate
rmmod: remove 'calculate': Resource temporarily unavailable
[root@FriendlyARM symbol]# lsmod
hello 521 0 - Live 0xbf02a000
calculate 584 1 hello, Live 0xbf024000
[root@FriendlyARM symbol]# rmmod hello
res_sub = 1
rmmod: module 'hello' not found
[root@FriendlyARM symbol]# lsmod
calculate 584 0 - Live 0xbf024000
[root@FriendlyARM symbol]# rmmod calculate
rmmod: module 'calculate' not found
[root@FriendlyARM symbol]# lsmod
[root@FriendlyARM symbol]#




八、内核模块知识补充


1、当处理器存在多个级别时,Unix使用最高级别(超级用户态)和最低级别(用户态)。

2、每当应用程序执行系统调用或者被硬件中断挂起时,Unix将执行模式从用户空间切换到内核空间。执行系统调用的内核代码运行在进程上下文中,它代表调用进程执行操作,因此能够访问进程地址空间的所有数据。而处理硬件中断的内核代码和进程是异步的,与任何一个进程无关。模块代码在内核空间中运行,用于扩展内核的功能。一个驱动程序要执行先前讲述过的两类任务:模块中的某些函数作为系统调用的一部分而执行,而其他的函数则负责中断处理。

3、内核代码(包括驱动程序)必须是可重入的,它必须能够同时运行在多个上下文中。

九、初始化过程中的错误处理

通常情况下很少使用“goto”,但在出错处理是(可能是唯一的情况),它却非常有用。在大二学习C语言时,老师就建议不要使用“goto”,并说很少会用到。在这里也是我碰到的第一个建议使用“goto”的地方。“在追求效率的代码中使用goto语句仍是最好的错误恢复机制。”--《Linux设备驱动程序(第3版)》
以下是初始化出错处理的推荐代码示例:




View
Code

1 struct something *item1;
2 struct somethingelse *item2;
3 int stuff_ok;
4
5
6 void my_cleanup(void)
7 {
8     if (item1)
9
10
11         release_thing(item1);
12     if (item2)
13         release_thing2(item2);
14     if (stuff_ok)
15         unregister_stuff();
16     return;
17 }
18 int __init my_init(void)
19 {
20     int err = -ENOMEM;
21     item1 = allocate_thing(arguments);
22     item2 = allocate_thing2(arguments2);
23     if (!item2 || !item2)
24         goto fail;
25     err = register_stuff(item1, item2);
26     if (!err)
27         stuff_ok = 1;
28     else
29         goto fail;
30     return 0; /* success */
31
32
33  fail:
34         my_cleanup( );
35         return err;
36 }


参考:

《Linux设备驱动程序(第三版)》

《LINUX内核设计与实现(第三版)》

Tekkaman Ninja: http://blog.chinaunix.net/uid/20543672.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: