Linux设备驱动程序 之 ioctl

xiaohouye 2019-10-29

ioctl

除了读取和写入设备之外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制,通常这种需求使用ioctl方法支持,该方法实现了同名的系统调用;

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

1 int ioctl(int d, int request, ...);

原型中的可变参数不是数目不定的一串参数,而只是一个可选参数;可选参数的具体格式依赖于控制命令,也就是第二个参数;某些控制命令不需要参数,某些需要一个整数参数,某些需要一个指针参数;使用指针参数可以向ioctl传递任意数据,这样设备可以与用户空间交换任意数量的数据;

ioctl系统调用内核中的定义如下:

1 SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)

ioctl在file_operations中的函数原型如下:

1 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
2 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

大多数ioctl的实现都包含了一个switch语句来根据cmd参数选择对应的操作;不同的命令被赋予不同的数值,为了简化代码,通常会在代码中使用符号名代替数值,这些符号名由c语言的预处理语句定义,订制设备驱动程序通常会在它们的头文件中声明这些符号;

选择ioctl命令

ioctl命令号码定义使用了4个为字段,其含义如下:

type:幻数;选择一个号码,并在整个驱动程序中使用这个号码;这个字段是8位宽(_IOC_TYPEBITS);通常使用一个英文字母;比如#define MY_IOC_MAGIC ‘k’;需要注意避免命令号冲突;

number:序数(顺序编号);是8位宽(_IOC_NRBITS);通常从0开始顺序编号;

direction:如果命令涉及到数据的传输,则该位字段定义数据传输的方向;可以使用的值包括_IOC_NONE(没有数据传输)、_IOC_READ、_IOC_WRITE、_IOC_READ|_IOC_WRITE(双向传输);数据传输是从应用程序的角度来看的,也就是说,IOC_READ意味着从设备中读取数据,所以驱动程序必须向用户空间写入数据;注意,该字段是一个位掩码,因此可以使用逻辑AND操作从中分解出_IOC_READ和_IOC_WRITE;

size:所涉及的用户数据大小;这个字段的宽度与体系结构有关,通常是13或者14位,具体可以通过宏_IOC_SIZEBIT找到针对特定体系结构的具体数值;系统并不强制只用这个字段,也就是说,内核不会检查这个字段;对该字段的正确使用可以帮助我们检测用户程序的错误,并且如果我们从不改变相关的数据项大小的话,这个位字段哈可以帮我们实现向后的兼容性;但是,如果需要很大的数据传输,则可以忽略这个位字段;

用于构造命令编号的宏如下,其中type和number位字段通过参数传入,而size位字段通过对datatype参数取sizeof获取的;

_IO(type,nr)用于构造无参数的命令编号;

_IOR(type,nr,size)用于构造从驱动程序读取数据的命令编号;

_IOW(type,nr,size)用于构造用用户空间写入数据的命令;

_IOWR(type,nr,size)用于双向传输;

用于解开位字段的宏如下:

_IOC_DIR(nr)、_IOC_TYPE(nr)、_IOC_NR(nr)、_IOC_SIZE(nr);

返回值

ioctl实现通常就是一个基于命令号的switch语句;当命令号不合法时,有些内核函数返回-ENVAL(非法参数);POSIX标准规定,应该返回-ENOTTY,C库将这个错误码解释为不合适的设备ioctl;但是普遍做法是返回-EINVAL;

使用ioctl参数

ioctl的附加参数,如果是个整数,直接使用就可以了,如果是个指针,就需要注意一些问题;

当用一个指针指向用户空间时,必须确保指向的用户空间是合法的;对未验证的用户空间指针的访问,可能导致内核oops,系统崩溃或者安全问题;驱动程序应该负责对每个用到的用户空间地址做适当的检查,如果是非法地址则应该返回一个错误;

copy_from_user和copy_to_user可以安全的与用户空间交换数据,这两个函数也可以在ioctl中使用,但是因为ioctl通常涉及较小的数据项,因此可以通过其他方法更有效的操作;为此,我们首先要通过access_ok函数验证地址,而不传输数据,函数声明如下:<asm-generic/uaccess.h>

1 #define access_ok(type, addr, size) __access_ok((unsigned long)(addr),(size))

第一个参数type应是VERIFY_READ或者VERIFY_RIWTE,取决于要执行的动作是读取还是写入用户空间内存区;addr参数是一个用户空间地址,size是字节数,如ioctl要从用户空间读取一个整数,则size是sizeof(int);如果在指定地址处既要读取又要写入,则应该用VERIFY_WRITE,因为它是VERIFY_READ的超集;

与大多数函数不同,access_ok返回1标识成功,0标识失败;如果返回失败,则通常需要返回-EFAULT给调用者;

关于该函数,需要注意两点:第一,它并没有完成验证内存的全部工作,而只是检查了所引用的内存是否位于进程有对应访问权限的区域中,特别是要确保访问的地址没有指向内核空间的内存区;第二,大多数驱动程序代码中都不需要真正调用access_ok,因为内存管理程序会处理它;

数据传送

除了copy_from_user和copy_to_user函数之外,内核还提供了常用的数据大小为1,2,4,8字节优化过的一组函数;

#define put_user(x, ptr)
#define __put_user(x, ptr)

#define get_user(x, ptr)
#define __get_user(x, ptr)

其中put_user函数把数据x写到用户空间;它们相对比较快,当需要传递单个数据时,使用这些宏而不是用copy_to_user;由于这些宏在展开时不做类型检查,所以可以传递给put_user任意类型的指针,只要是个用户空间地址就行;传递数据大小依赖于ptr参数的类型,在编译时由编译器的内建指令sizeof和typeof确定;总之,若ptr是一个字符指针,就传递1个字节,2,4,8字节的情况类似;

put_user已经进行了检查确保进程可以写入指定的内存地址,并在成功时返回0,出错是返回-EFAULT;__put_user则做的检查少些,它不调用access_ok,但是如果地址指向的用户不能写入内存,会出现操作失败,因为__put_user要在已经使用过access_ok检查后使用;

get_user从用户空间接收一个数据,接收的数值保存在局部变量x中,返回值指明了操作是否成功;通用,__get_user应该在操作地址已经被access_ok检查通过后使用;

如果是不满足上述的传递大小的数值,则必须使用copy_to_user和copy_from_user;

权能与受限操作

权限相关的定义在<uapi/linux/capability.h>中,其中包含了系统能够理解的所有权能;不修改内核源码,驱动程序无法定义新的权能;对驱动程序开发来讲有意义的权能如下:

CAP_DAC_OVERRIDE

越过文件或者目录的访问限制的能力;

CAP_NET_ADMIN

执行网络管理任务的能力,包括哪些能影响网络接口的任务;

CAP_SYS_MODULE

载入或者卸载内核模块的能力

CAP_SYS_RAWIO

执行裸IO操作的能力,例如,访问设备端口或者直接与USB设备通信;

CAP_SYS_ADMIN

截获的能力,它提供了访问许多系统管理操作的途径;

在执行某项特权操作之前,需要检查调用进程是否有合适的权能;如果不进行这类检查,将导致用户进程执行非授权操作,从而影响系统稳定性和安全性;权能检查通过capable实现;在<linux/capability.h>中;

1 bool capable(int cap);

相关推荐

zhengkai00 / 0评论 2010-12-03
linux爱好者 / 0评论 2009-08-17