设备驱动之---ioctl()

 一、 什么是ioctl。 
      ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就
      是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数
      如下: 
      int ioctl(int fd, ind cmd, …); 
      其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设
      备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,有或没有是和
      cmd的意义相关的。 
      ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支
      持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。 
      
      二、 ioctl的必要性 
      如果不用ioctl的话,也可以实现对设备I/O通道的控制,但那就是蛮拧了。例如,我们可
      以在驱动程序中实现write的时候检查一下是否有特殊约定的数据流通过,如果有的话,
      那么后面就跟着控制命令(一般在socket编程中常常这样做)。但是如果这样做的话,会
      导致代码分工不明,程序结构混乱,程序员自己也会头昏眼花的。 
      所以,我们就使用ioctl来实现控制的功能。要记住,用户程序所作的只是通过命令码告
      诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要
      做的事情。 
      
      三、 ioctl如何实现 
      这是一个很麻烦的问题,我是能省则省。要说清楚它,没有四五千字是不行的,所以我这
      里是不可能把它说得非常清楚了,不过如果有读者对用户程序怎么和驱动程序联系起来感
      兴趣的话,可以看我前一阵子写的《write的奥秘》。读者只要把write换成ioctl,就知
      道用户程序的ioctl是怎么和驱动程序中的ioctl实现联系在一起的了。 
      我这里说一个大概思路,因为我觉得《Linux设备驱动程序》这本书已经说的非常清楚
      了,但是得化一些时间来看。 
      在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对
      应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事
      情,因为设备都是特定的,这里也没法说。关键在于怎么样组织命令码,因为在ioctl中
      命令码是唯一联系用户程序命令和驱动程序支持的途径。 
      命令码的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不
      会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的
      命令发给错误的设备。这些错误都会导致不可预料的事情发生,而当程序员发现了这些奇
      怪的事情的时候,再来调试程序查找错误,那将是非常困难的事情。 
      所以在Linux核心中是这样定义一个命令码的: 
      ____________________________________
      | 设备类型 | 序列号 | 方向 |数据尺寸|
      |----------|--------|------|--------|
      | 8 bit    |  8 bit |2 bit |8~14 bit|
      |----------|--------|------|--------|

      这样一来,一个命令就变成了一个整数形式的命令码。但是命令码非常的不直观,所以
      Linux Kernel中提供了一些宏,这些宏可根据便于理解的字符串生成命令码,或者是从
      命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数
      据传送方向和数据传输尺寸。
       
      这些宏我就不在这里解释了,具体的形式请读者察看Linux核心源代码中的和,文件里给
      除了这些宏完整的定义。这里我只多说一个地方,那就是"幻数"。 
      幻数是一个字母,数据长度也是8,所以就用一个特定的字母来标明设备类型,这和用一
      个数字是一样的,只是更加利于记忆和理解。就是这样,再没有更复杂的了。 
      更多的说了也没有,读者还是看一看源代码吧,推荐各位阅读《Linux 设备驱动程序》所
      带源代码中的short一例,因为它比较短小,功能比较简单,可以看明白ioctl的功能和细
      节。 

      四、 cmd参数如何得出 
      这里确实要说一说,cmd参数在用户程序端由一些宏根据设备类型、序列号、传送方向、
      数据尺寸等生成,这个整数通过系统调用传递到内核中的驱动程序,再由驱动程序使用解
      码宏从这个整数中得到设备的类型、序列号、传送方向、数据尺寸等信息,然后通过
      switch{case}结构进行相应的操作。 
      要透彻理解,只能是通过阅读源代码,我这篇文章实际上只是一个引子。Cmd参数的组织
      还是比较复杂的,我认为要搞熟它还是得花不少时间的,但是这是值得的,驱动程序中最
      难的是对中断的理解。 

      五、 小结 
      ioctl其实没有什么很难的东西需要理解,关键是理解cmd命令码是怎么在用户程序里生成
      并在驱动程序里解析的,程序员最主要的工作量在switch{case}结构中,因为对设备的

      I/O控制都是通过这一部分的代码实现的。


 

以下是生成命令码的相关宏定义_IO()   _IOR()  _IO_W()   _IOWR()

在驱动程序里, ioctl() 函数上传送的变量 cmd 是应用程序用于区别设备驱动程序请求处理内容的值。cmd除了可区别数字外,还包含有助于处理的几种相应信息。 cmd的大小为 32位,共分 4 个域:
     bit31~bit30 2位为 “区别读写” 区,作用是区分是读取命令还是写入命令。
     bit29~bit15 14位为 "数据大小" 区,表示 ioctl() 中的 arg 变量传送的内存大小。
     bit20~bit08  8位为 “魔数"(也称为"幻数")区,这个值用以与其它设备驱动程序的 ioctl 命令进行区别。
     bit07~bit00   8位为 "区别序号" 区,是区分命令的命令顺序序号。
像 命令码中的 “区分读写区” 里的值可能是 _IOC_NONE (0值)表示无数据传输,_IOC_READ (读), _IOC_WRITE (写) , _IOC_READ|_IOC_WRITE (双向)。
内核定义了 _IO() , _IOR() , IOW() 和 _IOWR() 这 4 个宏来辅助生成上面的 cmd 。下面分析 _IO() 的实现,其它的类似:< xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" />

在 asm-generic/ioctl.h 里可以看到 _IO() 的定义

      #define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)

再看 _IOC() 的定义

     #define _IOC(dir,type,nr,size) \
                   (((dir)  << _IOC_DIRSHIFT) | \
                   ((type) << _IOC_TYPESHIFT) | \
                   ((nr)   << _IOC_NRSHIFT) | \
                   ((size) << _IOC_SIZESHIFT))

可见,_IO() 的最后结果由 _IOC() 中的 4 个参数移位组合而成。
再看 _IOC_DIRSHIT 的定义

            #define _IOC_DIRSHIFT    (_IOC_SIZESHIFT+_IOC_SIZEBITS)

      _IOC_SIZESHIFT 的定义

           #define _IOC_SIZESHIFT    (_IOC_TYPESHIFT+_IOC_TYPEBITS)

      _IOC_TYPESHIF 的定义

           #define _IOC_TYPESHIFT    (_IOC_NRSHIFT+_IOC_NRBITS)

      _IOC_NRSHIFT 的定义

           #define _IOC_NRSHIFT    0

      _IOC_NRBITS 的定义

          #define _IOC_NRBITS    8

      _IOC_TYPEBITS 的定义

         #define _IOC_TYPEBITS    8

由上面的定义,往上推得到

      引 用

       _IOC_TYPESHIFT = 8

       _IOC_SIZESHIFT = 16

       _IOC_DIRSHIFT = 30

所以,(dir)  << _IOC_DIRSHIFT) 表是 dir 往左移 30 位,即移到 bit31~bit30 两位上,得到方向(读写)的属性;
       (size) << _IOC_SIZESHIFT) 位左移 16 位得到“数据大小”区;
       (type) << _IOC_TYPESHIFT) 左 移 8位得到"魔数区" ;
       (nr)   << _IOC_NRSHIFT)      左移 0 位( bit7~bit0) 。
这样,就得到了 _IO() 的宏值。

这几个宏的使用格式为

  • _IO (魔数, 基数);
  • _IOR (魔数, 基数, 变量型)
  • _IOW  (魔数, 基数, 变量型)
  • _IOWR (魔数, 基数,变量型 )

魔数 (magic number)
      魔数范围为 0~255 。通常,用英文字符 "A" ~ "Z" 或者 "a" ~ "z" 来表示。设备驱动程序从传递进来的命令获取魔数,然后与自身处理的魔数想比较,如果相同则处理,不同则不处理。魔数是拒绝误使用的初步辅助状态。设备驱动 程序可以通过 _IOC_TYPE (cmd) 来获取魔数。不同的设备驱动程序最好设置不同的魔数,但并不是要求绝对,也是可以使用其他设备驱动程序已用过的魔数。
基(序列号)数
      基数用于区别各种命令。通常,从 0开始递增,相同设备驱动程序上可以重复使用该值。例如,读取和写入命令中使用了相同的基数,设备驱动程序也能分辨出来,原因在于设备驱动程序区分命令时 使用 switch ,且直接使用命令变量 cmd值。创建命令的宏生成的值由多个域组合而成,所以即使是相同的基数,也会判断为不同的命令。设备驱动程序想要从命令中获取该基数,就使用下面的宏:
_IOC_NR (cmd)
通常,switch 中的 case 值使用的是命令的本身。
变量型
      变量型使用 arg 变量指定传送的数据大小,但是不直接代入输入,而是代入变量或者是变量的类型,原因是在使用宏创建命令,已经包含了 sizeof() 编译命令。比如 _IOR() 宏的定义是:

      引用

      #define _IOR(type,nr,size)    _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

而 _IOC_TYPECHECK() 的定义正是:

      引用

      #define _IOC_TYPECHECK(t) (sizeof(t))

设备驱动程序想要从传送的命令获取相应的值,就要使用下列宏函数:
      _IOC_SIZE(cmd)

_IO 宏

      该宏函数没有可传送的变量,只是用于传送命令。例如如下约定:

      引用

      #define TEST_DRV_RESET _IO ('Q', 0)

此时,省略由应用程序传送的 arg 变量或者代入 0 。在应用程序中使用该宏时,比如:

      ioctl (dev, TEST_DEV_RESET, 0)   或者  ioctl (dev, TEST_DRV_RESET) 。
这是因为变量的有效因素是可变因素。只作为命令使用时,没有必要判 断出设备上数据的输出或输入。因此,设备驱动程序没有必要执行设备文件大开选项的相关处理。

_IOR 宏
     该函数用 于创建从设备读取数据的命令,例如可如下约定:

     引用

     #define TEST_DEV_READ  _IRQ('Q', 1, int)

这说明应用程序从设备读取数据的大小为 int 。下面宏用于判断传送到设备驱动程序的 cmd 命令的读写状态:
     _IOC_DIR (cmd)
运行该宏时,返回值的类型 如下:

  • _IOC_NONE                             :  无属性
  • _IOC_READ                             :  可读属性
  • _IOC_WRITE                           : 可写属性
  • _IOC_READ | _IOC_WRITE : 可读,可写属性

使用该命令时,应用程序的 ioctl() 的 arg 变量值指定设备驱动程序上读取数据时的缓存(结构体)地址。
_IOW 宏
      用于创建设 备上写入数据的命令,其余内容与 _IOR 相同。通常,使用该命令时,ioctl() 的 arg 变量值指定设备驱动程序上写入数据时的缓存(结构体)地址。
_IOWR 宏
      用于创建设备上读写数据的命令。其余内 容与 _IOR 相同。通常,使用该命令时,ioctl() 的 arg 变量值指定设备驱动程序上写入或读取数据时的缓存 (结构体) 地址。
_IOR() , _IOW(), IORW() 的定义
      #define _IOR(type,nr,size)    _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
      #define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
      #define _IOWR(type,nr,size)    _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

 



关于IOCTL驱动的编写方法LDD这本书确实写的比较明白了,在这呢我就简单的做一个介绍。这里我主要描述自己编写IOCTL驱动时所遇到的问题及其原因。
    驱动里的ioctl函数主要实现不用read,write函数的与用户空间的简单数据交互及无参数的命令控制。那么我们如何实现这几种功能的IOCTL函数呢?ioctl驱动中以SWITCH{case A,case B}结构以实现对不同命令的响应,首先我们要对我们要使用的“A”,“B”命令定义一个整个操作系统内唯一的标识,同时这个标识又能够表明我们的操作类型及传递参数的类型(如果需要的话)。linux系统的每一个命令号被分为多个位字段,这些位字段包括type,number,direction,size,分别表示幻数(与设备相关的一个字母,以避免与内核冲突),序数(命令编号),方向位(以用户空间为参照的读,写和无数据传输),size(传递参数类型)。内核中/include/asm/ioctl.h,/Documentation/ioctl-number.txt两个文件表明了我们应该如何定义ioctl命令编号及自定义的幻数,。
    看起来构造IOTCL命令这么复杂,庆幸的是内核已经为我们编写了构造命令编号的宏命令。_IO(type,nr),_IOR(type,nr,datatype),_IOW(type,nr,datatype)。它们在<linux/ioctl.h>包含的<asm/ioctl.h>中有定义,所以在我们的驱动中应包含这个头文件。其中_IO()用做无参数的命令编号,_IOR()用做从驱动中读取数据的命令编号,_IOW()用做写入数据命令。 LDD中用:define SCULL_IOC_MAGIC 'k' ,define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC,1,int)表示SCULL_IOCSQUANTUM命令编号为向驱动中写数据,命令编号为1,传送参数类型为int。

这里把我所写的部分代码贴出来:
 驱动程序部分代码:
 #define MOTOR_MAGIC  'k'
 #define SET_PULSE _IOW(MOTOR_MAGIC,  1,int)
  .
  .
  static int motor_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
     int data=0; 
      int retval;
      switch (cmd) {
      case SET_PULSE:
              if (copy_from_user(&data, (int *)arg, sizeof(int)))
               return -EFAULT;
             .
             .
             .
}
    
应用程序部分代码:  
    #define MOTOR_MAGIC  'k'
    #define SET_PULSE _IOW(MOTOR_MAGIC,  1,int)
    int pulse,fileno;
    fileno = open("/tmp/usb",O_RDWR); 
    if (fileno == -1) { 
        printf("open device key error!\n"); 
        return 0; 
    }
       .
       .
       printf("Please input the pulse:\n"
       scanf("%d",pulse);  
       if(ioctl(fileno,SET_PULSE,pulse)<0){
          perror("ioctl error");
          exit(1);
       }       
        .
        . 
  这里我们一定要注意的是data 和pulse的数据类型一定要与SET_PULSE _IOW(MOTOR_MAGIC,  1,int)中定义的int类型严格一致,即使是unsigned int 和int类型之间也是有很大差别的,我就是因为设定了unsigned int类型的data和pulse却使用SET_PULSE _IOW(MOTOR_MAGIC,  1,int)搞了我两三天才搞明白。 再有就是ioctl函数的声明一定要与 static int motor_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg);相一致,我刚开始就是定义错了,也是搞了我好长时间才明白。因为内核非常的相信你,它不会检查你的IOTCL定义有没有问题,它只负责把你的IOCTL函数映射到驱动的ioctl操作上。其中inode和filp对应应用程序传递的文件描述符,这和传递给open方法的参数一样,参数cmd 由用户空间不经修改的传递给驱动程序,可选的arg参数无论用户空间传递的是指针还是值,它都以unsigned long的形式传递给驱动程序。

  还有就是我在驱动中明明是用的是使用地址的copy_from_usr函数,但用户程序却只能传递值.搞了半天最后才知道,原来我的用户程序中的scanf(x),'x'没有加'&'符号,所以这时从终端把读到的数据写到了'x'所代表的地址处,也就是说现在'x'所代表的地址正是所读入的数据.呵呵,总结出的经验就是----->对库函数的参数类型一定要搞清楚.



在Linux字符设备驱动入门(一)中,我们实现了字符设备的简单读写字符功能,接下来我们要在这个基础上加入ioctl功能。首先,我们先来看看3.0内核下../include/linux/fs.h中file_operations结构体的定义:


struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
};

      红色字体已经标出在kernel 3.0中已经完全删除了struct file_operations 中的ioctl 函数指针,剩下unlocked_ioctlcompat_ioctl,取而代之的是unlocked_ioctl,主要改进就是不再需要上大内核锁 (调用之前不再先调用lock_kernel()然后再unlock_kernel())。

       所以,在hellow.c中,我们在file_operations中加入成员函数hello_ioctl(红色字体部分):

/* file operations for hello device */
static struct file_operations hello_ops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = hello_ioctl,
    .open = hello_open,
    .read = hello_read,
    .write = hello_write,
    .release = hello_release,
};

hello_ioctl()的定义如下:

static int hello_ioctl( struct file *file,
            unsigned int cmd, unsigned long arg)
{    int temp = 0;
    switch(cmd)
    {
      case HELLO_CMD1:
               {
            temp = 1;
            if(copy_to_user( (int *)arg, &temp, sizeof(int))) return -EFAULT;
            break;
               }
      case HELLO_CMD2:
            {
            temp = 2;
            if(copy_to_user( (int *)arg, &temp, sizeof(int))) return -EFAULT;
            break;
            }
    }
    printk( KERN_NOTICE"ioctl CMD%d done!\n",temp);    

return 0;

}


       这里强调一下cmd的定义:

#define HELLO_MAGIC 'k'
#define HELLO_CMD1    _IO(HELLO_MAGIC,0x1a)
#define HELLO_CMD2    _IO(HELLO_MAGIC,0x1b)

            其中'k'为幻数,要按照Linux内核的约定方法为驱动程序选择ioctl编号,应该首先看看include/asm/ioctl.h和Documentation/ioctl-number.txt这两个文件,下面是ioctl.h的部分内容,也是比较重要的:

_IO(type, nr)
       用于构造无参数的命令编号;
_IOR(type, nr, datatype)
       用于构造从驱动程序中读取数据的命令编号;
_IOW(type, nr, datatype)
       用于写入数据的命令;
_IOWR(type, nr, datatype)
       用于双向传输。
注意千万不能重复定义。


注意对幻数的编号千万不能重复定义,如ioctl-number.txt已经说明‘k'的编号已经被占用的范围为:

'k'    00-0F    linux/spi/spidev.h    conflict!
'k'    00-05    video/kyro.h        conflict!

     所以我们在这里分别编号为0x1a和0x1b,到这里,我们已经完成了对ioctl功能的编写,接下来就是在测试程序中利用系统调用来测试它。

=============================================================

ioctl测试程序

=============================================================
#include <stdio.h>  
#include <fcntl.h>  
#include <stdlib.h>  
#include <string.h>  
#include <sys/types.h>  
#include <sys/stat.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define HELLO_MAGIC 'k'  //当然我们也可以定义一个相应的头文件,把ioctl的cmd放进里面,然后再include进 来
#define HELLO_CMD1    _IO(HELLO_MAGIC,0x1a)
#define HELLO_CMD2    _IO(HELLO_MAGIC,0x1b)

      int main(void)
{
    int ioctl_rdata;
    int fd, ret;

    fd = open ( "/dev/hellow" , O_RDWR);
    if ( fd == -1 )
    {
      perror("open");
      exit(0);
        }

    ret = ioctl( fd, HELLO_CMD2,&ioctl_rdata);
    if ( ret == -1)
    {
     perror("ioctl");
     exit(0);
    }
    printf("ioctl_rdata= %d \n",ioctl_rdata);

    close(fd);
    return 0;
}
=============================================================

运行结果

=============================================================

root@Ubuntu:~/share/hellow# insmod hellow.ko
root@Ubuntu:~/share/hellow# mknod /dev/hellow c 251 0
root@Ubuntu:~/share/hellow# ./a.out
ioctl_rdata= 2 
root@Ubuntu:~/share/hellow# dmesg | tail

[ 2431.126532] hello init. major:251, minor:0
[ 2453.326022] Hello device open!

[ 2453.326047] ioctl CMD2 done!
[ 2453.326487] Hello device close!



  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值