如何创建一个字符设备以及open、close、write、read等函数的使用以及file_operations结构体注释请看Linux驱动之创建字符驱动——学习笔记(3)。
这篇文章单独说明一下ioctl的配置及使用。
一、知识点引入
1、指针函数选择
通过查看file_operations结构体成员可以发现配置ioctl函数指针有两个成员:
- long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
- long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
compat_ioctl这个函数是为了使32位的应用程序可以与64位的内核驱动兼容而产生的。
最主要实现的功能就是:
- 将应用层32位的数据转成64位传给64位内核驱动。
- 将内核驱动64位的数据转成32位传给32位内核驱动。
所以调用关系如下:
- 当应用层是32位程序,内核及架构是32位程序,那么驱动的unlocked_ioctl函数被调用。
- 当应用层是32位程序,内核及架构是64位程序,那么驱动的compat_ioctl函数被调用。
- 当应用层是64位程序,内核及架构是64位程序,那么驱动的unlocked_ioctl函数被调用。
简单的说,就是在32位的内核驱动中只需要实现unlocked_ioctl函数,而在64位的内核驱动中必须实现unlocked_ioctl和compat_ioctl两个函数。
2、unlocked_ioctl函数参数说明
compat_ioctl和unlocked_ioctl的参数以及意义其实是一样的,所以就只拿其中一个说明就好了。
原型: unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
- 参数(*file):struct file结构体,定义在include/linux/fs.h。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file 。
- 参数(cmd):IO操作的指令,与应用层的ioctl的第二个函数相同,下面详细说明。
- 参数(arg):传递参数的指针地址(用户空间的),就是应用层的 ioctl 的第三个参数地址。使用的话强制转 __user 指针,然后再使用 copy_from_user 或者 copy_to_user 进行读写操作。
需要详细解释的是第二个参数(cmd)。从函数原型可以看出这是个 int 类型的数据,有32个bit,是应用层与驱动之间的指令“协议”。
为了保证该指令“协议”的唯一性,在Linux中提供了一种统一格式,将32位的 int 型数据划分为四个部分:
- dir(2 bit,第 31-30 位): 表示 ioctl 命令对设备的操作类型,2个bit有4中可能,分别为 访问模式(数据传输方向),可以为无、读、写、读写,相应的宏分别为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,定义在kernel\include\uapi\asm-generic\ioctl.h中。
- size(13/14bit,第 29-16位):,ARM 架构一般为 14 bit,表示每次读写数据的长度,即 ioctl 函数第三个参数 arg 数据的长度。
- type(8 bit,第 15-8 位): 表示设备类型,可以为任意 char 型字符,其主要作用是使 ioctl 命令有唯一的设备标识,通常用英文字符 “A” ~ “Z” 或者 “a” ~ “z” 来表示。可以参考kernel/Documentation/ioctl/ioctl-number.txt文件,选取一个还没有别定义的字符进行设置。一些文献译为 “幻数” 或者 “魔数”。
- nr(8bit,第 7-0位): 命令编号,可以为任意 unsigned char 型数据,取值范围 0~255。一个命令对应一个编号,通常从 0 开始编号递增。
为了方便使用,在Linux内核定义了相应的宏来辅助生成该cmd命令。
kernel\include\uapi\asm-generic\ioctl.h
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
#ifndef __KERNEL__
#define _IOC_TYPECHECK(t) (sizeof(t))
#endif
/* used to create numbers */
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#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)))
这四个宏的使用格式分别为:
- _IO(魔数, 编号),作用在 无数据读写 的命令。
- _IOR(魔数, 编号, 变量类型) ,作用在 读数据 的命令。
- _IOW(魔数, 编号, 变量类型),作用在 写数据 的命令。
- _IOWR(魔数, 编号,变量类型),作用在 读写数据 的命令。
相对应的Linux内核也有四个宏用来解析命令
/* used to decode ioctl numbers.. */
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
这四个宏的使用格式分别为:
- _IOC_DIR(命令),解析出数据方向。
- _IOC_TYPE(命令) ,解析出设备类型。
- _IOC_NR(命令),解析出命令编号。
- _IOC_SIZE(命令),解析出数据长度。
二、开始编程
1、实现的内容及流程
(1)要实现的内容
- 接着Linux驱动之创建字符驱动——学习笔记(3)实现的字符设备驱动模板继续编程。
- 实现unlocked_ioctl函数指针。
- 实现应用层程序通过ioctl函数发送命令给驱动以及实现数据读写。
(2)实现的流程
- 通过宏定义命令类型。
- 实现unlocked_ioctl函数内容,对命令进行分类,对数据进行读写。
- 应用层程序使用ioctl函数对驱动进行操作。
(3)实现的目标
- 应用层通过ioctl函数向应用发送一个无数据操作的命令。
- 应用层通过ioctl函数向应用发送一个写入数据的命令。
- 应用层通过ioctl函数向应用发送一个读取数据的命令。
- 应用层通过ioctl函数向应用发送一个写入数据同时又读取数据的命令。
2、驱动编程内容
(1)声明宏定义及添加头文件
#include <sys/ioctl.h>
typedef enum {
IO_NONE = 0,
IO_WRITE = 1,
IO_READ = 2,
IOCTL_WRITE_READ = 3,
IO_MAX
} IOCTL_NUMBER; /* IO命令编号枚举 */
#define IOCTL_MAGIC '~' /* 定义一个魔数,随便定义个 */
#define IOCTL_NONE _IO(IOCTL_MAGIC, IO_NONE) /* 无操作命令 */
#define IOCTL_WRITE _IOW(IOCTL_MAGIC, IO_WRITE, unsigned char[128]) /* 写数据命令 */
#define IOCTL_READ _IOR(IOCTL_MAGIC, IO_READ, unsigned char[128]) /* 读数据命令 */
#define IOCTL_WRITE_READ _IOWR(IOCTL_MAGIC, IOCTL_WRITE_READ, unsigned char[128]) /* 读写数据命令 */
这是利用Linux内核定义的宏生成的ioctl命令。
(2)unlocked_ioctl函数实现
/**************************************************************************************************
** 函数名称: hello_ioctl
** 功能描述: 应用层调用ioctl函数时同步调用此函数
** 输入参数: *file:文件结构体
** cmd:IO操作的指令
** arg:传递参数的指针地址(用户空间 的)
** 输出参数: 无
** 返回参数: 成功返回0,失败返回-1
**************************************************************************************************/
long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
void __user *ptr; /* 数据地址指针 */
int arg_len; /* 数据长度 */
long ret;
ptr = (void __user*) arg; /* 强转用户空间数据地址 */
if (_IOC_TYPE(cmd) != IOCTL_MAGIC) { /* 检查魔数 */
PRINT("[KERNEL]:%s, MAGIC is not match !\n", __FUNCTION__);
return -1;
}
if (_IOC_NR(cmd) > IO_MAX) { /* 检查编号 */
PRINT("[KERNEL]:%s, number over IO_MAX !\n", __FUNCTION__);
return -1;
}
arg_len = _IOC_SIZE(cmd);
switch (cmd) {
case IOCTL_NONE: /* 无操作命令 */
PRINT("[KERNEL]:%s ------ IOCTL_NONE\n", __FUNCTION__);
ret = 0;
break;
case IOCTL_WRITE: /* 写数据命令 */
PRINT("[KERNEL]:%s ------ IOCTL_WRITE\n", __FUNCTION__);
ret = ioctl_write(ptr, arg_len);
break;
case IOCTL_READ: /* 读数据命令 */
PRINT("[KERNEL]:%s ------ IOCTL_READ\n", __FUNCTION__);
ret = ioctl_read(ptr, arg_len);
break;
case IOCTL_WRITE_READ: /* 读写数据命令 */
PRINT("[KERNEL]:%s ------ IOCTL_WRITE_READ\n", __FUNCTION__);
ret = ioctl_write_read(ptr, arg_len);
break;
default:
ret = -1;
break;
}
return ret;
}
- 参数arg为应用层传递过来的参数数据指针,属于用户空间地址,需要使用copy_to_user或copy_from_user等函数进行读写。
- 根据命令进行分类,分发到不同函数中进行数据处理。
(3)将函数指针赋值给file_operations结构体。
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
.unlocked_ioctl = hello_ioctl,
}; /* 文件操作结构体 */
(4)写命令处理函数
/**************************************************************************************************
** 函数名称: ioctl_write
** 功能描述: 写命令执行函数
** 输入参数: *ptr:用户空间数据地址
** len:数据长度
** 输出参数: 无
** 返回参数: 成功返回0,失败返回-1
**************************************************************************************************/
static long ioctl_write(void __user *ptr, int len)
{
if (0 == access_ok(VERIFY_WRITE, ptr, len)) { /* 检查用户空间内存块是否可用 */
return -1;
}
memset(test_buff, 0, sizeof(test_buff));
if (0 != copy_from_user(test_buff, ptr, sizeof(test_buff))) {
PRINT("[KERNEL]:ERROR: %s write error!\n\n", __FUNCTION__);
return -1;
}
PRINT("[KERNEL]:%s ------ test_buff = %s\n", __FUNCTION__, test_buff);
return 0;
}
(5)读命令处理函数
/**************************************************************************************************
** 函数名称: ioctl_read
** 功能描述: 读命令执行函数
** 输入参数: *ptr:用户空间数据地址
** len:数据长度
** 输出参数: 无
** 返回参数: 成功返回0,失败返回-1
**************************************************************************************************/
static long ioctl_read(void __user *ptr, int len)
{
if (0 == access_ok(VERIFY_READ, ptr, len)) { /* 检查用户空间内存块是否可用 */
return -1;
}
strcat(test_buff, " from kernel!");
PRINT("[KERNEL]:%s test_buff = %s\n", __FUNCTION__, test_buff);
if (0 != copy_to_user(ptr, test_buff, sizeof(test_buff))) {
PRINT("[KERNEL]:ERROR: %s write error!\n\n", __FUNCTION__);
return -1;
}
return 0;
}
(6)读写命令处理函数
/**************************************************************************************************
** 函数名称: ioctl_write_read
** 功能描述: 读写命令执行函数
** 输入参数: *ptr:用户空间数据地址
** len:数据长度
** 输出参数: 无
** 返回参数: 成功返回0,失败返回-1
**************************************************************************************************/
static long ioctl_write_read(void __user *ptr, int len)
{
if (0 == access_ok(VERIFY_WRITE, ptr, len)) { /* 检查用户空间内存块是否可用 */
return -1;
}
/* 读取应用层写入的数据 */
memset(test_buff, 0, sizeof(test_buff));
if (0 != copy_from_user(test_buff, ptr, sizeof(test_buff))) {
PRINT("[KERNEL]:ERROR: %s write error!\n\n", __FUNCTION__);
return -1;
}
PRINT("[KERNEL]:%s ------ test_buff = %s\n", __FUNCTION__, test_buff);
/* 向应用层传输数据 */
memset(test_buff, 0, sizeof(test_buff));
sprintf(test_buff, "%s", "It is changed by kernel driver!");
PRINT("[KERNEL]:%s test_buff = %s\n", __FUNCTION__, test_buff);
if (0 != copy_to_user(ptr, test_buff, sizeof(test_buff))) {
PRINT("[KERNEL]:ERROR: %s write error!\n\n", __FUNCTION__);
return -1;
}
return 0;
}
3、应用编码内容
(1)源码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
/*************************************************************************************************/
// 局部宏定义
/*************************************************************************************************/
#define IOCTL_MAGIC '~' /* 定义一个魔数,随便定义个 */
#define IOCTL_NONE _IO(IOCTL_MAGIC, 0) /* 无操作命令 */
#define IOCTL_WRITE _IOW(IOCTL_MAGIC, 1, unsigned char[128]) /* 写数据命令 */
#define IOCTL_READ _IOR(IOCTL_MAGIC, 2, unsigned char[128]) /* 读数据命令 */
#define IOCTL_WRITE_READ _IOWR(IOCTL_MAGIC, 3, unsigned char[128]) /* 读写数据命令 */
#define DELAY50 usleep(50 * 1000); /* 延迟50ms */
int main(int argc,char **argv)
{
int ret; /* 读写返回值 */
int handle; /* 文件标识符 */
char str[128]; /* 测试用数组 */
handle = open("/dev/hello", O_RDWR); /* 打开驱动设备 */
if (handle == -1) {
printf("open /dev/hello fail!\n");
return 0;
}
DELAY50
ret = ioctl(handle, IOCTL_NONE, str); /* 发送无数据命令给驱动 */
DELAY50
printf("[user]:IOCTL_NONE, ret = %d\n", ret); /* 无数据命令执行结果 */
DELAY50
memset(str, 0, sizeof(str));
sprintf(str, "%s", "ABCDEFG"); /* 填写测试的内容 */
ret = ioctl(handle, IOCTL_WRITE, str); /* 发送带数据的命令给驱动 */
DELAY50
printf("[user]:IOCTL_WRITE, ret = %d, str = %s\n", ret, str); /* 写结果 */
DELAY50
memset(str, 0, sizeof(str));
ret = ioctl(handle, IOCTL_READ, str); /* 发送读数据的命令给驱动 */
DELAY50
printf("[user]:IOCTL_READ, ret = %d, str = %s\n", ret, str); /* 读结果 */
DELAY50
memset(str, 0, sizeof(str));
sprintf(str, "%s", "QWERT123456"); /* 填写测试的内容 */
ret = ioctl(handle, IOCTL_WRITE_READ, str); /* 发送带数据的命令给驱动,并从驱动读取数据出来 */
DELAY50
printf("[user]:IOCTL_WRITE_READ, ret = %d, str = %s\n", ret, str); /* 读结果 */
DELAY50
close(handle);
return 0;
}
应用层需要和驱动中相同的命令宏定义,可以写在头文件中引用,也可以分开定义。
三、测试
加载驱动hello.ko后执行hello应用层程序,结果如下。
根据结果可以看出在应用层分别调用了ioctl的四个命令,进行数据的读写操作,驱动都可以收到数据或者修改再传回。
root@imx6qsabresd:/tmp# ./hello
[24853.936864] [KERNEL]:hello_open ------
[24853.990864] [KERNEL]:hello_ioctl ------ IOCTL_NONE
[user]:IOCTL_NONE, ret = 0
[24854.096308] [KERNEL]:hello_ioctl ------ IOCTL_WRITE
[24854.101198] [KERNEL]:ioctl_write ------ test_buff = ABCDEFG
[user]:IOCTL_WRITE, ret = 0, str = ABCDEFG
[24854.207285] [KERNEL]:hello_ioctl ------ IOCTL_READ
[24854.212089] [KERNEL]:ioctl_read test_buff = ABCDEFG from kernel!
[user]:IOCTL_READ, ret = 0, str = ABCDEFG from kernel!
[24854.318376] [KERNEL]:hello_ioctl ------ IOCTL_WRITE_READ
[24854.323721] [KERNEL]:ioctl_write_read ------ test_buff = QWERT123456
[24854.330081] [KERNEL]:ioctl_write_read test_buff = It is changed by kernel driver!
[user]:IOCTL_WRITE_READ, ret = 0, str = It is changed by kernel driver!
[24854.437848] [KERNEL]:hello_release ------