字符设备驱动编程②
前言
我们接着讲,我们使用正点原子提供的字符设备驱动的模板来进行讲解。相信大家应该都已经知道代码的意思,我们来分析函数之间的调用关系。模板如下:
/* 注册字符设备驱动 */
/* 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找到操作字符设备的函数接口了。