《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 6 章 字符设备驱动
参考:宋宝华 著,机械工业出版社,2015年版
6.1 Linux 字符设备驱动结构
6.1.1 字符设备驱动的核心数据结构
Linux 字符设备驱动围绕三个核心数据结构展开:cdev、file_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 结构体
驱动的 open、read、write 等函数都会接收 file 或 inode 结构体指针,理解这两个结构体至关重要:
/*
* 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,这样在后续的read、write、ioctl函数中就可以通过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_create | copy_to_user()、copy_from_user()、mutex_lock() |
| 6.4 文件私有数据 | private_data 的概念与重要性;container_of 获取设备结构体;多设备支持;动态上下文分配 | container_of()、filp->private_data |
| 6.5 用户空间验证 | shell 命令验证;C 语言测试程序(含边界测试);并发访问测试;驱动卸载验证 | dd、hexdump、ioctl() |
字符设备驱动开发的关键要点
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年

1万+

被折叠的 条评论
为什么被折叠?



