字符设备驱动程序

字符设备驱动程序

  • 大多简单的硬件设备都依赖于字符设备驱动程序
  • 参考例程: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:次设备号基值、设备号范围

动态分配主设备号

重要的数据结构

  • 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宏

字符设备注册:

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

posted @ 2024-05-22 22:14  David_Dong  阅读(13)  评论(0编辑  收藏  举报