在前文中,我们按照一般内核模块的结构分析了globalmem_init函数和globalmem_exit函数。通过上述两个函数可以完成字符驱动的加载和卸载。那么本文将进一步分析字符设备驱动的实现。
linux2.6内核中使用cdev结构体来表述一个字符设备驱动,但是一般我们并不直接使用cdev结构体,而是将与该设备相关的信息与cdev街头体结合爱一起,定义一个新的结构体,比如
struct globalmem_dev { struct cdev cdev; unsigned char mem[GLOBALMEM_SIZE]; }; struct cdev { struct kobject kobj;//内嵌kobject对象 struct module *owner;//指向实现驱动程序的模块的指针,通常为THIS_MODULE const struct file_operations *ops;//指向此设备驱动程序文件操作结构体的指针 struct list_head list;//指向字符设备文件对应的索引节点链表的头 dev_t dev;//设备号 unsigned int count;//给该设备驱动程序分配的设备号范围的大小 };
就像前文所说的,设备号都是分配一个范围(count的大小),因此可能有很多个设备文件主设备号相同并且对应于同一个设备驱动。list所指向的链表就是由当前该设备驱动对应的设备文件索引节点组成。
我们现在回到globalmem_setup_cdev函数,它的主要作用就是申请并初始化一个cdev结构体,并且将通过cdev_add函数向系统内添加一个cdev,完成字符设备的注册。通常我们将cdev_add函数安排在字符设备驱动模块的加载函数中,而对应的将cdev_del函数放在字符设备驱动的卸载函数中。
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) { int err, devno = MKDEV(globalmem_major, index); cdev_init(&dev->cdev, &globalmem_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &globalmem_fops; err = cdev_add(&dev->cdev, devno, 1); if (err) printk(KERN_NOTICE "Error %d adding cdev%d", err, index); }
除此之外,globalmem_setup_cdev函数还会将cdev结构体中的struct file_operation类型的指针ops实例化。globalmem_fops全局变量是文件操作表,这个结构中含有许多文件操作函数类型的指针。当我们实现某些文件操作函数时,就可以将这些函数名赋值给这个结构中的相应变量。比如我们在稍候会实现globalmem_open函数,将其赋值给globalmem.open,那么当用户使用open系统调用对字符设备文件进行打开操作时,内核就会自动调用适合该设备文件的打开函数,也就是globalmem_open函数。
正如你所知的那样,Linux下一些皆为文件,当然设备也不例外。对于一个设备文件来说,用户通过VFS可以使用统一的系统调用接口对各种设备(文件)进行相关操作,比如open,read,write等等,用户可以不去考虑当前设备具体如何去操作。而在VFS层下——位于操作系统中的设备驱动就会对于每种设备去实现相应的操作函数。对于每类设备所实现的操作如何在用户层统一的表现出来,这就需要struct file_operations结构体。此结构体中包含大量的函数指针,这些函数指针便是用户层上统一的系统调用函数名,将设备驱动中实现的具体操作函数赋值给这些函数指针后,用户就可以使用统一的系统调用函数了。
接下来我们来看具体的文件操作函数是如何实现的。
文件打开函数将设备结构体指针赋值给私有数据,这个私有数据会在稍候的read以及write中被用到,而不是直接的使用globalmem_devp。
/*文件打开函数*/ int globalmem_open(struct inode *inode, struct file *filp) { /*将设备结构体指针赋值给文件私有数据指针*/ filp->private_data = globalmem_devp; return 0; }
在读函数中,首先将私有数据赋值给一个设备结构体指针。然后,判断要读的长度是否合法。接着利用copy_to_user函数内核空间的数据(dev->mem)拷贝到用户空间。关于这个copy_to_user函数的详细拷贝过程,我们也可以对其进行代码分析。如果拷贝成功,那么修改相应的指针即可完毕读操作。
/*读函数*/ static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) { unsigned long p = *ppos; unsigned int count = size; int ret = 0; struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/ /*分析获取有效的写长度*/ if (p >= GLOBALMEM_SIZE) return count ? - ENXIO: 0; if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p; /*从内核空间向用户空间写数据*/ if (copy_to_user(buf, (void*)(dev->mem + p), count)) { ret = - EFAULT; } else { *ppos += count; ret = count; printk(KERN_INFO "read %d bytes(s) from %d\n", count, p); } return ret; }
写函数与读函数的过程大体一直,不同的是使用了copy_from_user函数。这里不再详解。
接下来我们就可以使用一个简单测试程序来对我们所实现的字符设备驱动进行测试了。