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;
}