LV06-03-chrdev-05-ioctl的使用

本文主要是字符设备驱动——使用ioctl函数实现字符设备控制的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、概述

1. 背景

一般情况下,一个字符设备的驱动,除了读取和写入设备之外,大部分的驱动程序都需要通过设备驱动程序来执行各种类型的硬件控制。

例如,针对串口设备,驱动层除了需要提供对串口的读写,还需要提供对串口波特率、校验位、以及流控等配置信息的控制。

这些配置信息需要从应用层传递一些基本数据,相比普通的读写数据,控制数据仅仅也只是数据类型不同。同时传输的控制信息,数据量一般情况下也不会太大。

2. ioctl简介

ioctl 是设备驱动程序中用来控制设备的接口函数,一个字符设备驱动通常需要实现设备的 打开、关闭、读取、写入等功能,而在一些需要细分的情况下,就需要扩展新的功能,通常以增设 ioctl()命令的方式来实现。

二、应用层的ioctl

1. ioctl函数

我们可以使用man命令来查看ioctl函数的说明:

1
2
3
4
5
6
7
NAME
ioctl - control device

SYNOPSIS
#include <sys/ioctl.h>

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

该函数用于向设备发送控制和配置命令。用户程序所作的只是通过命令码cmd告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情,而ioctl就是负责接收cmd命令码来实现这些命令,它保证了程序的有序和整洁。

参数:

  • fd :是用户程序打开设备时返回的文件描述符。
  • cmd :是用户程序对设备的控制命令。
  • args:应用程序向驱动程序下发的参数,如果传递的参数为指针类型,则可以接收驱动向 用户空间传递的数据。

返回值:成功返回0;失败返回-1,同时设置errno。

2. 命令说明

在这个ioctl系统调用过程中,有一个非常关键的参数,就是cmd。其由用户空间直接不经修改的传递给驱动程序。大小为4个字节,在其定义中该参数被分为四个字段。

image-20241204194056970
  • cmd[31:30]:dir(direction),ioctl命令访问模式(属性数据传输方向),占据2bit,表示是由内核空间到用户空间,或是用户空间到内核空间
  • cmd[29:16]:size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数。
  • cmd[15:8]:type(device type),设备类型,一个驱动程序一般使用一个 type,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”(也就是说来源没有依据),可以为任意char型字符,例如 ‘a’、’b’、’c’ 等等,其主要作用是使ioctl命令有唯一的设备标识。
  • cmd[7:0]:nr(number),命令编号或者叫序数,表示这个设备的第几个命令,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl命令,通常从 0 开始编号递增。

<linux/ioctl.h>中包含的<asm/ioctl.h>头文件定义了一些构造命令编号的宏。我们可以参考一下这里ioctl.h - include/uapi/asm-generic/ioctl.h - Used to create numbers

1
2
3
4
5
6
7
8
9
10
/*
* Used to create numbers.
*
* NOTE: _IOW means userland is writing and kernel is reading. _IOR
* means userland is reading and kernel is writing.
*/
#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用于构造无数据传输的命令编号。_IOR用于构造从驱动程序中读取数据的命令编号。_IOW用于构造向设备写入数据的命令编号。_IOWR用于构造双向传输命令编号。

3. 命令定义实例

例如可以使用以下代码定义不需要参数、向驱动程序写参数、向驱动程序读参数三个宏:

1
2
3
#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IOW('L',1,int)
#define CMD_TEST2 _IOR('L',2,int)

三、驱动层的ioctl

1. unlocked_ioctl

应用程序中 ioctl 函数会调用 file_operation 结构体中的 unlocked_ioctl 接口,接口定义如下:fs.h - include/linux/fs.h - *unlocked_ioctl *

1
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

参数:

  • struct file * 参数:文件描述符。
  • unsigned int 参数:与应用程序的 cmd 参数对应,在驱动程序中对传递来的 cmd 参数进行判断从而 做出不同的动作。
  • unsigned long参数:与应用程序的 arg 参数对应,从而实现内核空间和用户空间参数的传递。

2. 函数实现

2.1 命令定义

1
2
3
#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)

2.2 sdev_ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static long sdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int val = 0;//定义 int 类型向应用空间传递的变量 val
switch(cmd)
{
case CMD_TEST0:
printk("this is CMD_TEST0\n");
break;
case CMD_TEST1:
printk("this is CMD_TEST1\n");
printk("arg is %ld\n",arg);//打印应用空间传递来的 arg 参数
break;
case CMD_TEST2:
val = 1;//将要传递的变量 val 赋值为 1
printk("this is CMD_TEST2\n");
if(copy_to_user((int *)arg, &val, sizeof(val)) != 0)
{
//通过 copy_to_user 向用户空间传递数据
printk("copy_to_user error \n");
}
break;
default:
break;
}

return 0;
}

四、使用实例

1. 源码编写

1.1 chrdev_ioctl_demo_app.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <sys/ioctl.h>

#define BUFSIZE 32 /* 设置最大偏移量为64, 方便打印完整的内存空间数据*/
#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)

static char usrdata[] = {"sumu"};

void usage_info(void)
{
printf("\n");
printf("+++++++++++++++++++++++++++++++++++++++++\n");
printf("+ help information @sumu +\n");
printf("+++++++++++++++++++++++++++++++++++++++++\n");

printf("help:\n");
printf("use format: ./app_name /dev/device_name arg1 ... \n");
printf(" ./chrdev_data_exchange_demo_app.out /dev/sdevice 1 x # 从x位置读取 \n");
printf(" ./chrdev_data_exchange_demo_app.out /dev/sdevice 2 x # 从x位置写入 \n");
printf(" ./chrdev_data_exchange_demo_app.out /dev/sdevice 3 x # x为任意值 \n");
printf(" 驱动中buf最大为1024字节 \n");
printf("\n");

printf("command info:\n");
printf(" (1)load module : insmod module_name.ko\n");
printf(" (2)unload module: rmmod module_name.ko\n");
printf(" (3)show module : lsmod\n");
printf(" (4)view device : cat /proc/devices\n");
printf(" (5)create device node: mknod /dev/device_name c major_num secondary_num \n");
printf(" (6)show device node : ls /dev/device_name \n");
printf(" (7)show device vlass : ls /sys/class \n");
printf("+++++++++++++++++++++++++++++++++++++++++\n");
}

int main(int argc, char *argv[])
{
int fd = -1;
int ret = 0;
char *filename = NULL;
unsigned int arg1 = 0;
unsigned int arg2 = 0;

char readbuf[BUFSIZE] = {0};
char writebuf[BUFSIZE] = {0};

unsigned int off1 = 0; // 定义读写偏移位置
unsigned int off2 = 0; // 定义读写偏移位置
unsigned int off = 0; // 定义读写偏移位置

printf("*** Build Time: %s %s,Git Version: %s Git Remote: %s***\n",
__DATE__, __TIME__, GIT_VERSION, GIT_PATH);
// ./xxx.out /dev/sdevice x x
if (argc != 4)
{
usage_info();
return -1;
}
// 解析参数
filename = argv[1];
arg1 = atoi(argv[2]);
arg2 = atoi(argv[3]);
printf("%s %s %d %d\n", argv[0], filename, arg1, arg2);

/* 打开驱动文件 */
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("can't open file %s !\n", filename);
return -1;
}

off1 = lseek(fd, 0, SEEK_END); // 读取字符设备文件可读写最大的大小
printf("%s mem buf size is %d\n", filename, off1);
if (arg1 == 1)
{
off = arg2;// 获取读写的偏移位置
/* 从驱动文件读取数据 */
ret = lseek(fd, off, SEEK_SET); // 将偏移量设置为距离起始地址 off的位置
if (ret < 0)
{
printf("lseek %s %d failed!\n", filename, off);
}

ret = read(fd, readbuf, BUFSIZE);
if (ret < 0)
{
printf("read file %s failed!\n", filename);
}
off2 = lseek(fd, 0, SEEK_CUR); // 读取当前位置的偏移量
/* 读取成功,打印出读取成功的数据 */
printf("read data \"%s\" from %s! off=%d off2=%d\n", readbuf, filename, off, off2);

}
else if (arg1 == 2)
{
off = arg2;// 获取读写的偏移位置
ret = lseek(fd, off, SEEK_SET); // 将偏移量设置为距离起始地址 off的位置
if (ret < 0)
{
printf("lseek %s %d failed!\n", filename, off);
}

/* 向设备驱动写数据 */
memcpy(writebuf, usrdata, sizeof(usrdata));
ret = write(fd, writebuf, sizeof(usrdata));
if (ret < 0)
{
printf("write file %s failed!\n", filename);
}
off2 = lseek(fd, 0, SEEK_CUR); // 读取当前位置的偏移量
printf("write \"%s\" to %s success!off=%d off2=%d\n", usrdata, filename, off, off2);
}
else if (arg1 == 3)
{
int val = 0;

ioctl(fd,CMD_TEST0);
ioctl(fd,CMD_TEST1, 1);
ioctl(fd,CMD_TEST2, &val);
printf("val = %d\n", val);
}
/* 关闭设备 */
ret = close(fd);
if (ret < 0)
{
printf("can't close file %s !\n", filename);
return -1;
}

return 0;
}

1.2 chrdev_ioctl_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

#define CHRDEV_NAME "sdev" /* 设备名,cat /proc/devices 查看与设备号的对应关系 */
#define CLASS_NAME "sclass" /* 类名,在 /sys/class 中显示的名称 */
#define DEVICE_NAME "sdevice" /* 设备节点名,在 /sys/class/class_name/ 中显示的名称以及 /dev/ 下显示的节点名 */
#define BUFSIZE 32 /* 设置最大偏移量为32 */

#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)
#define CMD_TEST3 _IOW('S', 3, int)

struct __CMD_TEST{
int a;
int b;
int c;
};

static dev_t dev_num; // 定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
static struct cdev g_cdev_dev; // 定义cdev结构体类型的变量g_cdev_dev
static struct class *p_class_dev; // 定于struct class *类型结构体变量 p_class_dev,表示要创建的类
static char mem[BUFSIZE] = {0}; // 设置数据存储数组mem

static int sdev_open(struct inode *inode, struct file *file)
{
printk("This is sdev_open!\n");
return 0;
}

static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;

if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_to_user(buf, mem + offset, count))
{
// 将mem中的值写入buf,并传递到用户空间
printk("copy_to_user error!\n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("mem buf[%d] %c\n", i, mem[i]); // 将mem中的值打印出来
}
printk("read offset is %llu, count is %d\n", offset, count);
*off = *off + count; // 更新偏移值

return count;
}

static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;
if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_from_user(mem + offset, buf, count))
{
// 将buf中的值,从用户空间传递到内核空间
printk("copy_to_user error \n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("mem buf[%d] %c\n", i, mem[i]); // 将mem中的值打印出来
}
printk("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
*off = *off + count; // 更新偏移值

return count;
}

static int sdev_release(struct inode *inode, struct file *file)
{
printk("This is sdev_release!\n");
return 0;
}

static loff_t sdev_llseek(struct file *file, loff_t offset, int whence)
{
loff_t new_offset = 0; // 定义loff_t类型的新的偏移值
switch (whence) // 对lseek函数传递的whence参数进行判断
{
case SEEK_SET:
if (offset < 0 || offset > BUFSIZE)
{
return -EINVAL; // EINVAL=22 表示无效参数
}
new_offset = offset; // 如果whence参数为SEEK_SET,则新偏移值为offset
break;
case SEEK_CUR:
if ((file->f_pos + offset < 0) || (file->f_pos + offset > BUFSIZE))
{
return -EINVAL;
}
new_offset = file->f_pos + offset; // 如果whence参数为SEEK_CUR,则新偏移值为file->f_pos + offset,file->f_pos为当前的偏移值
break;
case SEEK_END:
if (file->f_pos + offset < 0)
{
return -EINVAL;
}
new_offset = BUFSIZE + offset; // 如果whence参数为SEEK_END,则新偏移值为BUFSIZE + offset,BUFSIZE为最大偏移量
break;
default:
break;
}
file->f_pos = new_offset; // 更新file->f_pos偏移值

return new_offset;
}

static long sdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int val = 0;//定义 int 类型向应用空间传递的变量 val
switch(cmd)
{
case CMD_TEST0:
printk("this is CMD_TEST0\n");
break;
case CMD_TEST1:
printk("this is CMD_TEST1\n");
printk("arg is %ld\n",arg);//打印应用空间传递来的 arg 参数
break;
case CMD_TEST2:
val = 1;//将要传递的变量 val 赋值为 1
printk("this is CMD_TEST2\n");
if(copy_to_user((int *)arg, &val, sizeof(val)) != 0)
{
//通过 copy_to_user 向用户空间传递数据
printk("copy_to_user error \n");
}
break;
case CMD_TEST3:
{
struct __CMD_TEST cmd_test3 = {0};
if (copy_from_user(&cmd_test3, (int *)arg, sizeof(cmd_test3)) != 0)
{
printk("copy_from_user error\n");
}
printk("cmd_test3.a = %d\n", cmd_test3.a);
printk("cmd_test3.b = %d\n", cmd_test3.b);
printk("cmd_test3.c = %d\n", cmd_test3.c);
break;
}
default:
break;
}

return 0;
}

static struct file_operations g_cdev_dev_ops = {
.owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = sdev_open,
.read = sdev_read,
.write = sdev_write,
.release = sdev_release,
.llseek = sdev_llseek,
.unlocked_ioctl = sdev_ioctl,
}; // 定义file_operations结构体类型的变量g_cdev_dev_ops

// 模块入口函数
static int __init chrdev_ioctl_demo_init(void)
{
int ret; // 定义int类型的变量ret,用来判断函数返回值
int major, minor; // 定义int类型的主设备号major和次设备号minor
printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
printk("chrdev_ioctl_demo module init!\n");

ret = alloc_chrdev_region(&dev_num, 0, 1, CHRDEV_NAME); // 自动获取设备号,设备名为chrdev_name
if (ret < 0)
{
printk("alloc_chrdev_region is error!\n");
}
printk("alloc_register_region is ok!\n");
major = MAJOR(dev_num); // 使用MAJOR()函数获取主设备号
minor = MINOR(dev_num); // 使用MINOR()函数获取次设备号
printk("major is %d, minor is %d !\n", major, minor);

cdev_init(&g_cdev_dev, &g_cdev_dev_ops); // 使用cdev_init()函数初始化g_cdev_dev结构体,并链接到g_cdev_dev_ops结构体
g_cdev_dev.owner = THIS_MODULE; // 将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
ret = cdev_add(&g_cdev_dev, dev_num, 1); // 使用cdev_add()函数进行字符设备的添加
if (ret < 0)
{
printk("cdev_add is error !\n");
}
printk("cdev_add is ok !\n");
p_class_dev = class_create(THIS_MODULE, CLASS_NAME); // 使用class_create进行类的创建,类名称为 class_dev
device_create(p_class_dev, NULL, dev_num, NULL, DEVICE_NAME); // 使用device_create进行设备的创建,设备名称为 device_dev

return 0;
}

// 模块出口函数
static void __exit chrdev_ioctl_demo_exit(void)
{
// 需要注意的是, 字符设备的注册要放在申请字符设备号之后,
// 字符设备的删除要放在释放字符驱动设备号之前。
cdev_del(&g_cdev_dev); // 使用cdev_del()函数进行字符设备的删除
unregister_chrdev_region(dev_num, 1); // 释放字符驱动设备号
device_destroy(p_class_dev, dev_num); // 删除创建的设备
class_destroy(p_class_dev); // 删除创建的类
printk("chrdev_ioctl_demo exit!\n");
}

module_init(chrdev_ioctl_demo_init); // 将__init定义的函数指定为驱动的入口函数
module_exit(chrdev_ioctl_demo_exit); // 将__exit定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_ioctl_demo 查看) */
MODULE_LICENSE("GPL v2"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

2. 开发板测试

1
2
3
4
5
6
7
8
# 加载驱动
insmod xxx_demo.ko

# app测试
./xxx_demo_app.out /dev/dev_node 3 x # ioctl测试

# 卸载驱动
rmmod xxx_demo.ko