全部博文(573)
分类: LINUX
2015-12-09 15:38:34
字符设备是linux最常见的一种设备类型,也是相对比较简单的。Linux文件系统的注册是先注册文件系统的类型,比如sysfs、devtmpfs、rootfs等,然后再初始化一个文件系统实例添加进系统中。字符设备的注册其实也是采用这种方式:先向系统注册一个字符设备类型,包括主次设备号,文件操作集等,然后才可能创建一个该类型的字符设备实例添加进系统中,当然这些同类型的实例都是依靠次设备号来区分的。
一、 字符设备注册相关函数
1.1 高度封装的函数 @ kernel/fs/char_dev.c
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
eg: register_chrdev(INPUT_MAJOR, "input", &input_fops);
第一个参数是该类字符设备的主设备号,例如这里的INPUT_MAJOR是13,那么如果调用该函数注册字符设备的时候传入的是0,表示需要让系统动态分配一个主设备号给该类字符设备,并将这个主设备号返回。
每类字符设备的此设备号从0~255,也就是说,每类字符设备最多可存在256个设备实例。
static inline void unregister_chrdev(unsigned int major, const char *name)
这个函数是注销系统中的一类字符设备,需要传入该类字符设备的主设备号和名字。
1.2 分散调用的函数
其上上面的函数都是对接下来要描述的函数的封装,我们注册字符设备的时候可以单独调用下面的函数来一步一步实现。
1. 注册或者分配主次设备号
如果需要系统动态分配主设备号的话,推荐调用下面的函数来实现:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
第一个参数是dev_t变量的指针,接受该函数即将返回的主次设备号组合;第二个
参数baseminor和第三个参数count分别表示最小次设备号和支持的次设备号的个数;最后一个参数是该类字符设备的名字。
eg:alloc_chrdev_region(&fm->dev_t, 0, 1, FM_NAME); 只支持一个该类型设备实例
返回0表示成功执行,返回一个负数则是出错。
如果主设备号已知,就可以调用以下两种函数来注册:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
实际上register_chrdev_region()函数时调用了__register_chrdev_region()函数来实现的,只是返回值更简单明了而已,推荐使用函数register_chrdev_region()。例如:
register_chrdev_region(MKDEV(INPUT_MAJOR, 0),255,"input");
2. cdev分配或者初始化
struct cdev是字符设备的关键结构体,这一步就是需要分配或者初始化该结构体。如果
cedv已经有现成的了,那么就可以直接调用函数cdev_init()来初始化,如果还没有分配cdev的空间那么就需要调用函数cdev_alloc()来分配空间并初始化。
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
使用cdev_alloc()函数的,记得在执行该函数之后需要对cdev->ops进行初始化,而在cdev_init()中有做这个工作。
3. 向系统注册这个字符设备类型
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
eg: cdev_add(cdev, MKDEV(cd->major, baseminor), count);
1.3 MKDE宏
该宏的实现是: #define MKDEV(ma,mi) ((ma)<<8 | (mi))
可以看出,linux系统中主设备号最大用24位表示,而次设备号只有256个,也就是说每一种字符设备最多可以有256个设备实例存在。
二、原理实现
以register_chrdev(INPUT_MAJOR, "input", &input_fops)这个为例。
2.1 字符设备类型注册
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
// 256个次设备号
}
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
// 注册主次设备号
cd = __register_chrdev_region(major, baseminor, count, name);
…
cdev = cdev_alloc();
…
cdev->owner = fops->owner;
cdev->ops = fops; // eg: &input_fops
kobject_set_name(&cdev->kobj, "%s", name); // eg: "input"
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
…
cd->cdev = cdev;
return major ? 0 : cd->major;
…
}
2.1.1 主次设备号注册
这里可以使用第一节中出现的三个函数中alloc_chrdev_region、register_chrdev_region、__register_chrdev_region的任意一个来实现这个,只是在调用时注意入口和出口参数的不同。这里以函数__register_chrdev_region()为例来介绍,在继续之前我们先来聊一下关于这个函数的关键。
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev;
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
@ kernel/include/linux/fs.h
#define CHRDEV_MAJOR_HASH_SIZE 255
chrdevs是一个大小为255的char_device_struct类型的指针数组,在字符设备的管理中,没有将其当做简单的指针数组来是使用,而是将其作为一个以主设备号对255的余数为键值的hash table在使用,也就是说,这个指针数组的每一个元素(下标为i)如果不为NULL的话,都可以将其作为一个链表来链接那些主设备号是i的倍数的字符设备的char_device_struct结构体(这就是next域的作用)。
那么接下来的这个函数的作用简单的来说,就是在获得chrdevs_lock互斥锁的情况下操作这个chrdevs hash table。具体如何操作,请看下面的源码分析:
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, i;
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
…
mutex_lock(&chrdevs_lock);
/* 如果主设备号是0,表示需要系统动态分配一个未占用的主设备号 */
if (major == 0) {
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
// 从chrdevs数组的最后开始动态分配,直到遇到一个未占用的指针数组元素。
if (chrdevs[i] == NULL)
break;
}
if (i == 0) { // 全部都已被分配,没找到一个可用的。
ret = -EBUSY;
goto out;
}
major = i; // 保存分配的主设备号
ret = major;
}
// 为结构体char_device_struct各域赋值,保存major,次设备号范围,name。
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
i = major_to_index(major); // major对 255取余数的结果得到查询hash表的键值。
/*hash表的每一项,都是一个无头链表,链表中的每一个元素(char_device_struct结构体)是按照字符设备的major从小到大排列;major可以相同,这里依据次设备号大小排列。这里次设备号绝对不能有任何交叉和重叠区域,否则就会注册失败。*/
// cp是一个二级指针,*cp是指向char_device_struct的指针,*cp可以是NULL。
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
/* 这里查找有些复杂,大致可以分成以下几种情况:
1. (*cp)->major > major,表示新注册的major可以插入到链表中,那么将掉过下面的if条件直接将major对应的char_device_struct插入链表。
2. 上面的for循环没有被break过,而是因为*cp = NULL才退出循环的,这个时候表名新加入的major是当前链表中最大的,这样也会直接跳过接下来的if语句,而将这个major对应的char_device_struct插入链表尾。
3. (*cp)->major == major,表示主设备号相等,所以接下来就是要判断次设备号是否会有冲突的可能:
(*cp)->major == major &&(((*cp)->baseminor >= baseminor) ||
cp)->baseminor + (*cp)->minorct > baseminor))
用上面的语句来检查次设备号的冲突情况,举个例子来说:假设当前的*cp对应的字符设备的次设备号范围是{32, 64},那么这里将会检测出新字符设备的baseminor为小于64的情况(小于32或者在32和64之间),因为这两种情况将可能会和原来的次设备号有互相覆盖,所以这两种情况都会进入下面的if条件来进一步检测。
不过这里如果新字符设备的baseminor是大于64的话,那么就不可能发生覆盖现象,所以这种情况是不会break掉上面的for循环,直到遇到链表尾或者baseminor处于{32, 64}的另两个区间内才会退出for循环。
New Old
[68,90] {32,64} -- { } [ ]
*/
/* 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 Old
1. [16,24] {32,64} -- [ ] { }
2. [28,36] {32,64} -- [ { ] }
3. [40,48] {32,64} -- { [ ] }
4. [54,70] {32,64} -- { [ } ]
*/
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) { // 检测出第2、3中情况
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) { // 检测出第4中情况
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);
}
2.1.2 cedv结构体初始化
一个特定类型的字符设备用结构体cdev来描述,结构体定义如下:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
cdev->owner = fops->owner;
cdev->ops = fops; // eg: &input_fops
kobject_set_name(&cdev->kobj, "%s", name); // eg: "input"
2.1.3 向系统添加一个cdev描述的字符设备类型
cdev_add(cdev, MKDEV(cd->major, baseminor), count);
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev; // 设备号的base值
p->count = count; // 次设备号的个数
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
kobj_map()是另外一个重点函数,关于这一个知识点,有必要从其init来分析一下。
void __init chrdev_init(void)
{
cdev_map = kobj_map_init(base_probe, &chrdevs_lock);
bdi_init(&directly_mappable_cdev_bdi);
}
static struct kobject *base_probe(dev_t dev, int *part, void *data)
{
if (request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev)) > 0)
/* Make old-style 2.4 aliases work */
request_module("char-major-%d", MAJOR(dev));
return NULL;
}
static struct kobj_map *cdev_map;
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;
} *probes[255]; // 大小为255的指针数组
struct mutex *lock;
};
struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock)
{
struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL);
struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL);
int i;
if ((p == NULL) || (base == NULL)) {
kfree(p);
kfree(base);
return NULL;
}
base->dev = 1;
base->range = ~0;
base->get = base_probe;
for (i = 0; i < 255; i++)
p->probes[i] = base;
p->lock = lock;
return p;
}
实际上cdev_map->probes这个指针数组也是会作为一个hash表来使用。这个函数kobj_map_init就是对这个hash表的每一项都初始化成执行同一个probe结构体(base指针所指的probe结构体)。从后面的kobj_map()的代码中可以看出,这个probe结构体是处于每一个链表的末尾。
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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; // n = 1
unsigned index = MAJOR(dev); // 主设备号
unsigned i;
struct probe *p;
// 该函数常见的用法是对cdev一个一个地进行map
if (n > 255)
n = 255;
p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
if (p == NULL)
return -ENOMEM;
for (i = 0; i < n; i++, p++) {
p->owner = module; // eg: NULL
p->get = probe; // eg: exact_match
p->lock = lock; // eg: exact_lock
p->dev = dev; // 主设备号
p->range = range; // 次设备号个数
p->data = data; // 主设备号对应的cdev结构体
}
mutex_lock(domain->lock);
// 而这一个循环也只是循环一次,就是为了将上面初始化好的probe函数添加到hash表对应的链表中去。
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
}
mutex_unlock(domain->lock);
return 0;
}
到这里,一个字符设备类型注册就这样结束了,接下来我们看一下,如何添加一个真实的字符设备实例。
...