时间轴

2025-11-11

init


设备号

在 Linux 系统中每一个设备都有相应的设备号,设备号有主设备号次设备号之分:

  • 主设备号用来表示一个特定的驱动
  • 次设备号用来管理下面的设备。

比如主设备号1表示USB驱动,然后次设备2表示鼠标驱动,次设备3表示键盘驱动

注册字符设备驱动之前需要先申请设备号

include/linux/types.h

1
2
3
4
5
6
7
typedef u32 __kernel_dev_t;

typedef __kernel_fd_set fd_set;
typedef __kernel_dev_t dev_t;
typedef __kernel_ino_t ino_t;
typedef __kernel_mode_t mode_t;
typedef unsigned short umode_t;

设备号dev_t是u32类型,高12位为主设备号低20位为次设备号

设备号操作宏

include/linux/kdev_t.h

1
2
3
4
5
6
#define MINORBITS	20
#define MINORMASK ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
  • MINORBITS表示次设备号的位数,一共20位
  • MINORMASK用于计算次设备号
  • MAJOR表示从dev_t中获取主设备号,本质是将dev_t右移20位
  • MINOR表示从dev_t中获取次设备号,本质是取低20位的值
  • MKDEV用于将主设备号和次设备号组成dev_t类型的设备号

设备号申请函数

在 Linux 驱动中可以使用以下两种方法进行设备号的申请。

include/linux/fs.h

1
2
3
extern int register_chrdev_region(dev_t, unsigned, const char *);

extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
  1. 通过 register_chrdev_region(dev_t from, unsigned count, const char *name) 函数进行静态申请设备号。

    • 函数原型
    1
    register_chrdev_region(dev_t from, unsigned count, const char *name)
    • 函数作用:静态申请设备号,对指定好的设备号进行申请。
    • 参数含义
      • from: 自定义的 dev_t 类型设备号,比如MKDEV(100,0)表示起始主设备号100,起始次设备号为0
      • count: 次设备的数量,表示在主设备号相同的情况下有几个次设备号。
      • name: 申请的设备名称
    • 函数返回值:申请成功返回 0,申请失败返回负数
  2. 通过alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char* name)函数进行动态申请设备号。

    • 函数原型
    1
    alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
    • 函数作用:动态申请设备号,内核会自动分配一个未使用的设备号,相较于静态申请设备号,动态申请会避免注册设备号相同引发冲突的问题。

    • 参数含义

    • dev *: 会将申请完成的设备号保存在 dev 变量中

      • baseminor: 次设备号的起始地址,次设备号一般从0开始,所以这个参数一般设置成0
      • count: 申请设备的数量
      • name: 申请的设备名称
    • 函数返回值:申请成功返回 0,申请失败返回负数

设备号释放函数

include/linux/fs.h

1
extern void unregister_chrdev_region(dev_t, unsigned);
  • 函数功能:设备号释放函数,注销字符设备以后要释放掉设备号
  • 函数参数:
    • 参数1:要释放的设备号
    • 参数2:释放的设备号的数量

示例

my_driver.c

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>

static int major = 0;
static int minor = 0;

module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);

static dev_t dev_id;

static int __init my_driver_init(void)
{
int ret;

if (major) {
dev_id = MKDEV(major, minor);

printk(KERN_INFO "major from module_param: %d", MAJOR(dev_id));
printk(KERN_INFO "minor from module_param: %d", MINOR(dev_id));

ret = register_chrdev_region(dev_id, 1, "my_driver device");

if (ret < 0) {
printk(KERN_ERR "register_chrdev_region error\n");
} else {
printk(KERN_INFO "register_chrdev_region ok\n");
}

} else {
ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device");

if (ret < 0) {
printk(KERN_ERR "alloc_chrdev_region error\n");
} else {
printk(KERN_INFO "alloc_chrdev_region ok\n");

printk(KERN_INFO "major allocated: %d", MAJOR(dev_id));
printk(KERN_INFO "minor allocated: %d", MINOR(dev_id));
}
}

printk(KERN_INFO "my_driver: Module loaded\n");

return 0;
}

static void __exit my_driver_exit(void)
{
if (dev_id) {
unregister_chrdev_region(dev_id, 1);
printk(KERN_INFO "unregister_chrdev_region ok\n");
}
printk(KERN_INFO "my_driver: Module unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("my_driver Kernel Module");

测试:

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
~ # insmod my_driver.ko
[ 18.414274] my_driver: loading out-of-tree module taints kernel.
[ 18.421489] alloc_chrdev_region ok
[ 18.421652] major allocated: 511
[ 18.421667] minor allocated: 0
[ 18.421910] my_driver: Module loaded
~ # cat /proc/devices | grep my_driver
511 my_driver device
~ # rmmod my_driver.ko
[ 27.856484] unregister_chrdev_region ok
[ 27.856638] my_driver: Module unloaded
~ # cat /proc/devices | grep my_driver
~ #

~ # insmod my_driver.ko major=512 minor=0
[ 16.721502] my_driver: loading out-of-tree module taints kernel.
[ 16.728530] major from module_param: 512
[ 16.728574] minor from module_param: 0
[ 16.728753] CHRDEV "my_driver device" major requested (512) is greater than the maximum (511)
[ 16.729588] register_chrdev_region error
[ 16.729760] my_driver: Module loaded
~ # rmmod my_driver.ko
[ 36.863583] unregister_chrdev_region ok
[ 36.864560] my_driver: Module unloaded
~ # insmod my_driver.ko major=500 minor=0
[ 40.101231] major from module_param: 500
[ 40.101265] minor from module_param: 0
[ 40.101430] register_chrdev_region ok
[ 40.101657] my_driver: Module loaded
~ # rmmod my_driver.ko
[ 50.626705] unregister_chrdev_region ok
[ 50.626876] my_driver: Module unloaded

注册字符类设备

include/linux/cdev.h

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
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_CDEV_H
#define _LINUX_CDEV_H

#include <linux/kobject.h>
#include <linux/kdev_t.h>
#include <linux/list.h>
#include <linux/device.h>

struct file_operations;
struct inode;
struct module;

struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;

void cdev_init(struct cdev *, const struct file_operations *);

struct cdev *cdev_alloc(void);

void cdev_put(struct cdev *p);

int cdev_add(struct cdev *, dev_t, unsigned);

void cdev_set_parent(struct cdev *p, struct kobject *kobj);
int cdev_device_add(struct cdev *cdev, struct device *dev);
void cdev_device_del(struct cdev *cdev, struct device *dev);

void cdev_del(struct cdev *);

void cd_forget(struct inode *);

#endif

GCC/Clang attribute(__randomize_layout),
用于内核的 结构体布局随机化(struct layout randomization),增加攻击者预测结构偏移的难度,提高内核安全性。

cdev结构体

字段 含义
kobj 继承自 struct kobject,使字符设备能挂在 sysfs 下(即 /sys/class/... 等路径)。
owner 指向拥有该设备的模块(THIS_MODULE),防止模块卸载时设备仍在使用。
ops 指向设备操作函数表(struct file_operations),定义 read/write/ioctl 等行为。
list 内部链表,用于内核维护所有注册的字符设备。
dev 设备号,类型是 dev_t(包含主设备号 major 和次设备号 minor)。
count 表示这个 cdev 控制的连续设备编号数量(常为 1)。

cdev_init

fs/char_dev.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
  • 作用:初始化一个静态分配的 struct cdev。建立cdev和file_operations之间的联系

cdev_add

fs/char_dev.c

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
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;

p->dev = dev;
p->count = count;

if (WARN_ON(dev == WHITEOUT_DEV))
return -EBUSY;

error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;

kobject_get(p->kobj.parent);

return 0;
}
  • 作用:向系统添加一个cdev结构体,也就是添加一个字符设备

cdev_del

fs/char_dev.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*
* NOTE: This guarantees that cdev device will no longer be able to be
* opened, however any cdevs already open will remain and their fops will
* still be callable even after cdev_del returns.
*/
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
  • 作用:系统中删除一个字符设备

示例

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>

static int major = 0;
static int minor = 0;

module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);

static dev_t dev_id;

static struct cdev cdev_test;

static struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
};

static int __init my_driver_init(void)
{
int ret;

if (major) {
dev_id = MKDEV(major, minor);

pr_info( "major from module_param: %d", MAJOR(dev_id));
pr_info( "minor from module_param: %d", MINOR(dev_id));

ret = register_chrdev_region(dev_id, 1, "my_driver device");

if (ret < 0) {
pr_err( "register_chrdev_region error\n");
return ret;
} else {
pr_info( "register_chrdev_region ok\n");
}

} else {
ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device");

if (ret < 0) {
pr_err( "alloc_chrdev_region error\n");
return ret;
} else {
pr_info( "alloc_chrdev_region ok\n");

pr_info( "major allocated: %d", MAJOR(dev_id));
pr_info( "minor allocated: %d", MINOR(dev_id));
}
}

cdev_init(&cdev_test, &cdev_test_ops);

//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
cdev_test.owner = THIS_MODULE;

ret = cdev_add(&cdev_test, dev_id, 1);

if (ret < 0) {
pr_err( "cdev_add error\n");
unregister_chrdev_region(dev_id, 1);
return ret;

}else{
pr_info( "cdev_add ok\n");
}

pr_info( "my_driver: Module loaded\n");

return 0;
}

static void __exit my_driver_exit(void)
{
if (dev_id) {
cdev_del(&cdev_test);
unregister_chrdev_region(dev_id, 1);
pr_info( "unregister_chrdev_region ok\n");
}
pr_info( "my_driver: Module unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("my_driver Kernel Module");

注意先删除设备再释放设备号。

file_operations结构体

Linux有一个很重要的概念叫一切皆文件,也就是Linux中的设备就像普通的文件一样。访问一个设备好像是在访问一个文件。在应用程序中我们可以使用open, read, write, close, ioctl这几个系统调用来操作驱动。当我们在应用程序中调用open函数的时候,最终会去执行驱动中的open函数。所以file_operations将系统调用和驱动程序连接起来了。

应用中调用open函数

1
fd = open("/dev/hello", O_RDWR);

驱动中执行open函数

1
2
3
4
5
6
7
8
9
10
11
12
static int cdev_test_open(struct inode *, struct file*){
printk("This is cdev_test open");
return 0;
}

static struct file_operations cdev_test_ops={
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release
}

linux中file_operations定义:

include/linux/fs.h

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
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
bool may_pollfree;
} __randomize_layout;

常见字段解析:

函数指针 用户态调用 作用 是否常用
owner 通常写为 THIS_MODULE,防止模块被正在使用时卸载
open open() 打开设备时调用,一般用于初始化或统计打开次数
release close() 关闭设备时调用,用于释放资源
read read() 用户读设备数据
write write() 用户写数据到设备
unlocked_ioctl ioctl() 用户发控制命令
poll poll()/select() 实现非阻塞 I/O(epoll 机制) ⚙️ 常见于网络或串口驱动
mmap mmap() 将物理内存映射到用户空间 ⚙️ GPU / DMA 驱动常用
llseek lseek() 文件位置偏移 ⚙️ 文件类驱动
fasync fcntl(F_SETFL, O_ASYNC) 支持异步通知 ⚙️ 串口驱动常见

返回值约定

  • ssize_t:>=0 成功字节数,<0 errno
  • int / long:0 成功,<0 errno
  • 指针:成功返回指针,失败返回 ERR_PTR(-errno)

llseek

1
loff_t (*llseek)(struct file *file, loff_t offset, int whence);
  • 作用:文件指针偏移操作(类似 lseek 系统调用)。
  • 参数
    • file:文件对象。
    • offset:偏移量。
    • whence
      • SEEK_SET:相对于文件开头
      • SEEK_CUR:相对于当前文件指针
      • SEEK_END:相对于文件末尾
  • 返回值
    • 新的文件指针位置(loff_t
    • 出错返回负值(如 -EINVAL

read

1
ssize_t (*read)(struct file *file, char __user *buf, size_t count, loff_t *pos);
  • 作用:从文件读数据到用户空间。
  • 参数
    • file:文件对象。
    • buf:用户空间缓冲区指针。
    • count:希望读取的字节数。
    • pos:文件指针位置,读完后需要更新。
  • 返回值
    • 成功:实际读取的字节数
    • 出错:负数 errno

write

1
ssize_t (*write)(struct file *file, const char __user *buf, size_t count, loff_t *pos);
  • 作用:把用户数据写入文件/设备。
  • 参数
    • file:文件对象
    • buf:用户缓冲区
    • count:写入字节数
    • pos:文件指针
  • 返回值
    • 成功:实际写入的字节数
    • 出错:负数 errno

mmap

1
int (*mmap)(struct file *, struct vm_area_struct *);
  • 作用:支持用户空间内存映射到设备。
  • 参数
    • file:文件对象
    • vma:虚拟内存区域结构
  • 返回值
    • 0 成功
    • <0 错误
  • mmap_supported_flags:映射时支持的标志。

open

1
int (*open)(struct inode *inode, struct file *file);
  • 作用:当用户态调用 open() 打开设备或文件时被调用。
  • 参数
    • inode:指向设备对应的 inode 结构,包含文件系统层信息和设备号。
    • file:指向文件对象(struct file),保存文件状态、偏移量、private_data 等。
  • 返回值
    • 0:成功打开
    • <0:出错,返回对应 errno(如 -EBUSY 表示设备忙,-ENOMEM 内存不足)
  • 典型用途
    • 检查设备是否已经被占用
    • 初始化 file->private_data 指针
    • 增加模块引用计数(THIS_MODULE

flush

1
int (*flush)(struct file *file, fl_owner_t id);
  • 作用:刷新文件缓冲区,确保用户态写入的数据在内核缓冲中被提交。
  • 参数
    • file:文件对象
    • id:文件句柄拥有者标识,一般是文件描述符对应的进程
  • 返回值
    • 0:成功
    • <0:出错
  • 说明
    • 对于大多数字符设备驱动,flush 不需要特殊实现,直接返回 0 即可
    • 在块设备或网络设备中,flush 可能用于提交缓存数据

release

1
int (*release)(struct inode *inode, struct file *file);
  • 作用:当用户态调用 close() 关闭设备或文件时被调用。
  • 参数
    • inode:设备对应 inode
    • file:文件对象
  • 返回值
    • 0:成功关闭
    • <0:出错
  • 典型用途
    • 释放在 open() 时分配的资源
    • 恢复设备状态(如标记为未被占用)
    • 减少模块引用计数

示例

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>

static int major = 0;
static int minor = 0;

module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);

static dev_t dev_id;

static struct cdev cdev_test;

int cdev_test_open(struct inode *inode, struct file *file)
{
pr_info("cdev_test_open was called");
return 0;
}

ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size,
loff_t *off)
{
pr_info("cdev_test_read was called");
return 0;
}
ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size,
loff_t *off)
{
pr_info("cdev_test_write was called");
return 0;
}
int cdev_test_release(struct inode *inode, struct file *file)
{
pr_info("cdev_test_release was called");
return 0;
}

static struct file_operations cdev_test_ops = { .owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release };

static int __init my_driver_init(void)
{
int ret;

if (major) {
dev_id = MKDEV(major, minor);

pr_info("major from module_param: %d", MAJOR(dev_id));
pr_info("minor from module_param: %d", MINOR(dev_id));

ret = register_chrdev_region(dev_id, 1, "my_driver device");

if (ret < 0) {
pr_err("register_chrdev_region error\n");
return ret;
} else {
pr_info("register_chrdev_region ok\n");
}

} else {
ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device");

if (ret < 0) {
pr_err("alloc_chrdev_region error\n");
return ret;
} else {
pr_info("alloc_chrdev_region ok\n");

pr_info("major allocated: %d", MAJOR(dev_id));
pr_info("minor allocated: %d", MINOR(dev_id));
}
}

cdev_init(&cdev_test, &cdev_test_ops);

//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
cdev_test.owner = THIS_MODULE;

ret = cdev_add(&cdev_test, dev_id, 1);

if (ret < 0) {
pr_err("cdev_add error\n");
unregister_chrdev_region(dev_id, 1);
return ret;

} else {
pr_info("cdev_add ok\n");
}

pr_info("my_driver: Module loaded\n");

return 0;
}

static void __exit my_driver_exit(void)
{
if (dev_id) {
cdev_del(&cdev_test);
unregister_chrdev_region(dev_id, 1);
pr_info("unregister_chrdev_region ok\n");
}
pr_info("my_driver: Module unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("my_driver Kernel Module");

设备结点

在 Linux 操作系统中一切皆文件,对于用来进行设备访问的文件称之为设备节点。程序通过操作这个”设备文件”,便可以操作对应的硬件。

1
fd = open("/dev/hello", O_RDWR);

这个“设备文件”就是设备结点。所以Linux设备结点是应用程序和驱动程序沟通的一个桥梁。设备节点被创建在/dev 目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[zhaohang@cyberboy linux_driver_learning]$ ll /dev
crw------- 4,15 root 11 Nov 10:05 󰡯 tty15
crw------- 4,16 root 11 Nov 10:05 󰡯 tty16
crw------- 4,17 root 11 Nov 10:05 󰡯 tty17
crw------- 4,18 root 11 Nov 10:05 󰡯 tty18
crw------- 4,19 root 11 Nov 10:05 󰡯 tty19
crw------- 4,20 root 11 Nov 10:05 󰡯 tty20
crw------- 4,21 root 11 Nov 10:05 󰡯 tty21
crw------- 4,22 root 11 Nov 10:05 󰡯 tty22
crw------- 4,23 root 11 Nov 10:05 󰡯 tty23
crw------- 4,24 root 11 Nov 10:05 󰡯 tty24
crw------- 4,25 root 11 Nov 10:05 󰡯 tty25
crw------- 4,26 root 11 Nov 10:05 󰡯 tty26
crw------- 4,27 root 11 Nov 10:05 󰡯 tty27
crw------- 4,28 root 11 Nov 10:05 󰡯 tty28
crw------- 4,29 root 11 Nov 10:05 󰡯 tty29

4,15表示主设备号和次设备号。crw中c表示字符设备。

根据设备节点的创建方式不同,分为了手动创建设备节点自动创建设备节点

Linux下创建结点的方式

手动创建设备结点

使用 mknod 命令手动创建设备节点,mknod 命令格式为:

1
mknod NAME TYPE MAJOR MINOR

参数含义:

  • NAME: 要创建的节点名称
  • TYPE: b 表示块设备,c 表示字符设备,p 表示管道
  • MAJOR:要链接设备的主设备号
  • MINOR: 要链接设备的从设备号

例如: 使用以下命令创建一个名为 device_test 的字符设备节点,设备的主设备号和从设备号分别为 236 和 0。

1
mknod /dev/device_test c 236 0

自动创建设备结点

自动创建设备节点是利用 udev 机制来实现的。

udev 是一个用户程序,通过检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件

自动创建设备节点需要在驱动中首先使用 class_create()函数创建一个类,创建的类位于于/sys/class/ 目录下之后使用 device_create()函数在这个类下创建相应的设备,在加载驱动模块时,用户空间中的 udev 会自动响应根据并/sys/class/ 下的信息创建设备节点。

在嵌入式Linux中我们使用的是mdev,mdev是udev的简化版本。在使用busybox构建的跟文件系统的时候,busybox会自动创建mdev。

class_create函数

linux/device/class.h

引用头文件使用linux/device/device.h因为device.h引用了class.h

1
2
3
4
5
6
7
8
/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

  • 函数作用:用于动态创建设备类。
  • 参数含义
    • owner:struct module 结构体类型的指针。一般赋值为 THIS_MODULE。
    • name:char 类型的指针,代表即将创建的 struct class 变量的名字。
  • 返回值:struct class * 类型的结构体
class_destroy函数

linux/device/class.h

1
extern void class_destroy(struct class *cls);
  • 函数作用:用于删除设备类。
  • 参数含义:
    • owner:struct module 结构体类型的指针,一般赋值为 THIS_MODULE。
    • name:char 类型的指针,代表即将创建的 struct class 变量的名字。
device_create函数

linux/device/device.h

使用class_create创建好类以后,还需要使用device_create函数在类下面创建一个设备。

1
2
3
__printf(5, 6) struct device *
device_create(struct class *cls, struct device *parent, dev_t devt,
void *drvdata, const char *fmt, ...);
  • 函数作用:用来在 class 类下创建一个设备文件。
  • 参数含义:
    • cls:指定所要创建的设备所从属的类。
    • parent:指定该设备的父设备,如果没有就指定为 NULL。
    • devt:指定创建设备的设备号。
    • drvdata:被添加到该设备回调的数据,没有则指定为 NULL。
    • fmt:添加到系统的设备节点名称。
  • 返回值:struct device * 类型结构体
device_destroy函数

linux/device/device.h

1
void device_destroy(struct class *cls, dev_t devt);
  • 函数作用:用来删除 class 类中的设备。
  • 参数含义:
    • cls:指定所要创建的设备所从属的类。
    • devt:指定创建设备的设备号。
示例
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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
// #include <linux/device/class.h>

static int major = 0;
static int minor = 0;

module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);

struct class *class;
struct device *device;

static dev_t dev_id;

static struct cdev cdev_test;
int cdev_test_open(struct inode *inode, struct file *file)
{
pr_info("cdev_test_open was called");
return 0;
}

ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size,
loff_t *off)
{
pr_info("cdev_test_read was called");
return 0;
}
ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size,
loff_t *off)
{
pr_info("cdev_test_write was called");
return 0;
}
int cdev_test_release(struct inode *inode, struct file *file)
{
pr_info("cdev_test_release was called");
return 0;
}

static struct file_operations cdev_test_ops = { .owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release };

static int __init my_driver_init(void)
{
int ret;

if (major) {
dev_id = MKDEV(major, minor);

pr_info("major from module_param: %d", MAJOR(dev_id));
pr_info("minor from module_param: %d", MINOR(dev_id));

ret = register_chrdev_region(dev_id, 1, "my_driver device");

if (ret < 0) {
pr_err("register_chrdev_region error\n");
return ret;
} else {
pr_info("register_chrdev_region ok\n");
}

} else {
ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device");

if (ret < 0) {
pr_err("alloc_chrdev_region error\n");
return ret;
} else {
pr_info("alloc_chrdev_region ok\n");

pr_info("major allocated: %d", MAJOR(dev_id));
pr_info("minor allocated: %d", MINOR(dev_id));
}
}

cdev_init(&cdev_test, &cdev_test_ops);

//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
cdev_test.owner = THIS_MODULE;

ret = cdev_add(&cdev_test, dev_id, 1);

if (ret < 0) {
pr_err("cdev_add error\n");
unregister_chrdev_region(dev_id, 1);
return ret;

} else {
pr_info("cdev_add ok\n");
}

class = class_create(THIS_MODULE, "test");
// /dev/my_driver
device = device_create(class, NULL, dev_id, NULL, "my_driver");
pr_info("my_driver: Module loaded\n");

return 0;
}

static void __exit my_driver_exit(void)
{
// 删除设备结点
device_destroy(class, dev_id);
// 删除该设备结点的class
class_destroy(class);

// 删除cdev
cdev_del(&cdev_test);
// 释放设备号
unregister_chrdev_region(dev_id, 1);

pr_info("unregister_chrdev_region ok\n");

pr_info("my_driver: Module unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("my_driver Kernel Module");

测试:

1
2
3
4
5
6
7
8
9
10
11
~ # insmod my_driver.ko
[ 8.681454] my_driver: loading out-of-tree module taints kernel.
[ 8.688800] alloc_chrdev_region ok
[ 8.688997] major allocated: 511
[ 8.689010] minor allocated: 0
[ 8.689322] cdev_add ok
[ 8.690848] my_driver: Module loaded
~ # ls /dev/ | grep my_driver
~ # mdev -s
~ # ls /dev/ | grep my_driver
my_driver

用户空间和内核空间

用户空间——>内核空间

  • 系统调用
  • 软中断
  • 硬件中断

用户空间和内核空间的数据交换

内核空间和用户空间的内存是不能互相访问的。但是很多应用程序都需要和内核进行数据的交换,例如应用程序使用 read()函数从驱动中读取数据,使用 write()函数向驱动中写数据,上述功能就需要使用 copy_from_user()和 copy_to_user()俩个函数来完成。

  • copy_from_user()函数是将用户空间的数据拷贝到内核空间。
  • copy_to_user()函数是将内核空间的数据拷贝到用户空间。

include/linux/uaccess.h

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
/*
* Architectures should provide two primitives (raw_copy_{to,from}_user())
* and get rid of their private instances of copy_{to,from}_user() and
* __copy_{to,from}_user{,_inatomic}().
*
* raw_copy_{to,from}_user(to, from, size) should copy up to size bytes and
* return the amount left to copy. They should assume that access_ok() has
* already been checked (and succeeded); they should *not* zero-pad anything.
* No KASAN or object size checks either - those belong here.
*
* Both of these functions should attempt to copy size bytes starting at from
* into the area starting at to. They must not fetch or store anything
* outside of those areas. Return value must be between 0 (everything
* copied successfully) and size (nothing copied).
*
* If raw_copy_{to,from}_user(to, from, size) returns N, size - N bytes starting
* at to must become equal to the bytes fetched from the corresponding area
* starting at from. All data past to + size - N must be left unmodified.
*
* If copying succeeds, the return value must be 0. If some data cannot be
* fetched, it is permitted to copy less than had been fetched; the only
* hard requirement is that not storing anything at all (i.e. returning size)
* should happen only when nothing could be copied. In other words, you don't
* have to squeeze as much as possible - it is allowed, but not necessary.
*
* For raw_copy_from_user() to always points to kernel memory and no faults
* on store should happen. Interpretation of from is affected by set_fs().
* For raw_copy_to_user() it's the other way round.
*
* Both can be inlined - it's up to architectures whether it wants to bother
* with that. They should not be used directly; they are used to implement
* the 6 functions (copy_{to,from}_user(), __copy_{to,from}_user_inatomic())
* that are used instead. Out of those, __... ones are inlined. Plain
* copy_{to,from}_user() might or might not be inlined. If you want them
* inlined, have asm/uaccess.h define INLINE_COPY_{TO,FROM}_USER.
*
* NOTE: only copy_from_user() zero-pads the destination in case of short copy.
* Neither __copy_from_user() nor __copy_from_user_inatomic() zero anything
* at all; their callers absolutely must check the return value.
*
* Biarch ones should also provide raw_copy_in_user() - similar to the above,
* but both source and destination are __user pointers (affected by set_fs()
* as usual) and both source and destination can trigger faults.
*/

static __always_inline __must_check unsigned long
__copy_from_user_inatomic(void *to, const void __user *from, unsigned long n)
{
instrument_copy_from_user(to, from, n);
check_object_size(to, n, false);
return raw_copy_from_user(to, from, n);
}

static __always_inline __must_check unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
might_fault();
if (should_fail_usercopy())
return n;
instrument_copy_from_user(to, from, n);
check_object_size(to, n, false);
return raw_copy_from_user(to, from, n);
}

/**
* __copy_to_user_inatomic: - Copy a block of data into user space, with less checking.
* @to: Destination address, in user space.
* @from: Source address, in kernel space.
* @n: Number of bytes to copy.
*
* Context: User context only.
*
* Copy data from kernel space to user space. Caller must check
* the specified block with access_ok() before calling this function.
* The caller should also make sure he pins the user space address
* so that we don't result in page fault and sleep.
*/
static __always_inline __must_check unsigned long
__copy_to_user_inatomic(void __user *to, const void *from, unsigned long n)
{
if (should_fail_usercopy())
return n;
instrument_copy_to_user(to, from, n);
check_object_size(from, n, true);
return raw_copy_to_user(to, from, n);
}

static __always_inline __must_check unsigned long
__copy_to_user(void __user *to, const void *from, unsigned long n)
{
might_fault();
if (should_fail_usercopy())
return n;
instrument_copy_to_user(to, from, n);
check_object_size(from, n, true);
return raw_copy_to_user(to, from, n);
}

#ifdef INLINE_COPY_FROM_USER
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (!should_fail_usercopy() && likely(access_ok(from, n))) {
instrument_copy_from_user(to, from, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
#else
extern __must_check unsigned long
_copy_from_user(void *, const void __user *, unsigned long);
#endif

#ifdef INLINE_COPY_TO_USER
static inline __must_check unsigned long
_copy_to_user(void __user *to, const void *from, unsigned long n)
{
might_fault();
if (should_fail_usercopy())
return n;
if (access_ok(to, n)) {
instrument_copy_to_user(to, from, n);
n = raw_copy_to_user(to, from, n);
}
return n;
}
#else
extern __must_check unsigned long
_copy_to_user(void __user *, const void *, unsigned long);
#endif

static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(check_copy_size(to, n, false)))
n = _copy_from_user(to, from, n);
return n;
}

static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(check_copy_size(from, n, true)))
n = _copy_to_user(to, from, n);
return n;
}
#ifdef CONFIG_COMPAT
static __always_inline unsigned long __must_check
copy_in_user(void __user *to, const void __user *from, unsigned long n)
{
might_fault();
if (access_ok(to, n) && access_ok(from, n))
n = raw_copy_in_user(to, from, n);
return n;
}
#endif

#ifndef copy_mc_to_kernel
/*
* Without arch opt-in this generic copy_mc_to_kernel() will not handle
* #MC (or arch equivalent) during source read.
*/
static inline unsigned long __must_check
copy_mc_to_kernel(void *dst, const void *src, size_t cnt)
{
memcpy(dst, src, cnt);
return 0;
}
#endif

copy_to_user()函数

函数原型:

1
2
3
4
5
6
7
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(check_copy_size(from, n, true)))
n = _copy_to_user(to, from, n);
return n;
}
  • 函数作用:把内核空间的数据复制到用户空间。
  • 参数含义:
    • *to 是用户空间的指针
    • from 是内核空间的指针
    • n 是从内核空间向用户空间拷贝的字节数
  • 返回值:等于0表示成功,其他表示失败

copy_from_user()函数

1
2
3
4
5
6
7
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(check_copy_size(to, n, false)))
n = _copy_from_user(to, from, n);
return n;
}
  • 函数作用:把用户空间的数据复制到内核空间。
  • 参数含义
    • *to 是内核空间的指针
    • from 是用户空间的指针
    • n 是从用户空间向内核空间拷贝的字节数
  • 返回值:等于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
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
#include "linux/printk.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

static int major = 0;
static int minor = 0;

module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);

struct class *class;
struct device *device;

static dev_t dev_id;

static struct cdev cdev_test;
int cdev_test_open(struct inode *inode, struct file *file)
{
pr_info("cdev_test_open was called");
return 0;
}

ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size,
loff_t *off)
{
char kbuf[] = "This is my_driver read\n";
if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) {
pr_err("copy kbuf to buf from kernel error\n");
return -1;
}
pr_info("copy kbuf to buf from kernel ok\n");
pr_info("cdev_test_read was called");
return 0;
}
ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size,
loff_t *off)
{
char kbuf[32] = { 0 };
if (copy_from_user(kbuf, buf, size) != 0) {
pr_err("copy buf to kbuf from kernel error\n");
return -1;
}
printk("read from user: %s", kbuf);
pr_info("cdev_test_write was called");
return 0;
}
int cdev_test_release(struct inode *inode, struct file *file)
{
pr_info("cdev_test_release was called");
return 0;
}

static struct file_operations cdev_test_ops = { .owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release };

static int __init my_driver_init(void)
{
int ret;

if (major) {
dev_id = MKDEV(major, minor);

pr_info("major from module_param: %d", MAJOR(dev_id));
pr_info("minor from module_param: %d", MINOR(dev_id));

ret = register_chrdev_region(dev_id, 1, "my_driver device");

if (ret < 0) {
pr_err("register_chrdev_region error\n");
return ret;
} else {
pr_info("register_chrdev_region ok\n");
}

} else {
ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device");

if (ret < 0) {
pr_err("alloc_chrdev_region error\n");
return ret;
} else {
pr_info("alloc_chrdev_region ok\n");

pr_info("major allocated: %d", MAJOR(dev_id));
pr_info("minor allocated: %d", MINOR(dev_id));
}
}

cdev_init(&cdev_test, &cdev_test_ops);

//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
cdev_test.owner = THIS_MODULE;

ret = cdev_add(&cdev_test, dev_id, 1);

if (ret < 0) {
pr_err("cdev_add error\n");
unregister_chrdev_region(dev_id, 1);
return ret;

} else {
pr_info("cdev_add ok\n");
}

class = class_create(THIS_MODULE, "test");
// /dev/my_driver
device = device_create(class, NULL, dev_id, NULL, "my_driver");
pr_info("my_driver: Module loaded\n");

return 0;
}

static void __exit my_driver_exit(void)
{
// 删除设备结点
device_destroy(class, dev_id);
// 删除该设备结点的class
class_destroy(class);

// 删除cdev
cdev_del(&cdev_test);
// 释放设备号
unregister_chrdev_region(dev_id, 1);

pr_info("unregister_chrdev_region ok\n");

pr_info("my_driver: Module unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("my_driver Kernel Module");

测试

测试代码:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv)
{
int ret, fd;

fd = open("/dev/my_driver", O_RDWR);
if (fd < 0) {
printf("open /dev/my_driver error\n");
return -1;
}

char rbuf[32] = { 0 };
ret = read(fd, rbuf, sizeof(rbuf));
if (ret < 0) {
printf("read error\n");
goto err;
}
printf("read from driver:%s\n", rbuf);

char wbuf[32] = "hello world\n";
ret = write(fd, wbuf, sizeof(wbuf));
if (ret < 0) {
printf("write error\n");
goto err;
}
success:
close(fd);
return 0;
err:
close(fd);
return -1;
}

makefile

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
TARGET_EXEC := test
SRC_DIRS := ./
BUILD_DIR := ./build

CC = aarch64-linux-gnu-gcc
CFLAGS = -g -Wall

# 源文件收集
SRCS := $(shell find $(SRC_DIRS) -name '*.c' -or -name '*.s' -or -name '*.S')

# 目标文件映射
OBJS := $(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))
OBJS := $(patsubst %.s,$(BUILD_DIR)/%.o,$(OBJS))
OBJS := $(patsubst %.S,$(BUILD_DIR)/%.o,$(OBJS))

# 包含路径
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

# 自动生成依赖文件
CPPFLAGS := $(INC_FLAGS) -MMD -MP

# 链接最终可执行文件
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
@mkdir -p $(dir $@)
$(CC) $(OBJS) -o $@ $(LDFLAGS)

# 编译规则 (C)
$(BUILD_DIR)/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# 编译规则 (汇编)
# 不需要预处理的汇编代码
$(BUILD_DIR)/%.o: %.s
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# 需要预处理的汇编代码
$(BUILD_DIR)/%.o: %.S
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# 清理
.PHONY: clean deploy
clean:
rm -rf $(BUILD_DIR)
deploy:
cp $(BUILD_DIR)/$(TARGET_EXEC) ~/tftp

# 自动依赖包含
-include $(OBJS:.o=.d)

测试输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~ # insmod my_driver.ko
[ 7.250701] my_driver: loading out-of-tree module taints kernel.
[ 7.257204] alloc_chrdev_region ok
[ 7.257387] major allocated: 511
[ 7.257406] minor allocated: 0
[ 7.257563] cdev_add ok
[ 7.258587] my_driver: Module loaded
~ # mdev -s
~ # ./test
[ 18.006803] cdev_test_open was called
[ 18.007016] copy kbuf to buf from kernel ok
read from driver:This is my_driver read

[ 18.007275] cdev_test_read was called
[ 18.010813] read from user: hello world
[ 18.011205] cdev_test_write was called

文件私有数据

通常在驱动开发中会为设备自定义设备结构体将设备结构体设置为文件私有数据。设备结构体用来存放硬件相关信息,如设备号、类、设备名称等。

Linux 中并没有明确规定要必须要使用文件私有数据,但是在 linux 驱动源码中随处可见文件私有数据,因此可以认为使用文件私有数据是 Linux 驱动遵循的“潜规则”,实际上文件私有数据也体现了 Linux 面向对象的思想。

文件私有数据就是将私有数据private_data指向设备结构体。

1
2
3
4
5
6
struct device_test dev1;

static int cdev_test_open(struct inode *inode, struct file *file){
file->private_data = &dev1;
return 0;
}

然后在read, write等函数中通过private_data访问设备结构体

1
2
3
4
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off_t){
struct device_test *test_dev = (struct device_test *)file->private_data;
return 0;
}

用全局变量也可以实现这样的功能,但是随着驱动代码复杂度增多,使用private_data更利于管理

示例

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

struct device_test {
// 设备号
dev_t dev_id;
char kbuf[32];
struct class *class;
struct device *device;
// cdev
struct cdev cdev_test;
};

struct device_test device1;

static int major_num;
static int minor_num;

module_param(major_num, int, S_IRUGO);
module_param(minor_num, int, S_IRUGO);

int cdev_test_open(struct inode *inode, struct file *file)
{
pr_info("cdev_test_open was called");
file->private_data = &device1;
return 0;
}

ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size,
loff_t *off)
{
struct device_test *dev1 = (struct device_test *)file->private_data;

if (copy_to_user(buf, dev1->kbuf, strlen(dev1->kbuf)) != 0) {
pr_err("copy kbuf to buf from kernel error\n");
return -1;
}
pr_info("copy kbuf to buf from kernel ok\n");
pr_info("cdev_test_read was called");
return 0;
}

ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size,
loff_t *off)
{
struct device_test *dev1 = (struct device_test *)file->private_data;
if (copy_from_user(dev1->kbuf, buf, size) != 0) {
pr_err("copy buf to kbuf from kernel error\n");
return -1;
}
printk("read from user: %s", dev1->kbuf);
pr_info("cdev_test_write was called");
return 0;
}
int cdev_test_release(struct inode *inode, struct file *file)
{
pr_info("cdev_test_release was called");
return 0;
}

static struct file_operations cdev_test_ops = { .owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release };

static int __init my_driver_init(void)
{
int ret;

// 申请设备号
if (major_num) {
device1.dev_id = MKDEV(major_num, minor_num);

pr_info("major from module_param: %d", MAJOR(device1.dev_id));
pr_info("minor from module_param: %d", MINOR(device1.dev_id));

ret = register_chrdev_region(device1.dev_id, 1,
"my_driver device");

if (ret < 0) {
pr_err("register_chrdev_region error\n");
return ret;
} else {
pr_info("register_chrdev_region ok\n");
}

} else {
ret = alloc_chrdev_region(&device1.dev_id, 0, 1,
"my_driver device");

if (ret < 0) {
pr_err("alloc_chrdev_region error\n");
return ret;
} else {
pr_info("alloc_chrdev_region ok\n");

pr_info("major allocated: %d", MAJOR(device1.dev_id));
pr_info("minor allocated: %d", MINOR(device1.dev_id));
}
}
// cdev初始化,注册字符类设备
cdev_init(&device1.cdev_test, &cdev_test_ops);

//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
device1.cdev_test.owner = THIS_MODULE;

ret = cdev_add(&device1.cdev_test, device1.dev_id, 1);

if (ret < 0) {
pr_err("cdev_add error\n");
unregister_chrdev_region(device1.dev_id, 1);
return ret;

} else {
pr_info("cdev_add ok\n");
}

device1.class = class_create(THIS_MODULE, "test");
// /dev/my_driver
device1.device = device_create(device1.class, NULL, device1.dev_id,
NULL, "my_driver");
pr_info("my_driver: Module loaded\n");

return 0;
}

static void __exit my_driver_exit(void)
{
// 删除设备结点
device_destroy(device1.class, device1.dev_id);
// 删除该设备结点的class
class_destroy(device1.class);

// 删除cdev
cdev_del(&device1.cdev_test);
// 释放设备号
unregister_chrdev_region(device1.dev_id, 1);

pr_info("unregister_chrdev_region ok\n");

pr_info("my_driver: Module unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("my_driver Kernel Module");

使用文件私有数据的场景

在Linux中,使用主设备号来表示对应的某一类驱动。用次设备号表示这类驱动下的各个设备。

假如现在我们的驱动要支持主设备相同,但是次设备号不同的设备。我们的驱动就可以利用文件私有数据来写。

container_of 宏

include/linux/kernel.h

1
2
3
4
5
6
#define container_of(ptr, type, member) ({				\
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
  • ptr 指向结构体中某个成员的指针。
  • type 目标结构体类型。
  • member 成员在结构体中的名字。
  • BUILD_BUG_ON_MSG(…)

编译期类型检查,保证 ptr 和结构体成员类型一致,避免类型错误。

  • __mptr - offsetof(type, member)

核心公式:用成员指针减去该成员在结构体中的偏移,得到结构体起始地址。

  • offsetof(type, member) 标准宏,计算成员在结构体内的字节偏移。

示例

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define BUF_SIZE 32

/* 设备结构体 */
struct device_test {
dev_t dev_num; // 设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev cdev_test; // cdev
char kbuf[BUF_SIZE]; // 内核缓冲区
};

static struct device_test dev1;
static struct device_test dev2;

static struct class *test_class;

/* 打开设备 */
static int cdev_test_open(struct inode *inode, struct file *file)
{
struct device_test *dev;

/* 根据 inode->i_cdev 获取 device_test 结构体 */
dev = container_of(inode->i_cdev, struct device_test, cdev_test);
file->private_data = dev;

pr_info("cdev_test_open: minor=%d\n", dev->minor);
return 0;
}

/* 读取设备 */
static ssize_t cdev_test_read(struct file *file, char __user *buf,
size_t size, loff_t *off)
{
struct device_test *dev = file->private_data;
size_t len = min(strlen(dev->kbuf), size);

if (copy_to_user(buf, dev->kbuf, len))
return -EFAULT;

pr_info("cdev_test_read: minor=%d, data=%s\n", dev->minor, dev->kbuf);
return len;
}

/* 写入设备 */
static ssize_t cdev_test_write(struct file *file, const char __user *buf,
size_t size, loff_t *off)
{
struct device_test *dev = file->private_data;
size_t len = min(size, (size_t)(BUF_SIZE - 1));

if (copy_from_user(dev->kbuf, buf, len))
return -EFAULT;

dev->kbuf[len] = '\0';
pr_info("cdev_test_write: minor=%d, data=%s\n", dev->minor, dev->kbuf);

return len;
}

/* 释放设备 */
static int cdev_test_release(struct inode *inode, struct file *file)
{
pr_info("cdev_test_release\n");
return 0;
}

/* 文件操作结构体 */
static struct file_operations cdev_test_fops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
};

/* 驱动入口 */
static int __init chr_fops_init(void)
{
int ret;

/* 申请两个连续设备号 */
ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "my_chardev");
if (ret < 0)
return ret;

dev1.major = MAJOR(dev1.dev_num);
dev1.minor = 0;
dev2.major = dev1.major;
dev2.minor = 1;

/* 初始化 cdev */
cdev_init(&dev1.cdev_test, &cdev_test_fops);
dev1.cdev_test.owner = THIS_MODULE;
cdev_add(&dev1.cdev_test, MKDEV(dev1.major, dev1.minor), 1);

cdev_init(&dev2.cdev_test, &cdev_test_fops);
dev2.cdev_test.owner = THIS_MODULE;
cdev_add(&dev2.cdev_test, MKDEV(dev2.major, dev2.minor), 1);

/* 创建设备类 */
test_class = class_create(THIS_MODULE, "test");
if (IS_ERR(test_class)) {
pr_err("class_create failed\n");
cdev_del(&dev1.cdev_test);
cdev_del(&dev2.cdev_test);
unregister_chrdev_region(dev1.dev_num, 2);
return PTR_ERR(test_class);
}

/* 创建设备节点 /dev/test1 和 /dev/test2 */
device_create(test_class, NULL, MKDEV(dev1.major, dev1.minor), NULL, "test1");
device_create(test_class, NULL, MKDEV(dev2.major, dev2.minor), NULL, "test2");

pr_info("my_chardev: Module loaded, major=%d\n", dev1.major);
return 0;
}

/* 驱动出口 */
static void __exit chr_fops_exit(void)
{
device_destroy(test_class, MKDEV(dev1.major, dev1.minor));
device_destroy(test_class, MKDEV(dev2.major, dev2.minor));
class_destroy(test_class);

cdev_del(&dev1.cdev_test);
cdev_del(&dev2.cdev_test);

unregister_chrdev_region(dev1.dev_num, 2);

pr_info("my_chardev: Module unloaded\n");
}

module_init(chr_fops_init);
module_exit(chr_fops_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("Dual minor char device driver example");

杂项设备驱动

杂项设备属于特殊的一种字符型设备,是对字符设备的一种封装,本质也是字符设备。

在 Linux 中,把无法归类的五花八门的设备定义成杂项设备。相较于字符设备,杂项设备有以下两个优点:

  1. 节省主设备号: 杂项设备的主设备号固定为 10,当系统中注册了多个杂项设备驱动时,只需使用子设备号进行区分即可。而字符设备不管是动态分配还是静态分配设备号,都会消耗一个主设备号,进而造成了主设备号浪费。
  2. 使用简单杂项设备驱动本身包含创建设备节点操作,不需要额外使用 class_create()函数和 device_create()函数实现自动创建设备节点操作。只需要填充驱动中的 file_operations 结构体中的成员即可。

定义

include/linux/miscdevice.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct device;
struct attribute_group;

struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};

extern int misc_register(struct miscdevice *misc);
extern void misc_deregister(struct miscdevice *misc);

定义一个杂项设备,一般只需要填充 miscdevice 结构体中 minor、name、fops 这三个成员变量。

  • minor 杂项指次设备号,可以从内核源码include/linux/miscdevice.h 文件中预定义的次设备号挑选,也可以自行定义子设备号(没有被其他设备使用即可),通常情况下将该参数设置为 MISC_DYNAMIC_MINOR,表示自动分配子设备号。
  • name 表示杂项设备的名字。也就是杂项设备驱动注册成功之后,会在 dev 目录下生成名为 name 的设备节点。
  • fops 指向了file_operations 的结构体,表示文件操作集。

misc_register

1
extern int misc_register(struct miscdevice *misc);
  • 函数作用:基于 misc_class 构造一个设备,将 miscdevice 结构挂载到 misc_list 列表上,并初始化与 linux设备模型相关的结构。进而起到杂项设备注册的作用。

  • 参数含义

    • misc: 杂项设备的结构体指针
  • 函数返回值:申请成功返回 0,申请失败返回负数

misc_deregister

1
extern void misc_deregister(struct miscdevice *misc);
  • 函数作用:从 mist_list 中删除 miscdevice,进而起到杂项设备卸载的作用。
  • 参数含义
    • misc: 杂项设备的结构体指针

示例

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>


static ssize_t misc_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
pr_info("misc_read\n");
return 0;
}

static ssize_t misc_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
pr_info("misc_write\n");
return 0;
}

/* 文件操作集合 */
static const struct file_operations misc_fops = {
.owner = THIS_MODULE,
.read = misc_read,
.write = misc_write,
};

/* 杂项设备结构体 */
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR, // 动态申请次设备号
.name = "test", // /dev/test
.fops = &misc_fops,
};

/* 驱动入口 */
static int __init misc_init(void)
{
int ret;

ret = misc_register(&misc_dev);
if (ret) {
pr_err("misc_register failed: %d\n", ret);
return ret;
}

pr_info("misc device registered successfully, minor=%d\n", misc_dev.minor);
return 0;
}

/* 驱动出口 */
static void __exit misc_exit(void)
{
misc_deregister(&misc_dev);
pr_info("misc device deregistered\n");
}

module_init(misc_init);
module_exit(misc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaohang");
MODULE_DESCRIPTION("Simple misc device example");

Linux驱动错误处理

goto 逆序释放

逆序释放:后申请的先释放

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
static int __init my_driver_init(void)
{
int ret;

ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "my_driver");
if (ret < 0)
goto fail_alloc;

cdev_init(&dev1.cdev_test, &cdev_test_fops);
dev1.cdev_test.owner = THIS_MODULE;

ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
if (ret < 0)
goto fail_cdev_add;

dev1.class = class_create(THIS_MODULE, "test1");
if (IS_ERR(dev1.class)) {
ret = PTR_ERR(dev1.class);
goto fail_class_create;
}

dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1");
if (IS_ERR(dev1.device)) {
ret = PTR_ERR(dev1.device);
goto fail_device_create;
}

pr_info("my_driver initialized successfully\n");
return 0;

fail_device_create:
class_destroy(dev1.class);
fail_class_create:
cdev_del(&dev1.cdev_test);
fail_cdev_add:
unregister_chrdev_region(dev1.dev_num, 1);
fail_alloc:
return ret;
}
  • 标签顺序是“逆序释放”,即 后申请的先释放
  • PTR_ERR() + IS_ERR() 用于判断 class_create()device_create() 的错误。
  • PTR_ERR() 将错误指针转换为错误码,并进行错误码的返回
  • goto 能避免重复写多次 if 检查和清理代码,使代码简洁。

IS_ERR()

如果一个函数返回值是指针类型,在调用出错的情况下会返回 NULL 指针,但是返回 NULL不能知道问题的确切性,一些函数需要返回一个实际的错误码以便于工程师能够基于返回值作出正确的判断。
对于任何一个指针来说,必然存在三种情况:

  • 合法指针
  • NULL(也就是空指针)
  • 错误指针(也就是无效指针)

在 Linux 内核中,所谓的错误指针指向了内核空间的最后一页,例如,对于一个 64 位系统来说,最后一页的地址是0xfffffffffffff000~0xffffffffffffffff,
最后一页地址是被保留的,并这段地址与内核定义的错误码相关联,因此可以使用错误码用来指明对应出错的情况,如果一个指针指向了该页的地址范围就被定义为错误指针。
在 Linux 内核源码中提供了错误指针相关的 API 函数,主要有 IS_ERR()、PTR_ERR()、ERR_PTR()函数等,其函数的源码在 include/linux/err.h 文件中。

include/linux/err.h

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
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_ERR_H
#define _LINUX_ERR_H

#include <linux/compiler.h>
#include <linux/types.h>

#include <asm/errno.h>

/*
* Kernel pointers have redundant information, so we can use a
* scheme where we can return either an error code or a normal
* pointer with the same return value.
*
* This should be a per-architecture thing, to allow different
* error and pointer decisions.
*/
#define MAX_ERRNO 4095

#ifndef __ASSEMBLY__

#define IS_ERR_VALUE(x) unlikely((unsigned long)(void *)(x) >= (unsigned long)-MAX_ERRNO)

static inline void * __must_check ERR_PTR(long error)
{
return (void *) error;
}

static inline long __must_check PTR_ERR(__force const void *ptr)
{
return (long) ptr;
}

static inline bool __must_check IS_ERR(__force const void *ptr)
{
return IS_ERR_VALUE((unsigned long)ptr);
}

static inline bool __must_check IS_ERR_OR_NULL(__force const void *ptr)
{
return unlikely(!ptr) || IS_ERR_VALUE((unsigned long)ptr);
}

/**
* ERR_CAST - Explicitly cast an error-valued pointer to another pointer type
* @ptr: The pointer to cast.
*
* Explicitly cast an error-valued pointer to another pointer type in such a
* way as to make it clear that's what's going on.
*/
static inline void * __must_check ERR_CAST(__force const void *ptr)
{
/* cast away the const */
return (void *) ptr;
}

static inline int __must_check PTR_ERR_OR_ZERO(__force const void *ptr)
{
if (IS_ERR(ptr))
return PTR_ERR(ptr);
else
return 0;
}

#endif

#endif /* _LINUX_ERR_H */

PTR_ERR()

把错误指针转成错误码

include/linux/err.h

1
2
3
4
static inline long __must_check PTR_ERR(__force const void *ptr)
{
return (long) ptr;
}

错误码

include/uapi/asm-generic/errno-base.h

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
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */

#endif

示例:

1
2
3
4
if (IS_ERR(dev1.class)) {
ret = PTR_ERR(dev1.class);
goto fail_class_create;
}

点亮一个LED灯(RK3568开发板)

TODO