字符设备驱动开发
好的!以下是对字符设备驱动开发的更详细分步指南,包含完整的代码示例、Makefile 配置和测试方法。
---
### **字符设备驱动开发详细步骤**
#### **1. 驱动代码框架(`my_char_driver.c`)**
```c
#include <linux/module.h>
#include <linux/fs.h> // 文件操作结构体 file_operations
#include <linux/cdev.h> // 字符设备结构体 cdev
#include <linux/device.h> // 设备类相关函数
#include <linux/uaccess.h> // copy_to_user/copy_from_user
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_char_class"
static int major_num;
static struct class *char_class = NULL;
static struct cdev my_cdev;
// 自定义设备数据结构(示例:包含一个缓冲区)
struct my_device_data {
char buffer[1024];
size_t buffer_len;
};
static struct my_device_data dev_data;
//---------------- 文件操作函数实现 -----------------
static int my_open(struct inode *inode, struct file *filp) {
printk(KERN_INFO "Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
printk(KERN_INFO "Device closed\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off) {
struct my_device_data *data = &dev_data;
// 检查是否已经读完
if (*off >= data->buffer_len)
return 0;
// 计算剩余可读数据长度
size_t remaining = data->buffer_len - *off;
len = min(len, remaining);
// 将内核缓冲区数据复制到用户空间
if (copy_to_user(buf, data->buffer + *off, len))
return -EFAULT;
*off += len; // 更新偏移量
return len;
}
static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) {
struct my_device_data *data = &dev_data;
// 检查写入是否超出缓冲区大小
if (*off + len > sizeof(data->buffer))
return -ENOMEM;
// 将用户空间数据复制到内核缓冲区
if (copy_from_user(data->buffer + *off, buf, len))
return -EFAULT;
*off += len;
data->buffer_len = *off; // 更新缓冲区长度
return len;
}
// 定义文件操作结构体
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
//---------------- 模块初始化和退出 -----------------
static int __init my_char_init(void) {
dev_t dev;
// 1. 动态申请主设备号
if (alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME) < 0) {
printk(KERN_ALERT "Failed to allocate device number\n");
return -1;
}
major_num = MAJOR(dev);
// 2. 初始化并注册字符设备
cdev_init(&my_cdev, &fops);
if (cdev_add(&my_cdev, dev, 1) < 0) {
unregister_chrdev_region(dev, 1);
printk(KERN_ALERT "Failed to add cdev\n");
return -1;
}
// 3. 创建设备类(用于自动创建设备节点)
char_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(char_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_ALERT "Failed to create class\n");
return PTR_ERR(char_class);
}
// 4. 创建设备节点(自动生成 /dev/my_char_dev)
device_create(char_class, NULL, dev, NULL, DEVICE_NAME);
printk(KERN_INFO "Device registered with major number %d\n", major_num);
return 0;
}
static void __exit my_char_exit(void) {
dev_t dev = MKDEV(major_num, 0);
// 销毁设备节点和类
device_destroy(char_class, dev);
class_destroy(char_class);
// 注销字符设备
cdev_del(&my_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "Device unregistered\n");
}
module_init(my_char_init);
module_exit(my_char_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A Simple Character Device Driver");
2. 编写 Makefile
obj-m += my_char_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
3. 编译并加载驱动
# 编译模块
make
# 加载驱动
sudo insmod my_char_driver.ko
# 查看设备号
cat /proc/devices | grep my_char_dev
# 检查设备节点是否自动创建
ls -l /dev/my_char_dev # 若未自动创建,手动创建:
# sudo mknod /dev/my_char_dev c <major_num> 0
# sudo chmod 666 /dev/my_char_dev
4. 测试驱动
- 写入数据到设备:
echo "Hello, Linux Driver!" > /dev/my_char_dev
- 从设备读取数据:
cat /dev/my_char_dev # 输出 "Hello, Linux Driver!"
- 查看内核日志:
dmesg | tail -n 10 # 应显示 "Device opened", "Device closed" 等日志
5. 关键概念详解
-
设备号分配:
alloc_chrdev_region
:动态分配主设备号,避免与系统设备冲突。MKDEV(major, minor)
:生成设备号(主设备号 + 次设备号)。
-
文件操作接口:
open
:设备打开时调用(可初始化资源)。release
:设备关闭时调用(释放资源)。read
/write
:必须使用copy_to_user
/copy_from_user
在内核和用户空间传递数据。
-
自动创建设备节点:
class_create
:创建设备类(在/sys/class/
生成目录)。device_create
:在/dev/
自动生成设备节点,无需手动mknod
。
6. 常见问题解决
- 权限不足:
sudo chmod 666 /dev/my_char_dev # 允许普通用户读写
- 模块加载失败:
- 检查
dmesg
输出,常见原因:函数未实现、符号未导出。
- 检查
- 写入后读取为空:
- 确保在
write
函数中更新缓冲区长度(如dev_data.buffer_len
)。
- 确保在
7. 扩展功能
-
添加 ioctl 控制:
#include <linux/ioctl.h> #define MY_IOCTL_CMD _IOR('k', 1, int) // 定义 ioctl 命令 static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case MY_IOCTL_CMD: printk(KERN_INFO "ioctl command received\n"); break; default: return -ENOTTY; } return 0; } // 在 file_operations 中添加: .unlocked_ioctl = my_ioctl,
-
支持多设备:
- 在
alloc_chrdev_region
中指定次设备号数量(如alloc_chrdev_region(&dev, 0, 2, DEVICE_NAME)
)。 - 在
cdev_add
中注册多个设备。
- 在
通过以上代码和步骤,你可以实现一个完整的字符设备驱动,并通过用户空间程序与其交互。建议从简单功能开始,逐步添加复杂逻辑(如并发控制、阻塞IO等)。
ubuntu@ubuntu:~/cstudy/chardevice$ vim my_char_driver.c
ubuntu@ubuntu:~/cstudy/chardevice$ vim Makefile
ubuntu@ubuntu:~/cstudy/chardevice$ make
make -C /lib/modules/5.15.0-138-generic/build M=/home/ubuntu/cstudy/chardevice modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-138-generic'
CC [M] /home/ubuntu/cstudy/chardevice/my_char_driver.o
/home/ubuntu/cstudy/chardevice/my_char_driver.c: In function ‘my_read’:
/home/ubuntu/cstudy/chardevice/my_char_driver.c:40:5: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
40 | size_t remaining = data->buffer_len - *off;
| ^~~~~~
MODPOST /home/ubuntu/cstudy/chardevice/Module.symvers
CC [M] /home/ubuntu/cstudy/chardevice/my_char_driver.mod.o
LD [M] /home/ubuntu/cstudy/chardevice/my_char_driver.ko
BTF [M] /home/ubuntu/cstudy/chardevice/my_char_driver.ko
Skipping BTF generation for /home/ubuntu/cstudy/chardevice/my_char_driver.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-138-generic'
ubuntu@ubuntu:~/cstudy/chardevice$ sudo insmod my_char_driver.ko
[sudo] password for ubuntu:
ubuntu@ubuntu:~/cstudy/chardevice$ cat /proc/devices | grep my_char_dev
238 my_char_dev
ubuntu@ubuntu:~/cstudy/chardevice$ ls -l /dev/my_char_dev
crw------- 1 root root 238, 0 Apr 26 12:29 /dev/my_char_dev
ubuntu@ubuntu:~/cstudy/chardevice$ echo "Hello, Linux Driver!" > /dev/my_char_dev
-bash: /dev/my_char_dev: Permission denied
ubuntu@ubuntu:~/cstudy/chardevice$ sudo echo "Hello, Linux Driver!" > /dev/my_char_dev
-bash: /dev/my_char_dev: Permission denied
ubuntu@ubuntu:~/cstudy/chardevice$ sudo chmod 666 /dev/my_char_dev
ubuntu@ubuntu:~/cstudy/chardevice$ echo "Hello, Linux Driver!" > /dev/my_char_dev
ubuntu@ubuntu:~/cstudy/chardevice$ cat /dev/my_char_dev
Hello, Linux Driver!
ubuntu@ubuntu:~/cstudy/chardevice$ dmesg | tail -n 10
dmesg: read kernel buffer failed: Operation not permitted
ubuntu@ubuntu:~/cstudy/chardevice$ sudo dmesg | tail -n 10
[ 1529.175921] clocksource: wd-tsc-wd read-back delay of 201000ns, clock-skew test skipped!
[ 1839.997249] clocksource: timekeeping watchdog on CPU3: hpet wd-wd read-back delay of 358000ns
[ 1839.997262] clocksource: wd-tsc-wd read-back delay of 261000ns, clock-skew test skipped!
[ 1954.852763] Device registered with major number 238
[ 2135.640308] clocksource: timekeeping watchdog on CPU0: hpet wd-wd read-back delay of 185000ns
[ 2135.640320] clocksource: wd-tsc-wd read-back delay of 178000ns, clock-skew test skipped!
[ 2217.541106] Device opened
[ 2217.541164] Device closed
[ 2239.696820] Device opened
[ 2239.697123] Device closed