明白并使用 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 = {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 = '\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 分步剖析示例代码
[*] 装备号分配:
[*]使用register_chrdev(0, DEVICE_NAME, &fops)动态分配主装备号,并将其绑定到装备名称。
[*]注册失败时返回负值,通常需要打印错误信息以便调试。
[*] 定义file_operations结构:
[*]file_operations是字符装备的核心结构,用于形貌字符装备的操作举动:
[*].open:在用户空间调用open时实行。
[*].read:用户调用read读取装备数据时实行。
[*].write:用户调用write写入装备数据时实行。
[*].release:在装备被关闭时调用。
[*] 与用户空间交互:
[*]copy_to_user:将内核中的数据拷贝到用户空间,需查抄是否返回错误。
[*]copy_from_user:将用户空间数据拷贝到内核,需确保长度合法。
[*]使用这些函数的缘故原由是内核和用户空间的内存不共享,直接访问可能导致非法访问错误。
[*] 装备日志输出:
[*]使用printk打印日志信息,有助于了解装备运行状态。
[*]日志可通过dmesg命令查看。
2.3 测试字符装备
我们可以通过以下步调测试该字符装备:
[*]编译并加载模块:
[*]使用make编译模块
[*]使用sudo insmod main.ko命令加载模块。
[*]创建装备节点:
[*]查看主装备号. 使用sudo dmesg | grep major命令, 或者cat /proc/devices | grep simple_char_device
[*]创建装备: sudo mknod /dev/simple_char_device c <major_number> 0
[*]测试装备:
[*]使用echo写入数据:echo "Hello" | sudo tee /dev/simple_char_device
[*]使用cat读取数据:cat /dev/simple_char_device
[*]卸载模块:
[*]使用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 对象。
[*]功能较有限,保举用于较简朴的场景。
方式二(保举):使用 cdev 和 cdev_add
cdev 是内核提供的字符装备核心数据结构,使用该方式注册字符装备更加灵活且符合当代驱动开辟规范。
步调:
[*]初始化字符装备对象:cdev_init。
[*]分配装备号:alloc_chrdev_region。
[*]将装备添加到内核: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 的绑定。这个过程分为以下步调:
[*] 装备号的分配
[*]使用 alloc_chrdev_region 动态分配主装备号和次装备号范围。
[*]或者,使用 register_chrdev_region 手动指定装备号范围。
dev_t dev_num;
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
[*] 初始化 cdev 结构
[*]每个字符装备通过 struct cdev 表示,其核心字段 ops 指向装备驱动的 file_operations。
[*]使用 cdev_init 初始化 struct cdev。
struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops);
[*] 将 cdev 添加到内核
[*]使用 cdev_add 将装备添加到内核,建立装备号与 cdev 的映射。
[*]cdev_add 会将装备号插入到 kobj_map 结构中,以便后续通过装备号快速找到对应的 cdev 和 file_operations。
cdev_add(&my_cdev, dev_num, 1);
[*] 创建装备节点
[*]使用 mknod 命令创建装备节点,或者通过用户空间的装备管理工具(如 udev)自动完成。
3. 文件操作的调用链
以下是用户空间步调调用字符装备时的调用链和关键步调:
用户调用 open 系统调用
[*]用户步调调用 open("/dev/my_device", ...)。
[*]内核通过文件系统找到 /dev/my_device 对应的 inode,并从中获取装备号(主装备号和次装备号)。
内核剖析装备号并找到 cdev
在以前老的内核中, 内核通过主装备号,从 chrdevs 哈希表(chrdevs)中找到注册的字符装备。现在已经弃用了这种方式。现在使用 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
[*] 内核通过 struct cdev 的 ops 字段获取对应的 file_operations 结构,并初始化 file->f_op。
[*] 内核调用 file_operations 中的 open 回调函数,完成装备打开。
调用链总结:
用户程序 -> open() -> vfs_open() -> chrdev_open() -> cdev->ops->open()
小结
[*]file_operations 是字符装备的操作接口,通过一系列回调函数实现用户空间与装备的交互。
[*]字符装备通过主装备号和次装备号唯一标识,并通过 cdev 结构与 file_operations 绑定。
[*]内核通过 chrdev_open 和 kobj_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 守护进程自动创建装备节点。
核心函数:
[*]class_create:创建装备类,在 /sys/class 下注册。
[*]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");
关键步调:
[*]动态分配主装备号:使用 register_chrdev。
[*]创建装备类:class_create 创建装备类,在 /sys/class 下可见。
[*]创建装备对象:device_create 将装备注册到 /sys/class/<class_name>。
[*]加载驱动时创建装备节点:udev 守护进程会在 /dev 中自动创建装备节点。
udev 自动创建节点的工作原理:
[*]内核通过 class_create 和 device_create 向 /sys/class 添加装备信息。
[*]udev 监听 /sys 文件系统的事件,发现新装备时根据装备属性规则自动创建节点。
4.3 查看装备节点信息
查看装备类和装备信息:
加载驱动后,可以通过以下命令查看装备信息:
ls /sys/class/simple_char_class
查看装备号:
通过 dmesg 日志获取主装备号和次装备号:
dmesg | grep "Registered char device"
4.4 小结
[*]手动创建:通过 mknod 创建装备节点,但需要指定装备号,手动管理贫苦。
[*]自动创建:结合 class_create 和 device_create 配合 udev,实现装备节点的动态创建,当代驱动开辟的保举方式。
通过动态分配装备号和自动创建装备节点,字符装备驱动的加载、管理和用户访问变得更加简洁和高效。
5. 总结
本文介绍了Linux内核中的字符装备,这是一种支持逐字节数据传输的装备类型,与块装备相比不需要缓冲区。字符装备广泛用于直接硬件交互,如读取传感器或控制外设。文中具体形貌了编写简朴字符装备驱动的过程,包括定义file_operations结构来处理打开、读写和关闭操作,以及使用register_chrdev动态分配主装备号。
进一步探讨了创建字符装备的差别方法,强调了使用cdev结构和cdev_add函数的上风,这种方式提供了更灵活的控制,适合复杂场景。同时讨论了装备号静态与动态分配的区别,指出动态分配是当代开辟中的保举做法。对于file_operations的作用机制,文章表明了它如何作为接口实现用户空间与字符装备之间的交互,并深入分析了从用户调用到内核响应的整个过程。
末了,针对装备节点的创建,提出了两种方式:一是用户手动通过mknod命令创建;二是利用内核代码配合udev规则自动创建,后者在当代系统中更为常见且便捷。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]