Linux - статьи

Работа с файлами устройств


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

Однако, этого не всегда бывает достаточно. Допустим, что у вас есть модем, подключенный к компьютеру через последовательный порт (это может быть и внутренний модем, с точки зрения CPU он "выглядит" как модем, связанный с последовательным портом). Естественное решение -- использовать файл устройства для передачи данных модему (это могут быть команды модема или данные, которые будут посланы в телефонную линию) и для чтения данных из модема (ответы модема на команды или данные, полученные из телефонной линии). Однако, это оставляет открытым вопрос о том, как взаимодействовать непосредственно с последовательным портом, например, как настроить скорость обмена.

Ответ: в Unix следует использовать специальную функцию с именем ioctl (сокращенно от Input Output ConTroL). Любое устройство может иметь свои команды ioctl, которые могут читать (для передачи данных от процесса ядру), писать (для передачи данных от ядра к процессу), и писать и читать, и ни то ни другое, [8]

Функция ioctl вызывается с тремя параметрами: дескриптор файла устройства, номер ioctl и третий параметр, который имеет тип long, используется для передачи дополнительных аргументов. [9]

Номер ioctl содержит комбинацию бит, составляющих старший номер устройства, тип команды и тип дополнительного параметра. Обычно номер ioctl создается макроопределением (_IO, _IOR, _IOW или _IOWR, в зависимости от типа) в файле заголовка. Этот заголовочный должен подключаться директивой #include, к исходным файлам программы, которая использует ioctl для обмена данными с модулем. В примере, приводимом ниже, представлены файл заголовка chardev.h и программа, которая взаимодействует с модулем ioctl.c.


Если вы предполагаете использовать ioctl в ваших собственных модулях, то вам надлежит обратиться к файлу Documentation/ioctl-number.txt с тем, чтобы не "занять" зарегистрированные номера ioctl.
Пример 6-1. chardev.c
/* * chardev.c - Пример создания символьного устройства * доступного на запись/чтение */
#include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/fs.h> #include <asm/uaccess.h> /* определения функций get_user и put_user */
#include "chardev.h" #define SUCCESS 0 #define DEVICE_NAME "char_dev" #define BUF_LEN 80
/* * Устройство уже открыто? Используется для * предотвращения конкурирующих запросов к устройству */ static int Device_Open = 0;
/* * Ответ устройства на запрос */ static char Message[BUF_LEN];


/* * Позиция в буфере. * Используется в том случае, если сообщение оказывется длиннее * чем размер буфера. */ static char *Message_Ptr;
/* * Вызывается когда процесс пытается открыть файл устройства */ static int device_open(struct inode *inode, struct file *file) { #ifdef DEBUG printk("device_open(%p)\n", file); #endif
/* * В каждый конкретный момент времени только * один процесс может открыть файл устройства */ if (Device_Open) return -EBUSY;
Device_Open++; /* * Инициализация сообщения */ Message_Ptr = Message; try_module_get(THIS_MODULE); return SUCCESS; }
static int device_release(struct inode *inode, struct file *file) { #ifdef DEBUG printk("device_release(%p,%p)\n", inode, file); #endif
/* * Теперь мы готовы принять запрос от другого процесса */ Device_Open--;
module_put(THIS_MODULE); return SUCCESS; }
/* * Вызывается когда процесс, открывший файл устройства * пытается считать из него данные. */ static ssize_t device_read(struct file *file, /* см. include/linux/fs.h*/ char __user * buffer, /* буфер для сообщения */ size_t length, /* размер буфера */ loff_t * offset) { /* * Количество байт, фактически записанных в буфер */ int bytes_read = 0;


#ifdef DEBUG printk("device_read(%p,%p,%d)\n", file, buffer, length); #endif
/* * Если достигнут конец сообщения -- вернуть 0 * (признак конца файла) */ if (*Message_Ptr == 0) return 0;
/* * Собственно запись данных в буфер */ while (length && *Message_Ptr) {
/* * Поскольку буфер располагается в пространстве пользователя, * обычное присвоение не сработает. Поэтому * для записи данных используется put_user, * которая копирует данные из пространства ядра * в пространство пользователя. */ put_user(*(Message_Ptr++), buffer++); length--; bytes_read++; }
#ifdef DEBUG printk("Read %d bytes, %d left\n", bytes_read, length); #endif
/* * Вернуть количество байт, помещенных в буфер. */ return bytes_read; }
/* * Вызывается при попытке записи в файл устройства */ static ssize_t device_write(struct file *file, const char __user * buffer, size_t length, loff_t * offset) { int i;
#ifdef DEBUG printk("device_write(%p,%s,%d)", file, buffer, length); #endif
for (i = 0; i < length && i < BUF_LEN; i++) get_user(Message[i], buffer + i);
Message_Ptr = Message;
/* * Вернуть количество принятых байт */ return i; }
/* * Вызывается, когда процесс пытается * выполнить операцию ioctl над файлом устройства. * Кроме inode и структуры file функция получает * два дополнительных параметра: * номер ioctl и дополнительные аргументы. * */ int device_ioctl(struct inode *inode, /* см. include/linux/fs.h */ struct file *file, /* то же самое */ unsigned int ioctl_num, /* номер и аргументы ioctl */ unsigned long ioctl_param) { int i; char *temp; char ch;
/* * Реакция на различные команды ioctl */ switch (ioctl_num) { case IOCTL_SET_MSG: /* * Принять указатель на сообщение (в пространстве пользователя) * и переписать в буфер. * Адрес которого задан в дополнительно аргументе. */ temp = (char *)ioctl_param;
/* * Найти длину сообщения */ get_user(ch, temp); for (i = 0; ch && i < BUF_LEN; i++, temp++) get_user(ch, temp);
device_write(file, (char *)ioctl_param, i, 0); break;


case IOCTL_GET_MSG: /* * Передать текущее сообщение вызывающему процессу - * записать по указанному адресу. */ i = device_read(file, (char *)ioctl_param, 99, 0);
/* * Вставить в буфер завершающий символ \0 */ put_user('\0', (char *)ioctl_param + i); break;
case IOCTL_GET_NTH_BYTE: /* * Этот вызов является вводом (ioctl_param) и * выводом (возвращаемое значение функции) одновременно */ return Message[ioctl_param]; break; }
return SUCCESS; }
/* Объявлнеия */
/* * В этой структуре хранятся адреса функций-обработчиков * операций, производимых процессом над устройством. * Поскольку указатель на эту структуру хранится в таблице устройств, * она не может быть локальной для init_module. * Отсутствующие указатели в структуре забиваются значением NULL. */ struct file_operations Fops = { .read = device_read, .write = device_write, .ioctl = device_ioctl, .open = device_open, .release = device_release, /* оно же close */ };
/* * Инициализация модуля - Регистрация символьного устройства */ int init_module() { int ret_val; /* * Регистрация символьного устройства * (по крайней мере - попытка регистрации) */ ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &Fops);
/* * Отрицательное значение означает ошибку */ if (ret_val < 0) { printk("%s failed with %d\n", "Sorry, registering the character device ", ret_val); return ret_val; }
printk("%s The major device number is %d.\n", "Registeration is a success", MAJOR_NUM); printk("If you want to talk to the device driver,\n"); printk("you'll have to create a device file. \n"); printk("We suggest you use:\n"); printk("mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM); printk("The device file name is important, because\n"); printk("the ioctl program assumes that's the\n"); printk("file you'll use.\n");
return 0; }
/* * Завершение работы модуля - дерегистрация файла в /proc */ void cleanup_module() { int ret;
/* * Дерегистрация устройства */ ret = unregister_chrdev(MAJOR_NUM, DEVICE_NAME);


/* * Если обнаружена ошибка -- вывести сообщение */ if (ret < 0) printk("Error in module_unregister_chrdev: %d\n", ret); }
Пример 6-2. chardev.h
/* * chardev.h - определения ioctl. * * Определения, которые здесь находятся, * должны помещаться в заголовочный файл потому, * что они потребуются как модулю ядра (chardev.c), так и * вызывающему процессу (ioctl.c) */
#ifndef CHARDEV_H #define CHARDEV_H
#include <linux/ioctl.h>
/* * Старший номер устройства. В случае использования ioctl, * мы уже лишены возможности воспользоваться динамическим номером, * поскольку он должен быть известен заранее. */ #define MAJOR_NUM 100
/* * Операция передачи сообщения драйверу устройства */ #define IOCTL_SET_MSG _IOR(MAJOR_NUM, 0, char *) /* * _IOR означает, что команда передает данные * от пользовательского процесса к модулю ядра * * Первый аргумент, MAJOR_NUM -- старший номер устройства. * * Второй аргумент -- код команды * (можно указать иное значение). * * Третий аргумент -- тип данных, передаваемых в ядро */
/* * Операция получения сообщения от драйвера устройства */ #define IOCTL_GET_MSG _IOR(MAJOR_NUM, 1, char *) /* * Эта команда IOCTL используется для вывода данных. * Нам по прежнему нужен буфер, размещенный в адресном пространстве * вызывающего процесса, куда это сообщение должно быть переписано. */
/* * Команда получения n-ного байта сообщения */ #define IOCTL_GET_NTH_BYTE _IOWR(MAJOR_NUM, 2, int) /* * Здесь команда IOCTL работает как на ввод, так и на вывод. * Она принимает от пользователя номер байта (n), * и возвращает n-ный байт сообщения (Message[n]). */
/* * Имя файла устройства */ #define DEVICE_FILE_NAME "char_dev"
#endif
Пример 6-3. ioctl.c
/* * ioctl.c - Пример программы, использующей * ioctl для управления модулем ядра * * До сих пор мы ползовались командой cat, * для передачи данных в/из модуля. * Теперь же мы должны написать свою программу, * которая использовала бы ioctl. */
/* * Определения старшего номера устройства и коды операций ioctl */ #include "chardev.h"


#include <fcntl.h> /* open */ #include <unistd.h> /* exit */ #include <sys/ioctl.h> /* ioctl */
/* * Функции работы с драйвером через ioctl */
ioctl_set_msg(int file_desc, char *message) { int ret_val;
ret_val = ioctl(file_desc, IOCTL_SET_MSG, message);
if (ret_val < 0) { printf("Ошибка при вызове ioctl_set_msg: %d\n", ret_val); exit(-1); } }
ioctl_get_msg(int file_desc) { int ret_val; char message[100];
/* * Внимание - ядро понятия не имеет -- какой длины буфер мы используем * поэтому возможна ошибка, связанная с переполнением буфера. * В реальных проектах вам необходимо предусмотреть * передачу в ioctl двух дополнительных параметров: * собственно буфера сообщения и его длину */ ret_val = ioctl(file_desc, IOCTL_GET_MSG, message);
if (ret_val < 0) { printf("Ошибка при вызове ioctl_get_msg: %d\n", ret_val); exit(-1); }
printf("Получено сообщение (get_msg): %s\n", message); }
ioctl_get_nth_byte(int file_desc) { int i; char c;
printf("N-ный байт в сообщении (get_nth_byte): ");
i = 0; while (c != 0) { c = ioctl(file_desc, IOCTL_GET_NTH_BYTE, i++);
if (c < 0) { printf ("Ошибка при вызове ioctl_get_nth_byte на %d-м байте.\n", i); exit(-1); }
putchar(c); } putchar('\n'); }
/* * Main - Проверка работоспособности функции ioctl */ main() { int file_desc, ret_val; char *msg = "Это сообщение передается через ioctl\n";
file_desc = open(DEVICE_FILE_NAME, 0); if (file_desc < 0) { printf("Невозможно открыть файл устройства: %s\n", DEVICE_FILE_NAME); exit(-1); }
ioctl_get_nth_byte(file_desc); ioctl_get_msg(file_desc); ioctl_set_msg(file_desc, msg);
close(file_desc); }
Пример 6-4. Makefile
obj-m += chardev.o
Для облегчения сборки примера, предлагается скрипт, который выполнит эту работу за вас:
Пример 6-5. build.sh
#/bin/sh
# сборка пользовательского приложения gcc -o ioctl ioctl.c
# создание файла устройства mknod char_dev c 100 0

Содержание раздела