字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
![](https://i-blog.csdnimg.cn/blog_migrate/17b7cdf3105eb3ed288da9691cf32723.png)
1 字符设备的初步认识
1.1 与普通文件差异
在 Linux 中一切皆文件,字符设备也是以文件的形式存在于系统中,下面来通过 ls -l
命令比较普通文件与字符文件差异:字符设备
crw-rw-rw- 1 root tty 5, 0 4月 13 08:13 tty
crw--w---- 1 root tty 4, 0 4月 12 16:58 tty0
crw--w---- 1 root tty 4, 1 4月 12 16:58 tty1
普通文件
-rw-r--r-- 1 root root 3106 10月 23 2015 .bashrc
drwx------ 2 root root 4096 2月 16 2017 .cache/
drwx------ 3 root root 4096 3月 12 2017 .gnupg/
两者最大的差异在日期前两列,字符设备前两列分别表示主设备号和次设备号。另外,字符设备不像普通文件,在硬盘上占有正在的数据区。
1.2 与块设备的差异
- 字符设备顺序读写、块设备随机读取。
- 字符设备按字符逐个读写,而块设备按数据片(chunk)随机读取。
- 字符设备直接读取、块设备上面还有一层文件系统。
1.3 设备文件的创建命令
mknod 用于创建设备文件。
2 设备号
设备号分为主设备号和次设备号,主设备号与驱动程序对应,一个不同类型的驱动对应一个独有的主设备号;此设备号与具体设备对应,同一类设备下,每个都对应一个独有的次设备号。
-设备号类型
dev_t是无符号长整型号,
typedef u_long dev_t;
typeddef unsigned long u_long;
- 设备号计算
(1)主设备号 = MAJOR(dev_t dev)
(2)次设备号 = MINOR(dev_t dev)
(3)设备编号 = MKDEV(int major,int minor)
3 字符设备及其驱动函数
3.1 chrdevs 散列表
在 linux 内核中有一个记录所有已分配的字符设备编号,该散列表中每一个元素都是一个 char_device_struct 结构指针,其结构如下:
static struct char_device_struct {
struct char_device_struct *next; // 指向散列冲突链表中的下一个元素的指针
unsigned int major; // 主设备号
unsigned int baseminor; // 起始次设备号
int minorct; // 设备编号的范围大小
char name[64]; // 处理该设备编号范围内的设备驱动的名称
struct file_operations *fops;
struct cdev *cdev; // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
内核并不是每一个字符设备定义一个 char_device_struct 结构体,而是为一组对应同一个字符设备驱动的设备编号范围定义一个(也就是说与主设备号对应)。chrdevs 散列表的大小是 255,散列算法是把每组字符设备编号范围的主设备号以 255 取模插入相应的散列桶中,同一个散列桶中的字符设备编号范围是按照次设备号递增排序的。
3.2 驱动注册
字符设备注册函数有以下三种原型:
//动态分配一个主设备号
extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
//已有主设备号注册
extern int register_chrdev_region(dev_t, unsigned, const char *);
//注册主设备号的的次设备范围为 0 -250,早期接口,同时会注册一个设备
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
均调用此函数实现
extern int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops);
其源码为:
/*
* 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.
*/
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);
//若major 为 0,则系统自动分配一个主设备号
if (major == 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 (%d) is greater than the maximum (%d)\n",
name, major, CHRDEV_MAJOR_MAX);
ret = -EINVAL;
goto out;
}
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
//将major转换为散列表的 索引
i = major_to_index(major);
//查找该节点应该插入散列的位置,由于是hash表,故不同key(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;
//若该major已经存在,则判断设备范围是否在已有节点范围内,若重合,则返回错误
/* 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;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
3.3 设备注册
内核中每个字符设备都对应一个 cdev 结构的变量,下面是它的定义:
<include/linux/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; //隶属于同一主设备号的次设备号的个数.
};
设备初始化:
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;
}
向内核注册一个设备
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;
}