字符设备驱动内部实现

Last edited
Last updated July 21, 2023
Pages
Tags
💡
在之前的字符设备驱动实验中,使用了 register_chrdev 来注册设备驱动,但是这个接口自动会为主设备占用掉 256 个次设备的资源,而使用其他接口手动注册的方式则可以节省这些资源。

1. 字符设备驱动内部实现框图

notion image
  1. 所有的 Linux 文件都有一个自己的 inode 号,通过 ls -i 可以查看
  1. 驱动设备文件也不例外,这个 inode 号对应内核内存中的 inode 结构体:linux/fs.h 中的 inode 结构体源码
  1. inode 结构体中有一个 dev_t i_rdev 的属性,这个属性是一个指针,执行驱动的结构体,例如字符设备驱动 cdev 结构体:linux/cdev.h 的源码
  1. cdev 结构体拥有 file_operations 的属性,它保存了驱动的读写等回调方法
详细分析见第 5,第 6 小节。

2. 字符设备驱动分步注册步骤

  1. 为字符设备驱动对象申请空间
  1. 初始化字符设备驱动对象
  1. 注册字符设备驱动对象
  1. 注销字符设备驱动对象

3. 相关 API

相关的 API 接口都在 #include <linux/cdev.h>

3.1 驱动对象结构体

struct cdev { struct kobject kobj; struct module *owner;// 模块对象指针 THIS_MODULE const struct file_operations *ops;// 操作方法结构体指针 struct list_head list;// 用于构成链表的成员 dev_t dev;// 设备号 unsigned int count;// 设备资源数量 }

3.2 字符设备驱动对象申请空间

struct cdev cdev; // 栈区 struct cdev *cdev_alloc(void); // 堆区
功能:手动申请字符设备驱动对象空间
参数:无
返回值:成功返回申请到的对象空间首地址,失败返回 NULL

3.3 初始化设备驱动对象

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能:实现字符设备驱动对象的部分初始化
参数
  • cdev:字符设备驱动对象指针
  • fops:操作方法结构体指针

3.4 申请设备资源和设备号

int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:用于静态指定设备号
参数
  • from:申请的第一个设备资源的设备号
  • count:申请的设备资源的数量
  • name:驱动的名字
返回值:成功返回 0,失败返回错误码 MINOR(dev)
👉
[56692.673353] CHRDEV "myled" major requested (800) is greater than the maximum (511) 在我的 Ubuntu 23.04 以及开发板上,最大可指定主设备号是 511。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
功能:用于动态申请设备号
参数
  • dev:存放设备号的变量首地址
  • baseminor:申请的设备资源的数量
  • count:申请的设备资源的数量
  • name:驱动的名字
返回值:成功返回 0,失败返回错误码

3.5 注册字符设备驱动对象

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
功能:用于注册字符设备的驱动对象
参数
  • p:字符设备驱动对象指针
  • dev:设备号
  • count:设备数量
返回值:成功返回 0,失败返回错误码

3.6 注销字符设备驱动对象

void cdev_del(struct cdev *p)
功能:注销字符设备驱动对象
参数
  • p:字符设备驱动对象指针

3.7 释放设备号

void unregister_chrdev_region(dev_t from, unsigned count)
功能:用户释放设备号
参数
  • from:释放的设备号
  • count:申请的设备数量

4. 字符设备驱动分步注册实例

下面的代码精简了 prink 的输出,注意步骤顺序,以及错误时退出的方法:
static int __init mycdev_init(void) { int ret, i; // ---------- 1.分配字符设备驱动对象空间 cdev_alloc -------------------- cdev = cdev_alloc(); if (cdev == NULL) { return -EFAULT; goto out1; } // ---------- 2.字符设备驱动对象部分初始化 cdev_init -------------------- cdev_init(cdev, &fops); // ---------- 3.申请设备号 register_chrdev_region/alloc_chrdev_region ---------- if (major > 0) { // 静态指定设备号 ret = register_chrdev_region(MKDEV(major, minor), 3, "myled"); if (ret) { goto out2; } } else { // 动态申请设备号 ret = alloc_chrdev_region(&devno, minor, 3, "myled"); if (ret) goto out2; major = MAJOR(devno); // 根据设备号获取主设备号 minor = MINOR(devno); // 根据设备号获取次设备号 } // ---------- 4.注册字符设备驱动对象 cdev_add() -------------------- ret = cdev_add(cdev, MKDEV(major, minor), 1); if (ret) goto out3; // ---------- 5.向上提交目录 -------------------- cls = class_create(THIS_MODULE, "myled"); if (IS_ERR(cls)) { ret = -PTR_ERR(cls); goto out4; } // ---------- 6.向上提交设备节点 -------------------- for (i = 0; i < 3; i++) { dev = device_create(cls, NULL, MKDEV(major, minor + i), NULL, "myled%d", i); if (IS_ERR(dev)) { ret = -PTR_ERR(dev); goto out5; } } // init_all_led() return 0; out5: for (--i; i >= 0; i--) { // 从失败的位置开始回退 device_destroy(cls, MKDEV(major, minor + i)); } class_destroy(cls); out4: cdev_del(cdev); out3: unregister_chrdev_region(MKDEV(major, minor), 3); out2: kfree(cdev); out1: return ret; }
相对应的注销函数:
static void __exit mycdev_exit(void) { //1.销毁设备信息 device_destroy int i; for (i = 0; i < 3; i++) device_destroy(cls, MKDEV(major, minor + i)); //2.销毁目录 class_destroy class_destroy(cls); //3.注销对象 cdev_del() cdev_del(cdev); //4.释放设备号 unregister_chrdev_region() unregister_chrdev_region(MKDEV(major, minor), 3); //5.释放对象空间 kfree() kfree(cdev); }

5. inode 结构体的作用

只要文件存在于文件系统中,在内核中就会有一个对应的 struct inode 结构体空间,这里保存了当前文件的相关信息。另外通过 inode 结构体实现上下级的关联。
struct inode { umode_t i_mode; //文件的类型和其他的mode kuid_t i_uid;//用户id kgid_t i_gid;//用户组id  unsigned int i_flags;//文件相关的标志  const struct inode_operations *i_op;//文件操作方法结构体指针 dev_t i_rdev; //设备号  union { struct block_device *i_bdev;//块设备指针 struct cdev *i_cdev; //字符设备对象指针 }; };

6. file 结构体的作用

open 函数第一个参数是设备文件路径,通过文件路径找到 inode 结构体,进而回调到驱动中 open 操作方法。但是 read()/write*()/ioctl() 这些函数的参数不是文件的路径,那么 VFS 如何通过文件描述符回调到这些函数对应的驱动中的操作方法的呢?(文件描述符)

6.1 文件描述符是什么

当打开一个文件,那么在当前进程中就会有一个文件描述符和当前文件关联,文件描述符是一个非负整数,一个进程中文件描述符最多有 1024 个。每一个进程都有自己独立的一套文件描述符,文件描述符是依赖于进程存在。想要探究文件描述符到底有什么意义,就要知道它在进程中的位置。一个进程运行在操作系统上,在内核一定会存在一个 struct task_struct 结构体空间,这个空间中存放的就是进程相关的信息。
struct task_struct { volatile long state;//进程状态 void *stack;//栈区首地址 int on_cpu;//在哪个cpu上执行 struct list_head tasks;//构造链表 pid_t pid; //当前进程进程号 struct task_struct __rcu *parent;//父进程描述结构体 struct files_struct *files;//进程内打开的文件相关信息存放空间的指针 }; struct files_struct { struct file __rcu * fd_array[NR_OPEN_DEFAULT]; }; //进程内部有一个成员files,这个成员是和进程打开的文件相关的结构体, //在这个结构体中存在一个struct file*类型的数组fd_array。 //我们open函数返回的文件描述符就是这个数组的下标
notion image

6.2 file 结构体

当在一个进程中打开一个文件时,内核就会产生一个 struct file 类型的空间,这个空间中存放的就是打开的文件相关的信息,包括打开的模式,打开的文件的基本属性等信息,open 函数在打开文件时 VFS 虚拟文件系统层会在 fd_array 数组中申请一个空间,将当前文件的 struct file 结构体指针存放到数组中的指定位置,并且把这个位置的额下标以函数返回值的形式返回,这个返回值就是文件描述符。
notion image
struct file { struct path f_path;//当前文件的路径 struct inode *f_inode;//当前文件的inode结构体指针 const struct file_operations *f_op;//操作方法结构体指针 unsigned int f_flags;//打开文件时填写的各种标志 void *private_data;//打开的文件对应的file结构体内部的私有数据, } //可用于操作方法之间传参