[深入理解linux内核]-Linux字符设备驱动
系列 | 内容 |
---|---|
深入理解linux内核 | Linux字符设备驱动 |
环境:
平台 | 内核版本 | 安卓版本 |
---|---|---|
RK3399 | Linux4.4 | Android7.1 |
1、内核模块开发
1.1、内核模块
内核模块和应用程序有诸多不同之处。内核模块先得注册自己以便在内核中占得一席之地,然后等待请求服务,这部分在初始化函数完成,模块最后还必须退出,而不像应用进程那样随时可以终止而不去处理清理工作。但是内核模块退出函数中,务必小心翼翼地撤销恢复初始化所做的一切工作。否则系统复位之前一直将残存遗留在系统中。模块功能的最优秀的特点可能是其卸载功能,这样一来不必每次都经历重启机器的漫长周期,可以缩短内核开发周期。
1.2、加载和卸载模块
模块建立完成,然后该加载到内核中,insmod
实现模块的加载,完成模块代码和数据到内核的加载。加载驱动模块时可以指定模块参数。内核头文件的重要性不言而喻。对内核模块而言有几个必须的头文件,几乎所有的模块中都会用到。
文件名 | 内容 |
---|---|
linux/module.h |
头文件里有大量加载模块所必需的函数和符号 |
linux/init.h |
头文件用来指定初始化及清理函数 |
moudleparam.h |
头文件包含模块参数问题 |
我们知道Linux
是支持GPL
等通常的公开许可证的,所以自己的模块中也应该指定它,当然不是强制要求。为此应包含如下行:
MODULE_LICENSE(”GPL”);
模块中可能还有一些描述性质的定义:
定义 | 内容 |
---|---|
MODULE_AUTHOR |
声明模块作者 |
MODULE_DESCRIPION |
模块功能简要声明 |
MODULE_VERSION |
版本信息 |
lsmod
可获知当前装载到内核中的模块信息,rmmod
工具移除内核模块。
1.3、模块初始化和退出
模块初始化函数注册模块提供的任何功能。实际初始化函数定义模式常常如下:
static int __init initfunc(void)
{
//Initialization——Handle
)
module_init(init_func)
- 1
- 2
- 3
- 4
- 5
初始化函数应当被声明为static
,除此文件之外不可见,无意义,故通常这样声明。__init
只是个标记,表示仅仅在初始化才使用。moudle init
却是必备强制要求的。这个宏定义增加了一个特殊的段到模块目标代码,是为了标记初始化函数位置所在。没有这个宏,就不会调用初始化函数。
清理函数也是模块的必备部分,它通常用于注销接口,在模块被移除的时候调用。目的是返回所占用资源给系统。该函数定义如下:
static void __exit cleanup_function(void)
{
/*Cleanup*/
)
module_exit(cleanup_function);
- 1
- 2
- 3
- 4
- 5
声明为void
是因为清理函数不需要返回值。 exit
标记类似于init
,也只用于模块卸载之时。最后moudle exit
也是为了使得内核能够找到清理函数。若无清理函数,则内核不会允许模块被卸载。
1.4、初始化中的错误处理
注册初始化函数到内核过程中,必须始终检查返回值是否成功。若失败则必要的措施必须被施展处理。果然注册过程发生错误了,则必须撤销掉任何在失败之前所注册的动作。内核不管这些注册信息。因此如果发生任何错误,模块必须能自己按照相反注册顺序回滚撤销所有先前注册的东西。否则,由于有一些未取消的指针等存在,内核就置身于一个不稳定、不安全的状态,万不得已之下,经常地,唯一方法就是重启系统,因此关于初始化的错误处理一点也不能马虎。
2、主次编号
Linux
字符设备以文件的形式呈现在文件系统中,这样的特殊文件可以通过文件名字进行存取操作,大大的透明化了其底层的差异。这些文件也称之为设备文件。惯例上均位于/dev
目录,可通过ls
命令查看。通常用字符c
来标示字符设备文件。
惯例而用,主编号标识某设备相连的对应驱动程序。例如4表示虚拟控制台和串口终端。而次设备编号则由内核去分配管理。它来决定内核确定文件所指设备。此设备号的作用,仅仅是标示了该驱动程序所关联的设备。
2.1、设备编号的内部表示
内核在<linux/types.h>
中定义的dev_t
类型用来表示持有设备的编号包括主次设备编号,主次部分都包括。旧版本的内核中,该数据类型仅仅表示256
个主设备编号和256
个次设备编号,虽然一直到现在人们认为这个范围足够用,但是为了未来的某种可能的假设变化,新的内核版本重新扩充了dev_t
所能表示的范围。dev_t
现在是32
位,12
位用作主设备编号,剩余20
位用作次设备编号。当然也许可能还会有相关变化,最好且应该是不直接使用,而应该遵从<linux/kdev_t.h>
中定义的宏。获得主次编号应使用:
MAJOR(dev_t dev);
MINOR(dev_t dev);
反之,知道主次设备编号,也可以转换为dev_t
类型:
MKDEV(int major, int minor);
如此一来数据的规格变化无论以后怎么变化都被屏蔽了。
2.2、分配和释放设备编号
字符驱动程序编写之始,首先得获取一个或多个设备编号来使用。旧的方式是采用手动注册方式,前提是明确知道自己所需的可用编号范围。使用如下在<linux/fs.h>
中声明的函数:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
参数 | 内容 |
---|---|
first |
请求分配起始设备编号,次设备编号通常为0 |
count |
是连续请求的编号个数,count 不能过大而导致溢出到下一主设备编号 |
name |
设备名称,注册成功后可在/proc/devices 中查获而知。 |
然而问题在于,我们常常很难知道可用的主设备号,因此新的策略就被Linux
内核开发社区提出来,产生了动态分配设备编号的机制。由内核动态分配可用的一个主设备编号:
int allocchrdevregion(devt *dev, unsigned int firstminor, unsigned int count, char *name);
参数 | 内容 |
---|---|
dev |
用来保存成功分配所得第一个编号 |
firstminor |
第一个请求的次设备号,常为0 |
count |
连续请求的编号个数 |
name |
名称 |
无论何种方式请求分配设备编号,不使用后均使用下面函数释放掉:
void unregister_chrdev_region(dev_t first,unsigned int count);
调用此函数通常在模块的cleanup
函数中。
3、重要数据结构
注册设备编号仅仅只是编写驱动开始的第一步而已,还有诸多驱动组件需要涉及考虑。但是,不论如何,绝大多数驱动程序都会涉及3
个极为重要的内核数据结构,即为file_operations
、file
和inode
结构。
3.1、文件操作
设备编号是标示驱动程序和设备的,那么注册得到编号后,下一步工作是考虑如何设备编号关联到对设备的操作管理。那么在<linux/fs.h>
中定义的file_operation
结构就是建立此连接的纽带。
此结构中定义包含了一组函数指针,是对设备对象的操作方法。也就是针对设备文件的系统调用。将file_opermion
或指向此结构的指针惯称为fops
,那么每个打开的设备文件(内核中有个file
结构表示文件,容后介绍)和这一组操作关联。这些fops
所指的每个字段就是驱动程序所要实现的动作。若对其中某些操作不予支持,可以置为NULL
。
当观察file_operations
定义支持的多种方法时,会注意到不少参数中有__user
标记,表示该被修饰的指针是个用户空间指针,故不可直接使用。下面简单举例说明设备上需要实现的操作函数:
成员 | 内容 |
---|---|
struct module *owner |
这个成员只是指向拥有该结构模块的指针,表明为谁所有。通常被初始化为<linux/module.h> 中定义的宏THIS MODULE |
ssize_t(*read)(struct file*,char __user*,size_t,loft_t*) |
这个成员是实现设备的读方法的操作 |
ssize_t(*write)(struct file*,const char __user*,size_t,loft_t*) |
这个成员实现设备的写方法的操作 |
int(*open)(struct inode*,struct file*) |
虽然这是通常作为第一个操作而被设备驱动执行,却不要求一定声明实现 |
该方法。 | |
int(*ioctl)(struct inode*,struct file*,unsigned int,unsigned long) |
可以对设备执行特定命令的方法 |
int(*release)(struct inode*,struct file*); |
文件结构被释放时引用该操作 |
3.2、文件结构
第二个驱动中的重要数据结构是定义于<linux/fs.h>
中的struct file
结构。
文件结构是内核在open
之时创建用来代表一个打开的任意文件。该结构由内核传递给前述定义的文件操作函数,这样一来,打开的内核中表示的文件就与文件操作可以关联起来。文件不使用后关闭时,内核才释放file结构。同样内核源码中我们将struct file
指针惯称之为filp
(”file pointer”
)。
下面简单举例说明struct file
比较重要的成员:
成员 | 内容 |
---|---|
mode_t f_mode |
表示文件模式。如用FMODE_READ 标示文件可读,而FMODE_WRITE 标示可写 |
unsigned int f_flags |
表示文件标志,如常见的O_RDONLY 、O_NONBLOCK 等标记 |
struct file_operations *f_op |
表示和文件关联的操作 |
void *private_data |
可用来保存私有状态信息,在跨系统调用的使用中非常有用。 |
3.3、inode 结构
inode
结构是内核中唯一表示文件结点的结构,而file
只是内核中表示打开的文件结构。二者含义不可混淆。既然文件可以被打开多次,那么就会有多个file
结构,但是唯一标示的文件结点inode
却始终只有一个。
虽然inode
结构包含了很多文件信息,但是通常只有两个字段在驱动编程中有用武之地。一个是dev_t i_rdev
,这表示设备文件的结点,其中有设备编号信息。另外一个是struct cdev *i_cdev
,表示了字符设备的内部表示。
i_rdev
类型后来发生了改变,使得大量驱动程序被破坏。为防止这类情况,增加了可移植性的宏编码方式从inode中获取主次设备编号:
unsigned int iminor(struct inode*inode);
//获取次设备编号
unsigned int imajor(struct inode*inode);
//获取主设备编号
4、字符驱动
4.1、字符设备注册
前述部分,提到字符设备在内核中是由struct cdev
来表示的。故而在调用设备操作函数前,注册分配一个或几个这样的结构是必须的。该结构及相关辅助函数定义在<linux/cdev.h>
中。
旧版的注册字符设备驱动的方式是:
int register_chrdev(unsigned int major,const char *name,struct file_operations *fops);
系统中移除设备的函数接口是:
int unregister_chrdev(unsigned int major,const char *name)
4.2、读和写
那么核心的问题就是如何解决用户缓存区和内核的数据安全交互。这也是这个函数的核心功能。内核提供了类似memcopy
功能的函数,来实现跨越内核和用户的数据传递。
unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);
unsigned long copy_from_user(void *to,const void_user *from,unsigned long count);
这2
个函数除了实现数据copy
传递功能外,还对__user
用户指针进行安全检查。若为无效指针,就不会进行copy
操作。
实际设备的read
方法就是使用copy_to_user
从内核设备中读数据到用户空间。
而write
方法使用copy_from_user
实现相反的功能。
4.3、ioctl接口
绝大多数设备驱动有着对硬件控制特殊操作能力。而ioctl
方法往往是最容易最直接的选择。该方法实现设备文件用户空间的ioctl
系统调用。
ioctl
方法驱动实现内核原型:
int(*ioctl)(struct inode *node,struct file *flip,unsigned int cmd,unsigned long arg)
inode
和flip
指针对应于ioctl
系统调用中的file
描述符fd
。而cmd
参数对应于系统调用的命令参数,由用户传递进来。而不论系统调用中可选参数部分是指针还是长整型。内核中的实现均使用unsigned long
类型来表示。其实由于多种命令选择,可以想象,实现ioctl
可能需要选择判断cmd
参数的switch
结构。
通常可以在头文件中预定义命令编号的方式去实现。
首先应该考虑的是ioctl命令编号在系统中应当是唯一存在的。因为可能发生访问错误设备却使用正确命令的情况。为避免这种错误,Linux
中将这些编码规范划分了位段。旧的使用16
位整数,高8位是关联这个设备的”魔幻”数而低8
位是一个序号,在设备内唯一。使用这种老传统的驱动程序仍非常之多。现在有了新的划分,选择编号可以先参考include/asm/octl.h
和Documentation
下的ioctl-number.txt
文件。新的位段有四种字段:type
(即魔幻数)、number
(序号)、direction
(传送方向)和size
(数据大小)。
可以使用定义在<linux/ioctl.h>
中的宏来帮助建立命令编号:
命令 | 内容 |
---|---|
_IO(type, nr) |
用于构建无参数命令编号 |
_IOR(type, nr, datatype) |
用于构建从驱动中读数据的命令编号 |
_IOW(type, nr,datatype) |
用于构建向驱动中写数据的命令编号 |
_IOWR(type, nr, datatype) |
用于双向传送 |
参考头文件中有关这些宏的细节。
关于ioctl
的实现,也涉及到参数在用户空间和内核的交互传递。有一组定义在<asrn/uaccess.h>
中的函数实现特意为数据大小为1
、2
、4
和8
字节进行拷贝传递。不使用copy_to_user
等函数是因为它们传输单数据更加快速方便。
put_user(datum,ptr)
传递依赖于sizeof(ptr)
大小的datum
到用户空间。
get_user(local,ptr)
这个宏定义用来从用户空间接收单个数据并存储于变量local
。
文章来源: xuesong.blog.csdn.net,作者:内核笔记,版权归原作者所有,如需转载,请联系作者。
原文链接:xuesong.blog.csdn.net/article/details/104092307
- 点赞
- 收藏
- 关注作者
评论(0)