Character device drivers
实验室目标:
- 了解字符设备驱动程序背后的概念
- 了解可以在字符设备上执行的各种操作
- 使用等待队列
设备驱动程序是与硬件设备交互的内核组件(通常是模块)
在UNIX中有两类设备文件:
- 第一类设备,字符设备:(例如:键盘、鼠标、串行端口、声卡、操纵杆)
- 慢速设备
- 管理少量数据
- 访问数据不需要频繁的查找查询
- 通常,这些设备的 Read/Write 是按字节顺序执行
- 第二类设备,块设备:(例如:硬盘驱动器、光盘、RAM 磁盘、磁带驱动器)
- 数据量大
- 数据按块组织
- 搜索频繁
- 对于这些设备,Read/Write 是在数据块级别完成的
因此,UNIX 提供了两种设备驱动程序:
- 字符驱动 - character driven
- 块驱动 - block driven
- PS:Linux中还有一种网络驱动 - plot driven(不是本篇文章的重点)
对于这两种类型的设备驱动程序,Linux 内核提供了不同的 API,其中大多数参数都有直接含义:
file
和inode
:标识设备类型文件size
:要读取或写入的字节数offset
:要读取或写入的位移(将相应更新)user_buffer
:从中 Read/Write 的用户缓冲区whence
:搜索方式(搜索操作开始的位置)cmd
和arg
:用户发送到 ioctl 调用的参数(IO控制)
Majors and Minors
Linux 中,设备具有与之关联的唯一固定标识符,由两部分组成:major and minor
- major:标识设备的类型(IDE 磁盘、SCSI 磁盘、串行端口等)
- minor:标识具体的设备(第一个磁盘、第二个串行端口等)
PS:因为物理设备已经被驱动抽象为“在 Linux 上运行的软件”,所以 Linux 可以通过这种方式定位具体的物理设备
Inode and File
从文件系统的角度来看,inode
表示文件:
inode
的属性是与文件关联的大小,权限,时间inode
唯一标识文件系统中的文件
从用户的角度来看,file
表示文件:
file
的属性是inode
,文件名,文件打开属性,文件位置- 所有打开的文件都有与之关联的
file
结构体
回到设备驱动程序,有两个实体几乎总是具有标准的使用方式:
inode
:更用于确定执行操作的设备的 major and minorfile
:用于确定打开文件的标志,还用于保存和访问(以后)私有数据
Registration and unregistration of character devices
设备的注册/注销是通过指定 major and minor 设备来实现的
- 类型
dev_t
用于保留设备的标识符(major and minor),并且可以使用 MKDEV 宏获取
对于设备标识符的静态分配和静态注销:
1 |
|
分配标识符后,必须初始化字符设备并且必须通知内核,然后才能注册/删除字符设备:
1 |
|
使用案例如下:
1 |
|
同一个 dev_t
可以注册多个字符设备,每次 open(DEVICE_PATH, O_RDONLY)
时,本质上是和一个具体的字符设备进行交互(使用 struct cdev
父类结构体中的数据)
因此,如果两个进程访问同一个字符设备,就很可能在临界区引发安全问题,所以我们要在字符设备的 open
上加锁,禁止其被二次打开
为了程序的并发性,通常我们需要为同一个 dev_t
注册多个字符设备,在进程 open
提供不同的字符设备供其使用
Access to the address space of the process
设备的驱动程序是应用程序和硬件之间的接口,因此,我们经常必须访问用户空间数据(但不能以取消引用用户空间指针的方式,来直接访问用户空间)
直接访问用户空间指针可能会导致:
- 不正确的行为(根据体系结构的不同,用户空间指针可能无效或映射到内核空间)
- 内核 oops(用户模式指针可以引用非驻留内存区域)
- 安全问题
因此通过调用下面的宏函数来正确访问用户空间数据:
1 |
|
下图说明了 Read 操作以及如何在用户空间和驱动程序之间传输数据:
- 当驱动 driver 有足够多的可用数据时,它将准确地将所需 size 的数据传输给用户
- 当驱动 driver 没有足够多的可用数据时,它将把所有的可用数据传输给用户
Read 操作的案例:
1 | static int my_read(struct file *file, char __user *user_buffer, |
下图说明了 Write 操作以及如何在用户空间和驱动程序之间传输数据:
- 写入操作将响应来自用户空间的写入请求,其范围不会大于最大驱动程序容量 MAXSIZ
Write 操作的案例:
1 | static int my_write(struct file *file, const char __user *user_buffer, |
Ioctl
除了 Read 和 Write 操作之外,驱动程序还需要能够执行某些物理设备控制任务(这些操作是通过实现函数来完成的)
可用通过如下函数完成此操作:
1 | static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg); |
file
:打开的设备文件描述符cmd
:从用户空间发送的命令arg
:指向用户空间的指针,使用copy_from_user
来安全地获取其值
在实现该功能之前,必须选择与命令对应的数字(建议使用宏定义 _IOC(dir, type, nr, size)
来完成此操作),然后在一个 Switch-Case 中完成各个命令
使用案例如下:(在用户空间调用 ioctl
,对应到内核就是 my_ioctl
)
1 |
|
Waiting queues
等待队列是正在等待特定事件的进程的列表,使用 wait_queue_head_t
类型定义,可由函数/宏使用:
1 |
|
Exercises
要解决练习,您需要执行以下步骤:
- 从模板准备 skeletons
- 构建模块
- 将模块复制到虚拟机
- 启动 VM 并在 VM 中测试模块
1 | make clean |
直接看最终代码:
1 | /* |
- 在用户态执行的测试代码:
1 | /* |
- 结果:
1 | root@qemux86:~/skels/device_drivers/kernel |
1 | root@qemux86:~/skels/device_drivers# ./user/so2_cdev_test p |
1 | root@qemux86:~/skels/device_drivers# ./user/so2_cdev_test g |
1 | root@qemux86:~/skels/device_drivers |
感觉在锁和等待队列这一块还不是很熟悉,还需要多写代码