通过介绍字符设备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位表示次设备号。在实际使用中,是通过中定义的宏来转换格式。
(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 < 0) {
- printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
- return result;
- }
3.几个数据结构
大部分驱动都涉及到三个重要的内核数据结构:file_operation,file,inode
①file_operation
在中定义,包含了一组函数指针,用来建立驱动程序和设备编号的链接。
- struct file_operations {
- struct module *owner;
- 指向拥有该模块的指针,一般初始化为THIS_MODULE,它是定义在<linux/module.h>中的一个宏。
- loff_t (*llseek) (struct file *, loff_t, int);
- llseek用来修改文件的当前读写位置,并将新位置作为返回值。loff_t是一个长偏移量,至少占64位宽。如果这个函数指针为NULL,对seek的调用将以某种不可预期的方式修改file结构中的计数器
- ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
- 从设备中读取数据,该函数赋为NULL时,将导致read系统调用出错并返回-EINVAL(非法参数)
- ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
- 向设备中写数据,该函数赋为NULL时,将导致write系统调用出错并返回-EINVAL(非法参数)
- ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
- ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
- int (*readdir) (struct file *, void *, filldir_t);
- unsigned int (*poll) (struct file *, struct poll_table_struct *);
- poll方法是poll,epoll和select这三个系统调用的后端实现。用来查询某个或多个文件描述符上的读取或写入是否会被阻塞。poll应该返回一个位掩码,用来指出非阻塞的读取或写入是否可能,并且也会向内核提供将调用进程置于休眠状态知道I/O变为可能时的信息,如果设为NULL,则设备会被认为可读可写,且不会被阻塞。
- int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
- ioctl提供了一种执行设备特定命令的方法,另外内核还能识别一部分ioctl命令,而不必调用fops表中的ioctl。若为NULL,ioctl系统调用将返回错误(-ENOTTY,“No such ioctl for device”)
- long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
- long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
- int (*mmap) (struct file *, struct vm_area_struct *);
- mmap用于请求将设备内存映射到进程地址空间。若为NULL,mmap系统调用将返回-ENODEV.
- int (*open) (struct inode *, struct file *);
- 若为NULL,则设备的打开操作永远成功,但系统不会通知驱动程序。
- int (*flush) (struct file *, fl_owner_t id);
- 关闭设备文件描述符副本时,执行设备上尚未完结的操作,(仅用于几个驱动程序),若为NULL,忽略用户请求。
- int (*release) (struct inode *, struct file *);
- 当file结构被释放时,调用这个操作,与open相仿,也可置为NULL.
- int (*fsync) (struct file *, struct dentry *, int datasync);
- int (*aio_fsync) (struct kiocb *, int datasync);
- int (*fasync) (int, struct file *, int);
- int (*lock) (struct file *, int, struct file_lock *);
- 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 (*flock) (struct file *, int, struct file_lock *);
- ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
- ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
- int (*setlease)(struct file *, long, struct file_lock **);
- };
scull设备驱动程序所实现的知识最重要的设备方法,它的file_operation结构初始化为:
- 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函数,在文件的所有实例都被关闭后,内核会释放这个数据结构
- struct file {
- /*
- * fu_list becomes invalid after file_free is called and queued via
- * fu_rcuhead for RCU freeing
- */
- union {
- struct list_head fu_list;
- struct rcu_head fu_rcuhead;
- } f_u;
- struct path f_path;
- #define f_dentry f_path.dentry
- #define f_vfsmnt f_path.mnt
- const struct file_operations *f_op;
- //文件的关联操作可以在任何需要的时候修改,这种技巧允许相同的open代码可以根据要打开的次设备号替换filp->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 cdev
void 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 <linux/kernel.h>
- 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 < qset; i++)
- kfree(dptr->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 <linux/slab.h>
- 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 <asm/uaccess.h>
- 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<返回值
.==0,表示到达文件尾。
.<0,表示出错,错误吗在中定义。
write返回值:
.==count,完成所请求直接传送。
.0<返回值
.==0表示什么都没写入,标准库会重复调用write。
.负值,表示出错。
无论传输了多少数据,一般而言都应该更新*offp说表示的文件位置,以便反映在新系统调用成功完成后当前的文件文字。
7.模块测试:
该模块测试代码参考TekkamanNinja的。功能是,先把buf1的20个字符写入scull,然后再读出来打印出来。
默认量子集是ldd3的值,qset = 1000,quantum=4000。
阅读(223) | 评论(0) | 转发(0) |