接下来我们自己来实现一个字符设备,进行一个实操演示。
一、字符设备驱动的基本结构
驱动程序主要包括以下几个关键部分:
- 注册设备号和 cdev
- 实现 file_operations 结构体(包含 read/write 等操作)
- 创建设备类和设备节点
- 资源释放和模块卸载
1、注册设备号和cdev
作用: 每个字符设备都需要一个唯一的“设备号”(主设备号+次设备号),供内核区分不同设备。cdev 是内核中真正代表你设备的“对象”。
步骤及API说明:
- 使用
alloc_chrdev_region()或register_chrdev_region()申请设备号。 - 定义并初始化一个
struct cdev类型的对象,用来描述你的设备。 - 把
cdev对象和设备号“绑定”起来,注册到系统。
2、实现file_operations结构体
作用: 定义驱动支持哪些“文件操作”——比如 read、write、open、release 等。以后应用程序访问设备节点(如 /dev/mydev)时,内核就会自动调用你填写的这些函数,实现具体数据的读写和设备管理。
结构体模板:
c
static struct file_operations mydev_fops = {
.owner = THIS_MODULE, // 模块所有者(必须填写)
.open = mydev_open, // 打开操作
.release = mydev_release, // 关闭/释放
.read = mydev_read, // 读操作
.write = mydev_write, // 写操作
// 可以根据需要实现ioctl、poll等
};1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
说明:
- 你只需要实现你自己的
mydev_open、mydev_read等函数(函数名可以自定义)。 - 没有实现的操作可以不填,相关功能就不可用。
3、创建设备类和设备节点
作用: 让你的设备能够被用户空间访问(如 /dev/mydev 文件),通常用 class_create() 和 device_create() 自动生成设备节点,大部分现代 Linux 配合 udev 都能自动生效。
步骤及API说明:
- 创建一个 class,用于 sysfs 分类和 udev 识别。
- 生成对应的设备节点(/dev/xxx)。
说明:
- 卸载时要用
device_destroy、class_destroy清理。
4、资源释放和模块卸载
作用: 驱动退出时必须“善后”——撤销前述所有注册和资源分配,避免系统资源泄漏、节点残留。
步骤及API说明:
- 删除设备节点和 class。
- 删除 cdev 对象,释放设备号。
- 释放内存等其它资源。
二、目录与文件组织
新建项目目录 05_char_device/,创建主要的代码文件:
mychardev.cMakefile
三、驱动源码详解
1、驱动源码编写
实现 /dev/mychardev 设备文件,支持应用程序读写,通过驱动内部的一个内存缓冲区交换数据。
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
// 设备名和类名的宏定义
#define DEV_NAME "device_test" // 设备节点名称
#define CLASS_NAME "class_test" // 设备类名称
static dev_t dev_num; // 保存设备号
static struct cdev cdev_test; // 字符设备结构体
static struct class *class_test; // 设备类指针
// 打开设备时调用的函数
// inode: 指向文件的 inode 结构体指针
// file: 文件结构体指针
static int chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_open called\n"); // 打印打开信息到内核日志
return 0; // 返回0表示成功
}
// 读设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 期望读取的字节数
// off: 偏移量指针
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_read called\n"); // 打印读操作信息
return 0; // 返回0表示没有数据可读
}
// 写设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 要写入的字节数
// off: 偏移量指针
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_write called\n"); // 打印写操作信息
return size; // 返回写入的字节数,表示写入成功
}
// 关闭设备时调用的函数
// inode: 指向文件的 inode 结构体指针
// file: 文件结构体指针
static int chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_release called\n"); // 打印关闭信息
return 0; // 返回0表示成功
}
// file_operations 结构体,指明本设备支持的操作
static struct file_operations cdev_fops_test = {
.owner = THIS_MODULE, // 拥有者,一般为 THIS_MODULE
.open = chrdev_open, // open 操作
.read = chrdev_read, // read 操作
.write = chrdev_write, // write 操作
.release = chrdev_release, // release 操作
};
// 模块加载时自动调用的初始化函数
static int __init chrdev_fops_init(void)
{
int ret;
int major, minor;
// 1. 自动申请设备号,主设备号和次设备号由内核分配
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "mychardev: alloc_chrdev_region failed\n"); // 申请失败
return ret;
}
major = MAJOR(dev_num); // 获取主设备号
minor = MINOR(dev_num); // 获取次设备号
printk(KERN_INFO "mychardev: alloc_chrdev_region ok: major=%d, minor=%d\n", major, minor);
// 2. 初始化 cdev 结构体,并添加到内核
cdev_init(&cdev_test, &cdev_fops_test); // 初始化 cdev
ret = cdev_add(&cdev_test, dev_num, 1); // 注册 cdev 到内核
if (ret < 0) {
printk(KERN_ERR "mychardev: cdev_add failed\n");
unregister_chrdev_region(dev_num,1); // 失败时释放设备号
return ret;
}
// 3. 创建设备类,便于自动创建设备节点
class_test = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(class_test)) {
printk(KERN_ERR "mychardev: class_create failed\n");
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num,1);
return PTR_ERR(class_test);
}
// 4. 创建设备节点 /dev/device_test
if (device_create(class_test, NULL, dev_num, NULL, DEV_NAME) == NULL) {
printk(KERN_ERR "mychardev: device_create failed\n");
class_destroy(class_test);
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num,1);
return -ENOMEM;
}
printk(KERN_INFO "mychardev: chrdev driver loaded successfully\n"); // 驱动加载成功
return 0;
}
// 模块卸载时自动调用的清理函数
static void __exit chrdev_fops_exit(void)
{
device_destroy(class_test, dev_num); // 删除设备节点
class_destroy(class_test); // 删除设备类
cdev_del(&cdev_test); // 注销 cdev
unregister_chrdev_region(dev_num, 1); // 释放设备号
printk(KERN_INFO "mychardev: chrdev driver unloaded\n"); // 卸载信息
}
// 指定模块的初始化和退出函数
module_init(chrdev_fops_init); // 加载模块时调用
module_exit(chrdev_fops_exit); // 卸载模块时调用
MODULE_LICENSE("GPL"); // 模块许可证声明1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2、源码详解
全局变量定义
c
static dev_t dev_num; // 保存设备号(主+次)
static struct cdev cdev_test; // 字符设备对象
static struct class *class_test; // 设备类指针1
2
3
2
3
dev_t dev_num: 保存设备号(主/次合成),用于内核管理和表示你的驱动。struct cdev cdev_test: Linux 内核的“字符设备对象”,管理和内核的关联。struct class *class_test: 用于自动生成 /dev/ 设备节点和 sysfs 分类。
open/release函数
c
static int chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_open called\n");
return 0;
}
static int chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_release called\n");
return 0;
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- 当应用程序调用
open("/dev/device_test", ...)或close()时,内核自动调用这些函数。 - 当前只打印日志,无额外资源操作,方便后续扩展(如分配/释放硬件缓存、初始化设备等)。
read/write函数
c
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_read called\n");
return 0;
}
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_write called\n");
return size;
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
chrdev_read:应用用read()读取设备会调用这里,现在只是日志,返回0,表示没有数据。(真实场景应把内核数据拷给用户,并返回实际字节数)chrdev_write:应用用write()会进这个函数。这里日志返回size,模拟“写入成功”。(真实驱动要把数据读入内核缓冲,或者转发给硬件)
file_operations结构体
c
static struct file_operations cdev_fops_test = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release,
};1
2
3
4
5
6
7
2
3
4
5
6
7
- 该结构体描述驱动支持哪些操作,每项都指向你自己的实现函数。
- 这是内核和应用之间的“操作函数跳板”。
驱动加载函数
alloc_chrdev_region:系统自动分配唯一设备号,添加到内核管理。MAJOR/MINOR:宏取设备号主/次部分,有时debug非常有用。cdev_init/cdev_add:配置/注册 cdev,实现 file_operations功能与设备号的关联。class_create/device_create:现代 Linux 推荐做法,让内核自动生成/dev/device_test节点,用户无需手动mknod。
驱动卸载函数
c
static void __exit chrdev_fops_exit(void)
{
device_destroy(class_test, dev_num);
class_destroy(class_test);
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "mychardev: chrdev driver unloaded\n");
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
核心思想: 分配/创建的资源(设备号、cdev、class、device节点)都要在卸载时完全释放,防止内核资源泄漏。
四、Makefile编写
在 05_char_device/ 下的 Makefile 如下编写:
makefile
export ARCH=arm64
# 交叉编译器绝对路径前缀
export CROSS_COMPILE=/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
# 和源文件名一致
obj-m += mychardev.o
# 内核源码目录
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
和之前编写的 Makefile 几乎一摸一样!
这不过这里变为了 mychardev.o
CROSS_COMPILE:依旧是SDK中的编译器路径前缀。KDIR:依旧是内核源码目录。
五、驱动编译与加载
终端进入 05_char_device/ 目录,执行:
sh
make1
生成 mychardev.ko :
- 将
mychardev.ko复制到开发板中(U盘、TF卡或者SSH都可以),然后加载驱动模块:
sh
sudo insmod mychardev.ko1
- 检查节点是否创建成功:
sh
ls -l /dev/device_test1
- 查看内核日志或调试信息(寻找所有和
mychardev相关的日志):
sh
dmesg | grep -E mychardev1