Linux编程之ioctl

ioctl是Linux系统中用于设备控制的重要接口,本文详细介绍了应用程序如何使用ioctl,内核如何处理ioctl调用,以及如何在驱动程序中实现ioctl接口。通过实例展示了设备驱动程序与应用程序交互的过程,包括获取熵池大小、清除熵池以及自定义ioctl命令的实现。
AI助手已提取文章相关产品:

在和设备驱动程序通信时,ioctl是很常用的一个调用,常用来配置、查询或者修改设备的配置。反过来说,编写驱动程序时,ioctl也是经常要实现的一个接口,以便应用程序可以方便地控制设备驱动。

应用程序中的ioctl

ioctl函数的原型如下:

       #include <sys/ioctl.h>

       int ioctl(int fd, unsigned long request, ...);

可以看出该函数是一个可变参数的函数。第一个参数是一个文件描述符,通常用open调用来打开。在Linux中,一切皆文件,哪怕你调用open打开一个设备,也得到一个文件描述符。这一切要归功于内核里的VFS(虚拟文件系统)。第二个参数request是控制代码,或者说是控制命令、请求代码都可以。根据第二个参数的不同,后面所带的参数个数和类型都可能不一样。

内核中的ioctl

当引用进程调用ioctl时,Linux内核中实现的系统调用被调用。系统调用是Linux的一种标准机制,在不同的CPU体系结构中的具体实现方式可能不一样。ioctl系统调用定义于内核源码的fs/ioctl.c中,其定义如下:

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
	struct fd f = fdget(fd);
	int error;

	if (!f.file)
		return -EBADF;

	error = security_file_ioctl(f.file, cmd, arg);
	if (error)
		goto out;

	error = do_vfs_ioctl(f.file, fd, cmd, arg);
	if (error == -ENOIOCTLCMD)
		error = vfs_ioctl(f.file, cmd, arg);

out:
	fdput(f);
	return error;
}

从ioctl系统调用的源码可以看出,它首先会调用do_vfs_ioctl,如果do_vfs_ioctl不支持这个控制命令,则调用vfs_ioctl

do_vfs_ioctl函数的源码可点击链接直接查看,它主要处理一些文件系统通用的控制命令,如FIOASYNC等,详细控制命令列表可查看该函数源码。另外,该函数还会判断文件节点是不是常规文件,如果是则调用file_ioctl函数。后者也处理一些跟文件相关的控制命令。

对于设备驱动的控制命令,一般do_vfs_ioctl并不会直接处理,而是调用vfs_ioctl,该函数代码如下:

long vfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int error = -ENOTTY;

	if (!filp->f_op->unlocked_ioctl)
		goto out;

	error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
	if (error == -ENOIOCTLCMD)
		error = -ENOTTY;
 out:
	return error;
}
EXPORT_SYMBOL(vfs_ioctl);

可见,该函数最终会调用到filp>f_op->unlocked_ioctl函数。这里filp时一个文件对象,在open是创建。每个文件对象都又一个文件操作接口,即f_op。如果一个设备驱动程序支持虚拟文件系统VFS,那么它就会注册一个文件操作接口,即f_op,而这个接口中包含几个基本函数,如open、close、read、write、ioctl等。因此,通过这个调用,最终调用到了设备驱动程序的unlocked_ioctl函数。

以/dev/urandom设备为例,它是一个字符型设备,其源码位于drivers/char/random.c。它的文件操作接口定义如下:

const struct file_operations urandom_fops = {
	.read  = urandom_read,
	.write = random_write,
	.unlocked_ioctl = random_ioctl,
	.compat_ioctl = compat_ptr_ioctl,
	.fasync = random_fasync,
	.llseek = noop_llseek,
};

而该文件操作接口的注册则是在注册/dev/urandom设备是一起注册的。不过新内核的代码为了提高效率,把多个设备一起作为内存设备注册。具体代码位于drivers/char/mem.c

具体到ioctl接口,其实现如下:

static long random_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
	int size, ent_count;
	int __user *p = (int __user *)arg;
	int retval;

	switch (cmd) {
	case RNDGETENTCNT:
		/* inherently racy, no point locking */
		ent_count = ENTROPY_BITS(&input_pool);
		if (put_user(ent_count, p))
			return -EFAULT;
		return 0;
	case RNDADDTOENTCNT:
		if (!capable(CAP_SYS_ADMIN))
			return -EPERM;
		if (get_user(ent_count, p))
			return -EFAULT;
		return credit_entropy_bits_safe(&input_pool, ent_count);
	case RNDADDENTROPY:
		if (!capable(CAP_SYS_ADMIN))
			return -EPERM;
		if (get_user(ent_count, p++))
			return -EFAULT;
		if (ent_count < 0)
			return -EINVAL;
		if (get_user(size, p++))
			return -EFAULT;
		retval = write_pool(&input_pool, (const char __user *)p,
				    size);
		if (retval < 0)
			return retval;
		return credit_entropy_bits_safe(&input_pool, ent_count);
	case RNDZAPENTCNT:
	case RNDCLEARPOOL:
		/*
		 * Clear the entropy pool counters. We no longer clear
		 * the entropy pool, as that's silly.
		 */
		if (!capable(CAP_SYS_ADMIN))
			return -EPERM;
		input_pool.entropy_count = 0;
		return 0;
	case RNDRESEEDCRNG:
		if (!capable(CAP_SYS_ADMIN))
			return -EPERM;
		if (crng_init < 2)
			return -ENODATA;
		crng_reseed(&primary_crng, NULL);
		crng_global_init_time = jiffies - 1;
		return 0;
	default:
		return -EINVAL;
	}
}

应用程序ioctl示例

下面的例子通过ioctl获取/dev/urandom的熵池的大小,然后通过ioctl清除熵池。由于ioctl的命令代码是每个设备自定义的,因此针对每个设备的ioctl需要包含该设备的头文件。下面的例子中就是<linux/random.h>。需要注意的是清除熵池的操作需要管理员权限,因此运行时应该用sudo来运行。

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

int main(int argc, char *argv[])
{
	int ent_cnt;
	int fd = open("/dev/urandom", O_RDONLY);
	if (fd < 0) {
		perror("open");
	} else {
		if (ioctl(fd, RNDGETENTCNT, &ent_cnt) < 0) {
			perror("ioctl");
		} else {
			printf("entropy count (bits): %d\n", ent_cnt);
		}
		if (ioctl(fd, RNDCLEARPOOL) < 0) {
			perror("ioctl");
		}
		if (ioctl(fd, RNDGETENTCNT, &ent_cnt) < 0) {
			perror("ioctl");
		} else {
			printf("entropy count (bits) after clear: %d\n", ent_cnt);
		}
		close(fd);
	}
	return 0;
}

编译并运行该程序,可得到如下结果。

gcc -o rnd_ioctl rnd_ioctl.c
sudo ./test_ioctl
entropy count (bits): 2249
entropy count (bits) after clear: 0

驱动程序ioctl示例

下面这个例子演示如何在驱动程序中实现ioctl接口。

创建一个目录dummy,然后在下面创建dummy_main.c,为驱动程序源代码。驱动程序的入口用module_init指定,在安装驱动程序时入口被调用;驱动程序的卸载函数则用module_exit指定,卸载驱动时调用。这个驱动程序要做的事情很简单,就是注册一个dummy设备/dev/dummy,并且支持一个ioctl命令DUMMY_COUNT。这个ioctl命令的作用就是获取对/dev/dummy设备进行ioctl命令的次数。因为任何应用程序都可以对这个设备做ioctl,因此用一个原子变量来累计。其详细代码如下所示。注意在这个驱动程序中,在注册文件操作接口时,并没有指定open以及release等操作函数,但这不代表该设备没有这两个操作函数。所有支持文件系统操作接口的设备都必须实现open函数。在这个例子中,/dev/dummy设备是miscdevice设备,而miscdevice设备又属于字符类设备,内核中对字符类设备都提供默认的open和release操作函数实现。如果你的驱动程序不覆盖这个实现,则采用默认实现。

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>
#include "dummy.h"

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("dummy driver");

static atomic_t dummy_ioctl_cnt = ATOMIC_INIT(0);
static long dummy_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int __user *p = (int __user *)arg;
	switch (cmd) {
		case DUMMY_COUNT:
			atomic_inc(&dummy_ioctl_cnt);
			if (put_user(atomic_read(&dummy_ioctl_cnt), p))
				return -EFAULT;
			break;
		default:
			return -EOPNOTSUPP;
	}
	return 0;
}
static const struct file_operations dummy_fops = {
	.owner = THIS_MODULE,
	.unlocked_ioctl = dummy_ioctl,
	.compat_ioctl = compat_ptr_ioctl,
};

static struct miscdevice dummy_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "dummy",
	.nodename = "dummy",
	.fops = &dummy_fops,
	.mode = 0666,
};
static int __init dummy_register(void)
{
	int err;
	err = misc_register(&dummy_dev);
	if (err == 0) {
		printk("dummy dev registered, minor %d\n", dummy_dev.minor);
	}
	return err;
}
static void __exit dummy_unregister(void)
{
	misc_deregister(&dummy_dev);
}

static int __init dummy_init(void)
{
	return dummy_register();
}
static void __exit dummy_exit(void)
{
	dummy_unregister();
}

module_init(dummy_init);
module_exit(dummy_exit);

再创建一个头文件dummy.h,在里面定义ioctl命令码,如下所示。

#ifndef _UAPI_LINUX_DUMMY_H
#define _UAPI_LINUX_DUMMY_H
#include <linux/ioctl.h>

#define DUMMY_COUNT	_IOR('R', 0x1, int)

#endif

编译驱动程序需要一个Makefile。编译驱动需要安装内核编译支持,并指定build目录;除此之外还需要指定当前驱动源码所在目录。由于dummy驱动不是built-in驱动,因此在Makefile中采用obj-m。如下所示。

obj-m:=dummy.o
dummy-objs:=dummy_main.o

.PHONY: all clean
all:
	make -C /lib/modules/`uname -r`/build M=`pwd` modules

clean:
	@rm -rf *.o *.ko *.mod *.mod.c *.order *.symvers

最后在dummy调用make命令即可编译。

make
make -C /lib/modules/`uname -r`/build M=`pwd` modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-60-generic'
  CC [M]  dummy_main.o
  LD [M]  dummy.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  dummy.mod.o
  LD [M]  dummy.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-60-generic'

编译完成会生成dummy.ko,可调用insmod安装。安装驱动需要管理员权限。安装完成后可查看/dev/dummy检查驱动程序是否正确加载。如下所示。

sudo insmod ./dummy.ko
ls -l /dev/dummy
crw-rw-rw- 1 root root 10, 57 1月  12 08:12 /dev/dummy

需要注意的是,这个例子中并没有对生成的dummy.ko做strip操作。在正式产品开发中,一般都需要在Makefile中定义一个安装步骤,在安装时做strip操作以减小驱动程序模块文件尺寸。

 

下面再用一个简单的应用程序来验证驱动程序的ioctl操作。在同一个目录下创建dummy_ioctl.c,并编译运行。运行了应用程序两次,可以看到ioctl操作在驱动程序中被正确累加,并由ioctl正确返回。

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

int main(int argc, char *argv[])
{
	int i;
	int dummy_cnt;
	int fd = open("/dev/dummy", O_RDONLY);
	if (fd < 0) {
		perror("open");
	} else {
		for (i = 0; i < 3; i++) {
			if (ioctl(fd, DUMMY_COUNT, &dummy_cnt) < 0) {
				perror("ioctl");
			} else {
				printf("#%d: dummy_cnt: %d\n", i, dummy_cnt);
			}
		}
		close(fd);
	}
	return 0;
}
 gcc -o dummy_ioctl dummy_ioctl.c
./dummy_ioctl
#0: dummy_cnt: 1
#1: dummy_cnt: 2
#2: dummy_cnt: 3
./dummy_ioctl
#0: dummy_cnt: 4
#1: dummy_cnt: 5
#2: dummy_cnt: 6

 

 

 

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值