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

理解并使用 Linux 内核的字符设备

理解并使用 Linux 内核的字符设备

1. 引言

1.1 什么是字符设备

字符设备是 Linux 中的一类设备,支持以字符为单位进行数据传输。与块设备不同,字符设备不需要缓冲区,即数据是逐字节直接传递的。典型的字符设备包括串口、键盘、鼠标、伪终端等。

用个简单的比喻:字符设备像流水线,生产(写)和消费(读)可以同时进行且无需额外的仓库(缓冲区)。

1.2 字符设备的用途与典型应用场景

字符设备的主要用途是与硬件直接交互,比如读取传感器数据或控制某些外设。典型场景包括:

  • 提供用户空间与硬件交互的接口。
  • 模拟设备,用于调试或测试。
  • 创建自定义的和应用层通信的方法。

1.3 字符设备的特点(与块设备的对比)

特点字符设备块设备
数据传输单位字符(逐字节)块(通常为 512 字节或更大)
是否有缓冲区无(直接传递)
典型场景键盘、串口磁盘、U盘
接口file_operations 的方法实现I/O 调度层支持

2. 编写一个简单的字符设备

下文所有代码都基于6.9.1内核

2.1 示例代码及功能介绍

以下是一个简单的字符设备驱动示例,功能是从用户空间读取数据并将其回显。此代码展示了字符设备的核心操作流程,适合入门学习。

创建main.c文件如下

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h> // 用于copy_to_user和copy_from_user

#define DEVICE_NAME "simple_char_device" // 设备名称

static int major;                        // 主设备号
static char message[256] = {0};          // 缓存区,用于存储用户写入的数据
static int open_count = 0;               // 打开设备的次数计数器

// 打开设备
static int device_open(struct inode *inode, struct file *file) {
    open_count++;
    printk(KERN_INFO "Device opened %d time(s)\n", open_count);
    return 0; // 成功返回0
}

// 读取设备数据到用户空间
static ssize_t device_read(struct file *file, char __user *buffer, size_t len, loff_t *offset) {
    size_t message_len = strlen(message); // 获取消息长度

    if (*offset >= message_len) // 如果偏移量超出消息长度,返回0表示EOF
        return 0;
    if (len > message_len - *offset) // 如果读取长度超过剩余数据,截取剩余部分
        len = message_len - *offset;

    if (copy_to_user(buffer, message + *offset, len)) // 数据拷贝到用户空间
        return -EFAULT; // 失败返回错误码

    *offset += len; // 更新偏移量
    return len;     // 返回读取的字节数
}

// 写入数据到设备
static ssize_t device_write(struct file *file, const char __user *buffer, size_t len, loff_t *offset) {
    if (len > sizeof(message) - 1) // 检查写入数据是否超出缓冲区
        return -EINVAL; // 无效参数错误

    memset(message, 0, sizeof(message)); // 清空缓冲区
    if (copy_from_user(message, buffer, len)) // 从用户空间拷贝数据
        return -EFAULT; // 失败返回错误码

    message[len] = '\0'; // 确保字符串以空字符结尾
    printk(KERN_INFO "Received: %s\n", message); // 打印接收到的数据
    return len; // 返回写入的字节数
}

// 释放设备(关闭)
static int device_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device closed\n");
    return 0; // 成功返回0
}

// 定义文件操作结构
static struct file_operations fops = {
    .open = device_open,       // 打开设备
    .read = device_read,       // 读取设备
    .write = device_write,     // 写入设备
    .release = device_release, // 释放设备
};

// 模块初始化函数
static int __init char_device_init(void) {
    // 动态注册字符设备,获取主设备号
    major = register_chrdev(0, DEVICE_NAME, &fops);
    if (major < 0) {
        printk(KERN_ALERT "Failed to register device\n");
        return major; // 返回错误码
    }
    printk(KERN_INFO "Registered char device with major number %d\n", major);
    return 0; // 成功返回0
}

// 模块卸载函数
static void __exit char_device_exit(void) {
    unregister_chrdev(major, DEVICE_NAME); // 注销字符设备
    printk(KERN_INFO "Unregistered char device\n");
}

module_init(char_device_init); // 指定初始化函数
module_exit(char_device_exit); // 指定卸载函数

MODULE_LICENSE("GPL"); // 模块许可声明
MODULE_AUTHOR("Your Name"); // 模块作者
MODULE_DESCRIPTION("A simple char device driver"); // 模块描述

Makefile文件如下

obj-m += main.o

all:
	# 使用内核源码路径编译模块
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	# 清理编译生成的文件
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

2.2 分步解析示例代码

  1. 设备号分配

    • 使用register_chrdev(0, DEVICE_NAME, &fops)动态分配主设备号,并将其绑定到设备名称。
    • 注册失败时返回负值,通常需要打印错误信息以便调试。
  2. 定义file_operations结构

    • file_operations是字符设备的核心结构,用于描述字符设备的操作行为:
      • .open:在用户空间调用open时执行。
      • .read:用户调用read读取设备数据时执行。
      • .write:用户调用write写入设备数据时执行。
      • .release:在设备被关闭时调用。
  3. 与用户空间交互

    • copy_to_user:将内核中的数据拷贝到用户空间,需检查是否返回错误。
    • copy_from_user:将用户空间数据拷贝到内核,需确保长度合法。
    • 使用这些函数的原因是内核和用户空间的内存不共享,直接访问可能导致非法访问错误。
  4. 设备日志输出

    • 使用printk打印日志信息,有助于了解设备运行状态。
    • 日志可通过dmesg命令查看。

2.3 测试字符设备

我们可以通过以下步骤测试该字符设备:

  1. 编译并加载模块
    • 使用make编译模块
    • 使用sudo insmod main.ko命令加载模块。
  2. 创建设备节点
    • 查看主设备号. 使用sudo dmesg | grep major命令, 或者cat /proc/devices | grep simple_char_device
    • 创建设备: sudo mknod /dev/simple_char_device c <major_number> 0
  3. 测试设备
    • 使用echo写入数据:echo "Hello" | sudo tee /dev/simple_char_device
    • 使用cat读取数据:cat /dev/simple_char_device
  4. 卸载模块
    • 使用sudo rmmod main.ko命令卸载模块。

3. 深入解析 Linux 内核中的字符设备

字符设备是 Linux 驱动开发中最基础的设备类型之一。通过字符设备,用户可以实现对硬件的读写操作。本节将探讨创建字符设备的不同方式、设备号的分配方法,以及 file_operations 的作用和实现细节。

3.1 创建字符设备的两种方式

方式一:使用 register_chrdev

register_chrdev 是一种简单的字符设备注册方式。通过调用该函数,可以快速注册一个字符设备并关联 file_operations 接口。

示例

int major = register_chrdev(0, DEVICE_NAME, &fops);
if (major < 0) {
    printk(KERN_ALERT "Failed to register device\n");
    return major;
}
printk(KERN_INFO "Device registered with major number %d\n", major);

特点

  • 操作简单,适合快速开发和调试。
  • 不需要显式创建 struct cdev 对象。
  • 功能较有限,推荐用于较简单的场景。

方式二(推荐):使用 cdevcdev_add

cdev 是内核提供的字符设备核心数据结构,使用该方式注册字符设备更加灵活且符合现代驱动开发规范。

步骤

  1. 初始化字符设备对象:cdev_init
  2. 分配设备号:alloc_chrdev_region
  3. 将设备添加到内核:cdev_add

示例

struct cdev my_cdev;
dev_t dev_num;

// 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
major = MAJOR(dev_num);

// 初始化字符设备
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;

// 注册到内核
if (cdev_add(&my_cdev, dev_num, 1) < 0) {
    printk(KERN_ALERT "Failed to add cdev\n");
    unregister_chrdev_region(dev_num, 1);
    return -1;
}
printk(KERN_INFO "Device registered with major number %d\n", major);

特点

  • 适合复杂设备驱动程序的开发。
  • 提供更细粒度的控制,例如支持同时创建多个设备, 配合device_create自动创建设备等。

3.2 分配设备号:静态与动态分配

设备号由 主设备号次设备号 组成。主设备号标识驱动程序类型,次设备号标识具体的设备实例。主次设备号加在一起就可以唯一标识一个具体的设备。

静态分配

开发者可以直接指定设备号。这种方式简单,但可能与其他驱动冲突。

示例

#define MAJOR_NUM 240
register_chrdev(MAJOR_NUM, DEVICE_NAME, &fops);

优缺点

  • 优点:便于调试和定位。
  • 缺点:设备号固定,可能与其他模块冲突。

动态分配

动态分配通过内核自动分配主设备号,推荐在现代开发中使用。

使用 register_chrdev 分配设备号

int major = register_chrdev(0, DEVICE_NAME, &fops);
if (major < 0) {
    printk(KERN_ALERT "Failed to register device\n");
    return major;
}
printk(KERN_INFO "Device registered with major number %d\n", major);

使用 alloc_chrdev_region 分配设备号

dev_t dev_num;
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
major = MAJOR(dev_num);

动态分配的设备号可以通过 /proc/devices 查看。
以下是补充和完善后的 3.3 理解 file_operations 与字符设备的交互原理 章节内容。包括技术细节的完善和错误的修正,同时以逻辑清晰的方式组织内容:

3.3 理解 file_operations 与字符设备的交互原理

file_operations 结构体定义了一组操作回调函数,用于描述字符设备如何响应来自用户空间的操作请求。这些回调函数实现了设备驱动程序与用户空间之间的接口,覆盖了文件操作的各个方面(如打开、读写、关闭等)。为了深入理解字符设备如何通过 file_operations 实现交互,我们需要从设备号与文件系统的关系、设备的注册过程,以及文件操作的调用链三方面入手。

1. 设备号与文件系统的关系
  • 设备号

    • 每个字符设备通过设备号唯一标识,由主设备号 (major) 和次设备号 (minor) 组成。
      • 主设备号:标识负责管理该类设备的驱动程序。
      • 次设备号:区分同一驱动程序下的不同设备实例。
  • 设备节点

    • 字符设备在文件系统中表现为特殊文件,称为设备节点(例如 /dev/my_device)。设备节点的 inode 结构包含了对应的设备号。
    • 用户空间程序通过系统调用(如 open)访问设备节点,内核通过解析设备号找到对应的驱动程序,并最终调用 file_operations 中的回调函数。
2. 字符设备的注册与绑定

为了让字符设备能被内核管理并提供给用户空间使用,驱动程序需要完成设备的注册和 file_operations 的绑定。这个过程分为以下步骤:

  1. 设备号的分配

    • 使用 alloc_chrdev_region 动态分配主设备号和次设备号范围。
    • 或者,使用 register_chrdev_region 手动指定设备号范围。
    dev_t dev_num;
    alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    
  2. 初始化 cdev 结构

    • 每个字符设备通过 struct cdev 表示,其核心字段 ops 指向设备驱动的 file_operations
    • 使用 cdev_init 初始化 struct cdev
    struct cdev my_cdev;
    cdev_init(&my_cdev, &my_fops);
    
  3. cdev 添加到内核

    • 使用 cdev_add 将设备添加到内核,建立设备号与 cdev 的映射。
    • cdev_add 会将设备号插入到 kobj_map 结构中,以便后续通过设备号快速找到对应的 cdevfile_operations
    cdev_add(&my_cdev, dev_num, 1);
    
  4. 创建设备节点

    • 使用 mknod 命令创建设备节点,或者通过用户空间的设备管理工具(如 udev)自动完成。
3. 文件操作的调用链

以下是用户空间程序调用字符设备时的调用链和关键步骤:

用户调用 open 系统调用
  1. 用户程序调用 open("/dev/my_device", ...)
  2. 内核通过文件系统找到 /dev/my_device 对应的 inode,并从中获取设备号(主设备号和次设备号)。
内核解析设备号并找到 cdev

在以前老的内核中, 内核通过主设备号,从 chrdevs 哈希表(chrdevs[CHRDEV_MAJOR_HASH_SIZE])中找到注册的字符设备。现在已经弃用了这种方式。现在使用 chrdev_open 函数,通过次设备号在 kobj_map 中查找对应的 struct cdev

  static int chrdev_open(struct inode *inode, struct file *file)
  {
      struct cdev *p = kobj_lookup(cdev_map, inode->i_rdev, NULL);
      if (!p)
          return -ENODEV;

      file->f_op = p->ops;
      if (file->f_op->open)
          return file->f_op->open(inode, file);
      return 0;
  }
绑定 file_operations
  1. 内核通过 struct cdevops 字段获取对应的 file_operations 结构,并初始化 file->f_op

  2. 内核调用 file_operations 中的 open 回调函数,完成设备打开。

    调用链总结:

    用户程序 -> open() -> vfs_open() -> chrdev_open() -> cdev->ops->open()
    
小结
  • file_operations 是字符设备的操作接口,通过一系列回调函数实现用户空间与设备的交互。
  • 字符设备通过主设备号和次设备号唯一标识,并通过 cdev 结构与 file_operations 绑定。
  • 内核通过 chrdev_openkobj_map 将设备号解析为 file_operations,从而实现了用户空间系统调用与设备驱动的衔接。

4. 创建设备节点

设备节点是用户空间与内核设备驱动程序交互的入口。在 Linux 中,字符设备需要一个设备节点(如 /dev/simple_char_device)供用户访问。

4.1 用户手动创建设备节点

设备节点可以通过 mknod 命令手动创建。
语法如下:

sudo mknod /dev/simple_char_device c <major> <minor>
参数说明:
  • /dev/simple_char_device:设备节点的路径。
  • c:设备类型,c 表示字符设备,b 表示块设备。
  • <major>:主设备号,用于标识字符设备驱动程序。
  • <minor>:次设备号,用于区分驱动程序中的不同设备实例。
示例:

假设主设备号为 240,次设备号为 0:

sudo mknod /dev/simple_char_device c 240 0
sudo chmod 666 /dev/simple_char_device  # 设置读写权限

用户空间通过设备节点与字符设备交互。例如:

echo "Hello" > /dev/simple_char_device
cat /dev/simple_char_device
缺点:
  • 手动创建节点不方便,且设备号可能在系统重启或驱动加载时发生变化。

4.2 使用内核代码配合 udev 动态创建设备节点

现代 Linux 系统中,推荐通过内核和 udev 配合实现设备节点的自动创建。内核代码通过创建设备类和设备对象,通知 udev 守护进程自动创建设备节点。

核心函数:
  1. class_create:创建设备类,在 /sys/class 下注册。
  2. device_create:为设备类添加设备,在 /sys/class/<class_name>/<device_name> 下注册。
完整代码示例:

以下是一个字符设备驱动中动态创建设备节点的示例:

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

#define DEVICE_NAME "simple_char_device"
#define CLASS_NAME "simple_char_class"

static int major;                      // 主设备号
static struct class *char_class;       // 设备类
static struct device *char_device;     // 设备对象

// 文件操作函数
static int dev_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device opened\n");
    return 0;
}

static ssize_t dev_read(struct file *file, char __user *buffer, size_t len, loff_t *offset) {
    char *msg = "Hello from kernel!";
    size_t msg_len = strlen(msg);

    if (*offset >= msg_len)
        return 0;

    if (len > msg_len - *offset)
        len = msg_len - *offset;

    if (copy_to_user(buffer, msg + *offset, len))
        return -EFAULT;

    *offset += len;
    return len;
}

static ssize_t dev_write(struct file *file, const char __user *buffer, size_t len, loff_t *offset) {
    printk(KERN_INFO "Data written to device\n");
    return len;
}

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

// 文件操作结构体
static struct file_operations fops = {
    .open = dev_open,
    .read = dev_read,
    .write = dev_write,
    .release = dev_release,
};

static int __init char_init(void) {
    // 动态分配主设备号
    major = register_chrdev(0, DEVICE_NAME, &fops);
    if (major < 0) {
        printk(KERN_ALERT "Failed to register char device\n");
        return major;
    }
    printk(KERN_INFO "Registered char device with major number %d\n", major);

    // 创建设备类
  
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 11, 0)
    char_class = class_create(THIS_MODULE, CLASS_NAME);
#else
    char_class = class_create(CLASS_NAME);
#endif
    if (IS_ERR(char_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create class\n");
        return PTR_ERR(char_class);
    }

    // 创建设备
    char_device = device_create(char_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
    if (IS_ERR(char_device)) {
        class_destroy(char_class);
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create device\n");
        return PTR_ERR(char_device);
    }

    printk(KERN_INFO "Device created successfully\n");
    return 0;
}

static void __exit char_exit(void) {
    device_destroy(char_class, MKDEV(major, 0)); // 销毁设备
    class_destroy(char_class);                  // 销毁类
    unregister_chrdev(major, DEVICE_NAME);      // 注销设备号
    printk(KERN_INFO "Char device unregistered\n");
}

module_init(char_init);
module_exit(char_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple char device with auto node creation");
关键步骤:
  1. 动态分配主设备号:使用 register_chrdev
  2. 创建设备类class_create 创建设备类,在 /sys/class 下可见。
  3. 创建设备对象device_create 将设备注册到 /sys/class/<class_name>
  4. 加载驱动时创建设备节点:udev 守护进程会在 /dev 中自动创建设备节点。
udev 自动创建节点的工作原理:
  • 内核通过 class_createdevice_create/sys/class 添加设备信息。
  • udev 监听 /sys 文件系统的事件,发现新设备时根据设备属性规则自动创建节点。

4.3 查看设备节点信息

查看设备类和设备信息:

加载驱动后,可以通过以下命令查看设备信息:

ls /sys/class/simple_char_class
查看设备号:

通过 dmesg 日志获取主设备号和次设备号:

dmesg | grep "Registered char device"

4.4 小结

  1. 手动创建:通过 mknod 创建设备节点,但需要指定设备号,手动管理麻烦。
  2. 自动创建:结合 class_createdevice_create 配合 udev,实现设备节点的动态创建,现代驱动开发的推荐方式。

通过动态分配设备号和自动创建设备节点,字符设备驱动的加载、管理和用户访问变得更加简洁和高效。

5. 总结

本文介绍了Linux内核中的字符设备,这是一种支持逐字节数据传输的设备类型,与块设备相比不需要缓冲区。字符设备广泛用于直接硬件交互,如读取传感器或控制外设。文中详细描述了编写简单字符设备驱动的过程,包括定义file_operations结构来处理打开、读写和关闭操作,以及使用register_chrdev动态分配主设备号。

进一步探讨了创建字符设备的不同方法,强调了使用cdev结构和cdev_add函数的优势,这种方式提供了更灵活的控制,适合复杂场景。同时讨论了设备号静态与动态分配的区别,指出动态分配是现代开发中的推荐做法。对于file_operations的作用机制,文章解释了它如何作为接口实现用户空间与字符设备之间的交互,并深入分析了从用户调用到内核响应的整个过程。

最后,针对设备节点的创建,提出了两种方式:一是用户手动通过mknod命令创建;二是利用内核代码配合udev规则自动创建,后者在现代系统中更为常见且便捷。


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

相关文章:

  • 深入探索仓颉编程语言:函数与结构类型的终极指南
  • 嘉立创创建工程
  • Python 项目组织最佳实践:从脚本到大型项目的进化之路
  • 【QED】爱丽丝与混沌的无尽海
  • 机器学习系列(一)——K-近邻算法
  • vLLM (2) - 架构总览
  • 鸿蒙开发面试准备和经验
  • RabbitMQ中的普通Confirm模式:深入解析与最佳实践
  • 【spring-cloud-gateway总结】
  • 20241225在ubuntu20.04.5下监控SSD
  • 重温设计模式--5、职责链模式
  • 基于 Nginx 的网站服务器与 LNMP 平台搭建指南
  • 使用ForceBindIP绑定应用到指定IP
  • 第十七届山东省职业院校技能大赛 中职组“网络安全”赛项任务书正式赛题
  • 【Redis】配置序列化器
  • 每天40分玩转Django:Django管理界面
  • 「下载」智慧产业园区-数字孪生建设解决方案:重构产业全景图,打造虚实结合的园区数字化底座
  • 鸿蒙项目云捐助第二十一讲云捐助项目物联网IoT模拟器的使用
  • (ES Modules)prettier格式化typescript源码
  • ubuntu 如何重装你的apt【apt-get报错: symbol lookup error/undefined symbol】
  • SpringBoot Restful接口同时支持多个文件上传和参数传递
  • 要查询 `user` 表中 `we_chat_subscribe` 和 `we_chat_union_id` 列不为空的用户数量
  • springboot473基于web的物流管理系统(论文+源码)_kaic
  • xshell 隧道
  • 华为手机建议使用adb卸载的app
  • NTLM 中继到 LDAP 结合 CVE-2019-1040 接管全域