当前位置: 首页 > article >正文

【Linux】【字符设备驱动】深入解析

在这里插入图片描述

Linux字符设备驱动程序用于控制不支持随机访问的硬件设备,如串行端口、打印机、调制解调器等。这类设备通常以字符流的形式与用户空间程序进行交互。本节将深入探讨字符设备驱动的设计原理、实现细节及其与内核其他组件的交互。

1. 引言

字符设备驱动程序是Linux内核的重要组成部分,它们负责处理来自用户空间应用程序的读写请求,并将这些请求转换为对硬件设备的实际操作。字符设备驱动程序通常用于控制串行端口、打印机、调制解调器等设备。

2. 字符设备的基本概念

2.1 设备号

在Linux系统中,每个字符设备都有一个唯一的设备号,设备号由主设备号和次设备号组成。主设备号用于标识一组相关设备,次设备号用于区分同一组中的不同设备。

2.1.1 设备号分配

设备号由内核动态分配或手动指定。例如:

sudo mknod /dev/my_char_dev c 240 0

这里,240 是主设备号,0 是次设备号。

2.2 设备文件

字符设备文件通常位于 /dev 目录下,它们代表了实际的硬件设备。设备文件可以通过 mknod 命令创建,也可以通过 udev 规则自动创建。

2.2.1 创建设备文件

使用 mknod 命令创建一个字符设备文件:

sudo mknod /dev/my_char_dev c 240 0

2.3 设备驱动程序

字符设备驱动程序负责处理来自用户空间应用程序的请求,并将这些请求转换为对硬件设备的操作。驱动程序通常包括初始化函数、读写操作函数等。

2.3.1 初始化函数

驱动程序初始化函数负责注册设备号、初始化 file_operations 结构体、注册字符设备等。

2.3.2 文件操作结构

struct file_operations 结构体包含了各种文件操作函数指针,如 readwriteopen 等。

3. 字符设备驱动程序的底层原理

3.1 内核与用户空间交互

字符设备驱动程序通过内核提供的接口与用户空间应用程序进行交互。当用户空间程序对设备文件进行读写操作时,内核会调用相应的驱动程序函数。

3.1.1 系统调用

系统调用是内核与用户空间交互的主要方式。当用户空间程序调用 readwrite 系统调用时,内核会调用对应的驱动程序函数。

3.1.2 文件操作结构

struct file_operations 结构体定义了一系列文件操作函数,这些函数由内核在适当的时机调用。

struct file_operations {
    int (*read)(struct file *, char __user *, size_t, loff_t *);
    int (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*open)(struct inode *, struct file *);
    int (*release)(struct inode *, struct file *);
    ...
};

3.2 设备注册与管理

3.2.1 设备号注册

设备号注册是在内核中创建一个设备文件的步骤之一。设备号由主设备号和次设备号组成,通过 register_chrdev_region 函数注册。

register_chrdev_region(dev_num, 1, "my_char_dev");
3.2.2 设备文件操作结构初始化

设备文件操作结构初始化包括设置读写操作函数等。

cdev_init(&c_dev, &fops);
3.2.3 设备注册

设备注册是将设备文件操作结构添加到内核中,使设备可以被访问。

cdev_add(&c_dev, dev_num, 1);

3.3 设备操作函数

字符设备驱动程序需要实现一系列设备操作函数,如读、写、打开、关闭等。

3.3.1 读操作

读操作函数负责从设备中读取数据,并返回给用户空间程序。

static ssize_t dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    if (*ppos >= PAGE_SIZE)
        return 0;
    if (copy_to_user(buf, &buf[*ppos], count))
        return -EFAULT;
    *ppos += count;
    return count;
}
3.3.2 写操作

写操作函数负责将用户空间程序的数据写入设备。

static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    if (*ppos >= PAGE_SIZE)
        return -ENOSPC;
    if (copy_from_user(&buf[*ppos], buf, count))
        return -EFAULT;
    *ppos += count;
    return count;
}
3.3.3 打开和关闭操作

打开操作函数负责初始化设备,关闭操作函数负责释放设备资源。

static int dev_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device opened.\n");
    return 0;
}

static int dev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed.\n");
    return 0;
}

4. 字符设备驱动程序的编写

4.1 模块初始化和卸载

字符设备驱动程序通常是一个内核模块,模块需要定义初始化和卸载函数。

4.1.1 初始化函数

初始化函数通常包括注册设备号、初始化 file_operations 结构体、注册字符设备等。

static int __init dev_init(void)
{
    // 注册字符设备
    register_chrdev_region(dev_num, 1, "my_char_dev");

    // 初始化字符设备结构
    cdev_init(&c_dev, &fops);

    // 添加字符设备到设备类
    class = class_create(THIS_MODULE, "my_char_class");
    device = device_create(class, NULL, dev_num, NULL, "my_char_dev");

    // 注册字符设备
    cdev_add(&c_dev, dev_num, 1);

    return 0;
}
4.1.2 卸载函数

卸载函数负责注销字符设备、销毁设备类等。

static void __exit dev_exit(void)
{
    // 删除字符设备
    cdev_del(&c_dev);

    // 移除设备
    device_destroy(class, dev_num);

    // 销毁设备类
    class_unregister(class);

    // 注销字符设备区域
    unregister_chrdev_region(dev_num, 1);
}

4.2 设备操作函数

字符设备驱动程序需要实现一系列设备操作函数,如读、写、打开、关闭等。

4.2.1 读操作

读操作函数负责从设备中读取数据,并返回给用户空间程序。

static ssize_t dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    if (*ppos >= PAGE_SIZE)
        return 0;
    if (copy_to_user(buf, &buf[*ppos], count))
        return -EFAULT;
    *ppos += count;
    return count;
}
4.2.2 写操作

写操作函数负责将用户空间程序的数据写入设备。

static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    if (*ppos >= PAGE_SIZE)
        return -ENOSPC;
    if (copy_from_user(&buf[*ppos], buf, count))
        return -EFAULT;
    *ppos += count;
    return count;
}
4.2.3 打开和关闭操作

打开操作函数负责初始化设备,关闭操作函数负责释放设备资源。

static int dev_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device opened.\n");
    return 0;
}

static int dev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed.\n");
    return 0;
}

5. 设备文件的创建

设备文件通常位于 /dev 目录下,可以通过 mknod 命令或 udev 规则创建。

5.1 使用 mknod 创建设备文件

sudo mknod /dev/my_char_dev c 240 0

5.2 使用 udev 规则自动创建设备文件

可以编写 udev 规则来自动创建设备文件:

# /etc/udev/rules.d/99-my-device.rules

KERNEL=="my_char_dev", MODE="0660", OWNER="root", GROUP="users", SYMLINK+="my_char_dev"

6. 用户空间程序

用户空间程序用于读写字符设备文件。

6.1 用户空间程序示例

编写一个简单的用户空间程序来读写设备文件:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;
    char buffer[256];
    ssize_t bytes_written, bytes_read;

    // 打开设备文件
    fd = open("/dev/my_char_dev", O_RDWR);
    if (fd == -1) {
        perror("Failed to open device");
        return 1;
    }

    // 写入数据
    bytes_written = write(fd, "Hello, World!", 13);
    if (bytes_written == -1) {
        perror("Failed to write to device");
        close(fd);
        return 1;
    }

    // 读取数据
    bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read == -1) {
        perror("Failed to read from device");
        close(fd);
        return 1;
    }

    printf("Read from device: %.*s\n", (int)bytes_read, buffer);

    // 关闭设备文件
    close(fd);

    return 0;
}

6.2 编译和运行用户空间程序

编译用户空间程序:

gcc -o my_prog my_prog.c

运行用户空间程序:

./my_prog

7. 字符设备驱动的调试

7.1 使用 printk 调试

printk 函数可以用于在内核模块中输出调试信息。

printk(KERN_INFO "Hello, World!\n");

7.2 使用 syslog 调试

syslog 函数可以用于将调试信息发送到系统日志。

#include <linux/syscalls.h>
...
syslog(KERN_INFO, "Hello, World!");

8. 字符设备驱动的优化

8.1 避免死锁

在多线程或多进程环境下,需要小心处理设备的读写操作,避免死锁。

8.1.1 互斥锁

使用互斥锁来保护共享资源。

static DEFINE_MUTEX(my_mutex);

mutex_lock(&my_mutex);
// 执行关键操作
mutex_unlock(&my_mutex);

8.2 提高性能

通过优化读写操作,减少不必要的上下文切换,提高设备驱动程序的性能。

8.2.1 非阻塞 I/O

支持非阻塞 I/O 可以提高设备驱动程序的性能。

static int dev_poll(struct file *file, poll_table *wait)
{
    poll_wait(file, wait, POLLIN | POLLOUT);
    return POLLIN | POLLOUT;
}

static const struct file_operations fops = {
    .owner          = THIS_MODULE,
    .read           = dev_read,
    .write          = dev_write,
    .open           = dev_open,
    .release        = dev_release,
    .poll           = dev_poll,
};

9. 字符设备驱动的应用案例

9.1 串行通信设备驱动

串行通信设备驱动用于控制串行端口,如 COM 端口。

// 串行通信设备驱动示例

// 设备打开操作
static int dev_open(struct inode *inode, struct file *file)
{
    // 初始化串行端口
    init_serial_port();
    return 0;
}

// 设备关闭操作
static int dev_release(struct inode *inode, struct file *file)
{
    // 释放串行端口资源
    release_serial_port();
    return 0;
}

// 设备读操作
static ssize_t dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    // 从串行端口读取数据
    read_from_serial_port(buf, count);
    return count;
}

// 设备写操作
static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    // 将数据写入串行端口
    write_to_serial_port(buf, count);
    return count;
}

9.2 LED 控制设备驱动

LED 控制设备驱动用于控制 LED 灯。

// LED 控制设备驱动示例

// 设备打开操作
static int dev_open(struct inode *inode, struct file *file)
{
    // 初始化 LED
    init_led();
    return 0;
}

// 设备关闭操作
static int dev_release(struct inode *inode, struct file *file)
{
    // 释放 LED 资源
    release_led();
    return 0;
}

// 设备读操作
static ssize_t dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    // 读取 LED 状态
    read_led_status(buf, count);
    return count;
}

// 设备写操作
static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    // 设置 LED 状态
    set_led_status(buf, count);
    return count;
}

10. 字符设备驱动的高级特性

10.1 设备属性

设备属性允许用户通过 /sys/class/<class>/device 目录下的文件来查询和修改设备状态。

10.1.1 添加设备属性

使用 sysfs API 添加设备属性:

static ssize_t show_attr(struct device *dev, struct device_attribute *attr, char *buf)
{
    return sprintf(buf, "%d\n", some_value);
}

static ssize_t store_attr(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
    some_value = atoi(buf);
    return count;
}

static DEVICE_ATTR(my_attr, 0644, show_attr, store_attr);

static struct attribute *my_attrs[] = {
    &dev_attr_my_attr.attr,
    NULL,
};

static struct device_type my_dev_type = {
    .name = "my_char_dev",
    .attrs = my_attrs,
};

class_create(THIS_MODULE, "my_char_class");
device_create(class, NULL, dev_num, NULL, "my_char_dev");
device_create_file(device, &dev_attr_my_attr);

10.2 设备同步

为了保证数据的一致性和完整性,设备驱动程序需要处理同步问题。

10.2.1 原子操作

使用原子操作来保护数据的一致性。

static atomic_t my_counter;

atomic_inc(&my_counter);
atomic_dec(&my_counter);

11. 字符设备驱动的错误处理

11.1 错误检测

在设备驱动程序中检测和处理错误是必要的。

11.1.1 返回值检查

使用返回值来检查函数是否成功执行。

if (ioctl(fd, MY_IOCTL_CMD, &arg) < 0) {
    perror("ioctl failed");
    return -1;
}

11.2 错误报告

在设备驱动程序中报告错误信息。

11.2.1 日志记录

使用 printksyslog 记录错误信息。

printk(KERN_ERR "Error occurred in driver.\n");

12. 总结

Linux字符设备驱动程序用于控制不支持随机访问的硬件设备,如串行端口、打印机等。通过编写字符设备驱动程序,可以实现对这些设备的高效控制。希望本文能帮助读者更好地理解和掌握Linux字符设备驱动程序的开发技巧,并深入了解其底层原理。


http://www.kler.cn/a/417625.html

相关文章:

  • Beans模块之工厂模块注解模块CustomAutowireConfigurer
  • 云计算部署模式全面解析
  • Ubuntu x64下交叉编译ffmpeg、sdl2到目标架构为aarch64架构的系统(生成ffmpeg、ffprobe、ffplay)
  • Ollama教程:轻松上手本地大语言模型部署
  • C++ 学习:深入理解 Linux 系统中的冯诺依曼架构
  • (dpdk f-stack)-堆栈溢出-野指针-内存泄露(问题定位)
  • LabVIEW实现UDP通信
  • Android获取状态栏、导航栏的高度
  • 【2025最新计算机毕业设计】基于SpringBoot+Vue文化创意展示与交流平台【提供源码+答辩PPT+文档+项目部署】
  • YOLO系列论文综述(从YOLOv1到YOLOv11)【第14篇:YOLOv11——在速度和准确性方面具有无与伦比的性能】
  • 动捕 动作捕捉学习笔记
  • C++内存对齐
  • 【从零开始的LeetCode-算法】263. 丑数
  • python全栈开发《67.不同数据类型间的转换:列表集合元组的转换》
  • 【Leecode】Leecode刷题之路第66天之加一
  • Maven CMD命令
  • 共享售卖机语音芯片方案选型:WTN6020引领智能化交互新风尚
  • 【Ant Design Pro】1. config 配置
  • 实战ansible-playbook:Ansible Vault加密敏感数据(三)
  • 田忌赛马五局三胜问题matlab代码
  • 大模型训练核心技术RLHF
  • 关于扩散方程的解
  • 命令行应用开发初学者指南:脚手架篇、UI 库和交互工具
  • 【AI】Jetson Nano烧写SD卡镜像:Ubuntu20.04
  • Vue 2.0->3.0学习笔记(Vue 3 (五)- 新的组件)
  • 本地学习axios源码-如何在本地打印axios里面的信息