驱动学习(八)ioctl设备管理

前言
  以字符设备为例。一般情况下,一个字符设备的驱动,除了读取和写入设备之外,大部分的驱动程序都需要通过设备驱动程序来执行各种类型的硬件控制。

  例如,针对串口设备,驱动层除了需要提供对串口的读写,还需要提供对串口波特率、校验位、以及流控等配置信息的控制。

  这些配置信息需要从应用层传递一些基本数据,相比普通的读写数据,控制数据仅仅也只是数据类型不同。同时传输的控制信息,数据量一般情况下也不会太大。

ioctl
  在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个字节,在其定义中该参数被分为四个字段。


命令封装
  <linux/ioctl.h>中包含的<asm/ioctl.h>头文件定义了一些构造命令编号的宏。在驱动中可通过以下宏定义快速组合一个命令。_IO用于构造无数据传输的命令编号。_IOR用于构造从驱动程序中读取数据的命令编号。_IOW用于构造向设备写入数据的命令编号。_IOWR用于构造双向传输命令编号。

下面的示例则是我们即将要编写的驱动代码中的命令定义:

#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含义
  示例中使用到了put_user,get_user宏定义。这里有一个比较迷惑的地方,一般情况下Linux内核代码中宏定义都是全大写字母,这里却用的小写字母,很容易让人以为是一个函数。这两个同样可用于内核空间和用户空间之间的数据拷贝。

  put_user可以向用户空间传递单个数据。单个数据并不是指一个字节数据,对ARM而言,put_user一次性可传递一个char,short或者int型的数据,即1、2或者4字节。用put_user比用copy_to_user要快:

//put_user可以向用户空间传递单个数据
int put_user(x,p)

参数
    x 为内核空间的数据,
    p 为用户空间的指针。
返回值
    传递成功,返回 0,否则返回-EFAULT。


  get_user可以从用户空间获取单个数据,单个数据并不是指一个字节数据,对ARM而言,get_user一次性可获取一个char、short或者 int型的数据,即1、2或者4字节。用get_user比用get_from_user要快:

//get_user可以从用户空间获取单个数据
int get_user(x,p)

参数
    x为内核空间的变量
    p为用户空间的指针。
返回值
    获取成功,返回0,否则返回-EFAULT.


  这里很多新手朋友会犯错误。习惯性认为,单个变量传入函数中获取值需要取地址。前面页说到了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()


                        
原文链接:https://blog.csdn.net/weixin_44570083/article/details/136509118

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值