[深入理解linux内核]-Linux字符设备驱动

举报
内核笔记 发表于 2021/06/08 23:43:36 2021/06/08
【摘要】 系列内容深入理解linux内核Linux字符设备驱动 环境: 平台内核版本安卓版本RK3399Linux4.4Android7.1 文章目录 1、内核模块开发1.1、内核模块1.2、加载和卸载模块1.3、模块初始化和退出1.4、初始化中的错误处理 2、主次编号2.1、设备编号的内部表示2.2、分配和释放设备编号 3、重要数据结构3.1、文件操作3.2、文件结构...
系列 内容
深入理解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_operationsfileinode结构。

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_RDONLYO_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)

inodeflip指针对应于ioctl系统调用中的file描述符fd。而cmd参数对应于系统调用的命令参数,由用户传递进来。而不论系统调用中可选参数部分是指针还是长整型。内核中的实现均使用unsigned long类型来表示。其实由于多种命令选择,可以想象,实现ioctl可能需要选择判断cmd参数的switch结构。
通常可以在头文件中预定义命令编号的方式去实现。

首先应该考虑的是ioctl命令编号在系统中应当是唯一存在的。因为可能发生访问错误设备却使用正确命令的情况。为避免这种错误,Linux中将这些编码规范划分了位段。旧的使用16位整数,高8位是关联这个设备的”魔幻”数而低8位是一个序号,在设备内唯一。使用这种老传统的驱动程序仍非常之多。现在有了新的划分,选择编号可以先参考include/asm/octl.hDocumentation下的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>中的函数实现特意为数据大小为1248字节进行拷贝传递。不使用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

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

举报
请填写举报理由
0/200