文章目录
- [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_create 和 device_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 驱动开发中承上启下的关键一环。