目录
🍅点击这里查看所有博文
随着自己工作的进行,接触到的技术栈也越来越多。给我一个很直观的感受就是,某一项技术/经验在刚开始接触的时候都记得很清楚。往往过了几个月都会忘记的差不多了,只有经常会用到的东西才有可能真正记下来。存在很多在特殊情况下有一点用处的技巧,用的不多的技巧可能一个星期就忘了。
想了很久想通过一些手段把这些事情记录下来。也尝试过在书上记笔记,这也只是一时的,书不在手边的时候那些笔记就和没记一样,不是很方便。
很多时候我们遇到了问题,一般情况下都是选择在搜索引擎检索相关内容,这样来的也更快一点,除非真的找不到才会去选择翻书。后来就想到了写博客,博客作为自己的一个笔记平台倒是挺合适的。随时可以查阅,不用随身携带。
同时由于写博客是对外的,既然是对外的就不能随便写,任何人都可以看到。经验对于我来说那就只是经验而已,公布出来说不一定我的一些经验可以帮助到其他的人。遇到和我相同问题时可以少走一些弯路。
既然决定了要写博客,那就只能认真去写。不管写的好不好,尽力就行。千里之行始于足下,一步一个脚印,慢慢来
,写的多了慢慢也会变好的。权当是记录自己的成长的一个过程,等到以后再往回看时,就会发现自己以前原来这么菜😂。
本系列博客所述资料均来自互联网
,并不是本人原创(只有博客是自己写的)。出于热心,本人将自己的所学笔记整理并推出相对应的使用教程,方面其他人学习。为国内的物联网事业发展尽自己的一份绵薄之力,没有为自己谋取私利的想法
。若出现侵权现象,请告知本人,本人会立即停止更新,并删除相应的文章和代码。
前言
以字符设备为例。一般情况下,一个字符设备的驱动,除了读取和写入设备之外,大部分的驱动程序都需要通过设备驱动程序来执行各种类型的硬件控制。
例如,针对串口设备,驱动层除了需要提供对串口的读写,还需要提供对串口波特率、校验位、以及流控等配置信息的控制。
这些配置信息需要从应用层传递一些基本数据,相比普通的读写数据,控制数据仅仅也只是数据类型不同。同时传输的控制信息,数据量一般情况下也不会太大。
IOTCTL
在Linux应用空间中,针对字符设备应用程序可通过ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。一般用来用来控制设备的状态,修改设备的配置。
函数
/*
参数:
@fd:打开设备文件的时候获得文件描述符
@ cmd:第二个参数:给驱动层传递的命令,需要注意的是,驱动层的命令和应用层的命令一定要统一
@第三个参数: "..."在C语言中,很多时候都被理解成可变参数。
返回值
成功:0
失败:-1,同时设置errno
*/
int iotctl(int fd, unsigned long request, ...);
该系统调用进入内核空间后,设备驱动中被调用的file_operations结构体对应的函数指针如下:
/*
参数:
@file: vfs层为打开字符设备文件的进程创建的结构体,用于存放文件的动态信息
@ cmd: 用户空间传递的命令,可以根据不同的命令做不同的事情
@第三个参数: 用户空间的数据,主要这个数据可能是一个地址值(用户空间传递的是一个地址),也可能是一个数值,也可能没值
返回值
成功:0
失败:带错误码的负值
*/
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
两者之间的参数对应关系如下图所示:
命令
ioctl主要用来来实现控制的功能。用户程序所作的只是通过命令码cmd告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情,而ioctl就是负责接收cmd命令码来实现这些命令,它保证了程序的有序和整洁。
在这个系统调用之间,有一个非常关键的参数,就是cmd。其由用户空间直接不经修改的传递给驱动程序。大小为4个字节,在其定义中该参数被分为四个字段。
字段 | 取值 | 释意 |
---|---|---|
设备类型(8bit) | 字符 0~255 | 类型或叫幻数,代表一类设备,一般用一个字母或者1个8bit的数字 |
序列号(8bit) | 0~255 | 代表这个设备的第几个命令 |
方向(2bit) | 只读 只写 读写 | 表示是由内核空间到用户空间,或是用户空间到内核空间 |
数据大小(16bit) | 0~65535 | 表示需要读写的参数大小 |
命令构建
<linux/ioctl.h>
中包含的<asm/ioctl.h>
头文件定义了一些构造命令编号的宏。在驱动中可通过以下宏定义快速组合一个命令。_IO
用于构造无数据传输的命令编号。_IOR
用于构造从驱动程序中读取数据的命令编号。_IOW
用于构造向设备写入数据的命令编号。_IOWR
用于构造双向传输命令编号。
_IO(type, nr)
_IOR(type, nr, datatype)
_IOW(type, nr, datatype)
_IOWR(type, nr, datatype)
//其中,type和number位段从以上宏的参数中传入,size位段通过对datatype参数取sizeof获得。
下面的示例则是我们即将要编写的驱动代码中的命令定义:
#define DEV_FIFO_TYPE 'k'
#define DEV_FIFO_CLEAN _IO(DEV_FIFO_TYPE,0)
#define DEV_FIFO_GETVALUE _IOR(DEV_FIFO_TYPE,1,int)
#define DEV_FIFO_SETVALUE _IOW(DEV_FIFO_TYPE,2,int)
在使用ioctl命令编号时,一定要避免与预定义命令重复,否则,命令冲突,设备不会响应。下列ioctl命令对任何文件(包括设备特定文件)都是预定义的:
FIOCTLX //设置执行时关闭标志
FIONCLEX //清除执行时关闭标志
FIOASYNC //设置或复位文件异步通知
FIOQSIZE //返回文件或目录大小
FIONBIO //文件非阻塞型IO,file ioctl non-blocking i/o
命令检查
在设备的驱动中,虽然定义了一系列命令。但是用户空间中的应用代码在执行的过程中,有可能并不会按照预定义的命令进行调用。那么在驱动模块中,该如何检查传入的命令是否合法?
在<asm/ioctl.h>
头文件中还定义了一些用于解析命令编号的宏。_IOC_TYPE
获取cmd的幻数,来判断应用程序传下来的命令type是否正确。_IOC_NR
获取cmd的序号。_IOC_DIR
获取cmd的数据传输方向,来判断命令是读还是写。_IOC_SIZE
获取cmd的用户数据大小。
_IOC_TYPE(cmd)
_IOC_NR(cmd)
_IOC_DIR(cmd)
_IOC_SIZE(cmd)
我们在驱动程序的iotctl的处理函数中,一般会先判断命令的类型是否正确。若类型不正确则直接报错,并返回错误代码。
if(_IOC_TYPE(cmd) != DEV_FIFO_TYPE){
pr_err("cmd %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
return-ENOTTY;
}
紧接着会获取命令的传输方向,然后再通过宏access_ok来判断用户层传递的内存地址是否合法。
/*
参数:
type: VERIFY_READ 或是 VERIFY_WRITE,取决于是读取还是写入用户空间内存区。
如果在该地址处既要读取,又要写入,则应该用:VERIFY_WRITE,因为它是VERIFY_READ的超集。
addr: 一个用户空间的地址
size: 如果要读取或写入一个int型数据,则为sizeof(int)
返回值:
为1(成功)或0(失败)
如果返回失败,驱动程序通常返回-EFAULT给调用者来验证地址的合法性。
Note:
access_ok不检查空指针,如果传入空指针,是可以通过判断的。空指针需要另行检查
access_ok不做校验内存存取的完整工作; 它只检查内存引用是否在这个进程有合理权限的内存范围中,且确保这个地址不指向内核空间内存
大部分驱动代码不需要真正调用 access_ok,而直接使用put_user(datum, ptr)和get_user(local, ptr),它们带有校验的功能,确保进程能够写入给定的内存地址
*/
#define __range_ok(addr, size) \
(test_thread_flag(TIF_USERSPACE) \
&& (((unsigned long)(addr) >= 0x80000000) \
|| ((unsigned long)(size) > 0x80000000) \
|| (((unsigned long)(addr) + (unsigned long)(size)) > 0x80000000)))
int access_ok(int type,const void *addr,unsigend long size);
if(_IOC_DIR(cmd)&_IOC_READ)
{
if(p == NULL){
pr_err("arg is NULL!\n");
return-EFAULT;
}
ret = !access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
}
else if( _IOC_DIR(cmd)&_IOC_WRITE)
{
if(p == NULL){
pr_err("arg is NULL!\n");
return-EFAULT;
}
ret = !access_ok(VERIFY_READ,(void __user*)arg,_IOC_SIZE(cmd));
}
if(ret){
pr_err("arg bad access \n");
return-EFAULT;
}
最后一步就是检查命令的序号了,不过一般情况下我们也不会单独提取出序号进行检查。大多数情况都是直接根据cmd的值进行散转。根据不同的命令进入到不同的分支进行相应的处理。
void __user *argp = (void __user *)arg;
int __user *p = argp;
switch(cmd)
{
case DEV_FIFO_CLEAN:
printk("DEV_FIFO_CLEAN\n");
break;
case DEV_FIFO_GETVALUE:
err = put_user(knum, p);
printk("DEV_FIFO_GETVALUE %d\n",knum);
break;
case DEV_FIFO_SETVALUE:
err = get_user(knum, p);
printk("DEV_FIFO_SETVALUE %d\n",knum);
break;
default:
pr_err("bad cmd %ld.\n",cmd);
return -EINVAL;
}
驱动编写
节省篇幅,这里的代码是在上一篇文章设计内容的基础上修改的。驱动代码中首先补充file_operations的**unlocked_ioctl **函数。
static struct file_operations hello_ops =
{
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
.unlocked_ioctl = hello_ioctl,
};
iotctl函数这里就不多做解释了,就是上面命令检查的内容。
/*ioctl(fd,DEV_FIFO_GETVALUE, &num);*/
static int knum = 99;
long hello_ioctl (struct file *filep, unsigned int cmd, unsigned long arg)
{
long err, ret = 0;
void __user *argp = (void __user *)arg;
int __user *p = argp;
if(_IOC_TYPE(cmd) != DEV_FIFO_TYPE){
pr_err("cmd %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
return-ENOTTY;
}
if(_IOC_DIR(cmd)&_IOC_READ)
{
if(p == NULL){
pr_err("arg is NULL!\n");
return-EFAULT;
}
ret = !access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
}
else if( _IOC_DIR(cmd)&_IOC_WRITE)
{
if(p == NULL){
pr_err("arg is NULL!\n");
return-EFAULT;
}
ret = !access_ok(VERIFY_READ,(void __user*)arg,_IOC_SIZE(cmd));
}
if(ret){
pr_err("arg bad access \n");
return-EFAULT;
}
switch(cmd)
{
case DEV_FIFO_CLEAN:
printk("DEV_FIFO_CLEAN\n");
break;
case DEV_FIFO_GETVALUE:
err = put_user(knum, p);
printk("DEV_FIFO_GETVALUE %d\n",knum);
break;
case DEV_FIFO_SETVALUE:
err = get_user(knum, p);
printk("DEV_FIFO_SETVALUE %d\n",knum);
break;
default:
pr_err("bad cmd\n");
return -EINVAL;
}
return err;
}
示例中使用到了put_user,get_user宏定义。这里有一个比较迷惑的地方,一般情况下Linux内核代码中宏定义都是全大写字母,这里却用的小写字母,很容易让人以为是一个函数。这两个同样可用于内核空间和用户空间之间的数据拷贝。
put_user可以向用户空间传递单个数据。单个数据并不是指一个字节数据,对ARM而言,put_user一次性可传递一个char,short或者int型的数据,即1、2或者4字节。用put_user比用copy_to_user要快:
参数
x 为内核空间的数据,
p 为用户空间的指针。
返回值
传递成功,返回 0,否则返回-EFAULT。
int put_user(x,p)
get_user可以从用户空间获取单个数据,单个数据并不是指一个字节数据,对ARM而言,get_user一次性可获取一个char、short或者 int型的数据,即1、2或者4字节。用get_user比用get_from_user要快:
参数
x为内核空间的变量
p为用户空间的指针。
返回值
获取成功,返回0,否则返回-EFAULT.
int get_user(x,p)
这里很多新手朋友会犯错误。习惯性认为,单个变量传入函数中获取值需要取地址。前面页说到了get_user是一个宏,使用get_user时并不需要对内核空间的变量取地址。将宏定义展开,可清晰的看到所谓传入的x就是一个直接赋值而已(x) = (__typeof__(*(ptr))) __gu_val
。
#define __get_user(x,ptr) \
__get_user_nocheck((x),(ptr),sizeof(*(ptr)))
#define __get_user_nocheck(x,ptr,size) \
({ \
long __gu_err = 0; \
unsigned long __gu_val; \
__chk_user_ptr(ptr); \
switch (size) { \
case 1: __get_user_8(ptr); break; \
case 2: __get_user_16(ptr); break; \
case 4: __get_user_32(ptr); break; \
case 8: __get_user_64(ptr); break; \
default: __get_user_unknown(); break; \
} \
(x) = (__typeof__(*(ptr))) __gu_val; \
__gu_err; \
})
实验结果
实验代码如下,在打开驱动程序后。首先,分别写了三个错误行为,依次是错误的命令类型(幻术)、错误的命令序号(不存在该命令)、错误的地址(写入数据时传入了0x9999999999999)、错误的地址(读取数据时传入了NULL)。
#if 1
#define DEV_FIFO_TEST_TYPE _IO('L', 0)
if(ioctl(fd, DEV_FIFO_TEST_TYPE) < 0) {
perror("DEV_FIFO_TEST_TYPE");
}
#define DEV_FIFO_TEST_NR _IO(DEV_FIFO_TYPE, 3)
if(ioctl(fd, DEV_FIFO_TEST_NR) < 0) {
perror("DEV_FIFO_TEST_NR");
}
if(ioctl(fd, DEV_FIFO_ETVALUE, 0x9999999999999) < 0) {
perror("DEV_FIFO_TEST_DIR");
}
if(ioctl(fd, DEV_FIFO_GETVALUE, NULL) < 0) {
perror("DEV_FIFO_TEST_DIR");
}
#endif
紧接着依次调用头文件中定义的三个命令,最后又读取了DEV_FIFO_SETVALUE写入的数据。
通过代码分析,驱动模块中默认的num(knum)是99。第一次读取,num值应该是99,紧接着num加一,并将num通过iotctl写入到驱动中。那么第二次读取的值,理应为100。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h> //close
#include "beep.h"
void main(void)
{
int num = 0;
int fd = open("/dev/hellodev",O_RDWR);
if(fd < 0) {
perror("open fail \n");
return;
}
#if 1
#define DEV_FIFO_TEST_TYPE _IO('L', 0)
if(ioctl(fd, DEV_FIFO_TEST_TYPE) < 0) {
perror("DEV_FIFO_TEST_TYPE");
}
#define DEV_FIFO_TEST_NR _IO(DEV_FIFO_TYPE, 3)
if(ioctl(fd, DEV_FIFO_TEST_NR) < 0) {
perror("DEV_FIFO_TEST_NR");
}
if(ioctl(fd, DEV_FIFO_GETVALUE, 0x9999999999999) < 0) {
perror("DEV_FIFO_TEST_DIR");
}
if(ioctl(fd, DEV_FIFO_GETVALUE, NULL) < 0) {
perror("DEV_FIFO_TEST_DIR");
}
#endif
if(ioctl(fd, DEV_FIFO_CLEAN) < 0) {
perror("DEV_FIFO_CLEAN");
return;
}
if(ioctl(fd, DEV_FIFO_GETVALUE, &num) < 0) {
perror("DEV_FIFO_GETVALUE");
return;
}
printf("get num = %d \n",num);
num++;
ioctl(fd,DEV_FIFO_SETVALUE, &num);
printf("set num = %d \n",num);
if(ioctl(fd,DEV_FIFO_GETVALUE, &num) < 0) {
perror("DEV_FIFO_GETVALUE");
return;
}
printf("get num = %d \n",num);
close(fd);
return;
}
代码运行的结果如下,可以看到模拟的几个错误行为,也都正常识别到。读写的数据也是符合预期的。
root@ubuntu:# insmod ./hello.ko
root@ubuntu:# gcc ./test.c
root@ubuntu:# ./a.out
DEV_FIFO_TEST_TYPE: Inappropriate ioctl for device
DEV_FIFO_TEST_NR: Invalid argument
DEV_FIFO_TEST_DIR: Bad address
DEV_FIFO_TEST_DIR: Bad address
get num = 99
set num = 100
get num = 100
root@ubuntu:# dmesg
[261820.060585] hello_open()
[261820.060596] cmd 19456,bad magic 0x4c/0x6b.
[261820.060670] bad cmd
[261820.060684] arg bad access
[261820.060696] arg is NULL!
[261820.060708] DEV_FIFO_CLEAN
[261820.060715] DEV_FIFO_GETVALUE 99
[261820.060725] DEV_FIFO_SETVALUE 100
[261820.060734] DEV_FIFO_GETVALUE 100
[261820.060807] hello_release()
那么本篇博客就到此结束了,这里只是记录了一些我个人的学习笔记,其中存在大量我自己的理解。文中所述不一定是完全正确的,可能有的地方我自己也理解错了。如果有些错的地方,欢迎大家批评指正。如有问题直接在对应的博客评论区指出即可,不需要私聊我。我们交流的内容留下来也有助于其他人查看,说不一定也有其他人遇到了同样的问题呢😂。