Linux字符设备驱动——入口函数完全解析

inode——索引

inode 译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 … … )被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。
做个比喻,比如一本书,存储设备或分区就相当于这本书,Block相当于书中的每一页,inode 就相当于这本书前面的目录,一本书有很多的内容,如果想查找某部份的内容,我们可以先查目录,通过目录能最快的找到我们想要看的内容。

当我们用ls 查看某个目录或文件时,如果加上-i 参数,就可以看到inode节点了;比如ls -li lsfile.sh ,最前面的数值就是inode信息

struct file

struct file结构体定义在include/linux/fs.h中定义。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。

cdev

此部分转自Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析,强烈建议大家看一下,这位博主写的非常好!

在Linux内核中:

a – 使用cdev结构体来描述字符设备;

b – 通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;

c – 通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

在Linux字符设备驱动中:

a – 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;

b – 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;

c – 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;

用户空间访问该设备的程序:

a – 通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;

源码解析

这里简单说一下cdev_add()将新的struct probe 添加到散列表中,其中的void* data则和cdev绑定,方便后续open系统调用使用。

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	int error;

	p->dev = dev;
	p->count = count;

	error = kobj_map(cdev_map, dev, count, NULL,
			 exact_match, exact_lock, p);
	if (error)
		return error;

	kobject_get(p->kobj.parent);

	return 0;
}

重点来看一下kobj_map函数:

在这里插入图片描述

设备号分配

两种方法分配设备号

register_chrdev_region和alloc_chrdev_region是新版本推荐使用的主设备号注册函数,优点是注册的次设备号的数量可以自己决定,缺点就是需要自己完成cdev的init 和 add。二者的对比如下:

动态分配

alloc_chrdev_region:内核自动分配设备号,用于动态注册(因为内部调用__register_chrdev_region()时传入的主设备号为0),优先从254~234范围内进行主设备号的分配,如果不在此范围内,内核会给出警告

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

静态申请

register_chrdev_region:用于静态注册,用于设备号已知的情况下进行静态注册,如果传入的主设备号为0,则也相当于动态注册,原因就在于无论是register_chrdev_region还是alloc_chrdev_region ,其内部都调用了__register_chrdev_region()函数。

int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
	struct char_device_struct *cd;
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		cd = __register_chrdev_region(MAJOR(n), MINOR(n),
			       next - n, name);
		if (IS_ERR(cd))
			goto fail;
	}
	return 0;
fail:
	to = n;
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
	return PTR_ERR(cd);
}

__register_chrdev_region(): Register a single major with a specified minor range.

注册单个主设备号及旗下指定范围的次设备号。

if (major == 0) {
		for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
			if (chrdevs[i] == NULL)
				break;
		}

		if (i < CHRDEV_MAJOR_DYN_END)
			pr_warn("CHRDEV \"%s\" major number %d goes below the dynamic allocation range\n",
				name, i);

		if (i == 0) {
			ret = -EBUSY;
			goto out;
		}
		major = i;
	}

__register_chrdev_region()调用图解,填充chrdevs散列表,记录主次设备号。

在这里插入图片描述

自动分配设备号+注册cdev

register_chrdev是老版本的内核常用的,缺点就是一次申请255个次设备号,占用内存多,容易造成资源浪费。之所以一次注册255个次设备号是因为其内部调用__register_chrdev_region()函数时传入了如下参数:

static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

优点就是这个函数不仅帮我们完成了设备号的分配,内部还调用了cdev_add()和cdev_init()函数自动的完成了struct probe *probes[255]散列表的初始化和注册,probes用来管理cdev,虽然做了很多无用功,但这样让代码看起来更简洁。

参数及返回值:

传入 0 则自动分配主设备号,否则使用自定义主设备号+申请255个次设备号

class 介绍

参考:https://blog.csdn.net/hustyangju/article/details/21018103

在这里插入图片描述

一个类是一个设备的高级视图, 它抽象出低级的实现细节. 驱动可以见到一个SCSI 磁盘或者一个 ATA 磁盘, 在类的级别, 它们都是磁盘. 类允许用户空间基于它们做什么来使用设备, 而不是它们如何被连接或者它们如何工作.

几乎所有的类都在 sysfs 中在 /sys/class 下出现. 因此, 例如, 所有的网络接口可在 /sys/class/net 下发现, 不管接口类型. 输入设备可在 /sys/class/input 下, 以及串行设备在 /sys/class/tty. 一个例外是块设备, 由于历史的原因在 /sys/block.

cdev_add()执行后将会在/proc/devices文件中看到字符设备,但是在/dev/目录下没有生成相应的设备文件。直到class_device_create()执行后才会在/dev/目录下生成设备文件。而class_create()执行后会在/sys/class/目录下生成目录。

字符设备驱动入口函数初始化流程

有了以上基础,下面用几个例子来说明一下关于字符设备的入口函数中函数的调用流程

如果想方便省事,不在乎这点资源的浪费可以这样设计

static int __init led_init(void)
{
	int err;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "100ask_led", &led_drv);  /* /dev/led */


	led_class = class_create(THIS_MODULE, "100ask_led_class");
    /* 这里之所以没有调用device_create()函数,是因为此框架下将具体单板的资源设备下层,通用的驱动设为上层,实现了驱动的上下分离 */
	err = PTR_ERR(led_class);
	if (IS_ERR(led_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "100ask_led");
		return -1;
	}
	
	return 0;
}

动态分配主设备号alloc_chrdev_region() + cdev_init() + cdev_add()

/* 入口函数 */
static int adxl34x_drv_init(void)  
{  
  	int ret;
	dev_t adxl34x_devid;
	
	//printk(KERN_INFO"%s OK.\n",__func__);
	if(alloc_chrdev_region(&adxl34x_devid, 0, 1, "adxl345") < 0)
    {
        printk(KERN_ERR"Unable to alloc_chrdev_region.\n");
        return -EINVAL;
    } 
    adxl34x_major = MAJOR(adxl34x_devid);
	cdev_init(&adxl34x_cdev, &adxl34x_fops);        
    ret = cdev_add(&adxl34x_cdev, adxl34x_devid, 1);
    if (ret < 0)
    {
        printk(KERN_ERR "Unable to cdev_add.\n");
        goto error;
    }
        
    adxl34x_class = class_create(THIS_MODULE, "adxl345"); 
    device_create(adxl34x_class, NULL, MKDEV(adxl34x_major, 0), NULL, "adxl345"); 

	return 0;
error:
    unregister_chrdev_region(MKDEV(adxl34x_major, 0), 1);
    return -EINVAL;
	
}  


/* 修饰入口函数 */
module_init(adxl34x_drv_init);

自定义静态分配主设备号示例:

static int __init raw_init(void)
{
	dev_t dev = MKDEV(RAW_MAJOR, 0);
	int ret;

	if (max_raw_minors < 1 || max_raw_minors > 65536) {
		printk(KERN_WARNING "raw: invalid max_raw_minors (must be"
			" between 1 and 65536), using %d\n", MAX_RAW_MINORS);
		max_raw_minors = MAX_RAW_MINORS;
	}

	raw_devices = vzalloc(sizeof(struct raw_device_data) * max_raw_minors);
	if (!raw_devices) {
		printk(KERN_ERR "Not enough memory for raw device structures\n");
		ret = -ENOMEM;
		goto error;
	}

	ret = register_chrdev_region(dev, max_raw_minors, "raw");
	if (ret)
		goto error;

	cdev_init(&raw_cdev, &raw_fops);
	ret = cdev_add(&raw_cdev, dev, max_raw_minors);
	if (ret)
		goto error_region;
	raw_class = class_create(THIS_MODULE, "raw");
	if (IS_ERR(raw_class)) {
		printk(KERN_ERR "Error creating raw class.\n");
		cdev_del(&raw_cdev);
		ret = PTR_ERR(raw_class);
		goto error_region;
	}
	raw_class->devnode = raw_devnode;
	device_create(raw_class, NULL, MKDEV(RAW_MAJOR, 0), NULL, "rawctl");

	return 0;

error_region:
	unregister_chrdev_region(dev, max_raw_minors);
error:
	vfree(raw_devices);
	return ret;
}

还可以结合一下,做一个判断:

if (major) {
		devid = MKDEV(major, 0);
		rc = register_chrdev_region(devid, PC8736X_GPIO_CT, DEVNAME);
	} else {
		rc = alloc_chrdev_region(&devid, 0, PC8736X_GPIO_CT, DEVNAME);
		major = MAJOR(devid);
	}

···
    
	cdev_init(&pc8736x_gpio_cdev, &pc8736x_gpio_fileops);
	cdev_add(&pc8736x_gpio_cdev, devid, PC8736X_GPIO_CT);

于是,在入口函数中,我们将自定义的file_operations结构体的地址传给cdev_init()函数,函数内部将其与cdev结构体的ops绑定

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);
    /* 指向传入的file_operations结构体 */
	cdev->ops = fops;
}

那么,用户空间调用open函数是如何查找file_operation的接口的呢?

open函数如何查找file_operation的接口

简单来讲,用户空间调用open函数,最终会导致内VFS调用chardev_open()函数,之后,struct file对象的f_op指向cdev的ops,
之后应用层调用read write等函数时便可以通过struct file的ops对象直接找到对应的驱动读写函数,对应最后一个框图中的“ 绿色通道 ”。

在这里插入图片描述

  • get_unused_fd_flags

    • 为本次操作分配一个未使用过的文件描述符
  • do_file_opendd

    • 生成一个空白struct file结构体
    • 从文件系统中查找到文件对应的inode
  • do_dentry_open

static int do_dentry_open(struct file *f,
			  struct inode *inode,
			  int (*open)(struct inode *, struct file *))
{
	...
	/*把inode的i_fop赋值给struct file的f_op*/
	f->f_op = fops_get(inode->i_fop);
	...
	if (!open)
		open = f->f_op->open;
	if (open) {
		error = open(inode, f);
		if (error)
			goto cleanup_all;
	}
	...
}
  • def_chr_fops->chrdev_open

    ​ ebf-buster-linux/fs/char_dev.c

static int chrdev_open(struct inode *inode, struct file *filp)
{
	const struct file_operations *fops;
	struct cdev *p;
	struct cdev *new = NULL;
	...
	struct kobject *kobj;
	int idx;
	/*从内核哈希表cdev_map中,根据设备号查找自己注册的sturct cdev,获取cdev中的file_operation接口*/
	kobj = kobj_lookup(cdev_map, inode>i_rdev,&idx);
	new = container_of(kobj, struct cdev, kobj);
	...
	inode->i_cdev = p = new;
	...
	fops = fops_get(p->ops);
	...
	/*把cdev中的file_operation接口赋值给struct file的f_op*/
	replace_fops(filp, fops);
	
	/*调用自己实现的file_operation接口中的open函数*/
	if (filp->f_op->open) {
		ret = filp->f_op->open(inode, filp);
		if (ret)
			goto out_cdev_put;
	}
	...
}

在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值