• Linux 虚拟字符设备globalmem


    在虚拟设备驱动中,分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,用于实现自定义的虚拟字符设备globalmem实例。
    globalmem 没有任何实用价值,仅用于讲解问题。

    globalmem设备驱动

    头文件、宏、设备结构体

    定义globalmem设备结构

    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/init.h>
    #include <linux/cdev.h>
    #include <linux/slab.h>
    #include <linux/uaccess.h>
    
    #define GLOBALMEM_SIZE  0x1000 // 4KB
    #define MEM_CLEAR       0x1
    #define GLOBALMEM_MAJOR 230
    
    static int globalmem_major = GLOBALMEM_MAJOR; // 主设备号
    module_param(globalmem_major, int, S_IURGO);  // 向当前模块传入的参数
    
    /* globalmem设备结构 */
    struct globalmem_dev {
        struct cdev cdev; // 对应globalmem字符设备的dev
        unsinged char mem[GLOBALMEM_SIZE]; // 使用的内容mem[]
    };
    
    struct globalmem_dev *globalmem_devp;
    

    加载与卸载设备驱动

    // 完成cdev的初始化与添加
    // dev: 指向globalmem dev设备结构
    // index: globalmem_dev结构指针数组的索引, 代表设备索引
    static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
    {
        int err, devno = MKDEV(globalmem_major, index);
        
        cdev_init(&dev->cdev, &globalmem_fops); // 初始化cdev, 并建立与file_operations联系
        dev->cdev.owner = THIS_MODULE; // 所属模块
        err = cdev_add(&dev->cdev, devno, 1);      // 注册一个设备
        if (err)
            printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
    }
    
    // 模块加载函数
    static int __init globalmem_init(void)
    {
        int ret;
        dev_t devno = MKDEV(globalmem_major, 0); // 指定设备号
        
        if (globalmem_major)
            ret = register_chrdev_region(devno, 1, "globalmem"); // 由调用者指定设备号, 向系统申请设备号
        else {
            ret = alloc_chrdev_region(&devno, 0, 1, "globalmem"); // 由系统决定, 向系统申请设备号
            globalmem_major = MAJOR(devno);
        }
    
        if (ret < 0)
            return ret;
    
        globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL); // 向内核申请内存
        if (!globalmem_devp) {
            ret = -ENOMEM;
            goto fail_malloc;
        }
    
        globalmem_setup_cdev(globalmem_devp, 0);
        return 0;
    
        fail_malloc: // kzalloc异常处理
        unregister_chrdev_region(devno, 1);
        return ret;
    }
    module_init(globalmem_init);
    

    globalmem_setup_cdev完成cdev的初始化和添加,其中在初始化cdev时,需要一个file_operations 结构对象globalmem_fops。globalmem_fops是与globalmem设备驱动的文件操作结构体对象:

    static const struct file_operations globalmem_fops = {
        .owner = THIS_MODULE,
        .llseek = globalmem_llseek,
        .read = globalmem_read,
        .write = globalmem_write,
        .unlocked_ioctl = globalmem_ioctl,
        .open = globalmem_open,
        .release = globalmem_release,
    };
    

    下面实现file_operations绑定的文件操作。

    读写函数

    globalmem的读写操作,主要是让设备结构体中的mem[] 与用户空间交互,随着访问的字节数变更,更新文件读写偏移位置。

    读函数

    // filp: 要读取的文件指针
    // buf: 用户空间缓冲区地址
    // size: 用户空间缓冲区大小(bytes) 
    // ppos [in-out]: 指向要读的位置相对于文件开头的偏移
    static ssize_t globalmem_read(struct file* filp, char __user *buf, size_t size, loff_t *ppos)
    {
        unsigned long p = *ppos;
        unsigned int count = size;
        int ret = 0;
        struct globalmem_dev *dev = filp->private_data;
        
        if (p >= GLOBALMEM_SIZE) // 达到文件末尾
            return 0; // EOF
        if (count > GLOBALMEM_SIZE - p) // 要读的数据超过mem[]边界
            count = GLOBALMEM_SIZE - p;
        
        if (copy_to_user(buf, dev->mem + p, count)) { // 将内核空间mem[p..p+count)拷贝到用户空间buf
            ret = -EFAULT;
        } else {
            *ppos += count; // 更新偏移位置
            ret = count;    // 已读取字节数
    
            printk(KERN_INFO "read %u bytes(s) from %lu\n", count ,p);
        }
        return ret;
    }
    

    写函数

    // filp: 要写的文件指针
    // buf: 用户空间缓冲区地址
    // size: 用户空间缓冲区大小(bytes)
    // ppos [in-out]: 指向要写的位置相对于文件开头的偏移
    static ssize_t globalmem_write (struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
    {
        unsigned long p = *ppos;
        unsigned int count = size;
        int ret = 0;
        struct globalmem_dev* dev = filp->private_data;
    
        if (p >= GLOBALMEM_SIZE)  // 要写的位置超出mem[]边界
            return 0;
    
        if (count > GLOBALMEM_SIZE - p) // 要写的用户缓冲区大小超过mem[]大小, 截断
            count = GLOBALMEM_SIZE - p;
    
        if (copy_from_user(dev->mem + p, buf, count)) {
            ret=  -EFAULT;
        } else {
            *ppos += count; // 更新偏移位置
            ret = count;    // 已读取字节数
    
            printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
        }
        return ret;
    }
    

    seek函数

    seek() 函数对文件定位的起始地址可以是文件开头(SEEK_SET, 0),当前位置(SEEK_CUR, 1)和文件末尾(SEEK_END, 2)。假设globalmem支持从文件开头和当前位置的相对偏移,不支持文件末尾的相对偏移。
    定位时,应该检查用户请求的合法性,若不合法,函数返回-EINVAL,合法时更新文件的当前位置并返回该位置。

    // 重新定位文件偏移
    static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) 
    {
        loff_t ret = 0;
        switch(orig) {
        case 0: // SEEK_SET
            if (offset < 0) {
                ret = -EINVAL; // 参数错误
                break;
            }
            if ((unsigned int)offset > GLOBALMEM_SIZE) {
                ret = -EINVAL;
                          break;
            }
            filp->f_pos = (unsinged int)offset;
            ret = filp->f_pos;
            break;
    
        case 1: // SEEK_CUR
            if ((filp->f_pos + offset) > GLOBALMEM_SIZE) { // 超出mem[]上边界
                ret = -EINVAL;
                break;
            }
            if ((filep->f_pos + offset) < 0) { // 超出mem[]下边界
                ret = -EINVAL;
                break;
            }
            filp->f_pos += offset;
            ret = filp->f_pos;
            break;
        default:
            ret = -EINVAL;
            break;
        }
        return ret;
    }
    

    ioctl函数

    1)globalmem设备驱动的ioctl()函数
    globalmem设备驱动的ioctl() 接受MEM_CLEAR命令(前面已定义为0x01),该命令将全局内存的有效数据长度清0;对于不支持的命令,该函数返回-EINVAL。

    // 应用程序中, 通过ioctl(fd, cmd, ...)来调用
    // cmd: 命令
    // arg: 命令参数, 由驱动程序根据不同命令解释
    static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
    {
        struct globalmem_dev *dev = filp->private_data;
        
        switch (cmd) {
        case MEM_CLEAR:
            memset(dev->mem, 0, GLOBALMEM_SIZE);
            printk(KERN_INFO, "globalmem is set to zero\n");
            break;
        default: // 其他不支持的命令
            return -EINVAL;
        }
        return 0;
    }
    

    globalmem的读写操作,主要是让设备结构体中的mem[] 与用户空间交互,随着访问的字节数变更,更新文件读写偏移位置。

    2)ioctl() 命令的命名规则

    前面提到,MEM_CLEAR被定义为0x01,实际上这不是一种推荐的方法。因为不同设备驱动很可能拥有相同命令号,导致命令码污染。

    Linux 内核推荐采用一套统一的ioctl() 命令生成方式。命令码组成:

    设备类型 序列号 方向 数据尺寸
    8bit 8bit 2bit 13/14bit
    • 设备类型字段,是一个“幻数”,可以是0~0xFF,内核中的ioctl-number.txt给出了一些推荐的和已经被使用的“幻数”。新设备驱动定义的“幻数”应避免与其冲突。
    • 方向字段,表示数据传送的方向,可能值_IOC_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)和_IOC_READ | _IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。
    • 数据尺寸,表示涉及到用户数据的大小,成员的宽度依赖于体系结构,通常是13或14bit。

    内核还定义了几个宏_IO(), _IOR(), _IOW(), _IOWR(),用于辅助生成命令。

    #include <asm-generic/ioctl.h>
    
    #define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
    #define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
    #define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
    #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
    
    /* _IO, _IOR 等使用的_IOC宏 */
    #define _IOC(dir,type,nr,size) \
                       (((dir)  << _IOC_DIRSHIFT) | \
                       ((type) << _IOC_TYPESHIFT) | \
                       ((nr)   << _IOC_NRSHIFT) | \
                       ((size) << _IOC_SIZESHIFT))
    

    宏的作用是根据传入的type: 设备类型、nr: 序列号、size: 数据长度、宏名隐含的方向,这几个参数移位组合生成的命令码。

    因为globalmem的MEM_CLEAR命令不涉及数据传输,所以它可以定义为:

    // 可以将MEM_CLEAR 宏定义修改为以下方式
    #define GLOBALMEM_MAGIC 'g'
    #define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0)
    

    使用文件私有数据

    大多数Linux驱动遵循“潜规则”:将文件的私有数据private_data,指向设备结构体,再用read/write/ioctl/llseek 等文件操作函数通过private_data 访问设备结构体。

    /*
    * Linux 驱动遵循潜规则: 将文件的私有数据private_data 指向设备结构体
    * 供read/write/ioctl/llseek 等文件操作函数通过private_data 访问设备结构体
    */
    static int globalmem_open (struct inode *inode, struct file *filp)
    {
        filp->private_data = globalmem_devp;
        return 0;
    }
    

    用户空间验证globalmem驱动

    加载驱动

    make命令编译globalmem驱动后,得到globalmem.ko。
    通过insmod命令加载驱动程序:

    # insmod globalmem.ko
    [  909.744023] globalmem_drv: loading out-of-tree module taints kernel.
    # lsmod 
    Module                  Size  Used by
    globalmem_drv           2599  0
    evbug                   2078  0
    inv_mpu6050_spi         2052  0
    inv_mpu6050            10948  2 inv_mpu6050_spi
    

    注意到insmod加载globalmem驱动时,提示“loading out-of-tree module taints kernel”,这是由于自行编写的驱动没有加入Kconfig树,但仍能正常加载,内核给出的提示。参考:https://blog.csdn.net/y24283648/article/details/108608239

    已经注册的字符设备,可以通过"cat /proc/devices" 命令查看到

    # cat /proc/devices
    ...
    230 globalmem
    ...
    

    globalmem的主设备号为230,这个正是我们在驱动程序中指定的主设备号。

    如果驱动程序中,没有通过调用class_create和device_create 为globalmem创建逻辑设备,那么也可以用mknod创建之,这样APP就可以通过 "/dev/globalmem" 访问设备驱动了。

    # mknod /dev/glbalmem c 230 0
    

    这里"/dev/glbalmem" 是要创建的逻辑设备名称,c表示char device(字符设备),230是主设备号,0是次设备号(如果只有1个设备,次设备号通常为0)。

    命令行验证读写功能

    echo命令可以调用驱动程序的write,因此可用于验证设备的写;
    cat命令可以调用驱动程序的read,因此可以用于验证设备的读。

    # echo "hello world" > /dev/globalmem
    
    # cat /dev/globalmem
    hello world
    

    如果启用了sysfs文件系统,还会发现多出了 /sys/module/globalmem 目录。

    APP测试程序验证读写功能

    以write, read 的顺序,对文件"/dev/globalmem" 分别进行写、读操作。

    int main()
    {
        int fd;
        int nread, nwrite;
        char buf[1024];
        
        fd = open("/dev/globalmem", O_RDWR);
        if (fd < 0) {
            perror("open /dev/globalmem error");
            return -1;
        }
        
        strcpy(buf, "hello, this is globalmem device driver");
        nwrite = write(fd, buf, strlen(buf));
        if (nwrite < 0) {
            perror("write /dev/globalmem error");
            return -1;
        }
        printf("write %d(bytes) to globalmem: %s\n", nwrite, buf);
        
        nread = read(fd, buf, sizeof(buf));
        if (nread < 0) {
            perror("read /dev/globalmem error");
            return -1;
        }
        buf[nread] = '\0';
        printf("read %d(bytes) from globalmem: %s\n", nread, buf);
        close(fd);
        return 0;
    }
    

    将APP编译成目标globalmem_test,运行之

    # ./globalmem_test
    [  105.426376] written 38 bytes(s) from 0
    write 38(bytes) to globalmem: hel[  105.431113] read 1024 bytes(s) from 38
    lo, this is globalmem device driver
    read 1024(bytes) from globalmem:
    

    发现APP与驱动程序能正常打印。

    完整源码:参见ch6_chardev | gitee


    参考

    [1]宋宝华. Linux设备驱动开发详解[M]. 人民邮电出版社, 2010.

  • 相关阅读:
    from fake_useragent import UserAgent
    teamviewer 安装 仅学习
    利用pandas 中的read_html 获取页面表格
    第十二天 最恶心的考试题
    第十三天 生成器和生成器函数, 列表推导式
    第十一天 函数名的使用以及第一类对象, 闭包, 迭代器
    第十天 动态参数,名称空间,作用域,函数的嵌套,gloabal / nonlocal 关键字
    初始函数, 函数的定义,函数名,函数体以及函数的调用,函数的返回值,函数的参数
    第八天 文件的读,写,追加,读写,写读,seek()光标的移动,修改文件以及另一种打开文件的方式
    第七天 1.基础数据类型的补充 2.set集合 3.深浅拷贝
  • 原文地址:https://www.cnblogs.com/fortunely/p/16464236.html
Copyright © 2020-2023  润新知