Linux驱动 - 一个虚拟字符设备(缓冲区)

系列 - Linux驱动初试

书接上文,不再对模块构建的环境作赘述。传送门:编译一个模块

该模块的效果是,用户态通过read()/write()调用可以读写我们的设备文件(/dev/myCharDev),在内核中实现一个ringbuffer,用户可以多次写入数据,并将先前写入的数据读出来。

顶层流程:

  • 模块加载时,获取设备号(注册或分配)
  • 初始化字符设备结构(cdev_init()),需要指定file_operation回调集
  • 注册cdev(cdev_add())
  • 创建设备类(class_create())+设备文件(device_create())
  • 卸载时释放设备类、设备文件、设备号

我们需要创建一个struct file_operations类型的结构,它是字符设备可被操作的动作合集。在这里,我们为它创建:

  • open - 打开设备文件
  • release - 释放设备文件
  • read - 读取设备文件
  • write - 写入设备文件
    这四个动作。它们的原型分别为:

当用户open()对应设备文件时执行。

  • struct inode: 设备文件在内核中的inode对象,包含设备号、权限、文件类型
    最常用操作是用来获取设备号:int minor = iminor(inode), major = imajor(inode); 用来判断哪个设备被打开,支持多设备。例如,/dev/led0/dev/led1minor做区分
  • struct file :当前打开的文件实例,fd对应的内核对象
    每次open都会创建一个struct file,里面有一个重要字段file->private_data,常用它来保存设备上下文,这样read/write时可以召回
int xx_open(struct inode *inode, struct file *file);

当用户close()对应设备文件时执行。参数等同于open,用于关闭设备,释放资源。
内核使用kfree释放堆内存。


int xx_release(struct inode *inode, struct file *file);

用户用read()读文件或者用cat读文件时调用。

  • struct file:当前打开的文件实例,主要用file->private_data访问设备上下文。
  • char __user buf:用户空间缓冲区地址,用户调用read时传入的接收地址
    注意
    __user标识这是用户空间指针,必须用copy_to_user访问
  • size_t count:用户请求读取的字节数。驱动未必返回此值,可以返回较小实际值或0(EOF)
  • loff_t f_pos:文件偏移量。用户执行过lseek后此选项生效。
  • 返回值:实际读到的字节数。可以小于count或0(EOF)
ssize_t xx_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos);

用户用write()写或echo到设备文件时执行。

注意
用户空间指针必须用copy_from_user访问

ssize_t xx_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos);

#include <linux/module.h> // 模块基础
#include <linux/init.h>   // 初始化宏

#include <linux/fs.h>      // 文件操作
#include <linux/device.h>  // 设备模型
#include <linux/uaccess.h> // 用户空间访问
#include <linux/slab.h>    // 内存分配
#include <linux/cdev.h>    // 字符设备(进阶用)
#include <linux/kernel.h>

#include <linux/string.h>
#define DEFAULT_RB_SIZE (512)

static struct myChardev_t
{
    uint8_t *buff;
    uint32_t front;
    uint32_t back;
    uint32_t rb_size;
} myCharDev;

static uint8_t buff[DEFAULT_RB_SIZE];

static int myChardev_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "chardev rb opened.\n");
    return 0;
}
static int myChardev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "chardev rb released.\n");
    return 0;
}

static ssize_t myChardev_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos)
{
    size_t i = 0;

    while (i < count && myCharDev.front != myCharDev.back)
    {
        if (copy_to_user(&buf[i], &myCharDev.buff[myCharDev.back], 1))
        {
            return -EFAULT;
        }
        myCharDev.back = (myCharDev.back + 1) & (DEFAULT_RB_SIZE - 1);
        ++i;
    }
    return i;
}

static ssize_t myChardev_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{
    size_t i = 0;
    while (i < count)
    {
        size_t next = (myCharDev.front + 1) & (DEFAULT_RB_SIZE - 1);
        if (next == myCharDev.back)
            break;
        if (copy_from_user(&myCharDev.buff[myCharDev.front], &buf[i], 1))
            return -EFAULT;

        myCharDev.front = next;
        ++i;
    }
    return i;
}

static dev_t dev_num;
static struct cdev rb_cdev;

static struct class *cls;

static struct file_operations ring_fops = {
    .owner = THIS_MODULE,
    .open = myChardev_open,
    .read = myChardev_read,
    .write = myChardev_write,
    .release = myChardev_release,
};

static char *my_class_devnode(const struct device *dev, umode_t *mode)
{
    if (mode)
        *mode = 0666; // 读写权限都给用户
    return NULL;
}

static int __init virdev_init(void)
{
    memset(buff, 0, sizeof(buff));
    myCharDev.buff = buff;
    myCharDev.front = 0;
    myCharDev.back = 0;
    myCharDev.rb_size = DEFAULT_RB_SIZE;

    int ret = alloc_chrdev_region(&dev_num, 0, 1, "myrb");
    if (ret < 0)
    {
        printk(KERN_INFO "virChardev: 设备号分配失败\n");
        return ret;
    }

    cdev_init(&rb_cdev, &ring_fops);
    ret = cdev_add(&rb_cdev, dev_num, 1);
    if (ret)
    {
        printk(KERN_INFO "virChardev: 新增字符设备失败\n");
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    //deprecated in Kernel 6.12
    // cls = class_create(THIS_MODULE, "myrb"); 
    cls = class_create("myrb"); 
    cls->devnode = my_class_devnode;
    device_create(cls, NULL, dev_num, NULL, "myrb");
    printk(KERN_INFO "virChardev: 驱动已加载,major=%d\n", MAJOR(dev_num));
    return 0;
}

static void __exit virdev_exit(void)
{
    device_destroy(cls, dev_num);
    class_destroy(cls);
    cdev_del(&rb_cdev);
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "virChardev: 驱动已卸载\n");
}
module_init(virdev_init);
module_exit(virdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("KurehaTian");
MODULE_DESCRIPTION("Description");
MODULE_VERSION("1.0");

对于之前介绍过的部分,不再赘述。

头文件引用+宏定义:

#include <linux/fs.h>       
#include <linux/device.h>   
#include <linux/uaccess.h>  
#include <linux/slab.h>     
#include <linux/cdev.h>     

#include <linux/string.h>   // 内核的memset等实现
#define DEFAULT_RB_SIZE (512)
  • linux/fs.h: 文件操作
  • linux/device.h: 设备模型
  • linux/uaccess.h: 用户空间访问
  • linux/slab.h: 内存分配
  • linux/cdev.h: 字符设备

环形缓冲定义:

  • buff指向一段空间
  • front back 是头尾位置
  • 初始化时将myCharDevbuff指针指向buff数组
static struct myChardev_t
{
    uint8_t *buff;
    uint32_t front;
    uint32_t back;
    uint32_t rb_size;
} myCharDev;
static uint8_t buff[DEFAULT_RB_SIZE];

设备的 file_operation:

  • 打开设备文件时执行
static int myChardev_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "chardev rb opened.\n");
    return 0;
}
  • 关闭设备文件时执行
static int myChardev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "chardev rb released.\n");
    return 0;
}
注意
open close是每打开/关闭都会执行,所以慎重考虑在这个地方初始化/释放资源!
举个例子,如果在这里归零/初始化,那么每次echo/cat的状态都带不到下条指令!
  • 从设备(内核)读取数据到用户空间
static ssize_t myChardev_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos)
{
    size_t i = 0;

    while (i < count && myCharDev.front != myCharDev.back)
    {
        if (copy_to_user(&buf[i], &myCharDev.buff[myCharDev.back], 1))
        {
            return -EFAULT;
        }
        myCharDev.back = (myCharDev.back + 1) & (DEFAULT_RB_SIZE - 1);
        ++i;
    }
    return i;
}
  • 从用户空间写入到内核空间
static ssize_t myChardev_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{
    size_t i = 0;
    while (i < count)
    {
        size_t next = (myCharDev.front + 1) & (DEFAULT_RB_SIZE - 1);
        if (next == myCharDev.back)
            break;
        if (copy_from_user(&myCharDev.buff[myCharDev.front], &buf[i], 1))
            return -EFAULT;

        myCharDev.front = next;
        ++i;
    }
    return i;
}
  • file_operation定义
static struct file_operations ring_fops = {
    .owner = THIS_MODULE,
    .open = myChardev_open,
    .read = myChardev_read,
    .write = myChardev_write,
    .release = myChardev_release,
};

模块加载动作:

  • 设计设备文件权限回调
static char *my_class_devnode(const struct device *dev, umode_t *mode)
{
    if (mode)
        *mode = 0666; // 读写权限都给用户
    return NULL;
}
  • 清空环形缓冲区
static dev_t dev_num;
static struct cdev rb_cdev;
static int __init virdev_init(void)
{
    memset(buff, 0, sizeof(buff));
    myCharDev.buff = buff;
    myCharDev.front = 0;
    myCharDev.back = 0;
    myCharDev.rb_size = DEFAULT_RB_SIZE;
    //... ...
  • 分配设备号
    //... ...
    int ret = alloc_chrdev_region(&dev_num, 0, 1, "myrb");
    if (ret < 0)
    {
        printk(KERN_INFO "virChardev: 设备号分配失败\n");
        return ret;
    }

    cdev_init(&rb_cdev, &ring_fops);
    ret = cdev_add(&rb_cdev, dev_num, 1);
    // 也可用register_chrdev()静态指定设备号
    if (ret)
    {
        printk(KERN_INFO "virChardev: 新增字符设备失败\n");
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }
  • 创建设备类和设备文件
     //deprecated in Kernel 6.12
    // cls = class_create(THIS_MODULE, "myrb"); 
    // 创建设备类
    cls = class_create("myrb"); 

    //设备节点回调
    cls->devnode = my_class_devnode;

    //创建设备
    device_create(cls, NULL, dev_num, NULL, "myrb");
    printk(KERN_INFO "virChardev: 驱动已加载,major=%d\n", MAJOR(dev_num));
    return 0;
}

模块卸载动作:

注意
注意设备、类、字符设备销毁顺序,必须和初始化顺序相反
原则:先销毁最上层,再销毁底层资源

static void __exit virdev_exit(void)
{
    device_destroy(cls, dev_num);
    class_destroy(cls);

    cdev_del(&rb_cdev);
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "virChardev: 驱动已卸载\n");
}

执行以下指令:

$ echo "helloWorld" > /dev/myrb 
$ cat /dev/myrb
helloWorld

  • “全局变量”: 生命周期是从模块加载直至卸载。
    static修饰的全局变量仅在当前模块中生效,否则可以被其他模块访问到。但是,由于各个模块的生命周期不尽相同,所以可能会存在风险。对于需要与其他模块共享的变量或函数,应当使用EXPORT_SYMBOLEXPORT_SYMBOL_GPL导出到内核符号表(GPL模块只能被GPL模块引用),在其他模块中extern获取到它。编译时extern告诉编译器此符号在别处,会尝试在内核符号表中查找这个符号。找得到就绑定导出的地址,找不到就加载失败,内核崩溃。因此引入了模块引用计数机制,如果引用模块A的其他模块没有全部卸载,则模块A不能被卸载。
  • “局部变量” : 在内核栈中。在哪个task中陷入内核态并调用内核函数,这些变量就放在哪个task的内核栈中。每个task的内核栈大约为8KB/16KB。每个task都会有自己独立的用户栈和内核栈,具体见文《Linux深入探索之进程与线程的关系》(如果没加链接那就是我还没写)。

在内核中操作用户空间内存必须使用这两个函数。原型:

static inline int copy_from_user(void *to, const void __user volatile *from, unsigned long n);
static inline int copy_to_user(void __user volatile *to, const void *from,  unsigned long n);

为什么要用这个方式访问?
在用户态中不许访问内核地址,内核态中也不准直接用memcpy等方式访问。 因为这个指针可能不合法,也可能无权限,也可能这个用户页不在物理内存中,需要先触发page fault机制来swap-in。
copy_to/from_user执行了严格的检查:

  • 检查用户地址有效性access_ok(to,n)
  • 保护内核页表,用内核机制安全访问用户页,如果有page fault捕获并返回,不会直接panic
  • 处理跨页访问/分段,逐页处理

为什么内核代码能访问到调用它的进程的用户空间内存?
因为在哪里陷入内核调用,肯定就是在哪个进程地址空间中,此时上下文中使用该进程的页表,寻址使用该进程的地址空间。
关于页表,后面另文再表。

Linux内核中,设备模型分为三个层次:

  • 设备类(class):代表一类设备,用class_create()创建,会出现在/sys/class/下,把同类设备归组便于 sysfs 管理。
  • 字符/块设备(cdev/bdev):代表具体的驱动实例,必须和主/次设备号绑定,把驱动对应到设备号上。(struct cdev类型, cdev_init, cdev_add
  • 设备(device):具体的硬件或驱动实例,用于用户空间创建设备节点。用device_create创建。

device_create本质上做了两件事:

  1. 在 sysfs 中创建了 device 条目: /sys/class/<classname>/<devname>
  2. /dev/ 下创建设备节点,对应传入的 dev_t(主/次设备号)。

而设备文件本身是一个入口点,用户空间通过open("/dev/devname")访问内核,内核根据cdev找到对应file_operation执行,同一类里面调用的是同一个逻辑。

若同一个class创建了多个device

struct class *cls = class_create("myclass");
device_create(cls, NULL, MKDEV(240, 0), NULL, "dev0");
device_create(cls, NULL, MKDEV(240, 1), NULL, "dev1");
device_create(cls, NULL, MKDEV(240, 2), NULL, "dev2");
  • /dev/dev0/dev/dev1/dev/dev2 都指向同一个 cdev / 驱动实例
  • 内核上下文(驱动代码、全局变量、fops)是 共享的;
  • 不同 device 节点只是 不同的访问入口,你可以在 open()ioctl() 中根据 inode->i_rdev(设备号)区分处理逻辑

要实现同一种设备不同节点的特异化:

  • 可以通过minor作区分,不同节点打开的minor是不同的, 驱动共用一份fops,通过minor分流
  • 通过面向对象设计,将设备对象化,用devdata存储上下文(在inode->private_data里),即可在同一个驱动逻辑里做不同处理

引申概念:

  • container_of 可以用于通过某结构体的成员地址和名称,捕获到这个结构体的地址。