分类:
2012-07-09 18:35:35
只看了两天的源码,分析的过程是源码加想象,可能与实际实现有一定差别。因为目的只是为了大致了解字符设备的实现原理。更重要的是,要彻底分析,必须要对文件系统有深入了解才行,显然不是我目前能力可以办到的。kernel作为一个整体,单独学任何一块都比较困难。文件系统等很难绕过去,而单独分析文件系统又会因为涉及到太多东西而无法完成。只能采用这种农村包围城市的方法,先从外围下手。
Linux中万物都是文件,设备也是这样。要使用一个设备,首先要为它建立一个文件,然后通过下面的方法操作它。
int fd; char buf[100]; fd =open(“/dev/TestChar”, O_RDONLY, 0); if (-1 == fd) return ERROR;
read(fd, buf, 100); close(fd); |
如何创建一个设备文件呢?作为一个标准的文件,可以用open创建(实际上调用的是create)。如果是一个设备,则要通过mknod创建。这是以为设备驱动的创建方式是非标准的。假如通过open或create创建文件,kernel默认会调用该文件所在目录属于的设备的驱动。
什么意思呢?
如果我创建一个文件名为“/myfile”的文件,因为是在根目录下,所以会用根目录所在设备的驱动。一般是磁盘驱动ext2或ext3。如果我通过mount命令把一个u盘关在了“/mnt”上,再创建文件“/mnt/myfile”,则会调用u盘的驱动。可是现在需要创建的“/dev/TestChar”是新的字符设备,显然不能用 “/dev”的方法创建。
对于这种情况,这用mknod创建,由用户指定创建时使用的驱动。
第二节 什么是mknod
现在我们再进一步讨论下mknod。
乍看mknod,似乎有点奇怪。既然是创建文件,为何不叫mkdevfile,而叫mknod呢?
原因很简单,它真的是make node,而不是make device file。
前面提到Linux万物都是文件,这有点让人混淆,应该说linux中外物都是inode,file则是inode的一层外衣。我们在任何一个目录下,调用ls看到的所有条目,包括子目录与文件,都对应着一个inode。Inode中保存着文件名、属性、驱动等信息。严格意义上讲,代表着一个文件的file结构体此时可能还没有,只有调用open才会生成file结构体。File结构体把操作一个文件或设备所需的信息,加上传入的flags、mode等,保存起来,以备驱动调用。
mknod 的作用是创建inode节点,这是我们就明白了,open之前必须先用mknod创建inode节点。创建完inode节点用我们就可以用ls命令查看它了。open该节点后,则可以对inode对应的设备进行读写等操作。
mknod调用格式如下:
mknod Name { b | c } Major Minor
具体含义不讲了,需要说明的是他的参数,Major对应着一组驱动,也就是后面我们要写的东西。
创建普通文件的inode使用的是create,其内部也是调用的mknod,其Major号默认就是存储设备驱动。而我们要创建的inode就没这么幸运了。需要我们来指定Major号,也就是主设备号。
至此,我们虽然什么都没做,但也明白了创建字符设备驱动的唯一需要的就是一个主设备号。如何得到一个主设备号呢?
第三节 如何获得主设备号
先来看看系统原有的设备号,查看“/proc/devices”文件的内容,一般是这样的:
Character devices:
1 mem
2 pty
3 ttyp
180 usb
Block devices:
2 fd
8 sd
第二列是驱动名字,第一列就是主设备号。如何添加新的设备号呢?既然是在proc目录下,显然不能直接读写。在用户态我们是无能为力的,因为主设备号只能在kernel中操作。添加新的主设备号就是驱动初始化中首先要做的事情。
Kernel中添加新设备号的函数是register_chrdev_region(),它可以把一个设备号添加到内核中,但前提是我们知道哪个设备号没人用。不要以为“/proc/devices”中没有的设备号就是可用的,很多设备号是预留的。为了避免这一问题,最好的办法是让kernel为我们分配一个。对应函数是alloc_chrdev_region()。
完成上面的操作我们就可以在“/proc/devices”中查看对应的设备号了。
第四节 如何添加驱动
有了设备号,似乎一切都具备了,再回到刚才的mknod上去,填上主设备号(先不考虑次设备号),执行。我没试过,但我猜应该是失败的。为什么呢?因为还缺点东西。
到现在为止,我们都做了哪些工作?申请主设备号,创建节点,等着打开设备。
设备…….
似乎还没有给设备写驱动。这就好像是我们报名参加考试,却没有去考,考试结果当然是failed。alloc_chrdev_region()是报名,mknod是结果,而写驱动就是答题的过程。Kernel中使用cdev_add为某个设备号添加具体驱动。其原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
dev是设备号,count是指可以创建几个这种类型的设备,也就是次设备号的范围。简单来讲主设备号表示驱动类型,次设备号表示设备实例。
P是该设备的数据维护结构。这个结构也挺复杂,因此kernel提供单独的函数cdev_init()对它进行初始化。P可以是我们自己申请,也可以是通过cdev_alloc()获得。好处是系统会在卸载模块时自动释放它,比较方便。
Cdev的结构比较复杂,但只有两项需要关心。一个是owner,一般填THIS_MODULE,另一个是ops,填真正的设备驱动,如open、read等。
至此,可以看到我们要做的就是实现ops的各个函数。通常只需要实现open、read、write、ioctl等几个函数。
还有一个涉及到实现细节的问题。
通常我们维护一个设备驱动需要很多数据,只靠一个统一的cdev结构体是不够的,因此真正实现时会创建一个自定义的结构体,如TestChar_dev,cdev结构体作为其中一个元素。
后见面我们会看到,open等函数只能得到cdev结构体的指针,我们需要通过kernel提供的container_of()来获取TestChar_dev的指针。
实现一个字符设备驱动,最主要的是实现下列方法:
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
这些函数的大部分参数都很简单,复杂些的只有两个,file与inode。Inode我们只会用到inode->i_cdev,用于获得cdev结构体。获得后用container_of得到真正的TestChar_dev结构体首地址,存储到file->中。之后用实现read与write时只会用到参数file。
第五节 开发过程整理
完成上面的分析后,我们整理一下开发过程。
驱动开发:
1. 使用alloc_chrdev_region()或register_chrdev_region()添加一个主设备号;
2. 自己申请或使用cdev_alloc()获得一块cdev结构;
3. 实现ops中需要调用的方法;
4. 用cdev_init()初始化cdev结构;
5. 使用cdev_add()把cdev结构体与新添加的主设备号关联起来;
增加节点:
Mknod /dev/TestChar c major 0
第六节 整理数据结构
对整体的数据结构作个总结。
Chdevs是维护char型设备的数组。Cdev_map是维护cdev数据结构的数组。my_cdev0与my_cdev1是两种设备驱动。驱动中调用alloc_chrdev_region()时,会在chrdevs中申请一个设备号。调用cdev_add时,会调用kobj_map把它映射到cdev_map中,并把刚申请到设备号写进去。最后调用mknod,在文件系统中创建一个设备节点,用来挂载设备驱动程序。
本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。