Linux 设备驱动学习笔记 - 高级字符驱动操作篇 - Note.3[ioctl 接口]

Linux 设备驱动学习笔记 - 高级字符驱动操作篇 - Note.3[ioctl 接口]

  • LINUX DEVICE DRIVERS,3RD EDITION
  • 里面很多地方都摘取自 Linux 内核源码

在应用层与内核层之间传输数据时, 例如如果仅靠 1 和 0 来控制 LED 灯的亮灭等操作, 存在着众多不稳定因素一个字节的数据有可能被环境所产生的干扰而改变或者丢失, 造成一系列的误操作或无响应, 也不利于代码的可阅读性和可维护性linux内核专门针对小数据(一个字节),提供了专门的数据交互接口ioctl --> input output control --> 输入输出控制 --> ioctl为寄存器或者32位gpio口提供专门交互使用的接口最终产生的0或者1是不混乱的,是唯一识别一个设备的

6.1 ioctl 接口

在用户空间, ioctl 系统调用有下面的原型:

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

原型中的点不表示一个变数目的参数, 而是一个单个可选的参数, 传统上标识为 char *argp.
这些点在那里只是为了阻止在编译时的类型检查. 第 3 个参数的实际特点依赖所发出的特定的控制命令( 第 2 个参数 ).
一些命令不用参数, 一些用一个整数值, 以及一些使用指向其他数据的指针. 使用一个指针是传递任意数据到 ioctl 调用的方法;
设备接着可与用户空间交换任何数量的数据.

每个 ioctl 命令, 基本上, 是一个单独的, 常常无文档的系统调用, 并且没有方法以任何类型的全面的方式核查这些调用.
也难于使非结构化的 ioctl 参数在所有系统上一致工作; 例如, 考虑运行在 32-位模式的一个用户进程的 64-位 系统.
结果, 有很大的压力来实现混杂的控制操作, 只通过任何其他的方法.
可能的选择包括嵌入命令到数据流或者使用虚拟文件系统, 要么是 sysfs 要么是设备特定的文件系统.

在内核空间的 file_operations 结构体中的原型:

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

ioctl 驱动方法有和用户空间版本不同的原型:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd,	unsigned long arg);
		|-->inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. 
		|-->cmd 参数从用户那里不改变地传下来
		|-->可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针. 
			如果调用程序不传递第 3 个参数被驱动操作收到的 arg 值是无定义的. 
			因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.

如果你可能想到的, 大部分 ioctl 实现包括一个大的 switch 语句来根据 cmd 参数, 选择正确的做法. 不同的命令有不同的数值,
它们常常被给予符号名来简化编码. 符号名通过一个预处理定义来安排. 定制的驱动常常声明这样的符号在它们的头文件中.

6.1.1 选择 ioctl 命令

在为 ioctl 编写代码之前, 你需要选择对应命令的数字. ioctl 命令数字应当在这个系统是唯一的,
为了阻止向错误的设备发出正确的命令而引起的错误.
一个 FIFO 或者一个音频设备. 如果这样的 ioctl 号是唯一的, 这个应用程序得到一个 EINVAL 错误而不是继续做不应当做的事情.

定义 ioctl 命令号的正确方法使用 4 个位段, 它们有下列的含义. 这个列表中介绍的新符号定义在 <linux/ioctl.h>.

使用一个 32 位的 unsigned int cmd 来控制
	bits    meaning
	31-30  00 - no parameters: uses _IO macro   --> 指的是可变参数没有
	10 - read: _IOR                      --> 读IO
	01 - write: _IOW                     --> 写IO
	11 - read/write: _IOWR               --> 读写IO
	
	29-16  size of arguments                    --> 可变参数所指向内存的大小
	
	15-8   ascii character supposedly           --> 具体操作掩码的类型 --> 256种类型
	unique to each driver
	
	7-0    function #                           --> 具体操作掩码的功能 --> 256种功能
	通过不同位发生变化,参数不同操作掩码

ioctl 在头文件中的解释
/* ioctl command encoding: 32 bits total, command in lower 16 bits,
 * size of the parameter structure in the lower 14 bits of the
 * upper 16 bits.
 * Encoding the size of the parameter structure in the ioctl request
 * The highest 2 bits are reserved for indicating the ``access mode''.
 * NOTE: This limits the max parameter size to 16kB -1 !
 */

#define _IOC_NRBITS     8
#define _IOC_TYPEBITS   8
#define _IOC_SIZEBITS   14
#define _IOC_DIRBITS    2

#define _IOC_NRMASK     ((1 << _IOC_NRBITS)-1)
#define _IOC_TYPEMASK   ((1 << _IOC_TYPEBITS)-1)
#define _IOC_SIZEMASK   ((1 << _IOC_SIZEBITS)-1)
#define _IOC_DIRMASK    ((1 << _IOC_DIRBITS)-1)

#define _IOC_NRSHIFT    0
#define _IOC_TYPESHIFT  (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT  (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT   (_IOC_SIZESHIFT+_IOC_SIZEBITS)

/*
* Direction bits.
*/
#define _IOC_NONE       0U
#define _IOC_WRITE      1U
#define _IOC_READ       2U

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

/* used to create numbers */
#define _IO(type,nr)	    _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)      _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size)      _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size)     _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

/* 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)

/* ...and for the drivers/sound files... */

#define IOC_IN	  (_IOC_WRITE << _IOC_DIRSHIFT)
#define IOC_OUT	 (_IOC_READ << _IOC_DIRSHIFT)
#define IOC_INOUT       ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
#define IOCSIZE_MASK    (_IOC_SIZEMASK << _IOC_SIZESHIFT)
#define IOCSIZE_SHIFT   (_IOC_SIZESHIFT)
例子:
#define LED_ON _IO('k',0)		--> _IO('k',0) --> _IOC(_IOC_NONE,('k'),(0),0)
#define LED_ON_R _IOR('k',1,int) 	--> _IOC(_IOC_READ,'k',1,sizeof(int))
#define LED_ON_W _IOW('k',1,int) 	--> _IOC(_IOC_WRITE,'k',1,sizeof(int))
			
.unlocked_ioctl = myioctl,  --> 加到file_operations结构体中
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);  --> 应用层的ioctl函数
	|--> struct file *  : struct file 结构体指针
	|--> unsigned int --> 操作掩码 (unsigned long request)
	|--> unsigned long --> 可变参数

6.1.3 预定义的命令

预定义命令分为 3 类:

  • 可对任何文件发出的(常规, 设备, FIFO, 或者 socket) 的那些.

  • 只对常规文件发出的那些.

  • 对文件系统类型特殊的那些.

下列 ioctl 命令是预定义给任何文件, 包括设备特殊的文件:
(1)FIOCLEX
设置 close-on-exec 标志(File IOctl Close on EXec). 设置这个标志使文件描述符被关闭, 当调用进程执行一个新程序时.

(2)FIONCLEX
清除 close-no-exec 标志(File IOctl Not CLose on EXec). 这个命令恢复普通文件行为, 复原上面 FIOCLEX 所做的. FIOASYNC 为这个文件设置或者复位异步通知(如同在本章中"异步通知"一节中讨论的). 注意直到 Linux 2.2.4 版本的内核不正确地使用这个命令来修改 O_SYNC 标志. 因为两个动作都可通过 fcntl 来完成, 没有人真正使用 FIOASYNC 命令, 它在这里出现只是为了完整性.

(3)FIOQSIZE
这个命令返回一个文件或者目录的大小; 当用作一个设备文件, 但是, 它返回一个 ENOTTY 错误.

(4)FIONBIO
“File IOctl Non-Blocking I/O”(在"阻塞和非阻塞操作"一节中描述). 这个调用修改在 filp->f_flags 中的 _NONBLOCK 标志.

给这个系统调用的第 3 个参数用作指示是否这个标志被置位或者清除. (我们将在本章看到这个标志的角色).
注意常用的改变这个标志的方法是使用 fcntl 系统调用, 使用 F_SETFL 命令.

6.1.4 使用 ioctl 参数

驱动的 ioctl 代码, 如何使用额外的 unsigned long arg 参数.
如果它是一个整数, 就容易: 它可以直接使用. 如果它是一个指针, 必须小心些.当用一个指针引用用户空间, 我们必须确保用户地址是有效的.
试图存取一个没验证过的用户提供的指针可能导致不正确的行为, 一个内核 oops, 系统崩溃, 或者安全问题.
它是驱动的责任来对每个它使用的用户空间地址进行正确的检查, 并且返回一个错误如果它是无效的.

在第 3 章, 我们看了 copy_from_user 和 copy_to_user 函数, 它们可用来安全地移动数据到和从用户空间. 这些函数也可用在 ioctl 方法中, 但是 ioctl 调用常常包含小数据项, 可通过其他方法更有效地操作. 开始, 地址校验(不传送数据)由函数 access_ok 实现, 它定义在 <asm/uaccess.h>:

int access_ok(int type, const void *addr, unsigned long size);

第一个参数应当是 VERIFY_READ 或者 VERIFY_WRITE, 依据这个要进行的动作是否是读用户空间内存区或者写它.
addr 参数持有一个用户空间地址, size 是一个字节量.

如果 ioctl 需要从用户空间读一个整数, size 是 sizeof(int). 如果你需要读和写给定地址, 使用 VERIFY_WRITE, 因为它是 VERIRY_READ 的超集.不象大部分的内核函数, access_ok 返回一个布尔值: 1 是成功(存取没问题)和 0 是失败(存取有问题). 如果它返回假, 驱动应当返回 -EFAULT 给调用者.

  • 注意:
    首先, 它不做校验内存存取的完整工作; 它只检查看这个内存引用是在这个进程有合理权限的内存范围中. 特别地, access_ok 确保这个地址不指向内核空间内存.
    第二, 大部分驱动代码不需要真正调用 access_ok. 后面描述的内存存取函数为你负责这个.
scull 源码利用了 ioclt 号中的位段来检查参数,switch 之前:
int err = 0, tmp;
int retval = 0;

/*
* extract the type and number bitfields, and don't decode
* wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok()
*/
if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC){
    return -ENOTTY;
}
if (_IOC_NR(cmd) > SCULL_IOC_MAXNR){
    return -ENOTTY;
}

/*
	* the direction is a bitmask, and VERIFY_WRITE catches R/W
	* transfers. `Type' is user-oriented, while
	* access_ok is kernel-oriented, so the concept of "read" and
	* "write" is reversed
	*/
if (_IOC_DIR(cmd) & _IOC_READ){
    err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
}
else if (_IOC_DIR(cmd) & _IOC_WRITE){
    err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
}
if (err){
    return -EFAULT;
}

在调用 access_ok 之后, 驱动可安全地进行真正的传输. 加上 copy_from_user 和copy_to_user_ 函数,
程序员可利用一组为被最多使用的数据大小(1, 2, 4, 和 8 字节)而优化过的函数. 这些函数在下面列表中描述,
它们定义在 <asm/uaccess.h>:

put_user(datum, ptr)
__put_user(datum, ptr)

这些宏定义写 datum 到用户空间; 它们相对快, 并且应当被调用来代替 copy_to_user 无论何时要传送单个值时.这些宏已被编写来允许传递任何类型的指针到 put_user, 只要它是一个用户空间地址. 传送的数据大小依赖 prt 参数的类型, 并且在编译时使用 sizeof 和 typeof 等编译器内建宏确定. 结果是, 如果 prt 是一个 char 指针, 传送一个字节, 以及对于 2, 4, 和 可能的 8 字节. put_user 检查来确保这个进程能够写入给定的内存地址. 它在成功时返回 0, 并且在错误时返回 -EFAULT.

__put_user 进行更少的检查(它不调用 access_ok), 但是仍然能够失败如果被指向的内存对用户是不可写的. __put_user 应当只用在内存区已经用 access_ok 检查过的时候.作为一个通用的规则, 当你实现一个 read 方法时, 调用 __put_user 来节省几个周期, 或者当你拷贝几个项时, 因此, 在第一次数据传送之前调用 access_ok 一次, 如同上面 ioctl 所示.

get_user(local, ptr)
__get_user(local, ptr)

这些宏定义用来从用户空间接收单个数据. 它们象 put_user 和 __put_user, 但是在相反方向传递数据. 获取的值存储于本地变量 local;
返回值指出这个操作是否成功. 再次, __get_user 应当只用在已经使用 access_ok 校验过的地址.

6.1.5 兼容性和受限操作

内核在许可权管理上排他地使用能力, 并且输出 2 个系统调用 capget 和 capset, 来允许它们被从用户空间管理.
全部能力可在 <linux/capability.h> 中找到. 这些是对系统唯一可用的能力; 对于驱动作者或者系统管理员,
不可能不修改内核源码而来定义新的. 设备驱动编写者可能感兴趣的这些能力的一个子集, 包括下面:
(1)CAP_DAC_OVERRIDE
这个能力来推翻在文件和目录上的存取的限制(数据存取控制, 或者 DAC).

(2)CAP_NET_ADMIN
进行网络管理任务的能力, 包括那些能够影响网络接口的.

(3)CAP_SYS_MODULE
加载或去除内核模块的能力.

(4)CAP_SYS_RAWIO
进行 “raw” I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯.

(5)CAP_SYS_ADMIN
一个捕获-全部的能力, 提供对许多系统管理操作的存取.

(6)CAP_SYS_TTY_CONFIG
进行 tty 配置任务的能力

6.1.6 ioctl 命令的实现

ioctl 的 scull 实现只传递设备的配置参数, 并且象下面这样容易
switch(cmd)
{
    case SCULL_IOCRESET:
        scull_quantum = SCULL_QUANTUM;
        scull_qset = SCULL_QSET;
        break;
    case SCULL_IOCSQUANTUM: /* Set: arg points to the value */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        retval = __get_user(scull_quantum, (int __user *)arg);
        break;
    case SCULL_IOCTQUANTUM: /* Tell: arg is the value */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        scull_quantum = arg;
        break;
    case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */
        retval = __put_user(scull_quantum, (int __user *)arg);
        break;
    case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */
        return scull_quantum;
    case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        tmp = scull_quantum;
        retval = __get_user(scull_quantum, (int __user *)arg);
        if (retval == 0)
            retval = __put_user(tmp, (int __user *)arg);
        break;
    case SCULL_IOCHQUANTUM: /* sHift: like Tell + Query */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        tmp = scull_quantum;
        scull_quantum = arg;
        return tmp;
    default: /* redundant, as cmd was checked against MAXNR */
        return -ENOTTY;
}
return retval;

从调用者的观点看(即从用户空间), 这 6 种传递和接收参数的方法看来如下:

int quantum;
ioctl(fd,SCULL_IOCSQUANTUM, &quantum); /* Set by pointer */
ioctl(fd,SCULL_IOCTQUANTUM, quantum); /* Set by value */
ioctl(fd,SCULL_IOCGQUANTUM, &quantum); /* Get by pointer */
quantum = ioctl(fd,SCULL_IOCQQUANTUM); /* Get by return value */
ioctl(fd,SCULL_IOCXQUANTUM, &quantum); /* Exchange by pointer */

quantum = ioctl(fd,SCULL_IOCHQUANTUM, quantum); /* Exchange by value */
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值