Linux文字デバイスドライバにおけるioctlの実装と活用

ioctlとは

Linux文字デバイスドライバにおいて、ioctlは必須の関数です。ソフトウェアの観点から見れば、単なる関数ですが、強力なハードウェアコントローラーと呼んでも過言ではありません。アプリケーション開発において、ioctlの魅力を実感しました。これはソフトウェアレベルでアプリケーション層とドライバ層間のデータ交換を実現できるだけでなく、特定のロジックと組み合わせてハードウェアを制御することも可能です。

ioctl関数の基本

ioctl関数のプロトタイプは以下の通りです:

long device_ioctl (struct file *filp, unsigned int cmd, unsigned long arg);

機能:対応するデバイスに対して指定された制御操作を実行(各種属性の設定・取得など)

パラメータ:

  • filp:openによって生成されたstruct file型のオブジェクトを指し、このioctlに対応するopenを表します
  • cmd:どの操作を実行するかを示します
  • arg:cmdと連携して使用するパラメータ

戻り値:成功時は0、失敗時は-1

実装例

3.1 ドライバ層

まずヘッダファイルを見てみましょう。以下はcustom_dev.h内の定義と、ioctl関数で使用されるマジックナンバーです。_IOR(DEV_MAGIC,1,int*)はioctlによる読み取り操作を実現することを示しています。

#ifndef CUSTOM_DEV_H
#define CUSTOM_DEV_H

#include 

#define DEV_MAGIC 'c'
#define DEV_IOCTL_GET_SIZE _IOR(DEV_MAGIC, 1, int*)
#define DEV_IOCTL_GET_COUNT _IOR(DEV_MAGIC, 2, int*)

#endif

次にcustom_dev.cの内容を見ていきます:

#include 
#include 
#include 
#include 
#include 
#include "custom_dev.h"

#define BUFFER_SIZE 128

static int major_num = 220;     // 主デバイス番号
static int minor_num = 0;       // 従デバイス番号
static int device_count = 1;    // デバイス数

struct device_info {
    struct cdev dev;            // 各デバイスタイプごとにcdev構造体
    char buffer[BUFFER_SIZE];   // カーネル空間のバッファ
    int current_size;           // 現在のデータサイズ
};

static struct device_info dev_data;

static int device_open(struct inode *inode, struct file *file)
{
    file->private_data = (void *)container_of(inode->i_cdev, struct device_info, dev);
    printk(KERN_INFO "Device opened successfully\n");
    return 0;
}

static ssize_t device_read(struct file *file, char __user *buffer, size_t length, loff_t *offset)
{
    int bytes_read = 0;
    int result = 0;
    struct device_info *dev = (struct device_info *)file->private_data;

    if (length > dev->current_size) {
        bytes_read = dev->current_size;
    } else {
        bytes_read = length;
    }

    result = copy_to_user(buffer, dev->buffer, bytes_read);
    if (result) {
        printk(KERN_ERR "Failed to copy data to user space\n");
        return -EFAULT;
    }

    // 残りのデータをバッファの先頭に移動
    memmove(dev->buffer, dev->buffer + bytes_read, dev->current_size - bytes_read);
    dev->current_size -= bytes_read;

    return bytes_read;
}

static ssize_t device_write(struct file *file, const char __user *buffer, size_t length, loff_t *offset)
{
    int bytes_written = 0;
    int result = 0;
    struct device_info *dev = (struct device_info *)file->private_data;

    if (length < BUFFER_SIZE - dev->current_size) {
        bytes_written = length;
    } else {
        bytes_written = BUFFER_SIZE - dev->current_size;
    }

    result = copy_from_user(dev->buffer + dev->current_size, buffer, bytes_written);
    if (result) {
        printk(KERN_ERR "Failed to copy data from user space\n");
        return -EFAULT;
    }

    dev->current_size += bytes_written;

    return bytes_written;
}

static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    int __user *user_arg = (int *)arg;
    int max_size = BUFFER_SIZE;
    int result = 0;
    struct device_info *dev = (struct device_info *)file->private_data;

    switch (cmd) {
        case DEV_IOCTL_GET_SIZE:
            result = copy_to_user(user_arg, &max_size, sizeof(int));
            if (result) {
                printk(KERN_ERR "Failed to send max size to user\n");
                return -EFAULT;
            }
            break;
            
        case DEV_IOCTL_GET_COUNT:
            result = copy_to_user(user_arg, &dev->current_size, sizeof(int));
            if (result) {
                printk(KERN_ERR "Failed to send current size to user\n");
                return -EFAULT;
            }
            break;
            
        default:
            printk(KERN_ERR "Unknown ioctl command\n");
            return -ENOTTY;
    }

    return 0;
}

static int device_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed successfully\n");
    return 0;
}

// 文字デバイスの操作関数群
static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .write = device_write,
    .read = device_read,
    .unlocked_ioctl = device_ioctl,
    .release = device_release,
};

static int __init driver_init(void)
{
    int ret;
    dev_t dev_num;

    dev_num = MKDEV(major_num, minor_num);
    
    // デバイス番号の登録
    ret = register_chrdev_region(dev_num, device_count, "custom_device");
    if (ret) {
        // 手動登録失敗の場合は自動割り当て
        ret = alloc_chrdev_region(&dev_num, 0, device_count, "custom_device");
        if (ret) {
            printk(KERN_ERR "Failed to allocate device number\n");
            return ret;
        }
        major_num = MAJOR(dev_num);
        minor_num = MINOR(dev_num);
        dev_num = MKDEV(major_num, minor_num);
    }

    // cdevの初期化
    cdev_init(&dev_data.dev, &fops);
    dev_data.dev.owner = THIS_MODULE;

    // デバイスのカーネルへの登録
    ret = cdev_add(&dev_data.dev, dev_num, device_count);
    if (ret) {
        printk(KERN_ERR "Failed to add device to kernel\n");
        unregister_chrdev_region(dev_num, device_count);
        return ret;
    }

    printk(KERN_INFO "Custom device driver loaded\n");
    return 0;
}

static void __exit driver_exit(void)
{
    dev_t dev_num = MKDEV(major_num, minor_num);
    
    // デバイスのカーネルからの削除
    cdev_del(&dev_data.dev);
    
    // デバイス番号の解放
    unregister_chrdev_region(dev_num, device_count);
    
    printk(KERN_INFO "Custom device driver unloaded\n");
}

module_init(driver_init);
module_exit(driver_exit);
MODULE_LICENSE("GPL");

3.2 アプリケーション層

以下のコードの主なロジックは以下の通りです:

  1. デバイスファイルを読み書きモードで開く
  2. ioctlを使用してドライバから書き込み可能な最大バイト数を取得
  3. 「こんにちは」という文字列をカーネル空間に書き込む
  4. ioctlを使用して現在ドライバに書き込まれたバイト数を取得(「こんにちは」は6バイト)
  5. 最後にデータを読み取って表示する
#include 
#include 
#include 
#include 
#include 
#include "custom_dev.h"

int main(int argc, char *argv[])
{
    int fd = -1;
    char data[6];
    int max_capacity = 0;
    int current_data = 0;

    if (argc < 2) {
        printf("Usage: %s \n", argv[0]);
        return EXIT_FAILURE;
    }

    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return EXIT_FAILURE;
    }

    // 最大容量の取得
    if (ioctl(fd, DEV_IOCTL_GET_SIZE, &max_capacity) < 0) {
        perror("Failed to get max size");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("Max capacity: %d bytes\n", max_capacity);

    // データの書き込み
    write(fd, "こんにちは", 6);

    // 現在のデータサイズの取得
    if (ioctl(fd, DEV_IOCTL_GET_COUNT, ¤t_data) < 0) {
        perror("Failed to get current size");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("Current data size: %d bytes\n", current_data);

    // データの読み取り
    read(fd, data, 6);
    printf("Read data: %s\n", data);

    close(fd);
    return EXIT_SUCCESS;
}

実行結果

上記のコードを実行すると、以下のような出力が得られます:

$ ./app /dev/custom_device
Max capacity: 128 bytes
Current data size: 6 bytes
Read data: こんにちは

まとめ

本稿では、Linux文字デバイスドライバにおけるioctlの実装方法について解説しました。ioctlはドライバ開発において非常に重要な関数です。ioctlの使用方法だけでなく、その内部動作原理を理解することも重要です。これにより、より効果的にデバイスドライバを開発し、アプリケーションとドライバ間の効率的な通信を実現できます。

タグ: linux デバイスドライバ ioctl カーネルプログラミング 文字デバイス

6月14日 19:05 投稿