LV10-02-字符设备驱动-02-基本驱动函数实现
本文主要是字符设备驱动基本函数的实现的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
Linux开发板 | 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2 |
u-boot | 2013.01 |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
文件 | 下载链接 |
--- | --- |
前边那些笔记其实已经可以完成一个最基本的字符设备驱动框架了,但是我们还缺少自己编写的驱动函数,这一部分主要介绍几个重要驱动函数的实现。
一、两个常用结构体
1. struct inode
该结构体是内核中记录文件元信息的结构体,它定义在linux
源码的include/linux/fs.h
中,这里只列我们可能会关注的成员:
1 | struct inode |
- 内核中每个
struct inode
对象对应着一个实际文件,一对一。 open()
打开一个文件时,如果内核中该文件对应的inode
对象已存在则不再创建,不存在才创建。- 内核中用此类型对象关联到对此文件的操作函数集(对设备而言就是关联到具体驱动代码)。
2. struct file
该结构体是读写文件内容过程中用到的一些控制性数据组合而成的对象,可以称之为文件操作引擎或者叫文件操控器,它定义在linux
源码的include/linux/fs.h
中,这里只列我们可能会关注的成员:
1 | struct file |
open
函数被调用成功一次,则创建一个该对象,因此可以认为一个该类型的对象对应一次指定文件的操作。open
同一个文件多次,每次open
都会创建一个该类型的对象。- 文件描述符数组中存放的地址指向该类型的对象。
- 每个文件描述符都对应一个
struct file
对象的地址。
该结构体存在于设备的操作函数集的每个函数参数中,我们会经常使用该结构体的private_data
成员来存放我们的设备的私有数据,以完成参数的传递。
二、几个函数
1. copy_to_user()
1.1 函数说明
我们使用以下命令查询一下函数所在头文件:
1 | grep copy_to_user -r -n ~/5linux/linux-3.14/include |
经过查找,会发现该函数定义在linux
源码的include/asm-generic/uaccess.h
目录中,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于内核空间的数据复制到用户空间。
【函数参数】
to
:void __user *
类型,目标地址(用户空间)。from
:void *
类型,源地址(内核空间)。n
:unsigned long
类型,要拷贝数据的字节数。
【返回值】long
类型,如果复制成功,返回值为 0
,如果复制失败则返回负数 。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)该函数定义在linux
源码的include/asm-generic/uaccess.h
中,但是在包含头文件的时候注意使用asm/uaccess.h
,这个吧,我也不清楚是为什么,这样编译的时候使用make ARCH=arm
不报错,但是直接编译x86
平台的模块的话就会报错,可能是内核版本问题,我们是要在ARM
平台上跑驱动,所以就按照asm/uaccess.h
就可以啦。
1.2 使用实例
暂无
2. copy_from_user()
2.1 函数说明
我们使用以下命令查询一下函数所在头文件:
1 | grep copy_from_user -r -n ~/5linux/linux-3.14/include |
经过查找,会发现该函数定义在linux
源码的include/asm-generic/uaccess.h
目录中,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于将用户空间的数据拷贝到内核空间。
【函数参数】
to
:void *
类型,目标地址(内核空间)。from
:void __user *
类型,源地址(用户空间)。n
:unsigned long
类型,要拷贝数据的字节数。
【返回值】long
类型,如果复制成功,返回值为 0
,如果复制失败则返回负数 。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)该函数定义在linux
源码的include/asm-generic/uaccess.h
中,但是在包含头文件的时候注意使用asm/uaccess.h
,这个吧,我也不清楚是为什么,这样编译的时候使用make ARCH=arm
不报错,但是直接编译x86
平台的模块的话就会报错,可能是内核版本问题,我们是要在ARM
平台上跑驱动,所以就按照asm/uaccess.h
就可以啦。
2.2 使用实例
暂无
3. container_of()
3.1 函数说明
我们使用以下命令查询一下函数所在头文件:
1 | grep "#define container_of" -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于通过结构体某个成员的首地址获取整个结构体变量的首地址,该函数其实是一个宏,在网上看到它被称之为内核第一宏,也不知真假😂,关于原理,这里我就不写了,可以看这里,我自己觉得说的挺清晰的Linux内核宏Container_Of的详细解释_Linux_脚本之家 (jb51.net) 。
【函数参数】
ptr
:结构体内某成员member
的地址。type
:要求首地址的结构体的类型。member
:结构体内的某成员名。
【返回值】目前只知道返回的是要求的结构体的首地址,我们一般会用强制类型转换转换为我们需要的地址类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.2 使用实例
暂无
三、基本驱动函数实现
1. 打开与关闭设备
1.1 myopen()
当我们使用字符设备的时候首先肯定需要打开设备文件,当我们的应用程序调用open()
函数的时候,相应的驱动中的myopen()
便会被调用。我们自己编写的myopen()
函数如下:
1 | /** |
1.2 myclose()
该驱动函数对应应用程序中的close()
函数,我们自己编写的myclose()
函数如下:
1 | /** |
2. 读写设备操作
2.1 myread()
该驱动函数对应应用程序中的open()
函数。
1 | /** |
2.2 mywrite()
该驱动函数对应应用程序中的write()
函数。
1 | /** |
3. 设备控制操作
3.1 myioctl()
该驱动函数对应应用程序中的ioctl()
函数。
1 | /** |
1 |
|
【注意事项】arg
可以用于进行参数的传递,可以进行强制类型转换,以表示不同类型的数据,并不单纯的就是unsigned long
。
3.2 cmd
3.2.1 命令格式
在linux
中这样定义一个命令:
dir
(direction
):ioctl
命令访问模式(属性数据传输方向),占据2bit
,可以为_IOC_NONE
、_IOC_READ
、_IOC_WRITE
、_IOC_READ | _IOC_WRITE
,分别指示了四种访问模式:无数据、读数据、写数据、读写数据。type
(device type
):设备类型,占据8 bit
,在一些文献中翻译为 “幻数” 或者 “魔数”(也就是说来源没有依据),可以为任意char
型字符,例如'a'
、'b'
、'c'
等等,其主要作用是使ioctl
命令有唯一的设备标识。nr
(number
):命令编号或者叫序数,占据8 bit
,可以为任意unsigned char
型数据,取值范围0~255
,如果定义了多个ioctl
命令,通常从0
开始编号递增。size
:涉及到ioctl
函数 第三个参数arg
,占据13bit
或者14bit
(体系相关,arm
架构一般为14
位),指定了arg
的数据类型及长度,如果在驱动的ioctl
实现中不检查,通常可以忽略该参数。
3.2.2 相关函数
linux
内核为我们提供了几个宏来帮助我们定义命令,这些宏定义在linux
源码的这个文件中:
1 | include/uapi/asm-generic/ioctl.h |
我们可以使用以下命令看来查询定义这些宏的头文件:
1 | grep "#define _IO" -r -n ~/5linux/linux-3.14/include |
会发现这些宏定义都出现在include/uapi/asm-generic/ioctl.h
文件中,我们打开它,可以看到如下信息(我自己添加了注释,只写了几个自己用到的):
1 | // ... ... |
【注意事项】我们使用的时候需要加上asm/ioctl.h
这个头文件。
4. 设备属性结构体
一般来说,我们创建一个字符设备驱动,这个字符设备需要有这些属性:主设备号、次设备号、设备号、设备数量,字符设备结构对象、读写缓冲区等等,随着功能的增加,设备驱动的属性也会增加,我们全部用全局变量的话,有些不是太好管理,这个时候我们可以自定义一个结构体来表示在我们的这个字符设备驱动的各个属性。如,
1 |
|
这样的话也方便了我们参数的传递以及对整个驱动程序的属性管理。
5. 设置文件私有数据
我们在驱动的各个函数之间进行参数传递的时候,使用的的是struct _my_dev
这个结构体中的各个成员,我们不能在每个驱动函数中都使用全局变量吧,这样读写很容易出现问题。
经过观察会发现,struct file_operations
结构体中所有的函数指针都带有struct file
这个结构体参数,于是,我们便可以将包含了设备驱动各个属性的结构体变量传给struct file
结构体的private_data
成员。这个工作在myopen()
函数中完成:
1 | /** |
之后我们在mywrite()
、myread()
、 myclose()
等我们自己实现的驱动函数中直接读取private_data
即可得到设备结构体。 例如,
1 | /** |