LDD3学习笔记(二)--简单的字符设备
通过老大的提示,自己的努力,完成了第三章的学习,最后自己实现了一个类似书本scull字符设备驱动模块。
什么叫字符设备,什么叫字符设备驱动。字符设备和字符设备驱动是两个不同的概念。字符设备就是以字节为单位进行顺序访问的一类设备的总称。典型的常用的字符设备有:键盘,串口,控制台等。字符设备驱动程序就是提供操作字符设备的机制。
一、主设备号与次设备号
在自己的系统上输入: ls -l /dev观察输出。我们会发现如下面所示的文件的详细信息。
- crw-r--r-- 1 root root 4, 0 6月 26 2010 systty
- crw-rw-rw- 1 root tty 5, 0 6月 26 2010 tty
- crw--w---- 1 root root 4, 0 6月 26 2010 tty0
- crw--w---- 1 root root 4, 1 6月 26 2010 tty1
- crw--w---- 1 root tty 4, 10 6月 26 2010 tty10
- crw--w---- 1 root tty 4, 11 6月 26 2010 tty11
这是我系统输出的一部分。我们对字符设备的访问是通过文件系统内的设备名称进行的。内核把设备当作特殊的文件或者说是设备文件来进行操作。它们通常位于/dev目录下。crw-rw-rw- 这一部分是文件权限等方面的说明。第一项的“c"标识它是一个字符设备文件,可能你会看到其他的文件是"b" "l"字样,它们代表不同的设备文件类型。文件拥有者和拥有组后面两个数字就是设备的主设备号和次设备号。例如4、5都是主设备号,0 、1 、10 、11等都是次设备号。
主设备号用来标识设备对应的驱动程序;次设备号用于正确确定设备文件对应的设备。这个怎么理解呢?例如我们要操作某个设备,我们怎么做呢?首先,我们要知道设备在/dev下的设备文件名。这个设备文件提供主设备号以及次设备号。然后内核通过设备文件提供的主设备找到设备驱动程序(操作设备由驱动程序实现)。最后通过主设备号和次设备构成的设备号找到正确的设备。有了操作的对象(设备)和操作的方法(驱动程序)那就可以完成了我们的要求。一个驱动程序可以操作多个设备,所以不同的设备可以具有相同的主设备号。那为什么说次设备号用于正确确定设备文件呢?不是说通过主设备号和次设备号构成的设备号找到正确的设备吗?确实是这样,因为我们在添加设备到内核的时候我们是关联设备号的。因为不同的设备可以具有相同的主设备号,那不同的次设备号和相同的主设备号结合不就构成了不同的设备号了?不就标识了不同的设备?这么说可能会让人有更晕的感觉。不急,等到理解了设备的注册后再来理解这个应该比较容易。但确实只能是不同的设备才能标识不同的设备。假如一个设备的的主设备是4次设备是5,另一个设备主设备是5次设备是5,那不能说这两个是同一个设备吧。只能综合主设备和次设备才可以的嘛。接下来就是设备编号的介绍。
二、设备编号的内部表达
在上面主设备和次设备的介绍中我们提到设备编号,真正能标识不同的设备的是设备编号。每一个设备有一个唯一的设备编号。经常看电影我们看到,监狱里的犯人都不被呼姓名,而直接呼囚衣上的编号,我们现在说的设备也就是这个意思。
在内核中,用dev_t类型来保存设备编号,我们可以猜测它其实是一个无符号整型。这个类型在<linux/types.h>中定义。
设备号由主设备号和次设备号构成。如:广东省深圳市这个东东由省名和市名构成。内核提供三个宏来实现这三个东东的转换。分别是:MKDEV(int major, int minor) MAJOR(dev_t dev) MINOR(dev_t dev)。这三个宏名非常直观表明作用,不必多说。这三个宏在<linux/kde_t.h>中定义。
三、分配和释放设备编号
内核是通过设备编号找到设备的,理所当然地要建立一个字符设备那必须要获得字符设备编号。要建立多少个字符设备就要得到多少个字符设备编号。完成这一工作有两种方式,一种是静态获取,一种是动态获取。分别由:register_chrdev_region() alloc_chrdev_region()这两个函数实现。成功调用申请设备编号的函数后,在系统的/proc/devices下就会包含设备以及设备主设备号的信息。函数在<linux/fs.h>中声明。字符设备不再使用时应该释放它们占用的编号。由unregister_chrdev_region()。这三个函数的原型如下:
- int register_chrdev_region(dev_t first, unsigned int count,
- char *name);
- int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
- unsigned int count, char *name);
- void unregister_chrdev_region(dev_t first, unsigned int count);
分配多个设备编号时,分配到的编号的主设备号都是一样的。所以有时也说成是分配主设备号,其实我我也想知道分配函数的源代码,可是我不知道它在哪里定义,也不知道怎么找。
四、动态分配主设备号
我自己也真想知道是分配了主设备号再得到设备号,还是分配设备才得到主设备号。反正传出参数dev_t类型,意思是说参数是设备编号。当然这不影响我们的学习,但是知道个所以然是最好的,好读书并求甚解是最好的。
关于选择静态还是动态分配的讨论书本说的比较清楚,并且也很容易看懂,那就多看看书。一般我们采用动态的分配的方式。作者提供的相关源代码我认为非常经典了。我们在完成字符设备编号的申请完全可以只做些变量名的修改就可以使用了。
- 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;
- }
一旦分配了设备号就可以读取/proc/devices以获得主设备号。作者提供的源代码包含一个scull_load脚本主要实现三个功能:一、加载模块。二、获取主设备号(awk这个工具)。三、建立设备节点,也就是设备文件(mknod)。更多的技巧书本也讲得比较详细,比较容易懂。
五、字符设备的注册
分配好设备编号后就可以注册字符设备了。内核中由struct cdev结构表示字符设备。这个结构在<linux/cdev>中定义。
- struct cdev {
- struct kobject kobj;
- struct module *owner;
- const struct file_operations *ops;
- struct list_head list;
- dev_t dev;
- unsigned int count;
- };
字符设备的注册分三步:一、定义字符设备结构。二、初始化。三、添加字符设备到内核中。下面我们一步步地理解。
根据分配和初始化字符设备结构的不同,有两种不同的方式。
方法一、
1、动态获得字符设备结构并初始化
- struct cdev *my_cdev = cdev_alloc();
- my_cdev->ops = *my_fops;
- my_cdev->ower = THIS_MODULE;
cdev_alloc()只是动态分配了一个struct cdev结构,所以我们必须自己初始化struct cdev成员。一般我们设备owner和ops(这个结构我们随后再讲)
cdev_alloc()函数的代码可以在<fs/char_dev.c>中找到
- struct cdev *cdev_alloc(void)
- {
- struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
- if (p) {
- INIT_LIST_HEAD(&p->list);
- kobject_init(&p->kobj, &ktype_cdev_dynamic);
- }
- return p;
- }
2、把设备好的cdev结构添加到内核中去:
- int cdev_add(struct cdev *dev, dev_t num; unsigned int count);
从这个添加函数的参数我们知道,字符设备结构和设备编号是关联的。这也是为什么我们在注册设备前必须要先获得设备编号。
方法二、
1、分配一个字符设备结构。
很简单,struct cdev cdev;这个方法是书本采用的方法。但这里有一个不太容易理解的地方。书本把字符设备嵌入到了scull这个设备里。我在看书本的时候晕了好久,书本又说字符设备内核结构是struct cdev,但scull的结构又是struct scull_dev,这个要注意区分。嵌入到scull这个设备里后,在分配这种设备时,肯定也就分配了struct cdev.
2、初始化字符设备结构
- 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; //这句可以省略,在cdev_init中已经做过
- err = cdev_add (&dev->cdev, devno, 1);
- /* Fail gracefully if need be */
- if (err)
- printk(KERN_NOTICE "Error %d adding scull%d", err, index);
- }
初始化设备用到了一个函数:cdev_init()看了原型就知道为什么那句可以省掉了。
原型:
- void cdev_init(struct cdev *cdev, const struct file_operations *fops)
- {
- memset(cdev, 0, sizeof *cdev);
- INIT_LIST_HEAD(&cdev->list);
- kobject_init(&cdev->kobj, &ktype_cdev_default);
- cdev->ops = fops;
- }
3、添加设备结构到内核中去,和上面的方法一样。
两种方式完成的工作是一模一样的,只不过是用到的函数不一样。
到目前为止我们已经完成了字符设备的创建,并把它添加到内核里了。不过工作才刚刚开始。
六、file_operations结构
到目前为止我们已经完成了设备的创建工作。接下来我们就要定义操作设备的机制。关于机制和策略的讨论已经超出我现在的水平了。设备驱动程序提供我们操作设备的能力。对设备能做什么操作是由我们编写的驱动程序决定的。内核提供一个结构把这些操作和设备关联起来。这个结构就是file_operations。这个结构在<linux/fs.h>中定义。file_operations就是把对设备能做的操作与设备关联起来。浏览这个结构的内容的时候你会发现,它的成员包含一系列的函数指针,这些函数的函数名和linux的系统调用是一样的。对,它们是一一对应关系。比如我们调用系统调用:read("/dev/scull",buf,5)这个系统调用最终就会调用scull这个设备的file_operations里的read成员。当然在这你在知道什么叫系统调用。非常有必要快速浏览一个file_operations这个结构的内容。
那这个结构怎么用呢?我们不是说这个结构把设备操作能力与设备关联起来吗?那我们猜想它肯定是在设备创建的时候与设备关联的。事实也是我们猜想的这样。再返回设备注册的时候所做的操作。还记得吗,有两种设备注册方式。一是用cdev_alloc(),然后我们再初始化这字符设备结构中的一个重要的成员:my_cdev->ops=fops。注意查看一下struct cdev这个结构的成员,它有这样一个成员,struct file_operations *ops,fops通过就是一个指向file_operations结构的指针。这样设备就与file_operations结构关联了。书本提供的源代码是这样实现的:
- 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,
- };
定义了一个scull_fops结构,然后用这个结构初始化scull_dev这个设备的struct cdev结构成员。有个成员:owner不是一个操作函数指针,关于它的说明请参考书本,几乎所有的情况下它都要被初始化成THIS_MODLE.其实书本的的例子是把struct cdev这个内核表示的字符设备结构嵌入到scull这个设备里。所以它struct cdev的ops成员的方式如下:
- 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);
- }
这样设备和定义操作设备能力就关联起来了。理解file_operations的作用非常重要,因为在我看来编写一个字符设备驱动最重要的就是编写file_operations函数成员。其实应该说几乎所以的设备驱动程序都是这样的。
到这里基本上应该可以得到了一个字符设备驱动程序的架构了。
七、file和inode结构。
书本重点提到了file和inode,但好像基本上很少讲到file和inode,我在看书的时候就很奇怪,都说这两个结构是很重要的,必不可少的,那为什么讲的时候没给人一种重要的感觉。呵呵,至少我在看书的时候有这个奇怪。file表示一个打开的文件,也就是说只有文件被打开才分配这样的结构。inode是文件在内核内部的表示,准确地说应该是文件在文件系统上的表示,不管怎么样,只要文件存在于文件系统上,那就分配一个inode结构。再进一步区别file和inode,file结构只在打开文件时分配,并且打开一次分配一个,同时打开多次,那就分配多个,但都指向同一个inode.另外习惯上用filp来指向file结构。这两个重要的结构在<linux/fs.h>里定义,非常有必要参考书本上的介绍去理解这两个结构的成员。
注意这个两个结构是作为驱动程序模块的传入参数被内核传进来的,所以在我们的驱动模块里不会看到它们被定义。怎么理解呢?
先说inode,inode是文件系统上表示文件,而file是打开的文件在内核内的表示。还是不解?很难办,因为我也很难描述。我尽量吧,呵呵。我们注册设备的时候只是让内核在内部表示这个设备,我们也说过内核把设备当作特殊的文件看待,但文件又要创建在文件系统的基础上,而文件系统又是通过inode来描述文件。这个时候我们用到一个工具:mknod,这个工具就是在文件系统上创建一个已经在内核注册了的设备,用文件的形式表示设备。mknod的用法是:mknod /dev/scull0 c major minor总共4个参数,设备文件名,设备文件类型(c表示字符设备,b表示块设备),主设备号,次设备号。这个命令要root用户才能运行。这个工具就是在文件系统上创建inode结构。inode结构有两个重要的成员:dev_t i_rdev,包含设备编号,struct cdev *i_cdev;指向我们设备编号对应的设备。再来看mknod的用法,major minor组成dev_t i_rdev也就是设备编号,struct cdev *i_cdev指向这个设备编号对应的设备结构。
因为我们已经用足够多的信息创建了设备文件,所以我们访问设备文件的时候就可以通过设备文件的inode结构里的dev_t i_rdev和struct cdev *i_cdev找到我们注册的设备结构。还是不懂?再来看一下我们向内核添加字符设备结构的代码,
- int cdev_add(struct cdev *dev, dev_t num, unsigned int count),
第一个参数第二个参数不就是我们介绍的那两个成员吗?
还有file结构。我们说过file结构表示打开的文件。这个结构是内核在打开设备文件的时候创建的,也就是说,内核在其内部用file结构表示每一个打开的文件,所以file结构是内核创建的,不用在驱动程序里定义,也不可能在驱动程序里定义,只有内核才有创建这个结构的能力。关于file结构的内核也是我们必须要了解的。我们再仔细看struct file_operations各函数指针成员,那些函数的参数都包含:strict inode,struct file 这两个参数。而且我们要记住这两个参数是由内核传给驱动模块的。来看具体的例子:
- 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;
- return 0;
- }
省略了一些语句,但不影响讨论。其实还不是很明显,为了简化讨论的简结,这里不深入讨论了,其实是我深入不了,呵呵。假设有下面操作设备文件的调用:
- int fd;
- fd = open("/dev/scull0", buf,5); //这个是系统调用open,请参考它的用法。
内核在处理这个调用的时候大概会做这些工作:
一、调用system_call来处理系统调用open,老实处理的具体细节我也不懂,呵呵。
二、通过/dev/scull0这个设备文件的主设备号找到驱动程序,通过设备文件的inode的dev_t i_rdev和struct cdev *i_cdev找到具体的设备
三、通过驱动程序模块和设备结构的ops(file_operations)调用scull_open
四、返回filp
描述得比较粗糙,这是因为我本身理解得也比较粗糙,还请高手指点,当然如果理解错误并误人子弟,我表示歉意。
这里省略了file_operations结构成员的编写,其实它是一个驱动程序最重要的部分。但由于每个人每个驱动提供的机制不一样,编写这个结构的成员的方法各不一样。总的来说,你要驱动程序提供什么机制就编写对应的成员,而每个成员都有一个具体的系统调用与其对应。理解了字符设备驱动程序的结构后再理解scull设备就比较容易了。书本的scull设备的结构总的来说还是比较复杂的,所以其file_operations对于一个学习驱动程序的新手来说还是比较难理解的。但是我相信,理解了书本上讲到的理论后再去读源代码应该比较容易,然后再反过来去理解理论就可以比较深入地理解那些原理了。
附:
一、这是我们老大在我们学习时提出的几点要求,大家也可以参考,按着这些要求去看书,去学习。
1, 理解什么是字符设备
2,字符设备的主设备号,次设备号,内核使用主设备号,找到驱动模块,而次设备号,通过什么途径传递给驱动模块代码使用的。
3,怎样自己创建设备节点, mknod的使用方法
4,主设备号可以预先固定,也可以通过alloc_chrdev_region动态获得的。驱动中是怎么注册字符设备的。思考一下注册的操作,内核会做哪 些工作
可以通过cat /proc/devices获得装载的模块主设备号,可以通过一个脚本,读取这个文件,获得主设备号,并通过mknod创建设备节点。
5,字符设备的file_operations包括那些成员函数指针,这些指针在应用中是怎么对应使用的。
6,尝试写一个字符设备驱动,理解每个kernel API的用法和含义。