三 字符设备驱动

  1. 申请分配主设备号

  2. 为特定设备相关的数据结构分配内存

  3. 将入口函数(open、read等)与字符驱动程序的cdev抽象相关联

  4. 将主设备号与cdev相关联

  5. 在/dev和/sys下创建节点

  6. 初始化硬件

对字符设备(c)的访问是通过文件系统内的设备名称进行的,那些名称被称为特殊文件、设备文件、或者文件系统树的节点,它们通常位于/dev目录

1 设备编号

主设备号:高12位,用来区分设备类型,标识设备对应的驱动程序

例如:/dev/null 和/dev/zero由驱动程序1管理

次设备号:由内核使用,区分同类型的不同设备,用于正确确定设备文件所指的设备。可以通过次设备号获得一个指向内核设备的直接指针。也可以将次设备号当做设备本地数组的索引。


image-20211029173144129.png

内核用dev_t类型来保存设备编号,

1.1 dev_t

设备编号的内部表达

在内核中,dev_t类型用来保存设备编号:包括主设备号和次设备号

 dev_t :主设备号12位 + 次设备号20位

1.2 生成设备号

linux提供相应的宏实现dev_t 与设备号之间的转换。

 #include<linux/kdev.h>
 MAJOR(dev) // 从dev_t中获取主设备号
 MINOR(dev) // 获取次设备号
 MKDEV(ma,mi) // 通过主设备号ma和次设备号mi生成dev_t 

1.3 分配设备编号

建立一个字符设备之前,驱动程序首先要做的事情就是获得一个或多个设备编号

  • 用户提前知道设备编号---register_chrdev_region()

  • 用户不知道设备编号---alloc_chrdev_region()

1.3.1 register_chrdev_region()

 register_chrdev_region(dev_t first,unsigned int count,char *name)
 参数:
  firet:要分配的设备编号范围的初始值(first的次设备号常设为0)
  count:连续编号的个数
  name:和该范围相关的设备名称,将出现在/proc/devices 和sysfs中
 返回值:成功:0
  失败:-EFAULT

缺点:驱动被广泛应用时,随机选定的主设备号可能会导致设备号冲突,而使得驱动程序无法注册

1.3.2 alloc_chrdev_region()

 int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);
 函数功能:动态分配
 参数:
  dev:用于输出,在成功完成调用之后,将保存已分配范围的第一个编号
  firstminor:要使用的被请求的第一个次设备号,通常为0
  count:所请求的连续设备编号的个数
  name:和该编号关联的设备名称,它将出现在/proc/devices和 sysfs中

缺点:分配的主设备号不能保证始终一致,所以无法预先创建设备节点

1.4 unregist_chrdev_region()

释放设备编号:删除设备链表中的元素

 void unregist_chrdev_region(dev_t from,unsigned int count);
 参数:
  from:设备号
  count:要卸载的个数 
 unregister_chrdev()

2 数据结构

2.1 struct file_operation

 /* 指针:指向模块拥有者,一般为THIS_MODULE 驱动程序入口地址,知晓结构拥有者的身份可以让内核帮助管理*/
  struct module *owner;
 
 /* mmap就是建立内核空间映射到用户空间虚拟地址上,之后,应用程序直接访问映射后虚拟地址,实际是在访问内核空间 */
 int (*mmap) (struct file *, struct vm_area_struct *);
 
 /* 打开设备 */
 int (*open) (struct inode *, struct file *);  
 
 /* 关闭设备 */   
 int (*release) (struct inode *, struct file *);  

 __user 表示指针是用户空间指针,不能被直接引用。

2.2 struct file

内核结构,不会出现在用户程序中。代表一个打开的文件(不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间中都有一个对应的file结构)

指向struct file的指针通常被称为filp

2.3 struct inode

内核用inode结构在内部表示文件。

struct file表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但是它们都指向单个inode结构

常规,通常只有

 dev_t   i_rdev;// 对表示设备文件的inode结构,该字段包含了真正的设备编号。
 struct  cdev  *i_dev;// 表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。

2.4 struct cdev

描述字符设备的抽象。

 //#include<cdev.h>
 struct cdev {
  struct kobject kobj;
  struct module *owner;
  const struct file_operations *ops;
  struct list_head list;
  dev_t dev;
  unsigned int count;
 };

对应API

file_operation:cdev_init

dev_t : cdev_add()

3 字符设备注册

3.1 cdev_init()

建立cdev和file_operations之间的连接

void cdev_init(struct cdev *cdev, struct file_operations *fops);
my_cdev->owner = THIS_MODULE;

函数原型:

image-20211109172139434.png

3.2 cdev_alloc()

// 分配
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;

3.3 cdev_add()

将主/次设备号和cdev关联

// 通知内核cdev的消息
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
参数:num是设备对应的第一个设备编号
    count和该设备关联的设备编号的数量,通常取1
注意:在驱动程序上没有完全准备好处理设备上的操作时,不能调用cdev_add();   

3.4 移除设备

void  cdev_del(struct  cdev *dev);

示例:

image-20211109172102076.png

4 类

版本:2.6.26之后

驱动中加入对sysfs + udev的支持

头文件 /include/linux/device.h

流程:在驱动初始化代码里

  • 调用class_create()为该设备创建一个class

  • 为每个设备调用devide_create()


属于应用层,不要试图在内核的配置选项里找到它,加入对udev的支持很简单,以字符设备驱动为例,在驱动初始化的代码里调用class_create()为该设备创建一个class,再


4.1 struct class

用来创建类,存放于sysfs下面

4.2 class_create()

为设备创建sysfs入口点

struct class shm_class;//共享内存类
shm_class = class_create(THIS_MODULE,"shm_class");
参数1:指定类的所有者是哪个模块
参数2:指定类型名
返回值:

4.3 class_destroy()

void class_destroy(struct class *cls)

5 设备节点

5.1 device_create()

在/dev目录下创建相应的设备节点。

加载模块时,要用户空间中的udev会自动响应device_create()函数,去/sysfs目录下寻找对应的类,从而创建设备节点。

函数功能:
    寻找对应的类从而创建设备节点
extern struct device *device_create(struct class *cls, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)

参数:
   cls:该设备从属的类
   parent:该设备的父设备,NULL:表示无
   devt:设备号
   drvdata:设备名称
   fmt:从

5.2 device_destory()

 void device_destroy(struct class *dev, dev_t devt);

3.2 device_create_file()

建立设备属性文件。

函数功能:
    在/sys/class/下创建一个属性文件,从而通过这个属性进行读写就能完成对应的数据操作
函数原型:
    int device_create_file(struct device *, struct device_attribute *)

原理:

  • DEVICE_ATTR宏创建一个名为dev_attr_##_name的属性结构,dev_attr_##_name用于device_create_file()
#include </linux/device.h>
    #define DEVICE_ATTR(_name, _mode, _show, _store)
            struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
  • 初始化device_attribute结构体的宏
#include </linux/sysfs.h>

#define __ATTR(_name,_mode,_show,_store) { 
    .attr = {.name = __stringify(_name), .mode = _mode },  
    .show   = _show,                                        
    .store  = _store,                                       
}
  • struct device_attribute 的定义
#include </linux/device.h>

    /* interface for exporting device attributes */
    struct device_attribute {
        struct attribute    attr;
        // 函数指针
        ssize_t (*show)(struct device *dev, struct device_attribute *attr,
                char *buf);
        ssize_t (*store)(struct device *dev, struct device_attribute *attr,
                 const char *buf, size_t count);
    };

3.3 device_remove_file()

4 数据交互

fop的.read和.write是负责在用户空间和设备指教交换数据的主要字符驱动函数。

驱动write: 数据下传

驱动read:数据上送

读写函数系列扩展:

  • fsync()

  • aio_read() / aio_write()

  • mmap()

需要注意的问题:

  • 访问时是否需要等待设备I/O结束?------》阻塞和非阻塞操作

  • 很多驱动程序的数据访问函数依靠中断来获取数据,并且需要通过等待队列等数据结构和中断上下文代码来通信

  • 内核不能直接访问用户空间的缓冲区,反之亦然.

    read()需要借助copy_to_user()

    write()需要借助copy_from_user()

4.1 设备I/O

原理:对于只支持内存映射的I/O寄存器的计算机体系架构:<mark style="box-sizing: border-box; background: rgb(255, 255, 0); color: rgb(0, 0, 0); text-indent: 0px;">通过把I/O端口地址重新映射到内存地址来伪装端口I/O</mark>

访问I/O端口的内联函数:

单数据传输

一次传输一个数据

/*8bit,字节读写端口*/
void outb(unsigned char byte,unsigned port);
// port: I/O地址,虚拟地址    
unsigned inb(unsigned port);

inw();//字,16bit
outw()

inl();
outl();//双字, 32bit

即使是在64位的体系架构上,端口地址空间也只使用最大32bit的数据通路

在用户空间访问I/O端口的前提:

  • 编译程序时必须带-O 选项来强制内联函数的展开

串操作

作为补充,有些处理器上实现一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字。这些指令称为串操作指令。

image-20221012134658675.png

4.2 数据同步

4.2.1 fsync()

驱动程序完成数据下传,并不能保证数据成功写入设备中,如果应用程序要确保成功,可以调用fsync()。

fsync()驱动程序确保数据从驱动程序缓冲区中排出,并且写到设备。

4.2.2 异步I/O(AIO)

用户数据存储在多个缓冲区并需要发送至设备,驱动需要从不同缓冲区手机数据,并将其分发至设备。

image-20221012155717938.png

struct iovec

包含数据缓冲区的地址和长度

4.3 mmap

将设备内存和用户的虚拟内存关联,应用程序可以调用相应的系统调用,也可以调用mmap(),直接在返回的内存区操作,以访问设备驻留的内存。

4.4 ioctl

当应用程序需要请求某些设备特定的动作时,这个例程接收并实现应用程序的命令。

4.4.1 示例-CRC

CRC:循环冗余校验

  1. 调整校验和:CMOS内容被修改后重新计算CRC,计算出的校验和被存储在CMOS存储体预先指定的偏移上

  2. 验证校验和:用于检查CMOS内容是否完好。通过比较针对当前内容计算出的CRC和以前存储的CRC来完成

5 轮询

当设备上有数据到来,或者驱动程序准备好接收新数据时,系统最好能够采用同步或异步的方式通知

5.1 同步-poll()

poll_table结构

内核数据结构,表示等待队列,由被轮询等待数据的设备驱动程序所拥有

poll_wait()

为内核poll_table添加一个等待队列后休眠。

5.2 异步-fasync()

考虑性能,一些应用程序需要以异步方式获得设备驱动程序的通知

5 程序结构

(1)初始化例程

(2)入口函数集——file_operation

(3)中断例程、底半部例程、定时器处理例程、内核辅助线程以及其他组成部分(对用户空间透明)

字符设备驱动初始化工作

  1. 申请分配主设备号

  2. 为特定设备相关的数据结构分配内存

  3. 将入口函数(open、read等)与字符驱动程序的cdev抽象相关联

  4. 将主设备号与cdev相关联

  5. 在/dev和/sys下创建节点

  6. 初始化硬件

    主要是硬件资源的申请与配置,主要涉及地址映射,寄存器读写等相关操作。

    ioremap()将物理地址映射成虚拟地址。

image-20211109160924103.png

在linux内核中,采用cdev描述字符设备,成员dev_t 来定义设备号,以确定字符设备的唯一性,通过成员file_operations来定义字符设备驱动提供给VFS的接口函数

在字符设备驱动中,模块加载函数(register_chrdev_region()或者alloc_chrdev_region())来静态或者动态获取设备号,通过cdev_init()建立cdev与file_operations之间的连接,通过cdev_add()向系统添加一个cdev来完成注册。模块卸载函数cdev_del()来注销cdev,通过unregister_chrdev_region()来释放设备号。

用户空间访问该设备的程序通过linux系统调用,同名调用file_operations。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容