高级字符程序操作
本章内容:
和用户空间保持同步的几个途径。
任何使用进程休眠及唤醒,如何实现非阻塞IO.
在设备可读或者可写时通知用户空间。
介绍几种访问设备的策略。
编者:张永辉 2012年10月19日
//--------------------------------------------------------------------------------------------------
6.1 ioctl
除了读写,还要控制设备等,常使用ioctl()方法来支持,
在用户空间, ioctl系统调用如下:
int ioctl(int fd, unsigned long cmd, ...);
... : 表示函数有数目不定的参数。事实上会使用固定的参数,传统上为char *argp.
cmd :可以是整形参数,或指针。
驱动的原型:
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
inode和filp 与open方法的参数相同。
cmd:来自用户,
arg:以unsigned long形式传递,不管它是整数还是指针.若用户调用不传递第3个参数,则arg值未定义。
大部分ioctl使用switch语句根据cmd参数,选择正确的做法.
cmd参数常通过预处理来定义符号名. scull.h为scull声明了要使用的符号。
6.1.1 选择ioctl命令 --cmd
1 当然可以选择从0或者1开始,但为了命令在系统唯一,使用下面的方法。
2 linux统一如下:
2.1 include/asm/ioctl.h 定义了type,序号,传输方向,和参数大小
<linux/ioctl.h> 对字段的定义
type 8 位宽(_IOC_TYPEBITS).
选择一个号码并在整个驱动中使用。(参考了ioctl-number.txt)
number 8 位(_IOC_NRBITS)宽
序号(顺序编号)
direction
定义数据传输方向
_IOC_NONE(没有数据传输)
_IOC_READ 读 (从设备读取数据)
_IOC_WRITE 写
_IOC_READ|_IOC_WRITE (数据在 2 个方向被传送).
_IOC_READ和_IOC_WRITE可使用一个逻辑AND分解出来.
size 13 或14位 (_IOC_SIZEBITS)
用户数据大小,不强制使用该段。
<asm/ioctl.h>定义宏来帮助建立命令号,它被包含在<linux/ioctl.h>中
_IO(type,nr) (给没有参数的命令)
_IOR(type, nre, datatype) (给从驱动中读数据的)
_IOW(type,nr,datatype) (给写数据)
_IOWR(type,nr,datatype) (给双向传送)
可被用在驱动中来解码这的宏:
_IOC_DIR(nr),
_IOC_TYPE(nr),
_IOC_NR(nr),
_IOC_SIZE(nr).
scull示例:
#define SCULL_IOC_MAGIC 'k' //type字段 k作为幻数
#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
#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)
#define SCULL_IOC_MAXNR 14
* S means "Set" 通过指针设置
* T means "Tell" 直接使用参数值
* G means "Get": 通过设置指针来应答
* Q means "Query": 通过返回值应答
* X means "eXchange": 原子的交换G和S
* H means "sHift": 原子地交换T和Q
2.2 Documentation/ioctl-number.txt. 列举了内核已经使用了的命令号
3 返回值
同样可以返回一个整数,或者一个代表地址的整数。
若使用了非法的命令,可以返回 -ENIVAL("Invalid argument")或-ENOTTY
4 预定义命令
有一组命令,由系统定义,在调用ioctl方法之前被解释,如果自定义与此类命令相同,则ioctl行为无法预测。
分三类:
1 可对任何文件发出的(普通,设备,FIFO,或socket)
2 只对常规文件的命令.
3 针对文件系统的特殊命令.
设备驱动只关注第一组,它的type是'T'
下列ioctl命令是预定义给任何文件,包括设备特殊的文件:
FIOCLEX
FIONCLEX
FIOQSIZE
FIONBIO
6.1.2 选择ioctl参数 --arg
arg 参数:
1 若是一个整数,直接使用即可。
2 若是用户空间指针,必须是合法的,否则可能导致内核崩溃。驱动负责检查该指针,若非法则返回错误。
方法一:使用copy_to_user()或者copy_from_user().
方法二:其他更有效的方法:
#include <asm/uaccess.h>
int access_ok(int type,const void *addr,unsigned long size)
type: VERIFY_READ 读
VERIFY_WRITE 读写
addr: 用户空间地址,
size 若是针对一个整数可以是sizeof(int).
返回:bool类型。0表示失败,驱动应该返回-EFAULT给调用者
scull示例:
int err = 0, tmp;
int retval = 0;
//命令检查
if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) { return -ENOTTY; }
if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) { return -ENOTTY; }
//地址检查
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;}
经过检查后,就可以使用数据传输函数了,除了copy_to_user()和copy_from_user(),可以使用如下宏:
他们只针对 1、2、4、8字节的单个数据。
定义 <asm/uaccess.h>:
//向用户空间传输数据
put_user(datum, ptr) 调用access_ok做类型检查
__put_user(datum, ptr) 此宏展开不做类型检查
datum:将写到用户空间的数据;
ptr:用户空间指针。 根据此类型将传输相应的字节数。
//从用户空间取得数据
get_user(local, ptr) 调用access_ok做类型检查
__get_user(local, ptr) 此宏展开不做类型检查
local 本地接收变量
ptr:用户空间指针。
若编译报错'coversion to non-scalar type requested'应该使用copy_to_user 或者 copy_from_user.
6.1.5 兼容性和受限操作
对设备的访问由设备文件的权限控制,驱动程序通常不用权限检查。有时也只允许部分权限。
内核在许可权管理上有2个系统调用capget()和capset(), 来允许它们被从用户空间管理.
具体:暂略。
6.1.6 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); 指针设置
ioctl(fd,SCULL_IOCTQUANTUM, quantum); 值 设置
ioctl(fd,SCULL_IOCGQUANTUM, &quantum); 指针获取
quantum=ioctl(fd,SCULL_IOCQQUANTUM); 值 获取
ioctl(fd,SCULL_IOCXQUANTUM, &quantum); 指针交换
quantum=ioctl(fd,SCULL_IOCHQUANTUM, quantum); 值交换
使用其中一种即可。
6.1.7 非IO控制设备
有时控制设备最好是通过写控制序列到设备自身来实现.
例如:这个技术用在控制台驱动中,序列被用来移动光标, 改变缺省的颜色。
通过打印来控制的缺点是它给设备增加了策略限制
对于不用传送数据而只是响应命令的设备,此方法是很好的方法,如控制机器人。