Skip to content

LV025-ioctl控制

一、概述

1. 背景

一般情况下,一个字符设备的驱动,除了读取和写入设备之外,大部分的驱动程序都需要通过设备驱动程序来执行各种类型的硬件控制。例如,针对串口设备,驱动层除了需要提供对串口的读写,还需要提供对串口波特率、校验位、以及流控等配置信息的控制。

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

2. ioctl 简介

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

二、应用层的 ioctl

1. ioctl 函数

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

c
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

c
/*
 * 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. 命令定义实例

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

c
#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 *

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

参数说明

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

2. 函数实现

2.1 命令定义

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

2.2 sdev_ioctl

c
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

c
#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

c
#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. 开发板测试

shell
# 加载驱动
insmod xxx_demo.ko

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

# 卸载驱动
rmmod xxx_demo.ko