Chinaunix首页 | 论坛 | 博客
  • 博客访问: 108582
  • 博文数量: 45
  • 博客积分: 1810
  • 博客等级: 上尉
  • 技术积分: 345
  • 用 户 组: 普通用户
  • 注册时间: 2009-09-03 21:57
文章分类
文章存档

2010年(26)

2009年(19)

我的朋友

分类: LINUX

2010-05-12 13:09:00

第三章 字符设备驱动程序
主设备号和次设备号:
对字符设备的访问是通过文件系统的设备名称来访问的,那些名称被称为特诉文件或设备文件,或简单称之为文件系统的节点,它们通常位于/dev 下,字符设备驱动程序通常可以通过 ls -l 命令开查看,输出第一列“c“的即为字符设备,在列出的信息中,在最乎修改时间之前的两列对应设备的主设备号和次设备号。通常而言,主设备号对应驱动程序。次设备号有内核使用,用于正确确定设备文件所指的设备。我们可以通过次设备号获得一个指向内核设备的直接指针,除了知道次设备号用来指向驱动程序所实现的设备之外,无须多关心次设备号的其他信息。

设备编号的内部表达:
使用 下的宏 ,获得主设备号和次设备号: 
MAJOR(dev_t dev)
MINOR(dev_t dev)
将主设备号和次设备号转换成 dev_t 的格式: MKDEV(int major , int minor);(dev_t 类型的数据是一个32的数,前12位用来表示主设备号,后20位用来表示次设备号)

分配和释放设备编号:
#include
int register_chrdev_region (dev_t first ,unsigned int count , char *name )
first 是要分配的设备
编号的起始值,first的次设备号经常被设为0(在这里first对应的是主设备号) ,但对该函数来将并不是必需的,count 是我们所请求的连续设
备编号的个数,当count 的很大时,所请求的范围可能会下一个主设备号重叠。name 是我们的注册的设备的名字,它将出现在/proc/devices 和 sysfs 中,该函数成功返回0,失败返回一个负的错误代码。
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count ,char *name )
设备编号的动态分配,dev 是仅用于输出的参数,在成功完成调用后将保存已分配的范围的第一个编号,firstminor 是要使用的被请求的第一个次设备号,count 和 name 和上面的含义一样。
释放设备编号:
void unregister_chrdev_region(dev_t first , unsigned int count)
不管用哪种方法注册的设备号,最终都应用这个函数释放。
假如我们的设备驱动会广泛的被使用,我们就应该使用动态分配设备号的方法。我们注册的设备号可以在 /proc/devices  中看到。
在动态分配主设备号的情况下,要加载这类驱动程序的模块的脚本,可以使用awk 这类的工具从/proc/devices 中获取信息,并在/dev 目录中创建设备文件。

一些重要的数据结构
文件操作:
为我们保留的设备编号链接相应的设备驱动程序。file_operations 结构就是来实现这种来链接的。
#include
file_operations 结构里面包含一族函数指针,每个打开的文件都合一族函数关联。(通过包含执向一个file_operations 结构的f_ops 字段)。
按照惯例,file_operations 结构或只想这类结构的指针叫做 fops 。这个结构的每个字段都指向驱动程序中实现特定操作的函数,对于不支持的函数应该将此字段置为NULL。
file_operations 的 具体结构:
struct nodule *owner 
loff_t (*llseek) (struct file * , loff_t , int);
ssize_t ( *read) (struct file * ,char __user *,size_t,loff_t *);
ssize_t (*aio_read) (struct kiocb*,char __user * , size_t , loff_t );
ssize_t (*write) (struct file * , const char __user *,size_t, loff_t*);
ssize_t (*ato_write) (struct kiocb *,const char __user *,size_t , loff_t *);
int (*readdir) (struct file * , void *,filldir_t);
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 *);
int (*open) (struct inode *,struct file *);
int (*flush) (struct file *);
int (*release) (struct inode * , struct file *);
int (*fsync) (struct file * , struct dentry *, int);
int (*aio_fsync) (struct kiocb * , int ) ; 
int (*lock) (struct file * , in t, struct file_block *);
ssize_t (*readv) (struct file * , const struct iovec *,unsigned long ,loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *,unsigned long , loff_t *);
ssize_t (*sendfile) (struct file * , loff_t *, size_t , read_actor_t, void * ) ;
ssize_t (*sendpage) (struct file * , struct page * , int , size_t, loff_t, int);
unsigned long (*get_unmapped_area) (struct file * , unsigned long,unsigned long , unsigned long , unsigned long );
int (*check_flags) (int)
int (*dir_notify) (struct file *,unsigned long);

scull 设备驱动所实现的是最重要的设备方法,它的file_operations 结果会被初始化为:
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 结构 和 用户空间里的 FILE 结构没有任何关联,struct file 是内核结构,不会出现在用户程序里。
file 结构代表一个打开的文件,每一个打开的文件在内核空间里都有一个对应的file结构。在内核open时创建,在close时 由内核释放这个结构。
file 结构的主要成员:
mode_t f_mode : 文件模式。它通过FMODE_READ,FMODE_WRITE位来标示文件是否可读或可写。
loff_t f_pos  : 文件当前的读写位置,是一个64位的数(long long)。
unsigned int f_flags : 文件标志。如 O_RDNOLY , O_NONBLOCK,O_SYNC,检查用户请求的操作是否为非阻塞的操作。这些标志定义在
中。
struct file_operations *f_ops : 与文件相关的操作。
void *private_data :  是夸系统调用时保存状态信息的非常有用的资源,驱动程序可以用这个字段指向已分陪的数据,但是一定要在内核销毁file 结构前在release 方法中释放内存。
strut dentry *f_dentry : 文件对应的目录项结构。除了用filp->f_dentry->d_inode 的方式来访问索引节点节后之外,设备驱动程序的开发者们无须关心dentry 结构。
实际的结构中还有其他的一些成员,但他们对驱动程序没有太大的用处,驱动程序从不自己填写file结构,所以忽略他们是安全的。

inode 结构:
内核用inode 结构来表示内部文件。它和file 结构不同,后者表示打开的文件描述符。对于单个文件,可能会有多个文件描述符与其对应,但都指向一个inode 结构。
对驱动程序有用的inode 结构:
dev_t i_rdev : 表示设备文件的inode 结构,该字段包含的真正的设备编号;
struct cdev *i_cdev : struct cdev 表示了字符设备的内核的内部结构。当inode 指向一个字符设备文件时,该字段包含了指向 struct cdev 结构的指针。
内核中定义的宏:
unsigned int iminor ( struct inode *inode)
unsigned int imajor ( struct inode *inode)
从一个inode 结构中获得主设备号和次设备号。

字符设备的注册:
内核内部使用 struct cdev 来表示字符设备,在内核调用设备操作之前必须分配并注册一个或多个上述结构。
定义了了这个结构和与这个结构相关的一些辅助函数。
创建并初始化 struct cdev 结构:
第一种:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
第二中(只是初始化):
void cdev_init ( struct cdev *dev , struct file_operations *fops);
struct cdev 结构中还有一个所有者字段,我们应该将其初始化为: THIS_MODULE;

int cdev_add ( struct cdev *dev , dev_t num , unsigned int count );
通过调用此结构来告诉内核该结构的信息。num 该设备对应的第一个设备编号。count 是 应该和该设备关联的设备编号的数量。count 经长置1。但是有时候一个字符设备会对应多个设备编号。
在使用 cdev_add 函数时应牢记,这个调用可能会失败,返回一个负的错误码,给设备不会被添加到系统中。只要cdev_add 返回了,我们的设备就活了,它的操作就可以被内核调用,因此,在驱动程序还没有完全准备号醋栗设备上的操作时,就不能调用cdev_add。

Scull 中的设备注册:
在scull内部,使用 struct scull_dev 来表示每个设备,该结构定义如下:
struct scull_dev{
struct scull_qset *data; /*指向第一个量子级集的指针*/
int quantum;  /*当前量子的大小*/
int qset; /*当前数组的大小*/
unsigned long size; /*保存在其中的数据总量*/
unsigned int access_key; /*由 sculluid 和 scullpriv 是有*/
struct semphore sem; /*互斥信号量*/
struct cdev cdev; /*字符设备结构*/
};
struct cdev 是设备和内核直接的接口,该结构通过下面的代码将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);
if(err){
printk(KERN_NORICE "Error %d adding scull %d",err,index);
}
}

早期的办法:
注册一个字符设备驱动程序的经典方式是:
int register_chrdev(unsigned int major,const char *name,struct file_operations *fops);
major 是主设备号,name是设备的名字(将会出现在/proc/devices中),fops是指向file_operations 的指针。
使用register_chrdev 将为给定的主设备号注册0-255作为次设备号,并为每个设备建立一个cdev 结构,使用这一接口的驱动程序必须能够处理所有的256个次设备号的open调用。而且也不能使用大于256的主设备号和次设备号。
与 register_chrdev 相对应的从系统中一处设备的操作是:
int unregister_chrdev(unsigned int major , const char *name);

Open 方法:
open方法提供给设备以初始化的能力,从而为以后的操作完成初始化的准备。
open 在大多数设备中应完成的操作为:
检查设备特定的错误(诸如设备未就绪或类似的硬件问题);
如果设备是首次打开,则对其进行初始化;
如果必要,更新f_ops指针;
分配并填写置于filp->private_data 里的数据结构。
open 的原型: int (*open)(struct inode *inode ,struct file *filp);
其中inode 参数在i_cdev 字段包含我们的所需要的信息,即我们先前设置的cdev 结构。

container_of(pinter,container_type,container_field);
内核提供给我们的宏,帮助我们找到包含container_field结构的结构,返回一个目结构的指针。(这个宏需要一个container_field 字段的指针,该字段包含在container_type 类型的结构中,然后返回包含该字段的结构指针。在scull_open 中,这个宏用来找到适当的设备结构:
struct scull_dev *dev 
dev  = container_of(inode->i_dev,,struct scull_dev,cdev);
file->privater_data = dev;
另一个确定要打开的设备的方法是: 检查保存在inode 结构中的次设备号。如果你用宏 register_chrdev 注册自己的设备,则必须使用这一技术。请一定用iminor 宏从inode 结构中过的次设备号,并确定它对应于驱动程序真正的准备打开设备。
经过简化的scull_open 代码如下:
int scull_open(struct inode *inode , struct file *filp)
{
struct scull_dev *dev;
dev = container_of (inode->i_cdev,struct scull_dev,cdev);
filp->privater_data = dev;
if((filp->f_flags & O_ACCMODE) ==  O_WRNOLY){
scull_trim(dev);
}
return 0;
}

release 方法:
release 方法对应于 open 方法来说的,这个设备方法应该完成以下的工作:
释放由open分配的,保存在 filp->private_data 中的内容。
在最后一次关闭操作时关闭设备。
scull 的基本模型没有需要关闭的硬件,因此需要的代码量最少(因为scull_open为每个设备都替换了不同的filp->f_ops , 所以不同的设备由不同的函数关闭)
int scull_release(struct inode *inode , struct file *filp)
{
return 0;
}
并不是每一个close 操作都会引起对 release 方法的调用,只有真正的释放设备数据结构的close调用才会调用这个方法。内核对每个file 结构都维护着一个其被使用的计数器。只有在file结构的计数归0时,close 系统调用才会执行release 方法。每一个open操作对只对应于一个release 操作。

scull 内存的使用:
scull 驱动程序引入了linux 内核中两个非常重要的函数:
#include
void *kmalloc(size_t size,int flags);
void kfree (void *ptr);
kmalloc 函数将试图分配size个字节大小的内存空间,其返回指向该空间的指针,分配失败时返回NULL。flags 描述内存的分配方法(在在这里我们只使用 GFP_KERNEL)。由这个函数分配的内存应由kfree来释放,将NULL传递给kfree 是合法的。

在scull_dev结构中,每个设备都是一个指针链表,其中每一个指针都指向一个scull_qset 结构,struct_dev 中的成员 data(一个指针) 指向scull_qset 链表的第一个成员,每一个struct_qset 的 data(双重指针) 成员指向一个数组(在发布的源码里长度是1000),每一个数组成员指向一个4K的存储空间,称为一个量子。struct_qset 本身代表一个量子集。而 多个struct_qset 结构又构成了scull_dev 中的以 data 开始的链表。
scull设备驱动不应为对对量子和量子级的尺寸做强制性规定,在scull 中可以使用几种方式来改变这个值。在编译时修改scull.h中的宏 SCULL_QUANTUM 或 SCULL_QSET , 或在模块加载时设置 scull_quantun 和 scull_qset 的整数值;或者在运行时使用 ioctl 修改当前值以及默认值。
struct scull_qset{
void **data;
struct scull_qset *next;
}

 下面代码显示如何让用struct scull_cdev 和 scull_qset 来保存数据;strull_trim 函数负责整个数据区。
int scull_trim(strull strull_dev *dev)
{
strull scull_qset *next, *dptr;
int qset = dev->qset;
int i ;
for(dptr = dev->data;dptr;dptr = next){
if(dptr->data){
for(i=0 ;i
kfree(dptr->data[i]);
}
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->sise = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}

read 和 write 方法:
read write 一个是 拷贝数据到用户空间,一个是从用户空间拷贝数据。
ssize_t read (struct filr *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);
内核代码不直接引用用户空间里的内容。(原因是多样的)
文件中定义了一些用来内核访问用户空间的函数。
scull 的 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 coung);
访问任何用户空间的数据都必须是可重入的,,并且必须能和其他驱动程序函数并发的执行,并且能够处于合法休眠的状态。
这两个函数还将检查用户空间的指针是否有效。如果指针无效,就不会进行数据拷贝,如果在拷贝过程中遇到无效地址,则仅仅会复制部分呢内容。在这中情况下,返回值是还要拷贝的内存数量值。
如果不需要检查用户空间指针,那么建议读者转用 __copy_to_user 和 __copy_from_user 。

pread 和 pwrite 系统调用:
read  和 write 方法的调用都会更新 *ffop 所表示的文件位置。而perad 和 pwrite 从一个给定的文件偏移量开始操作,并且不会修该文件位置。他们会传入一个指针,该指针指向用户提供的位置,而会丢弃驱动程序所做的任何修改。
read 和 write 方法都返回一个负值,大于等于0的返回值告诉调用程序成功传送了多少字节,如果正确传输过程中发生错误,返回值将是正确传输的字节数,但这个错误只能在下一次调用中才会得到报告。尽管内核通过返回负值来表示错误,但运行在用户空间的程序看到的始终是作为返回值的-1,为了找出出错的原因,用户空间的程序要通过访问errno 标量。
scull 代码利用了部分读取的规则,每一次调用嗯scull_read 时只处理一个量子级,当确实需要更过的数据时,就重新调用这个调用。
如果进程A在读设备,此时进程B以写模式打开给设备,那么次设备会被截断为长度0,此时进程A发现自己超出了文件尾,并且在下次调用read 时返回0。
下面是read的代码:
ssize_t scull_read(struct file *filp,chra __user *buf,sizt_t count,loff_t *f_ops)
{
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 ;
size_t retval  = 0;
if(down_interruptible(&dev->sem)){
return -ERESTARTSYS;
}
if(*f_pos >= itemsize) //读写位置已经超过文件尾,读 0 字节数据,返回 retval (0)
goto out;
if(*f_pos + count > dev->size) //加入要读数据将要超过文件尾,那么可以的数据为 dev-size - *f_ops
count = dev-size - *f_ops;
/*在量子集中寻找链表项,qset 索引以及便宜量*/
item = *f_pos / itemsize ; 
rest = *f_pos % itemsize ; 
s_ops = rest / quantum ; 
q_ops = rest % quantum ; 
/*沿链表前行,知道正确的位置*/
dptr = scull_fllow(dev , item ) ;
if(dptr == NULL || !(dptr -> data ) || !dptr->data[s_pos])
goto out ; 
/*读取该量子数据知道结尾*/
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 方法: 
write 方法和 read 方法类似,遵循的规则和read 十分相似,同时 write 也是每次只处理一个量子
wtire 的代码:
scull_write(struct file *flip , 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 ; 
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
/*在量子中寻找链表项 , qset , 索引 以及偏移量*/
item = (long)*f_pos / itemsize ; 
qset = (long)*f_pos % itemsize ; 
s_pos = qset / quantum ; 
q_pos = qset % quantum ; 
/*沿着该链表前行,找到正确的些位置*/
dptr = scull_fllow( dev , item ) ; 
if(dptr == NULL)
goto out ; 
if(!(dptr->data))
dptr->data = kmalloc( qset * sizeof(char *),  GFP_KERNEL) ;//申请空间存放指向量子的指针
if(!(dptr->data[s_pos])){
dptr->data[s_pos] = kmalloc(quantum , GFP_KERNEL);
if(!dptr->data[s_pos]);
goto out ;
}
/*将数据写入该量子,直到结尾*/
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 ; 
if(dev->size < *f_pos)
dev->size = *f_pos;
out :
 up(&dev->sem);
 return retval;

readv 和 writev :
这些“向量”型的函数具有具有一个结构数组,每个结构包含一个指向缓冲区的指针和一个长度值。readv 调用可用于将指定数量的数据一次读入每个缓冲区。writev 则是把各个缓冲区的数据收集起来在一次写入的操作中进行输出。
如果驱动程序没有实现 readv  和 writev  ,readv 和 writev 会通过对 read 和write 的调用方法的多次调用来实现。
向量操作的函数的原型:
ssize_t (*readv) (struct file *filp,const struct iovec *iov,unsigned long count , loff_t *ppos);
ssize_t (*writev) (struct file *filp,const struct iovec *iov,unsigned long count , loff_t *ppos);
iovec 在 中定义。
struct iovec {
void __user *iov_base;
__kernel_size_t iov_len;
}
每个iovec 结构都描述了一个用于传输的数据块, 数据块的起始位置为 iov_base (用户空间)长度为 iov_len 个字节。函数原型中的 count 表示要操作多少个 iovec 结构。

试试新设备: 
当我们将所有的工作都进行完了之后我们就可以测试我们的驱动程序了,我们可以用cp dd 或者输入/输出重定向命令来测试这个驱动程序。
阅读(911) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~