Cover image for Linux字符设备基础

Linux字符设备基础

字数 15.6k
阅读
访客

时间轴

时间轴

2025-11-11

init

本文介绍了Linux字符设备驱动开发的基础知识,重点讲解了设备号的组成与操作。文章首先说明了主设备号和次设备号的作用,以及设备号dev_t在Linux内核中的存储格式(高12位为主设备号,低20位为次设备号)。随后详细介绍了设备号操作宏(MINORBITS、MINORMASK、MAJOR、MINOR、MKDEV)的用途,并对比了静态申请设备号函数register_chrdev_region()与动态申请设备号函数alloc_chrdev_region()的参数、返回值及适用场景,同时说明了设备号释放函数unregister_chrdev_region()的使用方法。最后,文章介绍了字符设备核心结构体cdev,包括其字段含义以及在内核中的注册与使用方式,为后续编写完整的字符设备驱动打下基础。

Linux 驱动笔记


设备号

在 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

1234567
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

123456
#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次设备号掩码,用于从 dev_t 中提取次设备号((dev) & MINORMASK
  • MAJOR表示从 dev_t 中获取主设备号,本质是将 dev_t 右移 20 位
  • MINOR表示从 dev_t 中获取次设备号,本质是取低 20 位的值
  • MKDEV用于将主设备号和次设备号组成 dev_t 类型的设备号

设备号申请函数

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

include/linux/fs.h

123
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中定义:

1234567
/* 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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
#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");

测试:

1234567891011121314151617181920212223242526272829303132
~ # 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_driver511 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

123456789101112131415161718192021222324252627282930313233343536373839
/* 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

123456789101112131415
/** * 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

1234567891011121314151617181920212223242526272829
/** * 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

12345678910111213141516
/** * 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);}
  • 作用:系统中删除一个字符设备

示例

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
#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 函数

123456789101112
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

123456789101112131415161718192021222324252627282930313233343536373839404142
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,防止模块被正在使用时卸载
openopen()打开设备时调用,一般用于初始化或统计打开次数
releaseclose()关闭设备时调用,用于释放资源
readread()用户读设备数据
writewrite()用户写数据到设备
unlocked_ioctlioctl()用户发控制命令
pollpoll()/select()实现非阻塞 I/O(epoll 机制)
mmapmmap()将物理内存映射到用户空间
llseeklseek()文件位置偏移
fasyncfcntl(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

read 步骤

  1. 防止读操作超出文件大小,并返回文件结束:
12
if (*pos >= filesize)	return 0; /* 0 means EOF */
  1. 读取的字节数不能超过文件大小。适当调整 count:
12
if (*pos + count > filesize)	count = filesize - (*pos);
  1. 找到读取的起始位置:
1
void *from = pos_to_address (*pos); /* convert pos into valid address */
  1. 将数据复制到用户空间缓冲区,失败时返回错误:
123
sent = copy_to_user(buf, from, count);if (sent)	return -EFAULT;
  1. 根据读取的字节数前移文件的当前位置,并返回复制的字节数:
123
*pos += count;return count;

write

1
ssize_t (*write)(struct file *file, const char __user *buf, size_t count, loff_t *pos);
  • 作用:把用户数据写入文件/设备。
  • 参数
    • file:文件对象
    • buf:用户缓冲区
    • count:请求传输的数据长度
    • pos:表示数据在文件中应写入的起始位置
  • 返回值
    • 成功:实际写入的字节数
    • 出错:负数 errno

write 步骤

  1. 检查来自用户空间的错误或无效请求。这个步骤只有在设备提供内存(电可擦编程只读存储器、I/O内存等)时才有意义,它可能有内存大小限制:
123
/* 如果试图写入文件末尾之外,则返回错误。这里的 filesize 对应设备内存的大小(如果有的话) */if ( *pos >= filesize ) return -EINVAL;
  1. 针对剩余字节数调整 count,以便不超出文件大小。这一步也不是必需的,与第(1)步中的相同条件相关:
123
/* 文件大小强制响应设备内存的大小 */if (*pos + count > filesize)	count = filesize - *pos;
  1. 找到开始写入的位置。只有当设备具有内存,并且write()方法要在其中写入指定的数据时,此步骤才是相关的。和第(2)步一样,这一步不是必需的:
12
/* 将pos转换为有效地址*/void *from = pos_to_address( *pos );
  1. 从用户空间复制数据,并将其写入相应的内核空间:
123456
if (copy_from_user(dev->buffer, buf, count) != 0){	retval = -EFAULT;	goto out;}/* 现在将数据从dev->buffer移动到物理设备 */
  1. 写入物理设备,在失败时返回错误:
1234
write_error = device_write(dev->buffer, count);if ( write_error )	return -EFAULT;
  1. 根据写入的字节数移动文件中光标的当前位置。最后,返回复制的字节数:
123
*pos += count;return count;

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()时分配的资源
    • 恢复设备状态(如标记为未被占用)
    • 减少模块引用计数

示例

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
#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 目录下。

12345678910111213141516
[zhaohang@cyberboy linux_driver_learning]$ ll /devcrw-------   4,15 root 11 Nov 10:05  tty15crw-------   4,16 root 11 Nov 10:05  tty16crw-------   4,17 root 11 Nov 10:05  tty17crw-------   4,18 root 11 Nov 10:05  tty18crw-------   4,19 root 11 Nov 10:05  tty19crw-------   4,20 root 11 Nov 10:05  tty20crw-------   4,21 root 11 Nov 10:05  tty21crw-------   4,22 root 11 Nov 10:05  tty22crw-------   4,23 root 11 Nov 10:05  tty23crw-------   4,24 root 11 Nov 10:05  tty24crw-------   4,25 root 11 Nov 10:05  tty25crw-------   4,26 root 11 Nov 10:05  tty26crw-------   4,27 root 11 Nov 10:05  tty27crw-------   4,28 root 11 Nov 10:05  tty28crw-------   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 的示例:

123456789101112
$ 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

12345678
/* 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 函数在类下面创建一个设备。

123
__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:指定创建设备的设备号。

示例

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
#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");

测试:

12345678910111213
$ 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()函数是将内核空间的数据拷贝到用户空间。

内核空间获取用户空间数据的常见方式:

  • 系统调用(如 read/write)
  • 软中断(socket 通信等)

include/linux/uaccess.h

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
/* * 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_USERstatic 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;}#elseextern __must_check unsigned long_copy_from_user(void *, const void __user *, unsigned long);#endif#ifdef INLINE_COPY_TO_USERstatic 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;}#elseextern __must_check unsigned long_copy_to_user(void __user *, const void *, unsigned long);#endifstatic __always_inline unsigned long __must_checkcopy_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_checkcopy_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_COMPATstatic __always_inline unsigned long __must_checkcopy_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_checkcopy_mc_to_kernel(void *dst, const void *src, size_t cnt){	memcpy(dst, src, cnt);	return 0;}#endif

整体调用链如下

123456789
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()

函数原型:

1234567
static __always_inline unsigned long __must_checkcopy_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()

1234567
static __always_inline unsigned long __must_checkcopy_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 表示成功,其他表示失败

copy_struct_from_user()

这是现代 syscall 推荐接口。

解决问题:用户空间传入结构体版本不同。

  • 如果用户传入的 struct 小,则内核自动补零
  • 如果用户传入的 struct 大,检查多出字段是否为 0

返回值

  • 0成功
  • -EFAULT访问失败
  • -E2BIG非零扩展字段
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
/** * copy_struct_from_user: copy a struct from userspace * @dst:   Destination address, in kernel space. This buffer must be @ksize *         bytes long. * @ksize: Size of @dst struct. * @src:   Source address, in userspace. * @usize: (Alleged) size of @src struct. * * Copies a struct from userspace to kernel space, in a way that guarantees * backwards-compatibility for struct syscall arguments (as long as future * struct extensions are made such that all new fields are *appended* to the * old struct, and zeroed-out new fields have the same meaning as the old * struct). * * @ksize is just sizeof(*dst), and @usize should've been passed by userspace. * The recommended usage is something like the following: * *   SYSCALL_DEFINE2(foobar, const struct foo __user *, uarg, size_t, usize) *   { *      int err; *      struct foo karg = {}; * *      if (usize > PAGE_SIZE) *        return -E2BIG; *      if (usize < FOO_SIZE_VER0) *        return -EINVAL; * *      err = copy_struct_from_user(&karg, sizeof(karg), uarg, usize); *      if (err) *        return err; * *      // ... *   } * * There are three cases to consider: *  * If @usize == @ksize, then it's copied verbatim. *  * If @usize < @ksize, then the userspace has passed an old struct to a *    newer kernel. The rest of the trailing bytes in @dst (@ksize - @usize) *    are to be zero-filled. *  * If @usize > @ksize, then the userspace has passed a new struct to an *    older kernel. The trailing bytes unknown to the kernel (@usize - @ksize) *    are checked to ensure they are zeroed, otherwise -E2BIG is returned. * * Returns (in all cases, some data may have been copied): *  * -E2BIG:  (@usize > @ksize) and there are non-zero trailing bytes in @src. *  * -EFAULT: access to userspace failed. */static __always_inline __must_check intcopy_struct_from_user(void *dst, size_t ksize, const void __user *src,		      size_t usize){	size_t size = min(ksize, usize);	size_t rest = max(ksize, usize) - size;	/* Deal with trailing bytes. */	if (usize < ksize) {		memset(dst + size, 0, rest);	} else if (usize > ksize) {		int ret = check_zeroed_user(src + size, rest);		if (ret <= 0)			return ret ?: -E2BIG;	}	/* Copy the interoperable parts of the struct. */	if (copy_from_user(dst, src, size))		return -EFAULT;	return 0;}

没有 copy_struct_to_user 这个函数

get_user()

123456789
// include/asm-generic/uaccess.h#define get_user(x, ptr)					\({								\	const void __user *__p = (ptr);				\	might_fault();						\	access_ok(__p, sizeof(*ptr)) ?		\		__get_user((x), (__typeof__(*(ptr)) __user *)__p) :\		((x) = (__typeof__(*(ptr)))0,-EFAULT);		\})

读一个简单变量,内部处理 fault。宏将用户空间的变量值复制到内核空间。注意,错误时x被设置为0。

  • x 表示存储结果的内核变量
  • ptr 是用户空间的源地址。

间接引用ptr的结果必须在没有强制类型转换的情况下可赋值给x。

成功时返回0,错误时返回-EFAULT

put_user()

12345678910
// include/asm-generic/uaccess.h#define put_user(x, ptr)					\({								\	void __user *__p = (ptr);				\	might_fault();						\	access_ok(__p, sizeof(*ptr)) ?		\		__put_user((x), ((__typeof__(*(ptr)) __user *)__p)) :	\		-EFAULT;					\})

该宏将内核空间变量值复制到用户空间。

  • x 表示要复制到用户空间的值
  • ptr 是用户空间中的目标地址。

x必须可分配给间接引用ptr的结果。换句话说,它们必须有(或指向)相同的类型。

成功时返回0,出错时返回-EFAULT

示例

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
#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");

测试

测试代码:

12345678910111213141516171819202122232425262728293031323334353637
#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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
TARGET_EXEC := testSRC_DIRS := ./BUILD_DIR := ./buildCC = aarch64-linux-gnu-gccCFLAGS = -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 deployclean:	rm -rf $(BUILD_DIR)deploy:	cp $(BUILD_DIR)/$(TARGET_EXEC) ~/tftp# 自动依赖包含-include $(OBJS:.o=.d)

测试输出:

12345678910111213141516
~ # 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 okread 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 指向设备结构体。

123456
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 访问设备结构体

1234
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 更利于管理

示例

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
#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");

测试:

123456789
$ 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 calledHello World from kernel[   15.000469] file_private_test_release is called

使用文件私有数据的场景

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

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

container_of 宏

include/linux/kernel.h

123456
#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)标准宏,计算成员在结构体内的字节偏移。

示例

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
#include <linux/module.h>#include <linux/init.h>#include <linux/cdev.h>#include <linux/slab.h>#include <linux/uaccess.h>#define KBUF_SIZE 32struct 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

1234567891011121314151617
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);
  • 函数作用:从 misc_list 中删除 miscdevice,进而起到杂项设备卸载的作用。
  • 参数含义
    • misc: 杂项设备的结构体指针

示例

123456789101112131415161718192021222324252627282930
#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 逆序释放

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

123456789101112131415161718192021222324252627282930313233343536373839
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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
/* 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

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

ERR_PTR()

把错误码转为指针

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

IS_ERR_OR_NULL()

判断是否是错误或者NULL

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

错误码

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

1234567891011121314151617181920212223242526272829303132333435363738394041
/* 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

中文翻译:

12345678910111213141516171819202122232425262728293031323334
#define EPERM 1 /* 操作不允许*/#define ENOENT 2 /* 没有这样的文件或目录*/#define ESRCH 3 /* 没有这样的进程 */#define EINTR 4 /* 中断系统调用 */#define EIO 5 /* I/O错误*/#define ENXIO 6 /* 没有这样的设备或地址 */#define E2BIG 7 /* 参数列表太长 */#define ENOEXEC 8 /* Exec格式错误 */#define EBADF 9 /* 错误的文件数量*/#define ECHILD 10 /* 没有子进程 */#define EAGAIN 11 /* 再试一次 */#define ENOMEM 12 /* 内存不足 */#define EACCES 13 /* 没有权限 */#define EFAULT 14 /* 错误的地址 */#define ENOTBLK 15 /* 块设备要求*/#define EBUSY 16 /* 设备或资源忙 */#define EEXIST 17 /* 文件已存在 */#define EXDEV 18 /* 跨设备的链接 */#define ENODEV 19 /* 没有这样的设备 */#define ENOTDIR 20 /* 不是目录 */#define EISDIR 21 /* 是目录 */#define EINVAL 22 /* 无效参数 */#define ENFILE 23 /* 文件表溢出*/#define EMFILE 24 /* 打开的文件太多 */#define ENOTTY 25 /* 不是打字机 */#define ETXTBSY 26 /* 文本文件忙 */#define EFBIG 27 /* 文件太大 */#define ENOSPC 28 /* 设备上没有空间了 */#define ESPIPE 29 /* 非法寻求 */#define EROFS 30 /* 只读文件系统 */#define EMLINK 31 /* 链接太多 */#define EPIPE 32 /* 破坏的管道 */#define EDOM 33 /* 函数域外的数学参数 */#define ERANGE 34 /* 数学结果无法表示 */

示例:

1234
if (IS_ERR(dev1.class)) {    ret = PTR_ERR(dev1.class);    goto fail_class_create;}

错误有时会跨越内核空间,传播到用户空间。如果返回的错误是对系统调用(open、read、ioctl、mmap)的响应,则该值将自动赋给用户空间errno全局变量,在该变量上调用strerror(errno)可以将错误转换为可读字符串:

123456
#include <errno.h> /* 访问errno全局变量 */#include <string.h>        if(wite(fd, buf, 1) < 0) {                printf("something gone wrong! %s\n", strerror(errno));        }

内核编码风格中:

  • 如果函数名称是动作或命令式命令,则函数返回的错误代码应该是整数;
  • 如果函数名称是一个谓词,则该函数应返回布尔值succeeded(成功的)。

例如,add_work是一个命令,add_work() 函数返回0表示成功,返回-EBUSY表示失败。

同样,PCI device present是谓词,pci_dev_present() 函数如果成功找到匹配设备,则返回 1;否则返回0。

RK3568 点亮 LED 灯

原理图

topeet RK3568 Working 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复用寄存器地址偏移
GPIO0B复用寄存器地址偏移

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

GPIOB7引脚复用
GPIOB7引脚复用

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

PMU_GRF
PMU_GRF

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

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

1
io -r -4 0xFDC2000C

GPIO 寄存器

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

Address Mapping
Address Mapping

RK3568 TRM中GPIO章节介绍:

3568 TRM
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
five GPIOs's register group have five different base addresses

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

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

寄存器偏移地址
寄存器偏移地址

GPIO_EXT_PORT
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
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
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命令测试

12345678910111213141516171819202122
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

示例

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
#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_OFFSETstruct 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");

测试代码:

123456789101112131415161718192021222324252627282930313233343536373839404142
#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);}
评论加载中…