第十章 linux-深入学习字符设备驱动编程②

我们接着讲,我们使用正点原子提供的字符设备驱动的模板来进行讲解。

前言

我们接着讲,我们使用正点原子提供的字符设备驱动的模板来进行讲解。相信大家应该都已经知道代码的意思,我们来分析函数之间的调用关系。模板如下:
`/* 注册字符设备驱动 /
/
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() 主要执行以下步骤:

  1. 分配一个新的 char_device_struct 结构,并用 0 填充。
  2. 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
  3. 根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
  4. 计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
  5. 将新的 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结构体变量所描述的字符设备,并且使这个设备立即可用。
在这里插入图片描述
在这里插入图片描述

创建类和创建设备就不多说了。

总结

0.struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带,是一系列指针的集合,每个被打开的文件都对应于一系列的操作,这就是file_operations,用来执行一系列的系统调用。
struct file代表一个打开的文件,在执行file_operation中的open操作时被创建,这里需要注意的是与用户空间inode指针的区别,一个在内核,而file指针在用户空间,由c库来定义。
struct inode被内核用来代表一个文件,注意和struct file的区别,struct inode一个是代表文件,struct file一个是代表打开的文件。
1.在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结构体中。
在这里插入图片描述

4.在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。
在这里插入图片描述
结构体之间的关系:
在这里插入图片描述
如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。
1.当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备)。还会分配一个struct file结构体。
2.根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备有一个struct cdev结构体。此结构体描述了字符设备所有的信息,其中最重要一项的就是字符设备的操作函数接口。
3.找到struct cdev结构体后,Linux内核就会将struct cdev结构体所在的内存空间首地记录在struct inode结构体的i_cdev成员中。将struct cdev结构体的中记录的函数操作接口地址记录在struct file结构体的f_op成员中。
4.任务完成,VFS层会给应用层返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层的应用程序就可以通过fd来找到strut file,然后在由struct file找到操作字符设备的函数接口了。

posted @ 2022-11-14 23:40  Paranoid-up  阅读(81)  评论(0)    收藏  举报