Linux内核是怎么设计字符设备的
Linux哲学
一切皆文件
如何把字符设备抽象成文件
复习文件描述符本质
open()函数,在文件系统中找到指定文件的操作接口,绑定到进程task_srtuct->files_struct->fd_array[]->file_operations
思路
把底层寄存器配置操作放在文件操作接口里面,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件来设置底层寄存器
硬件层原理
基本接口实现
-
查原理图,数据手册,确定底层需要配置的寄存器
-
类似裸机开发
-
实现一个文件的底层操作接口,这是文件的基本特征
struct file_operations
ebf-buster-linux/include/linux/fs.h
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文件操作接口注册到内核,内核通过主次设备号来登记记录它
-
构造驱动基本对象:struct cdev,里面记录具体的file_operations
cdev_init()
-
两个hash表
-
chrdevs:登记设备号
__register_chrdev_region()
-
cdev_map->probe:保存驱动基本对象struct cdev
cdev_add()
-
文件系统层原理
mknod指令+主从设备号
-
构建一个新的设备文件
-
通过主次设备号在cdev_map中找到cdev->file_operations
-
把cdev->file_operations绑定到新的设备文件中
到这里,应用程序就可以使用open()、write()、read()等函数来控制设备文件了
设备号的组成与哈希表
设备号
ebf-buster-linux/include/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))
理论取值范围
主设备号:2^12=1024*4=4k
次设备号:2^20=1024*1024=1M
-
已注册的设备号可以使用
cat /proc/devices
查看 -
内核是希望一个设备驱动(file_operation)可以独自占有一个主设备号和多个次设备号,而通常一个设备文件绑定一个主设备号和一个次设备号,所以设备驱动与设备文件是一对一或者一对多的关系。
hash table
哈希表、散列表
-
数组的优缺点:查找快,增删元素效率低,容量固定
-
链表的优缺点:查找慢,增删元素效率高,容量不限
-
哈希表:数组+链表
- 以主设备号为编号,使用哈希函数
f(major)=major%255
来计算数组下标 - 主设备号冲突(如0、255),则以次设备号为比较值来排序链表节点。
- 以主设备号为编号,使用哈希函数
哈希函数的设计目标:链表节点尽量平均分布在各个数组元素中,提高查询效率
从源码看如何管理设备号
关键数据结构梳理
ebf-buster-linux/fs/char_dev.c
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];
__register_chrdev_region函数分析
ebf-buster-linux/fs/char_dev.c
保存新注册的设备号到chrdevs哈希表中,防止设备号冲突
分析结论:
-
主设备号为0,动态分配设备号:
- 优先使用:255~234
-
其次使用:511~384
-
主设备号最大为512
从源码看如何保存file_operation接口
关键数据结构梳理
kernel/ebf-buster-linux/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;
} __randomize_layout;
ebf-buster-linux/fs/char_dev.c
哈希表probes
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];
struct mutex *lock;
};
cdev_init函数分析
ebf-buster-linux/fs/char_dev.c
保存file_operation到cdev中
cdev_add函数分析
ebf-buster-linux/fs/char_dev.c
根据哈希函数保存cdev到probes哈希表中,方便内核查找file_operation使用
如何创建一个设备文件
mknod引入
创建指定类型的特殊文件
mknod --help
用法:mknod [选项]... 名称 类型 [主设备号 次设备号]
Create the special file NAME of the given TYPE.
...
当类型为"p"时可不指定主设备号和次设备号,否则它们是必须指定的。
如果主设备号和次设备号以"0x"或"0X"开头,它们会被视作十六进制数来
解析;如果以"0"开头,则被视作八进制数;其余情况下被视作十进制数。
可用的类型包括:
b 创建(有缓冲的)区块特殊文件
c, u 创建(没有缓冲的)字符特殊文件
p 创建先进先出(FIFO)特殊文件
如:
mkmod /dev/test c 2 0
原理分析
init_special_inode函数分析
ebf-buster-linux/fs/inode.c
判断文件的inode类型,如果是字符设备类型,则把def_chr_fops作为该文件的操作接口,并把设备号记录在inode->i_rdev。
要点
inode上的file_operation并不是自己构造的file_operation,而是字符设备通用的def_chr_fops,那么自己构建的file_operation等在应用程序调用open函数之后,才会绑定在文件上。
open函数如何查找file_operation接口
-
get_unused_fd_flags
- 为本次操作分配一个未使用过的文件描述符
-
do_file_open
- 生成一个空白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;
}
...
}
led字符设备驱动实验
驱动模块= 内核模块(.ko)+ 驱动接口(file_operations)
-
在内核模块入口函数里获取gpio相关寄存器并初始化
-
构造file_operations接口,并注册到内核
-
创建设备文件,绑定自定义file_operations接口
-
应用程序echo通过写设备文件控制硬件led
驱动模块初始化
地址映射
GPIO寄存器物理地址和虚拟地址映射
ebf-buster-linux/arch/arm/include/asm/io.h
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
参数:
-
res_cookie:物理地址
-
size:映射长度
返回值:
- void * 类型的指针,指向被映射的虚拟地址
- __iomem 主要是用于编译器的检查地址在内核空间的有效性
虚拟地址读写
readl()/ writel() //过时
void iowrite32(u32 b, void __iomem *addr) //写入一个双字(32bit)
unsigned int ioread32(void __iomem *addr) //读取一个双字(32bit)
检查cpu大小端,调整字节序,以提高驱动的可移植性
自定义led的file_operations接口
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
- owner:设置驱动接口关联的内核模块,防止驱动程序运行时内核模块被卸载
- release:文件引用数为0时调用
拷贝数据
include/linux/uaccess.h
copy_from_user(void *to, const void __user *from, unsigned long n)
参数:
-
*to:将数据拷贝到内核的地址
-
*from 需要拷贝数据的用户空间地址
-
n 拷贝数据的长度(字节)
返回值:
失败:没有被拷贝的字节数
成功:0
register_chrdev函数
ebf-buster-linux/include/linux/fs.h
注册设备号函数到内核
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
__register_chrdev函数
kernel/ebf-buster-linux/fs/char_dev.c
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;
...
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
...
}
- 次设备号为0,次设备号数量为256