Linux驱动学习----字符设备驱动(一)
写在前面的话:
上一篇,我们讲到了简单模块的编写,以及它的加载编译。大家可能会感觉这是不是太简单了,确实,它仅仅只是一个HelloWorld的模块,没有什么实际的意义。
今天,我们就来点实际的,目标就是编写一个完整的字符设备驱动程序。
首先,我们来看看怎么样的文件时字符设备驱动。它们通常位于/dev目录下,字符设备驱动程序的设备文件可通过“ls-l”命令输出的第一列中的"c"来识别。块设备也在/dev目录下,它们由字符"b"来识别。
如下:
1 1 [root@localhost dev]# ls -l 2 2 crw-rw---- 1 root tty 2, 10 1月 7 02:13 ptypa 3 3 crw-rw---- 1 root tty 2, 11 1月 7 02:13 ptypb 4 4 crw-rw---- 1 root tty 2, 12 1月 7 02:13 ptypc 5 5 crw-rw---- 1 root tty 2, 13 1月 7 02:13 ptypd 6 6 crw-rw---- 1 root tty 2, 14 1月 7 02:13 ptype 7 7 crw-rw---- 1 root tty 2, 15 1月 7 02:13 ptypf 8 8 brw-rw---- 1 root disk 1, 0 1月 7 02:13 ram0 9 9 brw-rw---- 1 root disk 1, 1 1月 7 02:13 ram1 10 10 brw-rw---- 1 root disk 1, 10 1月 7 02:13 ram10 11 11 brw-rw---- 1 root disk 1, 11 1月 7 02:13 ram11 12 12 brw-rw---- 1 root disk 1, 12 1月 7 02:13 ram12 13 13 brw-rw---- 1 root disk 1, 13 1月 7 02:13 ram13 14 14 brw-rw---- 1 root disk 1, 14 1月 7 02:13 ram14 15 15 brw-rw---- 1 root disk 1, 15 1月 7 02:13 ram15
除此之外,我们还可以看见,第4列和第5列的两个数字表示了相应设备的主设备号和次设备号。通常而言,主设备号标识设备对应的驱动程序,而次设备号由内核使用,用于正确确定设备文件所指的设备。这是在系统中显示的设备编号,那么在内核中它又是怎么样的呢?dev_t(<linux/types.h>中定义)保存设备编号----包含主设备号和次设备号。在2.6.0版本中,dev_t是一个32位整数,其中的12位用来表示主设备号,其余20位用来表示次设备号。
好了,讲了这么多,接下来,让我们来看一下整个字符设备驱动的框架该怎么去搭建。
既然设备是按照它自己的编号去识别,那么,我们首要的任务就是去获得一个或多个设备编号。这里有两个函数可以做到:
int register_chrdev_region(dev_t from, unsigned count, const char *name); dev_t from:要分配的设备编号范围的起始值,其次设备号经常设置为0。 unsigned count:所请求的连续设备编号的个数。 const char *name:和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
但是在使用这个函数时,有一个前提条件,就是你必须知道设备编号,可是我们很多时候都是不知道的,因此,我只好将这个工作交给内核了:利用下面这个函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
dev_t *dev:用于输出的参数,在成功完成调用后,将保存已分配范围的第一个编号。
unsigned baseminor:要使用的被请求的第一个次设备号,通常是0。
unsigned count和const char *name和上面的意义一样。
这里还有一个至关重要的问题,就是我们在编写程序该用哪一个函数呢?各有千秋吧!对的,那么,我们究竟应该将它们都应用到我们的程序中:
1 static int __init module_framework_init(void) 2 { 3 int result; 4 dev_t devno = 0; 5 6 if (module_framework_major) { 7 devno = MKDEV(module_framework_major, module_framework_minor); 8 result = register_chrdev_region(devno, 1, "module_framework"); 9 } else { 10 result = alloc_chrdev_region(&devno, module_framework_major, 1, "module_framework"); 11 module_framework_major = MAJOR(devno); 12 } 13 if (result < 0) 14 return result; 15 16 module_framework_devp = kmalloc(sizeof(struct module_framework_dev), GFP_KERNEL); 17 if (!module_framework_devp) { 18 result = -ENOMEM; 19 goto fail_malloc; 20 } 21 memset(module_framework_devp, 0, sizeof(struct module_framework_dev)); 22 module_framework_setup_cdev(module_framework_devp, 0); 23 24 return 0; 25 fail_malloc: 26 unregister_chrdev_region(devno, 1); 27 return result; 28 }
这是我的程序中字符设备驱动初始化函数。其中将两个初始化函数结合起来了。
正如ldd3中所写的:分配主设备号的最佳方式是,默认采用动态分配方式,同时保留在加载甚至是编译时指定主设备号的余地。
当然,不管是使用哪个函数,我们现在已经可以将设备编号成功的申请好了。
那么下面我就要来说如何去释放它,因为你不可能一直都占有着它。当你要去释放它的时候,就要使用如下函数:
void unregister_chrdev_region(dev_t from, unsigned count);
1 static void __exit module_framework_exit(void) 2 { 3 cdev_del(&module_framework_devp->cdev); 4 kfree(module_framework_devp); 5 unregister_chrdev_region(MKDEV(module_framework_major, module_framework_minor), 1); 6 }
讲完了设备编号的获得和释放,接下来我们来看看一个与字符设备驱动密切相关的数据结构----cdev。它定义在<linux/cdev.h>
1 struct cdev { 2 struct kobject kobj; 3 struct module *owner; 4 const struct file_operations *ops; 5 struct list_head list; 6 dev_t dev; 7 unsigned int count; 8 };
对于这个结构体的使用,你既可以单独地将它作为一个结构来使用,也可以将其嵌进你自己设备特定的结构中。个人觉得,将其嵌进自己的结构体会变得方便和简洁。如下:
1 struct globalmem_dev 2 { 3 struct cdev cdev; 4 unsigned char mem[GLOBALMEM_SIZE]; 5 };
那么接下来的问题还是很实际的问题,该如何去使用它。
首先,它作为一个结构体,我们就需要去填充它,即初始化。利用下面这个函数(定义在fs/char_dev.c):
void cdev_init(struct cdev *cdev,conststruct file_operations *fops);
其中也有一个所有者字段,应被置为THIS_MODULE。
其次,就应该将其信息告诉内核,利用下面的函数(定义在fs/char_dev.c):
int cdev_add(struct cdev *p, dev_t dev, unsigned count); struct cdev *p:cdev结构体 dev_t dev:该设备对应的第一个设备编号 unsigned count:应该和该设备关联的设备编号的数量,经常取1
当然,要有始有终,既然有add,就必然会有delete。
void cdev_del(struct cdev *p);
最后,提醒一句:在驱动程序还没有完全准备好处理设备上的操作时,就不能调用cdev_add。
因为,cdev_add一调用,整个驱动就开始运行了,开始了设备上的操作;但是如果你的操作都没有弄好,你的驱动又该会驶往何方……
好了,就先讲到cdev吧。
至此,我们已经将字符设备的注册已经将将完了;下次,我们来看看字符设备的那些操作结构体,那样,我们的程序就完整了!