字符设备驱动

Notice: 这只是一个模板, 方便后续取用.

kernel version: 6.18.5

1 基本模板

内核程序.

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include <linux/device.h>
#include <linux/version.h>

#define DEVICE_NAME "chrdev_test"
#define CLASS_NAME "chrdev_class"
#define BUF_SIZE 1024
#define CMD_CLEAR _IO('k', 1)

struct my_device_data {
dev_t dev_num; // device number
struct cdev cdev;
struct class *class;
struct device *device;
char *buffer;
struct mutex lock;
};

static struct my_device_data my_dev;

static int chrdev_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "This is chrdev_open\n");
return 0;
}

static ssize_t chrdev_read(struct file *file, char __user *buf, size_t count, loff_t *offset) {
printk(KERN_INFO "This is chrdev_read\n");
int ret;
size_t avaliable;

mutex_lock(&my_dev.lock);
if (*offset >= BUF_SIZE) {
mutex_unlock(&my_dev.lock);
printk(KERN_WARNING "chrdev_read: buf is out of size");
return 0;
}

if (count > BUF_SIZE - *offset) {
count = BUF_SIZE - *offset;
}

ret = copy_to_user(buf, my_dev.buffer + *offset, count);
if (ret) {
mutex_unlock(&my_dev.lock);
return -EFAULT;
}

*offset += count;

mutex_unlock(&my_dev.lock);
printk(KERN_INFO "chrdev_read: read %d bytes\n", count);
return count;
}

static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) {
printk(KERN_INFO "This is chrdev_write\n");
int ret;
mutex_lock(&my_dev.lock);

if (*offset >= BUF_SIZE) {
mutex_unlock(&my_dev.lock);
return -ENOSPC;
}

if (count > BUF_SIZE - *offset) {
count = BUF_SIZE - *offset;
}

ret = copy_from_user(my_dev.buffer + *offset, buf, count);
if (ret) {
mutex_unlock(&my_dev.lock);
return -EFAULT;
}

*offset += count;

mutex_unlock(&my_dev.lock);
return count;
}

static int chrdev_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "This is chrdev_release");
return 0;
}

static long chrdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
switch(cmd) {
case CMD_CLEAR:
mutex_lock(&my_dev.lock);
memset(my_dev.buffer, 0, BUF_SIZE);
mutex_unlock(&my_dev.lock);
printk(KERN_INFO "chrdev_ioctl: buffer cleared via IOCTL\n");
break;
default:
return -EINVAL;
}
return 0;
}

static loff_t chrdev_llseek(struct file *file, loff_t offset, int whence) {
printk(KERN_INFO "This is chrdev_llseek");
loff_t new_pos;

switch (whence) {
case SEEK_SET: /* offset from file head */
new_pos = offset;
break;
case SEEK_CUR: /* offset from current pos */
new_pos = file->f_pos + offset;
break;
case SEEK_END: /* offset from file end*/
new_pos = BUF_SIZE + offset;
break;
default:
return -EINVAL;
}

if (new_pos < 0) {
return -EINVAL;
}

file->f_pos = new_pos;

return new_pos;
}

static struct file_operations fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.release = chrdev_release,
.read = chrdev_read,
.write = chrdev_write,
.unlocked_ioctl = chrdev_ioctl,
.llseek = chrdev_llseek,
};

static int __init chrdev_fops_init(void) {
int ret;
int major, minor;
mutex_init(&my_dev.lock);

my_dev.buffer = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!my_dev.buffer) {
printk(KERN_ERR "kzalloc failed\n");
return -ENOMEM;
}

// 1. alloc dev number
/*
* `dynamic alloc`:
* int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
* dev_t dev = MKDEV(235, 0);
* register_chrdev_region(dev, 1, "my_static_dev");
*
* `static alloc`:
* int register_chrdev_region(dev_t from, unsigned count, const char *name);
*/
ret = alloc_chrdev_region(&my_dev.dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "alloc_chrdev_region failed\n");
goto failed_mem;
}

printk(KERN_INFO "alloc_chrdev_region ok\n");
// major is only ocuppied with 12 bits width
major = MAJOR(my_dev.dev_num);
// minor is only ocuppied with 20 bits width
minor = MINOR(my_dev.dev_num);
printk(KERN_INFO "major is %d\n", major);
printk(KERN_INFO "minor is %d\n", minor);

// 2. init for cdev
cdev_init(&my_dev.cdev, &fops);
my_dev.cdev.owner = THIS_MODULE;

// 3. add cdev
ret = cdev_add(&my_dev.cdev, my_dev.dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "cdev_add failed\n");
goto failed_region;
}
printk(KERN_INFO "cdev_add is ok\n");

// 4. create class
// Notice: Linux 6.4+ remove `class_create's` fist argmument `THIS_MODULE`
// old kernel: class_create(THIS_MODULE, "class_test");
// new kernel: class_create("class_test");
// also we can use `LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)` to judge it
my_dev.class = class_create("class_test");
if (IS_ERR(my_dev.class)) {
printk(KERN_ERR "class_create failed! Error code: %d\n", ret);
ret = PTR_ERR(my_dev.class);
goto failed_cdev;
}

// 5. create node `/dev/adv_chrdev`
my_dev.device = device_create(my_dev.class, NULL, my_dev.dev_num, NULL, DEVICE_NAME);
if (IS_ERR(my_dev.device)) {
ret = PTR_ERR(my_dev.device);
printk(KERN_ERR "device_create failed! Error code: %d\n", ret);
goto failed_class;
}

printk(KERN_INFO "chrdev is ready");
return 0;

failed_class:
class_destroy(my_dev.class);
failed_cdev:
cdev_del(&my_dev.cdev);
failed_region:
unregister_chrdev_region(my_dev.dev_num, 1);
failed_mem:
kfree(my_dev.buffer);
return ret;
}

static void __exit chrdev_fops_exit(void) {
device_destroy(my_dev.class, my_dev.dev_num);
class_destroy(my_dev.class);
cdev_del(&my_dev.cdev);
unregister_chrdev_region(my_dev.dev_num, 1);
kfree(my_dev.buffer);
printk("module exit\n");
}

module_init(chrdev_fops_init);
module_exit(chrdev_fops_exit);
MODULE_LICENSE("GPL"); // also can be writed with "GPL v2"

应用程序.

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <getopt.h>

#define DEVICE_PATH "/dev/chrdev_test"
#define CMD_CLEAR _IO('k', 1)

void print_usage(char *prog_name) {
printf("Usage: %s [options]\n", prog_name);
printf("Options:\n");
printf(" -r Read data from device\n");
printf(" -w <string> Write string to device\n");
printf(" -c Clear device buffer (IOCTL)\n");
printf(" -s <offset> Seek to offset before operation\n");
}

int main(int argc, char *argv[]) {
int fd;
int opt;
char read_buf[1024];
int offset = 0;
int do_read = 0, do_clear = 0;
char *write_data = NULL;

if (argc < 2) {
print_usage(argv[0]);
return -1;
}

while ((opt = getopt(argc, argv, "rw:cs:")) != -1) {
switch (opt) {
case 'r': do_read = 1; break;
case 'w': write_data = optarg; break;
case 'c': do_clear = 1; break;
case 's': offset = atoi(optarg); break;
default: print_usage(argv[0]); return -1;
}
}

fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("Open device failed");
return -1;
}

if (offset != 0) {
if (lseek(fd, offset, SEEK_SET) < 0) {
perror("Lseek failed");
}
}

if (do_clear) {
if (ioctl(fd, CMD_CLEAR) < 0) {
perror("IOCTL Clear failed");
} else {
printf("Device buffer cleared.\n");
}
}

if (write_data) {
ssize_t bytes = write(fd, write_data, strlen(write_data));
if (bytes < 0) {
perror("Write failed");
} else {
printf("Wrote %zd bytes: %s\n", bytes, write_data);
}
}

if (do_read) {
memset(read_buf, 0, sizeof(read_buf));
ssize_t bytes = read(fd, read_buf, sizeof(read_buf) - 1);
if (bytes < 0) {
perror("Read failed");
} else {
printf("Read %zd bytes: %s\n", bytes, read_buf);
}
}

close(fd);
return 0;
}

效果.

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
sh-5.2# insmod /lib/modules/6.18.5/updates/chrdev.ko 
[ 14.781618] chrdev: loading out-of-tree module taints kernel.
[ 14.791590] alloc_chrdev_region ok
[ 14.792132] major is 241
[ 14.792492] minor is 0
[ 14.793071] cdev_add is ok
sh-5.2# ./tests/chrdev_usr -r
[ 14.794505] chrdev is ready
[ 18.975044] This is chrdev_open
[ 18.976375] This is chrdev_read
[ 18.977282] chrdev_read: read 1023 bytes
Read 1023 bytes:
sh-5.2# ./tests/chrdev_usr -w xzceqf454trfd43
[ 18.989735] This is chrdev_release
[ 25.635074] This is chrdev_open
[ 25.637552] This is chrdev_write
Wrote 15 bytes: xzceqf454trfd43
sh-5.2# ./tests/chrdev_usr -r
[ 25.659561] This is chrdev_release
[ 30.464881] This is chrdev_open
[ 30.465848] This is chrdev_read
[ 30.466200] chrdev_read: read 1023 bytes
Read 1023 bytes: xzceqf454trfd43
sh-5.2# ./tests/chrdev_usr -s 5 -r
[ 30.475604] This is chrdev_release
[ 34.422473] This is chrdev_open
[ 34.423981] This is chrdev_llseek
[ 34.425285] This is chrdev_read
[ 34.426301] chrdev_read: read 1019 bytes
Read 1019 bytes: f454trfd43
sh-5.2# ls /dev/chrdev_test
/dev/chrdev_test
sh-5.2# rmmod /lib/modules/6.18.5/updates/chrdev.ko
[ 34.443469] This is chrdev_release
[ 250.011079] module exit

1.2 杂项设备

杂项设备是一类特殊的字符设备.

在前面的驱动开发经历中, 为了创建一个字符设备,需要经历一个非常繁琐的流程:

  • alloc_chrdev_region(申请设备号)

  • cdev_init(初始化 cdev 结构体)

  • cdev_add(注册 cdev)

  • class_create(创建类,你刚才就在这里卡住了)

  • device_create(创建设备节点)

杂项设备的设计初衷, 就是为了把这 5 步缩减为 1 步.

杂项设备的特点是, 共享一个主设备号 ——– 10.

当我们注册一个杂项设备时, 内核会自动帮你处理 sysfs, classdevice 的创建. 也就是说, 我们不需要自己写 class_createdevice_create 了,内核会自动在 /dev 下生成设备文件.

内核程序.

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include <linux/miscdevice.h> // core

#define DEVICE_NAME "chrdev_test" // /dev/chrdev_test
#define BUF_SIZE 1024
#define CMD_CLEAR _IO('k', 1)

struct my_device_data {
char *buffer;
struct mutex lock;
};

static struct my_device_data my_dev;

static int chrdev_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "chrdev_misc: open\n");
return 0;
}

static int chrdev_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "chrdev_misc: release\n");
return 0;
}

static ssize_t chrdev_read(struct file *file, char __user *buf, size_t count, loff_t *offset) {
int ret;

if (*offset >= BUF_SIZE) {
return 0;
}

mutex_lock(&my_dev.lock);

if (count > BUF_SIZE - *offset) {
count = BUF_SIZE - *offset;
}

ret = copy_to_user(buf, my_dev.buffer + *offset, count);
if (ret) {
mutex_unlock(&my_dev.lock);
return -EFAULT;
}

*offset += count;
mutex_unlock(&my_dev.lock);

printk(KERN_INFO "chrdev_misc: read %zu bytes\n", count);
return count;
}

static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) {
int ret;

if (*offset >= BUF_SIZE) {
return -ENOSPC;
}

mutex_lock(&my_dev.lock);

if (count > BUF_SIZE - *offset) {
count = BUF_SIZE - *offset;
}

ret = copy_from_user(my_dev.buffer + *offset, buf, count);
if (ret) {
mutex_unlock(&my_dev.lock);
return -EFAULT;
}

*offset += count;
mutex_unlock(&my_dev.lock);

printk(KERN_INFO "chrdev_misc: wrote %zu bytes\n", count);
return count;
}

static long chrdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
switch(cmd) {
case CMD_CLEAR:
mutex_lock(&my_dev.lock);
memset(my_dev.buffer, 0, BUF_SIZE);
mutex_unlock(&my_dev.lock);
printk(KERN_INFO "chrdev_misc: buffer cleared via IOCTL\n");
break;
default:
return -EINVAL;
}
return 0;
}

static loff_t chrdev_llseek(struct file *file, loff_t offset, int whence) {
loff_t new_pos;

switch (whence) {
case SEEK_SET: new_pos = offset; break;
case SEEK_CUR: new_pos = file->f_pos + offset; break;
case SEEK_END: new_pos = BUF_SIZE + offset; break;
default: return -EINVAL;
}

if (new_pos < 0 || new_pos > BUF_SIZE) return -EINVAL;

file->f_pos = new_pos;
return new_pos;
}

static struct file_operations fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.release = chrdev_release,
.read = chrdev_read,
.write = chrdev_write,
.unlocked_ioctl = chrdev_ioctl,
.llseek = chrdev_llseek,
};

static struct miscdevice my_misc_device = {
.minor = MISC_DYNAMIC_MINOR, // auto alloc minor dev number
.name = DEVICE_NAME, // /dev/chrdev_test
.fops = &fops, // relate to fops
};

static int __init chrdev_init(void) {
int ret;

mutex_init(&my_dev.lock);
my_dev.buffer = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!my_dev.buffer) {
printk(KERN_ERR "kzalloc failed\n");
return -ENOMEM;
}

// 1. register misc device
// replace alloc_chrdev_region, cdev_init, cdev_add, class_create, device_create
ret = misc_register(&my_misc_device);
if (ret) {
printk(KERN_ERR "misc_register failed\n");
kfree(my_dev.buffer);
return ret;
}

printk(KERN_INFO "chrdev_misc: registered device /dev/%s\n", DEVICE_NAME);
return 0;
}

static void __exit chrdev_exit(void) {
// replace device_destroy, class_destroy, cdev_del, unregister_chrdev_region
misc_deregister(&my_misc_device);
kfree(my_dev.buffer);
printk(KERN_INFO "chrdev_misc: exit\n");
}

module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");