• 00_linux_最简单构建字符设备 2.4版本之前使用


    背景:怎么构建一个最简单的字符设备驱动并且可以使用app进行操作
    2.4和2.6不同其实也就是构造设备节点的时候 一个是手动构造,一个是kobj构造

    名称大致意思
    设备proc/devices/设备名称 insmode驱动
    设备节点/dev/xxx 对这个设备进行操作的文件 mknode使用主次设备号对设备关联
    设备文件/sys/设备文件/设备属性 另一种操控驱动的方案,使用设备属性操作 设备文件关联 kobj
    设备属性sys/设备文件/设备属性 一个设备文件有多个设备属性 设备属性关联 kobj_attr

    大致方法
    1.写驱动文件(file_operation),构造对应的read write 函数
    2.分配设备名称(/proc/devices/设备名称)
    3.注册进内核 使用主次设备号,设备名称 ,insmode 注册后 在/proc/devices/设备名称 就能找到对应设备
    4.对这个设备进行设备文件注册,mknode 命令,生成 /dev/设备文件名称
    5.应用程序打开 设备文件 获得设备的 file_operation 结构体 对设备进行操作

    1实现底层驱动程序

    linux中万物皆为文件
    图中的一个设备会有各种调用参数 open read write 来组成这个设备的 file_operation结构体
    内核层中 在驱动程序里面 各种调用参数来对底层的硬件进行操作 作为驱动程序注册进内核变成一个设备 也会有 open read write等操作接口

    2 注册这个驱动程序到内核变成一个设备

    开始有了驱动程序的 file_operation结构体
    主次设备号 和 file_operation 结构体进行内核中的注册
    注册到内核中 变成一个设备驱动程序

    major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops); //注册到内核
    
    • 1

    注册到内核后 有了 proc/devices/DEV_NAME

    mknod [选项]… 名称 类型 [主设备号 次设备号]

    2.1 驱动层注册详细描述

    其实在驱动层中是构造了一个char_device_struct 结构体,char_device_struct 结构体继承了驱动结构体
    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 cdev *cdev;      /* will die */
    
    } *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    3 对设备构建设备文件

    上一步已经有proc/devices/DEV_NAME
    对这个DEV_NAME设备构建出对应的设备文件进行操作
    使用mkmod /dev/test c 2 0 命令 构建对应设备的设备文件,通过查找内核设备所有的主次设备号挑选出对应的 设备
    /dev/test 设备文件的文件名
    c 文件为字符设备文件
    2 主设备号
    0 次设备号

    4 应用程序使用设备文件进行操作

    应用程序是一个进程
    进程有自己的结构体 task_srtuct
    在这里插入图片描述

    task_srtuct->files_struct->fd_array[]->file_operations
    //里面保存了打开的文件 inode 节点  对这个节点 进行 write read 操作
    
    • 1
    • 2

    使用 open(/dev/xxx) 打开这个文件 把这个文件的file_operation 保存在 inode 数组里面
    后续使用 read write 调用 inode节点的各种操作函数 对文件 /dev/xxx 这个设备文件进行操作

    详细介绍

    总结 : 写驱动程序 就是为了让app操作 驱动程序中的file_operation接口
    所以需要把驱动程序中的file_operation结构体注册到内核 内核有两个哈希表chrdevs[256],和cdev_map->probe[256]表
    chrdevs哈希表 记录 char_device_struct结构体指针 新增一个字符设备,需要查询哈希表里面有没有
    cdev_map->probe哈希表 记录probe结构体指针 用于记录驱动里file_operattion 所以我们根据主次设备号就能找到 要操作的file_operation结构体
    app使用open打开的文件 抽象为inode结构体 对这个文件的操作接口file_operation也记录在 inode结构体上
    这样就能通过app进行操作了
    下面用几个问答尽量说明白

    怎么把自己写的file_operation结构体注册到内核

    把file_operations文件操作接口注册到内核,内核通过主次设备号来登记记录它
    struct cdev 记录操作接口 ,继承kobj

    struct cdev {
    	struct kobject kobj;
    	struct module *owner;
    	const struct file_operations *ops;
    	struct list_head list;
    	dev_t dev;
    	unsigned int count;
    };
    //这个函数初始化cdev,赋值驱动里自己写的file_operation结构体
    cdev_init(struct cdev *cdev, const struct file_operations *fops)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    用 file_operation 的时候通过两个哈希表来找到 自己写的file_operation
    这时候需要引入两个全局变量 chrdevs 和 struct kobj_map *cdev_map
    里面保存了两个哈希表chrdevs[255],cdev_map->probe[255]

    第一个哈希表 chrdevs[255]:登记设备号
    注册方法 register_chrdev() 每次新增一个字符设备,需要查询哈希表里面有没有
    __register_chrdev_region()
    存在意义 : 保存字符设备,新增字符设备看放哪个地方,方便查询
    第二个 cdev_map->probe:保存驱动基本对象struct cdev
    注册方法 cdev_add()
    存在意义:根据主次设备号,找到cdev,找到file_operation结构体
    通过两个哈希表已经保存了cdev 保存了主次设备号 还有自己file_operation结构体
    这时候需要 mknod指令+主从设备号 建立设备节点
    把自己file_operation结构体和设备节点绑定
    通过主次设备号在cdev_map中找到cdev->file_operations
    把cdev->file_operations绑定到新的设备节点中

    设备号和哈希表是什么关系,和/proc/devices关系

    在这里插入图片描述
    register_chrdev() cdev_add() 注册设备号后
    已经注册了的设备号 可以使用cat /proc/device 个人感觉把哈希表1列出来
    看见对应的名字和主设备号
    内核希望一个驱动file_opera 占一个主设备号 多个次设备号

    源码中怎么管理设备号(哈希表1)

    char_device_struct 关键数据结构 作为linux哈希表1的链表组成

    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]; //初始化 指针数组  256的数组 都存了结构体指针 struct char_device_struct 
    
    //下面是往表中注册新设备 根据主次设备号 名字 新增char_device_struct 
    register_chrdev()	
    	__register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) 
    										//保存新注册的设备号到chrdevs哈希表中,防止设备号冲突
    											//这四个函数参数都用来构建 新char_device_struct 结构体 存入这个结构体到哈希表
    		struct char_device_struct *cd, **cp;																	
    		cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);//申请内存
    		mutex_lock(&chrdevs_lock);//增加互斥锁,这个锁也是全局变量
    		if (major == 0)
    			major = find_dynamic_major();//为0在chrdevs表中找一个空闲位置
    			
    		//保存主次设备号和设备个数
    		cd->major = major;
    		cd->baseminor = baseminor;
    		cd->minorct = minorct;
    		//保存设备名称
    		strlcpy(cd->name, name, sizeof(cd->name));
    		//计算主设备号对应哈希表的位置
    		i = major_to_index(major);
    		//遍历链表元素指针 如果主设备号是0,遍历的就是chrdevs[0]->char_device_struct链表
    		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;
    		//存入新的char_device_struct指针到哈希表中
    		cd->next = *cp;
    		*cp = cd;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    怎么保存file_operation接口(哈希表2)

    如果以后想使用fie_operation接口,可以通过设备号在probes哈希表(哈希表2)中
    找到probe->data指向的cdev结构体,再找到file_operation

    //struct cdev字符设备管理对象 继承kobj
    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;
    //哈希表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 *);
    		//空指针,内核常用技巧,用来保持sruct cdev 指针 就能拿到file_operation 结构体
    		void *data;
    	} *probes[255];  //这里也就是probes哈希表,第二个哈希表,用来记录file_operattion
    	struct mutex *lock;
    };
    //往哈希表2增加新的设备
    //这个函数初始化cdev,赋值file_operation结构体
    cdev_init(struct cdev *cdev, const struct file_operations *fops)
    	kobject_init(&cdev->kobj, &ktype_cdev_default); //初始化一个kobj(仅仅初始化,没有绑定目录等)
    	cdev->ops = fops;//给cdev的ops指针赋值 自己的file_operation接口
    //继续初始化一下cdev,把cdev放入probes哈希表
    cdev_add(struct cdev *p, dev_t dev, unsigned count) //接受cdev结构体,设备号,数量
    	p->dev = dev;  //保存设备号
    	p->count = count; //保存设备号和设备号的数量
    	error = kobj_map(cdev_map, dev, count, NULL, //把cdev结构体填充到probes哈希表上
    							exact_match, exact_lock, p);
    		unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;  //判断次设备号是否溢出
    		if (n > 255) //判断主设备号是否大于255
    		p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL); //申请内存
    		//申请完后给probe结构体赋值
    		p->owner = module;
    		p->get = probe;
    		p->lock = lock;
    		p->dev = dev;  //保存设备号
    		p->range = range; //保存本次设备号数量
    		p->data = data; //这里保存了cdev结构体
    		struct probe **s = &domain->probes[index % 255]; //保存在probes哈希表中
    	kobject_get(p->kobj.parent);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    自己怎么创建设备节点 怎么和file_operation连接

    新版本中使用kobj创建设备节点
    这里说老版本的 linux指令 mkmod /dev/test c 2 0
    图呢? 结构体呢!!!
    用户空间 : mknod命令->glibc库mknode()->sys_mkonde
    内核空间 : sys_mknodat()->vfs_mknod()->dir->i_op->mknod(ext4_mknod)->init_special_inode()
    init_special_inode()
    inode类型 如果是字符设备类型 def_chr_fops作为该文件的操作接口
    设备号记录在inode->i_rdev
    自己构建的file_operation等在应用程序调用open函数之后,才会绑定在文件上
    所以目前自己的设备节点连接的file_operation是 通用的def_chr_fops

    struct inode {
    	xxx......
    	dev_t			i_rdev;
    	struct inode_operations	*i_op;
    	struct cdev		*i_cdev;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    app程序怎么找到驱动程序里的file_operation结构体

    一个文件对应着一个inode 设备节点中的字符设备通用file_operation对inode里面的设备号查找
    找出驱动程序真正的 file_operation结构体 结构体进行替换
    再把真正的结构体保存在进程的数组 直接调用
    在这里插入图片描述

    fd = open("/dev/xxx",O_RDWR)
    驱动层
    sys_open()
    	do_sys_open()
    		get_unused_fd_flags()
    		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);
    			open = f->f_op->open; //这里的f_op 是上面的def_chr_fops
    			error = open(inode, f);
    				def_chr_fops->chrdev_open()!!!!
    					const struct file_operations *fops;
    					struct cdev *p;
    					struct kobject *kobj;
    					struct cdev *new = NULL;
    					/* 内核哈希表 cdev_map 中,根据设备号查找自己注册的sturct cdev,获取cdev中的file_operation接口
    					        不懂怎么保存在哈希表的回去看 保持哈希表 */
    					kobj = kobj_lookup(cdev_map, inode>i_rdev,&idx);//cdev_map全局变量,能知道内核哈希表
    														现在得到cdev里面的kobj成员
    					new = container_of(kobj, struct cdev, kobj);//从kobj成员得到cdev变量
    					inode->i_cdev = p = new;//把cdev赋值给inode节点
    					fops = fops_get(p->ops);//获取到自己设计的file_o
    					peration结构体
    					/*把cdev中的file_operation接口赋值给struct file的f_op*/
    					replace_fops(filp, fops);
    					/*调用自己实现的file_operation接口中的open函数*/
    					filp->f_op->open(inode, filp);
    					/* 后续对inode节点的操作,都是调用自己的file_operation结构体了 */
    		fd_install()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    新旧的差距在创建设备节点,那到底差哪
    新的方法

    class_create()创建目录 /sys/class/xxx 同时返回class对象
    device_create()创建目录 创建/sys/class/xxx/yyy目录 同时返回device对象
    虽然class_create() 和 device_create() 都能创建目录项
    但device_create()不止创建了 /sys/class/xxx/yyy 还在yyy目录下创建了属性文件(/sys/class/xxx/yyy/dev)
    属性文件记录硬件设备的设备号
    创建完属性文件后 继续使用kobject_uevent()发送驱动添加的消息 给用户空间的udev守护进程
    这时udev会找到创建的yyy 读取dev这个设备属性文件夹 获得硬件设备的设备号
    udev 调用mknod()函数 来创建/dev/yyy的设备节点
    新旧本质 到最后都调用了mknode()函数
    只不过新方法的函数参数 放在(/sys/class/xxx/yyy/dev)

  • 相关阅读:
    可编程控制器有几种编程语言以及它们的特点
    四、二叉树-上(Binary tree)
    Python Pandas Series转换为DataFrame
    Java题目集-Chapter 10 Object-Oriented Thinking
    第6章 初识Spring框架
    goland的字符串类型
    git使用进阶(二)
    Hadoop中的Yarn 生产环境核心参数配置案例、Yarn 案例实操(一)
    「USACO 做题笔记」USACO 2011 Nov Bronze
    [Java] Lock(锁)的tryLock失败是否需要unlock?
  • 原文地址:https://blog.csdn.net/weixin_43898067/article/details/128067932