Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Драйверы устройств в Linux

Часть 9: Управление вводом / выводом в Linux

Оригинал: "Device Drivers, Part 9: I/O Control in Linux"
Автор: Anil Kumar Pugalia
Дата публикации: August 1, 2011
Перевод: Н.Ромоданов
Дата перевода: июнь 2012 г.

В этой статье, которая является частью серии статей о драйверах устройств в Linux, рассказывается о типичной реализации ioctl() и ее использовании в Linux.

"Дайте мне ноутбук и расскажите мне о том, что было на практических занятиях по интерфейсам аппаратного обеспечения x86, которые проходили в лаборатории драйверов устройств Linux, а также о том, что планируется на следующих занятиях" — воскликнула Светлана, раздраженная тем, что она оказалась на больничной койке из-за пищевого отравления на вечеринке.

Друзья Светланы рассказали все, что было на предыдущем занятии, и сообщили ей, что им неизвестно, что планируется на следующем занятии, хотя занятие и будет связано с аппаратным обеспечением. Когда врач разрешим им остаться, они воспользовались это возможностью, чтобы поразмышлять о планах и поговорить о самой распространенной операции, используемой для управления аппаратным обеспечением - ioctl().

Введение в ioctl()

Операция ввода/вывода Input/Output Control (кратко - ioctl) является общей операцией или системным вызовом, который используется в большинстве драйверов различных категорий. Это один из самых универсальных системных вызовов. Если нет какого-нибудь другого системного вызова, который соответствует определенному требованию, то пользуются только системным вызовом ioctl().

К числу практических примеров относятся управление звуком для аудиоустройства, конфигурирование дисплея для видеоустройства, чтение регистров устройства и т. д. - в общем, все, что можно делать при вводе/выводе данных на устройство или выполнения конкретных операций с устройством, причем вызов достаточно универсален для любой работы (например, для отладки драйверов с помощью запроса из драйвера структур данных).

Вопрос: как это все можно получить с помощью единственного прототипа функции? Хитрость заключается в использовании двух основных параметров: команды и аргумента. Командой является число, представляющее некоторую операцию. Аргумент представляет собой соответствующий параметр для этой операции. В реализации функции ioctl() есть переключатель switch … case для выбора реализации соответствующих функций конкретной команды. До недавнего времени в ядре Linux использовался следующий прототип:

int ioctl(struct inode *i, struct file *f, unsigned int cmd, unsigned long arg);

Но, начиная с ядра 2.6.35, он изменился следующим образом:

long ioctl(struct file *f, unsigned int cmd, unsigned long arg);

Если требуется больше аргументов, то все они помещаются в структуру, и указатель на структуру становится 'одним' аргументом команды. Независимо от того, будет ли это целое число или указатель, аргумент в пространстве ядра рассматривается как длинное целое (long integer) и обрабатывается соответствующим образом.

Вызов ioctl() обычно реализуется как часть соответствующего драйвера, а затем точно также, как и в других системных вызовах, таких как open(), read() и т. д., соответствующим образом инициализуется указатель на функцию. Например, в символьных драйверах это будет поле указателя функции ioctl или unlocked_ioctl, которое должно быть инициализировано в struct file_operations.

Опять же, как и с другими системными вызовами, этот системный вызов эквивалентеен системному вызову ioctl() из пользовательского пространства, прототип которого указан в <sys/ioctl.h> следующим образом:

int ioctl(int fd, int cmd, ...);

Здесь параметр cmd точно такой же, как и в реализации ioctl() в драйвере, а переменная-аргумент construct (...) используется в качестве способа передать в ioctl() драйвера аргумент любого типа (но только один). Другие параметры будут игнорироваться.

Обратите внимание, что требуется, чтобы оба определения типа команды и типа аргумента команды были доступны как в драйвере (в пространстве ядра), так и в приложении (в пользовательском пространстве). Поэтому эти определения, как правило, помещаются в заголовочные файлы для каждого пространства.

Запрос значений внутренних переменных драйвера

Чтобы лучше понять эту скучную теорию, которая объяснялась выше, далее приводится код уже ранее упоминавшегося примера "отладки драйвера". В этом драйвере есть три статические глобальные переменные: status, dignity и ego, к которым необходимо обратиться и, возможно, работать с ними из приложения. В заголовочном файле query_ioctl.h определены соответствующий тип команды и соответствующий тип аргумента команды. Ниже приводится листинг:

#ifndef QUERY_IOCTL_H
#define QUERY_IOCTL_H
#include <linux/ioctl.h>
 
typedef struct
{
    int status, dignity, ego;
} query_arg_t;
 
#define QUERY_GET_VARIABLES _IOR('q', 1, query_arg_t *)
#define QUERY_CLR_VARIABLES _IO('q', 2)
#define QUERY_SET_VARIABLES _IOW('q', 3, query_arg_t *)
 
#endif

Если их использовать, то реализация ioctl() драйвера в query_ioctl.c будет выглядеть следующим образом:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/version.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <asm/uaccess.h>
 
#include "query_ioctl.h"
 
#define FIRST_MINOR 0
#define MINOR_CNT 1
 
static dev_t dev;
static struct cdev c_dev;
static struct class *cl;
static int status = 1, dignity = 3, ego = 5;
 
static int my_open(struct inode *i, struct file *f)
{
    return 0;
}
static int my_close(struct inode *i, struct file *f)
{
    return 0;
}
#if (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,35))
static int my_ioctl(struct inode *i, struct file *f, unsigned int cmd, unsigned long arg)
#else
static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
#endif
{
    query_arg_t q;
 
    switch (cmd)
    {
        case QUERY_GET_VARIABLES:
            q.status = status;
            q.dignity = dignity;
            q.ego = ego;
            if (copy_to_user((query_arg_t *)arg, &q, sizeof(query_arg_t)))
            {
                return -EACCES;
            }
            break;
        case QUERY_CLR_VARIABLES:
            status = 0;
            dignity = 0;
            ego = 0;
            break;
        case QUERY_SET_VARIABLES:
            if (copy_from_user(&q, (query_arg_t *)arg, sizeof(query_arg_t)))
            {
                return -EACCES;
            }
            status = q.status;
            dignity = q.dignity;
            ego = q.ego;
            break;
        default:
            return -EINVAL;
    }
 
    return 0;
}
 
static struct file_operations query_fops =
{
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_close,
#if (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,35))
    .ioctl = my_ioctl
#else
    .unlocked_ioctl = my_ioctl
#endif
};
 
static int __init query_ioctl_init(void)
{
    int ret;
    struct device *dev_ret;
 
    if ((ret = alloc_chrdev_region(&dev, FIRST_MINOR, MINOR_CNT, "query_ioctl")) < 0)
    {
        return ret;
    }
 
    cdev_init(&c_dev, &query_fops);
 
    if ((ret = cdev_add(&c_dev, dev, MINOR_CNT)) < 0)
    {
        return ret;
    }
 
    if (IS_ERR(cl = class_create(THIS_MODULE, "char")))
    {
        cdev_del(&c_dev);
        unregister_chrdev_region(dev, MINOR_CNT);
        return PTR_ERR(cl);
    }
    if (IS_ERR(dev_ret = device_create(cl, NULL, dev, NULL, "query")))
    {
        class_destroy(cl);
        cdev_del(&c_dev);
        unregister_chrdev_region(dev, MINOR_CNT);
        return PTR_ERR(dev_ret);
    }
 
    return 0;
}
 
static void __exit query_ioctl_exit(void)
{
    device_destroy(cl, dev);
    class_destroy(cl);
    cdev_del(&c_dev);
    unregister_chrdev_region(dev, MINOR_CNT);
}
 
module_init(query_ioctl_init);
module_exit(query_ioctl_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Anil Kumar Pugalia <email_at_sarika-pugs_dot_com>");
MODULE_DESCRIPTION("Query ioctl() Char Driver");

И, наконец, вызов соответствующих функций из приложения query_app.c будет выглядеть следующим образом:

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
 
#include "query_ioctl.h"
 
void get_vars(int fd)
{
    query_arg_t q;
 
    if (ioctl(fd, QUERY_GET_VARIABLES, &q) == -1)
    {
        perror("query_apps ioctl get");
    }
    else
    {
        printf("Status : %d\n", q.status);
        printf("Dignity: %d\n", q.dignity);
        printf("Ego    : %d\n", q.ego);
    }
}
void clr_vars(int fd)
{
    if (ioctl(fd, QUERY_CLR_VARIABLES) == -1)
    {
        perror("query_apps ioctl clr");
    }
}
void set_vars(int fd)
{
    int v;
    query_arg_t q;
 
    printf("Enter Status: ");
    scanf("%d", &v);
    getchar();
    q.status = v;
    printf("Enter Dignity: ");
    scanf("%d", &v);
    getchar();
    q.dignity = v;
    printf("Enter Ego: ");
    scanf("%d", &v);
    getchar();
    q.ego = v;
 
    if (ioctl(fd, QUERY_SET_VARIABLES, &q) == -1)
    {
        perror("query_apps ioctl set");
    }
}
 
int main(int argc, char *argv[])
{
    char *file_name = "/dev/query";
    int fd;
    enum
    {
        e_get,
        e_clr,
        e_set
    } option;
 
    if (argc == 1)
    {
        option = e_get;
    }
    else if (argc == 2)
    {
        if (strcmp(argv[1], "-g") == 0)
        {
            option = e_get;
        }
        else if (strcmp(argv[1], "-c") == 0)
        {
            option = e_clr;
        }
        else if (strcmp(argv[1], "-s") == 0)
        {
            option = e_set;
        }
        else
        {
            fprintf(stderr, "Usage: %s [-g | -c | -s]\n", argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr, "Usage: %s [-g | -c | -s]\n", argv[0]);
        return 1;
    }
    fd = open(file_name, O_RDWR);
    if (fd == -1)
    {
        perror("query_apps open");
        return 2;
    }
 
    switch (option)
    {
        case e_get:
            get_vars(fd);
            break;
        case e_clr:
            clr_vars(fd);
            break;
        case e_set:
            set_vars(fd);
            break;
        default:
            break;
    }
 
    close (fd);
 
    return 0;
}

Теперь над файлами query_app.c и query_ioctl.c выполните следующие операции:

  • С помощью команды make соберите драйвер query_ioctl (файл query_ioctl.ko) и приложение (файл query_app). Используйте следующий файл Makefile:
    # If called directly from the command line, invoke the kernel build system.
    ifeq ($(KERNELRELEASE),)
     
        KERNEL_SOURCE := /usr/src/linux
        PWD := $(shell pwd)
    default: module query_app
     
    module:
        $(MAKE) -C $(KERNEL_SOURCE) SUBDIRS=$(PWD) modules
     
    clean:
        $(MAKE) -C $(KERNEL_SOURCE) SUBDIRS=$(PWD) clean
        ${RM} query_app
     
    # Otherwise KERNELRELEASE is defined; we've been invoked from the
    # kernel build system and can use its language.
    else
     
        obj-m := query_ioctl.o
     
    endif
    
    
  • Загрузите драйвер с помощью команды insmod query_ioctl.ko.
    • ./query_app — чтобы отобразить значения переменных драйвера
    • ./query_app -c — чтобы очистить значения переменных драйвера
    • ./query_app -g - чтобы отобразить значения переменных драйвера
    • ./query_app -s — чтобы установить значения переменных драйвера (ранее операция не упоминалась)
  • Выгрузите драйвер с помощью команды rmmod query_ioctl.

Определение команд ioctl()

"Время посещений закончилось" - крикнул охранник. Светлана поблагодарила своих друзей, поскольку она теперь смогла разобраться с большей частью кода, в том числе необходимого, как было сказано выше, для функции copy_to_user(). Но ей хотелось бы узнать о макросах _IOR, _IO и т.д., которые использовались в определениях команд query_ioctl.h. Как ранее уже упомимналось для команды ioctl(), это лишь обычные номера. Но теперь в соответствии с стандартом POSIX для octl можно с помощью различных макросов в виде таких чисел дополнительно кодировать некоторую полезную информацию, касающуюся команд. В стандарте говорится о номерах для 32-битовых команд, сформированных из четырех компонент и занимающих биты [31:0]:

  1. Направление действия операции команды [биты 31:30] — чтение, запись, обе операции или ни одна из операций — заполняется соответствующим макросом (_IOR, _IOW, _IOWR, _IO).
  2. Размер аргумента команды [биты 29:16] — вычисляется с помощью sizeof() для конкретного типа аргумента команды - третий аргумент этих макросов.
  3. 8-битное магического числа [биты 15:8] — чтобы обеспечить относительную уникальность команды - как правило, символ ASCII (первый аргумент этих макросов).
  4. Оригинальный номер команды [биты 7:0] — фактический номер команды (1, 2, 3, ...), определенный в соответствии с нашим требованием - второй аргумент этих макросов.

Детали реализации смотрите в заголовочном файле <asm-generic/ioctl.h>.


К предыдущей статье Оглавление К следующей статье