如何使用ioctl()来操作我的内核模块?

15

我正在尝试编写一个内核模块,使用linux/timer.h文件。我已经使它在模块内部工作,现在我正在尝试让其从用户程序中工作。

以下是我的内核模块:

//Necessary Includes For Device Drivers.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/proc_fs.h>
#include <asm/uaccess.h>
#include <linux/timer.h>
#include <linux/ioctl.h>

#define DEVICE_NAME "mytimer"
#define DEVICE_FILE_NAME "mytimer"
#define MAJOR_NUM 61
#define MINOR_NUM 0

MODULE_LICENSE("Dual BSD/GPL");

static struct timer_list my_timer;

struct file_operations FileOps = 
{
    //No File Operations for this timer.
};

//Function to perform when timer expires.
void TimerExpire(int data)
{
    printk("Timer Data: %d\n", data);
}

//Function to set up timers.
void TimerSetup(void)
{
    setup_timer(&my_timer, TimerExpire, 5678);
    mod_timer(&my_timer, jiffies + msecs_to_jiffies(5000));
}

//Module Init and Exit Functions.
int init_module(void)
{
    int initResult = register_chrdev(MAJOR_NUM, "mytimer", &FileOps);

    if (initResult < 0)
    {
        printk("Cannot obtain major number %d\n", MAJOR_NUM);

        return initResult;
    }

printk("Loading MyTimer Kernel Module...\n");


return 0;
}
void cleanup_module(void)
{
    unregister_chrdev(MAJOR_NUM, "mytimer");
    printk("Unloading MyTimer Kernel Module...\n");
}
更具体地说,我希望我的用户程序调用TimerSetup()函数。我知道我需要使用ioctl(),但我不确定如何在我的模块文件中指定可通过ioctl()调用TimerSetup()。

另外,我的第二个问题是:我能够insmod我的模块,并且mknod到/dev/mytimer,使用正确的主设备号。但是当我尝试打开()它以便从中获取文件描述符时,它一直返回-1,我认为这是错误的。我确保权限没问题(事实上,我将其设置为777只是为了确保)... 仍然无法工作... 我是否漏掉了什么?

以下是用户程序:

#include <stdio.h>

int main(int argc, char* argv[])
{
    int fd = open("/dev/mytimer", "r");
    printf("fd: %d\n", fd);

    return 0;
}
3个回答

22

你需要的示例代码可以在drivers/watchdog/softdog.c中找到(此内容发布时基于Linux 2.6.33),该示例展示了正确的文件操作方式,以及如何允许用户空间使用ioctl()函数填充结构体。

实际上,对于任何需要编写微不足道的字符设备驱动程序的人来说,这是一个非常有效的教程。

当我回答自己的问题时,我分解了softdog的ioctl接口,这可能对你有帮助。

以下是概括(远非详尽)...

softdog_ioctl()中,你会看到一个简单的初始化结构体watchdog_info,用于宣传功能、版本和设备信息:

    static const struct watchdog_info ident = {
            .options =              WDIOF_SETTIMEOUT |
                                    WDIOF_KEEPALIVEPING |
                                    WDIOF_MAGICCLOSE,
            .firmware_version =     0,
            .identity =             "Software Watchdog",
    };

接下来我们看一个简单的情况,用户只想获得这些功能:

    switch (cmd) {
    case WDIOC_GETSUPPORT:
            return copy_to_user(argp, &ident, sizeof(ident)) ? -EFAULT : 0;

当然,这将填充相应的用户空间watchdog_info与上述初始化值。如果copy_to_user()失败,则返回-EFAULT,这会导致相应的用户空间ioctl()调用返回-1,并设置有意义的errno。

请注意,magic请求实际上在linux/watchdog.h中定义,以便内核和用户空间共享它们。

#define WDIOC_GETSUPPORT        _IOR(WATCHDOG_IOCTL_BASE, 0, struct watchdog_info)
#define WDIOC_GETSTATUS         _IOR(WATCHDOG_IOCTL_BASE, 1, int)
#define WDIOC_GETBOOTSTATUS     _IOR(WATCHDOG_IOCTL_BASE, 2, int)
#define WDIOC_GETTEMP           _IOR(WATCHDOG_IOCTL_BASE, 3, int)
#define WDIOC_SETOPTIONS        _IOR(WATCHDOG_IOCTL_BASE, 4, int)
#define WDIOC_KEEPALIVE         _IOR(WATCHDOG_IOCTL_BASE, 5, int)
#define WDIOC_SETTIMEOUT        _IOWR(WATCHDOG_IOCTL_BASE, 6, int)
#define WDIOC_GETTIMEOUT        _IOR(WATCHDOG_IOCTL_BASE, 7, int)
#define WDIOC_SETPRETIMEOUT     _IOWR(WATCHDOG_IOCTL_BASE, 8, int)
#define WDIOC_GETPRETIMEOUT     _IOR(WATCHDOG_IOCTL_BASE, 9, int)
#define WDIOC_GETTIMELEFT       _IOR(WATCHDOG_IOCTL_BASE, 10, int)

WDIOC显然代表着“Watchdog ioctl”

你可以进一步让你的驱动程序执行某些操作并将该操作的结果放入结构体中,然后将其复制到用户空间。例如,如果struct watchdog_info还有一个成员__u32 result_code。请注意,__u32只是内核版本的uint32_t

使用ioctl(),用户将对象的地址(无论是结构、整数还是其他任何东西)传递给内核,希望内核在相同的对象中编写回复,并将结果复制到提供的地址中。

另一件需要做的事情是确保当有人打开、从中读取、向其中写入或使用像ioctl()这样的钩子时,设备知道该做什么,这可以通过研究softdog轻松完成。

值得关注的是:

static const struct file_operations softdog_fops = {
        .owner          = THIS_MODULE,
        .llseek         = no_llseek,
        .write          = softdog_write,
        .unlocked_ioctl = softdog_ioctl,
        .open           = softdog_open,
        .release        = softdog_release,
};

当你看到 unlocked_ioctl 处理程序转到...没错,是 softdog_ioctl()。

我认为当处理 ioctl() 时,你可能会添加一层实际上并不存在的复杂性。实际上,它就是这么简单。出于同样的原因,大多数内核开发人员不希望添加新的 ioctl 接口,除非绝对必要。很容易弄丢 ioctl() 将要填充的类型和您用于执行此操作的魔法之间的联系,这是 copy_to_user() 经常失败、导致内核堆积大量用户空间进程陷入磁盘睡眠的主要原因。

对于定时器,我同意,ioctl() 是通往清醒状态的最短路径。


我确实看过那个,但不太理解它... 我看到了这个函数: static long softdog_ioctl(struct file *file, unsigned int cmd, unsigned long arg) 但不明白里面是什么... 那是唯一的ioctl函数吗? - hwrd
1
太棒了,看起来你在帖子中很好地解释了接口 :) 谢谢。 - hwrd
1
尽管这个答案已经超过5年了,但我确实有一个问题。如果进一步实现ioctl被认为是不好的,那么首选的替代方案是什么? - sherrellbc

8

最小可运行示例

在完全可重现的QEMU + Buildroot环境中测试,因此可能有助于其他人使他们的ioctl工作。GitHub上游: 内核模块 | 共享头文件 | 用户空间

最烦人的部分是理解一些低ID被劫持:如果cmd=2,则不会调用ioctl,必须使用_IOx宏。

内核模块:

#include <asm/uaccess.h> /* copy_from_user, copy_to_user */
#include <linux/debugfs.h>
#include <linux/module.h>
#include <linux/printk.h> /* printk */

#include "ioctl.h"

MODULE_LICENSE("GPL");

static struct dentry *dir;

static long unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long argp)
{
    void __user *arg_user;
    union {
        int i;
        lkmc_ioctl_struct s;
    } arg_kernel;

    arg_user = (void __user *)argp;
    pr_info("cmd = %x\n", cmd);
    switch (cmd) {
        case LKMC_IOCTL_INC:
            if (copy_from_user(&arg_kernel.i, arg_user, sizeof(arg_kernel.i))) {
                return -EFAULT;
            }
            pr_info("0 arg = %d\n", arg_kernel.i);
            arg_kernel.i += 1;
            if (copy_to_user(arg_user, &arg_kernel.i, sizeof(arg_kernel.i))) {
                return -EFAULT;
            }
        break;
        case LKMC_IOCTL_INC_DEC:
            if (copy_from_user(&arg_kernel.s, arg_user, sizeof(arg_kernel.s))) {
                return -EFAULT;
            }
            pr_info("1 arg = %d %d\n", arg_kernel.s.i, arg_kernel.s.j);
            arg_kernel.s.i += 1;
            arg_kernel.s.j -= 1;
            if (copy_to_user(arg_user, &arg_kernel.s, sizeof(arg_kernel.s))) {
                return -EFAULT;
            }
        break;
        default:
            return -EINVAL;
        break;
    }
    return 0;
}

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = unlocked_ioctl
};

static int myinit(void)
{
    dir = debugfs_create_dir("lkmc_ioctl", 0);
    /* ioctl permissions are not automatically restricted by rwx as for read / write,
     * but we could of course implement that ourselves:
     * https://dev59.com/oorda4cB1Zd3GeqPJC4F */
    debugfs_create_file("f", 0, dir, NULL, &fops);
    return 0;
}

static void myexit(void)
{
    debugfs_remove_recursive(dir);
}

module_init(myinit)
module_exit(myexit)

内核模块和用户空间之间共享的头文件:

ioctl.h

#ifndef IOCTL_H
#define IOCTL_H

#include <linux/ioctl.h>

typedef struct {
    int i;
    int j;
} lkmc_ioctl_struct;
#define LKMC_IOCTL_MAGIC 0x33
#define LKMC_IOCTL_INC     _IOWR(LKMC_IOCTL_MAGIC, 0, int)
#define LKMC_IOCTL_INC_DEC _IOWR(LKMC_IOCTL_MAGIC, 1, lkmc_ioctl_struct)

#endif

用户空间:

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "../ioctl.h"

int main(int argc, char **argv)
{
    int fd, arg_int, ret;
    lkmc_ioctl_struct arg_struct;

    if (argc < 2) {
        puts("Usage: ./prog <ioctl-file>");
        return EXIT_FAILURE;
    }
    fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open");
        return EXIT_FAILURE;
    }
    /* 0 */
    {
        arg_int = 1;
        ret = ioctl(fd, LKMC_IOCTL_INC, &arg_int);
        if (ret == -1) {
            perror("ioctl");
            return EXIT_FAILURE;
        }
        printf("arg = %d\n", arg_int);
        printf("ret = %d\n", ret);
        printf("errno = %d\n", errno);
    }
    puts("");
    /* 1 */
    {
        arg_struct.i = 1;
        arg_struct.j = 1;
        ret = ioctl(fd, LKMC_IOCTL_INC_DEC, &arg_struct);
        if (ret == -1) {
            perror("ioctl");
            return EXIT_FAILURE;
        }
        printf("arg = %d %d\n", arg_struct.i, arg_struct.j);
        printf("ret = %d\n", ret);
        printf("errno = %d\n", errno);
    }
    close(fd);
    return EXIT_SUCCESS;
}

8
你的file_operations结构中缺少一个.open函数指针,用于指定当进程尝试打开设备文件时要调用的函数。你还需要为ioctl函数指定一个.ioctl函数指针。
尝试阅读《The Linux Kernel Module Programming Guide》的第4章(字符设备文件)和第7章(与设备文件通信),链接在此第4章介绍了file_operations结构,其中包含由模块/驱动程序定义的执行各种操作(如openioctl)的函数指针。 第7章提供了有关通过ioctl与模块/驱动程序通信的信息。 《Linux Device Drivers, Third Edition》也是另一个很好的资源。

谢谢提供这些链接,它们看起来非常有帮助。 :) - hwrd
在《Linux内核模块编程指南》中有一些例子可以指引你朝着正确的方向前进。 - jschmier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接