i.MX6ULL学习笔记--字符设备驱动程序

简介

字符设备的驱动程序适合于大多数的简单硬件设备,比较容易上手。字符设备不可寻址,仅提供数据的流式访问,通过特殊的文件“字符设备节点”来访问。

1.驱动的配置过程

在这里插入图片描述
字符驱动程序的装载过程如上图所示,当驱动编写者编译出.ko文件后,通过insmod指令,执行__init 函数,在该函数中将会调用函数register_chrdev()来完成操作,首先完成设备号的申请、char_device_struct的申请并挂载到哈希表chrdevs中,然后申请到结构体cdev并进行初始化操作(文件操作符、kobj的关联),最后调用函数cdev_add()、kob_map()完成struct probe的申请,将对应的cdev结构体关联到probe结构体中,然后将初始化后的probe结构体按照主次设备号挂载到结构体kobj_map中的哈希表probes中。下面介绍相关的概念。

1.1设备号

主设备号标识设备对应的驱动程序,而次设备号则供内核使用,用于确认设备文件所致的设备。例如鼠标、键盘等虽然是不同的设备文件,但是由于他们都是usb协议,所以可以使用一个USB的驱动程序来驱动,那么他们的主设备号就是一致的,但是次设备号则有所不同。(这里还是有点不清晰,一个驱动程序在装载的过程中已经完成主次设备号的分配了)

这里的区分主次设备号是为了后面注册设备节点做准备,在使用指令mknod时,主设备号是确认的,次设备号则根据当前设备节点使用情况和最大设备数量决定。

设备号的获取可以通过静态获取(即指定设备号进行获取)也可以通过动态获取。file_operations结构体通过主设备号被保存在哈希表(chrdevs:全局变量)中,对象设备通过次设备号被保存在哈希表(cdev_map->probe:全局变量)中。那么内核只要通过主次设备号和哈希表,就能找到驱动程序和对象设备。

相对来说,发布版本最好使用动态设备号获取,这是由于静态设备号可能会因为环境的变化而起冲突(动态设备号的获取只初始化函数需要在调用register_chrdev_region时传入的设备号为0,具体的原因已经在附录源代码处进行了注释)。

实际上。通过附录数据结构中的介绍,可以知道file_operations结构体本质上是裸机操作的接口,即通过封装的概念将一些底层的驱动程序封装在结构体中,然后在内核中申请设备号,并绑定底层的驱动接口,就可以驱动某些设备了。

//linux/kdev_t.h
#define MINORBITS	20
#define MINORMASK	((1U << MINORBITS) - 1)

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))//主设备号
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))//次设备号
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))
//[MAJOR MINOR]

在目前的linux内核中,设备号dev是一个32位的无符号整形数据,主设备号占用12位,次设备号则占用20位。通过内核提供的宏定义可以获取主次设备号。

1.2哈希表-chrdevs

哈希表本质上就是数组加链表的组合结构,这是综合这两者之间的优劣而形成的,他们之间具体的差异可以查看笔记FreeRTOS学习笔记-列表中的相关介绍。

哈希表的结构示意图:
在这里插入图片描述

哈希表拥有255个成员,其中每个成员是一个指向链表的指针,某些有关联的信息将构成一个节点,并将节点挂载到对应的链表中。对于这样的结构,快速的找到某个功能的链表入口,并进行访问,同样链表的遍历存在时效的问题。

在哈希表chrdevs中,以主设备号对255求模后的结果为数组的下标 ,次设备号用来辨识不同的设备文件。通过求模来尽量均分主设备号,以达到快速访问的目的。

在分配主设备号的过程中,有部分地方并不清晰,即函数__register_chrdev_region在调用函数find_dynamic_major后,返回了一个主设备号,按照代码执行情况,这个设备号应该是不会重复的:函数优先动态分配255-234的设备号,其次考虑511-384的设备号,在找到预分配设备号时,都会遍历所有的节点,当该节点的主设备号和预分配设备冲突时会重新分配。那么函数__register_chrdev_region中的重叠判断又显得多余了,这让我百思不得其解,有可能是没理解到位。

但是基于预分配非0时(即不通过函数find_dynamic_major动态分配)则会出现基于输入的主次设备号进行分配,那么可能会产生冲突。

主要的冲突在于主设备号冲突时,次设备号的映射范围是否重叠,若次设备号重叠了就无法通过主+次设备号的方式来唯一确认驱动程序和驱动对象!而重叠分为三类:左重叠、右重叠以及包含性重叠。

对于哈希表chrdevs来说,主要的功能是根据主次设备号存储char_device_struct,这个结构体与驱动是对应的。(内核如何使用该结构还不知道,具体看下方2.5)

1.3哈希表-obj_map->probes

在这里插入图片描述
与哈希表chrdevs不同的是,probes被保存在全局变量结构体kobj_map中,同样是通过主次设备号进行排序(主设备号决定挂载在probes中的偏移量,次设备号决定在链表中的顺序)。功能上,从chrdevs中可以找到对应驱动设备的文件操作接口、引用次数等等属性。

1.4文件操作接口

内核给驱动程序开放了文件操作接口,该接口可以在附录数据结构中看到,包含了偏移、读取、写入、堵塞式操作等等。驱动开发者只需完善底层程序,然后在初始化函数中调用函数register_chrdev(),即可完成文件操作接口与cdev的结构体(字符设备管理结构体)的绑定。

1.5简单了解设备驱动模型

为了统一设备驱动的模型,Linux提供了sysfs文件系统,他和/proc文件系统是一样的,都希望用户态的程序可以访问到特定的内核数据信息。而普通的kobj结构体是sysfs文件系统的核心数据结构:每个kobj结构体都对应sysfs中的一个目录。具体到后面再进行深入学习。

在Linux中,无论是驱动还是目录都是以文件的形式存在,对于驱动而言,Linux通过结构体kset来关联一个驱动目录,将驱动相关的属性都挂载在这个目录下面。kset结构体的内容可以看附录中的数据结构,它自身继承了kobject结构体的属性(包含了一个这样的结构体),又使用一个表头来控制kobj结构体的链表。实际上Linux通过kset将kobject组织成一棵层次树,kset是同类型kobject结构的一个集合体,这个类型被记录在kset中的ktype中。可以通过调用函数kset_add();
kset_put();或者kobject_add();kobject_put();函数来增减引用的次数,实际上来说,kset的引用次数是引用内嵌的kobject的引用次数。

2.附录 (内附代码注释)

2.1数据结构

//与驱动模块相对应的文件操作接口
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

file_operations结构体是内核作为接口提供给驱动模块的工具,在Linux中,任意的模块都可以被看成是一个文件,为了实现这样的哲学思想,Linux内核使用使用了对象与方法的思路,将所有的操作都封装成了方法,所有的文件都看成对象。所以一方面文件操作结构体向应用层提供了驱动的接口,也向驱动层提供了接口函数的形式(传参、返回类型)。
值得注意的是,内核中,初始化这些方法的形式与平常结构体略有不同:

//示例
struct file_operations scull_fops{
	.owner = THIS_MODULE,
	.llseek = scull_llseek,
	.read = scull_read,
};

在传统的结构体初始化中,必须是按顺序、全成员的赋值过程。而这里采用的是标准C的标记化结构初始语法,即在成员前加上一个.符号,这样的声明方式则不需要被束缚于传统的赋值规则,那么驱动开发者在开发时则不需要消耗过多的无用功(记忆成员),这样也能有效的提高效率(一些不需要赋值的成员即使赋值成NULL也会消耗性能),顺便吐槽下KEIL好像不支持这个操作。

//linux/char_dev.c
#define CHRDEV_MAJOR_HASH_SIZE 255

static struct char_device_struct {
	struct char_device_struct *next;//下一个节点
	unsigned int major;		//主设备号
	unsigned int baseminor;
	int minorct;//次设备号的数量
	char name[64];//设备名称
	struct cdev *cdev;		/* will die被废弃了 */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];//哈希表255个成员

这里展示了哈希表的初始化过程,他通过声明char_device_struct结构体来指定哈系表中的成员是指向该结构体的指针。

//from kobject.h

struct kobj_type {
	void (*release)(struct kobject *kobj);//kobject被释放时执行的函数(即kobject引用次数等于0时
	//被调用来释放kobject)
	const struct sysfs_ops *sysfs_ops;//指向sysfs操作表的指针
	struct attribute **default_attrs;//缺省属性链表
	const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
	const void *(*namespace)(struct kobject *kobj);
	void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid);
};//这段描述了kobject的属性,实际上是描述了包括kobject的容器的属性

struct kobject {
	const char		*name;//驱动名称
	struct list_head	entry;//链表节点
	struct kobject		*parent;//上一级kobj结构体
	struct kset		*kset;//上一级kset结构体
	struct kobj_type	*ktype;//sysfs文件系统相关的操作和属性
	struct kernfs_node	*sd; /* sysfs directory entry */
	struct kref		kref;//引用次数
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
	struct delayed_work	release;
#endif
	unsigned int state_initialized:1;
	unsigned int state_in_sysfs:1;
	unsigned int state_add_uevent_sent:1;
	unsigned int state_remove_uevent_sent:1;
	unsigned int uevent_suppress:1;
};//
//管理字符设备对象的结构体from cdev.h
struct cdev {
	struct kobject kobj;//内核驱动基本对象
	struct module *owner;//相关内核模块
	const struct file_operations *ops;//设备驱动接口
	struct list_head list;//链表节点
	dev_t dev;//设备号 
	unsigned int count;//次设备号的数量
} __randomize_layout;


//from map.c
struct kobj_map {
	struct probe {
		struct probe *next;
		dev_t dev;
		unsigned long range;
		struct module *owner;
		kobj_probe_t *get;
		int (*lock)(dev_t, void *);
		void *data;//用于指向cdev结构体
	} *probes[255];
	struct mutex *lock;//互斥锁
};



struct kset {
	struct list_head list;//用于管理kobj结构体链表的表头
	spinlock_t list_lock;//自旋锁
	struct kobject kobj;//指向一个kobj
	const struct kset_uevent_ops *uevent_ops;//
} __randomize_layout;//from kobject.h


struct list_head {
	struct list_head *next, *prev;
};//from types.h

2.2函数

typedef unsigned long u_long;
typedef u_long dev_t;//from  coda.h

/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 */
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);//申请设备号n
		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);
}

/* Marks the bottom of the first segment of free char majors */
#define CHRDEV_MAJOR_DYN_END 234
/* Marks the top and bottom of the second segment of free char majors */
#define CHRDEV_MAJOR_DYN_EXT_START 511
#define CHRDEV_MAJOR_DYN_EXT_END 384

static int find_dynamic_major(void)
{
	int i;
	struct char_device_struct *cd;

	for (i = ARRAY_SIZE(chrdevs)-1; i >= CHRDEV_MAJOR_DYN_END; i--) {
		if (chrdevs[i] == NULL)//优先动态分配255-234的设备号
			return i;//若存在空的,则使用这个空的成员
	}

	for (i = CHRDEV_MAJOR_DYN_EXT_START;
	     i >= CHRDEV_MAJOR_DYN_EXT_END; i--) {//其次考虑511-384的设备号
		for (cd = chrdevs[major_to_index(i)]; cd; cd = cd->next)
			if (cd->major == i)//如果当前节点的主设备号已经存在,则不考虑这个节点
				break;

		if (cd == NULL)
			return i;//返回分配的设备号
	}

	return -EBUSY;
}


static DEFINE_MUTEX(chrdevs_lock);
/*
 * Register a single major with a specified minor range.
 *
 * If major == 0 this functions will dynamically allocate a major and return
 * its number.
 *
 * If major > 0 this function will attempt to reserve the passed range of
 * minors and will return zero on success.
 *
 * Returns a -ve errno on failure.
	major:主设备号
	baseminor:次设备号
	minorct:申请的设备数量
	name:设备名称
 */
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 ret = 0;
	int i;

	cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);//申请节点内存
	if (cd == NULL)
		return ERR_PTR(-ENOMEM);

	mutex_lock(&chrdevs_lock);

	if (major == 0) {//若主设备号为0,则动态分配
		ret = find_dynamic_major();//寻找主设备号
		if (ret < 0) {
			pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",
			       name);
			goto out;
		}
		major = ret;
	}

	if (major >= CHRDEV_MAJOR_MAX) {//边界检查
		pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",
		       name, major, CHRDEV_MAJOR_MAX-1);
		ret = -EINVAL;//被定义在 errno-base.h中
		goto out;
	}

	cd->major = major;
	cd->baseminor = baseminor;
	cd->minorct = minorct;
	strlcpy(cd->name, name, sizeof(cd->name));//初始化节点信息

	i = major_to_index(major);//找到映射的索引号

	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next){
		if ((*cp)->major > major || 
		    ((*cp)->major == major &&
		     (((*cp)->baseminor >= baseminor) ||
		      ((*cp)->baseminor + (*cp)->minorct > baseminor))))
			break;}//在哈系表指定的成员中的链表遍历进行排序

	/* Check for overlapping minor ranges.  */
	if (*cp && (*cp)->major == major) {//是否重叠?通过位与来判断,若A&&B=C,则A=B=C,均不等于0
		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;
		}

		if (new_min < old_min && new_max > old_max) {//新的次设备号的作用域包含了冲突的设备号中
			ret = -EBUSY;
			goto out;
		}

	}
//新成员赋值
	cd->next = *cp;
	*cp = cd;
	mutex_unlock(&chrdevs_lock);
	return cd;
out:
	mutex_unlock(&chrdevs_lock);
	kfree(cd);
	return ERR_PTR(ret);
}


int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
	     struct module *module, kobj_probe_t *probe,
	     int (*lock)(dev_t, void *), void *data)
{
	unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;	
	//在定义中,dev是一个32位的无符号整形(主设备号),
	//而range是次设备号,这里计算出range高于256的数据?即可以简化成n=range/256;?
	//(x>>8)=>x/256,(x+y-1)>>8 => (uint16_t)(x/256+y/256-1/256)-x/256+1,
	//不考虑这个步骤,这里出来的是range是指定数据的倍数,即需要多少个主设备来保证次设备号的管理。
	unsigned index = MAJOR(dev);
	unsigned i;
	struct probe *p;

	if (n > 255)
		n = 255;

	p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);
	if (p == NULL)
		return -ENOMEM;

	for (i = 0; i < n; i++, p++) {//初始化刚申请的probe结构体(每个结构体对应一个主设备号)
		p->owner = module;
		p->get = probe;
		p->lock = lock;
		p->dev = dev;
		p->range = range;
		p->data = data;//将kobj_map中的data变量指向cdev结构体
	}
	mutex_lock(domain->lock);
	for (i = 0, p -= n; i < n; i++, p++, index++) {
		struct probe **s = &domain->probes[index % 255];
		//定义了指向probe的双层指针并获取到哈系表的指定入口(主设备号对)求余
		while (*s && (*s)->range < range)//指定probes[]中的probe是否为存在(发生冲突)且子设备号是否小于等待插入的子设备号
			s = &(*s)->next;//依次寻找(排序过程,通过比较子设备号的大小)
		p->next = *s;
		*s = p;//插入
	}
	mutex_unlock(domain->lock);
	return 0;
}
/**
 * cdev_add() - add a char device to the system
 * @p: the cdev structure for the device
 * @dev: the first device number for which this device is responsible
 * @count: the number of consecutive minor numbers corresponding to this
 *         device
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */
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;
}


/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */
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;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值