ioctl 的作用
有些设备除了常规的读写操作外还有许多其他的特殊操作,这些特殊操作可以通过 ioctl 实现
应用层的 ioctl
应用层的 ioctl 如下:
//fd:文件描述符
//request:请求码,具体功能由驱动层实现的ioctl接口决定
//第3个参数是可选的,其含义由驱动层实现的ioctl接口决定
int ioctl(int fd, unsigned long request, ...);
驱动层的 ioctl
驱动层有两个 ioctl 函数,分别是 unlocked_ioctl 和 compat_ioctl ,这两个函数功能一样,只不过 compat_ioctl 用于64位系统执行32位应用时调用,它们的原型如下:
//file:文件句柄,为应用层 ioctl 的 fd 所关联的文件句柄
//request:请求码,与应用层 ioctl 的request 对应
//arg:控制参数,与应用层ioctl的第3个参数对应
long (*unlocked_ioctl) (struct file *file, unsigned int request, unsigned long arg);
long (*compat_ioctl) (struct file *file, unsigned int request, unsigned long arg);
ioctl请求码
ioctl 的请求码有特定的格式(虽然随意定义的整形也可以使用,但是还是建议按照规定格式进行定义),从高到低依次是8位设备类型、8位命令码、3位数据传输方向、13位数据传输大小,可以通过相应的宏定义生成,如下是生成控制命令的宏定义
//生成普通的请求码,type 设备类型,nr 控制命令
_IO(type, nr)
//生成一条要读数据的请求码,size 为读取的字节数,给数据类型或变量即可,宏定义内部通过sizeof计算其大小
_IOR(type, nr, size)
//生成一条要写数据的请求码,size 为写入的字节数
_IOW(type, nr, size)
//生成一条要读写数据的请求码,size 为读写的字节数
_IOWR(type, nr, size)
另外可以通过下列宏定义从请求码中提取相应字段:
//获取数据传输方向,_IOC_NONE 不传输数据,_IOC_READ 从驱动读数据,_IOC_WRITE 写数据到驱动,_IOC_READ|_IOC_WRITE 双向
_IOC_DIR(nr)
//获取设备类型
_IOC_TYPE(nr)
//获取控制命令
_IOC_NR(nr)
//获取数据传输大小
_IOC_SIZE(nr)
内核已占用的请求码
在执行驱动程序的ioctl函数前会先执行系统的ioctl函数,若在系统的ioctl函数中匹配上了相应的请求码则不会执行驱动层的ioctl函数,所以驱动层的请求码不能于系统的ioctl函数中的请求码冲突,下列请求码内核已经占用,驱动中不可再次使用:
FIOCLEX _IO('f', 1)
FIONCLEX _IO('f', 2)
FIONBIO _IOW('f', 126, int)
FIOASYNC _IOW('f', 125, int)
FIOQSIZE _IOR('f', 128, loff_t)
FIFREEZE _IOWR('X', 119, int)
FITHAW _IOWR('X', 120, int)
FS_IOC_FIEMAP _IOWR('f', 11, struct fiemap)
FIGETBSZ _IO(0x00,2)
lseek 的作用
文件句柄中有一个叫 loff_t f_pos 的成员,用于记录当前读写位置,他会随着 read 和 write 操作不断递增,lseek 函数的作用则是用来重新设置 loff_t f_pos 成员的值,使得应用层可以从指定偏移位置开始读写。
应用层的 lseek
应用层 lseek 如下:
//fd:文件描述符
//offset:偏移量
//whence:相对位置,SEEK_SET 相对于开始,SEEK_CUR 相对于当前位置,SEEK_END 相对于结尾
off_t lseek(int fd, off_t offset, int whence);
驱动层的 lseek
驱动层 lseek 如下:
//file:文件句柄,为应用层 lseek 的 fd 所关联的文件句柄
//offset:偏移量,与应用层的 offset 对应
//whence:相对位置,与应用层的 whence 对应
loff_t (*llseek) (struct file *file, loff_t offset, int whence);
驱动代码实现
驱动程序实现了一个全局内存,通过read、write可以读写内存,通过lseek可以改变当前读写位置,通过ioctl可以获取、设置全局内存大小清除内存中的数据,具体代码如下:
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/miscdevice.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
//次设备号,为MISC_DYNAMIC_MINOR表示自动分配
#define GMEM_MINOR MISC_DYNAMIC_MINOR
//设备文件名
#define GMEM_NAME "gmem"
//设备类型
#define GMEM_MAGIC 'G'
//ioctl请求码,需要于应用层传入的值对应
//获取全局内存的大小
#define GMEM_SET_SIZE _IOW(GMEM_MAGIC, 0, int32_t)
//设置全局内存大小
#define GMEM_GET_SIZE _IOR(GMEM_MAGIC, 1, int32_t)
//清除全局内存中的数据
#define GMEM_CLEAN _IO(GMEM_MAGIC, 2)
//全局内存地址
static uint8_t *gmem_buffer;
//全局内存大小
static uint32_t gmem_size = 1024;
//当前有效数据长度
static uint32_t data_length = 0;
//打开设备,对应应用层中的open
static int gmem_open(struct inode *inode, struct file *file)
{
return 0;
}
//释放设备,在应用层最后因此调用close时调用
int gmem_release(struct inode *inode, struct file *file)
{
return 0;
}
//读数据,对应应用层中的read
ssize_t gmem_read(struct file *file, char __user *buffer, size_t size, loff_t *pos)
{
uint32_t tmp_size;
if(*pos > data_length)
{
//当前读写位置已经超出范围
return -EAGAIN;
}
if(*pos == data_length)
{
//当前读写位置已到末尾
return 0;
}
//计算实际能读取的字节数
if((*pos + size) > data_length)
size = data_length - *pos;
//将数据从内核层拷贝到应用层,必须用次函数进行拷贝,不能用memcpy和赋值运算符
tmp_size = copy_to_user(buffer, &gmem_buffer[*pos], size);
//获取成功拷贝的字节数
tmp_size = size - tmp_size;
//当前读写位置向后便宜
*pos += tmp_size;
return tmp_size;
}
//写设备,对应应用层中的write
ssize_t gmem_write(struct file *file, const char __user *buffer, size_t size, loff_t *pos)
{
uint32_t tmp_size;
if(*pos > gmem_size)
{
//当前读写位置已经超出范围
return -EAGAIN;
}
if(*pos == gmem_size)
{
//当前读写位置已到末尾
return 0;
}
//计算实际能写入的字节数
if((*pos + size) > gmem_size)
size = gmem_size - *pos;
//将应用层数据拷贝到内核层的全局内存中
tmp_size = copy_from_user(&gmem_buffer[*pos], buffer, size);
//获取成功拷贝的字节数
tmp_size = size - tmp_size;
//当前读写位置向后便宜
*pos += tmp_size;
//改变数据有效长度
data_length += tmp_size;
return tmp_size;
}
//控制设备,对应应用层中的ioctl
long gmem_ioctl32(struct file *file, unsigned int request, unsigned long arg)
{
uint8_t *new_mem;
uint8_t *old_mem;
uint32_t new_size;
uint32_t copy_length;
//检查设备类型
if(_IOC_TYPE(request) != GMEM_MAGIC)
return -EINVAL;
//提取命令码
switch(_IOC_NR(request))
{
//重新设置全局内存的大小
case 0:
// printk("set memory size\r\n");
//检查新的大小是否合法
if(arg == 0)
return -EINVAL;
//获取新的全局内存大小,这里将第三个参数当作一个无符号整形
new_size = arg;
//计算拷贝到新内存中的数据长度
if(new_size >= data_length)
copy_length = data_length;
else
copy_length = new_size;
//重新分配内存
new_mem = vmalloc(new_size);
//将数据拷贝到新分配的内存中
memcpy(new_mem, gmem_buffer, copy_length);
//修改有效数据长度和当前读写偏移
data_length = copy_length;
if(file->f_pos > data_length)
file->f_pos = data_length;
//将gmem_buffer切换到新分配的内存
old_mem = gmem_buffer;
gmem_buffer = new_mem;
gmem_size = new_size;
//释放以前的内存
vfree(old_mem);
break;
//获取全局内存的大小
case 1:
// printk("get memory size\r\n");
//检查内存地址是否正常,这里将第三个参数当作一个无符号整形的指针
if(((unsigned long*)arg == NULL) || (_IOC_SIZE(request) != sizeof(uint32_t)))
return -EINVAL;
//读取村全局内存大小的变量到应用层
if(copy_to_user((unsigned long*)arg, &gmem_size, _IOC_SIZE(request)))
return -EIO;
break;
//复位全局内存的值
case 2:
// printk("clean memory\r\n");
//设置内存为的值为0
memset(gmem_buffer, 0, gmem_size);
//复位数据长度和读写偏移
data_length = 0;
file->f_pos = 0;
break;
default:
return -EINVAL;
}
return 0;
}
//功能与unlocked_ioctl一样,只有在64位系统中运行32位程序时才会调用
long gmem_ioctl64(struct file *file, unsigned int request, unsigned long arg)
{
return gmem_ioctl32(file, request, arg);
}
//当前读写位置设置,对应应用层的lseek
loff_t gmem_llseek(struct file *file, loff_t offset, int whence)
{
loff_t temp_pos;
temp_pos = 0;
switch(whence)
{
//offset的值是相对于起始位置
case SEEK_SET:
temp_pos = offset;
break;
//offset的值是相对于当前读写位置
case SEEK_CUR:
temp_pos = file->f_pos + offset;
break;
//offset的值是相对于结束位置
case SEEK_END :
temp_pos = gmem_size + offset;
break;
default:
temp_pos = -EINVAL;
break;
}
//当前读写位置只能在0~gmem_size之间
if((temp_pos >= gmem_size) || (temp_pos < 0))
{
printk("f_pos Out of range\r\n");
return -EINVAL;
}
else
{
//修改当前读写位置为设定值
file->f_pos = temp_pos;
// printk("f_pos = %lld\r\n", file->f_pos);
return temp_pos;
}
}
struct file_operations fops = {
.open = gmem_open,
.release = gmem_release,
.read = gmem_read,
.write = gmem_write,
.unlocked_ioctl = gmem_ioctl32,
.compat_ioctl = gmem_ioctl64,
.llseek = gmem_llseek,
};
struct miscdevice gmem_misc = {
.minor = GMEM_MINOR,
.name = GMEM_NAME,
.fops = &fops,
};
static int __init gmem_init(void)
{
int err = 0;
printk("global memory init\r\n");
//分配内存
gmem_buffer = vmalloc(gmem_size);
if(gmem_buffer == NULL)
{
printk("malloc mem failed\r\n");
return -ENOMEM;
}
//注册misc设备
err = misc_register(&gmem_misc);
if(err != 0)
{
printk("register misc failed\r\n");
return err;
}
return 0;
}
static void __exit gmem_exit(void)
{
printk("global memory exit\r\n");
//注销misc设备
misc_deregister(&gmem_misc);
//释放内存
vfree(gmem_buffer);
}
module_init(gmem_init);
module_exit(gmem_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("csdn");
MODULE_DESCRIPTION("ioctl test");
MODULE_ALIAS("gmem");
驱动测试代码实现
驱动测试代码先向驱动中写入了一个字符串,然后利用lseek将读写位置定位到开始位置后,再读取写入的数据并进行输出,接下来再通过ioctl获取全局内存大小,重新设置其大小,并进行读取输出,最后再清除全局内存中的数据,并进行读取输出,如下时测试程序代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
//设备文件名
#define GMEM_NAME "gmem"
//设备类型
#define GMEM_MAGIC 'G'
//ioctl请求码,需要于应用层传入的值对应
//获取全局内存的大小
#define GMEM_SET_SIZE _IOW(GMEM_MAGIC, 0, int32_t)
//设置全局内存大小
#define GMEM_GET_SIZE _IOR(GMEM_MAGIC, 1, int32_t)
//清除全局内存中的数据
#define GMEM_CLEAN _IO(GMEM_MAGIC, 2)
//读写缓存
static char rbuf[100];
static char wbuf[100] = "1234567890abcdefghijklmnopqrstuvwxy";
int main(void)
{
int fd;
int err = 0;
int32_t gmem_size;
//打开设备
fd = open("/dev/"GMEM_NAME, O_RDWR);
if(fd == -1)
{
perror("error");
return -1;
}
//将数据写入全局内存
if(write(fd, wbuf, strlen(wbuf)) == -1)
goto ERROR;
//将读写位置定位到开始位置
if(lseek(fd, 0, SEEK_SET) == -1)
goto ERROR;
//从全局内存读取数据并输出
memset(rbuf, 0, sizeof(rbuf));
if(read(fd, rbuf, sizeof(rbuf)) == -1)
goto ERROR;
printf("read1:%s\r\n", rbuf);
//获取全局内存的大小
if(ioctl(fd, GMEM_GET_SIZE, &gmem_size) == -1)
goto ERROR;
printf("gmem_size1 =%d\r\n", gmem_size);
//重新设置全局内存的大小
if(ioctl(fd, GMEM_SET_SIZE, 20) == -1)
goto ERROR;
//再获取全局内存的大小
if(ioctl(fd, GMEM_GET_SIZE, &gmem_size) == -1)
goto ERROR;
printf("gmem_size2 =%d\r\n", gmem_size);
//将读写位置定位到开始位置
if(lseek(fd, 0, SEEK_SET) == -1)
goto ERROR;
//再次从全局内存读取数据并输出
memset(rbuf, 0, sizeof(rbuf));
if(read(fd, rbuf, sizeof(rbuf)) == -1)
goto ERROR;
//因为全局内存大小缩小,写入的数据也随之被截断,所以这里输出的只有wbuf的前20字节
printf("read2:%s\r\n", rbuf);
//清空全局内存中的数据
if(ioctl(fd, GMEM_CLEAN) == -1)
goto ERROR;
if(lseek(fd, 0, SEEK_SET) == -1)
goto ERROR;
//再次从全局内存读取数据并输出,
memset(rbuf, 0, sizeof(rbuf));
if(read(fd, rbuf, sizeof(rbuf)) == -1)
goto ERROR;
//因为被清空,所以输出0字节数据
printf("read3:%s\r\n", rbuf);
//关闭设备
close(fd);
return 0;
ERROR:
perror("error");
close(fd);
return -1;
}
上机测试
- 从这里下载代码,并进行编译,然后拷贝到目标板跟文件系统的/root目录中
- 执行insmod ioctl.ko加载驱动
- 执行./app.out运行测试程序(程序第一次运行时全局内存大小为1024,所以read1:能输出是写入的全部数据,第二次执行时由于上一次把内存大小修改为了20字节,所以read1:能输出20字节的数据数据,多余的数据再写入全局内存时就已经被丢弃了)