Linux 2.6 字符设备驱动程序

、说明
笔记适用于 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 构建设备号。
2
初始化代表设备的 scull_dev 结构体
scull
源代码中定义了一个 scull_dev 结构体,包括 qset qutuam ,信号量 sem 以及 cdev 等字段。其中 qset qutuam 的初始化对于 Linux 驱动程序的知识点来说毫不相关,因此不加讨论。

只要知道,在加载 module 时所调用的 module 初始化函数中,可以初始化一些设备相关的变量。但是根据 Linux Device
Drvier3
作者的意思,设备相关的变量或者一些资源最好应当在 open 函数中初始化,比如像中断号等,虽然在 module 初始化函数中注册也是允许

的,但最好是在第一次打开设备,也就是 open 函数中再行分配。
3 初始化互斥体 init_MUTEX
互斥体 MUTEX ,也就是信号量的一个变种,与 completion ,自旋锁 spinlock 等等都与驱动中的并发和竞态相关,以后再说。

4
初始化在内核中代表设备的 cdev 结构体
其实在 Linux 内核中, cdev 结构体才是真正代表了某个设备。在内核调用设备的 open read 等操作之前,必须先分配并注册一个或者多个 cdev 结构。

想可以这么理解,主次设备号是涉外的,主要用来在与外部(指的是驱动 module Linux 内核以外)交互时确定身份;而 cdev 结构体则是涉内的,当
需要在 module 内部,或者与 Linux 内核之间传递一些变量,指针, buffer 等东东,或者要调用驱动 module 中的某个服务函数时,就要用到
cdev
结构体了。
scull 函数中, cdev 结构体的分配,注册与初始化使用了一个自定义的 scull_setup_cdev 函数,在该函数中,主要由以下 4 条语句对 cdev 进行初始化:
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
dev 变量是 scull 程序定义的代表设备的一个结构体,它包含了 cdev 结构体,对于 dev 来说, cdev 结构体应该说就是它的核心)
第一条语句是初始化 cdev 结构体,比如为 cdev 结构体分配内存,为 cdev
构体指定 file_operations 等等,而第三条语句的作用初看起来似乎与第一条有所重复。但 scull 程序中既然这么写想必就有它的用意,也许需
要看 Linux 内核源代码才能搞明白,但目前我是这么理解的:第一条语句中有关 file_operations 的部分是为了告诉 Linux 内核,该
cdev
结构体相关的 file_operations scull_fops ;而第二条语句则是真正为 cdev 指定了它的 file_operations
字段就是 scull_fops

scull_fops
file_operations 类型的变量, file_operations 也是一个结构体,而且是 Linux 驱动程序中很重要的一个结构体,在 scull 程序中,定义如下:
struct file_operations scull_fops = {
.owner =  THIS_MODULE,
.llseek =  scull_llseek,
.read =  scull_read,
.write =  scull_write,
.ioctl =  scull_ioctl,
.open =  scull_open,
.release =  scull_release,
};

上定义中,第一条 .owner 字段说明本 file_operations 结构体的拥有者是本驱动 module ,而接下来的几个字段则是告诉驱动
module
,当有相应的系统调用到达该 module 时, module 应该调用哪一个函数来为该系统调用服务。比如说,若有一个 open 系统调用到达
module
,则 module 通过查询 file_operations 结构体就知道了,与 open 系统调用相关的是 scull_open 函数,于是
module
就调用 scull_open 函数来为 open 系统调用服务了。其他几个字段也完全类似。
当然, Linux 内核定义的 file_operations 结构体还包括其他一些字段,比如异步读写等等,但还是等用到的时候再说吧。
cdev 初始化的第二条语句是 dev->cdev.owner = THIS_MODULE ,这条语句就是说正在初始化的 cdev 结构体的拥有者就是本 module
cdev 初始化的最后一条语句是 err = cdev_add
(&dev->cdev, devno,
1)
,该语句的目的就是告诉内核,该 cdev 结构体信息。因为 cdev_add 函数有可能调用失败,所以需要检测该函数调用的返回值。而一旦

cdev_add
调用成功返回,那么我们的设备就 了!也就是说,外部应用程序对它的操作就会被内核允许且调用。因此在驱动程序还没有完全准备好处理
设备上的操作时,就绝不能调用 cdev_add
三、 设备操作
驱动 module 因为由 insmod 的加载而进行了初始化之后,就会进入 潜伏 状态,也就是说,如果没有系统调用 ( open read ) ,那么 module 中定义的其他函数就绝不会运行!
这里所说的设备操作,是指当有系统调用到达驱动 module 时, module 就该调用某个或某些函数有所动作。
对于驱动开发来说,我主要关心的只有一点,那就是系统调用怎样把一些外部应用程序中的变量值传递给驱动 module
scull
程序中与设备操作相关的函数主要分为三类:初始化函数,实际的操作服务函数和清理函数。其中初始化函数只有一个,就是 open 函数,而操作服务函数则包括 read write llseek 等等函数,至于清理函数则是 release 函数。
1
open 函数
open
函数提供给驱动程序以初始化的能力,从而为以后的操作做准备。

起来在用 insmod 加载驱动后也有一个初始化动作,但那个初始化是相对于整个 Linux 内核,或者说是针对整个 module 在涉外时的全局意义上的初始
化;而 open 函数的初始化则是相对于设备操作来说的,是属于驱动内部的初始化,比如为以后 read 操作时用到的某个变量 ( file 结构体 ) 作一下初始
化,再比如初始化一下设备,清空一下 buffer 等等。
在大部分的驱动程序中, open 应该完成如下工作:
a
确定要打开的具体设备
b
检查设备特定的错误(诸如设备未就绪或类似的硬件问题)
c
如果设备是首次打开,则对其作一下初始化
d
如有必要,更新 f_op 指针
e
分配并填写置于 filp->private_data 里的数据结构
open 函数的原型如下 ( 指的是在 file_operations 结构体中的定义 )
int (*open)(struct inode *inode, struct file *filp)
在驱动开发时要做的,就是为该函数作具体实现,当然对 open 函数的名称可以自定义,只要在填写 file_operations 结构体中的 open 字段时,将自定义的 open 函数名称填上就可以了。在 scull 程序中,用的就是 scull_open 函数名。
open 函数原型中,有 inode filp 两个参数,都是外部应用程序在操作
设备时通过调用系统调用传递给驱动 module 的。于是驱动 module 就可以通过这两个参数来确定要打开的具体设备了。其实这里所说的具体设备,并不是
说驱动 module 需要从系统安装的所有设备中确定它所要服务的设备,而是指 module 需要从某一类拥有相同主设备号的设备中确定它要服务的设备。

所以这么说,是因为驱动 module 是对应于某一个主设备号的所有设备的。换一句话说,就是 Linux 内核只管设备的主设备号,而不理会设备的次设备号是
什么,如果有两个,三个或者更多个设备拥有同一个主设备号,那么不管外部应用程序要操作这些设备中的哪一个, Linux 内核都只会调用同一个驱动
module
。但是驱动 module 却不能不管次设备号了,因为它是跟某一个具体的设备打交道的,所以它需要根据 open 系统调用时传递给它的参数中找到
次设备号,从而确定那唯一的一个设备(也许驱动 module 也可以同时操作几个设备,但一时也想不起来)。
但是上面所说的通过次设备号找到具体的设备,只是其中一种方法;另外还有一种方法就是通过 cdev 结构体确定某个具体设备。

备所拥有的 cdev 结构体,或者次设备号,都保存在 open 函数的 inode 参数中。我们可以使用 container_of 宏通过 inode 所拥有的
cdev
确定具体设备,也可以使用 iminor 宏从 inode 所拥有的 i_rdev 确定次设备号 (i_rdev inode 结构体中的一个 dev_t 类型
的变量,其中保存了真正的设备主次编号 )
对于 open 函数中的 file 参数, scull 程序主要用它来做两件事:其一是将
根据 cdev 获得的代表设备的 scull_dev 结构体保存到 file->private_data 中,这样就可以方便今后对该设备结构体的访问
了,而不用每次都调用 container_of 宏或者 iminor 宏来找到设备结构体了;其二是根据 file 结构体中的 f_flags 字段来确定,这次的
open
调用,是以写方式打开设备,还是以读方式来打开设备。
2 read 函数
驱动 module file_operations 结构体中可以定义很多设备操作服务函数,但是我现在关心的是这些函数怎样与系统调用,或者说是外部应用程序交互,而不管具体的设备操作怎么实现,所以只记录 read 函数作为代表。
read 函数的原型如下:
ssize_t read(struct file *filp,  char __user *buf,  size_t count,  loff_t *f_pos)

read
函数原型中有 4 个参数,分别是 filp buf count f_pos
其中 file 结构体指针参数可以用来确定我们要操作的设备,因为在 open 函数中,我们将代表设备的结构体保存到了 filp private_data 字段中。
buf
参数是一个指向用户空间的 buffer 的指针 (buf 前面的 __user 表示用户空间 ) ,对于 read 来说,就是可以把要传送给外部应用程序的数据放入这
buffer 中。当然,我们不能简单地将数据 copy 到这个 buffer 中,而是要使用 Linux 内核提供的一些函数,比如 copy_to_user
数。
count
是请求传送的数据长度。
f_pos
是一个指向 “long offset type” 对象的指针,指明外部应用程序在文件中进行存取操作的位置。
read
函数的返回值是有符号整数类型的指,一般是 read 操作的实际存取数。
3 release 函数
release
函数的作用正好与 open 相反,有时候 release 函数的实现被称为 device_close ,而不是 device_release 。但无论那种形式,这个设备方法都应该完成如下任务:
a
、释放 open 分配的,保存在 file->private_data 中的所有内容。
b
、在最后一次关闭操作时关闭设备。
relese
函数由 close 系统调用引起,但并不是每一次 close 系统调用都
会引起 release 函数的调用。只有那些真正释放设备数据结构的 close 系统调用才会引起 release 函数的调用。因为 Linux 内核为每个
file
结构体维护其被引用多少次的计数器,只有当 file 结构体的计数器归 0 时, close 系统调用才会引用 release 函数,这只在删除这个结构时
才会发生,因此每次 open 驱动程序都只会看到一次对应的一次 release 调用。
四、 卸载驱动 module
每个重要的模块都需要一个清除函数,该函数在模块被移除前注销接口并向系统中返回所有资源。如果一个模块未定义清除函数,则内核不允许卸载该模块。
Linux
驱动 module 的卸载可以用 /sbin/rmmod
scull.ko
命令,这时 Linux 内核就会调用驱动程序中用 module_exit(scull_cleanup_module) 特殊宏定义的清除函

数,也就是说, module_exit 声明用来帮助 Linux 内核找到模块的清除函数,在 scull 程序中,清除函数就是
scull_cleanup_module
函数。
模块的清除函数需要撤销初始化函数注册的所有资源,并且习惯上 ( 但不是必须的 ) 以与初始化函数注
册相反的顺序进行撤销。需要注意的是,这里指的初始化函数是指用 module_init 宏声明的初始化函数,而不是指 open 函数,与 open 函数对应的
应当是 release 函数。
scull 程序中,清除函数主要做了两件事:一是 free 了所有为 scull 设备分配的内存;二是收回了初始化函数所注册的设备号。

你可能感兴趣的:(Linux 2.6 字符设备驱动程序)