您的位置:首页 > 运维架构 > Linux

Linux设备驱动程序学习(1)--字符设备驱动

2012-06-08 20:57 555 查看
本文一些内容转自《Linux 设备驱动程序》。

scull的设计

        编写驱动程序的第一步就是定义驱动程序为用户程序提供的能力(机制)。而scull这个设备是内存的一部分,本文中主要着手实现这样的一个scull设备:由一个全局持久的内存区域组成。“全局”是指设备如果被多次打开,打开它的所有文件描述符可共享该设备所包含的数据;“持久”是指如果该设备关闭后再打开,则其中的数据不会丢失。

设备号

        设备号包括主设备号和次设备号。通常而言,主设备号标识设备对应的驱动程序,次设备号由内核使用,用于正确确定设备文件所指的具体设备。Linux内核允许多个驱动程序共享主设备号,但是我们看到的大多设备仍然按照“一个主设备号对应一个驱动程序”的原则组织。

设备编号的内部表达

        Linux内核使用dev_t类型(在<linux/types.h>中定义)来表达设备编号,dev_t是一个32位的数,其中12位表示主设备号,其余20位表示次设备号。要从dev_t类型变量获得主设备号和次设备号应该使用以下两个宏:
MAJOR(dev_t devid);
MINOR(dev_t devid);
       相反,要将已知的主设备号和次设备号转换成对应的dev_t类型,应使用:
MKDEV(int major, int minor);

分配和释放设备编号

        在建立一个字符设备驱动之前,我们的驱动程序首先要获得一个或多个设备编号,这个工作可以采用两个方法。
1. 如果知道即将要使用的设备编号就使用以下函数:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
该函数在成功时返回0。first是要分配的设备编号范围的起始值,count是请求的连续设备编号的个数,name是和该设备编号范围相关的设备名称。如果我们提前明确知道所需要的设备编号,则使用上面的函数。但是我们经常不知道设备将要使用什么主设备号,这样的话下面的情况则是我们应该采取的。
2. 在运行的过程中使用下面的函数,这样内核将为我们恰当分配所需要的主设备号:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
dev是用于输出的参数,函数成功执行后该参数将保存已分配范围的第一个编号,firstminor是要使用的的第一个次设备号,通常为0,count和name同上。
当不再使用这些编号时应该使用下面的函数释放这些编号:
void unregister_chrdev_region(dev_t first, unsigned int count);
在LDD3中,作者建议以如下的方式分配设备号:

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);
}
if(result < 0)
{
printk(KERN_WARNING "cannot get major %d\n",scull_major);
return result;
}

三个重要的数据结构

文件操作--struct file_operations

         在驱动中要实现一些基本的对设备的操作接口,而将这些操作连接到设备编号,则是由struct file_operations这个结构体完成的,该结构体在<linux/fs.h>中定义。该结构体中包含了一组函数指针,每个打开的文件和一组函数关联(通过包含指向一个struct file_operations结构的f_ops字段)。这些函数主要用来实现系统调用,如open,read,write等。可以认为文件时对象,操作文件的函数成为方法,这体现了面向对象编程的思想。在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,
};

file结构

          file结构代表一个打开的文件,由内核在open时创建,并传递给在该文件上操作的所有函数,直到最后的close函数。在文件的所有实例被关闭后,内核会释放这个结构。在file结构中,有一个struct file_operations 结构指针,内核在open时对这个指针赋值。

inode结构

          内核用inode结构在内部表示文件,file结构表示打开的文件描述符,对单个文件,可能会有很多个表示打开的文件描述符(file结构),但它们都指向单个inode结构。inode结构中,dev_t i_rdev表示设备文件的设备编号,struct cdev *i_cdev表示该字符设备的内核内部结构。

字符设备的注册

          内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个cdev结构。如果打算在运行时获取一个独立的cdev结构,则应该如下编写代码:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
          如果将cdev结构嵌入到自定义的设备结构中,还需要用下面的函数初始化已分配的结构:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
cdev->owner=THIS_MODULE;
最后,通过下面函数告诉内核该字符结构的信息:
int cdev_add(struct cdev *dev, dev_t num,unsigned int count);
dev是cdev结构,num是该设备对应的第一个设备编号,count是和该设备关联的设备编号的数量,通常取1。要从系统中移除一个字符设备时,使用如下函数:
void cdev_del(struct cdev *dev);
在cdev_del调用后,就不应该再访问cdev结构了。

scull的注册

           在scull内部,通过一个struct scull_dev结构来表示设备,定义如下:
struct scull_dev
{
struct scull_qset *data;
int quantum;
int qset;
unsigned long size;
struct semaphore sem;
struct  cdev cdev;
};
按照上面所说的注册方法,对scull设备的注册代码如下:
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函数提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。在大部分驱动中,open应完成以下工作:

*检查设备特定的错误(如设备未就绪或类似的硬件问题);
*如果设备是首次打开,则对其进行初始化;
*如有必要,更新f_op指针;
*分配并填写置于filp->private_data里的数据结构。
open函数原型如下:
int (*open)(struct inode *inode, struct file *filp);
其中inode参数在其i_cdev字段中包含了我们所需要的信息,即我们之前设置的cdev结构。但是我们通常不需要cdev结构本身,而是希望得到包含cdev结构的scull_dev结构。这可以通过定义在<linux/kernel.h>中的container_of宏来实现:
container_of(pointer, container_type, container_field);
当得到scull_dev结构后,scull将一个指针保存到了file结构中private_data字段中,这样可以方便之后对该指针(指向scull_dev)的访问。因此,针对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 */
return 0
b2a6
;          /* success */
}

release函数

            release函数的功能与open相反,release函数应该完成以下任务:
*释放由open函数分配的、保存才filp->private_data字段中的内容;
*在最后一次关闭操作时关闭设备。
在scull设备中,没有需要关闭的硬件设备,因此release方法如下
int scull_release(struct inode *inode, struct file *filp)
{
return 0;
}
注:并不是每个close调用都会引起release函数的调用。详细见LDD3 page64。

read和write函数

read和write的任务是拷贝数据到应用程序空间或者从应用程序拷贝数据到内核空间。
ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
filp是文件指针,count是请求传输的数据长度,buf是一个指向用户空间的缓冲区,这个缓冲区或者保存要写入的数据,或者是一个存放新读入数据的空缓冲区。f_pos指明用户在文件中进行存取操作的位置。
buf参数是用户空间的指针,内核中的驱动代码不能直接引用其中的内容,因此,要在read和write方法中实现用户地址空间和内核地址空间数据的拷贝,应该使用如下代码:
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);
这两个函数不仅在内核空间和用户空间拷贝数据,还会检查用户空间的指针是否有效,若无效,则不会拷贝。若拷贝过程中遇到无效地址,则仅拷贝部分数据。这就是说,这两个函数的返回值是还需要拷贝的数据数量。
无论read和write函数传输了多少数据,一般而言都应更新*f_pos所表示的文件位置,以便反映在新系统调用成功后当前的文件位置。适当情况下,内核会将文件位置的改变传回到file结构。
出错时,read和write函数返回一个负值,大于等于0的返回值告诉应用程序成功传输了多少字节。针对scull的read、write函数代码如下:
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;
}
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;
}


这样,正确编译后加载驱动即可测试该驱动程序了!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息