一晃,这一个月又荒荒过去了,什么也没干,状态低糜。今天整理下状态,争取把字符设备驱动程序这一章的学习笔记整理完。
一,主设备号和次设备号
主设备号用于标识设备对应的驱动程序。次设备号用于正确确定设备文件所指的设备,由内核使用。
1,设备编号的内部表示:
内核中,用dev_t类型(在<linux/types.h>中定义)来保存设备编号---包括主设备号和次设备号。
主设备号,次设备号和dev_t之间的转换。
dev_t --------> 主设备号 + 次设备号:
MAJOR(dev_t dev); 获得主设备号
MINOR(dev_t dev); 获得次设备号
主设备号 + 次设备号 ---------> dev_t
MKDEV(int major, int minor);
2,分配和释放设备编号
在建立一个字符设备之前,首先要做的就是获取一个或者多个设备编号。
静态分配设备号:
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);
释放设备号:
void unregister_chrdev_region(dev_t first, unsigned int count);
通常,可在模块的加载函数中分配设备号,在模块的注销函数中释放设备号,scull的相应代码如下:
if (scull_major) {
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);
}
|
可见,当用户指定了主设备号和次设备号时,用的是静态分配的方式。当没指定的时候用的是动态的方式。
注意:分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
二,重要的数据结构
大部分字符设备驱动程序操作都会涉及到三个重要的内核数据结构。file_operations,file,inode.
文件操作:
结构file_operations:把自定义的操作与cdev的设备操作联系起来。
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.read = scull_read,
.write = scull_write,
.open = scull_open,
.release = scull_release,
};
|
然后会在字符设备初始化的函数中,将这些操作与cdev设备对应起来。
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
int err, devno = MKDEV(scull_major, scull_minor + index);
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
// dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
|
file结构:表示一个打开的文件。由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数。
inode结构:内核用此结构表示磁盘上存储的文件。
其中对驱动程序代码有用的为:
dev_t i_rdev:表示设备编号。
struct cdev *i_cdev: struct cdev是表示字符设备的内核数据结构。当inode指向一个字符设备文件时,该字段包含了对应的struct cdev结构的指针。
考虑到可移至性,我们不应该直接对i_rdev进行操作,而是使用以下两个宏来从inode中获得主设备号和此设备号。
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
三,字符设备的注册
内核内部使用struct cdev结构来表示字符设备。通常是将cdev结构嵌入到自己的特定设备结构中。在内核调用设备的操作之前,必须分配并注册该结构。代码中应该包含<linux/cdev.h>
注册部分有两种方法:2.6的方法和以前的方法。
2.6的方式:用cdev接口。
1,分配内存:
struct cdev *my_cdev = cdev_alloc();
|
2,初始化:
void cdev_init(struct cdev *cdev, struct file_operations, *fops);
|
这里,将操作集与cdev联系了起来。
3,注册:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
|
这里要注意的是,跟网络驱动程序一样,一旦该结构注册到内核中,则内核就可以调用其驱动程序操作该设备了,所以一定要都准备完成之后才可以把设备注册内核中。
4,注销:
void cdev_del(struct cdev *dev);
|
2.6以前的方法:
注册:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
|
这里,注册了以后内核会为该设备建立一个默认的cdev结构。
注销:
int unregister_chrdev(unsigned int major, const char *name);
|
应该使用新方法。snull对应的初始化注册代码为:
/*
* Set up the char_dev structure for this device.
*/
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
int err, devno = MKDEV(scull_major, scull_minor + index);
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
// dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
|
到目前位置,我们已经我们的字符设备注册到内核中了,下面来看看其中的一些设备方法。
四,open和release
open方法提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。在大多数驱动程序中,open应该完成以下工作:
1)检查设备是否有特定的错误(诸如设备未就绪或类似的硬件问题);
2)如果设备是首次打开,则对其进行初始化。
3)如有必要,更新f_op指针。
4)分配并填写置于filp->private_data里的数据结构。
下面是经过简化的scull的open代码:
int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */
/* now trim to 0 the length of the device if open was write-only */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
scull_trim(dev); /* ignore errors */
up(&dev->sem);
}
return 0; /* success */
}
|
这里有个container_of()函数。此函数的作用是通过cdev结构找到包含cdev结构的scull_dev结构。
一旦找到scull_dev以后,scull将一个指针保存到了file结构的private_data中,以便以后对该指针进行访问。(为什么不直接用一个scull_dev的指针来保存呢??这里我认为是因为file代表的是一个打开的文件,所以应该更合理一些)
release方法:
release方法的作用正好与open相反。一般应该完成以下工作:
1)释放由open分配的,保存在filp->private_data中的所有内容。
2)在最后一次关闭操作时关闭设备。
五,read和write
read和write方法完成的任务类似,即拷贝数据到应用程序空间,或反过来从应用程序空间拷贝数据。函数原型为:
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, char __user *buff, size_t count, loff_t *offp);
这里要注意:read和write方法的buff参数是用户空间的指针。因此,内核代码不能直接引用其中的内容。所以我们要通过特定的函数来对这个用户空间的缓冲区进行操作。如下:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
这两个函数在<asm/uaccess.h>中定义。
这两个函数也是read和write的核心函数。
snull对应的read代码如下:
ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr; /* the first listitem */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* how many bytes in the listitem */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - *f_pos;
/* find listitem, qset index, and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;
/* follow the list up to the right position (defined elsewhere) */
dptr = scull_follow(dev, item);
if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */
/* read only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;
if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
out:
up(&dev->sem);
return retval;
}
|
write的代码:
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; /* value used in "goto out" statements */
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
/* find listitem, qset index and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;
/* follow the list up to the right position */
dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
/* write only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;
if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
/* update the size */
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
up(&dev->sem);
return retval;
}
|
可以看出,这两个函数的大概流程就是找到位置,然后调用copy_to_user()或者copy_from_user(),然后更新指针位置。
ps:虽然感觉字符设备的大体流程和框架比较简单,但还是拖拖拉拉的弄了挺长时间,应该注意下自己的效率了。。