前言
我们接着讲,我们使用正点原子提供的字符设备驱动的模板来进行讲解。相信大家应该都已经知道代码的意思,我们来分析函数之间的调用关系。模板如下:
/* 注册字符设备驱动 */ /* 1、创建设备号 */ if (newchrled.major) { /* 定义了设备号 */ newchrled.devid = MKDEV(newchrled.major, 0); register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME); } else { /* 没有定义设备号 */ alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); /* 申请设备号 */ newchrled.major = MAJOR(newchrled.devid); /* 获取分配号的主设备号 */ newchrled.minor = MINOR(newchrled.devid); /* 获取分配号的次设备号 */ } printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor); /* 2、初始化cdev */ newchrled.cdev.owner = THIS_MODULE; cdev_init(&newchrled.cdev, &newchrled_fops); /* 3、添加一个cdev */ cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT); /* 4、创建类 */ newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); if (IS_ERR(newchrled.class)) { return PTR_ERR(newchrled.class); } /* 5、创建设备 */ newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME); if (IS_ERR(newchrled.device)) { return PTR_ERR(newchrled.device); } return 0; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit led_exit(void) { /* 注销字符设备驱动 */ cdev_del(&newchrled.cdev);/* 删除cdev */ unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */ device_destroy(newchrled.class, newchrled.devid); class_destroy(newchrled.class); }
在这里,关于函数的参数什么的就不讲了,如果不知道,可以找我前面写的博文。
注册字符设备编号
内核提供了三个函数来注册一组字符设备编号,这三个函数分别是
int register_chrdev(unsigned int, const char *,struct file_operations *)//int为0时候动态注册
int alloc_chrdev_region(dev_t, unsigned, const char *); //动态的申请注册一个设备号
int register_chrdev_region(dev_t, unsigned, const char *); //静态的申请和注册设备号
这三个函数都会调用一个共用的 __register_chrdev_region() 函数来注册一组设备编号范围(即一个 char_device_struct 结构)。
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) { struct char_device_struct *cd, **cp; int i; int ret = 0; cd = kmalloc(sizeof(struct char_device_struct), GFP_KERNEL);/*slab分配一个char_device_struct变量*/ if (cd == NULL) return ERR_PTR(-ENOMEM); memset(cd, 0, sizeof(struct char_device_struct));/*将刚刚分配的变量的内存区清零*/ write_lock_irq(&chrdevs_lock);/*关中断,禁止内核抢占+读写锁*/ /* temporary */ if (major == 0) {/*分配一个主设备号!*/ for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) { if (chrdevs[i] == NULL) break; } if (i == 0) { ret = -EBUSY; goto out; } major = i; ret = major; } cd->major = major; cd->baseminor = baseminor; cd->minorct = minorct;/*申请设备号的个数*/ cd->name = name; /****************处理char_device_struct变量的分配和初始化************/ /****************将char_device_struct变量注册到内核*****************/ i = major_to_index(major);/*将major对256取余数,得到可以存放char_device_struct在chrdevs中的索引*/ /* *退出循环: (1)chrdevs[i]为空 (2)chrdevs[i]的主设备号大于major (3)chrdevs[i]的主设备号等于major,但是次设备号大于等于baseminor */ for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ((*cp)->major > major || ((*cp)->major == major && (((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)))) break; /* *如果*cp不空,并且*cp的major与要申请的major相同, *此时,如果(*cp)->baseminor < baseminor + minorct,就会发生冲突 *因为和已经分配了的设备号冲突了。 *出错! */ /* Check for overlapping minor ranges. */ if (*cp && (*cp)->major == major) { int old_min = (*cp)->baseminor; int old_max = (*cp)->baseminor + (*cp)->minorct - 1; int new_min = baseminor; int new_max = baseminor + minorct - 1; /* New driver overlaps from the left. */ if (new_max >= old_min && new_max <= old_max) { ret = -EBUSY; goto out; } /* New driver overlaps from the right. */ if (new_min <= old_max && new_min >= old_min) { ret = -EBUSY; goto out; } } /* *所要申请的设备号可以满足 */ cd->next = *cp;/*按照主设备号从小到达的顺序排列*/ *cp = cd; write_unlock_irq(&chrdevs_lock); return cd; out: write_unlock_irq(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }
函数 __register_chrdev_region() 主要执行以下步骤:
- 分配一个新的 char_device_struct 结构,并用 0 填充。
- 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
- 根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
- 计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
- 将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。
初始化cdev
函数cdev_init()用于初始化一个静态分配的cdev结构体变量,函数cdev_init会自动初始化cdev->ops对象,将函数的第二个输入参数赋值给cdev->ops对象,不会初始化cdev->owner对象,因此在经过函数cdev_alloc()和函数cdev_init()处理之后的cdev结构体变量,在应用程序中只需要给cdev->owner对象赋值,此结构变量就可以被插入Linux内核系统了,作为一个可用的字符设备使用。
struct cdev { struct kobject kobj; /*字段 kobj用来描述设备的引用计数,在终端中,可以通过lsmod命令显示模块相关的信息,其中包括引用计数。*/ struct module *owner; /*字段owner描述了模块的从属关系,指向拥有这个结构的模块的指针,这个描述符只有对编译为模块方式的驱动才是有意义的,一般赋值为“THIS_MODULE”。*/ const struct file_operations *ops; /*字段ops描述了字符设备的操作函数指针,*/ struct list_head list; /*字段list描述了与cdev对应的字符设备文件的inode->i_devices的链表的表头。*/ dev_t dev; /*字段dev描述了字符设备的设备号,包括主设备号和次设备号。*/ unsigned int count; /*字段count指定设备编号范围的大小。*/ };
添加一个cdev
函数cdev_add()用于向Linux内核系统中添加一个新的cdev结构体变量所描述的字符设备,并且使这个设备立即可用。
创建类和创建设备就不多说了。
总结
- struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带,是一系列指针的集合,每个被打开的文件都对应于一系列的操作,这就是file_operations,用来执行一系列的系统调用。
struct file代表一个打开的文件,在执行file_operation中的open操作时被创建,这里需要注意的是与用户空间inode指针的区别,一个在内核,而file指针在用户空间,由c库来定义。
struct inode被内核用来代表一个文件,注意和struct file的区别,struct inode一个是代表文件,struct file一个是代表打开的文件。 - 在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体里面记录了这个文件的所有信息,例如:文件类型,访问权限等。
struct inode { umode_t i_mode; ... unsigned int i_flags; ..... dev_t i_rdev; loff_t i_size; ... struct list_head i_devices; union { struct pipe_inode_info *i_pipe; struct block_device *i_bdev; struct cdev *i_cdev; }; ... };
struct inode描述的是文件的静态信息,即这些信息很少会改变。而struct file描述的是动态信息,即在对文件的操作的时候,struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_pos(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变
2. 在Linux操作系统中,每个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应,并且该文件会有对应的主设备号和次设备号。
3. 在Linux操作系统中,每个驱动程序都要分配一个主设备号,字符设备的设备号保存在struct cdev结构体中。
- 在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。
结构体之间的关系:
如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。 - 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备)。还会分配一个struct file结构体。
- 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备有一个struct cdev结构体。此结构体描述了字符设备所有的信息,其中最重要一项的就是字符设备的操作函数接口。
- 找到struct cdev结构体后,Linux内核就会将struct cdev结构体所在的内存空间首地记录在struct inode结构体的i_cdev成员中。将struct cdev结构体的中记录的函数操作接口地址记录在struct file结构体的f_op成员中。
- 任务完成,VFS层会给应用层返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层的应用程序就可以通过fd来找到strut file,然后在由struct file找到操作字符设备的函数接口了。