字符设备驱动程序
字符设备驱动程序
- 大多简单的硬件设备都依赖于字符设备驱动程序
- 参考例程:scull驱动程序
- 注: 本笔记的内核以4.9.88版本为主
scll设计
- 设计驱动程序的第一步: 定义驱动程序能够提供的机制,即实现设备的抽象
- 源代码实现:
- scull0 ~ scull3: 由全局(多次打开共享数据)且持久(设备关闭后打开数据不会丢失)的内存区域;
- scullpipe0 ~ scullpipe3: FIFO管道,实现了不借助于中断的阻塞/非阻塞读写;
- scullsingle、scullpriv、sculluid、scullwuid: 其与scull0类似,但是实现了单进程限制(scullsingle)、控制台/会话私有(scullpriv)、限制单用户打开(sculluid、scullwuid)并返回错误(sculluid)或阻塞(scullwuid)。
主次设备号
- 设备文件(/dev):在
ls -l
输出的第一列中,字符设备为“c”,块为“b”; - 主次设备号位置:修改日期前的两个数
- 一般而言,主设备号表示驱动程序,次设备号指向实现设备(但是其实现实中没有这么严格,可以通过划分主次设备号来实现分区)
设备号内部表达
- <linux/types.h>中,32位数,前12位主设备号,后20位次设备号。
typedef __u32 __kernel_dev_t; typedef __kernel_dev_t dev_t;
- 主次设备号(int)和内部表达的转换:
MAJOR(dev_t dev); MINOR(dev_t dev);
与MKDEV(int major, int minor)
;
分配与释放设备号;
- 驱动程序注册设备获得设备号:声明:<linux/fs.h>,实现<fs/char_dev.c>
int register_chrdev_region(dev_t from, unsigned count, const char *name)
param:起始设备号、设备号范围、设备名称int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
param:设备号返回值、次设备号基值、设备号范围、设备名称
- 驱动程序析构时需要释放设备号:
void unregister_chrdev_region(dev_t from, unsigned count)
param:次设备号基值、设备号范围
动态分配主设备号
- 已分配的主设备号清单:Documentation/device.txt
- 获取动态分配的设备号:/proc/devices或/sys
- 在未使用设备树时,需要使用mknod对相应设备号进行申请并读取,其从/proc/devices中读取设备号并使用mknod注册、之后修改相应权限和设备的组。代码:https://github.com/duxing2007/ldd3-examples-3.x/blob/linux-4.9.y/scull/scull_load
- init脚本,其从配置文件读取:https://github.com/duxing2007/ldd3-examples-3.x/blob/linux-4.9.y/scull/scull.init
重要的数据结构
- file_opearations、file、inode
- 定义位置:<linux/fs.h>
file_opearations
- 其本质是一种fops
__user
前缀表示其为用户空间地址,要使用copy from user去完成- 核心数据结构:
- *owner:防止模块操作中释放,一般等于THIS_MODULE
- llseek:修改读写位置;
- read:读、read_iter:异步读(4.1版本后)
- write:写、write_iter:异步写(4.1后)
- poll:poll、epoll和select后端实现(查询读写阻塞情况)
- unlocked_ioctl:不持有文件所的ioctl(执行设备特定指令)提高并发效果、compat_ioctl(向后兼容的ioctl,如54位兼容32位)
- mmap:设备内存映射;
- open:打开设备文件,可以不是实现,但是系统不会通知应用程序
- flush:进程关闭设备文件描述符副本时,执行并等待设备操作,如果为NULL,内核会忽略用户请求;
- release:释放file结构体,和open对应;
- fsync:刷新数据(常用的是内存数据刷入磁盘)
- fasync:异步通知;
- lock:文件锁定;
- sendpage:发送数据到文件
- get_unmapped_area:获取在进程地址空间合适位置,便于底层设备内存段的映射(通常由内存管理实现);
- check_flags:查看fcntl的标志;
- flock:文件锁(整个文件)
- splice_write、splice_read:零拷贝读写
- 其他:setlease、fallocate、show_fdinfo、mmap_capabilities、copy_file_range、clone_file_range、dedupe_file_range;
file
- 注:其是一个内核结构,不会出现在用户空间中;
- 其代表了一个打开的文件,在open时被创建,close被销毁。其指针为filp
- 重要成员:
- f_mode:模式(读/写)
- f_pos:当前读写位置;
- f_flags:文件标志、阻塞\非阻塞等
- *f_op操作结构体,其允许方法重载;
- *private_data:调用时保存状态信息,常用于open和其他函数之间的信息传递
- *f_inode:所对应的目录项结构
inode结构:
- 对文件系统的信息,其在文件系统表示实际文件(可能被多个file所指向)
- 重要成员:
- i_rdev:设备文件的设备编号;
- *i_cdev:表示字符设备的内核结构,其与块设备、管道等同时放在一个联合体中以节省内存
- 从inode中获取设备号:iminor宏和imajor宏
字符设备注册:
- 核心代码<linux/cdev.h>
*=cdev_alloc()申请cdev-> *->ops = &fops;分配操作 -> *->owner = THIS_MODULE初始化 ;cdev_init()初始化结构 -> cdev_add()加入内核(完成后会被系统调用,因此最后调用)
cdev_del()删除cdev结构体 - 参考代码:https://github.com/duxing2007/ldd3-examples-3.x/blob/linux-4.9.y/scull/main.c#L611
- 早期方法:register_chrdev和unregister_chrdev
open和release
open:
- 完成工作:初始化设备,检查特定错误/首次打开应该初始化/更新f_ops指针/分配填写filp->private_data中数据结构;
- 可以使用宏container_of() 将传入inode中的cdev提取出来并提取到设备结构体中,之后将dev指针放入private_data中;
- 如果使用register, 应该检查inode中主次设备号是否与预期一致(使用iminor宏和imajor宏);
- 对于scull而言,由于其不维护打开计数,只维护使用计数,因此没有设备初始化;同时以写方式打开时,其长度被截为0;
release:
- 完成工作: 释放open分配的内容(filp->private_data中)/最后一次关闭操作时关闭设备
- 驱动程序
- scull:无,其没有需要关闭的硬件。
scull的内存使用(本质是对内存的管理)
- scull的设备:其使用的内存区域,长度可变;
- 使用Linux内存管理核心函数kmalloc与kfree,其位置在<linux/slab.h>
- 对大型页面的分配要使用分配页面的相应操作
- 设备:指针链表,其指向scull_qset结构,一个scull指针指向一个指针数组(1000个),数组指针指向一个内存区域(量子),大小为4000字节。其结构类似链表+页表的形式。量子大小可以通过修改宏(编译)、修改设置(加载),使用iotcl(运行)。
- 量子大小的默认策略:大多数情况下只有几kb传递;
- 代码结构:使用scull_qset结构体(二级指针+链表指针)作为链表节点,使用scull_dev保存设备信息+链表头结点。
read和write
-参数: 文件指针,用户缓存,数据长度,偏移量
- 注意: 在访问用户缓存时,要使用copy_to_user和copy_from_user实现,保护操作系统,其定义在<asm/uaccsee.h>
- 注意: 要求访问用户空间的任何函数都必须是可重入的,且能够并发执行,其必须能够处于能够合法休眠的状态。这是由于用户空间进程调度决定的。
- 在已知参数已经检查过时,可以调用内核版本(加入__前缀);
- read和write返回值:成功传输字节数,这要求驱动程序必须记住错误的发生。用户空间可以通过访问errno变量查看出错原因;
read
- 返回值:等于count-成功完成;正的但是小于count-未读取完全,需要重新读取;0-已经到达文件尾;负值-发生错误<linux/errno.h>中实现
- scull实现:每次调用只处理单一数据量子,超出设备大小返回0,当进程A读取设备时,进程B写入,进程A读取会被截断并返回0。
write
- 返回值:等于count-成功完成;正的但是小于count-未写入完全,需要重新写入;0-什么也没写入(阻塞);负值-发生错误<linux/errno.h>中实现
- scull实现:每次调用只处理单一数据量子,并在写入完成后对文件大小进行更新;写入时使用kmalloc申请空间并使用memset进行初始化;注意在写入时候,该程序会将大于一个量子数据缩小为单一量子大小(可能会影响数据完整性)
readv和writev(现在已经脱离了fop结构体,由vfs_readv来实现,最终会调用read_iter和write_iter)
- 输入:文件指针 + iovec传输数据块(定义在<linux/uio.h>文件中,其包括用户空间起始地址和长度) + 数据块数量 + 偏移量
- 驱动程序实现:其iovec的数据被iov_iter中的联合体接收,其他被kiocb结构体接收;
参考文献:
ldd3源代码的各版本实现:https://github.com/duxing2007/ldd3-examples-3.x/tree/linux-4.9.y/scull
内核新的ioctl方式--unlocked_ioctl和compat_ioctl(解决error:unknown field 'ioctl' specified in initializer):
https://blog.csdn.net/gatieme/article/details/71437163
Linux flock()函数--文件锁:https://blog.csdn.net/wteruiycbqqvwt/article/details/112672627
编译驱动时error: ‘struct file’ has no member named ‘f_dentry’:
https://blog.csdn.net/hn2zzzz1996/article/details/79496282
Linux x86_64系统调用简介:https://evian-zhang.github.io/introduction-to-linux-x86_64-syscall/index.html