我们通常用ioctl函数直接访问到内核,在内核执行操作,执行读写操作。那么这个函数指令的原理是什么,本文就展开介绍和使用一下ioctl。

1ioctl简介

1.1什么是ioctl

从ioctl这个名称上看,它是设备驱动程序中对设备的I/O通道进行管理的函数。

所谓对I/O通道进行管理,就是对设备的一些特性进行控制。例如串口的传输波特率、马达的转速等等, 但实际上ioctl所处理的对象并不限制是真正的I/O设备,还可以是其它任何一个内核设备。

说白了,ioctl以系统调用的形式提供了一条用户与内核交互的便捷途径。ioctl 这种方式的特点是用户层为主动,当用户层通过ioctl下发指令,内核给出相应的操作,具体的细节由自己实现的代码决定。

1.2 基本原理

kernel3.0之前,叫ioctl,之后改名为unlocked_ioctl。功能和接口基本相同,名字发生了变化

ioctl既可以往内核读也可以写,read/write在执行大数据量读/写时比较有优势。

在应用层调用ioctl函数时,内核会调用对应驱动中的unlocked_ioctl函数,向内核读写数据。

笔者这里的kernel直接参考最新的kernel-v5.19源码来分析。

内核实现ioctl()函数的是sys_ioctl()。内核中主要调用框架图如下图1所示

图1 内核中主要调用框架图 图1 内核中主要调用框架图

至于为什么在驱动层中还会看到compat_ioctl

这是因为为了相容性而出现的compat_ioctl。为了让32-bit的进程可以在64-bit上的system来执行ioctl这个函数。

2通信流程

2.1调用用户态的ioctl函数

在用户态只有函数的声明,最终的实现是在内核态,也就是内核态封装了一个函数可以用于用户态的调用。

驱动程序中的ioctl属于内核空间,然而应用程序属于用户空间,没有权限访问内核空间,那么需要系统需要从用户态转为内核态,在系统调用中,是通过**SWI(Software Interrupt)的方式**陷入内核态的,这里实际上是一个靠**软中断**实现的。

1//development/ndk/platforms/android-30/include/sys/ioctl.h
2int ioctl(int fd, int cmd, ...) ;

2.2在系统调用表中找到ioctl函数对应的函数名

2.2.1ioctl的驱动定义

首先找到定义的位置

 1//kernel/include/uapi/asm-generic/unistd.h
 2#ifndef __SYSCALL
 3#define __SYSCALL(x, y)
 4#endif
 5
 6#if __BITS_PER_LONG == 32 || defined(__SYSCALL_COMPAT)
 7#define __SC_3264(_nr, _32, _64) __SYSCALL(_nr, _32)
 8#else
 9#define __SC_3264(_nr, _32, _64) __SYSCALL(_nr, _64)
10#endif
11
12#ifdef __SYSCALL_COMPAT
13#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
14#define __SC_COMP_3264(_nr, _32, _64, _comp) __SYSCALL(_nr, _comp)
15#else
16#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _sys)
17#define __SC_COMP_3264(_nr, _32, _64, _comp) __SC_3264(_nr, _32, _64)
18#endif
19
20/* fs/ioctl.c */
21#define __NR_ioctl 29
22__SC_COMP(__NR_ioctl, sys_ioctl, compat_sys_ioctl)

因为这里边的宏定义比较多,直接替换掉宏定义

1//kernel/include/uapi/asm-generic/unistd.h
2__SYSCALL(29, sys_ioctl)

2.2.2系统调用表

系统调用表数组如下。如何解读系统调用表的用法,可以参考这里

1//kernel/arch/arc/kernel/sys.c
2#define __SYSCALL(nr, call) [nr] = (call),
3
4void* sys_call_table[NR_syscalls] = {
5    [0 ... NR_syscalls - 1] = sys_ni_syscall,
6    #include<asm/unistd.h>
7};

这里用的是c99的语法规则,其中数组初始化可以用[0 … n],这种写法。举例如下

1int my_array[6] = { [4] = 29, [2] = 15 };
2//或者写成:
3//省略到索引与值之间的=,GCC 2.5 之后该用法已经过时了,但 GCC 仍然支持
4int my_array[6] = { [4] 29, [2] 15 };     
5//两者均等价于:
6int my_array[6] = {0, 0, 15, 0, 29, 0};

直到对应的语法之后,其中#include<asm/unistd.h>就是罗列了所有的系统调用函数名。

 1//所以得到的ioctl函数就是下面这个宏定义
 2__SYSCALL(0, sys_io_setup)
 3__SYSCALL(1, sys_io_destroy)
 4__SYSCALL(2, sys_io_submit)
 5...
 6__SYSCALL(29, sys_ioctl)
 7...
 8//展开上述的宏定义
 9[0] = sys_io_setup,
10[1] = sys_io_destroy,
11[2] = sys_io_submit,
12...
13[29] = sys_ioctl,
14...
  1. 其中,NR_syscalls这个数是在#include<asm/unistd.h>中实际的系统调用函数名数量,比如435。那么NR_syscalls就会被435替换。

  2. 这个sys_call_table数组中的[0 … 434]是用于初始化的操作,其中这个sys_ni_syscall函数名返回的是-ENOSYS,说明如果没有找到#include<asm/unistd.h>中对应的函数,直接返回系统调用表初始化错误。

  3. 后面的操作,是相当于对其重新赋值,让每一个系统调用表数组都能够对应一个函数名,那么在找这个函数名的过程,只需要记住这个函数名在表中对应的序号即可,这里的ioctl函数,对应的就是29序号。即在系统调用表中找到ioctl函数对应的函数名为sys_ioctl。

那么现在替换之后再来看系统调用表

 1//kernel/arch/arc/kernel/sys.c
 2#define __SYSCALL(nr, call) [nr] = (call),
 3
 4void* sys_call_table[435] = {
 5    [0 ... 434] = sys_ni_syscall,
 6    [0] = sys_restart_syscall,
 7    [1] = sys_exit,
 8    [2] = sys_fork,
 9    ...
10    [29] = sys_ioctl,
11    ...
12};

关于system_call系统调用

因为上述得到的是系统调用表,而系统调用需要在这个系统调用表中查询。

通过软中断引发一个异常促使系统切换到内核去执行异常处理程序。该异常处理程序实际就是系统调用处理程序。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序—-系统调用处理程序。系统调用处理程序叫system_call(),它与硬件体系结构紧密相关,用汇编语言编写。

应用程序告诉内核自己需要执行一个系统调用时,通知内核的机制是靠system_call函数通过将给定的系统调用号(eax的值)与NR_syscalls做比较来检查其有效性(eax代表寄存器)。如果它大于或等于NR_syscalls,该函数返回-ENOSYS。否则执行相应的系统调用:call *sys_call_table(,%eax,4)。由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4。

除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入,所以需要把这些参数从用户空间传给内核。最简单的方法就是像传递系统调用参数那样也存放在寄存器里。

对应的系统调用流程如下

2.3找到内核中对应的函数指针

找到了函数名sys_ioctl,就要去找对应的定义和实现

2.3.1ioctl系统调用函数名定义

1//kernel/include/linux/syscalls.h
2asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long long);

2.3.2ioctl系统调用函数名实现

找到了这个定义之后,去找到实现。不过多了一个file结构体指针参数,可以将用户传递的fd转换为系统调用到内核内部file结构指针。当当前文件描述符表仅用于单个任务时,fget_light() / fput_light()不会触及引用计数。有关更多信息,请访问read this

 1//kernel/fs/ioctl.c
 2//实际上SYSCALL_DEFINE3就是sys_ioctl
 3SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
 4{
 5	struct file *filp;
 6	int error = -EBADF;
 7	int fput_needed;
 8
 9	filp = fget_light(fd, &fput_needed);//由fd得带filp指针
10	if (!filp)
11		goto out;
12
13	error = security_file_ioctl(filp, cmd, arg);
14	if (error)
15		goto out_fput;
16
17	error = do_vfs_ioctl(filp, fd, cmd, arg);
18 out_fput:
19	fput_light(filp, fput_needed);
20 out:
21	return error;
22}

补充说明:为什么SYSCALL_DEFINE3就是sys_ioctl

实际上这里的3代表有三个参数的意思,具体拆解可以点击这里,找到宏定义是如何一步步展开的,这里简单阐述。

1//include/linux/syscalls.h
2#define SYSCALL_DEFINE3(name, ...)  SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
3#define SYSCALL_DEFINEx(x, sname, ...)	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
4#define __SYSCALL_DEFINEx(x, name, ...)	asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))

__SYSCALL_DEFINEx宏定义

 1//include/linux/syscalls.h
 2#define __SYSCALL_DEFINEx(x, name, ...)					\
 3	asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__));		\
 4	static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__));	\
 5	asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__))		\
 6	{								\
 7		__SC_TEST##x(__VA_ARGS__);				\
 8		return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__));	\
 9	}								\
10	SYSCALL_ALIAS(sys##name, SyS##name);				\
11	static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))

SYSCALL_ALIAS定义

1//include/linux/syscalls.h
2#define SYSCALL_ALIAS(alias, name)					\
3	asm ("\t.globl " #alias "\n\t.set " #alias ", " #name "\n"	\
4	     "\t.globl ." #alias "\n\t.set ." #alias ", ." #name)

__SC_DECL3定义

1//include/linux/syscalls.h
2#define __SC_DECL1(t1, a1)      t1 a1
3#define __SC_DECL2(t2, a2, ...) t2 a2, __SC_DECL1(__VA_ARGS__)
4#define __SC_DECL3(t3, a3, ...) t3 a3, __SC_DECL2(__VA_ARGS__)

展开所有的宏定义之后,得到结果

 1asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg);		\
 2static inline long SYSC_ioctl(unsigned int fd,  unsigned int cmd, unsigned long arg);	\
 3asmlinkage long SyS_ioctl(long fd, long cmd, long arg)		\
 4{								\
 5    BUILD_BUG_ON(sizeof(int) > sizeof(long)); BUILD_BUG_ON(sizeof(int) > sizeof(long));               BUILD_BUG_ON(sizeof(int) > sizeof(long));				\
 6    return (long) SYSC_ioctl((unsigned int) fd, (unsigned int) cmd, (unsigned long) arg);	\
 7}								\
 8    SYSCALL_ALIAS(sys_ioctl, SyS_ioctl);				\
 9    static inline long SYSC_ioctl(unsigned int fd,  unsigned int cmd, unsigned long arg)
10{
11    code...
12}

其实里面做的工作,就是将系统调用的参数统一变为了使用long类型来接收,然后再强转为本来参数类型。据说这样折腾是为了修复一个漏洞来的。

2.3.3struct file结构体

struct file

这里的结构体相对比较复杂,我们只关注相关的结构体即可。

 1//kernel/include/linux/fs.h
 2struct file {
 3	union {
 4		struct llist_node	fu_llist;
 5		struct rcu_head 	fu_rcuhead;
 6	} f_u;
 7	struct path		f_path;
 8	struct inode		*f_inode;	/* cached value */
 9	const struct file_operations	*f_op;
10
11	/*
12	 * Protects f_ep, f_flags.
13	 * Must not be taken from IRQ context.
14	 */
15	spinlock_t		f_lock;
16	atomic_long_t		f_count;
17	unsigned int 		f_flags;
18	fmode_t			f_mode;
19	struct mutex		f_pos_lock;
20	loff_t			f_pos;
21	struct fown_struct	f_owner;
22	const struct cred	*f_cred;
23	struct file_ra_state	f_ra;
24
25	u64			f_version;
26#ifdef CONFIG_SECURITY
27	void			*f_security;
28#endif
29	/* needed for tty driver, and maybe others */
30	void			*private_data;
31
32#ifdef CONFIG_EPOLL
33	/* Used by fs/eventpoll.c to link all the hooks to this file */
34	struct hlist_head	*f_ep;
35#endif /* #ifdef CONFIG_EPOLL */
36	struct address_space	*f_mapping;
37	errseq_t		f_wb_err;
38	errseq_t		f_sb_err; /* for syncfs */
39} __randomize_layout
40  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

2.4与自定义的ioctl函数对应

2.4.1do_vfs_ioctl

sys_ioctl函数调用之后,会走到最关键的函数do_vfs_ioctl。这里面会对传入的cmd进行拆解,默认的cmd有下面几种,这些都是预定义的,这些命令会被当作预定义命令被内核处理而不是被设备驱动处理。

 1//kernel/fs/ioctl.c
 2int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
 3	     unsigned long arg)
 4{
 5	int error = 0;
 6	int __user *argp = (int __user *)arg;
 7	struct inode *inode = filp->f_path.dentry->d_inode;
 8
 9	switch (cmd) {
10	case FIOCLEX:
11		set_close_on_exec(fd, 1);
12		break;
13
14	case FIONCLEX:
15		set_close_on_exec(fd, 0);
16		break;
17
18	case FIONBIO:
19		error = ioctl_fionbio(filp, argp);
20		break;
21
22	case FIOASYNC:
23		error = ioctl_fioasync(fd, filp, argp);
24		break;
25
26	case FIOQSIZE:
27		if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode) ||
28		    S_ISLNK(inode->i_mode)) {
29			loff_t res = inode_get_bytes(inode);
30			error = copy_to_user(argp, &res, sizeof(res)) ?
31					-EFAULT : 0;
32		} else
33			error = -ENOTTY;
34		break;
35
36	case FIFREEZE:
37		error = ioctl_fsfreeze(filp);
38		break;
39
40	case FITHAW:
41		error = ioctl_fsthaw(filp);
42		break;
43
44	case FS_IOC_FIEMAP:
45		return ioctl_fiemap(filp, arg);
46
47	case FIGETBSZ:
48		return put_user(inode->i_sb->s_blocksize, argp);
49
50	default:
51		if (S_ISREG(inode->i_mode))//是否为常规文件若是常规文件
52			error = file_ioctl(filp, cmd, arg);
53		else
54			error = vfs_ioctl(filp, cmd, arg);//调用vfs_ioctl
55		break;
56	}
57	return error;
58}

补充说明

以下的ioctl命令对任何文件都是预定义的(属于内核系统级的预定义)

前面的五个预定义根据下面的2.5的协议,可以知道,实际上是0x54代表type为**‘T’**

  • FIOCLEX

设置执行时关闭标志(File IOctl CLose on Exec)。设置了这个标志之后,当调用进程执行一个新程序时,文件描述符将被关闭

  • FIONCLEX

清除执行时关闭标志(File IOctl Not CLose on EXec)。该命令将恢复通常的文件行为,并撤销上述FIOCLEX命令所做的工作。

  • FIOASYNC

设置或复位文件异步通知,注意知道Linux 2.2.4 内核都不正确使用这个命令修改O_SYNC标志。因为这两个动作可以通过fcntl完成,所以实际上没人使用这个命令。

  • FIOQSIZE

该命令返回文件或者目录的大小。不过当用于设备文件时,会导致ENOTTY错误的返回

  • FIONBIO

“File IOctl Non-Blocking I/O”,文件ioctl非阻塞型I/O。改调用秀阿贵filp->f_flags中的O_NONBLOCK标志。传递系统调用的第三个参数指明了是设置还是清除该标准。修改该标志的常用方法是由fcntl系统调用使用F_SETFL命令来完成。

1//kernel/include/uapi/asm-generic/ioctls.h
2#define FIONBIO		0x5421
3#define FIONCLEX	0x5450
4#define FIOCLEX		0x5451
5#define FIOASYNC	0x5452
6#ifndef FIOQSIZE
7# define FIOQSIZE	0x5460
8#endif

另外,还有几个预定义

其实内核cmd有一个格式,使用户cmd不与系统cmd冲突,解决办法就是用_IO_IOW_IOR_IOWR产生cmd。

1//kernel/include/uapi/linux/fs.h
2#define FIGETBSZ   _IO(0x00,2)	/* get the block size used for bmap */
3#define FIFREEZE	_IOWR('X', 119, int)	/* Freeze */
4#define FITHAW		_IOWR('X', 120, int)	/* Thaw */
5#define FS_IOC_FIEMAP			_IOWR('f', 11, struct fiemap)

2.4.1.1inode结构体

这里的函数有一个inode结构体。整个结构是比较复杂的数据结构,我们这里只关注i_mode,这个实际上是用于查看是否是可读可写的标志位。

 1//kernel/include/linux/fs.h
 2struct inode {
 3	umode_t			i_mode;
 4	unsigned short		i_opflags;
 5	kuid_t			i_uid;
 6	kgid_t			i_gid;
 7	unsigned int		i_flags;
 8
 9	const struct inode_operations	*i_op;
10	struct super_block	*i_sb;
11	struct address_space	*i_mapping;
12
13	unsigned long		i_ino;
14	union {
15		const unsigned int i_nlink;
16		unsigned int __i_nlink;
17	};
18	dev_t			i_rdev;
19	loff_t			i_size;
20	struct timespec64	i_atime;
21	struct timespec64	i_mtime;
22	struct timespec64	i_ctime;
23	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
24	unsigned short          i_bytes;
25	u8			i_blkbits;
26	u8			i_write_hint;
27	blkcnt_t		i_blocks;
28
29	/* Misc */
30	unsigned long		i_state;
31	struct rw_semaphore	i_rwsem;
32
33	unsigned long		dirtied_when;	/* jiffies of first dirtying */
34	unsigned long		dirtied_time_when;
35
36	struct hlist_node	i_hash;
37	struct list_head	i_io_list;	/* backing dev IO list */
38#ifdef CONFIG_CGROUP_WRITEBACK
39	struct bdi_writeback	*i_wb;		/* the associated cgroup wb */
40
41	/* foreign inode detection, see wbc_detach_inode() */
42	int			i_wb_frn_winner;
43	u16			i_wb_frn_avg_time;
44	u16			i_wb_frn_history;
45#endif
46	struct list_head	i_lru;		/* inode LRU list */
47	struct list_head	i_sb_list;
48	struct list_head	i_wb_list;	/* backing dev writeback list */
49	union {
50		struct hlist_head	i_dentry;
51		struct rcu_head		i_rcu;
52	};
53	atomic64_t		i_version;
54	atomic64_t		i_sequence; /* see futex */
55	atomic_t		i_count;
56	atomic_t		i_dio_count;
57	atomic_t		i_writecount;
58	union {
59		const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
60		void (*free_inode)(struct inode *);
61	};
62	struct file_lock_context	*i_flctx;
63	struct address_space	i_data;
64	struct list_head	i_devices;
65	union {
66		struct pipe_inode_info	*i_pipe;
67		struct cdev		*i_cdev;
68		char			*i_link;
69		unsigned		i_dir_seq;
70	};
71
72	__u32			i_generation;
73
74	void			*i_private; /* fs or device private pointer */
75} __randomize_layout;

2.4.1.2 常见的几个文件宏

关于几个宏定义,可以见下表所示

文件宏 含义
S_ISLNK(st_mode) 是否是一个连接
S_ISREG 是否是一个常规文件
S_ISDIR 是否是一个目录
S_ISCHR 是否是一个字符设备
S_ISBLK 是否是一个块设备
S_ISFIFO 是否是一个FIFO文件
S_ISSOCK 是否是一个SOCKET文件

下面所示的数据结构定义了stat节点,包含了linux的文件所有的属性

 1//kernel/include/uapi/asm-generic/stat.h
 2struct stat {
 3	unsigned long	st_dev;		 //文件的设备编号
 4	unsigned long	st_ino;		 //节点
 5	unsigned int	st_mode;	//文件的类型和存取的权限
 6	unsigned int	st_nlink;	 //连到该文件的硬连接数目,刚建立的文件值为1
 7	unsigned int	st_uid;		 //用户ID
 8	unsigned int	st_gid;		  //组ID
 9	unsigned long	st_rdev;	 //(设备类型)若此文件为设备文件,则为其设备编号
10	unsigned long	__pad1;
11	long		st_size;	//文件字节数(文件大小)
12	int		st_blksize;	 //块大小(文件系统的I/O 缓冲区大小)
13	int		__pad2;
14	long		st_blocks;	 //块数
15	long		st_atime;	 //最后一次访问时间
16	unsigned long	st_atime_nsec;
17	long		st_mtime;	 //最后一次修改时间
18	unsigned long	st_mtime_nsec;
19	long		st_ctime;	//最后一次改变时间(指属性)
20	unsigned long	st_ctime_nsec;
21	unsigned int	__unused4;
22	unsigned int	__unused5;
23};

2.4.2vfs_ioctl

根据上述调用,来到了下面的vfs_ioctl,这里开始就没有fd这个句柄的参数,只需要file结构体指针

 1//kernel/fs/ioctl.c
 2static long vfs_ioctl(struct file *filp, unsigned int cmd,
 3		      unsigned long arg)
 4{
 5	int error = -ENOTTY;
 6
 7	if (!filp->f_op || !filp->f_op->unlocked_ioctl)
 8		goto out;
 9unlocked_ioctl
10	error = filp->f_op->unlocked_ioctl(filp, cmd, arg);//调用unlocked_ioctl()
11	if (error == -ENOIOCTLCMD)
12		error = -EINVAL;
13 out:
14	return error;
15}	

这里先关注以下filp->f_op->unlocked_ioctl,很明显这里有两个结构体,一个是struct file,另外一个是struct file_operations。对于file结构体,上面2.3.3就有阐述。这里主要是看file_operations结构体。

2.4.3struct file_operations

这里file_operations结构体中的unlocked_ioctl实际上就是函数指针

 1//kernel/include/linux/fs.h
 2struct  file_operations { 
 3	struct module * owner;
 4	loff_t (* llseek ) (struct file *, loff_t , int ); 
 5	ssize_t (* read ) (struct file *, char __user *, size_t , loff_t *); 
 6	ssize_t (* write ) (struct file *, const char __user *, size_t , loff_t *); 
 7	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); 
 8	ssize_t(*write_iter) (struct kiocb *, struct iov_iter *); 
 9	int (*iterate) (struct file *, struct dir_context *); 
10	unsigned int (*poll) (struct file *, struct poll_table_struct *) ; 
11	long (*unlocked_ioctl) (struct file *, unsigned int , unsigned long ); 
12	long (*compat_ioctl) (struct file *, unsigned int , unsigned long ); 
13	int (*mmap) (struct file *, struct vm_area_struct *); 
14	...
15}

2.4.4自定义unlocked_ioctl的真实实现

可以自定义在文件操作集中,那么函数指针实际调用的就是unlocked_ioctl赋值的函数名。下面注册my_ioctl与函数指针unlocked_ioctl关联,直到这里才真正意义上的把ioctl的流程完整的对应起来了。也就是上述2.4.3中的filp->f_op->unlocked_ioctl(filp, cmd, arg)等于这里的my_ioctl(filp, cmd, arg)

1//文件操作集
2struct file_operations misc_fops = {
3    ...
4    .unlocked_ioctl = my_ioctl
5    ...
6};

2.5 ioctl 用户与驱动之间的协议

可以看到下面这个宏定义,这个宏定义包含了协议

1//kernel/include/uapi/asm-generic/ioctl.h
2#define _IOC(dir,type,nr,size) \
3    (((dir)  << _IOC_DIRSHIFT) | \
4     ((type) << _IOC_TYPESHIFT) | \
5     ((nr)   << _IOC_NRSHIFT) | \
6     ((size) << _IOC_SIZESHIFT))

这个协议就是 ioctl 方法第二个参数cmd 。理论上可以为任意 int 型数据,考虑到4个字节,正好是32位。在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 int 型数据划分为四个位段,如下图2所示

图2 用户与驱动之间的协议图 图2 用户与驱动之间的协议图
分区 bit位 含义
第一个分区(nr) 0-7 命令的编号,范围是0-255
第二个分区(type) 8-15 命令的幻数
第三个分区(size) 16-29 表示传递的数据的大小
第四个分区(dir) 30-31 代表读写的方向
  1. nr(number)

    命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;

  2. type(device type)

    设备类型,占据 8 bit,可以为任意 char 型字符,例如‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识

  3. size

    涉及到 ioctl 函数第三个参数 arg ,占据14bit,指定了 arg 的数据类型及长度;

  4. dir(direction)

    ioctl 命令访问模式(数据传输方向),占据 2 bit

    可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据0x00、读数据0x01、写数据0x11、读写数据0x11

 1//kernel/include/uapi/asm-generic/ioctl.h
 2/* used to create numbers */
 3//定义不带参数的 ioctl 命令
 4#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
 5//定义带写参数的 ioctl 命令(copy_from_user)
 6#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
 7//定义带读参数的ioctl命令(copy_to_user)
 8#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
 9//定义带读写参数的 ioctl 命令
10#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
11
12//内核还提供了反向解析 ioctl 命令的宏接口:
13#define _IOC_DIR(nr)        (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)	//方向
14#define _IOC_TYPE(nr)       (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)	//幻数
15#define _IOC_NR(nr)     (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)			//编号
16#define _IOC_SIZE(nr)       (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)	//大小

3举例说明

3.1应用层部分

 1//external/ioctl_test/ioctl_test.c
 2#include<stdio.h>
 3#include<stdlib.h>
 4#include<fcntl.h>
 5#include<string.h>
 6#include<unistd.h>
 7#include<sys/ioctl.h>
 8
 9#define CMD_TEST_0 _IO('A', 0)
10#define CMD_TEST_1 _IOR('A', 1, int)
11#define CMD_TEST_2 _IOW('A', 2, int)
12#define CMD_TEST_3 _IOWR('A', 3, int)
13
14int main(int argc, char *argv[]){
15    int fd = 0;
16    int revData = 0;
17
18    fd = open("/dev/ioctl_test", O_RDWR);
19    if(fd < 0){
20        printf("open failed\n");
21        exit(1);
22    }
23    printf("open success\n");
24
25    /*依次调用四个命令*/
26    ioctl(fd, CMD_TEST_0);
27    
28    revData = ioctl(fd, CMD_TEST_1);
29    printf("receive 1 data=%d\n", revData);
30
31    ioctl(fd, CMD_TEST_2, 99);
32
33    revData = ioctl(fd, CMD_TEST_3, 101);
34    printf("receive 3 data=%d\n", revData);
35
36    close(fd);
37    return 0;
38}

3.2驱动层部分

在 Linux 内核的include/linux目录下有miscdevice.h文件,要把自己定义的misc device从设备定义在这里。定义一个 MISC 设备(miscdevice 类型)以后我们需要设置 minor、name 和 fops 这三个成员变量。minor 表示子设备号,MISC 设备的主设备号为 10,这个是固定的,需要用户指定子设备号,Linux 系统已经预定义了一些 MISC 设备的子设备号。

name 就是此 MISC 设备名字,当此设备注册成功以后就会在/dev 目录下生成一个名为 name的设备文件。fops 就是字符设备的操作集合,MISC 设备驱动最终是需要使用用户提供的 fops操作集合。

3.2.1函数接口

1//完成misc设备注册。
2int misc_register(struct miscdevice * misc);
3//完成misc设备注销。
4int misc_deregister(struct miscdevice *misc);

驱动部分中的_init_exit,关键字实际上也是宏定义,可以点击这里

  1//kernel/drivers/soc/xxx/sdmmc_ioctl_test.c
  2#include <linux/init.h>
  3#include <linux/module.h>
  4#include <linux/miscdevice.h>
  5#include <linux/fs.h>
  6#include <linux/uaccess.h>
  7#include <linux/io.h>
  8
  9#define CMD_TEST_0 _IO('A', 0)       //不需要读写的命令
 10#define CMD_TEST_1 _IOR('A', 1, int) //从内核读取一个int的命令
 11#define CMD_TEST_2 _IOW('A', 2, int) //向内核写入一个int的命令
 12#define CMD_TEST_3 _IOWR('A', 3, int) //读写一个int的命令
 13
 14int misc_open(struct inode *a,struct file *b){
 15    printk("misc open \n");
 16    return 0;
 17}
 18
 19int misc_release (struct inode * a, struct file * b){
 20    printk("misc file release\n");
 21    return 0;
 22}
 23
 24long my_ioctl(struct file *fd, unsigned int cmd, unsigned long b){
 25    /*将命令按内容分解,打印出来*/
 26    printk("cmd type=(%c)\t nr=(%d)\t dir=(%d)\t size=(%d)\n", _IOC_TYPE(cmd), _IOC_NR(cmd), _IOC_DIR(cmd), _IOC_SIZE(cmd));
 27
 28    /* 检查设备类型 */
 29    if (_IOC_TYPE(cmd) != IOC_MAGIC) {
 30        pr_err("[%s] command type [%c] error!\n", \
 31               __func__, _IOC_TYPE(cmd));
 32        return -ENOTTY; 
 33    }
 34
 35    /* 检查序数 */
 36    if (_IOC_NR(cmd) > IOC_MAXNR) { 
 37        pr_err("[%s] command numer [%d] exceeded!\n", 
 38               __func__, _IOC_NR(cmd));
 39        return -ENOTTY;
 40    }
 41    
 42    /* 检查访问模式 */
 43    if (_IOC_DIR(cmd) & _IOC_READ)
 44        ret= !access_ok(VERIFY_WRITE, (void __user *)arg, \
 45                        _IOC_SIZE(cmd));
 46    else if (_IOC_DIR(cmd) & _IOC_WRITE)
 47        ret= !access_ok(VERIFY_READ, (void __user *)arg, \
 48                        _IOC_SIZE(cmd));
 49    if (ret)
 50        return -EFAULT;
 51
 52    switch(cmd){
 53        case CMD_TEST_0://不需要读写的命令
 54            printk("CMD_TEST_0\n");
 55            break;
 56        case CMD_TEST_1://从内核读取一个int的命令
 57            printk("CMD_TEST_1\n");
 58            return 1;
 59            break;
 60        case CMD_TEST_2://向内核写入一个int的命令
 61            printk("CMD_TEST_2 date=(%d)\n",b);
 62            break;
 63        case CMD_TEST_3://读写一个int的命令
 64            printk("CMD_TEST_3 date=(%d)\n",b);
 65            return b+1;
 66            break;
 67    }
 68
 69    return 0;
 70}
 71
 72//文件操作集
 73struct file_operations misc_fops = {
 74    .owner = THIS_MODULE,
 75    .open = misc_open,
 76    .release = misc_release,
 77    .unlocked_ioctl = my_ioctl
 78    .compat_ioctl = my_ioctl
 79};
 80
 81//ioctl_test,设备节点名,添加了这个语句可以在设备中ls找到这个设备节点,并且是c开头的
 82struct miscdevice misc_dev = {
 83    .minor = MISC_DYNAMIC_MINOR,
 84    .name = "ioctl_test",   
 85    .fops = &misc_fops
 86};
 87
 88//这里的__exit,这是一个宏定义,同下面的__init类似
 89static __exit int ioctl_init(void){
 90    int ret;
 91
 92    ret = misc_register(&misc_dev);  //注册杂项设备
 93    if(ret < 0){
 94        printk("misc regist failed\n");
 95        return -1;
 96    }
 97
 98    printk("misc regist succeed\n");
 99    return 0;
100}
101
102//这里的__init,这是一个宏定义
103//最常用的地方是驱动模块初始化函数的定义处,其目的是将驱动模块的初始化函数放入名叫.init.text的输入段
104static __init void ioctl_exit(void){
105    misc_deregister(&misc_dev);
106}
107
108//下面这两个宏,可以展开
109MODULE_LICENSE("Dual BSD/GPL");
110MODULE_AUTHOR("yangyang48");
111
112//这里可以改成module_init
113device_initcall_sync(ioctl_init); 
114module_exit(ioctl_exit);  

MODULE_AUTHOR和MODULE_LICENSE宏定义

这部分其实就是许多宏定义的嵌套,具体可以点击这里,可以详细的宏定义的展开说明

 1//kernel/include/linux/module.h
 2#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
 3#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
 4
 5// 连接形参的宏
 6#define ___module_cat(a,b) __mod_ ## a ## b 
 7#define __module_cat(a,b) ___module_cat(a,b)
 8
 9#ifdef MODULE
10#define __MODULE_INFO(tag, name, info)					  \
11static const char __module_cat(name,__LINE__)[]				  \
12  __used __attribute__((section(".modinfo"), unused, aligned(1)))	  \
13  = __stringify(tag) "=" info
14#else  /* !MODULE */
15/* This struct is here for syntactic coherency, it is not used */
16#define __MODULE_INFO(tag, name, info)					  \
17  struct __module_cat(name,__LINE__) {}
18#endif

MODULE_AUTHOR这部分其实可以直接展开

1 static const char __mod_authorXX[]  \
2 __used  __attribute__((section(".modinfo"),unused,aligned(1)))  \
3 = "author = yangyang48"

同理,MODULE_LICENSE也可以展开

1static const char __mod_licenseXX[]  \
2 __used  __attribute__((section(".modinfo"),unused,aligned(1)))  \
3 = "license= Dual BSD/GPL"

3.2.2驱动的校验部分

上面有一段内容是检验的部分,主要用于对用户自定义的协议进行校验。

  • 如果是cmd中的dir为读取模式,那么是用户空间向内核空间读取数据,那么需要检查用户空间的可写空间地址。
  • 如果是cmd中的dir为可写模式,那么是用户空间向内核空间写数据,那么需要检查用户空间的可写空间地址。
 1/* 检查设备类型 */
 2//这里定义的type是'A'
 3if (_IOC_TYPE(cmd) != IOC_MAGIC) {
 4    pr_err("[%s] command type [%c] error!\n", \
 5           __func__, _IOC_TYPE(cmd));
 6    return -ENOTTY; 
 7}
 8
 9/* 检查序数 */
10//这里定义的序数为4
11if (_IOC_NR(cmd) > IOC_MAXNR) { 
12    pr_err("[%s] command numer [%d] exceeded!\n", 
13           __func__, _IOC_NR(cmd));
14    return -ENOTTY;
15}    
16
17/* 检查访问模式 */
18if (_IOC_DIR(cmd) & _IOC_READ)
19    ret= !access_ok(VERIFY_WRITE, (void __user *)arg, \
20                    _IOC_SIZE(cmd));
21else if (_IOC_DIR(cmd) & _IOC_WRITE)
22    ret= !access_ok(VERIFY_READ, (void __user *)arg, \
23                    _IOC_SIZE(cmd));
24if (ret)
25    return -EFAULT;

补充

这里的access_ok函数,主要是防止内核被外部攻击

1//返回值:布尔值,1表示成功,0表示失败
2//type:检查用户空间地址的权限;VERIFY_READ或者VERIFY_WRITE;
3//<1>VERIFY_READ:驱动是否可以读取用户空间的指定地址;
4//<2>VERIFY_WRITE:驱动是否可以读取用户空间的指定地址;
5//<3>VERIFY_WRITE:驱动在指定用户空间地址既要读取也要写入,也是填这个;
6//addr:用户空间地址;
7//size:要操作的字节数;例如驱动要从指定用户空间地址读取一个int型整数,则size就是sizeof(int);
8int access_ok(int type, const void __user *addr, unsigned long size);

3.2.3调用流程总结

最终得到的调用流程如下所示,通过用户态调用的ioctl函数设备节点/dev/ioctl_test最终在内核态的my_ioctl实现这个流程。

3.3编译

kernel在驱动层的编译,需要依赖于Kconfig和Makefile文件;在应用层的编译,需要依赖于Android.mk。

3.3.1驱动层的编译

  • Kconfig 每个源码目录下提供选项
  • .config 源码顶层目录下保存选择结果
  • Makefile 每个源码目录下根据.config中的内容来告知编译系统如何编译

从这里可以看到,这里也是有一定的语法规则。其中Kconfig用来配置内核,它就是各种配置界面的源文件,内核的配置工具读取各个Kconfig文件,生成配置界面供开发人员配置内核,最后生成配置文件.config

config是关键字,表示一个配置选项的开始;紧跟着的IOCTL_TEST是配置选项的名称,省略了前缀CONFIG_ bool表示变量类型,即CONFIG_IOCTL_TEST 的类型。

字符串“This is IOCTL_TEST”是提示信息,在配置界面中上下移动光标选中它时,就可以通过按空格或回车键来设置CONFIG_IOCTL_TEST 的值

default代表tristate变量的值,一共有三个值y、n和m。如果是n代表所需要的文件不需要被编译进去,这里笔者需要编译进入所有使得,CONFIG_IOCTL_TEST 的值为y。

1config IOCTL_TEST
2    bool "This is IOCTL_TEST"
3    default y
4    help
5      IOCTL TEST support permissions for users and groups beyond the owner/group/world scheme.
6      If you don't know what Access Control Lists are, say N.

然后在Makefile文件中添加这个需要编译的二进制文件

1obj-y += sdmmc_ioctl_test.o
2//可以写成上述的样子
3obj-$(CONFIG_IOCTL_TEST) += sdmmc_ioctl_test.o

3.3.2应用层编译

这个比较简单,就是单纯的Android.mk文件即可

1LOCAL_PATH := $(call my-dir)
2include $(CLEAR_VARS)
3LOCAL_MODULE := ioctl_test
4LOCAL_SRC_FILES += ioctl_test.c
5LOCAL_C_INCLUDE += $(LOCAL_PATH)/
6LOCAL_C_FLAG := -Wreturn-local-addr -Wwriting_strings -fpermissive -fexceptions -Wall -Wno-unused-variable -lgcc_s -std=c99
7#编译生成动态文件so
8include $(BUILD_SHARE_LIBRARY)
9include $(call all_makefiles_under, $(LOCAL_PATH))

4总结

ioctl 是一种面向硬件驱动,是内核比较早的一种用户态内核态的交互方式,侧重于文件系统,方便添加对硬件驱动的处理。

ioctl机制可以在驱动中扩展特定的ioctl消息,用于将一些状态从内核反应到用户态。ioctl有很好的数据同步保护机制,不用担心内核和用户层的数据访问冲突,但是ioctl不适合传输大量的数据

本文通过介绍ioctl函数的流程,涉及到软中断系统调用表file结构指针用户与驱动之间的协议,通过一个demo把整个流程贯穿一遍,更好的熟悉了解到整个ioctl的通信流程。

两个可以参考的源码网站

aospxref.com

kernel.org

5下载

本文源码下载,点击这里

参考

[1]酷派技术团队, Linux用户态与内核态通信方式介绍及选型参考, 2022.

[2] WuYunTaXue, ioctl使用方法, 2021.

[3] pingandezhufu, Linux内核system_call中断处理过程, 2015.

[4] WuYunTaXue, ioctl使用方法, 2022.

[5] 鸿鹄实验室, 浅析syscall, 2021.

[6] 彼 方, Linux系统调用SYSCALL_DEFINE详解, 2021.

[7] maopig, 内核中_init,_exit中的作用, 2012.

[8] Android系统攻城狮, GFP_KERNEL的作用, 2018.

[9] 雪舞飞影, ioctl函数详解(Linux内核 ), 2021.

[10] ora___, ioctl系统调用过程(深入Linux(ARM)内核源码), 2019.

[11] 内核笔记, RK3399平台开发系列讲解(内核入门篇)1.8、IOCTL的用法详解, 2022.

[12] monkea123, 杂项设备(misc device), 2020.

[13] 菜鸟程序员的成长历程,Linux Kernel代码艺术——数组初始化 , 2013.

[14] Felix_8_, Kconfig文件的用途及解析, 2020.

[15] 无棱, Kconfig, 2022.

[16] qq_36412526, MODULE_AUTHOR宏(一), 2018.

[17] c枫_撸码的日子, [读书笔记]高级字符驱动程序(第六章), 2018.

[18] bytxl, S_ISDIR S_ISREG等常见的几个宏, 2012.

[19] 正在起飞的蜗牛, access_ok()函数介绍, 2022.

[20] 宋宝华, 为什么内核访问用户数据之前,要做access_ok?, 2019.