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

devtools/2024/12/26 21:07:19/

理解并使用 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表示EOFreturn 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.oall:# 使用内核源码路径编译模块make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean:# 清理编译生成的文件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);
#elsechar_class = class_create(CLASS_NAME);
#endifif (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.ppmy.cn/devtools/145625.html

相关文章

微信小程序页面之间的传值方式

在微信小程序的开发过程中&#xff0c;页面之间的传值是一个常见的操作。根据多年的实践&#xff0c;我就我所知道的小程序页面之间的传值方式&#xff0c;进行简单的总结。希望能够帮助大家。 一、URL参数传递 原理 这种方式类似于网页开发中的URL传参。在微信小程序中&…

面试题整理15----K8s常见的网络插件有哪些

面试题整理15----K8s常见的网络插件有哪些 常见的K8s网络插件有Flannel,Calico,Cilium,Weave,Antrea,Kube-OVN等.其中Calico, Flannel, 和 Cilium较为常用. Flannel: 实现方式: 基于 VXLAN 或 UDP 隧道在节点之间创建网络覆盖。相对简单易于理解和部署。功能: 提供基本的网络连…

基于深度学习(HyperLPR3框架)的中文车牌识别系统-前言

参考链接&#xff1a; GitHub - szad670401/HyperLPR: 基于深度学习高性能中文车牌识别 High Performance Chinese License Plate Recognition Framework.基于深度学习高性能中文车牌识别 High Performance Chinese License Plate Recognition Framework. - szad670401/HyperL…

Pyside6 在 pycharm 中的配置

打开文件->设置 找到 工具->外部工具 点击 号 创建一个外部工具 QtDesigner 名称:QtDesigner 程序&#xff1a;D:\miniconda\envs\ergoAI-qt\Lib\site-packages\PySide6\designer.exe 实参&#xff1a;$FileName$ 工作目录&#xff1a;$FileDir$ PyUIC 名称&#xf…

taiwindcss

1.安装 npm install -D tailwindcss postcss autoprefixer npx tailwindcss init 这会创建一个 tailwind.config.js 文件。注意&#xff1a;一定通过px tailwindcss init方式创建 2.tailwind.config.js module.exports {content: [./index.html,./src/**/*.{js,ts,jsx,ts…

在 CentOS 7 上安装 Node.js 20 并升级 GCC、make 和 glibc

在 CentOS 7 上安装 Node.js 20 并升级 GCC、make 和 glibc &#x1f4d6; 前言 在 CentOS 7 上使用 NVM 安装 Node.js 后&#xff0c;可能会遇到如下问题&#xff1a; node: /lib64/libm.so.6: version GLIBC_2.27’ not found (required by node) node: /lib64/libc.so.6:…

汽车IVI中控开发入门及进阶(44):杰发科智能座舱芯片

概述: 杰发科技自成立以来,一直专注于汽车电子芯片及相关系统的研发与设计。 产品布局: 合作伙伴: 杰发科技不断提升产品设计能力和产品工艺,确保产品达 到更高的质量标准。目前杰发科技已通过ISO9001质 量管理体系与CMMIL3认证。 杰发科技长期合作的供应商(芯片代工厂、…

今日总结 2024-12-25

一、开发要点总结 &#xff08;一&#xff09;组织架构编辑部门 数据交互与组件协作 共用 add-dept 组件实现新增和编辑场景&#xff0c;需精准区分两种场景下的数据获取、校验及处理逻辑。通过在父组件&#xff08;src/views/department/index.vue&#xff09;点击编辑时利用…