找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

481

积分

0

好友

61

主题
发表于 昨天 06:12 | 查看: 6| 回复: 0

字符设备驱动

设备驱动分类

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():写入单个数据到用户空间

数据交换流程图

同步机制

  • 信号量:用于保护共享资源,防止并发访问冲突
  • 互斥锁:用于确保同一时间只有一个进程访问资源
  • 自旋锁:用于内核态下的短时间同步

总结

驱动开发关键

  1. 设备号管理:分配、注册和释放设备号
  2. cdev管理:初始化、添加和删除cdev结构
  3. 设备文件管理:创建设备文件和类别
  4. file_operations实现:实现设备的各种操作方法
  5. 同步机制:保护共享资源,防止并发访问冲突
  6. 数据交换:正确处理用户空间和内核空间的数据传输

通过这个简单的字符设备驱动示例,我们走完了从编写、编译、加载到测试的完整流程。理解并掌握这些基础步骤和核心概念,是进入更复杂的Linux内核驱动开发世界的关键。如果想进一步探讨或查找更多系统编程资料,可以访问云栈社区




上一篇:瑞芯微RV1126B芯片解析:3TOPS NPU与AI-ISP性能实测
下一篇:TypeScript索引访问类型详解:从基础到企业级应用的实践指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-18 21:33 , Processed in 0.213902 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表