ioctl 是设备驱动程序中设备控制接口函数,一个字符设备驱动通常会实现设备打开、关闭、读、写等功能,在一些需要细分的情境下,如果需要扩展新的功能,通常以增设 ioctl() 命令的方式实现。
在文件 I/O 中,ioctl 扮演着重要角色,本文将以驱动开发为侧重点,从用户空间到内核空间纵向分析 ioctl 函数。
ioctl() 函数执行成功时返回 0,失败则返回 -1 并设置全局变量 errorno 值,如下:
EBADF d is not a valid descriptor. EFAULT argp references an inaccessible memory area. EINVAL Request or argp is not valid. ENOTTY d is not associated with a character special device. ENOTTY The specified request does not apply to the kind of object that the descriptor d references.因此,在用户空间使用 ioctl 时,可以做如下的出错判断以及处理:
int ret; ret = ioctl(fd, MYCMD); if (ret == -1) { printf("ioctl: %s\n", strerror(errno)); }在实际应用中,ioctl 最常见的 errorno 值为 ENOTTY(error not a typewriter),顾名思义,即第一个参数 fd 指向的不是一个字符设备,不支持 ioctl 操作,这时候应该检查前面的 open 函数是否出错或者设备路径是否正确
在新版内核中, 与 取代了 。unlocked_ioctl,顾名思义,应该在无大内核锁(BKL)的情况下调用;compat_ioctl,compat 全称 compatible(兼容的),主要目的是为 64 位系统提供 32 位 ioctl 的兼容方法,也是在无大内核锁的情况下调用。
在《Linux Kernel Development》中对两种 ioctl 方法有详细的解说。
在字符设备驱动开发中,一般情况下只要实现 unlocked_ioctl 函数即可,因为在 vfs 层的代码是直接调用 unlocked_ioctl 函数
// fs/ioctl.c static long vfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int error = -ENOTTY; if (!filp->f_op || !filp->f_op->unlocked_ioctl) goto out; error = filp->f_op->unlocked_ioctl(filp, cmd, arg); if (error == -ENOIOCTLCMD) { error = -ENOTTY; } out: return error; }前文提到 ioctl 方法第二个参数 cmd 为用户与驱动的 “协议”,理论上可以为任意 int 型数据,可以为 0、1、2、3……,但是为了确保该 “协议” 的唯一性,ioctl 命令应该使用更科学严谨的方法赋值,在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 int 型数据划分为四个位段,如下图所示:
在内核中,提供了宏接口以生成上述格式的 ioctl 命令:
// include/uapi/asm-generic/ioctl.h #define _IOC(dir,type,nr,size) \ (((dir) << _IOC_DIRSHIFT) | \ ((type) << _IOC_TYPESHIFT) | \ ((nr) << _IOC_NRSHIFT) | \ ((size) << _IOC_SIZESHIFT)) dir(direction),ioctl 命令访问模式(数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据;type(device type),设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如 ‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识;nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;通常而言,为了方便会使用宏 _IOC() 衍生的接口来直接定义 ioctl 命令:
// include/uapi/asm-generic/ioctl.h /* 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: 定义不带参数的 ioctl 命令 _IOW: 定义带写参数的 ioctl 命令(copy_from_user) _IOR: 定义带读参数的ioctl命令(copy_to_user) _IOWR: 定义带读写参数的 ioctl 命令同时,内核还提供了反向解析 ioctl 命令的宏接口:
// include/uapi/asm-generic/ioctl.h /* 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)本例假设一个带寄存器的设备,设计了一个 ioctl 接口实现设备初始化、读写寄存器等功能。在本例中,为了携带更多的数据,ioctl 的第三个可变参数为指针类型,指向自定义的结构体 struct msg。
1、ioctl-test.h,用户空间和内核空间共用的头文件,包含 ioctl 命令及相关宏定义,可以理解为一份 “协议” 文件,代码如下:
// ioctl-test.h #ifndef __IOCTL_TEST_H__ #define __IOCTL_TEST_H__ #include <linux/ioctl.h> // 内核空间 // #include <sys/ioctl.h> // 用户空间 /* 定义设备类型 */ #define IOC_MAGIC 'c' /* 初始化设备 */ #define IOCINIT _IO(IOC_MAGIC, 0) /* 读寄存器 */ #define IOCGREG _IOW(IOC_MAGIC, 1, int) /* 写寄存器 */ #define IOCWREG _IOR(IOC_MAGIC, 2, int) #define IOC_MAXNR 3 struct msg { int addr; unsigned int data; }; #endif2、ioctl-test-driver.c,字符设备驱动,实现了unlocked_ioctl 接口,根据上层用户的 cmd 执行对应的操作(初始化设备、读寄存器、写寄存器)。在接收上层 cmd 之前应该对其进行充分的检查,流程及具体代码实现如下:
// ioctl-test-driver.c ...... static const struct file_operations fops = { .owner = THIS_MODULE, .open = test_open, .release = test_close, .read = test_read, .write = test_write, .unlocked_ioctl = test_ioctl, }; ...... static long test_ioctl(struct file *file, unsigned int cmd, \ unsigned long arg) { //printk("[%s]\n", __func__); int ret; struct msg my_msg; /* 检查设备类型 */ if (_IOC_TYPE(cmd) != IOC_MAGIC) { pr_err("[%s] command type [%c] error!\n", \ __func__, _IOC_TYPE(cmd)); return -ENOTTY; } /* 检查序数 */ if (_IOC_NR(cmd) > IOC_MAXNR) { pr_err("[%s] command numer [%d] exceeded!\n", __func__, _IOC_NR(cmd)); return -ENOTTY; } /* 检查访问模式 */ if (_IOC_DIR(cmd) & _IOC_READ) ret= !access_ok(VERIFY_WRITE, (void __user *)arg, \ _IOC_SIZE(cmd)); else if (_IOC_DIR(cmd) & _IOC_WRITE) ret= !access_ok(VERIFY_READ, (void __user *)arg, \ _IOC_SIZE(cmd)); if (ret) return -EFAULT; switch(cmd) { /* 初始化设备 */ case IOCINIT: init(); break; /* 读寄存器 */ case IOCGREG: ret = copy_from_user(&msg, \ (struct msg __user *)arg, sizeof(my_msg)); if (ret) return -EFAULT; msg->data = read_reg(msg->addr); ret = copy_to_user((struct msg __user *)arg, \ &msg, sizeof(my_msg)); if (ret) return -EFAULT; break; /* 写寄存器 */ case IOCWREG: ret = copy_from_user(&msg, \ (struct msg __user *)arg, sizeof(my_msg)); if (ret) return -EFAULT; write_reg(msg->addr, msg->data); break; default: return -ENOTTY; } return 0; }3、ioctl-test.c,运行在用户空间的测试程序:
// ioctl-test.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include "ioctl-test.h" int main(int argc, char **argv) { int fd; int ret; struct msg my_msg; fd = open("/dev/ioctl-test", O_RDWR); if (fd < 0) { perror("open"); exit(-2); } /* 初始化设备 */ ret = ioctl(fd, IOCINIT); if (ret) { perror("ioctl init:"); exit(-3); } /* 往寄存器0x01写入数据0xef */ memset(&my_msg, 0, sizeof(my_msg)); my_msg.addr = 0x01; my_msg.data = 0xef; ret = ioctl(fd, IOCWREG, &my_msg); if (ret) { perror("ioctl read:"); exit(-4); } /* 读寄存器0x01 */ memset(&my_msg, 0, sizeof(my_msg)); my_msg.addr = 0x01; ret = ioctl(fd, IOCGREG, &my_msg); if (ret) { perror("ioctl write"); exit(-5); } printf("read: %#x\n", my_msg.data); return 0; }