本章介绍以scullc为例,如何写一个完整的字符驱动。详细说明请参考ldd3 chap3
the design of scullc
在设置一个驱动之前,首先得知道驱动相关的硬件有那些功能,我们的驱动要提供哪些能力。
scullc提供一段内存区域,用于模式字符设备的读写。
major and minor numbers
字符设备的访问 可以通过在相应的文件系统中查找其名字。其通常都位于 /dev 目录下。
可以通过 ls -l 命令看出字符设备 跟普通文件的一些差异:通常在 最后一次访问时间之前,普通文件显示的是文件的大小;而字符设备是主次设备号,之间以逗号分割。
主设备号 用于区分何种设备;次设备号 用于内核在“同种设备中”访问我们的设备。
the internal representation of device numbers
内核内部用dev_t 表示设备的设备号(定义在 linux/types.h 中,包括主次设备号。)在2.6.11内核中,dev_t 是一个32位的整形:其中高12位表示主设备号,低20位表示次设备号。
但为了代码的可移植性,我们不应做此假设。我们可以通过下面接口来获取其主次设备号。(定义在 linux/kdev_t.h)
MAJOR(dev_t dev);
MINOR(dev_t dev);
你也可以通过主次设备号 来获取设备号
MKDEV(int major, int minor);
allocating and freeing device numbers
<linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name);
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
成功返回0,失败返回负整数。
分配成功后,设备的主设备号,name可以 从/proc/devices 中查看。
void unregister_chrdev_region(dev_t first, unsigned int count);
上面的函数只是分配了设备的主次设备号,并没有实现应用层访问设备的具体操作。
some important data structures
大部分设备的具体操作都涉及到3个重要的内核数据结构:file_operations, file 和 inode。
file operations <linux/fs.h>
到目前为止,我们还未涉及 对我们预留分配的设备号的操作。而fops 正式如此。
struct module *owner; //这个域用于防止 设备在使用时 被卸载,通常初始化为 THIS_MODULE
loff_t (*llseek) (struct file*, loff_t, int);
ssize_t (*read)(struct file*, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file*, const char __user*, size_t, loft_t*);
int (*readdir)(struct file*, void *, filldir_t); //只对具体的文件系统有用,设备文件通常赋为NULL;
unsigned int (*poll)(struct file*, struct poll_table_struct *);
int (*ioctl)(struct inode*, struct file*, unsigned int, unsigned long);
int (*mmap) (struct file*, struct vm_area_struct *); //如果为NULL, mmap系统调用返回 -ENODEV。
int (*open)(struct inode*, struct file*);
int (*flush)(struct file*);
int (*release)(struct in ode*, struct file*);
scullc设备驱动只实现一些最重要的设备方法。初始化代码如下:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scullc_llseek,
.read = scullc_read,
.write = scullc_write,
.ioctl = scullc_ioctl,
.open = scullc_open,
.release = scullc_release,
};
the file structure <linux/fs.h>
文件结构代表着一个打开的文件。当文件“完全关闭时”释放该数据结构。
mode_t mode; //FMODE_READ, FMODE_WRITE;当涉及到open 或者 ioctl 时可能需要检测该标志位的读写权限,而read和write时,因为内核已经在调用相应的方法前检测过了,所以。。。
loff_t pos;
unsigned int f_flags; //O_RDONLY, O_NONBLOCK, and O_SYNC.
struct file_operations *f_op; //文件相关的操作。在open 设备时进行赋值。
void *private_data; //open系统调用会在 涉及具体设备的open方法之前 将该域设为NULL,你可以将该域另作它用或者不使用该域。当有一点:如果你将该域指向一块分配的区域,在相应的release方法中要记得释放该区域。
struct dentry *f_dentry; //文件所在的目录。设备文件通常不设置目录结构,除了通过filp->f_dentry->d_inode访问inode节点。
the inode structure
与file结构代表一个打开的文件描述符不同,inode 代表着一个文件。一个文件可以有很多打开的文件描述符,但只有一个inode节点。
dev_t i_rdev; //相应文件的设备号。
struct cdev *i_cdev; //当inode指向一个字符设备时,该域指向相应的字符设备。
char device registeration
在内核调用你的字符设备操作之前,你必须注册你的字符设备。首先,必须包含<linux/cdev.h>头文件。
有两种方式分配和初始化字符设备:
(1)如果你想在运行时分配一个cdev结构,你可以这样实现
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
(2)如果你想在相应设备中嵌入cdev结构,可以调用下面接口
void cdev_init(struct cdev *cdev, struct file_operations *fops);
一旦建立起 cdev,最终要通知内核,这里有一个字符设备需要加入。
int cdev_add(struct cddv *cdev, dev_t num, unsigned int count);
关于cdev_add 有两点需要注意的地方:(1)cdev_add 可能失败;(2)一旦cdev_add 返回成功,你的设备就被“激活”了,可能会立即受到访问。
卸载设备时需要将cdev从系统中移除:
void cdev_del(struct cdev *dev);
一旦调用该接口后就不应该再访问此设备了。
device registration in scullc
scullc 数据结构
struct scullc_dev {
void **data; /*pointer to the first quantum set*/
int quantum; /*the current quantum size*/
int qset; /*the current array size*/
unsigned long size; /*amount of data stored here*/
struct semaphore sem; /*mutual exclusion semaphore*/
struct cdev cdev; /*char device struct*/
};
先忽视该数据结构中的其他元素,来看一下cdev如何初始化以及添加到内核的。代码如下:
static void scullc_setup_cdev(struct scullc_dev *dev, int index)
{
int err, devno = MKDEV(scullc_major, scullc_minor + index);
cdev_init(&dev->cdev, &scullc_fops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cddv, devno, 1);
if(err)
printk(KERN_NOTICE "Error %d adding scull%d\n", err, index);
}
自我感觉这段代码写的并不好:原因在于如果部分 cdev cdev_add 失败,应该撤销之前添加成功的cdev。而此段代码明显不能。
the older way
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
void unregister_chrdev(unsigned int major, const char *name);
这两个接口只是对 cdev_init, cdev_add 以及 cddv_del 的提取和封装。
下面涉及具体的文件操作。
open and release
the open method
open方法 的作用主要用于 对设备驱动进行初始化,为之后的其他方法调用提供准备工作。主要执行的任务如下:
(1)检测设备相关的错误(如设备是否已经激活 或相似的硬件问题)
(2)第一次打开设备需要进行的初始化
(3)必要时更新f_op指针
(4)初始化filp->private_data域
原型:
int (*open)(struct inode *inode, struct file *filp);
scullc_open 简单实现如下:
int scullc_open(struct inode *inode, struct file *filp)
{
struct scullc_dev dev = container_of(&inode->i_cdev, struct scullc_dev, cdev);
filp->private_data = dev; /*for other method*/
/*now trim to 0 the length of the device if open for write-only*/
if( (fill->f_flags & O_ACCMODE) == O_RDONLY) {
scullc_trim(dev); /*ignore errors*/
}
return 0;
}
因为scullc不涉及到具体的硬件,所以没有上面列出的(1),(2)。
the release method
release方法跟open的角色相反。主要任务包括:
(1)释放在open中分配的 filp->private_data
(2)最后一次关闭设备时 “shut down the device”
scullc的release非常简单,仅仅是返回0.
因为scullc仅仅涉及对分配的内存区域的读写操作,不具有一般设备的代表性。所以scullc的读写及内存接口此处不作涉及。有兴趣的可以自己查看内核代码中字符设备的读写实现。