Linux内核与驱动:8.ioctl驱动基础

在 Linux 字符设备驱动开发中,open、read、write 等基础系统调用能够满足设备的基本数据读写需求。然而,当应用程序需要对设备进行更复杂的控制时------比如设置串口的波特率、配置 A/D 转换的采样精度、控制 LED 的闪烁频率等------仅仅依靠 read/write 就显得力不从心了。

ioctl(Input/Output Control)正是为解决这类问题而生的系统调用。它充当了用户空间应用程序与设备驱动程序之间的"特殊指令通道",允许应用程序向驱动程序发送控制命令和配置参数。可以这样理解:字符设备驱动像一个工具箱,基础功能(打开/关闭设备、读写数据)是标配工具,而 ioctl 就像这个工具箱的扩展插槽,当需要特殊操作时,就可以通过添加新的 ioctl 指令来实现额外功能

一个典型的例子是串口驱动:串口数据的收发通过 read/write 操作,而串口的波特率、校验位、停止位则通过 ioctl 进行设置。

1.ioctl函数介绍

用户空间ioctl函数原型为:

cpp 复制代码
int ioctl(int fd, int cmd, ...) ;

fd为文件描述符,cmd为命令码,......为参数;

ioctl 函数调用时,第二个参数 cmd 是一个 32 位的命令码,它并不是一个随意填写的整数,而是经过了精心的编码。Linux 将 32 位的 cmd 划分为四个位段:

位段 位范围 宽度 含义
方向(direction) 31-30 2 bits 数据传输方向
数据大小(size) 29-16 14 bits 传输的数据大小
设备类型(type/幻数) 15-8 8 bits 驱动标识符
命令编号(number) 7-0 8 bits 具体命令序号

我们可以通过位操作来构造cmd命令码,打包比较麻烦,所以内核提供了四个宏来构造命令码,简化了命令的定义过程:

含义 适用场景
_IO(type, nr) 无数据方向的命令 不传输任何参数的命令
_IOR(type, nr, data_type) 从驱动读取数据 获取设备状态、参数
_IOW(type, nr, data_type) 向驱动写入数据 设置设备参数、配置
_IOWR(type, nr, data_type) 双向数据传输 需要同时读写数据的命令

命令宏的使用格式如下:

cpp 复制代码
// 定义幻数(通常用一个ASCII字符标识你的驱动)
#define MY_DEVICE_TYPE 'M'

// 定义各种命令
#define MY_CMD_RESET      _IO(MY_DEVICE_TYPE, 0)          // 无参数命令
#define MY_CMD_GET_STATUS _IOR(MY_DEVICE_TYPE, 1, int)    // 读取状态
#define MY_CMD_SET_CONFIG _IOW(MY_DEVICE_TYPE, 2, struct my_config) // 设置配置
#define MY_CMD_EXCHANGE   _IOWR(MY_DEVICE_TYPE, 3, struct my_data)  // 双向传输

2.驱动中ioctl的实现

在新版 Linux 内核(2.6.36 以后)中,file_operations 结构体中原有的 ioctl 函数指针已被移除,取而代之的是 unlocked_ioctl 和 compat_ioctl 。对于大多数字符设备驱动,只需要现 unlocked_ioctl 即可。

内核中 unlocked_ioctl 的函数原型为:

cpp 复制代码
long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);

驱动ioctl的实现:

cpp 复制代码
#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IO('L',1)
#define CMD_TEST2 _IOW('A',0,int)
#define CMD_TEST3 _IOR('A',1,int)

long cdev_test_ioctl (struct file *filp, unsigned int cmd, unsigned long argv)
{
    int val = 0;
    int ret;
    switch(cmd)
    {
        case CMD_TEST0:
            printk("this is cmd0\n");
            break;
        case CMD_TEST2:
            val = (int)argv;
            printk("cmd2's argv:%d",val);
            break;
        case CMD_TEST3:
            val = 99;
            ret = copy_to_user((int*)argv,&val,sizeof(val));
            if(ret > 0)
            {
                printk("copy to user failed");
                return -1;
            }
            default:
                break;
    }
    return 0;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = cdev_test_open,
    .read = cdev_test_read,
    .write = cdev_test_write,
    .release = cdev_test_release,
    .llseek = cdev_test_llseek,
    .unlocked_ioctl = cdev_test_ioctl,
};

测试程序:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>

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

int main(int argc,char* argv)
{
    int fd = open("/dev/cdev_test_device",O_RDWR);
    if(fd < 0)
    {
        printf("open failed\n");
        perror("open failed");
        return -1;
    }
    int ret;
    // ret = ioctl(fd,CMD_TEST0);
    // if(ret < 0)
    // {
    //     printf("ioctl failed");
    //     return -1;
    // }
    int val;
    ret = ioctl(fd,CMD_TEST3,&val);
    if(ret < 0)
    {
        printf("ioctl failed");
        return -1;
    }
    printf("cmd3 val is %d",val);
    return 0;
}

注意,用户空间和内核空间都要声明:

cpp 复制代码
#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IO('L',1)
#define CMD_TEST2 _IOW('A',0,int)
#define CMD_TEST3 _IOR('A',1,int)

一般情况下可以写到一个头文件中。

3.ioctl传参相关

  • 如果用户程序调用 ioctl(fd, CMD, 100),那么驱动中 arg 的值就是 100

  • 如果用户程序调用 ioctl(fd, CMD, &data),那么驱动中 arg 的值就是 变量 data 在用户空间的虚拟地址

这种设计带来两种完全不同的数据传递方式,需要开发者根据命令码的定义来区分对待。

对比维度 传值(传递整数值) 传地址(传递指针)
用户调用示例 ioctl(fd, CMD, 100) ioctl(fd, CMD, &my_struct)
驱动中 arg 的含义 用户想要传递的数值本身 用户空间缓冲区的虚拟地址
驱动中访问方式 直接使用 arg 必须通过 copy_from_user / copy_to_user
适用数据类型 int,char等标量值 结构体、数组、字符串等复杂数据

温馨提示

  1. **类型转换:**在内核里,arg 是 unsigned long 类型。如果你传的是负数或者指针,记得进行适当的强制类型转换。
  2. **安全风险:**如果 arg 代表的是地址,千万不要在内核里直接解引用它(例如 int v = *(int *)arg;)。这会导致内核崩溃或安全漏洞,务必使用 copy_from_user。

小结

ioctl 是 Linux 设备驱动开发中不可或缺的核心接口。理解它的命令码编码规则、四个命令宏的用途、内核中 unlocked_ioctl 的实现方式以及用户空间的调用方法,是掌握字符设备驱动开发的关键一步。在实际项目中,合理地使用 ioctl 可以让驱动更加灵活、功能更加丰富。

相关推荐
风曦Kisaki2 小时前
Linux服务Day03:自定义YUM仓库、网络YUM仓库(HTTP/FTP)、MariaDB数据库基础操作
linux·网络·数据库
云栖梦泽2 小时前
Linux内核与驱动:7.从应用层 lseek() 到驱动层 .llseek,Linux 字符设备偏移控制详解
linux·c++
xcbeyond2 小时前
Linux 磁盘挂载
linux·运维·服务器
steins_甲乙2 小时前
从0做一个小型内存泄露检测器(2): elf文件的动态链接
c++
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(二十八)——图像格式与编解码:PNG/JPEG全掌握
开发语言·c++·windows·学习·图形渲染·win32
LoneEon2 小时前
Kubernetes高可用集群部署教程
linux·docker·kubernetes
Ricky_Theseus3 小时前
C++静态库
开发语言·c++
洛水水3 小时前
【力扣100题】14.两数相加
c++·算法·leetcode
AlanW3 小时前
# Vcpkg使用总结2
c++