首先实现ioctl系统调用,它是用来设备控制的公共接口。
然后,内核态与用户态保持同步。
掌握进程休眠和唤醒、实现阻塞I/O,以及在设备可读取或写入时通知用户空间。
最后,在驱动程序中实现设备访问策略。
ioctl
驱动程序通过ioctl执行各种类型的硬件控制。
1.在用户空间,ioctl系统调用具有如下原型:
int ioctl(int fd,unsigned long cmd,...);
参数 | 描述 |
---|---|
fd | 文件描述符 |
cmd | 控制命令 |
… | 表示可变数目的参数表,在实际使用中是一个可选参数,习惯上用char* argp定义。 |
每个ioctl命令就是一个独立的系统调用,而且是非公开的。
2.驱动程序中
int (*ioctl) (struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg);
参数 | 描述 |
---|---|
inode | 对应于应用程序传递的文件描述符fd,这与传给open方法的参数一致 |
filp | 对应于应用程序传递的文件描述符fd,这与传给open方法的参数一致 |
cmd | 由用户空间直接不经修改的传递给驱动程序 |
arg | 可选 以unsigned long的形式传递给驱动程序 |
选择ioctl命令
#include/asm/ioctl.h和Documentation/ioctl-number.txt
ioctl命令号定义:type、number、direction、size
参数 | 描述 | 位数 |
---|---|---|
type | 幻数。选择一个号码,并在整个驱动程序中使用这个号码 | 8bit(_IOC_TYPEBITS) |
number | 序数。顺序编号 | 8bits(_IOC_NRBITS) |
direction | 涉及数据传输时该字段定义数据传输的方向,涉及内容包括_IOC_NONE(无数据传输),_IOC_READ(从设备中读),_IOC_WRITE,_IOC_READ|_IOC_WRITE(双向数据传输)。数据传输是从应用程序的角度来看的,_IOC_READ意味着从设备中读取数据,所以应用程序必须向用户空间写入数据 | |
size | 表示所涉及的用户数据大小 | 通常为13位或是14位,具体可通过宏_IOC_SIZEBITS找到针对特定体系结构的具体数值。内核不会检查这个位字段,对该字段的检查可以帮助我们检测用户空间的错误。 |
<asm/ioctl.h>定义了一些构造命令编号的宏:
_IOR(type,nr,datetype) 构造从驱动程序中读取数据的命令
_IO(type,nr) 用于构造无参数的命令编号
_IOW(type,nr,datetype) 用于写入数据的命令编号
_IOWR(type,nr,datatype) 双向传输
type,number通过参数传入,size通过对datatype参数取sizeof获取
还有一些解开位字段的宏:_IOC_DIR(nr),_IOC_TYPE(nr),_IOC_NR(nr),_IOC_SIZE(nr)
_IOC_NR : 读取基数域值 (bit0~ bit7)
_IOC_TYPE : 读取魔数域值 (bit8 ~ bit15)
_IOC_SIZE : 读取数据大小域值 (bit16 ~ bit29)
_IOC_DIR : 获取读写属性域值 (bit30 ~ bit31)
scull的scull.h中有一些关于ioctl命令定义,这些命令是用来设置或获取驱动程序的配置参数:
#define SCULL_IOC_MAGIC 'k'
/* Please use a different 8-bit number in your code */
#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
* S means "Set" through a ptr,
* T means "Tell" directly with the argument value
* G means "Get": reply by setting through a pointer
* Q means "Query": response is on the return value
* X means "eXchange": switch G and S atomically
* H means "sHift": switch T and Q atomically
*/
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)
通过指针和显式的数值来实现整数参数的传递。
通过指针和显式的数值来实现返回值。
返回值 | 描述 |
---|---|
返回值为正 | 正常工作 |
返回值为负 | 错误,设置用户空间的errno变量 |
返回值
对非法的ioctl命令一般会返回-EINVAL
预定义命令
在使用ioctl命令编号时,一定要避免与预定义命令重复,否则,命令冲突,设备不会响应。
下列ioctl命令对任何文件(包括设备特定文件)都是预定义的:
预定义命令 | 描述 |
---|---|
FIOCTLX | 设置执行时关闭标志 |
FIONCLEX | 清除执行时关闭标志 |
FIOASYNC | 设置或复位文件异步通知 |
FIOQSIZE | 返回文件或目录大小,用于设备文件时会导致ENOTTY错误的返回 |
FIONBIO | 文件非阻塞型IO,file ioctl non-blocking i/o |
使用ioctl参数
参数类型 | 描述 |
---|---|
arg整数 | 直接用 |
arg指针 | 需检测后才能用 |
使用指针,首先得保证指针指向的地址(用户空间)合法。因此,在使用这个指针之前,我们应该使用access_ok函数来验证地址的合法性:
#include<asm/uaccess.h>
int access_ok(int type,const void *addr,unsigend long size)
返回值为1(成功)或0(失败),如果返回失败,驱动程序通常返回-EFAULT给调用者。
type: VERIFY_READ 或是 VERIFY_WRITE,取决于是读取还是写入用户空间内存区。
addr: 一个用户空间的地址。
size: 如果要读取或写入一个int型数据,则为sizeof(int)。
如果在该地址处既要读取,又要写入,则应该用:VERIFY_WRITE,因为它是VERIFY_READ的超集。
注意:首先, access_ok不做校验内存存取的完整工作; 它只检查内存引用是否在这个进程有合理权限的内存范围中,且确保这个地址不指向内核空间内存。其次,大部分驱动代码不需要真正调用 access_ok,而直接使用put_user(datum, ptr)和get_user(local, ptr),它们带有校验的功能,确保进程能够写入给定的内存地址,成功时返回 0, 并且在错误时返回 -EFAULT。
scull中main.c通过分析ioctl编号的位字段来检查参数:
int scull_ioctl(struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg)
{
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;
...
}
#include<asm/uaccess.h>
put_user(datum,ptr);
/*进行检查以确保进程可以写入指定的内存地址,成功返回0,出错返回-EFAULT*/
__put_user(datum,ptr);
/*使用时,速度快,不做类型检查,使用时可以给ptr传递任意类型的指针参数,只要是个用户空间的地址就行,传递的数据大小依赖于ptr参数的类型*/
get_user(datum.ptr);
__get_user(datum,ptr);
/*接收的数据被保存在局部变量local中,返回值说明其是否正确。同样,__get_user应该在操作地址被access_ok后使用*/
put_user vs __put_user:使用前做的检查,put_user多些,__put_user少些,
这些宏把datum写入到用户空间。当传递单个数据时,应用这些宏而不是copy_to_user。
一般用法:实现一个读取方法时,可以调用__put_user来节省几个时钟周期,或者在复制多项数据之前调用一次access_ok,像上面代码一样。
权能与受限操作
#include<linux/capability.h>
来由:驱动程序必须进行附加的检查以确认用户是否有权进行请求的操作
权能作用:基于权能的系统抛弃了那种要么全有,要么全无的特权分配方式,而是把特权操作划分成了独立的组。
#include<linux/capability.h>
int capable(int capability);
在执行一项特权之前,应先检查其是否具有这个权利:
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
权能 | 描述 |
---|---|
CAP_DAC_OVERRIDE | 越过在文件和目录上的访问限制(数据访问控制或 DAC)的能力 |
CAP_NET_ADMIN | 进行网络管理任务的能力, 包括那些能够影响网络接口的任务 |
CAP_SYS_MODULE | 加载或去除内核模块的能力 |
CAP_SYS_RAWIO | 进行 “raw”(裸)I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯 |
CAP_SYS_ADMIN | 截获的能力, 提供对许多系统管理操作的途径 |
CAP_SYS_TTY_CONFIG | 执行 tty 配置任务的能力 |
ioctl命令的实现
scull的ioctl实现只传递设备的可配置参数:
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))
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 -<