linux进阶:字符设备
设计字符设备
文件系统调用系统IO的内核处理过程
inode索引节点是文件系统中的一种数据结构,用于存储文件的元数据信息,包括文件的大小、访问权限、创建时间、修改时间等。每个文件在文件系统中都对应着一个唯一的inode节点,通过inode节点可以查找到文件的实际数据块的位置。inode节点通常存储在磁盘的inode表中,文件系统通过inode号来访问和管理文件。
file_operation结构体是函数指针表,用于定义文件的操作方法。当应用程序通过文件描述符打开文件时,内核会根据文件描述符找到对应的inode节点,并获取与inode节点关联的file_operation表。通过file_operation表中的函数指针,内核可以调用相应的函数来执行文件操作,如open、read、write、close等。不同内核可以有不同的file_operation表,因为不同的内核可能有不同的文件操作方法和特性。
task_struct结构体用于描述和管理进程,内容很多很复杂。里面有个成员变量是struct files_struct *files,用于存储与进程相关的文件描述符表的信息(文件描述符表记录了进程打开的文件以及相应的操作权限等信息)。要想获取进程的文件描述符相关信息,需要通过访问task_struct结构体的files指针来获取files_struct结构体,进而访问文件描述符表的信息。
files_struct结构体用于跟踪和管理进程打开的文件。fd_array[]为指针数组,用于存储进程打开的文件描述符的信息,即每个文件描述符都对应一个files_struct。通过fd_array数组可以快速访问和操作这些文件描述符,数组索引值对应着文件描述符的值。
硬件层原理
思路:把底层寄存器配置操作放在文件操作接口里,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件来配置底层寄存器。
基本接口实现:查原理图,数据手册,确定底层需要配置的寄存器。类似于裸机开发。实现一个文件的底层操作接口,这是文件的基本特征。
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() //把用户构建的file_operations结构体记录在内核驱动的基本对象
两个Hash表(帮助找到cdev结构体)
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=4K
次设备号:2^20=1M
cat /proc/devices:查看已注册的设备号。
内核是希望一个设备驱动(file_operation)可以独自占有一个主设备号和多个次设备号,而通常一个设备文件绑定一个主设备号和一个次设备号,所以设备驱动与设备文件是一对一或者一对多的关系。
Hash Table(哈希表、散列表,数组和链表的混合使用)
以主设备号为编号,使用哈希函数f(major)= major % 255 来计算主设备号的对应数组下标。
主设备号冲突(如0、255,都挂载在数组0下标),则以次设备号为比较值来排序链表节点。
哈希函数的设计目标:链表节点尽量平均分布在各个数组元素中,提高查询效率。
设备号管理
关键的数据结构:char_device_struct(存放在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; //内核字符对象(已丢弃) } *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
关键的函数:__register_chrdev_region(存放在ebf-buster-linux/fs/char_dev.c)
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,从chadevs哈希表中查找一个空闲位置 */ 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; 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; /* 如果主设备号相等,检查次设备号是否存在冲突 */ 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; /* 判断新设备的次设备号最大值是否位于链表节点的次设备号范围 */ if (new_max >= old_min && new_max <= old_max) {
/* 确定冲突,返回错误 */ ret = -EBUSY; goto out; } /* 判断新设备的次设备号最小值是否位于链表节点的次设备号范围 */ 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); }
上诉函数主设备号相等,判断新旧次设备号三种错误图如下。
保存新注册的设备号到chrdevs哈希表中,防止设备号冲突。
主设备为0时,动态分配设备号(优先使用255~234,其次使用511~384)。主设备号最大为512。
保存file_operation结构体
关键数据结构:cdev(存放在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;
关键数据结构:kobj_map(与哈希表有关,存放在ebf-buster-linux/drivers/base/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; } *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使用。
热门相关:恭喜你被逮捕了 未来兽世:买来的媳妇,不生崽 异世修真邪君 豪门情变,渣总裁滚远点! 豪门情变,渣总裁滚远点!