大家好,我是王鸽,这篇文章主要是Linux 编写一个字符设备驱动实例。Linux系统的设备可以划分为三大类:字符设备、块设备和网络设备。内核针对每一类设备都提供了对应的驱动模型框架,包括基本的内核设施和文件系统接口。这样设备驱动程序员在写某类设备驱动程序时,就有一套完整的驱动模型框架可以使用,从而可将大量的精力放在设备本身的操作上。内核版本不一样,导致API可能有变化,在较新的内核版本中,结构体的某些成员函数可能有所变化,或者设备注册的函数可能有更新。
字符设备包括模块的初始化、设备的注册、以及基本的读写操作。同时,他们可能希望了解如何测试这个驱动,比如使用用户空间的程序进行读写操作。因此,除了驱动代码,可能还需要提供一个简单的Makefile和测试步骤。
接下来,我需要考虑代码的结构。通常,一个字符设备驱动包括以下部分:
- 模块的初始化和退出函数(init_module和cleanup_module)。
- 设备的注册和注销,使用register_chrdev和unregister_chrdev。
- 实现file_operations结构体中的open、release、read、write等函数。
需要注意的是,在Linux内核编程中,不能使用标准库函数,必须使用内核提供的函数,比如printk代替printf,以及处理错误码的方式(返回负的错误码)。
simple_char_driver.c
#include<linux/module.h>#include<linux/fs.h>// file_operations#include<linux/uaccess.h>// copy_to_user/copy_from_user#include<linux/init.h>#include<linux/slab.h>// kmalloc/kfree#include<linux/device.h>// class_create/device_create#define DEVICE_NAME "simple_char"#define CLASS_NAME "simple_char_class"static int major_num;static char *device_buffer; // 设备内存缓冲区static struct class *char_class;static struct device *char_device;// 定义互斥锁防止并发访问staticDEFINE_MUTEX(buffer_mutex);// ----------------------------// 文件操作函数// ----------------------------staticintdevice_open(struct inode *inode, struct file *file){if (!mutex_trylock(&buffer_mutex)) { printk(KERN_ERR "Device is busy\n"); return -EBUSY;} return 0;}staticintdevice_release(struct inode *inode, struct file *file){ mutex_unlock(&buffer_mutex); return 0;}staticssize_tdevice_read(struct file *filp, char __user *buf, size_t len, loff_t *off){int bytes_read = 0;// 将内核缓冲区数据复制到用户空间if (copy_to_user(buf, device_buffer, len)) {return -EFAULT;} bytes_read = len; return bytes_read;}staticssize_tdevice_write(struct file *filp, constchar __user *buf, size_t len, loff_t *off){// 将用户空间数据复制到内核缓冲区 if (copy_from_user(device_buffer, buf, len)) { return -EFAULT;}return len;}// 定义文件操作结构体static struct file_operations fops = { .owner = THIS_MODULE, .open = device_open, .release = device_release, .read = device_read, .write = device_write,};// ----------------------------// 模块初始化和退出// ----------------------------staticint __init simple_char_init(void){// 1. 动态分配主设备号major_num = register_chrdev(0, DEVICE_NAME, &fops);if (major_num < 0) { printk(KERN_ALERT "Failed to register char device\n"); return major_num;}// 2. 创建设备类(用于自动创建设备节点)char_class = class_create(THIS_MODULE, CLASS_NAME);if (IS_ERR(char_class)) { unregister_chrdev(major_num, DEVICE_NAME); return PTR_ERR(char_class);}// 3. 创建设备文件 /dev/simple_charchar_device = device_create(char_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);if (IS_ERR(char_device)) { class_destroy(char_class); unregister_chrdev(major_num, DEVICE_NAME); return PTR_ERR(char_device);}// 4. 分配缓冲区内存(1KB)device_buffer = kmalloc(1024, GFP_KERNEL);if (!device_buffer) { device_destroy(char_class, MKDEV(major_num, 0)); class_destroy(char_class); unregister_chrdev(major_num, DEVICE_NAME); return -ENOMEM;}printk(KERN_INFO "Simple char device loaded (major=%d)\n", major_num);return 0;}staticvoid __exit simple_char_exit(void){// 清理资源kfree(device_buffer);device_destroy(char_class, MKDEV(major_num, 0));class_destroy(char_class);unregister_chrdev(major_num, DEVICE_NAME);printk(KERN_INFO "Simple char device unloaded\n");}module_init(simple_char_init);module_exit(simple_char_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Your Name");MODULE_DESCRIPTION("A simple character device driver");
2. 编译驱动的Makefile
obj-m += simple_char_driver.oKDIR ?= /lib/modules/$(shell uname -r)/buildall: make -C $(KDIR) M=$(PWD) modulesclean: make -C $(KDIR) M=$(PWD) clean
3. 编译与测试步骤
(1) 编译驱动
make
生成 simple_char_driver.ko 内核模块。
(2) 加载驱动
sudo insmod simple_char_driver.ko
检查设备号和节点:
dmesg | tail 查看加载日志(确认 major number)ls -l /dev/simple_char # 检查设备节点是否存在
(3) 测试读写
写入数据到设备echo "Hello, Kernel!" > /dev/simple_char# 从设备读取数据cat /dev/simple_char # 输出应为 "Hello, Kernel!"
(4) 卸载驱动
sudo rmmod simple_char_driver
4. 关键代码解析
| |
|---|
| |
| class_create / device_create | 自动在 /dev 下创建设备节点(无需手动 mknod) |
| copy_to_user / copy_from_user | |
| mutex_trylock / mutex_unlock | |
| |
5. 常见问题解决
权限不足:确保用户有访问 /dev/simple_char 的权限:
sudo chmod 666 /dev/simple_char
模块加载失败:
检查内核日志使用命令:dmesg | tail
确认内核头文件已安装:sudo apt install linux-headers-$(uname -r)
读写数据异常:确保测试字符串长度不超过缓冲区大小(代码中为1KB)。
此示例实现了一个基本的字符设备驱动,包含并发控制和自动设备节点创建功能,适用于学习或快速原型开发。