05-ioctl

本文深入解析Linux系统中ioctl函数的应用,涵盖从应用层到驱动层的ioctl函数细节,包括命令格式定义、解析示例及代码实现。通过具体实例,如控制LED灯的开和关,展示了ioctl函数如何在字符设备驱动中进行高效操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从内核中最简单的驱动程序入手,描述Linux驱动开发,主要文章目录如下(持续更新中):
01 - 第一个内核模块程序
02 - 注册字符设备驱动
03 - open & close 函数的应用
04 - read & write 函数的应用
05 - ioctl 的应用
06 - ioctl LED灯硬件分析
07 - ioctl 控制LED软件实现(寄存器操作)
08 - ioctl 控制LED软件实现(库函数操作)
09 - 注册字符设备的另一种方法(常用)
10 - 一个cdev实现对多个设备的支持
11 - 四个cdev控制四个LED设备
12 - 虚拟串口驱动
13 - I2C驱动
14 - SPI协议及驱动讲解
15 - SPI Linux驱动代码实现
16 - 非阻塞型I/O
17 - 阻塞型I/O
18 - I/O多路复用之 select
19 - I/O多路复用之 poll
20 - I/O多路复用之 epoll
21 - 异步通知


上两节说到了文件操作方法描述集中的open、release、read、write函数,这一节对文件操作方法描述集中的另外一个接口ioct进行描述。对于一些并不是所有字符设备都需要的函数操作,在文件操作方法描述集中统一的归类到ioctl函数中。

预备知识

1. 应用层ioctl函数

 应用层的ioctl函数原型如下,可以在ubuntu中输入 man 2 ioctl,查看函数的具体信息。

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

 函数中各个参数的含义如下:

@param1: 文件描述符,open函数的返回值
@param2: 请求cmd
@param3: 可变参数,一般传一个指针 *p,原因见驱动层ioctl函数参数3
@return: 成功返回0,失败返回-1

2. 驱动层ioctl函数

 驱动层的ioctl函数在file_operations结构体里面的函数指针是unlocked_ioctl,使用者可以对其重新设置,满足自己的使用需求。函数原型如下:

long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long args)

 函数中各个参数的含义如下:

@param1: 打开的文件的file结构指针
@param2: 和应用层ioctl()的第二个参数对应
@param3: 和应用层ioctl()的第三个参数对应,参数类型是unsigned long类型,因此只能传递四个字节,所以在应用层长传递地址,和上面应用层ioctl的描述相对应
@return: 成功0,失败错误码

3. 命令格式定义

 上面都提到了传递和接收时都有一个命令cmd,下面描述一下cmd的格式要求。查看 Documentation/ioctl/ioctl-decoding.txt 文档可以发现,一个命令由四部分组成,这样做的主要目的是为了避免命令定义的重复,其中命令的组成如下:

bits	meaning
31-30  	00 - no parameters: uses _IO macro	不带参数
		10 - read: _IOR						命令从驱动中读取数据,读方向
		01 - write: _IOW					命令把数据写入驱动中,写方向
		11 - read/write: _IOWR				读写方向
29-16	size of arguments					如果命令带参数,则指定参数所占内存空间的大小
15-8	ascii character supposedly			每个驱动全局唯一的幻数
		unique to each driver
7-0		function							命令码

 为了书写方便,在内核中定义了一些宏来对命令进行封装,这些宏的定义在文件 include/uapi/asm/ioctl.h 中。

#define _IOC(dir, type, nr, size) \
	( ((dir)  << _IOC_DIRSHIFT)  | \
	  ((type) << _IOC_TYPESHIFT) | \
	  ((nr)   << _IOC_NRSHIFT)   | \
	  ((size) << _IOC_SIZESHIFT) )  	
// dir : 2位   方向: 不带参数_IO;       读_IOR; 写_IOW;   可读可写_IOWR    ;
// size: 14位  指定参数所占内存空间的大小
// type: 8位   类型----> 给定一个ASCII码, a-z  =====>幻数 借助这个幻数让cmd更加唯一 ==== ftok 
// nr  : 8位   命令码的序号: 2^8 = 256; 对设备的控制状态

#define _IOC_NRSHIFT    0
#define _IOC_TYPESHIFT  (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT  (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT   (_IOC_SIZESHIFT+_IOC_SIZEBITS)
=====>
#define _IOC_NRSHIFT	0        // 0位
#define _IOC_TYPESHIFT	(0+8)    // 8位
#define _IOC_SIZESHIFT	(8+8)    // 16位
#define _IOC_DIRSHIFT	(16+13)  // 29位

 可以看出宏 _IOC分别对命令的四个组成部分进行移位操作,来构成ioctl传递的命令cmd。
 为了更加方便的书写,内核中又通过下面的四个宏来定义宏_IOC

#define _IO(type, nr)		    _IOC(_IOC_NONE, (type), (nr), 0)
#define _IOR(type, nr, size)	_IOC(_IOC_READ, (type), (nr), (_IOC_TYPECHECK(size)))
#define _IOW(type, nr, size)	_IOC(_IOC_WRITE,(type), (nr), (_IOC_TYPECHECK(size)))
#define _IOWR(type, nr, size)	_IOC(_IOC_READ|_IOC_WRITE, (type), (nr), (_IOC_TYPECHECK(size)))
#define _IOC_NONE  0U
#define _IOC_WRITE 1U
#define _IOC_READ  2U
#define _IOC_TYPECHECK(t) (sizeof(t))

 通过上面介绍的宏的定义,我们可以写出一个简单的命令cmd,过程如下。第一行定义了一个幻数,第二行定义了一个写的命令,命令码的序号为0,参数所占内存空间的大小是int型, 占四个字节。

// 示例1
#define led_magic 'd'
_IOW(led_magic, 0, int)
// 示例2
typedef struct led_node{
	int which;	// 哪一盏灯
	int status;	// 灯的状态,0表示灭,1表示亮
}led_node_t;
	
#define led_magic 'd'
#define LEDON  _IOW(led_magic, 0, struct led_node)

4. 命令格式的解析

 定义好了命令的格式之后,在驱动中需要将命令解析,以判断是不是应用层和驱动层的命令是不是对应的。在内核中定义了如下宏来获取命令的各个组成部分。

/* Used to decode ioctl numbers in drivers despite the leading underscore... */
#define _IOC_DIR(nr)    \
 ( (((((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK) & (_IOC_WRITE|_IOC_READ)) != 0)?   \
                            (((nr) >> _IOC_DIRSHIFT) & (_IOC_WRITE|_IOC_READ)):  \
                            (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK) )
#define _IOC_TYPE(nr)       (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr)         (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr)   \
 ((((((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK) & (_IOC_WRITE|_IOC_READ)) == 0)?    \
                         0: (((nr) >> _IOC_SIZESHIFT) & _IOC_XSIZEMASK))

 可以使用上面定义的各个宏来获取命令的各部分,常见应用如下:在驱动的ioctl函数中使用如下代码来判断命令的类型是否一致,一致则继续执行不一致则退出。

if ( _IOC_TYPE(cmd) != 'd')
{
	return -ENOTTY;	/* not a typewriter */
}

示例代码

1. demo.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#include "chrdev.h"

int major;
const char *name = "demoname";

int demo_open(struct inode *inode, struct file *filp)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	return 0;
}

int demo_release(struct inode *inode, struct file *filp)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	return 0;
}

ssize_t demo_read(struct file *filp, char __user *userbuf, size_t size, loff_t *offset)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	return 0;
}
ssize_t demo_write(struct file *filp, const char __user *userbuf, size_t size, loff_t *offset)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	return 	size;
}

long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
{
	led_node_t led;
	int lednum, ret;

	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	if ( _IOC_TYPE(cmd) != 'd')		/* 判断命令与头文件定义的是否一致 */
	{
		return -ENOTTY;	/* not a typewriter */
	}

	ret = copy_from_user(&led, (led_node_t *)args, sizeof(led_node_t));	/* 将用户空间传过来的ioctl参数传递给内核空间 */
	if (ret)
	{
		printk("copy_from_user failed.\n");
		goto copy_err;
	}
	
	switch (cmd)	/* switch - case 判断命令的内容 */
	{
	case LEDOFF:
		lednum = led.which;
		if (lednum == 0)
		{
		    printk("led0 off.\n");
			// 对硬件的控制
		}
		else if (lednum == 1)
		{
		    printk("led1 off.\n");
			// 对硬件的控制
		}
		else if (lednum == 2)
		{
			printk("led2 off.\n");
			// 对硬件的控制
		}
		break;
	case LEDON:
		lednum = led.which;
		if (lednum == 0)
		{
		    printk("led0 on.\n");
		}
		else if (lednum == 1)
		{
		    printk("led1 on.\n");
		}
		else if (lednum == 2)
		{
			printk("led2 on.\n");
		}
		break;
	default:
		printk("cmd error.\n");
	}

	return 0;

copy_err:
	return ret;
}

const struct file_operations fops = {
	.open = demo_open,
	.release = demo_release,
	.read = demo_read,
	.write = demo_write,
	.unlocked_ioctl = demo_ioctl,	/* 实现ioctl接口 */
};

struct class *cls;
struct device *dev;

static int __init demo_init(void)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	major = register_chrdev(0, name, &fops);	/* 注册字符设备 */
	if (major <= 0)
	{
		printk("register_chrdev failed.\n");
		goto err0;
	}

	cls = class_create(THIS_MODULE, "char_class");	/* 创建设备类 */
	if (cls == NULL)
	{
		printk("class_create failed.\n");
		goto err1;
	}

	dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "chrdev%d", 0);	/* 创建设备 */
	if (dev == NULL)
	{
		printk("device_create failed.\n");
		goto err2;
	}
	
	return 0;

err2:
	class_destroy(cls);
err1:
	unregister_chrdev(major, name);	
err0:
	return major;
}

static void __exit demo_exit(void)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	device_destroy(cls, MKDEV(major, 0));
	class_destroy(cls);
	unregister_chrdev(major, name);	
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");

 demo.c中增加了对ioctl接口的实现函数。在代码第41行定义了led节点的结构体,目的是为了接应应用层传递过来的参数;通过50行的 copy_from_user 函数将用户空间的参数拷贝给41行的创建的结构体中;在内核空间常用switch - case 对用户空间传过来的命令进行区分,以执行不同的硬件操作。在这一节中只是模拟了硬件的操作,下一节将对实际的寄存器进行操作,来控制开发板上的LED灯。

2. test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include "chrdev.h"

#define LED_NUM 3
const char *pathname = "/dev/chrdev0";

int main(void)
{
	int fd, i;
	led_node_t led;

	fd = open(pathname, O_RDWR, 0666);
	if (fd < 0)
	{
		printf("open failed.\n");
		return -1;
	}
	//printf("fd = %d\n", fd);

	for (i=0; i<LED_NUM; i++)
	{
		led.which  = i;	//第几盏灯
		led.status = 0;	//灯的状态,0表示灭
		ioctl(fd, LEDOFF, &led);
		sleep(1);

		led.which  = i;
		led.status = 1;	//1表示亮
		ioctl(fd, LEDON, &led);
		sleep(1);
	}

	close(fd);
	
	return 0;
}

 在test.c中首先调用open函数打开创建的设备,返回文件描述符。然后在for循环中分别通过对不同led的状态进行赋值,通过ioctl函数调用内核空间的ioctl以控制led灯的亮和灭;最后关闭文件描述符。

3. chrdev.h

#ifndef _CHRDEV_H_
#define _CHRDEV_H_

typedef struct led_node{
	int which;	// 那一盏灯
	int status;	// 灯的状态,0表示灭,1表示亮
}led_node_t;

#define led_magic 'd'
#define LEDON  _IOW(led_magic, 0, struct led_node)
#define LEDOFF _IOW(led_magic, 1, struct led_node)

#if 0
	@param1: 幻数 
	@param2: 命令码 
	@param3: 指定参数所占内存空间的大小,会调用#define _IOC_TYPECHECK(t) (sizeof(t)),所以不用写sizeof()
#endif

#endif /* chrdev.h */

 头文件中对ioctl中的命令进行了定义,定义了LEDON和LEDOFF两种命令,分别对应led灯的开和关。

4. Makefile

KERNELDIR ?= /home/linux/ti-processor-sdk-linux-am335x-evm-05.02.00.10/board-support/linux-4.14.79/
PWD := $(shell pwd)

all:
	make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) modules
	arm-linux-gnueabihf-gcc test.c -o app
install:
	sudo cp *.ko  app /tftpboot
	make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) clean
	rm app
clean:
	make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) clean
	rm app

obj-m += demo.o

5. 测试结果

root@am335x-evm:~# insmod demo.ko 
[ 4032.232559] demo_init -- 115.
root@am335x-evm:~# ./app 
[ 4039.328799] demo_open -- 13.
[ 4039.337438] demo_ioctl -- 43.
[ 4039.340821] led0 off.
[ 4040.351683] demo_ioctl -- 43.
[ 4040.354877] led0 on.
[ 4041.358094] demo_ioctl -- 43.
[ 4041.361233] led1 off.
[ 4042.371726] demo_ioctl -- 43.
[ 4042.375769] led1 on.
[ 4043.378167] demo_ioctl -- 43.
[ 4043.381295] led2 off.
[ 4044.390976] demo_ioctl -- 43.
[ 4044.395401] led2 on.
[ 4045.397897] demo_release -- 20.
root@am335x-evm:~# rmmod demo.ko 
[ 4147.374424] demo_exit -- 150.

 从测试结果可以看出,在执行./app后,会打开创建的led字符设备,然后调用了内核中的ioctl函数,通过驱动中 switch - case 对传进去的参数分析,执行不同的操作。例如:第一次传进去的led.which = 0, led.status = 0,对应输出的是led0 on.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值