Heng30的博客
搜索 分类 关于 订阅

如何给Linux字符设备驱动实现ioctl接口?

2025-03-01

应用层客户端程序使用ioctl来控制设备的状态是一种很常见的情况。下面的例子就演示了如何去实现一个这样的字符串设备驱动。

驱动实例代码

// simple_cdev.c

#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/device/class.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/minmax.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define DEV_MINOR_COUNTS 1
#define DEV_NAME "simple_cdev"

// 封装命令码
#define LED_CTL_CMD _IOW('L', 0, led_status_t *)
#define LED_GET_CMD _IOR('L', 1, led_status_t *)

typedef enum { LED1_ON = 0, LED1_OFF = 1, LED1_STATUS = 2 } led_status_t;

typedef struct {
    struct cdev *cdev;        // 字符设备
    struct class *class;      // 设备类
    struct device *dev;       // /dev目录下的设备
} simple_cdev_t;

static simple_cdev_t my_cdev = {0};

static int _open(struct inode *inode, struct file *file) {
    pr_info("call simple_cdev_open\n");
    return 0;
}

static int _release(struct inode *inode, struct file *file) {
    pr_info("call simple_cdev_release\n");
    return 0;
}

static long _unlocked_ioctl(struct file *file, unsigned int request_cmd,
                            unsigned long arg) {
    led_status_t status;
    static bool is_led1_on = false;

    int remain_len = copy_from_user(&status, (void *)arg, sizeof(led_status_t));
    if (remain_len > 0) {
        pr_err("unlocked_ioctl copy_from_user failed\n");
        return -EIO;
    }

    switch (request_cmd) {
    case LED_CTL_CMD: {
        if (status == LED1_ON) {
            is_led1_on = true;
            pr_info("unlocked_ioctl_LED1_ON\n");
            return 0;
        } else if (status == LED1_OFF) {
            is_led1_on = false;
            pr_info("unlocked_ioctl_LED1_OFF\n");
            return 0;
        }
        break;
    }
    case LED_GET_CMD: {
        if (status == LED1_STATUS) {
            pr_info("unlocked_ioctl_LED1_STATUS\n");
            return (long)is_led1_on;
        }
        break;
    }
    default:
        break;
    }
    return -1;
}

static struct file_operations fops = {
    .open = _open,
    .release = _release,
    .unlocked_ioctl = _unlocked_ioctl,
};

static int __init simple_cdev_init(void) {
    pr_info("simple_cdev_init\n");

    my_cdev.cdev = cdev_alloc();
    if (!my_cdev.cdev) {
        pr_err("simple_cdev cdev_alloc failed!\n");
        return -ENOMEM;
    }

    // 初始化字符设备
    cdev_init(my_cdev.cdev, &fops);

    // 申请设备号, 由系统分配主设备号和一个从设备号
    int ret =
        alloc_chrdev_region(&my_cdev.cdev->dev, 0, DEV_MINOR_COUNTS, DEV_NAME);
    if (ret) {
        pr_err("simple_cdev alloc_chrdev_region failed!\n");
        return ret;
    }

    pr_info("simple_cdev major = %d\n", MAJOR(my_cdev.cdev->dev));

    // 添加到内核
    ret = cdev_add(my_cdev.cdev, my_cdev.cdev->dev, DEV_MINOR_COUNTS);
    if (ret) {
        pr_err("simple_cdev cdev_add failed!\n");
        return ret;
    }

    // 申请设备类, 会在/sys/class目录下常见一个simple_cdev的目录
    my_cdev.class = class_create("simple_cdev");
    if (IS_ERR(my_cdev.class)) {
        pr_err("simple_cdev class_create failed\n");
        return PTR_ERR(my_cdev.class);
    }

    // 申请设备对象,会在/dev目录下创建设备文件simple_cdev,
    // 并且在/sys/class/simple_cdev目录下常见一个simple_cdev的设备节点
    my_cdev.dev =
        device_create(my_cdev.class, NULL, my_cdev.cdev->dev, NULL, DEV_NAME);
    if (IS_ERR(my_cdev.dev)) {
        pr_err("simple_cdev device_create failed\n");
        return PTR_ERR(my_cdev.dev);
    }

    return 0;
}

static void __exit simple_cdev_exit(void) {
    pr_info("simple_cdev_exit\n");

    device_destroy(my_cdev.class, my_cdev.cdev->dev);
    class_destroy(my_cdev.class);
    cdev_del(my_cdev.cdev);
    unregister_chrdev_region(my_cdev.cdev->dev, DEV_MINOR_COUNTS);
    kfree(my_cdev.cdev);
}

module_init(simple_cdev_init);
module_exit(simple_cdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("heng30");
MODULE_VERSION("v0.0.1");
MODULE_DESCRIPTION("A simple_cdev kernel module");

编译脚本

#!/bin/sh

top-dir = $(shell pwd)
kernel-version = $(shell uname -r)
kernel-dir ?= /lib/modules/$(kernel-version)/build

obj-m += simple_cdev.o

all:
        make -C $(kernel-dir) modules M=$(top-dir)

clean:
        rm -f *.o *.ko *.mod *.mod.c *.order *.symvers
        make -C $(kernel-dir) clean m=$(top-dir)

客户端程序

// ledctl.c
// 编译命令:gcc -o ledctl ledctl.c

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

#define BUFFER_SIZE 1024

// 封装命令码
#define LED_CTL_CMD _IOW('L', 0, led_status_t *)
#define LED_GET_CMD _IOR('L', 1, led_status_t *)

typedef enum { LED1_ON = 0, LED1_OFF = 1, LED1_STATUS = 2 } led_status_t;

static char buffer[BUFFER_SIZE];

int main(int argc, char *argv[]) {
    if (argc != 2) {
        perror("please input a device to control");
        return -1;
    }

    led_status_t status;
    int ret = 0, fd = open(argv[1], O_RDWR);
    assert(fd >= 0);

    while (true) {
        fgets(buffer, BUFFER_SIZE - 1, stdin);

        switch (buffer[0]) {
        case '0': {
            status = LED1_ON;
            ret = ioctl(fd, LED_CTL_CMD, &status);
            if (ret < 0) {
                printf("LED1_ON ioctl error: %d\n", ret);
            }
            break;
        }
        case '1': {
            status = LED1_OFF;
            ret = ioctl(fd, LED_CTL_CMD, &status);
            if (ret < 0) {
                printf("LED1_OFF ioctl error: %d\n", ret);
            }
            break;
        }
        case '2': {
            status = LED1_STATUS;
            ret = ioctl(fd, LED_GET_CMD, &status);
            printf("LED1_STATUS = %s\n", ret ? "ON" : "OFF");
            break;
        }
        default:
            break;
        }
    }

    close(fd);
}

测试

  • 安装驱动:insmod simple_cdev.ko

  • ./ledctl /dev/simple_cdev

2
LED1_STATUS = OFF
0
2
LED1_STATUS = ON
1
2
LED1_STATUS = OFF
  • 移除驱动:rmmod simple_cdev.ko