一、ioctl功能简介

open、write函数的功能无非就是为了进行用户空间和内核空间的数据交换,而ioctl呢?

大部分驱动除了需要具备读写设备的能力之外,还需要对设备具有控制能力,比如要求设备报告错误信息,弹出介质,设置波特率等。

这些操作通常是通过ioctl来实现的。

用户空间和内核空间实现ioctl的方法

1.在用户空间中

int  ioctl(int fd, unsigned long cmd, ...);

...表示的是编译器不对此参数进行类型检查,所以在我们传参的时候,尤其是传递指针的时候,需要注意了,一定要进行地址检查,否则可能产生致命的漏洞

fd是表示打开文件时得到的文件操作符,他讲会对应我们内核空间中inod和file两个参数,参数cmd表示一个ioctl指令,第三个...表示的是第二个参数cmd命令

所需要的参数,值得注意的是,该参数并不像printf中的可变参数列表,这里的...至多可以指定一个参数。当然你也可以不指定参数。当我们指定了一个参数的

时候,该参数要么是一个指针,要么是一个整数。

2.在内核空间中

int (*ioctl) (struct inode * node, struct file *filp, unsigned int cmd, unsigned long arg);

参数node和filp来源于用户空间的fd,cmd,arg来源于用户空间的cmd和arg,只是类型不同而已!

下面看网友总结的一张图



二、如何实现ioctl方法

1.定义cmd命令

2.实现ioctl底层驱动函数

下面仔细介绍这两步

三、关于cmd

从cmd的定义就可以知道,cmd是一个unsigned int的变量,所以只不过一个数而已,所以我们先来看一个简单的示例程序

1.首先在mem_pool.h文件中定义如下命令

#define MEM_CLEAR 			0x1  		/*清0全局内存*/

2.在底层驱动文件中mem_pool.c中的ioctl函数

/* ioctl设备控制函数 */
static int mem_pool_ioctl(struct inode *inodep, struct file *filp, unsigned
  int cmd, unsigned long arg)
{
	/*获得设备结构体指针*/
  struct mem_pool_dev *dev = filp->private_data;

  switch (cmd)
  {
    case MEM_CLEAR:
      memset(dev->mem, 0, mem_pool_SIZE);      
      printk( "mem_pool is set to zero\n");
      break;

    default:
    	printk( "cmd is error \n");
      return  - EINVAL;
  }
  return 0;
}
功能是将内存清零了

3.再看一个应用测试程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

	/*0. 包含命令定义 */
#include "mem_pool.h"  

int main()
{
	int fd = 0;
	int cmd;
	int arg = 0;
	char buf[4096] = "xbDeng";
	
	
	/*1. 打开设备文件*/
	fd = open("/dev/mem_pool",O_RDWR);
	if (fd < 0)
	{
		printf("Open Dev Mem0 Error!\n");
		return -1;
	}
	printf("%s \n",buf);
	/*2. *写文件*/
	if(write(fd,buf,sizeof(buf)) < 0 )
	{
	  printf("write error !! \n");
	}
	/*3. 注意,要读取内容,一定要记得定位*/
	lseek(fd,0,SEEK_SET);
	/*4. 读文件*/
	if(read(fd,buf,sizeof(buf)) < 0 )
	{
	  printf("read error !! \n");
	}

	printf("read fd is %s \n",buf);
	
	/*5. 调用命令MEM_CLEAR */
	printf("<--- Call MEM_CLEAR --->\n");
	cmd = MEM_CLEAR;
	if (ioctl(fd, cmd, &arg) < 0)
        {
        	printf("Call cmd MEM_CLEAR fail\n");
        	return -1;
	}
	lseek(fd,0,SEEK_SET);
	/*6. 再读文件*/
	if(read(fd,buf,sizeof(buf)) < 0 )
	{
	  printf("read error !! \n");
	}

	printf("%s \n",buf);	
	/*7. 关闭文件*/	
	close(fd);
	return 0;	
}

最后能够看到打印的内容如下所示



内存清理掉后就无法读出数据了。

其实按照上面的示例来做就已经可以让我们的程序正常运行了,但是不怕一万,就怕万一,万一哪天打开错了设备文件,并且调用了相同的ioctl,这样就完蛋了。因为这个文件里面同样有cmd对应实现。为了防止这样的事情发生,内核对cmd又有了新的定义,规定了cmd都应该不一样。

四、内核中cmd的前世今生

关于cmd的前世今生,以下内容出自某位网友,个人感觉这部分他说得很好。

以下来自http://blog.chinaunix.net/uid-25014876-id-59419.html

一个cmd被分为了4个段,每一段都有各自的意义,cmd的定义在<linux/ioctl.h>。注:但实际上<linux/ioctl.h>中只是包含了<asm/ioctl.h>,这说明了这是跟平台相关的,

ARM的定义在<arch/arm/include/asm/ioctl.h>,但这文件也是包含别的文件<asm-generic/ioctl.h>,千找万找,终于找到了。


在<asm-generic/ioctl.h>中,cmd拆分如下:


解释一下四部分,全部都在<asm-generic/ioctl.h>和ioctl-number.txt这两个文档有说明。
1)幻数:说得再好听的名字也只不过是个0~0xff的数,占8bit(_IOC_TYPEBITS)。这个数是用来区分不同的驱动的,像设备号申请的时候一样,内核有一个文档给出一

些推荐的或者已经被使用的幻数。

/*Documentation/ioctl/ioctl-number.txt*/
164 'w' all CERN SCI driver
165 'y' 00-1F packet based user level communications
166 <mailto:zapman@interlan.net>
167 'z' 00-3F CAN bus card
168 <mailto:hdstich@connectu.ulm.circular.de>
169 'z' 40-7F CAN bus card
170 <mailto:oe@port.de>


可以看到'x'是还没有人用的,我就拿这个当幻数!


2)序数:用这个数来给自己的命令编号,占8bit(_IOC_NRBITS),我的程序从1开始排序。


3)数据传输方向:占2bit(_IOC_DIRBITS)。如果涉及到要传参,内核要求描述一下传输的方向,传输的方向是以应用层的角度来描述的。
1)_IOC_NONE:值为0,无数据传输。
2)_IOC_READ:值为1,从设备驱动读取数据。
3)_IOC_WRITE:值为2,往设备驱动写入数据。
4)_IOC_READ|_IOC_WRITE:双向数据传输。



4)数据大小:与体系结构相关,ARM下占14bit(_IOC_SIZEBITS),如果数据是int,内核给这个赋的值就是sizeof(int)。


强调一下,内核是要求按这样的方法把cmd分类,当然你也可以不这样干,这只是为了迎合内核的要求,让自己的程序看上去很正宗。上面我的程序没按要求照样运行。


既然内核这样定义cmd,就肯定有方法让用户方便定义:
_IO(type,nr)	 //没有参数的命令
_IOR(type,nr,size)	 //该命令是从驱动读取数据
_IOW(type,nr,size)	 //该命令是从驱动写入数据
_IOWR(type,nr,size)	 //双向数据传输


上面的命令已经定义了方向,我们要传的是幻数(type)、序号(nr)和大小(size)。在这里szie的参数只需要填参数的类型,如int,上面的命令就会帮你检测类型的正确然后赋值sizeof(int)。


有生成cmd的命令就必有拆分cmd的命令:
_IOC_DIR(cmd)	 //从命令中提取方向
_IOC_TYPE(cmd)	 //从命令中提取幻数
_IOC_NR(cmd)	 //从命令中提取序数
_IOC_SIZE(cmd)	 //从命令中提取数据大小


越讲就越复杂了,既然讲到这,随便就讲一下预定义命令。
预定义命令是由内核来识别并且实现相应的操作,换句话说,一旦你使用了这些命令,你压根也不要指望你的驱动程序能够收到,因为内核拿掉就把它处理掉了。


分为三类:
1)可用于任何文件的命令
2)只用于普通文件的命令
3)特定文件系统类型的命令
以上来自 http://blog.chinaunix.net/uid-25014876-id-59419.html

五、高级cmd的诞生

cmd的基础知识看完了,那么就应该小试牛刀用一用,

1.首先还是看test.h

#ifndef __TEST_H
#define __TEST_H
#include <linux/ioctl.h>

#define TEST_MAGIC 'x'
#define MEM_CLEAR  _IO(TEST_MAGIC, 0)	/*清0全局内存*/
#endif


2.然后看mem_pool.h

#ifndef __MEM_POOL_H
#define __MEM_POOL_H
#include <linux/ioctl.h>

#define mem_pool_SIZE		0x2000	/*全局内存最大8K字节*/

/*mem_pool设备结构体*/
struct mem_pool_dev                                     
{                                                        
  struct cdev cdev; 		    /*cdev结构体*/                       
  unsigned char mem[mem_pool_SIZE]; /*全局内存*/        
};

#define TEST_MAGIC 'x'
#define MEM_CLEAR  _IO(TEST_MAGIC, 0)	/*清0全局内存*/

#define mem_pool_MAJOR 	250    	/*预设的mem_pool的主设备号*/


#endif


3.看我们的ioctl函数

/* ioctl设备控制函数 */
static int mem_pool_ioctl(struct inode *inodep, struct file *filp, unsigned
  int cmd, unsigned long arg)
{
	/*获得设备结构体指针*/
  struct mem_pool_dev *dev = filp->private_data;
  
  /*检验命令是否有效*/
	if(_IOC_TYPE(cmd) != TEST_MAGIC) return - EINVAL;
  switch (cmd)
  {
    case MEM_CLEAR:
      memset(dev->mem, 0, mem_pool_SIZE);      
      printk( "mem_pool is set to zero\n");
      break;

    default:
    	printk( "cmd is error \n");
      return  - EINVAL;
  }
  return 0;
}

4.最后就是应用程序检测了test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

	/*0. 包含命令定义 */
#include "test.h"  

int main()
{
	int fd = 0;
	int cmd;
	int arg = 0;
	char buf[4096] = "xbdeng";
	
	
	/*1. 打开设备文件*/
	fd = open("/dev/mem_pool",O_RDWR);
	if (fd < 0)
	{
		printf("Open Dev Mem0 Error!\n");
		return -1;
	}
	printf("%s \n",buf);
	/*2. *写文件*/
	if(write(fd,buf,sizeof(buf)) < 0 )
	{
	  printf("write error !! \n");
	}
	/*3. 注意,要读取内容,一定要记得定位*/
	lseek(fd,0,SEEK_SET);
	/*4. 读文件*/
	if(read(fd,buf,sizeof(buf)) < 0 )
	{
	  printf("read error !! \n");
	}

	printf("read fd is %s \n",buf);
	
	/*5. 调用命令MEM_CLEAR */
	printf("<--- Call MEM_CLEAR --->\n");
	cmd = MEM_CLEAR;
	if (ioctl(fd, cmd, &arg) < 0)
        {
        	printf("Call cmd MEM_CLEAR fail\n");
        	return -1;
	}
	lseek(fd,0,SEEK_SET);
	/*6. 再读文件*/
	if(read(fd,buf,sizeof(buf)) < 0 )
	{
	  printf("read error !! \n");
	}

	printf("%s \n",buf);	
	/*7. 关闭文件*/	
	close(fd);
	return 0;	
}
最后我们看结果,一模一样的



六、关于第三个参数arg

之前都没有讨论传参的问题,现在分析一下参数传递的问题

首先要明确的一点的是:在内核空间来说,任何从用户空间传进来的地址指针都要进行严格的检查,否则有可能出现意想不到的结果,就像我们之前的read和write函数一样

它是通过copy_from_user来进行数据交互的,毫无疑问,在这个函数中也有进行指针的检查。

1.首先是在mem_pool.h中

#ifndef __MEM_POOL_H
#define __MEM_POOL_H
#include <linux/ioctl.h>

#define mem_pool_SIZE		0x2000	/*全局内存最大8K字节*/

/*mem_pool设备结构体*/
struct mem_pool_dev                                     
{                                                        
  struct cdev cdev; 		    /*cdev结构体*/                       
  unsigned char mem[mem_pool_SIZE]; /*全局内存*/        
};

struct ioctl_data{
	unsigned int size;
	char buf[10];
};

#define TEST_MAGIC 'x'
#define MEM_SET  _IO(TEST_MAGIC, 0)	/*清0全局内存*/

#define mem_pool_MAJOR 	250    	/*预设的mem_pool的主设备号*/


#endif

声明了一个结构体,后面将用到

2.然后是在test.h中

#ifndef __TEST_H
#define __TEST_H
#include <linux/ioctl.h>
#define TEST_MAGIC 'x'
#define MEM_SET  _IO(TEST_MAGIC, 0)	/*清0全局内存*/
struct ioctl_data{
	unsigned int size;
	char buf[10];
};
#endif

这里我把用户空间和内核空间的东西分开了

3.在test.c中

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

	/*0. 包含命令定义 */
#include "test.h"  

struct ioctl_data mydata = {
.size = 10,
.buf  = "my name is",	
};

int main()
{
	int fd = 0;
	char buf[4096] = "  xbdeng";
	
	
	/*1. 打开设备文件*/
	fd = open("/dev/mem_pool",O_RDWR);
	if (fd < 0)
	{
		printf("Open Dev Mem0 Error!\n");
		return -1;
	}
	
	/*5. 调用命令MEM_SET */
	printf("<--- Call MEM_SET --->\n");
	if (ioctl(fd, MEM_SET, &mydata) < 0)
        {
        	printf("Call cmd MEM_SET fail\n");
        	return -1;
	}
	printf("%s \n",buf);	
	/*7. 关闭文件*/	
	close(fd);
	return 0;	
}

4.最后在底层驱动文件中

/* ioctl设备控制函数 */
static int mem_pool_ioctl(struct inode *inodep, struct file *filp, unsigned
  int cmd, unsigned long arg)
{
	/*获得设备结构体指针*/
  struct mem_pool_dev *dev = filp->private_data;
  struct ioctl_data pdata;
  printk("mem_pool_ioctl \n");
  /*检验命令是否有效*/
	if(_IOC_TYPE(cmd) != TEST_MAGIC) return - EINVAL;
  switch (cmd)
  {
    case MEM_SET:
    if (copy_from_user(&pdata, (struct ioctl_data*)arg, sizeof(struct ioctl_data))) //进行指针参数检测

    		return - EFAULT;    
      printk( "pdata.size = %d , pdata.buf = %s  \n",pdata.size,pdata.buf);
      break;


    default:
    	printk( "cmd is error \n");
      return  - EINVAL;
  }
  return 0;
}

最后看完打印信息了,上面是内核打印信息,下面是用户空间打印信息,消息就是这么传进来的。






Logo

更多推荐