字符设备驱动
设备驱动分类
Linux系统将设备分为三类:
- 字符设备:按字节流顺序访问的设备,如键盘、串口、LED等
- 块设备:可以随机访问固定大小数据块的设备,如硬盘、U盘等
- 网络设备:用于网络通信的设备,如网卡
字符设备驱动特点
- 面向流的设备,数据按顺序传输
- 通常不需要缓冲
- 通过设备文件(/dev目录下)访问
- 使用主设备号和次设备号标识
驱动开发环境
内核源码安装
# 安装内核源码
sudo apt-get install linux-source
# 解压源码
tar -jxvf /usr/src/linux-source-*.tar.bz2
开发工具安装
sudo apt-get install build-essential linux-headers-$(uname -r)
字符设备驱动开发流程
字符设备驱动实现
驱动框架结构
创建一个名为simple_char的字符设备驱动,实现基本的读写操作。理解Linux内核模块的基本结构和加载机制是开发驱动的第一步。
驱动实现示例
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/uaccess.h>
#include<linux/device.h>
#include<linux/slab.h>
// 设备名称
#define DEVICE_NAME "simple_char"
// 设备类别名称
#define CLASS_NAME "simple_class"
// 设备结构体
struct simple_char_dev {
struct cdev cdev; // 字符设备结构体
char data[256]; // 设备数据缓冲区
struct semaphore sem; // 信号量,用于同步
};
// 全局变量
static dev_t dev_num; // 设备号
static struct simple_char_dev *simple_dev; // 设备结构体指针
static struct class *simple_class; // 设备类别
static struct device *simple_device; // 设备
// 文件操作函数声明
static int simple_open(struct inode *inode, struct file *file);
static int simple_release(struct inode *inode, struct file *file);
static ssize_t simple_read(struct file *file, char __user *buf, size_t count, loff_t *pos);
static ssize_t simple_write(struct file *file, const char __user *buf, size_t count, loff_t *pos);
static long simple_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
// 文件操作结构体
static struct file_operations simple_fops = {
.owner = THIS_MODULE,
.open = simple_open,
.release = simple_release,
.read = simple_read,
.write = simple_write,
.unlocked_ioctl = simple_ioctl,
};
// 模块初始化函数
static int __init simple_init(void)
{
int ret;
printk(KERN_INFO "Simple Char Driver: Initializing\n");
// 1. 分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Simple Char Driver: Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "Simple Char Driver: Major = %d, Minor = %d\n",
MAJOR(dev_num), MINOR(dev_num));
// 2. 创建设备类别
simple_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(simple_class)) {
unregister_chrdev_region(dev_num, 1);
printk(KERN_ERR "Simple Char Driver: Failed to create class\n");
return PTR_ERR(simple_class);
}
// 3. 创建设备结构体
simple_dev = kmalloc(sizeof(struct simple_char_dev), GFP_KERNEL);
if (!simple_dev) {
class_destroy(simple_class);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ERR "Simple Char Driver: Failed to allocate device structure\n");
return -ENOMEM;
}
// 初始化数据缓冲区
memset(simple_dev->data, 0, sizeof(simple_dev->data));
// 初始化信号量
sema_init(&simple_dev->sem, 1);
// 4. 初始化cdev并添加到内核
cdev_init(&simple_dev->cdev, &simple_fops);
simple_dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&simple_dev->cdev, dev_num, 1);
if (ret < 0) {
kfree(simple_dev);
class_destroy(simple_class);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ERR "Simple Char Driver: Failed to add cdev\n");
return ret;
}
// 5. 创建设备文件
simple_device = device_create(simple_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(simple_device)) {
cdev_del(&simple_dev->cdev);
kfree(simple_dev);
class_destroy(simple_class);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ERR "Simple Char Driver: Failed to create device\n");
return PTR_ERR(simple_device);
}
printk(KERN_INFO "Simple Char Driver: Initialized successfully\n");
return 0;
}
// 模块退出函数
static void __exit simple_exit(void)
{
printk(KERN_INFO "Simple Char Driver: Exiting\n");
// 1. 销毁设备文件
device_destroy(simple_class, dev_num);
// 2. 删除cdev
cdev_del(&simple_dev->cdev);
// 3. 释放设备结构体
kfree(simple_dev);
// 4. 销毁设备类别
class_destroy(simple_class);
// 5. 释放设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Simple Char Driver: Exited successfully\n");
}
// 打开设备函数
static int simple_open(struct inode *inode, struct file *file)
{
struct simple_char_dev *dev;
// 获取设备结构体指针
dev = container_of(inode->i_cdev, struct simple_char_dev, cdev);
file->private_data = dev;
printk(KERN_INFO "Simple Char Driver: Device opened\n");
return 0;
}
// 释放设备函数
static int simple_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Simple Char Driver: Device released\n");
return 0;
}
// 读取设备函数
static ssize_t simple_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
struct simple_char_dev *dev = file->private_data;
ssize_t ret = 0;
// 等待信号量
if (down_interruptible(&dev->sem)) {
return -ERESTARTSYS;
}
// 检查读取位置是否超出范围
if (*pos >= sizeof(dev->data)) {
goto out;
}
// 调整读取长度
if (count > sizeof(dev->data) - *pos) {
count = sizeof(dev->data) - *pos;
}
// 从内核空间复制到用户空间
if (copy_to_user(buf, dev->data + *pos, count)) {
ret = -EFAULT;
goto out;
}
// 更新读取位置
*pos += count;
ret = count;
printk(KERN_INFO "Simple Char Driver: Read %ld bytes\n", count);
out:
up(&dev->sem);
return ret;
}
// 写入设备函数
static ssize_t simple_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
struct simple_char_dev *dev = file->private_data;
ssize_t ret = 0;
// 等待信号量
if (down_interruptible(&dev->sem)) {
return -ERESTARTSYS;
}
// 检查写入位置是否超出范围
if (*pos >= sizeof(dev->data)) {
goto out;
}
// 调整写入长度
if (count > sizeof(dev->data) - *pos) {
count = sizeof(dev->data) - *pos;
}
// 从用户空间复制到内核空间
if (copy_from_user(dev->data + *pos, buf, count)) {
ret = -EFAULT;
goto out;
}
// 更新写入位置
*pos += count;
ret = count;
printk(KERN_INFO "Simple Char Driver: Written %ld bytes\n", count);
out:
up(&dev->sem);
return ret;
}
// IOCTL函数
static long simple_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct simple_char_dev *dev = file->private_data;
int ret = 0;
// 等待信号量
if (down_interruptible(&dev->sem)) {
return -ERESTARTSYS;
}
switch (cmd) {
case 0x100: // 清除缓冲区
memset(dev->data, 0, sizeof(dev->data));
printk(KERN_INFO "Simple Char Driver: Buffer cleared\n");
break;
case 0x101: // 获取缓冲区大小
ret = copy_to_user((int __user *)arg, &(sizeof(dev->data)), sizeof(int));
if (ret) {
ret = -EFAULT;
}
break;
default:
ret = -EINVAL;
break;
}
up(&dev->sem);
return ret;
}
// 模块注册
module_init(simple_init);
module_exit(simple_exit);
// 模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_VERSION("1.0");
Makefile编写
创建一个Makefile用于编译内核模块:
obj-m += simple_char.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
编译与加载驱动
编译驱动
make
编译成功后,会生成以下文件:
simple_char.ko:内核模块文件
simple_char.mod.c:模块依赖信息
simple_char.mod.o:编译后的模块依赖对象
simple_char.o:编译后的驱动对象
modules.order:模块顺序文件
Module.symvers:模块符号版本文件
加载驱动
# 加载内核模块
sudo insmod simple_char.ko
# 查看是否加载成功
lsmod | grep simple_char
# 查看设备号
cat /proc/devices | grep simple_char
创建设备文件
# 手动创建设备文件(如果device_create没有自动创建)
sudo mknod /dev/simple_char c <major> <minor>
# 设置设备文件权限
sudo chmod 666 /dev/simple_char
查看驱动日志
dmesg | grep "Simple Char Driver"
用户空间测试
用户空间测试程序test_simple_char.c验证:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include<sys/ioctl.h>
#define DEVICE_FILE "/dev/simple_char"
int main() {
int fd;
char buffer[256];
int size;
// 打开设备文件
fd = open(DEVICE_FILE, O_RDWR);
if (fd < 0) {
perror("Failed to open device file");
return 1;
}
printf("Device opened successfully\n");
// 写入数据
const char *test_data = "Hello, Linux Character Driver!";
if (write(fd, test_data, strlen(test_data)) < 0) {
perror("Failed to write to device");
close(fd);
return 1;
}
printf("Written: %s\n", test_data);
// 重置文件位置
lseek(fd, 0, SEEK_SET);
// 读取数据
memset(buffer, 0, sizeof(buffer));
if (read(fd, buffer, sizeof(buffer)) < 0) {
perror("Failed to read from device");
close(fd);
return 1;
}
printf("Read: %s\n", buffer);
// 使用IOCTL获取缓冲区大小
if (ioctl(fd, 0x101, &size) < 0) {
perror("Failed to get buffer size");
close(fd);
return 1;
}
printf("Buffer size: %d bytes\n", size);
// 使用IOCTL清除缓冲区
if (ioctl(fd, 0x100) < 0) {
perror("Failed to clear buffer");
close(fd);
return 1;
}
printf("Buffer cleared\n");
// 验证缓冲区已清除
lseek(fd, 0, SEEK_SET);
memset(buffer, 0, sizeof(buffer));
if (read(fd, buffer, sizeof(buffer)) < 0) {
perror("Failed to read from device");
close(fd);
return 1;
}
printf("Read after clear: %s\n", buffer);
// 关闭设备文件
close(fd);
printf("Device closed\n");
return 0;
}
编译并运行测试程序:
gcc -o test_simple_char test_simple_char.c
./test_simple_char
卸载驱动
# 卸载内核模块
sudo rmmod simple_char
# 查看日志确认卸载
核心概念
设备号
- 主设备号:标识设备驱动
- 次设备号:标识同一驱动下的不同设备
- 设备号类型:
dev_t(32位,高12位主设备号,低20位次设备号)
file_operations结构体
| 成员函数 |
功能 |
owner |
模块所有者 |
open |
打开设备 |
release |
关闭设备 |
read |
从设备读取数据 |
write |
向设备写入数据 |
unlocked_ioctl |
设备控制命令 |
llseek |
定位文件指针 |
用户空间与内核空间数据交换
数据交换机制
Linux内核提供了专门的函数用于用户空间和内核空间之间的数据交换,这部分涉及到系统的核心内存管理机制:
copy_to_user():从内核空间复制数据到用户空间
copy_from_user():从用户空间复制数据到内核空间
get_user():获取单个用户空间数据
put_user():写入单个数据到用户空间
数据交换流程图
同步机制
- 信号量:用于保护共享资源,防止并发访问冲突
- 互斥锁:用于确保同一时间只有一个进程访问资源
- 自旋锁:用于内核态下的短时间同步
总结
驱动开发关键
- 设备号管理:分配、注册和释放设备号
- cdev管理:初始化、添加和删除cdev结构
- 设备文件管理:创建设备文件和类别
- file_operations实现:实现设备的各种操作方法
- 同步机制:保护共享资源,防止并发访问冲突
- 数据交换:正确处理用户空间和内核空间的数据传输
通过这个简单的字符设备驱动示例,我们走完了从编写、编译、加载到测试的完整流程。理解并掌握这些基础步骤和核心概念,是进入更复杂的Linux内核驱动开发世界的关键。如果想进一步探讨或查找更多系统编程资料,可以访问云栈社区。