一、驱动程序的特点
- 是应用和硬件设备之间的一个软件层 。
- 这个软件层一般在内核中实现
- 设备驱动程序的作用在于提供机制,而不是提供策略,编写访问硬件的内核代码时不要给用户强加任何策略
- 机制:驱动程序能实现什么功能。
- 策略:用户如何使用这些功能。
二、设备驱动分类和内核模块
- 设备驱动类型。Linux 系统将设备驱动分成三种类型
- 字符设备
- 块设备
- 网络设备
- 内核模块:内核模块是内核提供的一种可以动态加载功能单元来扩展内核功能的机制,类似于软件中的插件机制。这种功能单元叫内核模块。
- 通常为每个驱动创建一个不同的模块,而不在一个模块中实现多个设备驱动,从而实现良好的伸缩性和扩展性。
三、字符设备
- 字符设备是个能够象字节流(比如文件)一样访问的设备,由字符设备驱动程序来实现这种特性。通过/dev下的字符设备文件来访问。字符设备驱动程序通常至少需要实现 open、close、read 和 write 等系统调用所对应的对该硬件进行操作的功能函数。
- 应用程序调用system call(系统调用),例如:read、write,将会导致操作系统执行上层功能组件的代码,这些代码会处理内核的一些内部事务,为操作硬件做好准备,然后就会调用驱动程序中实现的对硬件进行物理操作的函数,从而完成对硬件的驱动,然后返回操作系统上层功能组件的代码,做好内核内部的善后事务,最后返回应用程序。
- 由于应用程序必须使用/dev目录下的设备文件(参见open调用的第1个参数),所以该设备文件必须事先创建。谁创建设备文件呢?
- 大多数字符设备是个只能顺序访问的数据通道,不能前后移动访问指针,这点和文件不同。比如串口驱动,只能顺序的读写设备。然而,也存在和数据区或者文件特性类似的字符设备,访问它们时可前后移动访问指针。例如framebuffer设备就是这样一个设备,应用程序可以用 mmap 或 lseek 访问图象的各个区域。
四、块设备
- 块设备通常是按照块为单位来访问数据,比如一块为512字节。
- 块设备也是通过 /dev 目录下的文件系统节点来访问。块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核和驱动程序的接口不同。
- 块设备除了给内核提供和字符设备一样的接口外,还提供了专门面向块设备的接口,块设备的接口必须支持挂装文件系统,通过此接口,块设备能够容纳文件系统,因此应用程序一般通过文件系统来访问块设备上的内容。
- 文件系统可能是除驱动程序外 Linux 系统中最重要的模块类型,与块设备驱动程序联系紧密。
五、网络设备驱动和网络接口
- 网络设备驱动不同于字符设备和块设备,不在/dev下以文件节点为代表,而是通过单独的网络接口(eth0、eth1)来代表。
- 任何网络事务都要经过一个网络接口,即一个能够和其它主机交换数据的设备。通常接口代表一个硬件设备(如网卡),但也可能是个纯软件设备。
- 内核和网络驱动程序间的通讯完全不同于内核和字符设备以及块设备驱动程序之间的通信,内核调用一套和数据包传输相关的函数。
六、设备文件和设备驱动
- 设备文件是文件系统上的一个节点,是一种特殊的文件,叫做设备文件。每个设备文件在用户空间代表了一个设备。
- 设备文件一般存在/dev目录下,用mknod命令创建。 设备文件有主、次设备号与其关联。
- 设备文件是用户应用程序和设备驱动的接口。应用程序一般只能通过设备文件来使用设备驱动的功能。
- 字符和块设备驱动必须有相应的设备文件来对应。
很明显,操作系统内部不可能用设备文件名来与物理设备及其驱动进行绑定。其实,操作系统内部是用设备号来与物理设备及其驱动进行绑定的。习惯上,用主设备号与驱动进行关联,用次设备号与具有相同驱动的不同物理设备关联(例如:2个硬盘)。
dennis@dennis-desktop:~$ ls -l /dev/sd[a-c]
brw-rw—- 1 root disk
brw-rw—- 1 root disk
brw-rw—- 1 root disk
当用户程序运行open("/dev/ttyS0",…)时,由于设备文件/dev/ttyS0有一个设备号与其关联,因此操作系统可以获知应用程序想操控的设备的设备号,而操作系统内部又将设备号与物理设备及其驱动进行了绑定,因此操作系统就可以知道应该调用哪一个驱动去控制哪一个设备。当然这一切的前提是,操作系统内部要将设备号与物理设备及其驱动进行绑定,那么操作系统内部是用什么手段完成这种绑定关系的呢?实际上,在操作系统内部存在一个结构体链表(就是上图中的Char device list,以后称它为设备链表),链表的每个节点代表一个绑定关系(也就是说:节点至少含有2个字段,1个用于记录设备号,另1个用于记录寻找驱动的信息,通常是一个指向驱动函数结构体的指针)。那么是谁生成节点并将它链入链表的呢?当然是驱动程序!
七、构造和运行模块
1、Kernel Module的特点
- 模块只是先注册自己以便服务于将来的某个请求,然后就立即结束。
- 模块可以是实现驱动程序,文件系统,或者其他功能。
- 加载模块后,模块运行在内核空间,和内核链接为一体。
2、模块与内核的接口函数(除掉read、write等功能函数)
生成节点并将它链入设备链表这个操作由驱动中的函数实现,这些函数什么时机运行呢?当然最合适的时机是内核加载模块(insmod 模块)的时候。
- 函数 init_module:内核加载模块的时候调用。主要功能是:为以后使用模块里的函数和变量预先做准备
- 函数cleanup_module:模块的第二个入口点,内核在模块即将卸载之前调用它。
3、操作模块相关的命令
- insmod: 加载模块。后面参数是模块文件名。
# insmod /lib/modules/hello.ko
Hello, world - rmmod:卸载模块。后面参数是模块名称。
# rmmod hello
Goodbye, cruel world - lsmod:列出当前内核使用的模块。或者查看/proc/modules文件。
- depmod:扫描/lib/modules/<kernel version>/目录下的所有内核模块,从而给内核模块生成依赖文件。
- 生成/lib/modules/<kernel version>/modules.dep文件,其中<kernel version>是当前运行内核的版本号
- modprobe:根据modules.dep文件探测并加载内核模块。只需要给出模块名称,自动寻找适合的模块文件,并进行加载。注意和insmod的不同之处。
- 可以自动寻找模块文件并加载。
- 自动寻找并加载依赖的模块。
#cat /lib/modules/2.6.22.6/modules.dep
/lib/modules/s3c24xx_buttons.ko: /lib/modules/leds.ko
/lib/modules/leds.ko:
# lsmod
Module
# modprobe s3c24xx_buttons
leds initialized
buttons initialized
# lsmod
Module
s3c24xx_buttons
leds
# rmmod leds
rmmod: leds: Resource temporarily unavailable
# rmmod s3c24xx_buttons
buttons driver unloaded
# lsmod
Module
leds
# rmmod leds
leds driver unloaded
# lsmod
Module
# insmod s3c24xx_buttons
s3c24xx_buttons: Unknown symbol ledoff
s3c24xx_buttons: Unknown symbol ledon
insmod: cannot insert ‘/lib/modules/s3c24xx_buttons.ko’: Unknown symbol in module (-1): No such file or directory
- modinfo:查看模块文件的基本信息
dennis@dennis-desktop:/work/studydriver/buttons$ modinfo s3c24xx_buttons.ko
filename:s3c24xx_buttons.ko
license:GPL
description:S3C2410/S3C2440 BUTTON Driver
author:YangZhu E-mail: scyz@263.net
depends:
vermagic:2.6.22.6 mod_unload ARMv4
4、内核模块的编译方法
内核源码树:指的是内核源代码tar包解压缩后形成的目录(包含其下级所有目录和文件)
已编译内核源码树:指的是已经成功生成过内核的内核源码树(即:已经成功执行过make uImage的内核源码树)
驱动大多都编译为模块,2.6内核中要想编译模块,必须先存在已经成功编译了的内核源码树(即:已编译内核源码树),且该源码树编译出来的内核就是该模块即将运行在其上的内核。
编译方法1:
- 编写Makefile:obj-m := hello.o
- 编译命令:make –C 内核源码树目录 M=`pwd` modules。例如:
dennis@dennis-desktop:/work/studydriver/examples/misc-modules$ make -C /work/system/linux-2.6.22.6/ M=`pwd` modules
对该make命令的解释:
要想编译内核模块,只需要在内核源码树的顶层目录下输入make modules来编译Makefile中的modules目标即可,剩下的事情,由内核构造系统全权替我们处理。但由于目前不处于内核源码树的顶层目录,并且当前目录下的Makefile也没有modules目标,因此使用-C参数来告知make程序需要在执行之前切换到/work/system/linux-2.6.22.6/目录。此外,由于模块的源代码在当前目录中,不在内核源码树中,因此需要使用M变量(该变量是内核构造系统的变量)告知内核构造系统,编译模块所需的源代码以及Makefile在当前目录(/work/studydriver/examples/misc-modules)中来找,而且最终生成的模块ko文件也要放在当前目录中。
编译方法2:
- 编写Makefile如下:
ifeq ($(KERNELRELEASE),)
modules:
modules_install:
clean:
.PHONY: modules modules_install clean
else
endif
- 编译命令:make
对该Makefile的解释:
当make时,由于变量KERNELRELEASE尚未赋值,因此ifeq ($(KERNELRELEASE),)为真,于是变量KERNELDIR被赋值为内核源码树目录/work/system/linux-2.6.22.6,变量PWD被赋值为当前目录/work/studydriver/examples/misc-modules,然后执行找到的第1个目标modules,从而执行命令
,而当该命令执行以调用内核构造系统的时候,内核构造系统会为变量KERNELRELEASE赋值,从而它不再为空,从而当前目录下的Makefile就变成了只有一行:obj-m := hello.o。此时情况与编译方法1的情况完全相同,因此2种编译方法得到了相同的结果。
最后将得到编译好的模块hello.ko
5、简单的内核模块例子
- 初始化函数:hello_init,使用宏module_init来声明。
- 销毁函数:hello_exit,使用宏module_exit来声明。
- 模块LICENSE信息,使用宏MODULE_LICENSE来说明
1 #include <linux/init.h>
2 #include <linux/module.h>
3 MODULE_LICENSE("Dual BSD/GPL");
4
5 static int hello_init(void)
6 {
7
8
9 }
10
11 static void hello_exit(void)
12 {
13
14 }
15
16 module_init(hello_init);
17 module_exit(hello_exit);
模块执行结果:
# insmod hello.ko
Hello, world
# rmmod hello
Goodbye, cruel world
6、带参数的内核模块例子
- 可以给模块在加载的时候传递参数
- 使用宏MODULE_PARM(变量名,变量类型,权限)来声明参数。变量有如下类型:short、ushort、int、uint、long、ulong、charp、bool
- 使用方法:insmod hellp.ko howmany=3 whom=“YangZhu"
1 #include <linux/init.h>
2 #include <linux/module.h>
3 #include <linux/moduleparam.h>
4 static char *whom = "world";
5 static int howmany = 1;
6 module_param(howmany, int, S_IRUGO | S_IWUSR);
7 module_param(whom, charp, S_IRUGO);
8
9 static int hello_init(void)
10 {
11
12
13
14
15 }
16
17 static void hello_exit(void)
18 {
19
20
21 }
22
23 module_init(hello_init);
24 module_exit(hello_exit);
模块执行结果:
# insmod hellop.ko
hellop: module license ‘unspecified’ taints kernel.
(0) Hello, world
# insmod hellop.ko howmany=3 whom="YangZhu"
(0) Hello, YangZhu
(1) Hello, YangZhu
(2) Hello, YangZhu
# rmmod hellop
howmany is 3, whom is YangZhu
Goodbye, cruel world
- 模块运行期间,参数变量会以文件的形式出现在/sys目录,MODULE_PARM宏中的权限指定了该文件的权限。可以通过改变/sys目录下文件的内容来改变参数变量的值
# insmod hellop.ko howmany=3 whom="YangZhu"
(0) Hello, YangZhu
(1) Hello, YangZhu
(2) Hello, YangZhu
# ls /sys/module/hellop/parameters/ -l
-rw-r–r–
-r–r–r–
# cat /sys/module/hellop/parameters/howmany
3
# cat /sys/module/hellop/parameters/whom
YangZhu
# echo 10 >/sys/module/hellop/parameters/howmany
# echo YangYong >/sys/module/hellop/parameters/whom
-sh: cannot create /sys/module/hellop/parameters/whom: Permission denied
# rmmod hellop
howmany is 10, whom is YangZhu
Goodbye, cruel world
7、编程注意事项
- License问题。Linux内核源码以GPL许可发布,模块如果不声明自己使用的license,加载的时候警告。可以使用MODULE_LICENSE(“GPL”)来避免。
- 避免“名字空间污染”:因为模块动态链接到内核里,最好不要输出内核中已有的全局函数或全局变量。否则会后者会影响前者。可以通过查看 /proc/kallsyms 来查看内核符号列表。解决方法:
- EXPORT_NO_SYMBOLS;使用此宏,这个模块不输出任何符号,除了使用下面的宏定义的符号。
- EXPORT_SYMBOL (name); 使用此宏定义的符号,强制输出。需要在EXPORT_NO_SYMBOLS使用之前使用才能输出。可以输出static的符号。
- 模块之间的依赖问题:有的模块依赖于其他模块的函数或者变量,在加载前需要先加载所依赖的所有模块后,才能成功加载。卸载模块时要先卸载被依赖的所有模块后,才能成功卸载。
8、集成模块到内核步骤
- 使用module_init和module_exit宏来定义内核模块接口函数,并确保模块工作正常(hello.c)
- 把模块文件hello.c拷贝到内核的选定目录(例如:drivers/char目录)
- 修改选定目录(例如:drivers/char目录)下的Kconfig文件和Makefile文件
- 修改Kconfig文件,增加如下:
config HELLO
tristate ‘New Hello’
- 修改Makefile文件,增加如下:
obj-$(CONFIG_HELLO) += hello.o
- 重新配置内核,选中要将该功能编译进内核,而不是编译为模块。
- 重新编译内核,如果成功,则得到新内核。
- 测试此内核,确保内核模块已集成。
八、查看系统支持的设备
- /proc/devices:系统支持的字符设备驱动和块设备驱动,及其对应的主设备号。
- # cat /proc/devices
Character devices:
1 mem
10 misc
29 fb
400 leds
232 buttonsBlock devices:
1 ramdisk
31 mtdblock
254 sbull
- # cat /proc/devices
- dmesg:查看系统的启动信息。可以看到系统支持的驱动的一些打印信息。
- # dmesg
leds initialized
snull: snull initialized
buttons initialized
snull: enter snull_open
- # dmesg
- /proc/ioports(/proc/iomem):查看设备的IO内存物理地址。
- # cat /proc/iomem
19000300-19000310 : cs8900
19000300-19000310 : cs8900
30000000-33ffffff : System RAM
30024000-30293fff : Kernel text
30294000-302f1f97 : Kernel data
49000000-490fffff : s3c2410-ohci
49000000-490fffff : ohci_hcd
4d000000-4d0fffff : s3c2410-lcd
4e000000-4e0fffff : s3c2440-nand
4e000000-4e0fffff : s3c2440-nand
50000000-50003fff : s3c2440-uart.0
50000000-500000ff : s3c2440-uart
50004000-50007fff : s3c2440-uart.1
50004000-500040ff : s3c2440-uart
50008000-5000bfff : s3c2440-uart.2
50008000-500080ff : s3c2440-uart
52000000-520fffff : s3c2440-usbgadget
53000000-530fffff : s3c2410-wdt
53000000-530fffff : s3c2410-wdt
54000000-540fffff : s3c2440-i2c
54000000-540fffff : s3c2440-i2c
55000000-550fffff : s3c2410-iis
55000000-550fffff : s3c2410-iis
56000010-5600001b : qq2440_leds
56000054-56000057 : qq2440_button34
56000064-56000067 : qq2440_button12
57000000-570000ff : s3c2410-rtc
57000000-570000ff : s3c2410-rtc
f0300000-f03fffff : s3c2410-lcd
- # cat /proc/iomem
- /proc/interrupts:查看正在使用的中断号
# cat /proc/interrupts
CPU0
16: 4 s3c-ext0 KEY4
18:0 s3c-ext0 KEY3
30:461932 s3c S3C2410 Timer Tick
32:0 s3c s3c2410-lcd
34:0 s3c I2SSDI
35:0 s3c I2SSDO
42:0 s3c ohci_hcd:usb1
43:0 s3c s3c2440-i2c
53:11257 s3c-ext eth0
55: 0 s3c-ext KEY2
63:0 s3c-ext KEY1
70:990 s3c-uart0 s3c2440-uart
71:2206 s3c-uart0 s3c2440-uart
83:0 - s3c2410-wdt
九、Linux驱动相关代码放在drivers/目录下,常见驱动目录介绍
- block/:常见块设备驱动。
- char/,serial/:虚拟终端,串口。
- net/:网络设备驱动。
- video:VGA和framebuffer设备驱动。
- Ide/,scsi/:IDE和SCSI设备驱动。
- 顶层目录的子目录sound/:声卡驱动。