本文共 11566 字,大约阅读时间需要 38 分钟。
这几天开始看比较难的 linux设备驱动程序了。
看到本书作者编写的代码,(从官网上下载到的示例:
大家也可以从这里下载。
http://download.csdn.net/detail/zhaole20094463/4541209)
不禁对该书作者科波特敬仰万分。看过之后终于知道为什么这本书比较难了。
我们对书籍评价的难,无非就是两个原因,一个是写法太艰涩,不好理解。
另外一个就是信息量太大。我想这本书难的原因就在于第二点吧。
我们学习程序编写的时候,我想除了看书的时候,最重要的就是看代码。
很不幸的是,这本书的代码也不是那么容易让我们看明白的。
其实,这样不怪作者。而是因为他在编写了该书的示例代码的时候考虑了特别多
的东西,考虑的特别多的情况。比如我们在进行设备注册时候分配设备号,我们可以
在编译时制定,也可以自动获取,也可以在我们加载模块的时候通过参数传递进去。
而该书作者的代码恰恰把这几点都做到了。
所以我试着自己将该作者的代码简化一些,自己编写一个简单的scull函数。
同时加上了详细的注释,希望能给大家一些帮助。
//#include <linux/config.h>
#include<linux/module.h>
#include<linux/init.h> #include<linux/kernel.h> #include<linux/fs.h> #include<linux/types.h> #include<linux/errno.h> #include<linux/fcntl.h> #include<linux/cdev.h> #include<linux/kdev_t.h> #include<asm/system.h> #include<asm/uaccess.h> #include<linux/slab.h> #define SCULL_MAJOR 0 #define SCULL_NR_DEVS 1 MODULE_LICENSE("Dual BSD/GPL"); int scull_major = SCULL_MAJOR; int scull_minor = 0; int scull_nr_devs = SCULL_NR_DEVS; int scull_quantum=4000; int scull_qset =1000; struct scull_qset { void **data; struct scull_qset *next; }; struct scull_dev { struct scull_qset *data; /* Pointer to first quantum set */ int quantum; /* the current quantum size */ int qset; /* the current array size */ unsigned long size; /* amount of data stored here */ unsigned int access_key; /* used by sculluid and scullpriv */ struct semaphore sem; /* mutual exclusion semaphore */ struct cdev cdev; /* Char device structure*/ };//由于我们的驱动程序没有硬件所以我们就自己在电脑的内存里划出一段来作为硬件。这个就是我们声明的硬件的结构体。 struct scull_dev *scull_devices; static void scull_setup_cdev(struct scull_dev *dev,int index); static int scull_release(struct inode * inode , struct file * filp); static int scull_open(struct inode * inode,struct file *filp); //ssize_t scull_read(struct file *filp;char __user *buff,ssize_t count,loff_t *offp); ssize_t scull_read(struct file *filp,const char __user *buff,size_t count,loff_t *f_pos); struct scull_qset *scull_follow(struct scull_dev *dev,int n); ssize_t scull_write(struct file *filp,const char __user *buff,size_t count,loff_t *f_pos); int scull_trim(struct scull_dev *dev); void scull_cleanup_module(void); struct file_operations scull_fops = //定义控制函数 { .owner = THIS_MODULE, .open = scull_open, .read = scull_read, .write = scull_write, .release = scull_release, }; int scull_trim(struct scull_dev *dev)//清空链表 { struct scull_qset *next,*dptr;//定义两个结构体指针 int qset = dev->qset; //将scull_dev中qset 取出存储在 qset中 int i;for(dptr = dev->data ; dptr ; dptr = next)//从第一个链表地址开始遍历,将数据清零,判断下一个链表的地址是不是NULL,
//链表末尾的指针指向NULL,操作条件是将链表的地址依次赋给dptr。 { if(dptr -> data) //如果链表地址不为空,执行下面操作 { for(i=0;i<qset;i++) //循环, kfree(dptr->data[i]);//kfree该链表的数据所占的内存 kfree(dptr->data);//释放链表头数据所占的内存 dptr->data = NULL;//将表头的数据写为NULL } next = dptr -> next;//将下一个链表的地址赋给next kfree(dptr); //释放该地址的内存 } dev->size = 0 ;//初始化scull_dev结构 dev->quantum = scull_quantum; dev->qset = scull_qset; dev->data = NULL; return 0; } ssize_t scull_read(struct file *filp,const char __user *buff,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;//获取dev结构中初始化的数值 int itemsize = quantum *qset;//一个量子集的大小,链表个数是4000个,链表中的链表项是1000个,则一个量子集为 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;//判断方法是文件读取偏移量+读取文件的个数,如果超过了则读取文件偏移量到文件末尾的数据个数 /*在量子集中寻找链表项 qset索引以及偏移量*/ item = (long)*f_pos / itemsize; rest = (long)*f_pos % itemsize; s_pos = rest / quantum;//确定读取第多少个节点即qset索引 q_pos = rest % quantum;//确定读取该节点中的数组的第多少项 即 偏移量 /*沿链表前行,直到正确的位置(在其他地方定义)*/ dptr = scull_follow(dev,item);//初始化n个链表结构 //如果dptr dptr->data dptr->data[s_pos]任意一个值为NULL 或0 则出错 if(dptr == NULL || !dptr->data || !dptr->data[s_pos]) goto out; //如果要读取的数据个数大于,链表节点个数则读取个数削减为链表项数 if(count > quantum - q_pos) count = quantum - q_pos;//链表总项数-节点偏移量 if(copy_to_user(buff,dptr->data[s_pos]+q_pos,count)) { retval = -EFAULT; goto out; } *f_pos += count;//将文件偏移量移到读取的位置 retval = count;//返回读取的字节个数 out: up(&dev->sem); return retval; } /*初始化n个链表结构,n取决于read函数中item的值*/ struct scull_qset *scull_follow(struct scull_dev *dev,int n) { struct scull_qset *qs= dev -> data;//声明一个链表类型,将设备中的数据存储在该链表中进行操作。 if(!qs)//如果该链表地址为0,则执行以下操作 { qs = dev->data = kmalloc(sizeof(struct scull_qset),GFP_KERNEL); //为该链表分配内存空间 if(qs == NULL) return NULL; memset(qs,0,sizeof(struct scull_qset));//将分配的空间初始化为0 } while(n--)//操作n个链表结构,对n个链表结构进行初始化 { if(!qs->next)//将链表中的下一个地址传递出来,如果该链表项没有分配内存则执行以下操作。 { qs->next = kmalloc(sizeof(struct scull_qset),GFP_KERNEL);//为链表项分配内存空间 if(qs->next == NULL)//分配内存出错,返回NULL return NULL; memset(qs->next,0,sizeof(struct scull_qset));//将内存空间初始化为0 } qs = qs -> next;//将下一个地址传递出来 continue; } return qs; } 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; if(down_interruptible(&dev->sem)) return -ERESTARTSYS;item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;s_pos = rest / quantum;
q_pos = rest % quantum; dptr = scull_follow(dev,item); if(dptr == NULL) goto out; if(!dptr->data[s_pos]) { dptr->data[s_pos] = kmalloc(qset * sizeof(char *),GFP_KERNEL); if(!dptr -> data[s_pos]) goto out; memset(dptr->data,0,qset*sizeof(char *)); }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)//根据写入的数据长度,修改结构体中size的大小
dev->size = *f_pos; out: up(&dev->sem); return retval; } static int scull_open(struct inode *inode, struct file *filp) { struct scull_dev *dev;dev = container_of(inode -> i_cdev,struct scull_dev,cdev);//获得scull_dev结构的指针
filp -> private_data = dev;//将该结构指针保存在文件描述符的私有数据中,供别的函数使用(如果有需要的的话) if((filp->f_flags & O_ACCMODE)==O_WRONLY) /*filp-> f_flag文件描述符 O_ACCMODE读写文件操作时,用于取出flag的低2位,测试该文件是不是以只写的方式打开 O_RDONLY<00>:只读打开 O_WRONLY<01>:只写打开 O_RDWR<02>:读写打开*/{
scull_trim(dev); } return 0; } //内核内部使用struct cdev 结构表示字符设备,内核调用设备之前,必须分配并注册一个或者多个这个结构。 static void scull_setup_cdev(struct scull_dev *dev,int index) { int err,devno = MKDEV(scull_major,scull_minor + index);//将主次设备号转换为dev_t类型,存储在devno中 /* linux-2.6.22/include/linux/cdev.h struct cdev { struct kobject kobj; // 每个 cdev都是一个 kobject struct module *owner; //指向实现驱动的模块 const struct file_operations *ops; // 操纵这个字符设备文件的方法 struct list_head list; //与 cdev对应的字符设备文件的inode->i_devices的链表头 dev_t dev; // 起始设备编号 unsigned int count; // 设备范围号大小 }; */ /*初始化cdev结构*/ cdev_init(&dev->cdev,&scull_fops);//将自己设定的结构嵌入到cdev结构中 dev->cdev.owner = THIS_MODULE; //初始化 struct module *owner; dev->cdev.ops = &scull_fops; //初始化 const struct file_operations *ops; // 操纵这个字符设备文件的方法 err = cdev_add(&dev->cdev,devno,1);//将初始化好的的cdev告诉内核, /* err是用来存放cdev_add注册函数在向系统注册字符设备时可能出现的错误。 cdev_add函数的返回值就是kobj_map的返回值。在kobj_map()函数中会调用 kmalloc申请内存,如果申请失败就返回-ENOMEM错误码,否则完成注册,返回0。 所以cdev_add()函数的返回值可能的情况是0或-ENOMEM。 */ if(err) printk(KERN_EMERG"Error %d adding scull %d\n",err,index); } int scull_init_module(void) { int result; dev_t dev = 0; /*获取设备号,分为两种模式一种是手动设置,另外一种是自动分配*/ if(scull_major)//如果scull_major不是默认的0,而是手动设置的话,执行下面的程序,否则执行else下面的代码。 { dev = MKDEV(scull_major,scull_minor);//将scull_major 转换成dev_t中的格式,获取dev_t中的主次设备号 result = register_chrdev_region(dev,scull_nr_devs,"scull");//注册设备号,和设备名,以及设备的个数:作用:将设备号scull_major 和设备名 scull联系起来声明给内核。 } else { result = alloc_chrdev_region(&dev, 0, scull_nr_devs,"scull"); //自动分配设备号,分配的个数由第三个参数确定,另外一个作用就是将设备号scull_major 和设备名 scull联系起来声明给内核。分配的设备号中的第一个保存在第一个参数中(dev) 发生错误返回负值 scull_major = MAJOR(dev);//获取自动分配出来的主设备号 } if(result < 0) { printk(KERN_EMERG"scull: can't get major %d\n",scull_major); return result; } scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev),GFP_KERNEL);//为我们的硬件 分配大小为 scull_dev个字节的内存 if(!scull_devices)//分配不成功返错误处理 { result = -ENOMEM; goto fail; //跳出注册,并释放已经注册的资源 } memset(scull_devices,0,scull_nr_devs * sizeof(struct scull_dev)); //将申请到的内存都填充为 0。/*初始化scull_devices 数组*/
scull_devices[0].quantum = scull_quantum;// scull_devices[0].qset = scull_qset;// init_MUTEX(&scull_devices[0].sem);//初始化信号量 scull_setup_cdev(&scull_devices,0);//调用这个函数,注册设备return result;
//定义出错出口函数 fail: scull_cleanup_module(); } static int sucll_release(struct inode * inode , struct file * filp) { return 0; } void scull_cleanup_module(void) { dev_t devno = MKDEV(scull_major,scull_minor); if(scull_devices) { cdev_del(&scull_devices[0].cdev);//从系统中移除一个字符设备 } kfree(scull_devices);//释放申请的内存 unregister_chrdev_region(devno,scull_nr_devs);//卸载注册的设备号 }module_init(scull_init_module);
module_exit(scull_cleanup_module);接着根据代码我们来解释一下,设备的注册和读写。
我们要验证该程序,那么就要编译为模块,这样加载调试方便。
编译为模块的Makefile
KERNELDIR ?= /lib/modules/2.6.25-14.fc9.i686/build/
PWD := $(shell pwd) #CC=$(CROSS_COMPILE)gcc obj-m :=scull.o modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules modules_install: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install 内核代码路径就读者自行修改了。接着还要建立设备节点了。
建立设备节点时,我们需要该设备的设备名,设备号,当然设备名是事先我们自己定义的,
当然知道,但是设备号呢,如果我们是自动获取的话,直接添加一条打印信息就好
printk(KERN_EMERG"设备号result=%d\n",scull_major);/*******************************************************/
在设备初始化函数,分配设备号的代码后添加该语句即可。我打印出的设备号是 247
接着执行
mknod /dev/scull c 247 0
接下来的测试工作就由你自己进行了。
大部分的知识都在注释中写的很详细了,我也花了很多心血去查,和分析,希望对大家有所帮助吧。
我只来说说设备的注册
在2.4及以前的内核中有一个字符型设备注册的经典方法
ret = register_chrdev(Demo_MAJOR,"demo_drv",&Test_ctl_ops);
这个方法之所以经典是因为它把对我们有意义的三项东西全部都包括了起来
第一项是设备号,如果是0的话,内核就会给你自动分配,其他数字只要不被使用了,都可以
第二项是我们的设备名,这其实是对应用程序有意义的,因为这会于/dev目录下生成的设备节点连接起来,他们需要同名
第三项就是我们的file_operations了,里面定义了我们的驱动程序处理子函数
也就是上例中的下列部分:
struct file_operations scull_fops = //定义控制函数
{ .owner = THIS_MODULE, .open = scull_open, .read = scull_read, .write = scull_write, .release = scull_release, }; 可是到了现在的2.6内核,内核中添加了新的注册方式,更有效率,更好的方式。当然现在也是兼容以上的注册方式的,不过慢慢的会被新的内核所丢弃的。接着我们来说说2.6内核中的设备注册方式。
同样对于我们有意义的也是上面所说的三项:
设备名
设备号
file_operations
如果是静态分配设备号呢
dev = MKDEV(scull_major,scull_minor);//将scull_major 转换成dev_t中的格式,获取dev_t中的主次设备号
result = register_chrdev_region(dev,scull_nr_devs,"scull");//注册设备号,和设备名,以及设备的个数:作用:将设备号scull_major 和设备名 scull联系起来声明给内核。那么动态分配呢
result = alloc_chrdev_region(&dev, 0, scull_nr_devs,"scull");
//自动分配设备号,分配的个数由第三个参数确定,另外一个作用就是将设备号scull_major 和设备名 scull联系起来声明给内核。分配的设备号中的第一个保存在第一个参数中(dev) 发生错误返回负值 scull_major = MAJOR(dev);//获取自动分配出来的主设备号上面这步就完成了设备号和设备名的连接
可是内核还不知道,怎么让内核知道呢?这时候我们就要通过一个cdev结构体了
在内中设备信息都是以结构体的方式存储的,我们的字符型设备呢就存储在cdev结构体中
/*初始化cdev结构*/
cdev_init(&dev->cdev,&scull_fops);//将自己设定的结构嵌入到cdev结构中 dev->cdev.owner = THIS_MODULE; //初始化 struct module *owner; dev->cdev.ops = &scull_fops; //初始化 const struct file_operations *ops; // 操纵这个字符设备文件的方法 err = cdev_add(&dev->cdev,devno,1);//将初始化好的的cdev告诉内核, printk(KERN_EMERG"告诉内核cdev后返回的值err = %d\n",err); /*这样我们先通过第一步将自己定义的file_operations 嵌入到cdev结构体中
然后两步初始化,cdev结构体。
最后一步将设备号,和cdev结构体传递给内核。
这样我们就完成了设备的注册。
那么设备的卸载呢?将刚才注册的过程反过来,把申请的按反方向(习惯)上释放掉。
printk(KERN_EMERG"卸载开始");
dev_t devno = MKDEV(scull_major,scull_minor); printk(KERN_EMERG"获取主次设备号成功\n"); if(scull_devices) { scull_trim(scull_devices); cdev_del(&scull_devices[0].cdev);//从系统中移除一个字符设备 printk(KERN_EMERG"系统中移除设备成功\n"); } kfree(scull_devices);//释放申请的内存 printk(KERN_EMERG"释放内存成功\n"); unregister_chrdev_region(devno,scull_nr_devs);//卸载注册的设备号 printk(KERN_EMERG"释放设备成功\n"); ———————————————— 版权声明:本文为CSDN博主「星河_SR」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/zhaole20094463/article/details/7926660