《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 6 章 字符设备驱动

《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 6 章 字符设备驱动

参考:宋宝华 著,机械工业出版社,2015年版


6.1 Linux 字符设备驱动结构

6.1.1 字符设备驱动的核心数据结构

Linux 字符设备驱动围绕三个核心数据结构展开:cdevfile_operations 和设备号。

(1)cdev 结构体

cdev(Character Device)是内核中表示字符设备的核心结构体,定义在 <linux/cdev.h>

struct cdev {
    struct kobject          kobj;       /* 内嵌 kobject,纳入设备模型 */
    struct module          *owner;      /* 所属模块(通常为 THIS_MODULE) */
    const struct file_operations *ops;  /* 文件操作函数集 */
    struct list_head        list;       /* 链表节点 */
    dev_t                   dev;        /* 设备号(主设备号+次设备号) */
    unsigned int            count;      /* 设备数量 */
};

cdev 的操作函数

/* 初始化 cdev(动态分配后使用) */
void cdev_init(struct cdev *cdev, const struct file_operations *fops);

/* 动态分配 cdev */
struct cdev *cdev_alloc(void);

/* 向内核注册 cdev(使其生效) */
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);

/* 从内核注销 cdev */
void cdev_del(struct cdev *cdev);
(2)file_operations 结构体

file_operations 是字符设备驱动最核心的数据结构,定义了驱动支持的所有文件操作:

/* 定义在 <linux/fs.h>,以下为常用字段 */
struct file_operations {
    struct module *owner;           /* 所属模块,通常为 THIS_MODULE */

    /* 文件定位 */
    loff_t  (*llseek)  (struct file *, loff_t, int);

    /* 数据读写 */
    ssize_t (*read)    (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)   (struct file *, const char __user *, size_t, loff_t *);

    /* 异步 I/O */
    ssize_t (*read_iter)  (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

    /* I/O 多路复用 */
    unsigned int (*poll) (struct file *, struct poll_table_struct *);

    /* 设备控制 */
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl)   (struct file *, unsigned int, unsigned long);

    /* 内存映射 */
    int (*mmap)    (struct file *, struct vm_area_struct *);

    /* 打开与关闭 */
    int (*open)    (struct inode *, struct file *);
    int (*flush)   (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);

    /* 同步 */
    int (*fsync)   (struct file *, loff_t, loff_t, int datasync);

    /* 其他 */
    int (*fasync)  (int, struct file *, int);
    int (*lock)    (struct file *, int, struct file_lock *);
};
(3)设备号

设备号(dev_t)是一个 32 位整数,由**主设备号(Major,高12位)次设备号(Minor,低20位)**组成:

#include <linux/kdev_t.h>

dev_t devno;

/* 合成设备号 */
devno = MKDEV(major, minor);

/* 分解设备号 */
int major = MAJOR(devno);
int minor = MINOR(devno);

/* 静态申请设备号(主设备号已知) */
int ret = register_chrdev_region(devno, count, name);

/* 动态申请设备号(推荐,由内核分配主设备号) */
int ret = alloc_chrdev_region(&devno, first_minor, count, name);

/* 释放设备号 */
unregister_chrdev_region(devno, count);
# 查看系统已使用的设备号
cat /proc/devices
# Character devices:
#   1 mem
#   4 tty
#   4 ttyS
#   5 /dev/tty
# 240~254 为本地/实验用设备号范围(推荐用于自定义驱动)

6.1.2 字符设备驱动的工作流程

字符设备驱动的完整工作流程:

驱动加载(insmod):
  module_init()
    ├── alloc_chrdev_region()   ← 申请设备号
    ├── cdev_init()             ← 初始化 cdev,关联 file_operations
    ├── cdev_add()              ← 向内核注册字符设备
    ├── class_create()          ← 创建设备类(/sys/class/xxx)
    └── device_create()         ← 创建设备(触发 udev 创建 /dev/xxx)

应用程序访问:
  open("/dev/xxx")
    └── 内核调用驱动的 .open 函数
  read(fd, buf, n)
    └── 内核调用驱动的 .read 函数
  write(fd, buf, n)
    └── 内核调用驱动的 .write 函数
  ioctl(fd, cmd, arg)
    └── 内核调用驱动的 .unlocked_ioctl 函数
  close(fd)
    └── 内核调用驱动的 .release 函数

驱动卸载(rmmod):
  module_exit()
    ├── device_destroy()        ← 删除设备(触发 udev 删除 /dev/xxx)
    ├── class_destroy()         ← 删除设备类
    ├── cdev_del()              ← 注销字符设备
    └── unregister_chrdev_region() ← 释放设备号

6.1.3 file 结构体与 inode 结构体

驱动的 openreadwrite 等函数都会接收 fileinode 结构体指针,理解这两个结构体至关重要:

/*
 * inode:文件的静态属性(每个文件只有一个 inode)
 * 在 open() 时传入,包含设备号等信息
 */
struct inode {
    umode_t         i_mode;     /* 文件类型和权限 */
    dev_t           i_rdev;     /* 设备号(对设备文件有效) */
    struct cdev    *i_cdev;     /* 指向字符设备 cdev 的指针 */
    /* ... */
};

/* 从 inode 获取主/次设备号 */
unsigned int major = imajor(inode);
unsigned int minor = iminor(inode);

/*
 * file:文件的动态属性(每次 open() 创建一个新的 file 实例)
 * 在 open/read/write/ioctl/release 中传入
 */
struct file {
    const struct file_operations *f_op;  /* 文件操作函数集 */
    loff_t          f_pos;      /* 当前读写位置(文件偏移量) */
    unsigned int    f_flags;    /* 打开标志(O_RDONLY/O_WRONLY/O_NONBLOCK 等) */
    fmode_t         f_mode;     /* 文件访问模式 */
    void           *private_data; /* 驱动私有数据(非常重要!) */
    /* ... */
};

private_data 的重要性:驱动通常在 open() 函数中将设备结构体指针赋给 file->private_data,这样在后续的 readwriteioctl 函数中就可以通过 filp->private_data 获取设备结构体,而无需使用全局变量。


6.1.4 字符设备驱动的注册与注销

Linux 提供了两种注册字符设备的方式:

方式一:早期接口(register_chrdev,不推荐)

/* 旧接口:一次注册 256 个次设备号 */
int major = register_chrdev(0, "mydev", &my_fops);
/* 0 表示动态分配主设备号 */

/* 注销 */
unregister_chrdev(major, "mydev");

方式二:新接口(cdev,推荐)

/* 新接口:精确控制设备号范围 */
dev_t devno;
struct cdev my_cdev;

/* 步骤1:申请设备号 */
alloc_chrdev_region(&devno, 0, 1, "mydev");

/* 步骤2:初始化 cdev */
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;

/* 步骤3:注册 cdev */
cdev_add(&my_cdev, devno, 1);

/* 注销步骤(逆序)*/
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);

6.2 globalmem 虚拟设备实例描述

6.2.1 globalmem 设备的设计目标

宋宝华在书中以 globalmem(全局内存)虚拟设备作为字符设备驱动的经典教学案例。globalmem 是一个纯软件模拟的虚拟设备,没有对应的真实硬件,其本质是一块内核内存,通过字符设备接口暴露给用户空间。

globalmem 设备的功能:

┌─────────────────────────────────────────────────────────┐
│                   用户空间                               │
│  open("/dev/globalmem")                                 │
│  write(fd, "Hello", 5)  → 数据写入内核内存              │
│  lseek(fd, 0, SEEK_SET) → 重置读写位置                  │
│  read(fd, buf, 5)       → 从内核内存读取数据             │
│  ioctl(fd, MEM_CLEAR, 0)→ 清空内核内存                  │
└─────────────────────────────────────────────────────────┘
                    ↕ 系统调用
┌─────────────────────────────────────────────────────────┐
│                   内核空间(globalmem 驱动)              │
│  ┌───────────────────────────────────────────────────┐  │
│  │  globalmem_dev 结构体                              │  │
│  │  ┌─────────────────────────────────────────────┐  │  │
│  │  │  unsigned char mem[GLOBALMEM_SIZE](4KB)    │  │  │
│  │  └─────────────────────────────────────────────┘  │  │
│  │  struct cdev cdev                                  │  │
│  │  struct mutex mutex(并发保护)                    │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

6.2.2 globalmem 设备的规格

globalmem 设备规格:

设备名称:globalmem
设备文件:/dev/globalmem
主设备号:230(静态分配,可修改)
次设备号:0
设备内存:4096 字节(4KB)
支持操作:open / release / read / write / llseek / ioctl
ioctl 命令:MEM_CLEAR(清空设备内存)
并发保护:互斥锁(mutex)

6.3 globalmem 设备驱动

6.3.1 完整驱动代码

/*
 * globalmem.c —— globalmem 字符设备驱动
 * 实现一个 4KB 的虚拟内存设备,支持读写、定位和清空操作
 *
 * 参考:宋宝华《Linux设备驱动开发详解》第6章
 */

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/mutex.h>

/* ── 宏定义 ──────────────────────────────────────────────── */
#define GLOBALMEM_SIZE   0x1000    /* 设备内存大小:4096 字节(4KB) */
#define MEM_CLEAR        0x1       /* ioctl 命令:清空内存 */
#define GLOBALMEM_MAJOR  230       /* 主设备号(0 表示动态分配) */
#define DEVICE_NUM       1         /* 设备数量 */

/* ── 设备结构体 ──────────────────────────────────────────── */
struct globalmem_dev {
    struct cdev  cdev;                  /* 内嵌字符设备结构体 */
    unsigned char mem[GLOBALMEM_SIZE];  /* 设备内存(4KB) */
    struct mutex  mutex;                /* 互斥锁,保护并发访问 */
};

/* ── 全局变量 ──────────────────────────────────────────────*/
static int globalmem_major = GLOBALMEM_MAJOR;
static struct globalmem_dev *globalmem_devp;  /* 设备结构体指针 */
static struct class  *globalmem_class;
static struct device *globalmem_device;

/* ── open 函数 ───────────────────────────────────────────── */
static int globalmem_open(struct inode *inode, struct file *filp)
{
    /*
     * 通过 container_of 从 inode->i_cdev 获取 globalmem_dev 指针
     * 将其保存到 filp->private_data,供其他函数使用
     */
    struct globalmem_dev *dev = container_of(inode->i_cdev,
                                              struct globalmem_dev, cdev);
    filp->private_data = dev;
    return 0;
}

/* ── release 函数 ────────────────────────────────────────── */
static int globalmem_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* ── ioctl 函数 ──────────────────────────────────────────── */
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:
        mutex_lock(&dev->mutex);
        memset(dev->mem, 0, GLOBALMEM_SIZE);  /* 清空设备内存 */
        mutex_unlock(&dev->mutex);
        pr_info("globalmem: 设备内存已清空\n");
        break;

    default:
        return -EINVAL;   /* 不支持的命令 */
    }

    return 0;
}

/* ── read 函数 ───────────────────────────────────────────── */
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;   /* 返回 0 表示 EOF */

    /* 调整读取字节数,不超过剩余可读数据 */
    if (count > GLOBALMEM_SIZE - p)
        count = GLOBALMEM_SIZE - p;

    mutex_lock(&dev->mutex);

    /*
     * copy_to_user:将内核空间数据复制到用户空间
     * 参数:用户空间目标地址,内核空间源地址,字节数
     * 返回:未能复制的字节数(0 表示全部成功)
     */
    if (copy_to_user(buf, dev->mem + p, count)) {
        ret = -EFAULT;   /* 用户空间地址无效 */
    } else {
        *ppos += count;  /* 更新文件位置 */
        ret = count;
        pr_info("globalmem: 读取 %u 字节,当前位置 %lu\n", count, p);
    }

    mutex_unlock(&dev->mutex);
    return ret;
}

/* ── write 函数 ──────────────────────────────────────────── */
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)
        return 0;

    if (count > GLOBALMEM_SIZE - p)
        count = GLOBALMEM_SIZE - p;

    mutex_lock(&dev->mutex);

    /*
     * copy_from_user:将用户空间数据复制到内核空间
     * 参数:内核空间目标地址,用户空间源地址,字节数
     * 返回:未能复制的字节数(0 表示全部成功)
     */
    if (copy_from_user(dev->mem + p, buf, count)) {
        ret = -EFAULT;
    } else {
        *ppos += count;
        ret = count;
        pr_info("globalmem: 写入 %u 字节,当前位置 %lu\n", count, p);
    }

    mutex_unlock(&dev->mutex);
    return ret;
}

/* ── llseek 函数 ─────────────────────────────────────────── */
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
    loff_t ret = 0;

    switch (orig) {
    case SEEK_SET:   /* 从文件头偏移 */
        if (offset < 0) {
            ret = -EINVAL;
            break;
        }
        if ((unsigned int)offset > GLOBALMEM_SIZE) {
            ret = -EINVAL;
            break;
        }
        filp->f_pos = (unsigned int)offset;
        ret = filp->f_pos;
        break;

    case SEEK_CUR:   /* 从当前位置偏移 */
        if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
            ret = -EINVAL;
            break;
        }
        if ((filp->f_pos + offset) < 0) {
            ret = -EINVAL;
            break;
        }
        filp->f_pos += offset;
        ret = filp->f_pos;
        break;

    default:
        ret = -EINVAL;
        break;
    }

    return ret;
}

/* ── file_operations 结构体 ──────────────────────────────── */
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,
};

/* ── cdev 初始化函数 ─────────────────────────────────────── */
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
    int err;
    dev_t devno = MKDEV(globalmem_major, index);

    cdev_init(&dev->cdev, &globalmem_fops);
    dev->cdev.owner = THIS_MODULE;

    err = cdev_add(&dev->cdev, devno, 1);
    if (err)
        pr_err("globalmem: 添加 cdev 失败,错误码 %d\n", err);
}

/* ── 模块加载函数 ─────────────────────────────────────────── */
static int __init globalmem_init(void)
{
    int ret;
    dev_t devno;

    /* 1. 申请设备号 */
    if (globalmem_major) {
        /* 静态申请:使用预定义的主设备号 */
        devno = MKDEV(globalmem_major, 0);
        ret = register_chrdev_region(devno, DEVICE_NUM, "globalmem");
    } else {
        /* 动态申请:由内核分配主设备号 */
        ret = alloc_chrdev_region(&devno, 0, DEVICE_NUM, "globalmem");
        globalmem_major = MAJOR(devno);
    }

    if (ret < 0) {
        pr_err("globalmem: 申请设备号失败\n");
        return ret;
    }

    /* 2. 分配设备结构体内存(kzalloc 分配并清零) */
    globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
    if (!globalmem_devp) {
        ret = -ENOMEM;
        pr_err("globalmem: 内存分配失败\n");
        goto fail_malloc;
    }

    /* 3. 初始化互斥锁 */
    mutex_init(&globalmem_devp->mutex);

    /* 4. 初始化并注册 cdev */
    globalmem_setup_cdev(globalmem_devp, 0);

    /* 5. 创建设备类(在 /sys/class/ 下创建 globalmem 目录) */
    globalmem_class = class_create(THIS_MODULE, "globalmem");
    if (IS_ERR(globalmem_class)) {
        ret = PTR_ERR(globalmem_class);
        goto fail_class;
    }

    /* 6. 创建设备(触发 udev 自动创建 /dev/globalmem) */
    globalmem_device = device_create(globalmem_class, NULL,
                                      MKDEV(globalmem_major, 0),
                                      NULL, "globalmem");
    if (IS_ERR(globalmem_device)) {
        ret = PTR_ERR(globalmem_device);
        goto fail_device;
    }

    pr_info("globalmem: 驱动加载成功,主设备号 %d\n", globalmem_major);
    return 0;

fail_device:
    class_destroy(globalmem_class);
fail_class:
    cdev_del(&globalmem_devp->cdev);
    kfree(globalmem_devp);
fail_malloc:
    unregister_chrdev_region(MKDEV(globalmem_major, 0), DEVICE_NUM);
    return ret;
}

/* ── 模块卸载函数 ─────────────────────────────────────────── */
static void __exit globalmem_exit(void)
{
    device_destroy(globalmem_class, MKDEV(globalmem_major, 0));
    class_destroy(globalmem_class);
    cdev_del(&globalmem_devp->cdev);
    kfree(globalmem_devp);
    unregister_chrdev_region(MKDEV(globalmem_major, 0), DEVICE_NUM);
    pr_info("globalmem: 驱动已卸载\n");
}

module_init(globalmem_init);
module_exit(globalmem_exit);

MODULE_AUTHOR("参考宋宝华《Linux设备驱动开发详解》");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("globalmem 虚拟字符设备驱动");

6.3.2 各函数详解

open 函数详解
static int globalmem_open(struct inode *inode, struct file *filp)
{
    struct globalmem_dev *dev;

    /*
     * container_of(ptr, type, member)
     *
     * inode->i_cdev 是 struct cdev 类型的指针
     * globalmem_dev 中内嵌了 struct cdev(成员名为 cdev)
     * 通过 container_of 可以从 cdev 指针反推出 globalmem_dev 指针
     *
     * 原理:globalmem_dev 地址 = inode->i_cdev 地址 - cdev 在结构体中的偏移
     */
    dev = container_of(inode->i_cdev, struct globalmem_dev, cdev);

    /*
     * 将设备结构体指针保存到 file->private_data
     * 这样在 read/write/ioctl/release 中可以直接获取,无需全局变量
     */
    filp->private_data = dev;

    return 0;
}
read 函数详解
static ssize_t globalmem_read(struct file *filp, char __user *buf,
                               size_t size, loff_t *ppos)
{
    /*
     * 参数说明:
     * filp:文件结构体指针(包含 private_data、f_pos 等)
     * buf:用户空间缓冲区地址(__user 标记提醒不能直接访问)
     * size:用户请求读取的字节数
     * ppos:当前文件位置指针(读取后需要更新)
     *
     * 返回值:
     * > 0:实际读取的字节数
     * = 0:已到文件末尾(EOF)
     * < 0:错误码(如 -EFAULT、-EINVAL)
     */

    unsigned long p = *ppos;
    unsigned int count = size;
    int ret = 0;
    struct globalmem_dev *dev = filp->private_data;

    /* 边界检查1:读取起始位置超出设备内存 */
    if (p >= GLOBALMEM_SIZE)
        return 0;

    /* 边界检查2:调整读取长度,不超过剩余数据 */
    if (count > GLOBALMEM_SIZE - p)
        count = GLOBALMEM_SIZE - p;

    mutex_lock(&dev->mutex);

    if (copy_to_user(buf, dev->mem + p, count)) {
        ret = -EFAULT;
    } else {
        *ppos += count;   /* 更新文件位置 */
        ret = count;
    }

    mutex_unlock(&dev->mutex);
    return ret;
}
write 函数详解
static ssize_t globalmem_write(struct file *filp, const char __user *buf,
                                size_t size, loff_t *ppos)
{
    /*
     * 参数说明:
     * buf:用户空间数据地址(const __user,只读)
     * size:用户请求写入的字节数
     * ppos:当前文件位置(写入后更新)
     *
     * 返回值:
     * > 0:实际写入的字节数
     * < 0:错误码
     */

    unsigned long p = *ppos;
    unsigned int count = size;
    int ret = 0;
    struct globalmem_dev *dev = filp->private_data;

    if (p >= GLOBALMEM_SIZE)
        return 0;

    if (count > GLOBALMEM_SIZE - p)
        count = GLOBALMEM_SIZE - p;

    mutex_lock(&dev->mutex);

    if (copy_from_user(dev->mem + p, buf, count)) {
        ret = -EFAULT;
    } else {
        *ppos += count;
        ret = count;
    }

    mutex_unlock(&dev->mutex);
    return ret;
}
llseek 函数详解
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
    /*
     * 参数说明:
     * filp:文件结构体
     * offset:偏移量(可以为负数)
     * orig:基准位置
     *   SEEK_SET (0):从文件头
     *   SEEK_CUR (1):从当前位置
     *   SEEK_END (2):从文件尾(globalmem 不支持)
     *
     * 返回值:
     * >= 0:新的文件位置
     * < 0:错误码
     */

    loff_t ret = 0;

    switch (orig) {
    case SEEK_SET:
        /* 新位置 = offset,必须在 [0, GLOBALMEM_SIZE] 范围内 */
        if (offset < 0 || (unsigned int)offset > GLOBALMEM_SIZE) {
            ret = -EINVAL;
            break;
        }
        filp->f_pos = (unsigned int)offset;
        ret = filp->f_pos;
        break;

    case SEEK_CUR:
        /* 新位置 = 当前位置 + offset */
        if ((filp->f_pos + offset) > GLOBALMEM_SIZE ||
            (filp->f_pos + offset) < 0) {
            ret = -EINVAL;
            break;
        }
        filp->f_pos += offset;
        ret = filp->f_pos;
        break;

    default:
        ret = -EINVAL;
        break;
    }

    return ret;
}
ioctl 函数详解
/*
 * ioctl 命令编码规范(Linux 内核推荐)
 *
 * _IO(type, nr)           ← 无参数的命令
 * _IOR(type, nr, size)    ← 从驱动读数据(驱动→用户)
 * _IOW(type, nr, size)    ← 向驱动写数据(用户→驱动)
 * _IOWR(type, nr, size)   ← 双向数据传输
 *
 * type:幻数(Magic Number),区分不同驱动,通常用一个字母
 * nr:命令编号(0~255)
 * size:数据类型大小
 */

/* globalmem 的 ioctl 命令定义(通常放在头文件中) */
#define GLOBALMEM_MAGIC  'g'                        /* 幻数 */
#define MEM_CLEAR        _IO(GLOBALMEM_MAGIC, 0)    /* 清空内存,无参数 */
#define MEM_GETSIZE      _IOR(GLOBALMEM_MAGIC, 1, int) /* 获取内存大小 */

static long globalmem_ioctl(struct file *filp, unsigned int cmd,
                             unsigned long arg)
{
    struct globalmem_dev *dev = filp->private_data;
    int size;

    /*
     * 验证命令有效性(可选但推荐)
     * _IOC_TYPE(cmd):提取幻数
     * _IOC_NR(cmd):提取命令编号
     */
    if (_IOC_TYPE(cmd) != GLOBALMEM_MAGIC)
        return -ENOTTY;   /* 不是本驱动的命令 */

    switch (cmd) {
    case MEM_CLEAR:
        mutex_lock(&dev->mutex);
        memset(dev->mem, 0, GLOBALMEM_SIZE);
        mutex_unlock(&dev->mutex);
        pr_info("globalmem: 内存已清空\n");
        break;

    case MEM_GETSIZE:
        size = GLOBALMEM_SIZE;
        /* 将内核数据传递给用户空间 */
        if (copy_to_user((int __user *)arg, &size, sizeof(int)))
            return -EFAULT;
        break;

    default:
        return -EINVAL;
    }

    return 0;
}

6.4 使用文件私有数据

6.4.1 文件私有数据的概念

file->private_data 是 Linux 驱动开发中最重要的技巧之一。它允许驱动将设备相关的私有数据与打开的文件实例关联起来,避免使用全局变量,从而支持多设备和多实例访问。

private_data 的作用:

不使用 private_data(使用全局变量):
  ┌─────────────────────────────────────────────────────┐
  │  全局变量 globalmem_devp                             │
  │  所有 open 实例共享同一个设备结构体指针               │
  │  问题:无法支持多个设备实例                           │
  └─────────────────────────────────────────────────────┘

使用 private_data:
  进程A open("/dev/globalmem0") → file_A->private_data = &dev[0]
  进程B open("/dev/globalmem1") → file_B->private_data = &dev[1]
  进程C open("/dev/globalmem0") → file_C->private_data = &dev[0]

  每个 file 实例都有自己的 private_data,指向对应的设备结构体
  驱动函数通过 filp->private_data 获取设备,无需全局变量

6.4.2 private_data 的使用模式

/* ── 标准使用模式 ──────────────────────────────────────── */

/* open:设置 private_data */
static int my_open(struct inode *inode, struct file *filp)
{
    struct my_dev *dev;

    /* 方法一:通过 container_of(推荐,适用于内嵌 cdev 的结构体) */
    dev = container_of(inode->i_cdev, struct my_dev, cdev);
    filp->private_data = dev;

    /* 方法二:通过次设备号索引(适用于多设备) */
    int minor = iminor(inode);
    filp->private_data = &my_devices[minor];

    /* 方法三:动态分配(每次 open 创建独立的上下文) */
    struct my_context *ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
    if (!ctx) return -ENOMEM;
    ctx->dev = dev;
    ctx->flags = filp->f_flags;
    filp->private_data = ctx;

    return 0;
}

/* read/write/ioctl:使用 private_data */
static ssize_t my_read(struct file *filp, char __user *buf,
                        size_t count, loff_t *ppos)
{
    /* 直接从 private_data 获取设备结构体,无需全局变量 */
    struct my_dev *dev = filp->private_data;

    /* 使用 dev 进行操作 */
    return 0;
}

/* release:清理 private_data(如果是动态分配的) */
static int my_release(struct inode *inode, struct file *filp)
{
    /* 如果 private_data 是动态分配的,需要在此释放 */
    struct my_context *ctx = filp->private_data;
    kfree(ctx);
    filp->private_data = NULL;
    return 0;
}

6.4.3 支持多设备的 globalmem 驱动

将 globalmem 扩展为支持多个设备实例(globalmem0、globalmem1 等):

/*
 * globalmem_multi.c —— 支持多设备实例的 globalmem 驱动
 * 同时创建 DEVICE_NUM 个设备:/dev/globalmem0 ~ /dev/globalmemN
 */

#define GLOBALMEM_SIZE   0x1000
#define MEM_CLEAR        0x1
#define GLOBALMEM_MAJOR  230
#define DEVICE_NUM       2    /* 创建2个设备实例 */

struct globalmem_dev {
    struct cdev   cdev;
    unsigned char mem[GLOBALMEM_SIZE];
    struct mutex  mutex;
};

/* 设备数组:存放多个设备实例 */
static struct globalmem_dev globalmem_devs[DEVICE_NUM];
static struct class *globalmem_class;

/* open:通过次设备号选择对应的设备实例 */
static int globalmem_open(struct inode *inode, struct file *filp)
{
    /*
     * container_of 自动找到对应的 globalmem_dev
     * 因为每个设备都有自己的 cdev,inode->i_cdev 指向对应设备的 cdev
     */
    struct globalmem_dev *dev = container_of(inode->i_cdev,
                                              struct globalmem_dev, cdev);
    filp->private_data = dev;
    return 0;
}

/* read/write/ioctl 函数与单设备版本相同,通过 private_data 访问设备 */

static int __init globalmem_init(void)
{
    int ret, i;
    dev_t devno = MKDEV(GLOBALMEM_MAJOR, 0);

    /* 申请 DEVICE_NUM 个连续的设备号 */
    ret = register_chrdev_region(devno, DEVICE_NUM, "globalmem");
    if (ret < 0) return ret;

    /* 创建设备类 */
    globalmem_class = class_create(THIS_MODULE, "globalmem");
    if (IS_ERR(globalmem_class)) {
        ret = PTR_ERR(globalmem_class);
        goto fail_class;
    }

    /* 初始化每个设备实例 */
    for (i = 0; i < DEVICE_NUM; i++) {
        mutex_init(&globalmem_devs[i].mutex);

        /* 初始化并注册 cdev */
        cdev_init(&globalmem_devs[i].cdev, &globalmem_fops);
        globalmem_devs[i].cdev.owner = THIS_MODULE;
        ret = cdev_add(&globalmem_devs[i].cdev,
                       MKDEV(GLOBALMEM_MAJOR, i), 1);
        if (ret) goto fail_cdev;

        /* 创建设备文件:/dev/globalmem0, /dev/globalmem1 */
        device_create(globalmem_class, NULL,
                      MKDEV(GLOBALMEM_MAJOR, i),
                      NULL, "globalmem%d", i);
    }

    pr_info("globalmem: %d 个设备创建成功\n", DEVICE_NUM);
    return 0;

fail_cdev:
    /* 清理已创建的设备 */
    while (--i >= 0) {
        device_destroy(globalmem_class, MKDEV(GLOBALMEM_MAJOR, i));
        cdev_del(&globalmem_devs[i].cdev);
    }
    class_destroy(globalmem_class);
fail_class:
    unregister_chrdev_region(devno, DEVICE_NUM);
    return ret;
}

static void __exit globalmem_exit(void)
{
    int i;
    for (i = 0; i < DEVICE_NUM; i++) {
        device_destroy(globalmem_class, MKDEV(GLOBALMEM_MAJOR, i));
        cdev_del(&globalmem_devs[i].cdev);
    }
    class_destroy(globalmem_class);
    unregister_chrdev_region(MKDEV(GLOBALMEM_MAJOR, 0), DEVICE_NUM);
}

module_init(globalmem_init);
module_exit(globalmem_exit);
MODULE_LICENSE("GPL v2");

6.4.4 private_data 的高级用法

/*
 * 高级用法:每次 open 创建独立的上下文
 * 适用于需要为每个打开实例维护独立状态的场景
 * 例如:每个打开实例有独立的读写缓冲区、独立的状态机
 */

struct my_context {
    struct my_dev  *dev;        /* 指向设备结构体 */
    unsigned int    open_flags; /* 本次 open 的标志 */
    loff_t          read_pos;   /* 本实例的读位置 */
    loff_t          write_pos;  /* 本实例的写位置 */
    char            buf[256];   /* 本实例的私有缓冲区 */
};

static int my_open(struct inode *inode, struct file *filp)
{
    struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev);
    struct my_context *ctx;

    /* 为本次 open 分配独立的上下文 */
    ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
    if (!ctx)
        return -ENOMEM;

    ctx->dev = dev;
    ctx->open_flags = filp->f_flags;
    ctx->read_pos = 0;
    ctx->write_pos = 0;

    filp->private_data = ctx;

    /* 增加设备引用计数(如果需要) */
    atomic_inc(&dev->open_count);

    return 0;
}

static int my_release(struct inode *inode, struct file *filp)
{
    struct my_context *ctx = filp->private_data;
    struct my_dev *dev = ctx->dev;

    atomic_dec(&dev->open_count);

    /* 释放本次 open 分配的上下文 */
    kfree(ctx);
    filp->private_data = NULL;

    return 0;
}

6.5 globalmem 驱动在用户空间的验证

6.5.1 编译与加载驱动

Makefile

KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD        := $(shell pwd)

obj-m := globalmem.o

all:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

编译与加载

# 编译驱动
make
# 生成 globalmem.ko

# 加载驱动
sudo insmod globalmem.ko

# 验证驱动加载成功
dmesg | tail -3
# [  100.123] globalmem: 驱动加载成功,主设备号 230

# 查看设备文件(udev 自动创建)
ls -l /dev/globalmem
# crw------- 1 root root 230, 0 Jun 21 10:00 /dev/globalmem

# 查看 sysfs 中的设备信息
ls /sys/class/globalmem/
# globalmem

# 查看已加载模块
lsmod | grep globalmem
# globalmem    16384  0

6.5.2 使用 shell 命令验证

# 修改设备文件权限(允许普通用户访问)
sudo chmod 666 /dev/globalmem

# ── 测试 write ──────────────────────────────────────────
echo "Hello, globalmem!" > /dev/globalmem
# 将字符串写入设备内存

# ── 测试 read ───────────────────────────────────────────
cat /dev/globalmem
# Hello, globalmem!
# (后面跟着大量空字节,cat 会显示到 EOF)

# ── 测试 dd(精确读写)──────────────────────────────────
# 写入 512 字节的数据
dd if=/dev/urandom of=/dev/globalmem bs=512 count=1
# 1+0 records in
# 1+0 records out
# 512 bytes copied

# 从设备读取 512 字节
dd if=/dev/globalmem of=/tmp/output.bin bs=512 count=1
# 1+0 records in
# 1+0 records out
# 512 bytes copied

# 比较写入和读取的数据是否一致
# (先写入已知数据,再读取比较)
echo "test data 12345" > /dev/globalmem
head -c 16 /dev/globalmem
# test data 12345

# ── 测试 hexdump(查看原始数据)────────────────────────
hexdump -C /dev/globalmem | head -5
# 00000000  74 65 73 74 20 64 61 74  61 20 31 32 33 34 35 0a  |test data 12345.|
# 00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

6.5.3 编写 C 语言测试程序

/*
 * test_globalmem.c —— globalmem 驱动测试程序
 * 测试 open/read/write/lseek/ioctl 功能
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>

#define DEVICE_FILE  "/dev/globalmem"
#define MEM_CLEAR    0x1           /* ioctl 清空命令 */
#define BUF_SIZE     256

int main(void)
{
    int   fd;
    char  write_buf[BUF_SIZE];
    char  read_buf[BUF_SIZE];
    ssize_t n;
    int   ret;

    /* ── 1. 打开设备 ──────────────────────────────────── */
    fd = open(DEVICE_FILE, O_RDWR);
    if (fd < 0) {
        perror("open failed");
        return EXIT_FAILURE;
    }
    printf("✓ 设备打开成功,fd = %d\n", fd);

    /* ── 2. 写入数据 ──────────────────────────────────── */
    memset(write_buf, 0, sizeof(write_buf));
    snprintf(write_buf, sizeof(write_buf),
             "Hello from test program! Time: %ld", time(NULL));

    n = write(fd, write_buf, strlen(write_buf));
    if (n < 0) {
        perror("write failed");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("✓ 写入 %zd 字节:\"%s\"\n", n, write_buf);

    /* ── 3. 重置文件位置到头部 ────────────────────────── */
    ret = lseek(fd, 0, SEEK_SET);
    if (ret < 0) {
        perror("lseek failed");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("✓ lseek 到文件头,当前位置 = %d\n", ret);

    /* ── 4. 读取数据 ──────────────────────────────────── */
    memset(read_buf, 0, sizeof(read_buf));
    n = read(fd, read_buf, strlen(write_buf));
    if (n < 0) {
        perror("read failed");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("✓ 读取 %zd 字节:\"%s\"\n", n, read_buf);

    /* ── 5. 验证读写数据一致性 ────────────────────────── */
    if (memcmp(write_buf, read_buf, strlen(write_buf)) == 0) {
        printf("✓ 数据一致性验证通过!\n");
    } else {
        printf("✗ 数据不一致!写入:\"%s\",读取:\"%s\"\n",
               write_buf, read_buf);
    }

    /* ── 6. 测试 lseek SEEK_CUR ──────────────────────── */
    lseek(fd, 0, SEEK_SET);
    lseek(fd, 5, SEEK_CUR);   /* 从当前位置前进5字节 */
    memset(read_buf, 0, sizeof(read_buf));
    n = read(fd, read_buf, 5);
    printf("✓ 从偏移5读取5字节:\"%.*s\"\n", (int)n, read_buf);

    /* ── 7. 测试 ioctl(清空内存)────────────────────── */
    ret = ioctl(fd, MEM_CLEAR, 0);
    if (ret < 0) {
        perror("ioctl MEM_CLEAR failed");
    } else {
        printf("✓ ioctl MEM_CLEAR 执行成功\n");
    }

    /* 验证内存已清空 */
    lseek(fd, 0, SEEK_SET);
    memset(read_buf, 0xFF, sizeof(read_buf));  /* 先填充非零值 */
    n = read(fd, read_buf, 16);
    int all_zero = 1;
    for (int i = 0; i < n; i++) {
        if (read_buf[i] != 0) { all_zero = 0; break; }
    }
    printf("✓ 清空后验证:内存%s全为零\n", all_zero ? "" : "未");

    /* ── 8. 测试边界条件 ──────────────────────────────── */
    /* 测试写入超出设备内存大小 */
    lseek(fd, 4090, SEEK_SET);   /* 定位到接近末尾 */
    char boundary_buf[20] = "0123456789ABCDEFGHIJ";
    n = write(fd, boundary_buf, 20);
    printf("✓ 边界写入测试:请求20字节,实际写入 %zd 字节\n", n);
    /* 预期:只写入 4096-4090=6 字节 */

    /* ── 9. 关闭设备 ──────────────────────────────────── */
    close(fd);
    printf("✓ 设备关闭成功\n");

    return EXIT_SUCCESS;
}

编译与运行测试程序

# 编译测试程序
gcc -o test_globalmem test_globalmem.c
# 或交叉编译
arm-linux-gnueabihf-gcc -o test_globalmem test_globalmem.c

# 运行测试
sudo ./test_globalmem

# 预期输出:
# ✓ 设备打开成功,fd = 3
# ✓ 写入 45 字节:"Hello from test program! Time: 1719000000"
# ✓ lseek 到文件头,当前位置 = 0
# ✓ 读取 45 字节:"Hello from test program! Time: 1719000000"
# ✓ 数据一致性验证通过!
# ✓ 从偏移5读取5字节:"from "
# ✓ ioctl MEM_CLEAR 执行成功
# ✓ 清空后验证:内存全为零
# ✓ 边界写入测试:请求20字节,实际写入 6 字节
# ✓ 设备关闭成功

# 同时查看内核日志
dmesg | tail -10
# [  100.001] globalmem: 写入 45 字节,当前位置 0
# [  100.002] globalmem: 读取 45 字节,当前位置 0
# [  100.003] globalmem: 内存已清空

6.5.4 并发访问测试

/*
 * test_concurrent.c —— 并发访问测试
 * 验证互斥锁是否正确保护并发访问
 */
#include <stdio.h>
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define DEVICE_FILE "/dev/globalmem"
#define THREAD_NUM  4
#define LOOP_COUNT  100

void *thread_func(void *arg)
{
    int thread_id = *(int *)arg;
    int fd;
    char write_buf[64];
    char read_buf[64];
    int i;

    fd = open(DEVICE_FILE, O_RDWR);
    if (fd < 0) {
        perror("open failed");
        return NULL;
    }

    for (i = 0; i < LOOP_COUNT; i++) {
        /* 写入线程标识数据 */
        snprintf(write_buf, sizeof(write_buf),
                 "Thread-%d Loop-%d", thread_id, i);

        lseek(fd, 0, SEEK_SET);
        write(fd, write_buf, strlen(write_buf));

        /* 立即读回验证 */
        lseek(fd, 0, SEEK_SET);
        memset(read_buf, 0, sizeof(read_buf));
        read(fd, read_buf, strlen(write_buf));

        /* 验证数据一致性 */
        if (strncmp(write_buf, read_buf, strlen(write_buf)) != 0) {
            printf("线程 %d:数据不一致!写:\"%s\",读:\"%s\"\n",
                   thread_id, write_buf, read_buf);
        }
    }

    close(fd);
    printf("线程 %d 完成 %d 次读写\n", thread_id, LOOP_COUNT);
    return NULL;
}

int main(void)
{
    pthread_t threads[THREAD_NUM];
    int ids[THREAD_NUM];
    int i;

    printf("启动 %d 个线程并发访问 globalmem...\n", THREAD_NUM);

    for (i = 0; i < THREAD_NUM; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, thread_func, &ids[i]);
    }

    for (i = 0; i < THREAD_NUM; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("并发测试完成!\n");
    return 0;
}
# 编译并发测试程序
gcc -o test_concurrent test_concurrent.c -lpthread

# 运行并发测试
sudo ./test_concurrent
# 启动 4 个线程并发访问 globalmem...
# 线程 0 完成 100 次读写
# 线程 1 完成 100 次读写
# 线程 2 完成 100 次读写
# 线程 3 完成 100 次读写
# 并发测试完成!
# (如果互斥锁工作正常,不会出现"数据不一致"的错误)

6.5.5 驱动卸载验证

# 卸载驱动
sudo rmmod globalmem

# 验证设备文件已自动删除(udev 处理)
ls /dev/globalmem
# ls: cannot access '/dev/globalmem': No such file or directory

# 验证 sysfs 条目已删除
ls /sys/class/globalmem/
# ls: cannot access '/sys/class/globalmem/': No such file or directory

# 查看内核日志
dmesg | tail -3
# [  200.001] globalmem: 驱动已卸载

# 验证模块已卸载
lsmod | grep globalmem
# (无输出)

本章小结

章节核心知识点关键 API
6.1 字符设备驱动结构cdev/file_operations/设备号三大核心结构;驱动完整工作流程;file 与 inode 结构体;新旧注册接口对比cdev_init()cdev_add()alloc_chrdev_region()
6.2 globalmem设备描述虚拟内存设备的设计目标;4KB内核内存;支持的操作集设备规格定义
6.3 globalmem驱动完整驱动代码;open/read/write/llseek/ioctl 五大函数详解;边界检查;互斥锁保护;class_create/device_createcopy_to_user()copy_from_user()mutex_lock()
6.4 文件私有数据private_data 的概念与重要性;container_of 获取设备结构体;多设备支持;动态上下文分配container_of()filp->private_data
6.5 用户空间验证shell 命令验证;C 语言测试程序(含边界测试);并发访问测试;驱动卸载验证ddhexdumpioctl()

字符设备驱动开发的关键要点

1. 设备号管理
   优先使用 alloc_chrdev_region() 动态申请,避免设备号冲突

2. 数据安全传输
   内核↔用户空间数据交换必须使用 copy_to_user / copy_from_user
   永远不要直接解引用用户空间指针

3. 边界检查
   read/write 函数必须检查 *ppos 是否超出设备范围
   防止越界访问导致内核崩溃

4. 并发保护
   多进程/多线程可能同时访问设备
   使用 mutex 保护共享数据(进程上下文)
   使用 spinlock 保护中断上下文中的共享数据

5. 使用 private_data
   在 open() 中设置 filp->private_data
   避免使用全局变量,支持多设备实例

6. 资源管理
   init 函数中申请的资源必须在 exit 函数中逆序释放
   使用 goto 错误处理模式确保资源不泄漏

7. 自动创建设备文件
   使用 class_create + device_create 触发 udev
   无需手动 mknod,驱动加载后 /dev/xxx 自动出现

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

apzxs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值