Linux内核|字符设备

Linux内核是怎么设计字符设备的

Linux哲学

一切皆文件

如何把字符设备抽象成文件
复习文件描述符本质

open()函数,在文件系统中找到指定文件的操作接口,绑定到进程task_srtuct->files_struct->fd_array[]->file_operations

Linux内核|字符设备_第1张图片

Linux内核|字符设备_第2张图片

思路

把底层寄存器配置操作放在文件操作接口里面,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件来设置底层寄存器

硬件层原理
基本接口实现
  • 查原理图,数据手册,确定底层需要配置的寄存器

  • 类似裸机开发

  • 实现一个文件的底层操作接口,这是文件的基本特征

    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

哈希表、散列表
Linux内核|字符设备_第3张图片

  • 数组的优缺点:查找快,增删元素效率低,容量固定

  • 链表的优缺点:查找慢,增删元素效率高,容量不限

  • 哈希表:数组+链表

    • 以主设备号为编号,使用哈希函数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

原理分析

Linux内核|字符设备_第4张图片

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接口

Linux内核|字符设备_第5张图片

  • 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

你可能感兴趣的:(linux,运维,服务器)