○、说明
笔记适用于Linux的2.6.10以后的内核。
笔记以Linux Device Driver3提供的scull程序(scull目录中的main.c和scull.h)为记录主线,并以该驱动程序中的各种系统调用和函数调用流程为记录顺序。比如,module_init( )和module_exit( )为相对应的一对系统调用,一般书籍中都会放在一起讨论,但是本笔记却不会这样,而是在需要调用的时候才会涉及,因此module_init( )会放在笔记开始时,也就是刚加载module时讨论,而module_exit( )则会放在笔记结束前,也就是要卸载module时再加以讨论。
该笔记的的目的是为了对Linux Device Drvier3中提到的各个知识点作一下整理,理清一下头绪,从而能让我对Linux驱动程序加深整体或者全局上的理解。
注:个人理解,有误难免!
*******************************************
驱动程序module的工作流程主要分为四个部分:
1、 用Linux提供的命令加载驱动module
2、 驱动module的初始化(初始化结束后即进入“潜伏”状态,直到有系统调用)
3、 当操作设备时,即有系统调用时,调用驱动module提供的各个服务函数
4、 卸载驱动module
一、 驱动程序的加载
Linux驱动程序分为两种形式:一种是直接编译进内核,另一种是编译成module形式,然后在需要该驱动module时手动加载。对于前者,还有待学习。
Module形式的驱动,Linux提供了两个命令用来加载:modprobe和insmod。
其中modprobe可以解决驱动module的依赖性,即假如正加载的驱动module若引用了其他module提供的内核符号或者其他资源,则 modprobe就会自动加载那些module,不过,使用modprobe时,必须把要加载的驱动module放在当前模块搜索路径中。而insmod 命令不会考虑驱动module的依赖性,但是它却可以加载任意目录下的驱动module。
一般来说,在驱动开发阶段,使用/sbin/insmod比较方便,因为不用将module放入当前module搜索路径中。
一旦使用insmod加载模块,则Linux内核就会调用module_init(scull_init_module)特殊宏,其中scull_init_module是驱动初始化函数,可自定义名称。
在用insmod加载module时,还可以给module提供模块参数,但是这需要在驱动源代码中加入几条语句,让模块参数对insmod和驱动程序可见,如:
static char *whom=”world”;
static int howmany=10;
module_param(howmany,int,S_IRUGO);
module_param(whom,charp,S_IRUGO);
这样,当使用/sbin/insmod scull.ko whom=”string” howmany=20这样的命令加载驱动时,whom和howmay的值就会传入scull驱动模块了。
驱动程序module被加载后,若对设备进行操作(如open,read,write等),驱动module就会调用相应的函数响应该操作。
那么,当对设备进行操作时,驱动module又怎么知道是自己应该有所响应,而不是其他的驱动module呢,也就是说,Linux内核怎么知道应该调用哪一个驱动module呢?
目前我只知道有两种方式将设备与驱动module联系在一起(也许应该说提供访问设备的一种途径比较恰当):其一是通过某些设备的ID(比如PCI设备和 USB设备的Device ID和Product ID),Linux内核根据这些ID调用驱动module;其二是在/dev目录下根据设备的主次设备号创建对应的设备节点(即设备文件),这样当操作 /dev目录下的设备文件时,就会调用相应的驱动module。
二、 驱动module的初始化
使用insmod加载驱动module时,需要让驱动module为设备做一些初始化动作,主要目的是让Linux内核知道这个设备(或者说module?),以及在以后对该设备进行操作(如open,read,write等等)时,让Linux内核知道,本module拥有哪些函数可以服务于系统调用。
因此,scull_init_module函数中主要做了以下几件事情:
a) 分配并注册主设备号和次设备号
b) 初始化代表设备的struct结构体:scull_dev
c) 初始化互斥体init_MUTEX(本笔记不整理)
d) 初始化在内核中代表设备的cdev结构体,最主要是将该设备与file_operations结构体联系起来。
1、 分配并注册主次设备号
设备号是在驱动module中分配并注册的,也就是说,驱动module拥有这个设备号(我的理解),而/dev目录下的设备文件却是根据这个设备号创建的,因此,当访问/dev目录下的设备文件时,驱动module就知道,自己该出场服务了(当然是由内核通知)。
在Linux内核看来,主设备号标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备(也就是/dev下的设备文件)服务;而次设备号则用来标识具体且唯一的某个设备。
在内核中,用dev_t类型(其实就是一个32位的无符号整数)的变量来保存设备的主次设备号,其中高12位表示主设备号,弟20位表示次设备号。
设备获得主次设备号有两种方式:一种是手动给定一个32位数,并将它与设备联系起来(即用某个函数注册);另一种是调用系统函数给设备动态分配一个主次设备号。
对于手动给定一个主次设备号,使用以下函数:
int register_chrdev_region(dev_t first, unsigned int count, char *name)
其中first是我们手动给定的设备号,count是所请求的连续设备号的个数,而name是和该设备号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
比如,若first为0x3FFFF0,count为0x5,那么该函数就会为5个设备注册设备号,分别是0x3FFFF0,0x3FFFF1, 0x3FFFF2,0x3FFFF3,0x3FFFF4,其中0x3(高12位)为这5个设备所共有的主设备号(也就是说这5个设备都使用同一个驱动程序)。而0xFFFF0,0xFFFF1,0xFFFF2,0xFFFF3,0xFFFF4就分别是这5个设备的次设备号了。
需要注意的是,若 count的值太大了,那么所请求的设备号范围可能会和下一个主设备号重叠。比如若first还是为0x3FFFF0,而count为0x11,那么 first+count=0x400001,也就是说为最后两个设备分配的主设备号已经不是0x3,而是0x4了!
用这种方法注册设备号有一个缺点,那就是若该驱动module被其他人广泛使用,那么无法保证注册的设备号是其他人的Linux系统中未分配使用的设备号。
对于动态分配设备号,使用以下函数:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
该函数需要传递给它指定的第一个次设备号firstminor(一般为0)和要分配的设备数count,以及设备名,调用该函数后自动分配得到的设备号保存在dev中。
动态分配设备号可以避免手动指定设备号时带来的缺点,但是它却也有自己的缺点,那就是无法预先在/dev下创建设备节点,因为动态分配设备号不能保证在每次加载驱动module时始终一致(其实若在两次加载同一个驱动module之间并没有加载其他的module,那么自动分配的设备号还是一致的,因为内核分配设备号并不是随机的,但是书上说某些内核开发人员预示不久的将来会用随机方式进行处理),不过,这个缺点可以避免,因为在加载驱动module后,我们可以读取/proc/devices文件以获得Linux内核分配给该设备的主设备号。
Linux Device Driver3提供了一个脚本scull_load和scull_unload,可以在动态分配的情况下为设备创建和删除设备节点。其实它也是利用了 awk工具从/proc/devices中获取了信息,然后才用mknod在/dev下创建设备节点。
其实scull_load和scull_unload脚本同样可以适用于其他驱动程序,只要重新定义变量并调整mknod那几行语句就可以了。
与主次设备号相关的3个宏:
MAJOR(dev_t dev):根据设备号dev获得主设备号;
MINOR(dev_t dev):根据设备号dev获得次设备号;
MKDEV(int major, int minor):根据主设备号major和次设备号minor构建设备号。