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.

posted @ 2022-07-10 22:11  明明1109  阅读(492)  评论(0编辑  收藏  举报