Linux驱动开发—ioctl命令构成,设备驱动基础使用ioctl详解

文章目录

1.什么是ioctl?

ioctl(输入/输出控制)是一种系统调用,用于设备驱动程序与用户空间应用程序之间进行设备特定的输入/输出操作。它通常用于对设备执行标准文件操作(如读取、写入、打开和关闭)之外的特殊操作。ioctl提供了一种通用的机制,通过文件描述符与设备进行交互,支持各种类型的设备操作和控制。

ioctl的工作原理如下:

  1. 定义命令和参数:设备驱动程序和用户空间应用程序之间的交互是通过命令和参数来实现的。命令通常是一个宏,表示特定的操作,而参数则是传递给驱动程序的数据。

  2. 调用ioctl系统调用 :用户空间应用程序通过文件描述符调用ioctl系统调用,传递命令和参数。例如:

    c 复制代码
    int ioctl(int fd, unsigned long request, ...);

    这里,fd是文件描述符,request是设备特定的操作命令,后面的省略号表示可选的参数。

  3. 设备驱动处理 :驱动程序在内核空间接收到ioctl请求后,根据传入的命令和参数执行相应的操作。驱动程序通常会在其文件操作结构中实现一个ioctl处理函数,用于处理不同的命令。

示例

假设有一个字符设备,需要通过ioctl来设置设备的某个参数:

  1. 定义命令

    c 复制代码
    #define MY_IOCTL_SET_PARAM _IOW('M', 1, int)
  2. 用户空间调用

    c 复制代码
    int fd = open("/dev/my_device", O_RDWR);
    int param = 42;
    ioctl(fd, MY_IOCTL_SET_PARAM, &param);
    close(fd);
  3. 设备驱动实现

    c 复制代码
    static long my_device_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
    {
        switch (cmd) {
            case MY_IOCTL_SET_PARAM:
                int param;
                if (copy_from_user(&param, (int __user *)arg, sizeof(param)))
                    return -EFAULT;
                // 设置设备参数
                break;
            default:
                return -EINVAL;
        }
        return 0;
    }

2.ioctl 与 write,read 有什么区别?

ioctlwriteread 的区别在于用途和灵活性。虽然 writeread 是用于基本的输入/输出操作的常见系统调用,但 ioctl 提供了更灵活的方式来处理设备特定的控制操作。

复杂的设备控制

writeread 主要用于传输数据,但设备通常需要进行更复杂的控制操作,例如:

  • 改变设备的配置参数。
  • 执行特定的硬件操作,如调整音量、设置网络接口参数、获取设备状态等。

例如,假设你有一个音频设备,你可能需要调整音量、均衡器设置等,这些操作并不适合通过 writeread 来实现,而更适合通过 ioctl 进行:

define AUDIO_SET_VOLUME _IOW('A', 1, int)

int fd = open("/dev/audio", O_RDWR);
int volume = 10;
ioctl(fd, AUDIO_SET_VOLUME, &volume);
close(fd);

多种数据类型和操作

ioctl 可以处理不同类型的数据和操作,而 writeread 仅限于简单的数据传输。ioctl 可以支持多种数据类型和复杂的控制逻辑,例如:

  • 传递结构体。

  • 执行读写组合操作。

    struct device_status {
    int power;
    int signal_strength;
    };

    #define DEVICE_GET_STATUS _IOR('D', 1, struct device_status)

    struct device_status status;
    ioctl(fd, DEVICE_GET_STATUS, &status);

简化应用层代码

如果所有控制操作都通过 writeread 实现,应用层代码可能需要处理大量的特定协议逻辑,这增加了代码复杂性。ioctl 使得驱动程序可以集中处理这些复杂性,简化应用层代码。

区分数据和控制

writeread 本质上是用于数据传输的,而 ioctl 专门用于控制操作。将数据传输和控制操作分开,可以使代码更加清晰和模块化。

3.ioctl命令的构成

设备类型 代表这一类设备,一般用一个字母或一个8bit 数字来表示
序列号 代表是这个设备的第几个命令
方向 表示命令的方向,只读(10) 只写(01) 写读(11) 无数据(00)
数据大小 用户数据的大小,传递是数据类型,比如传递四个字节,可以写int

ioctl 命令的构成一般包括一个由四个部分组成的宏定义,这四个部分分别指定了命令的类型、命令的编号、数据的方向以及数据的大小 。这种构成方式在 Linux 中非常常见。具体来说,一个 ioctl 命令通常使用如下的宏定义来构建:

c 复制代码
_IO(type, nr)          // 无参数的命令
_IOR(type, nr, size)   // 从设备读取数据
_IOW(type, nr, size)   // 向设备写入数据
_IOWR(type, nr, size)  // 读写数据

宏定义的组成部分

  1. type:一个字符,用于表示设备类型。通常使用一个唯一的字符来标识设备类别,例如 'T' 表示终端设备,'S' 表示串行设备等。

  2. nr:命令编号。一个整数,用于区分同一类型设备的不同命令。

  3. size:数据的大小。通常是数据结构的大小,用于指定传递给命令的数据大小。

具体的宏定义

这些宏的具体定义在系统头文件 <linux/ioctl.h> 中。以下是每个宏的详细说明:

_IO(type, nr)

定义一个无参数的 ioctl 命令:

c 复制代码
#define MY_IOCTL_NO_PARAM _IO('M', 1)

_IOR(type, nr, size)

定义一个从设备读取数据的 ioctl 命令:

c 复制代码
#define MY_IOCTL_READ _IOR('M', 2, int)

_IOW(type, nr, size)

定义一个向设备写入数据的 ioctl 命令:

c 复制代码
#define MY_IOCTL_WRITE _IOW('M', 3, int)

_IOWR(type, nr, size)

定义一个读写数据的 ioctl 命令:

c 复制代码
#define MY_IOCTL_READ_WRITE _IOWR('M', 4, struct my_struct)

举个栗子

假设有一个设备需要以下 ioctl 命令:

  1. 设置参数(无参数)。
  2. 获取状态(读取一个整数)。
  3. 设置配置(写入一个结构体)。
  4. 获取和设置配置(读写一个结构体)。

可以定义如下的 ioctl 命令:

c 复制代码
#define MY_IOCTL_SET_PARAM _IO('M', 1)
#define MY_IOCTL_GET_STATUS _IOR('M', 2, int)
#define MY_IOCTL_SET_CONFIG _IOW('M', 3, struct my_config)
#define MY_IOCTL_GET_SET_CONFIG _IOWR('M', 4, struct my_config)

struct my_config {
    int setting1;
    int setting2;
};

4.ioctl命令的分解

OCTL(输入输出控制)命令由 32 位整数表示,它包含了方向、类型、编号和大小等信息。我们将通过分解一个具体的 IOCTL 命令来理解其构成和解码方法。

宏定义

在 Linux 内核中,IOCTL 宏通常定义在 <linux/ioctl.h> 中。这里是一些关键宏的定义

#define _IOC_DIR(nr)   (((nr) >> _IOC_DIRSHIFT) & ((1 << _IOC_DIRBITS) - 1))
#define _IOC_TYPE(nr)  (((nr) >> _IOC_TYPESHIFT) & ((1 << _IOC_TYPEBITS) - 1))
#define _IOC_NR(nr)    (((nr) >> _IOC_NRSHIFT) & ((1 << _IOC_NRBITS) - 1))
#define _IOC_SIZE(nr)  (((nr) >> _IOC_SIZESHIFT) & ((1 << _IOC_SIZEBITS) - 1))

举个栗子

假设我们有以下 IOCTL 命令定义:

#define DEVICE_CMD3 _IOW('M', 2, int)

我们将分解这个命令,并解码其各个部分。

首先,看看 _IOW 宏的定义:

#define _IOW(type, nr, size)  _IOC(_IOC_WRITE, (type), (nr), sizeof(size))

将参数传入 _IOC 宏:

#define DEVICE_CMD3 _IOC(_IOC_WRITE, 'M', 2, sizeof(int))

让我们计算各个部分:

  • 方向 (_IOC_WRITE) :值为 1U
  • 类型 ('M') :ASCII 值为 0x4D
  • 编号 (2) :值为 2
  • 大小 (sizeof(int)) :在大多数平台上为 4

计算

#define _IOC_WRITE 1U
#define 'M' 0x4D
#define nr 2
#define size 4

使用 _IOC 宏计算值:

#define DEVICE_CMD3 _IOC(_IOC_WRITE, 'M', 2, sizeof(int)) = \
    (((1U) << _IOC_DIRSHIFT) | ((0x4D) << _IOC_TYPESHIFT) | ((2) << _IOC_NRSHIFT) | ((4) << _IOC_SIZESHIFT))

分解为二进制并位移:

  • 方向 (dir):1U << 30 = 0x40000000
  • 类型 (type):0x4D << 8 = 0x004D00
  • 编号 (nr):2 = 0x02
  • 大小 (size):4 << 16 = 0x00040000

组合这些部分:

0x40000000 | 0x00004D00 | 0x00000002 | 0x00040000 = 0x40044D02

解码

使用解码宏来验证这些值:

#include <stdio.h>
#include <linux/ioctl.h>

#define DEVICE_CMD3 _IOW('M', 2, int)

int main() {
    printf("DEVICE_CMD3 value: 0x%x\n", DEVICE_CMD3);
    printf("Type: %c\n", _IOC_TYPE(DEVICE_CMD3));
    printf("Number: %d\n", _IOC_NR(DEVICE_CMD3));
    printf("Direction: %d\n", _IOC_DIR(DEVICE_CMD3));
    printf("Size: %d\n", _IOC_SIZE(DEVICE_CMD3));

    return 0;
}

运行代码,输出如下:

DEVICE_CMD3 value: 0x40044d02
Type: M
Number: 2
Direction: 1
Size: 4

其中Write为 01 也就是代表方向为1

5.ioctl 设备使用

应用程序构成

ioctl 是一个用于设备控制的系统调用,允许用户空间应用程序与内核空间的设备驱动程序进行交互。它的主要功能是向设备发送控制命令或从设备读取状态信息。ioctl 的原型定义在 <sys/ioctl.h> 头文件中:

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

参数分析

  1. int fd
    • 表示文件描述符,是一个整数,通过 open 系统调用获取。该文件描述符指向需要控制的设备文件。
  2. unsigned long request
    • 表示控制命令,是一个无符号长整型。控制命令通常是一个通过宏定义的常量,用于指定需要执行的操作。命令值可以是 _IO, _IOR, _IOW, _IOWR 之一。
  3. ...
    • 表示可选参数。这些参数取决于 request 命令的类型,有些命令需要一个指针作为参数,有些则不需要。
    • 如果命令不需要参数,这个位置可以省略;
    • 如果需要参数,通常是一个指向数据结构的指针,用于传递数据或从设备读取数据。

返回值

  • 成功时,ioctl 返回 0
  • 失败时,返回 -1 并设置 errno 以指示错误类型。

无参数版

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>

// Define IOCTL commands
#define DEVICE_CMD0 _IO('M', 0)
#define DEVICE_CMD1 _IO('M', 1)
#define DEVICE_CMD2 _IO('L', 0)
#define DEVICE_CMD3 _IOW('M', 2, int)
#define DEVICE_CMD4 _IOR('M', 3, int)
#define DEVICE_CMD5 _IOWR('M', 4, int)

#define DEVICE_PATH "/dev/countdown_device"

int main()
{
    int fd;
    int ret;
    fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    // 每秒传递一次
    while (1)
    {
        ret = ioctl(fd, DEVICE_CMD0); // 两个参数代表没有传递参数
        sleep(1);
    }

    return 0;
}

驱动程序构成

驱动程序中,也要定义相同的命令 ,否则无法解析命令,可以提取出一个头文件,专门用来放置IOCTL宏命令

同样的文件结构体需要实现IOCTL操作

具体函数原型如下:

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
  • file对应的是 应用程序的fd
  • unsigned int cmd 对应的是具体宏命令
  • 第三个参数对应的是参数

具体代码 (无命令参数版)

static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    switch (cmd)
    {
    case DEVICE_CMD0:
        printk(" null parmater cmd is invoke...");
        break;
    case DEVICE_CMD1:
        
        break;
    case DEVICE_CMD2:
      
        break;
    case DEVICE_CMD3:
        
        break;
    default:
        return -ENOTTY; // 不支持的命令
    }
    return 0;
}
static struct file_operations fops = {
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write,
    .unlocked_ioctl = device_ioctl
    };

调用效果:(无参数版本)

相关推荐
朝九晚五ฺ2 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream2 小时前
Linux的桌面
linux
xiaozhiwise2 小时前
Makefile 之 自动化变量
linux
意疏4 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu4 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿5 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
我的K84096 小时前
Flink整合Hudi及使用
linux·服务器·flink
1900436 小时前
linux6:常见命令介绍
linux·运维·服务器
Camellia-Echo6 小时前
【Linux从青铜到王者】Linux进程间通信(一)——待完善
linux·运维·服务器