分类:
2011-09-11 11:17:45
本章通过scull( Simple Character Utility for Loading Localities)来展示字符设备驱动的编写。scull不依赖于硬件,它把内存当做设备来操作,但它可作为编写实际字符驱动程序的范例。
1、主设备号和次设备号
主设备号表示设备对应的驱动程序
次设备号被内核用来确定设备文件所指的设备(被内核用来决定使用哪个设备)
ls -l /dev 可以看到在输出结果的每一行的组名和最后修改时间之间有2个数,分别为设备文件的主设备号和次设备号。
在内核中,使用dev_t(
通过宏MAJOR、MINOR(
MAJOR(dev_t dev);
MINOR(dev_t dev);
而通过宏MKDEV可以将主设备号major和次设备号minor合成为一个dev_t。
MKDEV(int major, int minor)
2、分配和释放设备编号
建立一个字符驱动时,驱动程序需要做的第一件事是获取一个或多个设备编号。我们可以通过register_chrdev_region(
int register_chrdev_region(dev_t first, unsigned int count, char *name);
说明:
first 表示要分配的起始设备编号,其次编号部分常常是0(非强制性要求)
count 表示要分配的连续设备编号总数。注意, 如果 count 太大, 则可能会溢出到下一个主编号; 但是只要你要求的编号范围可用, 一切依旧会正确运行.
name 表示这个编号范围对应的设备的名称; 它会出现在 /proc/devices 和 sysfs 中.
返回值:分配成功返回0;否则返回一负数(error number)
在不知道设备使用哪个主设备号时,可以通过alloc_chrdev_region来动态分配设备编号。
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
说明:
dev为输出参数,表示起始设备编号;
firstminor表示分配的起始设备编号的次设备号必须是firstminor(第一个要用的次设备号);
count、name和返回值同register_chrdev_region
register_chrdev_region与alloc_chrdev_region分配的设备编号,在不需要再使用时,应当通过unregister_chrdev_region将设备编号释放。unregister_chrdev_region通常在你的模块cleanup函数里调用。
void unregister_chrdev_region(dev_t first, unsigned int count);
说明:first与count同register_chrdev_region
分配设备编号的最佳方法是默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的能力。在scull 源码中,定义了一个用于保存主设备号的全局变量 scull_major,(还有一个scull_minor给次设备号)。这个变量初始化为SCULL_MAJOR(scull.h,SCULL_MAJOR的缺省值是0, 意思是"使用动态分配")用户可以使用缺省值(使用动态分配)或者指定一个特殊主设备号(在编译前修改SCULL_MAJOR宏定义或者在 insmod 命令行指定一个值给 scull_major)。
if (scull_major != 0) { /* 预先指定了主设备号 */
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else { /* 动态自己生成设备编号,然后再利用设备编号得到主设备号;
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
"scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
动态分配设备编号的缺点是你无法提前创建设备节点, 因为分配给你的主设备会变化. 但是一旦编设备号分配号后, 你就可以从 /proc/devices 中读取到主设备号。
为了使用动态分配设备编号来加载驱动, 可使用一个简单的shell script(scull_load)来代替调用 insmod。在该script中,调用 insmod 后, 通过读取 /proc/devices 来获取主设备号并创建设备节点。scull源码中的scull_load太过复杂,下面的scull_load是我删减后的:
#!/bin/sh
MODULE_NAME="scull"
DEVICE_NAME="scull"
MODE="664"
#加载驱动模块
/sbin/insmod ./$MODULE_NAME.ko $* || exit 1
# 删除旧的设备节点
rm -f /dev/${DEVICE_NAME}[0-3]
#获取主设备号,并创建相应的设备节点
major=$(awk "\\$2==\"$MODULE_NAME\" {print \\$1}" /proc/devices)
mknod /dev/${DEVICE_NAME}0 c $major 0
mknod /dev/${DEVICE_NAME}1 c $major 1
mknod /dev/${DEVICE_NAME}2 c $major 2
mknod /dev/${DEVICE_NAME}3 c $major 3
#修改设备节点访问权限
chmod $MODE /dev/${DEVICE_NAME}[0-3]
Makefile也需要修改,scull_unload可以不改。我简化后的Makefile:
|
其中,scull-objs := main.o pipe.o access.o的意思是scull.o是通过main.c、pipe.c和access.c三个文件编译而成的。
3、file_operation
通过file_operation(
通常,将file_operation结构或者其指针称为fops。file_operation结构中每个成员必须指向驱动中的函数,并且这些函数各自实现一个特殊的操作(打开设备,读设备等), 或者对于不支持的操作置为 NULL。当指定为NULL时,调用该系统调用时,内核的确切行为因每个函数不同的。下面简单介绍了file_operation的各个成员.
struct module *owner
owner并不是一个设备操作; 而是一个指向拥有该file_operation的模块的指针(指明file_operation的拥有者)。这个成员用来防止模块在它的操作还在被使用时被卸载. 一般都将owner初始化为 THIS_MODULE(一个在
loff_t (*llseek) (struct file *, loff_t, int);
llseek用来改变文件的当前读/写位置。修改成功时,函数返回值为新的读写位置;否则,返回值为一负数(error number). loff_t 参数是一个"long offset"(长偏移量), 并且就算在 32位平台上也至少 64 位宽. 如果这个函数指针被置为NULL, seek 调用会以无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
read用来从设备中读取数据. 如果这个函数指针被置为NULL,read 系统调用返回-EINVAL("Invalid argument") . 若read返回一个非负值,代表已成功读取的字节数( 返回值是一个 "signed size" 类型, 常常为目标平台的整型).
ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);
aio_read用于从设备中异步读取数据(可能在aio_read返回时,读操作还没完成)。如果这个函数指针被置为NULL,所有aio_read操作会由read(同步读)代替进行.
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
write用于向设备写(发送)数据(该函数类似read)。如果这个函数指针被置为NULL,write系统调用返回-EINVAL。如果write返回一个非负值,代表已成功发送的字节数。
ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);
aio_write用于异步地向设备写数据(该函数类似aio_read)。如果这个函数指针被置为NULL,所有aio_write操作会由write(同步读)代替进行.
int (*readdir) (struct file *, void *, filldir_t);
readdir用来读取目录。该函数仅对常规文件系统有用,对于设备文件这个成员应当为 NULL。
unsigned int (*poll) (struct file *, struct poll_table_struct *);
poll 方法是poll, epoll, 和 select 3 个系统调用的后端实现。poll, epoll, 和 select, 都用作查询对一个或多个文件的读或写是否会阻塞。poll返回一个表示是否可以非阻塞读或写的位掩码, 并且, 可能地, 提供给内核用来使调用进程阻塞直到I/O操作变为可能的信息。如果一个驱动的 poll 方法为 NULL, 则设备被假定为不阻塞地可读可写.
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ioctl 系统调用使设备能够执行特殊命令(例如格式化软盘的一个磁道)。 ioctl 命令可以被内核识别,不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求系统调用返回一个-ENOTTY错误(设备无此 ioctl命令),.
int (*mmap) (struct file *, struct vm_area_struct *);
mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法NULL, mmap 系统调用返回 -ENODEV错误.
int (*open) (struct inode *, struct file *);
open用于打开设备文件,并进行必要的初始化工作。如果这个方法为NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
int (*flush) (struct file *);
flush 操作在进程关闭它的设备文件描述符的拷贝时被调用,它应当执行并且等待设备任何未完成的操作(不要和用户查询请求的 fsync 操作)。在驱动程序中很少使用flush。如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.
int (*release) (struct inode *, struct file *);
在文件结构file被释放时调用release. 如同 open, release 可以为 NULL.
int (*fsync) (struct file *, struct dentry *, int);
这个方法是 fsync 系统调用的后端实现,用于刷新未执行完和未执行的操作。如果这个指针是 NULL, 系统调用返回 -EINVAL.
int (*aio_fsync)(struct kiocb *, int);
aio_fsync为fsync 方法的异步版本.
int (*fasync) (int, struct file *, int);
fasync用来在设备的FASYNC标志被改变时通知设备(异步通知在第 6 章中介绍). 如果这个成员为NULL,则驱动不支持异步通知.
int (*lock) (struct file *, int, struct file_lock *);
lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个保护多个内存区的单个读或写操作; 这些系统调用允许它们这样做而且不必对数据进行额外拷贝. 如果这些函数指针为 NULL, 所有的这些操作会通过多次调用read 和 write来代替。
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
这个方法实现 sendfile 系统调用的读(使用最少的拷贝从一个文件描述符搬移数据到另一个).在设备驱动程序中,sendfile常常为NULL.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
sendpage 是 sendfile系统调用的另一半; 它被内核调用来发送数据, 一次发送相应文件的一一个页面。在设备驱动程序中,通常不实现 sendpage.
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
get_unmapped_area用来在进程的地址空间中找一个合适的位置来映射底层设备的内存段. 这个任务通常由内存管理程序进行; 这个方法可以使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动不实现该方法。
int (*check_flags)(int)
check_flags允许模块检查传递给 fnctl(F_SETFL...) 的标志.
int (*dir_notify)(struct file *, unsigned long);
dir_notify在应用程序使用 fcntl 来请求目录改变通知时被调用. 只对常规文件系统有用; 驱动程序不需要实现dir_notify.
4、file
在内核中,file(
注意:file与用户空间程序的FILE指针没有任何关系. FILE 定义在C函数库中, 从不出现在内核代码中. 而struct file是一个内核结构, 从不出现在用户程序中.
在内核源码中, struct file的指针常常称为filp("file pointer"). file的主要成员如下:
mode_t f_mode;
文件模式f_mode可以通过FMODE_READ、FMODE_WRITE来定义文件是可读的或者是可写的(或者都是)。你可能会想在你的 open 或者 ioctl 函数中通过检查f_mode,来确定文件的读写许可, 但是你没有必要这么做, 因为内核在调用你的方法之前已经检查过文件的读写许可了.
loff_t f_pos;
f_pos表示文件的当前读写位置(loff_t 在所有平台都是 64 位)。如果驱动程序需要知道文件的当前读写位置, 驱动可以读这个值,但是不应该修改这个值; read和write方法应当使用函数参数列表最后一个参数接收到的指针来更新文件的当前读写位置,而不是直接修改filp->f_pos. 但也有一个例外——llseek, 它的目的就是改变当前读写位置.
unsigned int f_flags;
f_flags为文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC(
struct file_operations *f_op;
f_op表示和文件关联的操作. 内核将分配f_op指针作为 open 系统调用实现的一部分, 然后在需要执行如何操作时读取f_op. 但内核从不会为了下次引用而保存filp->f_op的值。这意味着你可改变文件关联的文件操作, 在函数返回后新的file_operations就会起作用. 例如, 关联到主设备号 1 (/dev/null, /dev/zero, 等等)的 open 方法会根据打开的次设备号来替代 filp->f_op 中的操作. 这个做法允许, 在同一个主设备号并且不增加每个系统调用的开销下,实现多种行为. 替换文件操作的能力类似于面向对象编程的"函数重载".
void *private_data;
open 系统调用在调用驱动的open方法之前将private_data置为NULL. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是你必须在内核销毁文件结构之前, 在 release 方法中释放那个内存。通过private_data可以实现在系统调用间保存状态信息。
struct dentry *f_dentry;
f_dentry为与文件相关联的目录项( dentry ). 设备驱动编写者同常地不需要关心dentry 结构, 除非想通过filp->f_dentry->d_inode来访问inode结构.
file还有很多其他成员,但它们与设备驱动无关,所以这里不介绍它们。
5、inode
在内核中,inode用来表示文件。它和file(代表一个打开的文件)是不同的. 一个文件可能有多个file来代表文件的多次打开(多个文件打开描述符),但是它们都指向同一个inode 结构。inode结构包含大量与文件相关的信息。但只有2个成员在编写驱动代码时有用:
dev_t i_rdev;
对于设备文件节点, 这个成员包含实际的设备编号.
struct cdev *i_cdev;
struct cdev 是一个表示字符设备的内核数据结构。一个字符设备文件节点的i_cdev包含一个指向cdev结构的指针。
为了增强代码的可移植性,可以通过以下两个宏从一个inode获取其主次设备号。
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);