在嵌入式 Linux 开发中,字符设备驱动是最基础也最常用的驱动类型(如串口、按键、LED 等均属于字符设备)。本文将结合一个完整的hello字符设备驱动案例,拆解嵌入式 Linux 字符设备驱动的通用编写流程,并分析核心知识点与实战要点。
一、案例背景
本文案例包含两个文件:
- 驱动层(hello_drv.c):实现字符设备驱动的核心逻辑,完成设备注册、文件操作接口实现、自动创建设备节点等功能;
- 应用层(hello_drv_test.c):通过标准 Linux 文件 IO 接口(open/read/write/close)访问驱动设备,验证驱动功能。
二、嵌入式 Linux 字符设备驱动编写通用流程

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
/* 1. 确定主设备号 */
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;
#define MIN(a, b) (a < b ? a : b)
/* 3. 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
/* 2. 定义自己的file_operations结构体 */
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
/* 4. 把file_operations结构体告诉内核:注册驱动程序 */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello */
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
return 0;
}
/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数 */
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, "hello");
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
步骤 1:确定主设备号(或让内核分配)
作用
主设备号是字符设备的唯一标识,Linux 内核通过 "主设备号 + 次设备号" 区分不同设备(主设备号对应驱动类型,次设备号对应同一驱动下的多个设备)。
实现方式
- 手动指定 :直接定义
static int major = 240;(需确保未被其他驱动占用); - 内核自动分配 :定义
static int major = 0;(后续通过register_chrdev让内核分配可用主设备号)。
核心逻辑
主设备号是驱动与内核的 "身份凭证",后续注册驱动、创建设备节点都依赖它。
步骤 2:定义自己的file_operations结构体
作用
struct file_operations是内核与应用层的交互桥梁,它封装了驱动对外提供的所有操作接口(如open/read/write),应用层调用标准文件 IO 函数时,内核会转发到该结构体对应的驱动函数。
代码示例
// 定义file_operations结构体(模板)
static struct file_operations hello_drv = {
.owner = THIS_MODULE, // 固定赋值:表示驱动所属的内核模块
.open = hello_drv_open, // 后续实现的open接口
.read = hello_drv_read, // 后续实现的read接口
.write = hello_drv_write, // 后续实现的write接口
.release = hello_drv_close, // 后续实现的close接口
};
核心逻辑
file_operations是驱动的 "功能清单",内核通过它知道驱动支持哪些操作;其中.owner = THIS_MODULE是内核要求的必填项,用于模块引用计数管理。
步骤 3:实现drv_open/read/write等函数并填入结构体
作用
实现file_operations中声明的接口函数,完成内核与用户空间的数据交互、硬件操作等核心逻辑。
关键函数解析
(1)open函数(设备打开)
static int hello_drv_open(struct inode *node, struct file *file)
{
printk("设备已打开\n");
// 可在此处初始化硬件(如配置GPIO、申请资源)
return 0; // 返回0表示成功
}
- 入参:
inode是设备文件的索引节点(包含设备号),file是文件描述符对应的内核对象; - 作用:驱动初始化(如硬件配置、资源申请),应用层调用
open("/dev/hello")时触发。 - 调用的
printk是内核态的日志打印函数,其作用贯穿驱动开发的调试、运行监控、问题定位全流程,是嵌入式 Linux 驱动开发中最基础也最核心的调试手段之一。
疑问:我有一点不太明白,我们自己封装的驱动的这个hello_drv_open函数,不应该是调用相关的open函数来实现功能吗,这里面就一个打印函数,那这个函数怎么实现打开文件的功能的呢?
你这个疑问核心是没分清 "驱动层open函数的本质" 和 "普通文件open的功能" ------ 驱动里的hello_drv_open并不是 "打开文件",而是 **"响应应用层的open调用,完成设备的初始化 / 准备工作"**,它的 "打开功能" 是由内核框架兜底,而非函数内的代码直接实现。
一、先纠正核心认知:驱动open函数≠"打开文件"
我们先明确两个层面的open的区别:
| 层面 | 函数 / 操作 | 核心作用 |
|---|---|---|
| 应用层 | open("/dev/hello", O_RDWR) |
向内核发起 "访问设备" 的请求,内核会帮你创建文件描述符(fd),并关联到驱动 |
| 驱动层 | hello_drv_open |
响应内核的调用,完成设备 "打开前的初始化"(而非创建 fd / 打开文件) |
简单说:
- 应用层调用
open后,内核先完成所有 "文件层面" 的工作(比如分配 fd、检查权限、找到对应的驱动); - 内核确认一切合法后,才会调用驱动的
hello_drv_open,让你有机会做 "设备层面" 的准备。
你案例中hello_drv_open只有打印函数,依然能 "打开设备",本质是:"打开设备" 的核心逻辑由 Linux 内核框架实现,驱动的open函数只是 "可选的初始化钩子"。
二、内核是如何 "兜底实现打开功能" 的?
当你在应用层执行fd = open("/dev/hello", O_RDWR)时,内核会执行一套完整的流程,驱动的hello_drv_open只是其中一个 "可选步骤":
步骤 1:内核解析设备文件/dev/hello
内核从/dev/hello的 inode 节点中,提取出主设备号(比如 240)和次设备号(0)。
步骤 2:内核查找驱动映射表
内核查自己维护的 "字符设备驱动表",找到主设备号 240 对应的file_operations结构体(就是你注册的hello_drv)。
步骤 3:内核创建file结构体
内核为这次打开操作创建一个struct file对象(记录打开模式、当前偏移、驱动指针等),并分配一个文件描述符(fd)返回给应用层 ------ 这一步已经完成了 "打开" 的核心(拿到 fd)。
步骤 4:内核调用驱动的hello_drv_open
如果你的file_operations中指定了.open = hello_drv_open,内核就会调用这个函数;如果没指定(设为 NULL),内核也不会报错,直接认为 "打开成功"。
步骤 5:返回文件描述符
驱动的hello_drv_open返回 0(成功)后,内核把 fd 返回给应用层,应用层拿到 fd 就可以执行read/write了。
总结:"打开设备并拿到 fd" 的核心逻辑,是内核在调用hello_drv_open之前就完成的 ------ 驱动的open函数只是给你一个 "初始化设备" 的机会,哪怕你只写了打印,内核依然会完成 "打开" 的核心工作。
三、驱动open函数的真正价值:设备初始化(案例简化才只写打印)
你案例中hello_drv_open只有打印,是因为这是一个 "无硬件依赖的 demo";如果是真实硬件驱动(比如 LED / 按键 / 串口),hello_drv_open里会写满 "设备初始化代码",这才是它的核心作用。
(2)read函数(内核→用户空间读数据)
static ssize_t hello_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
// 核心:将内核缓冲区数据拷贝到用户空间(必须用copy_to_user,不能直接memcpy)
err = copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size); // 返回实际读取的字节数
}
- 入参:
__user *buf是用户空间缓冲区(内核不能直接访问),size是用户请求的读取长度; - 核心函数:
copy_to_user(dst, src, len)- 功能:安全地将内核空间数据拷贝到用户空间;
- 返回值:成功返回 0,失败返回未拷贝的字节数;
- 为什么不用
memcpy?用户空间地址在内核态是 "非法地址",直接访问会导致内核崩溃。
(3)write函数(用户→内核空间写数据)
static ssize_t hello_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
// 核心:将用户空间数据拷贝到内核缓冲区
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size); // 返回实际写入的字节数
}
- 核心函数:
copy_from_user(dst, src, len)- 功能:安全地将用户空间数据拷贝到内核空间;
- 返回值:成功返回 0,失败返回未拷贝的字节数。
(4)release函数(设备关闭)
static int hello_drv_close(struct inode *node, struct file *file)
{
printk("设备已关闭\n");
// 可在此处释放硬件资源
return 0;
}
- 作用:应用层调用
close(fd)时触发,用于释放资源(如关闭硬件、释放内存)。
步骤 4:通过register_chrdev向内核注册驱动
作用
将file_operations结构体 "注册" 到内核,让内核知道 "某个主设备号对应的驱动提供哪些操作接口"。
核心函数:register_chrdev
major = register_chrdev(major, "hello", &hello_drv);
- 入参:
major:主设备号(填 0 表示让内核自动分配);"hello":驱动名称(会显示在/proc/devices中);&hello_drv:驱动的file_operations结构体指针;
- 返回值:
- 成功:返回分配的主设备号(手动指定时返回传入的
major); - 失败:返回负数(如
-EBUSY表示主设备号已被占用)。
- 成功:返回分配的主设备号(手动指定时返回传入的
核心逻辑
注册完成后,内核会在字符设备列表中添加一条记录:"主设备号 X 对应的驱动是 hello_drv,支持 open/read/write 等操作"。
步骤 5:实现驱动入口函数(module_init)
作用
驱动的 "启动入口",当执行insmod hello_drv.ko安装驱动时,内核会自动调用该函数,完成驱动的初始化(如注册设备、创建设备类)。
代码示例
static int __init hello_init(void)
{
int err;
// 步骤4:注册字符设备
major = register_chrdev(0, "hello", &hello_drv);
// 后续步骤7会用到:创建设备类
hello_class = class_create(THIS_MODULE, "hello_class");
if (IS_ERR(hello_class)) { // 错误检查
unregister_chrdev(major, "hello"); // 注册失败则回滚
return -1;
}
return 0;
}
// 告诉内核:驱动的入口函数是hello_init
module_init(hello_init);
关键说明
__init:内核宏,标记该函数是初始化函数,驱动加载完成后会被释放(节省内存);- 错误处理:驱动开发必须严谨,某一步失败后要回滚已完成的操作(如注册设备后创建设备类失败,需先注销设备)。
步骤 6:实现驱动出口函数(module_exit)
作用
驱动的 "停止出口",当执行rmmod hello_drv卸载驱动时,内核会自动调用该函数,释放驱动占用的所有资源(避免内存泄漏)。
核心函数:unregister_chrdev
static void __exit hello_exit(void)
{
// 后续步骤7会用到:销毁设备节点、设备类
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
// 注销字符设备
unregister_chrdev(major, "hello");
}
// 告诉内核:驱动的出口函数是hello_exit
module_exit(hello_exit);
关键说明
__exit:内核宏,标记该函数是退出函数,仅在驱动卸载时调用;- 资源释放顺序:与入口函数的操作顺序相反(先创建的资源后销毁)。
步骤 7:自动创建设备节点(class_create/device_create)
作用
传统驱动需要手动执行mknod /dev/hello c 主设备号 次设备号创建设备节点,通过class_create和device_create可让内核自动生成/dev/xxx设备文件(依赖 udev 机制)。
核心函数解析
(1)class_create:创建设备类
hello_class = class_create(THIS_MODULE, "hello_class");
- 功能:在
/sys/class/目录下创建一个类(如/sys/class/hello_class); - 作用:为后续创建设备节点提供 "分类标识",udev 会监听
/sys/class下的变化。
(2)device_create:创建设备节点
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
- 入参:
hello_class:设备类指针;MKDEV(major, 0):将主设备号和次设备号(0)组合为dev_t类型;"hello":设备节点名称(最终生成/dev/hello);
- 功能:在
/dev/目录下生成设备节点文件,同时在/sys/class/hello_class/下创建设备目录。
对应出口函数的资源释放
// 销毁设备节点
device_destroy(hello_class, MKDEV(major, 0));
// 销毁设备类
class_destroy(hello_class);
最终补充:驱动元信息
驱动必须声明许可证(否则内核会报 "tainted" 警告),常用GPL:
MODULE_LICENSE("GPL"); // 声明许可证(必填)
MODULE_AUTHOR("Your Name"); // 可选:作者信息
MODULE_DESCRIPTION("Hello Char Device Driver"); // 可选:驱动描述