字符设备框架与驱动开发入门

文章目录

  • [1. 引言](#1. 引言)
  • [2. 字符设备架构解析](#2. 字符设备架构解析)
    • [2.1 交互架构](#2.1 交互架构)
    • [2.2 驱动开发步骤](#2.2 驱动开发步骤)
  • [3. 代码实现](#3. 代码实现)
    • [3.1 驱动初始化流程](#3.1 驱动初始化流程)
    • [3.2 完整代码实现](#3.2 完整代码实现)
      • [3.2.1 头文件与全局变量](#3.2.1 头文件与全局变量)
      • [3.2.2 file_operations 接口实现](#3.2.2 file_operations 接口实现)
      • [3.2.3 驱动入口与出口](#3.2.3 驱动入口与出口)
  • [4. 编译与验证](#4. 编译与验证)
    • [4.1 编译加载](#4.1 编译加载)
    • [4.2 检查设备节点](#4.2 检查设备节点)
    • [4.3 读写测试](#4.3 读写测试)
  • [5. 常见问题](#5. 常见问题)
  • [6. 驱动类型对比](#6. 驱动类型对比)
  • [7. 总结](#7. 总结)

1. 引言

在 Linux 驱动世界中,字符设备(Character Device)是最基础、最常见的"用户可交互"接口。

你可能接触过这些设备节点:

  • /dev/ttyS0 (串口)
  • /dev/input/event1 (输入设备)
  • /dev/gpiochip0 (GPIO控制器)

它们的共同点:表现形式都以 /dev/xxx 文件形式存在;操作方式支持 open(), read(), write(), ioctl() 等标准系统调用;本质都是字符设备驱动。

本文将带你从零开始写一个完整的字符设备驱动,打通从用户态 (User Space)到内核态(Kernel Space)的交互通路。

2. 字符设备架构解析

字符设备是一种以字节流方式读写的设备。与块设备(如硬盘)不同,它没有固定的块大小,通常按字节顺序访问。

2.1 交互架构

为了更好地理解用户程序是如何控制到底层驱动的,请看下图:
内核空间
用户空间
字符设备驱动
write('hello')
sys_write
查找主次设备号
调用对应函数
copy_from_user
用户应用程序

(echo / cat)
C 库

(glibc)
系统调用接口
虚拟文件系统(VFS)
cdev 结构体
file_operations

(open/read/write)
内核缓冲区

2.2 驱动开发步骤

开发一个字符设备驱动,主要包含以下四个核心步骤 :

步骤 关键动作 作用
1. 分配设备号 alloc_chrdev_region 申请合法的主/次设备号 (Major/Minor ID)
2. 初始化 cdev cdev_init 初始化核心结构体,绑定操作函数集
3. 实现接口 file_operations 实现 open, read, write 等具体业务逻辑
4. 注册设备 cdev_add & device_create 将驱动注册到内核,并创建 /dev 节点

3. 代码实现

我们将编写一个名为 /dev/hello_chrdev 的回显驱动:写入什么,读取时就返回什么。

3.1 驱动初始化流程

在看代码之前,需要先理清 init 函数的执行流:
失败
驱动加载
分配设备号

alloc_chrdev_region
初始化 cdev

cdev_init
注册 cdev

cdev_add
创建类

class_create
创建设备节点

device_create
完成
错误处理

3.2 完整代码实现

3.2.1 头文件与全局变量

c 复制代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h> // 包含 copy_to/from_user

#define DEVICE_NAME "hello_chrdev"

static dev_t dev_num;                // 存放设备号
static struct cdev hello_cdev;       // 字符设备核心结构
static struct class *hello_class;    // 用于自动创建设备节点
static char kernel_buffer[128];      // 内核侧数据缓冲区

3.2.2 file_operations 接口实现

这是字符设备驱动的"灵魂",定义了当用户操作文件时,内核具体做什么事情。

c 复制代码
// 对应用户层的 open()
static int hello_open(struct inode *inode, struct file *file)
{
	pr_info("hello_chrdev: device opened\n");
	return 0;
}

// 对应用户层的 read()
static ssize_t hello_read(struct file *file, char __user *buf, size_t len, loff_t *offset)
{
	// 使用内核帮助函数,处理偏移量和缓冲区边界
	return simple_read_from_buffer(buf, len, offset, kernel_buffer, strlen(kernel_buffer));
}

// 对应用户层的 write()
static ssize_t hello_write(struct file *file, const char __user *buf, size_t len, loff_t *offset)
{
	size_t to_copy = min(len, sizeof(kernel_buffer) - 1);

	// 关键:必须使用 copy_from_user 安全地从用户空间拷贝数据
	if (copy_from_user(kernel_buffer, buf, to_copy))
		return -EFAULT;

	kernel_buffer[to_copy] = '\0'; // 确保字符串结束符
	pr_info("hello_chrdev: received \"%s\"\n", kernel_buffer);
	return to_copy;
}

3.2.3 驱动入口与出口

c 复制代码
// 定义操作函数集
static const struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.read = hello_read,
	.write = hello_write,
};

static int __init hello_init(void)
{
	int ret;

	// 1. 动态分配设备号
	ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
	if (ret < 0)
		return ret;

	// 2. 初始化 cdev 并与 fops 绑定
	cdev_init(&hello_cdev, &hello_fops);

	// 3. 添加 cdev 到内核
	ret = cdev_add(&hello_cdev, dev_num, 1);
	if (ret < 0)
		return ret;

	// 4. 自动创建设备节点 /dev/hello_chrdev
	// 先创建类
	hello_class = class_create(THIS_MODULE, "hello_class");
	if (IS_ERR(hello_class))
		return PTR_ERR(hello_class);

	// 再创建设备
	device_create(hello_class, NULL, dev_num, NULL, DEVICE_NAME);

	pr_info("hello_chrdev: initialized successfully\n");
	return 0;
}

static void __exit hello_exit(void)
{
	// 销毁顺序与注册顺序相反
	device_destroy(hello_class, dev_num);
	class_destroy(hello_class);
	cdev_del(&hello_cdev);
	unregister_chrdev_region(dev_num, 1);
	pr_info("hello_chrdev: unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("dump linux");

4. 编译与验证

4.1 编译加载

bash 复制代码
make
sudo insmod hello_chrdev.ko
# 查看日志:hello_chrdev: initialized successfully
dmesg | tail

4.2 检查设备节点

bash 复制代码
# 输出示例:crw------- 1 root root 240, 0 ... (c 代表 character device)
ls -l /dev/hello_chrdev

4.3 读写测试

bash 复制代码
# 写入数据
echo "hello from user" > /dev/hello_chrdev
# 读取回显,输出: hello from user
cat /dev/hello_chrdev

5. 常见问题

现象 可能原因 排查建议
没有生成 /dev/xxx udev/mdev 机制未触发或代码漏写 检查 class_createdevice_create 是否执行成功
写入数据乱码 缓冲区溢出或未以 \0 结尾 检查 kernel_buffer 的边界处理及字符串结束符
copy_from_user 失败 用户指针非法 检查传入的用户空间地址是否有效,不要直接解引用用户指针
read() 死循环/总返回 0 偏移量未更新 read 函数必须更新 loff_t *offset,否则用户程序会以为文件一直在开头

6. 驱动类型对比

为了理清字符设备在驱动体系中的位置,我们将其与总线驱动做个对比:

特征 平台/总线驱动 (Platform/I2C/SPI) 字符设备驱动 (Char Device)
关注点 如何挂载 如何访问
驱动入口 probe() (匹配设备树/总线后触发) init() (模块加载时直接运行)
通信对象 硬件芯片 (GPIO, 寄存器) 用户空间程序 (通过文件 IO)
暴露方式 通常在 /sys/bus/... /dev/xxx 字符设备节点
典型应用 传感器、控制器底层驱动 虚拟设备、自定义通信接口、硬件驱动的上层封装

7. 总结

字符设备驱动是连接用户空间与内核空间的最直接桥梁。对内它可以调用 GPIO、I2C 等底层接口控制硬件,对外它提供标准的 open/read/write 文件接口供 App 调用。

掌握了字符设备驱动,就意味着你拥有了自定义 /dev 接口的能力,这是 Linux 驱动开发中承上启下的关键一环。

相关推荐
A9better4 小时前
嵌入式开发学习日志50——任务调度与状态
stm32·嵌入式硬件·学习
LUCIFER5 小时前
[驱动进阶——MIPI摄像头驱动(五)]rk3588+OV13855摄像头驱动加载过程详细解析第四部分——ISP驱动
linux·驱动开发
暮云星影6 小时前
四、linux系统 应用开发:UI开发环境配置概述 (一)
linux·ui·arm
DLGXY6 小时前
STM32——EXTI外部中断(六)
stm32·单片机·嵌入式硬件
a程序小傲7 小时前
得物Java面试被问:RocketMQ的消息轨迹追踪实现
java·linux·spring·面试·职场和发展·rocketmq·java-rocketmq
Ghost Face...7 小时前
i386 CPU页式存储管理深度解析
java·linux·服务器
LEEE@FPGA7 小时前
zynq 是不是有了设备树,再linux中不需要编写驱动也能控制
linux·运维·单片机
CQ_YM7 小时前
ARM之I2C与ADC
arm开发·嵌入式硬件·嵌入式·arm
RisunJan7 小时前
Linux命令-less(分页查看器)
linux·运维
梁正雄7 小时前
linux服务-MariaDB 10.6 Galera Cluster+garbd
linux·运维·mariadb