时间轴

2025-11-11

init


设备号

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

  • 主设备号:标识设备类型
  • 次设备号:在同一个驱动管理的多个设备实例之间进行区分

举例:

  • 主设备号 13 表示 input 子系统(处理输入设备)。
    • 次设备号 64 → /dev/input/event0(可能是 USB 键盘)
    • 次设备号 65 → /dev/input/event1(可能是 USB 鼠标)
  • 主设备号 188 表示 USB 串行设备
    • 次设备号 0 → /dev/ttyUSB0
    • 次设备号 1 → /dev/ttyUSB1

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

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 *);

register_chrdev_region()

  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,申请失败返回负数

注意,静态申请的设备号是有限制的:include/linux/fs.h中定义:

1
2
3
4
5
6
7
/* fs/char_dev.c */
#define CHRDEV_MAJOR_MAX 512
/* Marks the bottom of the first segment of free char majors */
#define CHRDEV_MAJOR_DYN_END 234
/* Marks the top and bottom of the second segment of free char majors */
#define CHRDEV_MAJOR_DYN_EXT_START 511
#define CHRDEV_MAJOR_DYN_EXT_END 384

alloc_chrdev_region()

  1. 通过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,申请失败返回负数

设备号释放函数

unregister_chrdev_region()

include/linux/fs.h

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

示例

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
#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)
{
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

在 C 语言中,如果你只需要使用某个结构体的 指针,而不需要知道它的内部成员(即不进行解引用或 sizeof),就可以只做 前置声明

1
struct file_operations;  // 告诉编译器:存在一个叫 file_operations 的 struct 类型

这样你就可以定义指针:

1
struct file_operations *fops;

但不能访问成员(因为编译器还不知道结构体内容):

1
fops->read(...);  // ❌ 编译错误:不完整类型

这在头文件中非常常见,用于 解耦依赖加快编译速度

extern存储类说明符(storage class specifier),用于声明 变量函数 的外部链接属性。它的作用是告诉编译器:“这个变量/函数在别处定义,这里只是声明”。

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 内部链表,将所有共享该 cdev 的 inode(即 /dev/xxx 文件)通过 inode->i_devices 链接到一起,形成一个“反向引用链表”。
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_init后最好调用cdev_test.owner = THIS_MODULE; 将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块

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_map:全局 kobj_map 结构(哈希表)
  • p:指向 struct cdev
  • 注意:cdev 内嵌了 struct kobject kobj,所以 cdev 可以被视为一个 kobject

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
#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)
{
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() 将物理内存映射到用户空间
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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>

static int major = 0;
module_param(major, int, S_IRUGO);
MODULE_PARM_DESC(major, "cdev_test sample, major device number");

static int minor = 0;
module_param(minor, int, S_IRUGO);
MODULE_PARM_DESC(minor, "cdev_test sample, minor device number");

dev_t dev_num;
struct cdev cdev;

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


ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){
pr_info("cdev_test read\n");
return 0;
}

ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t, loff_t *offset){
pr_info("cdev_test write\n");
return 0;
}


struct file_operations fops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
};

static int __init cdev_test_init(void)
{
int err;
if (major) {
dev_num = MKDEV(major, minor);
err = register_chrdev_region(dev_num, 1, "cdev test dev num");
if (err < 0) {
pr_err("register_chrdev_region error\n");
goto chrdev_region_err;
}
pr_info("register_chrdev_region success\n");
} else {
err = alloc_chrdev_region(&dev_num, 0, 1, "cdev test dev num");
if (err < 0) {
pr_err("alloc_chrdev_region error\n");
goto chrdev_region_err;
}
pr_info("alloc_chrdev_region success\n");
}
pr_info("dev_num: major[%d] minor[%d]", MAJOR(dev_num), MINOR(dev_num));

cdev_init(&cdev, &fops);
pr_info("cdev_init success\n");
cdev.owner = THIS_MODULE; // 将owner指向本模块,防止cdev操作时卸载模块

err = cdev_add(&cdev, dev_num, 1);
pr_info("cdev_add success\n");
if (err < 0) {
pr_err("cdev_add error\n");
goto cdev_add_err;
}

return 0;
cdev_add_err:
unregister_chrdev_region(dev_num, 1);
chrdev_region_err:
return err;
}

static void __exit cdev_test_exit(void)
{
cdev_del(&cdev);
pr_info("cdev is deleted\n");
unregister_chrdev_region(dev_num, 1);
pr_info("chrdev_region unregister success\n");
}

module_init(cdev_test_init);
module_exit(cdev_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("This is just a cdev test sample");

设备结点

在 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

对于上面 file_operations 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
$ insmod cdev_test.ko
[ 43.969734] cdev_test: loading out-of-tree module taints kernel.
[ 43.979783] alloc_chrdev_region success
[ 43.980242] dev_num: major[241] minor[0]
[ 43.980277] cdev_init success
[ 43.981192] cdev_add success
$ mknod /dev/cdev_test c 241 0
$ ls /dev/cdev_test
/dev/cdev_test
$ cat /dev/cdev_test
[ 68.723315] cdev_test open
[ 68.723900] cdev_test read

自动创建设备结点

自动创建设备节点是利用 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);
  • 函数作用:用于删除设备类。
  • 参数含义:
    • cls:调用class_create()函数返回的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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>

static int major = 0;

module_param(major, int, S_IRUGO);
MODULE_PARM_DESC(major, "mknod_test dev major num");

static int minor = 0;
module_param(minor, int, S_IRUGO);
MODULE_PARM_DESC(minor, "mknod_test dev minor num");

dev_t dev_num;


int mknod_test_open(struct inode *inode, struct file *file){
pr_info("This is mknod_test open\n");
return 0;
}
ssize_t mknod_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){
pr_info("This is mknod test read\n");
return 0;
}

int mknod_test_release (struct inode *inode, struct file *file){
pr_info("This is mknod test release\n");
return 0;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.open = mknod_test_open,
.read = mknod_test_read,
.release = mknod_test_release,
};

struct cdev cdev;
struct class *class;
struct device *dev;


static int __init mknod_test_init(void)
{
int err;

pr_info("mknod_test module init\n");

if (major) {
dev_num = MKDEV(major, minor);
err = register_chrdev_region(dev_num, 1, "mknod test device num");
if (err < 0) {
pr_err("register_chrdev_region error\n");
goto chrdev_region_err;
}
pr_info("register_chrdev_region success\n");
} else {
err = alloc_chrdev_region(&dev_num, 0, 1, "mknod test device num");
if (err < 0) {
pr_err("alloc_chrdev_region error\n");
goto chrdev_region_err;
}
pr_info("alloc_chrdev_region success\n");
}
pr_info("dev_t dev_num: major[%d], minor[%d]\n", MAJOR(dev_num), MINOR(dev_num));

cdev_init(&cdev, &fops);
cdev.owner = THIS_MODULE;

err = cdev_add(&cdev, dev_num, 1);
if (err < 0) {
pr_err("cdev add error\n");
goto cdev_add_err;
}

class = class_create(THIS_MODULE, "chrdev");
if (IS_ERR(class)) {
err = PTR_ERR(class);
pr_err("class_create error\n");
goto class_create_err;
}
dev = device_create(class, NULL, dev_num, NULL, "mknod_test_device");
if(IS_ERR(dev)){
err = PTR_ERR(dev);
pr_err("device create error\n");
goto device_create_err;
}

return 0;
device_create_err:
class_destroy(class);
class_create_err:
cdev_del(&cdev);
cdev_add_err:
unregister_chrdev_region(dev_num, 1);
chrdev_region_err:
return err;
}

static void __exit mknod_test_exit(void)
{
device_destroy(class, dev_num);
class_destroy(class);
cdev_del(&cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("mknod_test module exit\n");
}
module_init(mknod_test_init);
module_exit(mknod_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("This is just mknod test sample");

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ insmod mknod_test.ko
[ 11.793662] mknod_test: loading out-of-tree module taints kernel.
[ 11.803110] mknod_test module init
[ 11.803371] alloc_chrdev_region success
[ 11.804103] dev_t dev_num: major[241], minor[0]
$ ls /dev/mknod_test_device
/dev/mknod_test_device
$ cat /dev/mknod_test_device
[ 22.546620] This is mknod_test open
[ 22.547827] This is mknod test read
[ 22.548435] This is mknod test release
$ rmmod mknod_test.ko
[ 28.103036] mknod_test module exit

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

内核空间和用户空间的内存是不能互相访问的。但是很多应用程序都需要和内核进行数据的交换,例如应用程序使用 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

整体调用链如下

1
2
3
4
5
6
7
8
9
copy_from_user(to, from, n)
└─ check_copy_size(to, n, false) // 编译时对象大小检查(FORTIFY_SOURCE)
└─ _copy_from_user(to, from, n)
├─ might_fault() // 标记可能引起页错误(调度点)
├─ should_fail_usercopy() // 故障注入(用于测试)
├─ access_ok(from, n) // 检查用户指针是否合法(关键!)
├─ instrument_copy_from_user() // KASAN / UBSAN 检测
├─ raw_copy_from_user(...) // 实际拷贝(arch-specific)
└─ if (res) memset(..., 0, res) // **Zero-padding!**
  • Zero-padding: 如果只拷贝了部分数据(比如因为页错误),剩余未拷贝的部分会被清零。这是为了防止内核信息泄露(例如栈上未初始化内存被用户读到)。

  • access_ok():确保 from 指向用户空间且长度合法(防止访问内核地址)

  • check_object_size():防止缓冲区溢出(配合 FORTIFY_SOURCE)

  • instrument_*:KASAN 检测越界访问

即使拷贝失败,也不会 panic,而是返回未拷贝字节数,驱动可据此处理。

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/string.h>

static int major = 0;
module_param(major, int, S_IRUGO);
MODULE_DESCRIPTION("device num: major");

static int minor = 0;
module_param(minor, int, S_IRUGO);
MODULE_DESCRIPTION("device num: minor");

static char buf[] = "Hello World from kernel\n";

struct test_data {
dev_t dev_num;
struct cdev cdev;
struct class *class;
struct device *dev;
char kbuf[32];
};

static struct test_data *dat;

static int file_private_test_open(struct inode *inode, struct file *file)
{
pr_info("file_private_test_open is called\n");
// 设置私有数据
file->private_data = dat;
pr_info("file->private_data is set\n");
return 0;
}
static ssize_t file_private_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
struct test_data *data;
size_t len;

if (*offset != 0) { // EOF
return 0;
}

data = file->private_data;
len = min(size, strlen(dat->kbuf));

pr_info("file_private_test_read is called\n");



if (copy_to_user(buf, data->kbuf, len) != 0) {
pr_err("copy_to_user error\n");
return -EFAULT;
}

*offset += len;
return sizeof(data->kbuf);
}

static int file_private_test_release(struct inode *inode, struct file *file)
{
pr_info("file_private_test_release is called\n");
return 0;
}

static struct file_operations fops = {
.owner = THIS_MODULE,
.open = file_private_test_open,
.read = file_private_test_read,
.release = file_private_test_release,
};

static int __init file_private_test_init(void)
{
int err;

pr_info("file_private_test init\n");
dat = (struct test_data *)kmalloc(sizeof(struct test_data), GFP_KERNEL);

if (dat == NULL) {
pr_err("no memory\n");
goto kmalloc_fail;
}
strscpy(dat->kbuf, buf, sizeof(dat->kbuf));

// 申请设备号
if (major) {
dat->dev_num = MKDEV(major, minor);
err = register_chrdev_region(dat->dev_num, 1, "file_private_test chrdev region");
if (err < 0) {
pr_err("register_chrdev_region error\n");
goto chrdev_region_fail;
}
} else {
err = alloc_chrdev_region(&dat->dev_num, 0, 1, "file_private_test chrdev region");
if (err < 0) {
pr_err("register_chrdev_region error\n");
goto chrdev_region_fail;
}
}
// 添加cdev
cdev_init(&dat->cdev, &fops);
err = cdev_add(&dat->cdev, dat->dev_num, 1);
if (err < 0) {
pr_err("cdev_add error\n");
goto cdev_add_fail;
}
// 创建设备节点
dat->class = class_create(THIS_MODULE, "char_test");
if (IS_ERR(dat->class)) {
err = PTR_ERR(dat->class);
pr_err("create class error");
goto class_create_fail;
}
dat->dev = device_create(dat->class, NULL, dat->dev_num, NULL, "char_test_dev");
if (IS_ERR(dat->dev)) {
err = PTR_ERR(dat->dev);
pr_err("create device error\n");
goto device_create_fail;
}

return 0;
device_create_fail:
class_destroy(dat->class);
class_create_fail:
cdev_del(&dat->cdev);
cdev_add_fail:
unregister_chrdev_region(dat->dev_num, 1);
chrdev_region_fail:
kfree(dat);
kmalloc_fail:
return err;
}

static void __exit file_private_test_exit(void)
{
device_destroy(dat->class, dat->dev_num);
class_destroy(dat->class);
cdev_del(&dat->cdev);
unregister_chrdev_region(dat->dev_num, 1);
kfree(dat);
pr_info("file_private_test exit\n");
}
module_init(file_private_test_init);
module_exit(file_private_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("This is a test sample for file private data");

测试:

1
2
3
4
5
6
7
8
9
$ insmod file_private_data_test.ko
[ 8.022277] file_private_data_test: loading out-of-tree module taints kernel.
[ 8.032581] file_private_test init
$ cat /dev/char_test_dev
[ 14.998329] file_private_test_open is called
[ 14.998580] file->private_data is set
[ 14.999659] file_private_test_read is called
Hello World from kernel
[ 15.000469] file_private_test_release is called

使用文件私有数据的场景

在 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
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
#include <linux/module.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define KBUF_SIZE 32
struct container_data {
dev_t dev_num;
struct cdev cdev;
struct device *dev;
char kbuf[KBUF_SIZE];
};

static struct class *class;
static struct container_data *dat1;
static struct container_data *dat2;

int container_of_test_open(struct inode *inode, struct file *file)
{
struct container_data *dat;

// 通过container_of获取特定的container_data
dat = container_of(inode->i_cdev, struct container_data, cdev);
file->private_data = dat;

pr_info("container_of_test_open is called, dev major: %d, dev minor: %d\n",
MAJOR(dat->dev_num), MINOR(dat->dev_num));

return 0;
}

ssize_t container_of_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
struct container_data *dat = file->private_data;
size_t kbuf_len = strlen(dat->kbuf);
size_t len = min(size, (size_t)(kbuf_len - *offset));

if (*offset >= kbuf_len)
return 0;

if (copy_to_user(buf, dat->kbuf + *offset, len))
return -EFAULT;

*offset += len;

return len;
}

ssize_t container_of_test_write(struct file *file, const char __user *buf, size_t size,
loff_t *offset)
{
struct container_data *dat = file->private_data;
size_t bytes = min(size, (size_t)(KBUF_SIZE - 1 - *offset));

if (*offset >= KBUF_SIZE - 1)
return -ENOSPC;

if (copy_from_user(dat->kbuf + *offset, buf, bytes) != 0)
return -EFAULT;

*offset += bytes;
dat->kbuf[*offset] = '\0';

return bytes;
}

int container_of_test_release(struct inode *inode, struct file *file)
{
pr_info("container_of_test_release is called\n");
return 0;
}

static struct file_operations fops = {
.owner = THIS_MODULE,
.open = container_of_test_open,
.read = container_of_test_read,
.write = container_of_test_write,
.release = container_of_test_release,
};

static int __init container_of_test_init(void)
{
int err;
dat1 = (struct container_data *)kmalloc(sizeof(struct container_data), GFP_KERNEL);
if (dat1 == NULL) {
pr_err("no memory dat1");
err = -ENOMEM;
goto kmalloc_dat1_fail;
}
memset(dat1, 0, sizeof(struct container_data));

dat2 = (struct container_data *)kmalloc(sizeof(struct container_data), GFP_KERNEL);
if (dat2 == NULL) {
pr_err("no memory dat2");
err = -ENOMEM;
goto kmalloc_dat2_fail;
}
memset(dat2, 0, sizeof(struct container_data));

err = alloc_chrdev_region(&dat1->dev_num, 0, 2, "container_of_test chrdev region");
if (err < 0) {
pr_err("alloc_chrdev_region error\n");
goto alloc_chrdev_region_fail;
}
dat2->dev_num = MKDEV(MAJOR(dat1->dev_num), MINOR(dat1->dev_num) + 1);

cdev_init(&dat1->cdev, &fops);
err = cdev_add(&dat1->cdev, dat1->dev_num, 1);
if (err < 0) {
pr_err("cdev_add dat1 error\n");
goto cdev_add_dat1_fail;
}

cdev_init(&dat2->cdev, &fops);
err = cdev_add(&dat2->cdev, dat2->dev_num, 1);
if (err < 0) {
pr_err("cdev_add dat2 error\n");
goto cdev_add_dat2_fail;
}

class = class_create(THIS_MODULE, "chrdev");
if (IS_ERR(class)) {
err = PTR_ERR(class);
goto class_create_fail;
}
dat1->dev = device_create(class, NULL, dat1->dev_num, NULL, "container_of_test_dev%d", 0);
if (IS_ERR(dat1->dev)) {
err = PTR_ERR(dat1->dev);
goto device_create_dat1_fail;
}
dat2->dev = device_create(class, NULL, dat2->dev_num, NULL, "container_of_test_dev%d", 1);
if (IS_ERR(dat2->dev)) {
err = PTR_ERR(dat2->dev);
goto device_create_dat2_fail;
}

return 0;
device_create_dat2_fail:
device_destroy(class, dat1->dev_num);
device_create_dat1_fail:
class_destroy(class);
class_create_fail:
cdev_del(&dat2->cdev);
cdev_add_dat2_fail:
cdev_del(&dat1->cdev);
cdev_add_dat1_fail:
unregister_chrdev_region(dat1->dev_num, 2);
alloc_chrdev_region_fail:
kfree(dat2);
kmalloc_dat2_fail:
kfree(dat1);
kmalloc_dat1_fail:
return err;
}

static void __exit container_of_test_exit(void)
{
device_destroy(class, dat1->dev_num);
device_destroy(class, dat2->dev_num);
class_destroy(class);
cdev_del(&dat2->cdev);
cdev_del(&dat1->cdev);
unregister_chrdev_region(dat1->dev_num, 2);
kfree(dat2);
kfree(dat1);
pr_info("container_of_test exit\n");
}
module_init(container_of_test_init);
module_exit(container_of_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("This is a test sample for container_of");

杂项设备驱动

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

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

static struct file_operations fops = {
.owner = THIS_MODULE,
};
static struct miscdevice miscdev={
.minor=MISC_DYNAMIC_MINOR,// 动态申请次设备号
.name="miscdev",
.fops = &fops,
};

static int __init misc_device_test_init(void)
{
misc_register(&miscdev);
return 0;
}

static void __exit misc_device_test_exit(void)
{
misc_deregister(&miscdev);
}

module_init(misc_device_test_init);
module_exit(misc_device_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("This is a misc device sample");

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;
}

RK3568 点亮 LED 灯

原理图

topeet RK3568 Working LED原理图如下

topeet RK3568 Working LED原理图

可以看到LED灯接在了GPIO0_B7这个GPIO口上。当 GPIO0_B7 为高电平时,三极管 Q16导通,LED9 点亮。当 GPIO0_B7 为低电平时,三极管 Q16 截止,LED9 不亮。

查询寄存器地址

GPIO0B 的引脚复用

首先我们得设置GPIO0B的引脚复用,从PMU_GRF查找

GPIO0B复用寄存器地址偏移

搜索GPIO0B7可以看到其对应寄存器为PMU_GRF_GPIO0B_IOMUX_H的14:12位的设置

GPIOB7引脚复用

而PMU_GRF寄存器地址从下面可以看到是0xFDC20000

PMU_GRF

所以复用寄存器地址=基地址+偏移地址=0xFDC20000+0xC=0xFDC2000C。

使用 io 命令查看此寄存器的地址:

1
io -r -4 0xFDC2000C

GPIO 寄存器

由TRM中的Address Mapping可知GPIO0地址位0xFDD60000

Address Mapping

RK3568 TRM中GPIO章节介绍:

3568 TRM

可知RK3568寄存器分为:

  • 方向寄存器(DDR)

    • GPIO_SWPORT_DR_L
    • GPIO_SWPORT_DR_H
  • 数据寄存器(DR)

    • GPIO_SWPORT_DDR_L
    • GPIO_SWPORT_DDR_H
  • 外部输入寄存器(EXT_PORT)

    • GPIO_EXT_PORT

  • GPIO_SWPORT_DDR_L/H 决定 引脚是输入还是输出
  • GPIO_SWPORT_DR_L/H 决定 输出引脚时输出的电平
  • GPIO_EXT_PORT 反映 引脚当前的实际电平(只读)

数据寄存器和方向寄存器分为L和H

  • GPIOA和GPIOB地址落在L上
  • GPIOC和GPIOD地址落在H上

又:

five GPIOs's register group have five different base addresses

可知GPIO0~GPIO4这五组的寄存器是不同的。

我们需要先设置GPIO方向寄存器为输出,然后设置输出电平为高电平。

寄存器偏移地址

GPIO_EXT_PORT

GPIO 有四组 GPIO,分别是 GPIOA,GPIOB,GPIOC,GPIOD。每组又以 A0 ~ A7, B0 ~ B7, C0 ~ C7, D0 ~ D7 作为编号区分。

GPIO0B7 在L上,所以方向寄存器的偏移地址为 0x0008,数据寄存器偏移为0x0000。

GPIO0 的基地址为 0xFDD60000。因此

  • 方向寄存器的地址=基地址+偏移地址=0xFDD60000+0x0008=0xFDD60008

  • 数据寄存器的地址=基地址+偏移地址=0xFDD60000+0x0000=0xFDD60000

  • 外部输入寄存器地址=基地址+偏移地址=0xFDD60000+0x0070=0xFDD60070

方向寄存器

GPIO_SWPORT_DDR_L

可知31:16为可写位,因此GPIOB7应该是16+8+8-1=31,即bit 31设置write access

而0+8+8-1=15,即bit 15对应设置GPIO0B7的方向为input或output

数据寄存器

GPIO_SWPORT_DR_L

同理16+8+8-1=31,即bit 31设置write access

而0+8+8-1=15, 即bit 15对应设置GPIO0B7输出高电平或低电平

总结

  • 复用关系寄存器的基地址为 0xFDC20000
    • GPIO0B_IOMUX_H偏移地址为 000C ,所以要操作的地址为基地址+偏移地址=0xFDC2000C
      • GPIO0B_IOMUX_H的 bit 28 ~ bit 30 为bit write access, 0 为disable, 1为enable
      • GPIO0B_IOMUX_H的 bit 12 ~ bit 14 设置为0为GPIO0_B7,1为PWM0_M0,2为CPU_AVS
  • GPIO0 的基地址为 0xFDD60000
    • 方向寄存器地址=基地址+偏移地址=0xFDD60008
      • bit 31 对应GPIO0_B7的方向write access,0为disable,1为enable
      • bit 15 对应GPIO0_B7的方向,0为input,1为output
    • 数据寄存器地址=基地址+偏移地址=0xFDD60000
      • bit 31 对应GPIO0_B7的数据write access,0为disable,1为enable
      • bit 15 对应GPIO0_B7的数据,0为输出low,1为输出high

使用io命令测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
su root
# 复用关系寄存器
sudo io -4 -w 0xFDC2000C 0x70001000

# 验证是否写入成功,读
sudo io -4 -r 0xFDC2000C

# 设置输出
sudo io -4 -w 0xFDD60008 0x80008000

# 验证
sudo io -4 -r 0xFDD60008


# 读当前值
sudo io -4 -r 0xFDD60000

# 开灯
sudo io -4 -w 0xFDD60000 0x8000C000

# 关灯
sudo io -4 -w 0xFDD60000 0x80004000

示例

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
#include <linux/module.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/uaccess.h>

/* 复用寄存器 */
#define PMU_GRF_BASE 0xFDC20000
#define GPIO0B_IOMUX_OFFSET 0xC
#define GPIO0B_IOMUX PMU_GRF_BASE + GPIO0B_IOMUX_OFFSET

/* 数据寄存器与方向寄存器 */
#define GPIO0_BASE 0xFDD60000
#define GPIO_SWPORT_DDR_L_OFFSET 0x8
#define GPIO_SWPORT_DR_L_OFFSET 0x0
#define GPIO_EXT_PORT_OFFSET 0x70
// 方向寄存器
#define GPIO0_SWPORT_DDR_L GPIO0_BASE + GPIO_SWPORT_DDR_L_OFFSET
// 数据寄存器
#define GPIO0_SWPORT_DR_L GPIO0_BASE + GPIO_SWPORT_DR_L_OFFSET
// 外部输入寄存器,readonly
#define GPIO0_EXT_PORT GPIO0_BASE + GPIO_EXT_PORT_OFFSET

struct led_drv_data {
dev_t dev_num;
struct cdev cdev;
struct class *class;
struct device *dev;
void __iomem *gpio0b_iomux;
void __iomem *gpio0_swport_ddr_l;
void __iomem *gpio0_swport_dr_l;
void __iomem *gpio0_ext_port;
};

static struct led_drv_data *led_drv_data;

int light_up_led_open(struct inode *inode, struct file *file)
{
file->private_data = led_drv_data;
return 0;
}

ssize_t light_up_led_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
struct led_drv_data *drv_data = file->private_data;
u32 val;

if (size != sizeof(u32)) {
pr_info("read need 4 bytes\n");
return -ENOMEM;
}

val = readl(drv_data->gpio0_ext_port);
val &= 1 << 15;// gpio0_b7
val = val >> 15;

if (copy_to_user(buf, &val, sizeof(u32)) != 0) {
return -EFAULT;
}

return sizeof(u32);
}
ssize_t light_up_led_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
struct led_drv_data *drv_data = file->private_data;
u32 val;

if (size != sizeof(u32)) {
pr_err("write need 4 bytes\n");
return -ENOMEM;
}

if (copy_from_user(&val, buf, sizeof(u32)) != 0) {
return -EFAULT;
}
if (val > 0) {
// 配置为GPIO输出
val = readl(drv_data->gpio0_swport_ddr_l);
val |= 0x80008000;
writel(val, drv_data->gpio0_swport_ddr_l);
// 打开
val = readl(drv_data->gpio0_swport_dr_l);
val |= 0x80008000;
writel(val, drv_data->gpio0_swport_dr_l);
} else if (val == 0) {
// 配置为GPIO输出
val = readl(drv_data->gpio0_swport_ddr_l);
val |= 0x80008000;
writel(val, drv_data->gpio0_swport_ddr_l);
// 关闭
val = readl(drv_data->gpio0_swport_dr_l);
val |= 0x80000000;
val &= 0xffff7fff;
writel(val, drv_data->gpio0_swport_dr_l);
}

return sizeof(u32);
}

int light_up_led_release(struct inode *inode, struct file *file)
{
return 0;
}

struct file_operations fops = {
.owner = THIS_MODULE,
.open = light_up_led_open,
.read = light_up_led_read,
.write = light_up_led_write,
.release = light_up_led_release,
};

static int __init light_up_led_init(void)
{
int err;
u32 val;

led_drv_data = kzalloc(sizeof(struct led_drv_data), GFP_KERNEL);
if (led_drv_data == NULL) {
err = -ENOMEM;
goto kzalloc_fail;
}
err = alloc_chrdev_region(&led_drv_data->dev_num, 0, 1, "led chrdev region");
if (err < 0)
goto alloc_chrdev_region_fail;

cdev_init(&led_drv_data->cdev, &fops);
led_drv_data->cdev.owner = THIS_MODULE;
err = cdev_add(&led_drv_data->cdev, led_drv_data->dev_num, 1);
if (err < 0)
goto cdev_add_fail;

led_drv_data->class = class_create(THIS_MODULE, "test_led");
if (IS_ERR(led_drv_data->class)) {
err = PTR_ERR(led_drv_data->class);
goto create_class_fail;
}
led_drv_data->dev = device_create(led_drv_data->class, NULL, led_drv_data->dev_num, NULL,
"test_led%d", 0);
if (IS_ERR(led_drv_data->dev)) {
err = PTR_ERR(led_drv_data->dev);
goto device_create_fail;
}

led_drv_data->gpio0b_iomux = ioremap(GPIO0B_IOMUX, 4);
led_drv_data->gpio0_swport_ddr_l = ioremap(GPIO0_SWPORT_DDR_L, 4);
led_drv_data->gpio0_swport_dr_l = ioremap(GPIO0_SWPORT_DR_L, 4);
led_drv_data->gpio0_ext_port = ioremap(GPIO0_EXT_PORT, 4);

// 设置引脚复用为GPIO
val = readl(led_drv_data->gpio0b_iomux);
val |= 0x70000000; // write access
val &= 0xFFFF8FFF; // gpio0_b7
writel(val, led_drv_data->gpio0b_iomux);

return 0;
device_create_fail:
class_destroy(led_drv_data->class);
create_class_fail:
cdev_del(&led_drv_data->cdev);
cdev_add_fail:
unregister_chrdev_region(led_drv_data->dev_num, 1);
alloc_chrdev_region_fail:
kfree(led_drv_data);
kzalloc_fail:
return err;
}

static void __exit light_up_led_exit(void)
{
iounmap(led_drv_data->gpio0_ext_port);
iounmap(led_drv_data->gpio0b_iomux);
iounmap(led_drv_data->gpio0_swport_ddr_l);
iounmap(led_drv_data->gpio0_swport_dr_l);

device_destroy(led_drv_data->class, led_drv_data->dev_num);
class_destroy(led_drv_data->class);
cdev_del(&led_drv_data->cdev);
unregister_chrdev_region(led_drv_data->dev_num, 1);
kfree(led_drv_data);
}
module_init(light_up_led_init);
module_exit(light_up_led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("light up a led on topeet RK3568 board");

测试代码:

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
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main(int argc, char **argv)
{
int fd, err;
uint32_t val = 0;
fd = open("/dev/test_led0", O_RDWR);
if (fd < 0)
goto fail;

if (argc == 1) { // read
err = read(fd, &val, 4);
if (err < 0)
goto fail;
printf("read:0x%08x\n", val);
} else if (argc >= 2) { // write
val = atoi(argv[1]);
if (val < 0) {
perror("unsupported\n");
exit(EXIT_FAILURE);
}
err = write(fd, &val, 4);
if (err < 0)
goto fail;
printf("write val: %u success\n", val);

} else {
perror("argc error\n");
exit(EXIT_FAILURE);
}

return 0;
fail:
fprintf(stderr, "Error:%s[errno:%d]", strerror(errno), errno);
exit(EXIT_FAILURE);
}