零基础学Linux内核之设备驱动篇(4)_字符设备1_基本概念

零基础学Linux内核系列文章目录

前置知识篇
1. 进程
2. 线程
进程间通信篇
1. IPC概述
2. 信号
3. 消息传递
4. 同步
5. 共享内存区
编译相关篇
1. GCC编译
2. 静态链接与动态链接
3. makefile入门基础
设备驱动篇
1. 设备驱动概述
2. 内核模块_理论篇
3. 内核模块_实验篇
4. 字符设备1_基本概念


一、前言

本节介绍一下字符设备的前置知识,包括设备号dev_t / 字符设备cdev / 设备节点 / 设备文件,以及设备相关操作f_op / inode / file相关结构体,为后续字符设备抽象成文件做个铺垫。


二、前置条件


三、本文参考资料

《 [野火]i.MX Linux开发实战指南》
百度


四、正文部分

4.1 相关概念

4.1.1 设备号

使用设备编号来表示设备,主设备号区分设备类别(使用不同驱动程序),次设备号标识具体的设备
对于字符的访问是通过文件系统的名称进行的,
这些名称被称为特殊文件、设备文件,或者简单称为文件系统树的节点,
Linux根目录下有 /dev 这个文件夹,专门用来存放设备中的驱动程序,我们可以使用ls -l 以列表的形式列出系统中的所有设备。
其中,每一行表示一个设备,每一行的第一个字符表示设备的类型

如下图:’ c ‘用来标识字符设备,’ b ‘用来标识块设备,’ I ‘用来标识符号链接文件,’ d ‘用来标识目录文件,’ - ‘用来标识普通文件,’ p '命名管道文件
如0hostif0是一个字符设备c,它的主设备号是239,次设备号是4
在这里插入图片描述
一般来说,主设备号指向设备的驱动程序,次设备号指向某个具体的设备。如图,I2C-0,I2C-1属于不同设备(0/1)但是共用一套驱动程序(89)
在这里插入图片描述

4.1.2 内核中设备编号的含义

在内核中,dev_t用来表示设备编号,dev_t 是一个32位的数,其中,高12位表示主设备号,低20位表示次设备号
也就是理论上主设备号取值范围:0 - 2^ 12,次设备号0 - 2^ 20。
实际上在内核源码中__register_chrdev_region(…)函数中,major被限定在0-CHRDEV_MAJOR_MAX,CHRDEV_MAJOR_MAX是一个宏,值是512。
在kdev_t.h中,设备编号通过移位操作最终得到主/次设备号码,同样主/次设备号也可以通过位运算变成dev_t类型的设备编号
具体实现参看上面代码MAJOR(dev)、MINOR(dev)和MKDEV(ma,mi)。

  • dev_t定义 (内核源码/include/types.h)

    typedef u32 __kernel_dev_t;
    typedef __kernel_dev_t               dev_t;
    
  • 设备号相关宏 (内核源码//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))
    
    // 用于将主设备号和次设备号合成一个设备号,主设备可以通过查阅内核源码的Documentation/devices.txt文件,而次设备号通常是从编号0开始。
    #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))  
    

4.1.3 cdev结构体

内核通过一个**散列表(哈希表)**来记录设备编号。 哈希表由数组和链表组成,吸收数组查找快,链表增删效率高,容易拓展等优点。

以主设备号为cdev_map编号,使用哈希函数f(major)=major%255来计算组数下标(使用哈希函数是为了链表节点尽量平均分布在各个数组元素中,提高查询效率);
主设备号冲突,则以次设备号为比较值来排序链表节点。

如下图所示,内核用struct cdev结构体来描述一个字符设备,并通过struct kobj_map类型的散列表cdev_map来管理当前系统中的所有字符设备。
在这里插入图片描述

  • cdev结构体(内核源码/include/linux/cdev.h)

    struct cdev {
    	/* 内嵌的内核对象,通过它将设备统一加入到“Linux设备驱动模型”中管理(如对象的引用计数、电源管理、热插拔、生命周期、与用户通信等) */
    	struct kobject kobj;
    	
    	/* 字符设备驱动程序所在的内核模块对象的指针 */
    	struct module *owner;
    	
    	/* 文件操作,在应用程序通过文件系统(VFS)呼叫到设备设备驱动程序中实现的文件操作类函数过程中,ops起着桥梁纽带作用 */
    	/* VFS与文件系统及设备文件之间的接口是file_operations结构体成员函数,这个结构体包含了对文件进行打开、关闭、读写、控制等一系列成员函数 */
    	const struct file_operations *ops;
    	
    	/* 用于将系统中的字符设备形成链表(这是个内核链表的一个链接因子,可以再内核很多结构体中看到这种结构的身影) */
    	struct list_head list;
    	
    	/*  字符设备的设备号,有主设备和次设备号构成 */
    	dev_t dev;
    	
    	/* 属于同一主设备好的次设备号的个数,用于表示设备驱动程序控制的实际同类设备的数量 */
    	unsigned int count;
    };
    
  • 驱动链表
    管理所有设备的驱动,添加或查找
    添加是发生在我们编写完驱动程序,加载到内核。
    查找是在调用驱动程序,由应用层用户空间去查找使用open函数。

    驱动插入链表的顺序由设备号检索,就是说主设备号和次设备号除了能区分不同种类的设备和不同类型的设备,还能起到将驱动程序加载到链表的某个位置,在下面介绍的驱动代码的开发无非就是添加驱动(添加设备号、设备名和设备驱动函数)和调用驱动。

  • kobj_map
    管理当前系统中的所有字符设备

    static struct kobj_map *cdev_map;
    int kobj_map(struct kobj_map *, dev_t, unsigned long, struct module *,
    	     kobj_probe_t *, int (*)(dev_t, void *), void *);
    

4.1.4 设备节点 / 设备文件

Linux中设备节点是通过“mknod”命令来创建的
一个设备节点其实就是一个文件, Linux中称为设备文件。
有一点必要说明的是,在Linux中,所有的设备访问都是通过文件的方式, 一般的数据文件称为普通文件,设备节点称为设备文件

设备节点被创建在/dev下,是连接内核与用户层的枢纽,就是设备是接到对应哪种接口的哪个ID上。
相当于硬盘的inode一样的东西,记录了硬件设备的位置和信息在Linux中,所有设备都以文件的形式存放在/dev目录下, 都是通过文件的方式进行访问,

设备节点是Linux内核对设备的抽象,一个设备节点就是一个文件。

应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。

 

4.2 相关数据结构

4.2.1 file_operations结构体

file_operation就是把系统调用和驱动程序关联起来的关键数据结构。
这个结构的每一个成员都对应着一个系统调用

读取file_operation中相应的函数指针,接着 把控制权转交给函数指针指向的函数,从而完成了Linux设备驱动程序的工作。
在这里插入图片描述
在系统内部,I/O设备的存取操作通过特定的入口点来进行,而这组特定的入口点恰恰是由设备驱动程序提供的。
通常这组设备驱动程序接口是由结构file_operations结构体向系统说明的,它定义在**prj_switch\kernel\kernel-2.6.32-longterm\include\linux\fs.h(不同内核可能内容不同)**中。
传统上, 一个file_operations结构或者其一个指针称为fops(或者它的一些变体)结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作, 或者对于不支持的操作留置为NULL。
当指定为NULL指针时内核的确切的行为是每个函数不同的。

file_operations结构体(内核源码/include/linux/fs.h)

struct file_operations {
	struct module *owner;
	
	/* 用于修改文件的当前读写位置,并返回偏移后的位置 */
	/* 参数file传入了对应的文件指针,通常用于读取文件的信息,如文件类型、读写权限 */
	/* 参数loff_t指定偏移量的大小 */
	/* 参数int是用于指定新位置指定成从文件的某个位置进行偏移,SEEK_SET表从文件起始处开始偏移;SEEK_CUR表从当前位置开始偏移;SEEK_END表从文件结尾开始偏移 */
	loff_t (*llseek) (struct file *, loff_t, int);
	
	/* 用于读取设备中的数据,并返回成功读取的字节数 */
	/* 该函数指针被设置为NULL时,会导致系统调用read函数报错,提示“非法参数” */
	/* file类型指针变量,char__user*类型的数据缓冲区,__user用于修饰变量,表明该变量所在的地址空间是用户空间的 */
	/* 内核模块不能直接使用该数据,需要使用copy_to_user函数来进行操作!!!! */
	/* size_t类型变量指定读取的数据大小 */
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	
	/* 用于向设备写入数据,并返回成功写入的字节数 */
	/* 在访问__user修饰的数据缓冲区,需要使用copy_from_user函数 */
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	
	/* 提供设备执行相关控制命令的实现方法,对应于应用程序的fcntl函数以及ioctl函数 */
	/* 在 kernel 3.0 中已经完全删除了 struct file_operations 中的 ioctl 函数指针 */
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	
	/* 设备驱动第一个被执行的函数,一般用于硬件的初始化 */
	/* 如果该成员被设置为NULL,则表示这个设备的打开操作永远成功 */
	int (*open) (struct inode *, struct file *)
	
	/* 当file结构体被释放时,将会调用该函数,与open函数相反,该函数可以用于释放 */
	int (*release) (struct inode *, struct file *);

};

上面,我们提到read和write函数时,需要使用copy_to_user函数以及copy_from_user函数来进行数据访问,写入/读取成功函数返回0,失败则会返回未被拷贝的字节数。
copy_to_user和copy_from_user函数(内核源码/include/asm-generic/uaccess.h)

static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
/* to:指定目标地址,也就是数据存放的地址 */
/* from:指定源地址,也就是数据的来源 */
/* n:指定写入/读取数据的字节数 */

4.2.2 file结构体

内核中用file结构体来表示每个打开的文件,每打开一个文件,内核会创建一个结构体,并将对该文件上的操作函数传递给该结构体的成员变量f_op
当文件所有实例被关闭后,内核会释放这个结构体。

如下代码中,只列出了我们本章需要了解的成员变量。
file结构体(内核源码/include/fs.h)

struct file {
	const struct file_operations *f_op;
	
	/* needed for tty driver, and maybe others */
	/* 该指针变量只会用于设备驱动程序中,内核并不会对该成员进行操作。因此,在驱动程序中,通常用于指向描述设备的结构体 */
	void *private_data;
};

4.2.3 inode结构体

VFS inode包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。
它是Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁

内核使用inode结构体在内核内部表示一个文件。
因此,它与表示一个已经打开的文件描述符的结构体(即file 文件结构)是不同的,我们可以使用多个file文件结构表示同一个文件的多个文件描述符
但此时, 所有的这些file文件结构全部都必须 只能指向一个inode结构体
inode结构体包含了一大堆文件相关的信息,但是就针对驱动代码来说,我们只要关心其中的两个域即可:

dev_t i_rdev:
	表示设备文件的结点,这个域实际上包含了设备号。

struct cdev *i_cdev:
	struct cdev是内核的一个内部结构,它是用来表示字符设备的,
	当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。

 


五、总结

Linux根目录下有 /dev 这个文件夹,专门用来存放设备中的驱动程序。
使用设备编号来表示设备,主设备号区分设备类别(使用不同驱动程序),次设备号标识具体的设备。

在内核中,dev_t用来表示设备编号,dev_t 是一个32位的数,其中,高12位表示主设备号,低20位表示次设备号。

内核用struct cdev结构体来描述一个字符设备,并通过struct kobj_map类型的散列表cdev_map来管理当前系统中的所有字符设备。
在这里插入图片描述

struct cdev {
	/* 内嵌的kobject对象 */
	struct kobject kobj;
	/* 所属模块 */
	struct module *owner;
	/* 文件操作结构体 */
	const struct file_operations *ops;
	/* 链表句柄 */
	struct list_head list;
	/* 设备号 */
	dev_t dev;
	unsigned int count;
};

Linux中设备节点是通过“mknod”命令来创建的,一般的数据文件称为普通文件,设备节点称为设备文件

file_operation就是把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用。
传统上,一个file_operations结构或者其一个指针称为fops(或者它的一些变体)结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作, 或者对于不支持的操作留置为NULL。

VFS inode是 Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。
内核使用inode结构体在 内核内部表示一个文件。
使用多个file文件结构表示同一个文件的多个文件描述符,但此时,所有的这些file文件结构全部都必须 只能指向一个inode结构体 。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值