【linux】驱动-3-字符设备驱动
[toc]
前言
- 以野火i.M 6U为例
3. 字符设备驱动
需要明确的是模块和驱动是两回事。 本笔记开始记录驱动的相关知识。
3.1 Linux设备分类
Linux设备可分为三:字符设备、块设备和网络设备。
网络设备:是一种特殊设备,它并不存在于/dev下面,主要用于网络数据的收发。
Linux系统将设备分别抽象为 struct cdev, struct block_device,struct net_devce 三个对象,具体的设备都可以包含着三种对象从而继承和三种对象属性和操作, 并通过各自的对象添加到相应的驱动模型中,从而进行统一的管理和操作。
3.2 设备相关概念
3.2.1 设备号
设备号分 主设备号 和 次设备号。 主设备号区别设备类型。 次设备号表示具体的设备。、 设备号用 dev_t 来表示,dev_t 是32 bit的数,其中高 12 bit 表示主设备号,低 20 bit 表示次设备号。 但是需要注意的是,在内核源码中会设定一个宏 CHRDEV_MAJOR_MAX,主设备号限定在 0-CHRDEV_MAJOR_MAX 中。
三个函数来处理设备号:
- MAJOR(dev):使用该函数来获得设备号中的主设备号。
- MINOR(dev):使用该函数来获得设备号中的次设备号。
- MKDEV(ma,mi):使用该函数来获得设备号中的设备号。
3.2.2 设备节点
设备节点(设备文件):Linux设备节点是通过 mknod
命令来创建的;也可以通过代码函数来创建(如:device_creat)。
一般的数据文件称为普通文件,而设备节点的文件称为设备文件。
设备节点被创建在 /dev 路径下,通过设备节点就可以操作对应的设备了。
3.2.3 APP open 文件理解 **
APP 每打开一个文件时,都会获得一个文件句柄(一个整数),每一个句柄在内核中都有与之对应的 struct file(打开文件时创建的)。 APP 关闭一个文件时,句柄对应的 struct file 会被内核释放,句柄值也会交回系统。
同样的道理,APP 打开设备节点,即是设备文件时,内核也会创建一个 struct file 的数据结构。 但是需要注意的是该结构体里面的 struct file_operations *f_op 是由驱动程序提供的。
内核使用 inode 结构体在内核内部表示一个文件,
3.3 数据结构
主要介绍几个数据结构:struct file、sruct file_operations、struct inode。
3.3.1 struct file
看节点 《3.2.3 APP open 文件理解》 即可 struct file 源码在 内核源码/include/linux 下 其中包含 sruct file_operations ,就是让 APP 能够操作该文件。
struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; /* ¦* Protects f_ep_links, f_flags. ¦* Must not be taken from IRQ context. ¦*/ spinlock_t f_lock; enum rw_hint f_write_hint; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock; loff_t f_pos; struct fown_struct f_owner; const struct cred *f_cred; struct file_ra_state f_ra; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif /* needed for tty driver, and maybe others */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct list_head f_ep_links; struct list_head f_tfile_llink; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; } __randomize_layout
3.3.2 struct file_operations
file_operations 就是把系统调用和驱动程序关联起来,提供文件操作函数(在这里即是驱动函数)。 在编写驱动程序的时候,创建一个该类型的结构体,然后编写部分数,填充该结构体部分内容即可。 struct file 源码在 内核源码/include/linux 下
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 (*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); int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,u64); int (*fadvise)(struct file *, loff_t, loff_t, int); } __randomize_layout;
在编写 read 和 write 函数时需要用到两个函数来 copy 数据的:
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
:用于 write 函数时 copy 来自APP的数据。(即是 APP-->内核)static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
:用于 read 函数时 copy 数据到APP。(即是 内核-->APP)
(后源:一般都是前面目的地址,后面是源地址)
3.3.2 struct inode
内核使用 inode 结构体在内核内部表示一个文件。 与文件描述符的结构体(struct file)不同。
可以使用多个文件结构体表示同一个文件的多个描述符,但是都得指向同一个 inode 结构体,因为同一个文件,对应同一个 inode 结构体。
inode 结构体 包含很多文件相关的信息,但是对于字符设备文件,只需要关心两个域即可:
- dev_t i_rdev:设备文件的结点,包含了设备号。
- struct cdev *i_cdev:struct cdev 是内核的一个内部结构,它是用来表示字符设备的,当 inode 结点 指向一个字符设备文件时,此域为指向inode结构体。
3.4 字符设备驱动程序框架 **
字符设备驱动程序框架(框架是一样的,只是这里介绍新函数):
- 实现设备驱动:填充 struct operations 结构体部分内容。
- 驱动初始化
-
分配设备编号(dev_t):
静态分配:
register_chrdev_region()
。 - 动态分配:
alloc_chrdev_region()
。 - 初始化 cdev:创建并初始化一个 cdev 结构体:
cdev_init()
。 - 注册 cdev:向内核注册一个 cdev 结构体:
cdev_add()
。 - 新建一个设备节点:建立设备节点,绑定设备号和 cdev。
- 驱动注销:
-
释放 cdev:
cdev_del()
。 - 归还申请的主设备号:
unregister_chrdev_region()
。
3.4.1 实现设备驱动
实现设备驱动其实就是填充 struct operations 结构体部分内容,并关联到主设备号。
3.4.2 驱动初始化和注销
3.4.2.1 设备号的申请和归还
register_chrdev_region()
:
- 该函数用于静态申请一个或多个设备号。
- 注:使用该函数时最好去内核源码的 Documentation/devices.txt 查看一下使用规则。
- 函数原型:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
: from:dev_t类型的变量,用于指定字符设备的起始设备号,如果要注册的设备号已经被其他的设备注册了,那么就会导致注册失败。 - count:指定要申请的设备号个数,count的值不可以太大,否则会与下一个主设备号重叠。
- name:用于指定该设备的名称,我们可以在/proc/devices中看到该设备。
- 成功返回 0;失败返回错误码。
alloc_chrdev_region()
:
- 该函数用于动态申请设备号。
- 函数原型:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
: dev:dev_t类型的指针变量,用于存放分配到的设备编号的起始值。 - baseminor:次设备号的起始值,通常情况下为 0。
- count:次设备号的个数。
- name:用于指定该设备的名称,我们可以在/proc/devices中看到该设备。
- 成功返回 0;失败返回错误码。
unregister_chrdev_region()
:
- 该函数用于注销设备号,归还给内核。
- 函数原型:
void unregister_chrdev_region(dev_t from, unsigned count)
: from:指定需要注销的字符设备的设备编号起始值,我们一般将定义的dev_t变量作为实参。 - count:指定需要注销的字符设备编号的个数,该值应与申请函数的count值相等,通常采用宏定义进行管理。
- 成功返回 0;失败返回错误码。
以上三个函数都属于新的函数,当然也就旧版的,目前依旧兼容,主要区别是:
- 旧版的申请了主设备号后,跟着的256个次设备号会全部被注册,导致资源浪费。
register_chrdev()
包含了cdev_init()
和cdev_add()
,而新版没有。**
register_chrdev()
:
- 该函数用于申请设备号。该函数包含了
cdev_init()
和cdev_add()
。 - 函数原型:
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); }
- major:用于指定主设备号,若为0,则动态分配。
- name:字符设备的名称。
- fops:字符设备驱动,即是struct file_operations。
- 返回主设备号。
unregister_chrdev()
:
- 该函数用于注销设备号,归还给内核。该函数包含了
cdev_del()
。 - 函数原型:
static inline void unregister_chrdev(unsigned int major, const char *name) { __unregister_chrdev(major, 0, 256, name); }
- major:用于指定主设备号。
- name:字符设备的名称。
- 无返回。
3.4.2.2 初始化 cdev
cdev 在内核,表示字符设备文件。(相当于其它文件的 inode)
初始化 cdev 使用函数 void cdev_init(struct cdev *cdev, const struct file_operations *fops)
:
- cdev:struct cdev 类型的指针变量,指向需要关联的字符设备结构体;
- fops:file_operations 类型的结构体指针变量,一般将实现操作该设备的结构体 file_operations 结构体作为实参。
3.4.3 设备的注册和注销(设备节点)
注册和注销设备都有两种方法:
- 代码
- 命令
设备的注册(代码):
cdev_add()
函数用于向内核的cdev_map散列表添加一个新的字符设备。- 函数原型:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
: p:struct cdev类型的指针,用于指定需要添加的字符设备; - dev:dev_t类型变量,用于指定设备的起始编号;
- count:指定注册多少个设备。
设备的注销(代码):
函数原型:cdev_del()
用于注销设备。void cdev_del(struct cdev *p)
: p:struct cdev类型的指针,用于指定需要删除的字符设备。
注:从系统中删除cdev,cdev设备将无法再打开,但任何已经打开的cdev将保持不变, 即使在cdev_del返回后,它们的FOP仍然可以调用。因为 struct file 还在。
设备的注册(命令):
方法:mknod 设备名 设备类型 主设备号 次设备号
- 类型: P:创建先进先出(FIFO)特殊文件。可不指定主设备号和次设备号(其它类型必须指定主次设备号);
- b:创建(有缓冲的)区块特殊文件;
- c:创建(没有缓冲区)字符特殊文件;
- u:创建(没有缓冲区)字符特殊文件。
3.5 实战例程
3.6 字符设备驱动框架总结
字符设备驱动程序框架(框架是一样的,只是这里介绍新函数):
- 实现设备驱动:填充 struct operations 结构体部分内容。
- 驱动初始化
-
分配设备编号(dev_t):
静态分配:
register_chrdev_region()
。 - 动态分配:
alloc_chrdev_region()
。 - 初始化 cdev:创建并初始化一个 cdev 结构体:
cdev_init()
。 - 注册 cdev:向内核注册一个 cdev 结构体:
cdev_add()
。 - 新建一个设备节点:建立设备节点,绑定设备号和 cdev。
- 驱动注销:
-
释放 cdev:
cdev_del()
。 - 归还申请的主设备号:
unregister_chrdev_region()
。
驱动模块实现分点实现步骤:
- 模块的实现: 实现入口函数;
- 实现出口函数;
- 标注协议等信息。
-
实现驱动内容,struct operations 结构体内容;
-
创建一个设备类;
-
删除设备节点;
参考
- Linux驱动学习——简单字符设备
- 手把手教你学linux驱动开发”OK6410系列之03---LED字符设备驱动
- LINUX—字符设备驱动之-globalmem
- linux字符设备驱动驱动流程
- Linux 26 字符设备驱动一
- Linux 字符设备驱动开发基础(六)—— VFS 虚拟文件系统解析
- Linux 字符设备驱动结构(二)—— 自动创建设备节点
- Linux字符设备驱动之异步通知
- Linux 设备驱动之字符设备(一)
- Linux字符设备驱动之LED驱动
- linux字符设备驱动的 ioctl 幻数
- linux 设备驱动笔记 - 字符设备驱动
- Linux驱动学习(四)——高级字符设备驱动程序
- Linux 字符设备驱动开发基础(三)—— read()、write() 相关函数解析
- linux2.6字符设备驱动开发模板
- Linux字符驱动中动态分配设备号与动态生成设备节点
- linux分类驱动对字符设备框架压力的卸载
- Linux(2.6.35.7)字符设备驱动注册接口
- linux驱动开发(五)--内核注册字符设备的新接口
- linux驱动设备--------字符设备的注册