Day 10–11 · 字符设备驱动¶
预计时长:2 小时 / 天,共 4 小时
类型:实验
做什么¶
实现一个功能完整的字符设备驱动,覆盖设备号申请、cdev 注册、file_operations 实现、自动创建 /dev 节点、用户空间数据交换。结束时你能从用户态 open/read/write/close 这个设备,数据在内核缓冲区里被正确读写。
要了解什么¶
1. 设备号:主设备号 + 次设备号¶
Linux 用 dev_t(32位)表示设备号:高 12 位是主设备号(标识驱动),低 20 位是次设备号(标识同驱动下的具体设备)。
动态分配(现代推荐做法):
dev_t devno;
alloc_chrdev_region(&devno, 0, 1, "mydev");
// 参数:&devno(输出),baseminor(起始次设备号),count(设备数量),name
静态指定(老做法,可能冲突):
用动态分配,主设备号在 /proc/devices 中可查。
2. cdev 体系(不用 register_chrdev)¶
register_chrdev 是老接口,一个主设备号只能对应一套 file_operations。cdev 体系更灵活,可以给不同次设备号绑定不同 file_operations。
struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops); // 绑定 file_operations
my_cdev.owner = THIS_MODULE;
cdev_add(&my_cdev, devno, 1); // 注册到内核(之后设备就可访问了)
// 注意:cdev_add 之后驱动必须准备好响应,因为用户可能立刻 open
3. copy_to_user / copy_from_user¶
绝对不能在内核态直接 dereference 用户指针。 理由: 1. 用户指针可能是无效地址(用户故意传 NULL 或非法地址攻击内核) 2. 用户空间内存可能被换出(page fault 在某些内核上下文不合法) 3. 32/64 位混合模式下地址空间不同
// 从用户空间读取数据到内核缓冲区
ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
char kbuf[256];
if (count > sizeof(kbuf))
count = sizeof(kbuf);
if (copy_from_user(kbuf, buf, count))
return -EFAULT; // 返回 -EFAULT 表示地址无效
// 处理 kbuf...
return count;
}
// 向用户空间写数据(从内核角度是"读"操作)
ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
char *data = "hello from kernel\n";
size_t len = strlen(data);
if (*pos >= len) return 0; // EOF
if (copy_to_user(buf, data + *pos, min(count, len - *pos)))
return -EFAULT;
*pos += min(count, len - *pos);
return min(count, len - *pos);
}
4. class_create + device_create:自动生成 /dev 节点¶
没有 device_create,加载驱动后需要手动 mknod /dev/mydev c 主设备号 次设备号。用 class/device 机制,udev 会自动创建节点:
// 在 init 函数中
struct class *my_class = class_create(THIS_MODULE, "mydev_class");
struct device *my_device = device_create(my_class, NULL, devno, NULL, "mydev");
// 之后 /dev/mydev 自动出现
// 在 exit 函数中
device_destroy(my_class, devno);
class_destroy(my_class);
5. open 中的私有数据传递¶
struct my_dev {
char buffer[1024];
size_t buf_len;
struct cdev cdev;
// ...
};
static int my_open(struct inode *inode, struct file *filp)
{
struct my_dev *dev;
// inode->i_cdev 指向我们注册的 cdev
dev = container_of(inode->i_cdev, struct my_dev, cdev);
filp->private_data = dev; // 存入 file,后续 read/write 直接用
return 0;
}
练习¶
练习:完整字符设备驱动¶
chardev.c(完整实现,带详细注释):
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/mutex.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("learner");
MODULE_DESCRIPTION("Character device driver practice");
#define DEVICE_NAME "mychardev"
#define BUF_SIZE 4096
struct chardev_data {
char *buffer;
size_t buf_len;
struct mutex lock; // 保护 buffer 的互斥锁
struct cdev cdev;
};
static dev_t devno;
static struct class *chardev_class;
static struct chardev_data *chardev;
/* ——— file_operations 实现 ——— */
static int chardev_open(struct inode *inode, struct file *filp)
{
struct chardev_data *dev;
dev = container_of(inode->i_cdev, struct chardev_data, cdev);
filp->private_data = dev;
pr_info(DEVICE_NAME ": opened (pid=%d)\n", current->pid);
return 0;
}
static int chardev_release(struct inode *inode, struct file *filp)
{
pr_info(DEVICE_NAME ": released\n");
return 0;
}
static ssize_t chardev_read(struct file *filp, char __user *buf,
size_t count, loff_t *pos)
{
struct chardev_data *dev = filp->private_data;
ssize_t ret;
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS; // 被信号中断,让系统调用重启
if (*pos >= dev->buf_len) {
ret = 0; // EOF
goto out;
}
count = min(count, dev->buf_len - (size_t)*pos);
if (copy_to_user(buf, dev->buffer + *pos, count)) {
ret = -EFAULT;
goto out;
}
*pos += count;
ret = count;
pr_debug(DEVICE_NAME ": read %zu bytes at offset %lld\n", count, *pos - count);
out:
mutex_unlock(&dev->lock);
return ret;
}
static ssize_t chardev_write(struct file *filp, const char __user *buf,
size_t count, loff_t *pos)
{
struct chardev_data *dev = filp->private_data;
ssize_t ret;
if (count > BUF_SIZE)
count = BUF_SIZE;
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
if (copy_from_user(dev->buffer, buf, count)) {
ret = -EFAULT;
goto out;
}
dev->buf_len = count;
*pos = count;
ret = count;
pr_info(DEVICE_NAME ": wrote %zu bytes\n", count);
out:
mutex_unlock(&dev->lock);
return ret;
}
static const struct file_operations chardev_fops = {
.owner = THIS_MODULE,
.open = chardev_open,
.release = chardev_release,
.read = chardev_read,
.write = chardev_write,
.llseek = default_llseek,
};
/* ——— 模块初始化 / 退出 ——— */
static int __init chardev_init(void)
{
int ret;
/* 1. 动态分配设备号 */
ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err(DEVICE_NAME ": alloc_chrdev_region failed: %d\n", ret);
return ret;
}
pr_info(DEVICE_NAME ": major=%d, minor=%d\n", MAJOR(devno), MINOR(devno));
/* 2. 分配设备数据结构 */
chardev = kzalloc(sizeof(*chardev), GFP_KERNEL);
if (!chardev) {
ret = -ENOMEM;
goto err_region;
}
chardev->buffer = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!chardev->buffer) {
ret = -ENOMEM;
goto err_chardev;
}
mutex_init(&chardev->lock);
/* 3. 初始化并注册 cdev */
cdev_init(&chardev->cdev, &chardev_fops);
chardev->cdev.owner = THIS_MODULE;
ret = cdev_add(&chardev->cdev, devno, 1);
if (ret < 0) {
pr_err(DEVICE_NAME ": cdev_add failed: %d\n", ret);
goto err_buffer;
}
/* 4. 创建设备类和设备节点(生成 /dev/mychardev) */
chardev_class = class_create(THIS_MODULE, DEVICE_NAME "_class");
if (IS_ERR(chardev_class)) {
ret = PTR_ERR(chardev_class);
goto err_cdev;
}
if (IS_ERR(device_create(chardev_class, NULL, devno, NULL, DEVICE_NAME))) {
ret = PTR_ERR(device_create(chardev_class, NULL, devno, NULL, DEVICE_NAME));
goto err_class;
}
pr_info(DEVICE_NAME ": initialized, /dev/%s created\n", DEVICE_NAME);
return 0;
err_class:
class_destroy(chardev_class);
err_cdev:
cdev_del(&chardev->cdev);
err_buffer:
kfree(chardev->buffer);
err_chardev:
kfree(chardev);
err_region:
unregister_chrdev_region(devno, 1);
return ret;
}
static void __exit chardev_exit(void)
{
device_destroy(chardev_class, devno);
class_destroy(chardev_class);
cdev_del(&chardev->cdev);
kfree(chardev->buffer);
kfree(chardev);
unregister_chrdev_region(devno, 1);
pr_info(DEVICE_NAME ": removed\n");
}
module_init(chardev_init);
module_exit(chardev_exit);
配套用户态测试程序 test_chardev.c:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int fd;
char wbuf[] = "Hello from userspace! Testing chardev.\n";
char rbuf[256] = {0};
ssize_t n;
fd = open("/dev/mychardev", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
/* 写入 */
n = write(fd, wbuf, strlen(wbuf));
printf("Wrote %zd bytes\n", n);
/* 重置文件偏移 */
lseek(fd, 0, SEEK_SET);
/* 读回 */
n = read(fd, rbuf, sizeof(rbuf) - 1);
printf("Read %zd bytes: %s", n, rbuf);
/* 验证数据一致 */
if (strcmp(wbuf, rbuf) == 0)
printf("Data integrity: OK\n");
else
printf("Data integrity: MISMATCH!\n");
close(fd);
return 0;
}
Makefile(驱动 + 测试程序):
KDIR ?= ~/kernel/linux-stable
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-gnueabihf-
obj-m += chardev.o
all: module usertest
module:
make -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
usertest:
$(CROSS_COMPILE)gcc -o test_chardev test_chardev.c
clean:
make -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
rm -f test_chardev
在 QEMU 中验证:
- [ ] 把
chardev.ko和test_chardev打包进 initramfs,启动 QEMU - [ ]
insmod chardev.ko,检查dmesg,确认主设备号 - [ ]
ls -la /dev/mychardev,确认设备节点存在 - [ ]
./test_chardev,验证写入读出 - [ ] 用
cat /dev/mychardev验证读接口 - [ ] 用
echo "test" > /dev/mychardev验证写接口 - [ ]
rmmod chardev,确认/dev/mychardev消失
延伸阅读¶
| 资料 | 具体位置 | 说明 |
|---|---|---|
| Linux Device Drivers Corbet 等 | Ch.3 "Char Drivers" | 字符驱动经典讲解,LDD3 |
| 《Linux 设备驱动开发详解》宋宝华 | 第 4–5 章 | 字符设备详细讲解,例子丰富 |
| Linux Device Driver Development John Madieu | Ch.3 | 基于 5.x 内核的字符驱动 |
| 内核源码 | drivers/char/mem.c |
内核自带的经典字符驱动(/dev/null, /dev/zero 等) |
| 内核源码 | include/linux/fs.h |
file_operations 结构体定义,每个函数指针的文档在这 |