2.6字符设备之ioctl函数和lseek函数

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;
}

上机测试

  1. 这里下载代码,并进行编译,然后拷贝到目标板跟文件系统的/root目录中
  2. 执行insmod ioctl.ko加载驱动
    在这里插入图片描述
  3. 执行./app.out运行测试程序(程序第一次运行时全局内存大小为1024,所以read1:能输出是写入的全部数据,第二次执行时由于上一次把内存大小修改为了20字节,所以read1:能输出20字节的数据数据,多余的数据再写入全局内存时就已经被丢弃了)
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值