字符设备
对字符设备的访问是通过文件系统内的设备名称进行的,这些名称被称为特殊文件/设备文件,通常位于/dev目录,这些字符设备在ls -l
的情况下第一列显示为c来识别
设备文件项最后的修改日期前的两个用逗号分隔的数代表了设备的主设备号和次设备号:
主设备号标识设备对应的驱动程序,例如上图中的vsc1和vsc2等驱动都被驱动程序7管理
次设备号被内核使用,用于确定设备文件所指的设备,我们可以通过次设备号获得一个指向内核设备的直接指针,也可以将次设备号当作设备本地数组的索引
设备编号的内部表达
在内核中dev_t
类型来保存设备编号,包括主设备号和次设备号,要获取dev_t的主设备号和次设备号,可以通过<linux/kdev_t.h>
中定义的宏获取:
MAJOR(dev_t dev); // 获取主设备号
MINOR(dev_t dev); // 获取次设备号
MKDEV(int major, int minor); // 构建dev_t结构体
分析scull内核驱动
scull驱动程序可以在这里下载:https://github.com/freestyl3r/kernel-drivers
初始化设备
scull设备的初始化在scull_init_module
这个函数中,在字符设备驱动程序中,在驱动初始化的时候应该向内核注册好对应的字符设备
0x01 初始化设备号
在内核中,设备号由dev_t
这个结构来描述,内核提供了两个宏MAJOR(dev_t dev);
和MINOR(dev_t dev)
;来分别获取主设备号和次设备号
内核也提供了MKDEV(ma,mi)
来创建dev_t
结构体
内核提供了两个函数来注册字符设备:int register_chrdev_region(dev_t, unsigned, const char *);
和int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
,前者为老版本的函数,需要自己选择设备的主设备号,但是由于已经有一些主版本号被使用,所以自己选择的主设备号不能冲突。后者为新函数,该函数传入一个dev_t
的结构体,在调用结束后会注册字符设备并且自动分配一个主设备号
对于alloc_chrdev_region
函数,第二个参数为次设备号的开始,第三个参数为有多少个次版本号(会生成如scull0、scull1、scull2…)这样的设备,最后一个参数为设备的名称
如上的scull中注册设备号的代码,就会注册scull0 - scull3的4个设备
0x02 初始化每个设备
在内核中,每个字符设备使用cdev
来描述,实际open,read,write等系统调用操作的也是一个个的cdev。可以使用内核内存分配函数kmalloc来初始化一片cdev的结构数组,在scull中,使用scull_dev
作为cdev的一个包装,用来存储数据
在scull中,先调用kmalloc申请了一篇4个scull_dev大小的内存区域并初始化为0,之后对每个scull_dev进行初始化
想要初始化cdev结构,需要调用void cdev_init(struct cdev *, const struct file_operations *);
,cdev_init
的第一个参数是注册设备的dev_t
函数,我们可以使用MKDEV来获取之前注册的dev_t
结构。第二个参数是对应设备支持的操作,这里是一种面向对象的思想,file_operations
中定义了一系列的函数指针,这些函数指针代表了这个设备支持的所有操作,类似于面向对象思想中的接口,每一个注册的字符设备都需要实现这些方法:
可以看到里面有很多我们熟悉的如open、read等方法,设备需要实现这些方法使得设备可以和用户空间的程序交互,scull实现了其中的一些方法供调用
在设备初始化之后,就可以调用cdev_add
将设备添加进去了
open方法
在内核中,file结构用来表示打开文件,这里的file结构为内核数据结构,用户空间不可见,其中的private_data可以被用来存储一些跨系统调用的状态信息,内核会在每次open之前将private_data初始化为NULL
内核中还有一个重要的数据结构是inode结构,inode是实际指向文件的结构,多个打开文件的文件描述符file结构都指向同一个inode结构,在inode中保存了dev_t
和cdev
结构
由于open
函数传来的inode虽然包含cdev的结构,但是驱动中实际使用的是包装过的scull_dev
,内核提供了container_of
函数来获取对应的scull_dev
结构:
为了后续访问方便,可以将private_data
设置为实际的scull_dev
结构
其中如果filp->f_flags
是写入的话,那么就需要将设备的数据区相应缩小为0(类似写文件的时候,会覆盖掉之前的旧文件)
read和write方法
read和write方法用来向设备读取值或者向设备写入值,因为这里有用户空间和内核空间的数据交换,read和write的第二个参数都是用户空间的缓冲区指针,但是由于内核的代码没有虚拟内存的保护,直接在驱动中访问用户空间的指针可能会导致页错误。而且直接使用用户空间的指针而不检查的话可能会导致内核的漏洞。
内核提供了两组函数用于分别将用户空间的数据拷贝到内核空间,或者将内核空间的数据拷贝到用户空间,这两组函数在投文件<asm/uaccess.h>
中定义:
copy_from_user:从from(用户空间地址)读取n个字节到to指定的内存
copy_to_user:从from读取n个字节到to(用户空间地址)指定的内存
卸载设备
在驱动的exit函数中,需要对init函数中所申请的内存进行free(kfree),并且卸载cdev结构(cdev_del),取消对字符设备的注册(unregister_chrdev_region)
scull中的设备卸载比较简单:
编译驱动
因为scull的代码比较老,在新内核中已经弃用了file_operations
转而使用了proc_ops
,所以我们需要对scull的代码进行一定的修改才能编译,proc_ops
的结构基本上一致: