在和设备驱动程序通信时,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

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

被折叠的 条评论
为什么被折叠?



