ldd3学习之三: 字符驱动
2017-03-29 10:49
183 查看
通过介绍字符设备scull(Simple Character Utility for Loading Localities,区域装载的简单字符工具)的驱动程序编写,来学习Linux设备驱动的基本知识。scull可以为真正的设备驱动程序提供样板。
1.scull设备
编写驱动程序的第一步,就是定义驱动程序为用户程序提供的能力(机制)。ldd3里用的设备是内存的一部分,可以做任何想做的事情。
scull0-scull3:
这四个设备分别由一个全局且持久的内存区域组成。“全局”是指:设备多次被打开,打开它的所有文件描述符可共享该设备的数据。“持久”是指:若设备关闭后再打开,数据不会丢失。可以使用常用的命令来访问和测试这个设备,如cp,cat以及shell的I/O重定向等。
scullpipe0-scullpipe3
这四个FIFO设备与管道类似。一个进程读取另一个进程写入的数据,如果有多个进程读取一个设备,就会为数据发生竞争。scullpipe的内部实现将说明在不借助中断的情况下如何实现阻塞式和非阻塞式读/写操作。也可以采用硬件中断与他们的设备保持同步(多采用)。
scullsingle
scullpriv
sculluid
scullwuid
这些设备与scull0相似,但在何时允许open操作方面有一些限制。
scullsingle一次只允许一个进程使用该驱动程序
scullpriv对每个虚拟空间(或X终端会话)是私有的,因为每个控制台的进程获取的内存区不同。
sculluid/scullwuid可以多次被打开,但每次只能有一个用户打开,如果一个用户锁定了该设备,sculluid将返回“Device Busy”的错误。
每个scull设备都展示了驱动程序不同的功能,也提出了不同的难点。
2.主次设备号
主设备号表示设备对应的驱动程序;次设备号由内核使用,用来确定设备文件所指的设备。除了知道次设备号用来指向驱动程序所使用的设备之外,内核本身基本不关心次设备号的其他信息。
内核用dev_t类型()来保存设备编号,在V2.6中,dev_t是一个32位的数,12位表示主设备号,20位表示次设备号。
在实际使用中,是通过中定义的宏来转换格式。
建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。
分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
以下是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 f_op中的操作
spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
//可用于任何目的或者忽略这个字段。驱动程序可以用这个字段指向已分配的数据,但一定要在内核销毁file结构之前在release方法中释放内存。
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
③inode结构
内核用inode结构在内部表示文件,与file(表示打开的文件描述符)不同,单个文件对应单个的inode结构。重要的字段:
dev_t i_rdev;
对表示设备的inode结构,该字段包含了真正的设备编号
struct cdev * i_cdev;
struct cdev是表示字符设备的内核的内部结构,当inode指向一个字符设备文件时,它包含了指向struct cdev结构的指针。
两个从inode获得设备号的宏:
unsigned int iminor(struct inode * inode);
unsigned int imajor(struct inode * inode);
4.字符设备注册
内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。代码应包含,它定义了struct cdev以及与其相关的一些辅助函数。
注册一个字符设备基本步骤如下:
①为struct cdev 分配空间(如果已经将struct cdev 嵌入到自己的设备的特定结构体中,并分配了空间,这步略过!)
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
②初始化struct cdevvoid cdev_init(struct cdev *cdev, const struct file_operations *fops)
③初始化cdev.owner
cdev.owner = THIS_MODULE;
④cdev设置完成,通知内核struct cdev的信息(在驱动程序还没完全准备好处理设备上的操作时,就不能调用cdev_add)
int cdev_add(struct cdev *cdev, dev_t num, unsigned count)
⑤从系统中移除一个字符设备:
void cdev_del(struct cdev *dev)
将cdev结构传递到cdev_del函数之后,就不应再访问cdev结构了。
5.scull的内存模型
scull的结构(可以用来表示每个设备)如下:
struct scull_dev{
struct scull_qset * data; //指向第一个量子集的指针
int quantum; //量子的大小(每个指针分配的内存)
int qset; //当前量子集的大小(指针数组)
unsigned long size; //保存在其中的数据总量
unsigned int access_key; //由sculluid和scullpriv使用
struct semaphore sem; //互斥信号量
struct cdev cdev; //字符设备结构
};
实际的数据结构:
struct scull_qset{
void **data;
struct scull_qset *next;
}
scull的初始化代码:(已经分配了scull_dev空间)
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);
}
6.scull的设备驱动方法
①open方法
提供给驱动程序以初始化的能力,为以后的操作完成初始化做准备,在大部分驱动程序中,open完成如下工作:
.检查设备特定的错误(如设备未就绪或类似的硬件问题)。
.如果设备是首次打开,这对其初始化
.如有必要,更新f_op指针
.分配并填写置于filp->private_data里的数据结构
原型:
int (*open) (struct inode *inode, struct file *filp);
inode 的i_cdev字段包含我们需要的cdev结构,但是通常我们需要得到的不是cdev,而是包含cdev的scull_dev结构。用如下宏实现。
#include
container_of(pointer,container_type,container_field);
经过稍微简化的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->private_data = dev;
if ((filp->f_flags & O_ACCMODE) == O_WRONLY){
scull_trim(dev);//当以写方式打开时,把设备长度截为0
}
return 0;
}
scull_trim在以写入方式scull_open和scull_cleanup模块中调用,释放dev数据区
int scull_trim(struct scull_dev *dev)
{
struct scull_qset *next,*dptr;
int qset = dev->qset;
int i;
for (dptr = dev->data;dptr;dptr = next){
if (dptr->data){
for(i = 0; i data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}
一个在设备链表中得到正确scull_dev的函数,在read和write中调用
/*Follow the list*/
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
struct scull_qset *qs = dev->data;
/* Allocate first qset explicitly if need be */
if (! qs) {
qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs == NULL)
return NULL;
memset(qs, 0, sizeof(struct scull_qset));
}
/* Then follow the list */
while (n--) {
if (!qs->next) {
qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs->next == NULL)
return NULL;
memset(qs->next, 0, sizeof(struct scull_qset));
}
qs = qs->next;
continue;
}
return qs;
}
函数功能:若所找的scull_qset存在,则返回其指针,若不存在,一边为链表分配空间,一边沿链表前行,直到所需的节点被分配到,并被初始化及返回。
引入两个函数:
#include
void *kmalloc(size_t size,int flags);
void kfree(void *ptr);
②release方法
完成以下任务:
.释放由open分配的,保存在filp->private_data中的所有内容。
.在最后一次关闭操作时关闭设备。
只有当file结构的计数归0时,close系统调用才会执行release发放。每次close都会调用flush。这保证了每次open驱动那个程序只会弹道对应的一个release调用。
③read和write
内核空间与用户空间的数据不能直接传输,原因如下:
.在内核模式运行中,用户空间指针是无效的,该地址可能根本无法被映射到内核空间,或指向某未知区域。
.用户空间的内存是分页的,而在系统调用被调用时,涉及到的内存可能根本不在RAM中。直接对用户内存操作将导致页错误,其结果可能是一个oops。
.用户提供的程序可能有缺陷或是恶意程序,威胁系统稳定和安全。
so,永远不要直接引用用户空间的指针。
#include
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);
ssize_t read(struct file *filp,char __user *buff,size_t count,loff_t *offp);
ssize_t write(struct file * filp,const char __user *buff,size_t count,loff_t *offp);
访问用户空间的任何函数必须是可重入的,并且必须可和其他驱动程序并发执行
read返回值:
.==count,说明说请求直接传输成功。
.0
1.scull设备
编写驱动程序的第一步,就是定义驱动程序为用户程序提供的能力(机制)。ldd3里用的设备是内存的一部分,可以做任何想做的事情。
scull0-scull3:
这四个设备分别由一个全局且持久的内存区域组成。“全局”是指:设备多次被打开,打开它的所有文件描述符可共享该设备的数据。“持久”是指:若设备关闭后再打开,数据不会丢失。可以使用常用的命令来访问和测试这个设备,如cp,cat以及shell的I/O重定向等。
scullpipe0-scullpipe3
这四个FIFO设备与管道类似。一个进程读取另一个进程写入的数据,如果有多个进程读取一个设备,就会为数据发生竞争。scullpipe的内部实现将说明在不借助中断的情况下如何实现阻塞式和非阻塞式读/写操作。也可以采用硬件中断与他们的设备保持同步(多采用)。
scullsingle
scullpriv
sculluid
scullwuid
这些设备与scull0相似,但在何时允许open操作方面有一些限制。
scullsingle一次只允许一个进程使用该驱动程序
scullpriv对每个虚拟空间(或X终端会话)是私有的,因为每个控制台的进程获取的内存区不同。
sculluid/scullwuid可以多次被打开,但每次只能有一个用户打开,如果一个用户锁定了该设备,sculluid将返回“Device Busy”的错误。
每个scull设备都展示了驱动程序不同的功能,也提出了不同的难点。
2.主次设备号
主设备号表示设备对应的驱动程序;次设备号由内核使用,用来确定设备文件所指的设备。除了知道次设备号用来指向驱动程序所使用的设备之外,内核本身基本不关心次设备号的其他信息。
内核用dev_t类型()来保存设备编号,在V2.6中,dev_t是一个32位的数,12位表示主设备号,20位表示次设备号。
在实际使用中,是通过中定义的宏来转换格式。
(dev_t)-->主、次设备号 | MAJOR(dev_t dev) MINOR(dev_t dev) |
主、次设备号-->(dev_t) | MKDEV(int major,int minor) |
#include //指定设备编号 int register_chrdev_region(dev_t first, unsigned int count,char *name); 成功时返回0,错误是返回错误码。 first的次设备好常被置为0,但不是必需的。count是所请求的连续设备编号的个数,name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中 //动态生成设备编号 int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count, char *name); dev仅用于输出参数,调用成功后将保存已分配范围的第一个编号, firstminor被请求的第一个次设备号,通常是0.count,name同register_chrdev_region。 void unregister_chrdev_region(dev_t first, unsigned int count); //释放设备编号 在模块清除函数中调用unregister_chrdev_region函数。 |
以下是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 f_op中的操作
spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
//可用于任何目的或者忽略这个字段。驱动程序可以用这个字段指向已分配的数据,但一定要在内核销毁file结构之前在release方法中释放内存。
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
③inode结构
内核用inode结构在内部表示文件,与file(表示打开的文件描述符)不同,单个文件对应单个的inode结构。重要的字段:
dev_t i_rdev;
对表示设备的inode结构,该字段包含了真正的设备编号
struct cdev * i_cdev;
struct cdev是表示字符设备的内核的内部结构,当inode指向一个字符设备文件时,它包含了指向struct cdev结构的指针。
两个从inode获得设备号的宏:
unsigned int iminor(struct inode * inode);
unsigned int imajor(struct inode * inode);
4.字符设备注册
内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。代码应包含,它定义了struct cdev以及与其相关的一些辅助函数。
注册一个字符设备基本步骤如下:
①为struct cdev 分配空间(如果已经将struct cdev 嵌入到自己的设备的特定结构体中,并分配了空间,这步略过!)
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
②初始化struct cdevvoid cdev_init(struct cdev *cdev, const struct file_operations *fops)
③初始化cdev.owner
cdev.owner = THIS_MODULE;
④cdev设置完成,通知内核struct cdev的信息(在驱动程序还没完全准备好处理设备上的操作时,就不能调用cdev_add)
int cdev_add(struct cdev *cdev, dev_t num, unsigned count)
⑤从系统中移除一个字符设备:
void cdev_del(struct cdev *dev)
将cdev结构传递到cdev_del函数之后,就不应再访问cdev结构了。
5.scull的内存模型
scull的结构(可以用来表示每个设备)如下:
struct scull_dev{
struct scull_qset * data; //指向第一个量子集的指针
int quantum; //量子的大小(每个指针分配的内存)
int qset; //当前量子集的大小(指针数组)
unsigned long size; //保存在其中的数据总量
unsigned int access_key; //由sculluid和scullpriv使用
struct semaphore sem; //互斥信号量
struct cdev cdev; //字符设备结构
};
实际的数据结构:
struct scull_qset{
void **data;
struct scull_qset *next;
}
scull的初始化代码:(已经分配了scull_dev空间)
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);
}
6.scull的设备驱动方法
①open方法
提供给驱动程序以初始化的能力,为以后的操作完成初始化做准备,在大部分驱动程序中,open完成如下工作:
.检查设备特定的错误(如设备未就绪或类似的硬件问题)。
.如果设备是首次打开,这对其初始化
.如有必要,更新f_op指针
.分配并填写置于filp->private_data里的数据结构
原型:
int (*open) (struct inode *inode, struct file *filp);
inode 的i_cdev字段包含我们需要的cdev结构,但是通常我们需要得到的不是cdev,而是包含cdev的scull_dev结构。用如下宏实现。
#include
container_of(pointer,container_type,container_field);
经过稍微简化的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->private_data = dev;
if ((filp->f_flags & O_ACCMODE) == O_WRONLY){
scull_trim(dev);//当以写方式打开时,把设备长度截为0
}
return 0;
}
scull_trim在以写入方式scull_open和scull_cleanup模块中调用,释放dev数据区
int scull_trim(struct scull_dev *dev)
{
struct scull_qset *next,*dptr;
int qset = dev->qset;
int i;
for (dptr = dev->data;dptr;dptr = next){
if (dptr->data){
for(i = 0; i data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}
一个在设备链表中得到正确scull_dev的函数,在read和write中调用
/*Follow the list*/
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
struct scull_qset *qs = dev->data;
/* Allocate first qset explicitly if need be */
if (! qs) {
qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs == NULL)
return NULL;
memset(qs, 0, sizeof(struct scull_qset));
}
/* Then follow the list */
while (n--) {
if (!qs->next) {
qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs->next == NULL)
return NULL;
memset(qs->next, 0, sizeof(struct scull_qset));
}
qs = qs->next;
continue;
}
return qs;
}
函数功能:若所找的scull_qset存在,则返回其指针,若不存在,一边为链表分配空间,一边沿链表前行,直到所需的节点被分配到,并被初始化及返回。
引入两个函数:
#include
void *kmalloc(size_t size,int flags);
void kfree(void *ptr);
②release方法
完成以下任务:
.释放由open分配的,保存在filp->private_data中的所有内容。
.在最后一次关闭操作时关闭设备。
只有当file结构的计数归0时,close系统调用才会执行release发放。每次close都会调用flush。这保证了每次open驱动那个程序只会弹道对应的一个release调用。
③read和write
内核空间与用户空间的数据不能直接传输,原因如下:
.在内核模式运行中,用户空间指针是无效的,该地址可能根本无法被映射到内核空间,或指向某未知区域。
.用户空间的内存是分页的,而在系统调用被调用时,涉及到的内存可能根本不在RAM中。直接对用户内存操作将导致页错误,其结果可能是一个oops。
.用户提供的程序可能有缺陷或是恶意程序,威胁系统稳定和安全。
so,永远不要直接引用用户空间的指针。
#include
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);
ssize_t read(struct file *filp,char __user *buff,size_t count,loff_t *offp);
ssize_t write(struct file * filp,const char __user *buff,size_t count,loff_t *offp);
访问用户空间的任何函数必须是可重入的,并且必须可和其他驱动程序并发执行
read返回值:
.==count,说明说请求直接传输成功。
.0
相关文章推荐
- 学习Ldd3--字符设备驱动(第三章)
- 字符设备驱动相关函数及数据结构简介 (ldd3)
- Linux字符设备驱动学习
- LDD3驱动学习笔记0------写在前面的话
- 简单字符设备驱动代码+注释(ldd3第三章例子)
- 基于mini6410的linux驱动学习总结(五 字符设备驱动程序实例分析(虚拟设备驱动))
- LDD3 笔记: 第3章 字符设备的驱动
- 国嵌--linux字符设备驱动学习之memdev设备
- LDD3 读书笔记 之 第 3 章 字符驱动
- LDD3笔记:第三章 字符设备驱动
- Linux设备驱动程式学习(5)-高级字符驱动程式操作[(2)阻塞型I/O和休眠]
- 字符设备驱动的学习总结
- tony之linux driver_LDD3_scull字符设备驱动编译在新内核编译问题
- linux设备驱动学习(6) 高级字符驱动学习--阻塞型I/0
- LDD3 读书笔记 之 第 3 章 字符驱动
- 字符设备驱动学习笔记(2.6.23)
- 字符设备驱动相关函数及数据结构简介 (ldd3)
- linux驱动学习(四) linux字符设备驱动 cdev
- 基于mini6410的linux驱动学习总结(二 字符设备与块设备的区别)
- Linux设备驱动程式学习(6)-高级字符驱动程式操作[(3)设备文档的访问控制]