三、linux字符驱动详解

news/2025/2/22 18:12:02

在上一节完成NFS开发环境的搭建后,本节将探讨Linux字符设备驱动的开发。字符设备驱动作为Linux内核的重要组成部分,主要负责管理与字符设备(如串口、键盘等)的交互,并为用户空间程序提供统一的读写操作接口。

驱动代码

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/uaccess.h>
#include <linux/cdev.h> 

#define DEVICE_NAME "hello_chrdev"
#define BUFFER_SIZE 100

// 设备结构体
typedef struct {
    char buffer[BUFFER_SIZE];
    struct class *class;
    struct device *device;
    dev_t dev_num;
    struct cdev cdev;
} HelloDevice;

static HelloDevice hello_dev;

// 打开设备
static int hello_open(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "Hello device opened\n");
    return 0;
}

// 读取设备
static ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    size_t len = strlen(hello_dev.buffer);
    if (*f_pos >= len) {
        return 0;
    }
    if (count > len - *f_pos) {
        count = len - *f_pos;
    }
    if (copy_to_user(buf, hello_dev.buffer + *f_pos, count)) {
        return -EFAULT;
    }
    *f_pos += count;
    return count;
}

// 写入设备
static ssize_t hello_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    if (count > BUFFER_SIZE - 1) {
        count = BUFFER_SIZE - 1;
    }
    if (copy_from_user(hello_dev.buffer, buf, count)) {
        return -EFAULT;
    }
    hello_dev.buffer[count] = '\0';
    *f_pos += count;
    return count;
}

// 关闭设备
static int hello_release(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "Hello device closed\n");
    return 0;
}

// 文件操作结构体
static struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .read = hello_read,
    .write = hello_write,
    .release = hello_release,
};

// 模块初始化函数
static int __init hello_init(void) {
    int ret;

    // 分配设备号
    ret = alloc_chrdev_region(&hello_dev.dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        printk(KERN_ERR "Failed to allocate character device number\n");
        return ret;
    }

    // 创建类
    hello_dev.class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(hello_dev.class)) {
        unregister_chrdev_region(hello_dev.dev_num, 1);
        printk(KERN_ERR "Failed to create class\n");
        return PTR_ERR(hello_dev.class);
    }

    // 创建设备
    hello_dev.device = device_create(hello_dev.class, NULL, hello_dev.dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(hello_dev.device)) {
        class_destroy(hello_dev.class);
        unregister_chrdev_region(hello_dev.dev_num, 1);
        printk(KERN_ERR "Failed to create device\n");
        return PTR_ERR(hello_dev.device);
    }

    // 初始化 cdev 结构体
    cdev_init(&hello_dev.cdev, &hello_fops);
    hello_dev.cdev.owner = THIS_MODULE;

    // 添加字符设备到系统
    ret = cdev_add(&hello_dev.cdev, hello_dev.dev_num, 1);
    if (ret < 0) {
        device_destroy(hello_dev.class, hello_dev.dev_num);
        class_destroy(hello_dev.class);
        unregister_chrdev_region(hello_dev.dev_num, 1);
        printk(KERN_ERR "Failed to add character device\n");
        return ret;
    }

    printk(KERN_INFO "Hello device initialized. Major: %d, Minor: %d\n", MAJOR(hello_dev.dev_num), MINOR(hello_dev.dev_num));
    return 0;
}

// 模块卸载函数
static void __exit hello_exit(void) {
    cdev_del(&hello_dev.cdev);
    device_destroy(hello_dev.class, hello_dev.dev_num);
    class_destroy(hello_dev.class);
    unregister_chrdev_region(hello_dev.dev_num, 1);
    printk(KERN_INFO "Hello device removed\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello world character device driver");

函数接口详解

1. 模块初始化与退出相关函数
alloc_chrdev_region
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
  • 功能:动态分配一组连续的字符设备号。
  • 参数
    • dev:用于存储分配到的设备号。
    • baseminor:起始的次设备号。
    • count:要分配的设备号数量。
    • name:设备的名称,用于在 /proc/devices 中显示。
  • 返回值:成功返回 0,失败返回负数错误码。
class_create
struct class *class_create(struct module *owner, const char *name);
  • 功能:在 /sys/class 目录下创建一个设备类。
  • 参数
    • owner:指向模块的指针,通常为 THIS_MODULE
    • name:类的名称。
  • 返回值:成功返回指向 struct class 的指针,失败返回错误指针。
device_create
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
  • 功能:在 /sys/class/<class_name> 目录下创建设备节点,并在 /dev 目录下创建对应的设备文件。
  • 参数
    • class:指向设备类的指针。
    • parent:父设备指针,通常为 NULL
    • devt:设备号。
    • drvdata:设备驱动数据,通常为 NULL
    • fmt:设备名称的格式化字符串。
  • 返回值:成功返回指向 struct device 的指针,失败返回错误指针。
cdev_init
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
  • 功能:初始化字符设备结构体 struct cdev,并关联文件操作结构体 struct file_operations
  • 参数
    • cdev:指向 struct cdev 的指针。
    • fops:指向 struct file_operations 的指针。
cdev_add
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
  • 功能:将字符设备添加到内核中。
  • 参数
    • p:指向 struct cdev 的指针。
    • dev:设备号。
    • count:设备数量。
  • 返回值:成功返回 0,失败返回负数错误码。
module_initmodule_exit
module_init(hello_init);
module_exit(hello_exit);
  • 功能:分别指定模块加载和卸载时调用的函数。
2. 文件操作相关函数
  • 在 Linux 内核中,struct file_operations 结构体是字符设备驱动与用户空间进行交互的关键桥梁,其中 openreadwriterelease 是比较常用的操作函数。

    struct file_operations` 结构体中相关成员介绍

    open 函数
    int (*open) (struct inode *inode, struct file *filp);
    
    • 功能:当用户空间使用 open() 系统调用打开设备文件时,内核会调用驱动中注册的 open 函数。该函数通常用于执行设备的初始化操作,如分配资源、检查设备状态等。
    • 参数
      • struct inode *inode:指向文件对应的索引节点,包含了文件的元信息,如文件类型、权限等。
      • struct file *filp:指向文件对象,代表了一个打开的文件实例,包含了文件的当前状态、偏移量等信息。
    • 返回值:成功时返回 0,失败时返回负数错误码。
    read 函数
    ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
    
    • 功能:当用户空间使用 read() 系统调用从设备文件读取数据时,内核会调用驱动中的 read 函数。该函数负责将设备中的数据复制到用户空间的缓冲区。
    • 参数
      • struct file *filp:指向文件对象。
      • char __user *buf:用户空间的缓冲区指针,用于存储从设备读取的数据。
      • size_t count:用户请求读取的字节数。
      • loff_t *f_pos:文件的当前偏移量指针,可通过修改该指针来更新文件的读写位置。
    • 返回值:成功时返回实际读取的字节数,返回 0 表示已到达文件末尾,失败时返回负数错误码。
    write 函数
    ssize_t (*write) (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
    
    • 功能:当用户空间使用 write() 系统调用向设备文件写入数据时,内核会调用驱动中的 write 函数。该函数负责将用户空间缓冲区中的数据复制到设备中。
    • 参数
      • struct file *filp:指向文件对象。
      • const char __user *buf:用户空间的缓冲区指针,包含了要写入设备的数据。
      • size_t count:用户请求写入的字节数。
      • loff_t *f_pos:文件的当前偏移量指针。
    • 返回值:成功时返回实际写入的字节数,失败时返回负数错误码。
    release 函数
    int (*release) (struct inode *inode, struct file *filp);
    
    • 功能:当用户空间使用 close() 系统调用关闭设备文件时,内核会调用驱动中的 release 函数。该函数通常用于执行设备的清理操作,如释放资源、关闭设备等。
    • 参数
      • struct inode *inode:指向文件对应的索引节点。
      • struct file *filp:指向文件对象。
    • 返回值:成功时返回 0,失败时返回负数错误码。
3. 模块卸载相关函数
cdev_del
void cdev_del(struct cdev *p);
  • 功能:从内核中移除字符设备。
  • 参数
    • p:指向 struct cdev 的指针。
device_destroy
void device_destroy(struct class *class, dev_t devt);
  • 功能:销毁 /sys/class/<class_name> 目录下的设备节点和 /dev 目录下的设备文件。
  • 参数
    • class:指向设备类的指针。
    • devt:设备号。
class_destroy
void class_destroy(struct class *cls);
  • 功能:销毁 /sys/class 目录下的设备类。
  • 参数
    • cls:指向 struct class 的指针。
unregister_chrdev_region
void unregister_chrdev_region(dev_t from, unsigned count);
  • 功能:释放之前分配的字符设备号。
  • 参数
    • from:起始的设备号。
    • count:要释放的设备号数量。

编译和测试

编写 Makefile
obj-m += helloworld.o
KDIR := linux-5.15.18/
PWD := $(shell pwd)
default:
    $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

linux-5.15.18/ 替换为实际的 Linux 5.15.18 内核源码路径。

编译驱动

在终端中执行 make 命令编译驱动模块。

测试驱动

在 QEMU 终端中:

  1. 使用 insmod helloworld.ko 加载驱动模块。
  2. 使用 echo “Hello World” > /dev/helloworld 向设备写入数据。
  3. 使用 cat /dev/helloworld 从设备读取数据。
  4. 使用 rmmod hello_chrdev.ko 卸载驱动模块。
    在这里插入图片描述

http://www.niftyadmin.cn/n/5862614.html

相关文章

http代理IP怎么实现?如何解决代理IP访问不了问题?

HTTP代理是一种网络服务&#xff0c;它充当客户端和目标服务器之间的中介。当客户端发送请求时&#xff0c;请求首先发送到代理服务器&#xff0c;然后由代理服务器转发到目标服务器。同样&#xff0c;目标服务器的响应也会先发送到代理服务器&#xff0c;再由代理服务器返回给…

tidb实时同步到mysql

客户要求实时同步表的数据到mysql&#xff0c;但这个表在tidb。 测试直接通过tidb cdc写入到mysql&#xff0c;有些字段是null&#xff0c;所以中间加了一个kafka实现 客户库中创建表 CREATE TABLE tb_1 (id bigint primary key,cid bigint,gid bigint,fee DECIMAL(10,2),cre…

若依框架实现动态失效时间JWT Token的实践指南

一、功能需求背景 在前后端分离架构中&#xff0c;JWT&#xff08;JSON Web Token&#xff09;作为无状态认证方案被广泛使用。若依&#xff08;RuoYi&#xff09;框架的TokenService默认采用固定失效时间策略&#xff0c;但在实际开发中常需要根据业务场景动态调整Token有效期…

GUI编程(window系统→Linux系统)

最近有个项目需要将windows系统的程序往Linux系统上面移植&#xff0c;由于之前程序没有考虑过多平台兼容的问题&#xff0c;导致部分功能不可用以下是对近期遇到的问题的总结&#xff0c;以及相应的解决方案和经验分享。 1. Python 模块安装与管理 在 Linux 系统中&#xff0…

C#设计模式 学习笔记

概述 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的代码设计经验的总结 本文是《C#设计模式》书籍的学习笔记 面向对象设计原则 单一职责原则 一个对象应该只包含单一的职责&#xff0c;并且该职责被完整地封装在一个类中 开闭原则 对扩展开放&#xff0c;对…

java(spring boot)实现向deepseek/GPT等模型的api发送请求/多轮对话(附源码)

我们再启动应用并获取api密钥后就可以对它发送请求了&#xff0c;但是官方文档对于如何进行多轮对话以及怎么自定义参数并没有说的很清楚&#xff0c;给的模板也没有java的&#xff0c;因此我们需要自己实现。 import org.json.JSONArray; import org.json.JSONObject;import j…

设计模式教程:迭代器模式(Iterator Pattern)

迭代器模式&#xff08;Iterator Pattern&#xff09;是设计模式中的一种行为型模式&#xff0c;它允许顺序访问一个集合对象中的元素&#xff0c;而无需暴露集合对象的内部结构。换句话说&#xff0c;迭代器模式提供了一个方法&#xff0c;能让你遍历集合中的元素&#xff0c;…

给老系统做个安全检查——Burp SqlMap扫描注入漏洞

背景 在AI技术突飞猛进的今天&#xff0c;类似Cursor之类的工具已经能写出堪比大部分程序员水平的代码了。然而&#xff0c;在我们的代码世界里&#xff0c;仍然有不少"老骥伏枥"的系统在兢兢业业地发光发热。这些祖传系统的代码可能早已过时&#xff0c;架构可能岌…