1、前言
当我们在讨论linux内核驱动开发时,就不得不提到ioctl这个及其重要的函数。它是字符类设备驱动程序中实现对设备控制的接口之一。 ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。
2、ioctl系统调用过程
2.1、用户空间的ioctl系统调用的原型声明
#include <sys/ioctl.h> ----原型声明所在的头文件
int ioctl(int fd, int cmd, ...) ;
(1)参数 fd:文件描述符,也就是我们使用open系统调用打开设备时返回的设备句柄,因此在使用ioctl之前,必须首先使用open系统调用获取IO操作的设备句柄。
(2)参数 cmd:交互协议,设备驱动根据此cmd执行对应操作;一般这些cmd需要借助__IOW/__IOR/__IO/__IOWR等实现定义。
(3)...:表示此系统调用是可变参数,除了上述两个必要参数外,如有其他信息可以通过添加其他参数完成。
(4)函数执行成功返回0,执行失败时返回-1并设置全局变量errornoz值:
a)EBADF--fd is not a valid descriptor.
b)EFAULT--argp references an inaccessible memory area.
c)EINVAL--Request or argp is not valid.
d)ENOTTY--fd is not associated with a character special device.
e)ENOTTY--The specified request does not apply to the kind of object that the descriptor d references.
(5)ioctl典型应用实例:
int ret = 0;
int g_fd = 0;
g_fd = open("/dev/vow",O_RDONLY);
if(g_fd<0){
printf("open: %s\n", strerror(errno));
return -ENODEV;
}
ret = ioctl(g_fd, MY_IOCTL_CMD);
if (ret == -1) {
printf("ioctl: %s\n", strerror(errno));
}
(6)在ioctl执行失败时errno的值通常为ENOTTY(error not a typewriter),就是第一个参数g_fd指向的不是一个字符设备,不支持ioctl操作。
2.2、内核驱动中ioctl
在新版内核中,unlocked_ioctl()与compat_ioctl()取代了ioctl()。unlocked_ioctl(),顾名思义,应该在无大内核锁(BKL)的情况下调用;compat_ioctl(),compat全称compatible(兼容的),主要目的是为64位系统提供32位ioctl的兼容方法,也是在无大内核锁的情况下调用。在《Linux Kernel Development》中对两种ioctl方法有详细的解说。两个函数的原型说明如下:
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
关于这两个函数的详细说明
Not long ago, there existed only a single ioctlmethod. Today, there are three methods.unlocked_ioctl() is the same as ioctl(), except it is called without the Big KernelLock (BKL). It is thus up to the author of that function to ensure proper synchronization.Because the BKL is a coarse-grained, inefficient lock, drivers should implementunlocked_ioctl() and not ioctl().
compat_ioctl() is also called without the BKL, but its purpose is to provide a 32-bit compatible ioctl method for 64-bit systems. How you implement it depends on your existing ioctlcommands. Older drivers with implicitly sized types (such as long) should implement a compat_ioctl() method that works appropriately with 32-bit applications. This generally means translating the 32-bit values to the appropriate types for a 64-bit kernel. New driversthat have the luxury of designing their ioctl commands from scratch should ensure all their arguments and data are explicitly sized, safe for 32-bit apps on a 32-bit system, 32-bit apps on a 64-bit system, and 64-bit apps on a 64-bit system. These drivers can then point the compat_ioctl() function pointer at the same function as
unlocked_ioctl().
Note:在字符设备驱动开发中,一般情况下只要实现unlocked_ioctl()即可,因为在vfs层的vfs_ioctl()是直接调用unlocked_ioctl()。
// fs/ioctl.c
static long vfs_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
int error = -ENOTTY;
if (!filp->f_op || !filp->f_op->unlocked_ioctl)
goto out;
error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
if (error == -ENOIOCTLCMD) {
error = -ENOTTY;
}
out:
return error;
}
2.3、用户与驱动之间的协议
在驱动程序实现时,其内部主体是一个switch-case结构,每一个case对应一个命令码以便做出对应的处理操作。这些case的区分就是通过ioctl中的第二个参数实现的。ioctl中的cmd是实现用户程序命令和驱动程序交互的唯一途径。linux内核中针对cmd做了明确的要求,具体如下:
cmd的定义在<linux/ioctl.h>。
cmd[31:30]—数据(args)的传输方向---dir
cmd[29:16]—数据(args)的大小---type
cmd[15:8]—>命令的类型,可以理解成命令的密钥,一般为ASCII码---nr
cmd[7:0] —>命令的序号,是一个8bits的数字---size
这样一来,一个命令就变成了一个整数形式的命令码;但是命令码非常的不直观,所以Linux Kernel中提供了一些宏。这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。
在内核中,提供了宏接口以生成上述格式的 ioctl 命令:
// include/uapi/asm-generic/ioctl.h
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
(1)dir(direction),ioctl 命令访问模式(数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据;
(2)type(device type),设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如
‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识;
(3)nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;
(4)size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;
通常而言,为了方便会使用宏 _IOC() 衍生的接口来直接定义 ioctl 命令:
// include/uapi/asm-generic/ioctl.h
/* used to create numbers */
#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)))
_IO: 定义不带参数的 ioctl 命令
_IOW:定义带写参数的 ioctl 命令(copy_from_user)
_IOR:定义带读参数的ioctl命令(copy_to_user)
_IOWR:定义带读写参数的 ioctl 命令
同时,内核还提供了反向解析 ioctl 命令的宏接口:
// include/uapi/asm-generic/ioctl.h
/* used to decode ioctl numbers */
#define _IOC_DIR(nr) (((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_SIZESHIFT) & _IOC_SIZEMASK)
我们在实际应用中还会使用"幻数"来定义cmd命令,这个幻数其实就是一个字符,ACSII码在0-255之间,典型应用如下:
#define VOW_IOC_MAGIC 'V'
/* below is vow control message */
#define VOW_SET_CONTROL _IOW(VOW_IOC_MAGIC, 0x03, unsigned int)
#define VOW_SET_SPEAKER_MODEL _IOW(VOW_IOC_MAGIC, 0x04, unsigned int)
#define VOW_CLR_SPEAKER_MODEL _IOW(VOW_IOC_MAGIC, 0x05, unsigned int)
3、系统过程执行过程
3.1、系统调用号定义
/*usr/include/asm/unistd.h*/
#define __NR_ioctl (__NR_SYSCALL_BASE+ 54)
3.2、系统调用入口
/*arch/arm/kernel/calls.S*/
/* 55 */ CALL(sys_ioctl)
3.3、声明系统调用sys_ioctl()
/* /include/linux.h */
asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg);
3.4、系统调用安装
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
其中:
/include/linux/syscalls.h
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...) __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))
3.5、内核系统调用接口--SYSCALL_DEFINE3
/* /vendor/open_source/multipath_tcp/mptcp-0.94.7/fs/ioctl.c */
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
int error;
struct fd f = fdget(fd);
if (!f.file)
return -EBADF;
error = security_file_ioctl(f.file, cmd, arg);
if (!error)
error = do_vfs_ioctl(f.file, fd, cmd, arg);
fdput(f);
return error;
}
3.6、do_vfs_ioctl函数
/* /kernel-4.14/fs/ioctl.c */
/*
* When you add any new common ioctls to the switches above and below
* please update compat_sys_ioctl() too.
*
* do_vfs_ioctl() is not for drivers and not intended to be EXPORT_SYMBOL()'d.
* It's just a simple helper for sys_ioctl and compat_sys_ioctl.
*/
int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
unsigned long arg)
{
int error = 0;
int __user *argp = (int __user *)arg;
struct inode *inode = filp->f_path.dentry->d_inode;
switch (cmd) {
case FIOCLEX:
set_close_on_exec(fd, 1);
break;
case FIONCLEX:
set_close_on_exec(fd, 0);
break;
case FIONBIO:
error = ioctl_fionbio(filp, argp);
break;
case FIOASYNC:
error = ioctl_fioasync(fd, filp, argp);
break;
case FIOQSIZE:
if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode) ||
S_ISLNK(inode->i_mode)) {
loff_t res = inode_get_bytes(inode);
error = copy_to_user(argp, &res, sizeof(res)) ?
-EFAULT : 0;
} else
error = -ENOTTY;
break;
case FIFREEZE:
error = ioctl_fsfreeze(filp);
break;
case FITHAW:
error = ioctl_fsthaw(filp);
break;
case FS_IOC_FIEMAP:
return ioctl_fiemap(filp, arg);
case FIGETBSZ:
return put_user(inode->i_sb->s_blocksize, argp);
default:
if (S_ISREG(inode->i_mode))//是否为常规文件若是常规文件
error = file_ioctl(filp, cmd, arg);
else
error = vfs_ioctl(filp, cmd, arg);//调用vfs_ioctl
break;
}
return error;
}
3.7、调用vfs_ioctl()
/* /kernel-4.14/fs/ioctl.c */
static long vfs_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
int error = -ENOTTY;
if (!filp->f_op || !filp->f_op->unlocked_ioctl)
goto out;
/*调用unlocked_ioctl()*/
error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
if (error == -ENOIOCTLCMD)
error = -EINVAL;
out:
return error;
}
3.8、调用文件集中定义的unlocked_ioctl()函数
这个unlocked_ioctl是struct file_operations{}中的一个成员函数指针,也就是我们在内核驱动中最终要实现的函数,在这个函数中我们可以根据ioctl传递过来的cmd做区分,以便完成不同操作。
/* /kernel-4.14/include/linux/fs.h */
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
} __randomize_layout;
一般我们在驱动中需要重点针对file_operations中函数指针进行实现并进行赋值。
static const struct file_operations VOW_fops = {
.owner = THIS_MODULE,
.open = My_VowDrv_open,
.release = My_VowDrv_release,
.unlocked_ioctl = My_VowDrv_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = My_VowDrv_compat_ioctl,
#endif
.write = My_VowDrv_write,
.read = My_VowDrv_read,
.flush = My_VowDrv_flush,
.fasync = My_VowDrv_fasync,
.mmap = MyVowDrv_remap_mmap
};
这里面的My_VowDrv_ioctl就是unlocked_ioctl的最终执行函数。
4、参考链接
https://blog.csdn.net/qq_33487044/article/details/81489268
https://blog.csdn.net/zqixiao_09/article/details/50859302
https://blog.csdn.net/weixin_38815998/article/details/103510385