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

UnixForum





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

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

Часть 6: Декодирование файловых операций для символьного устройства

Оригинал: "Device Drivers, Part 6: Decoding Character Device File Operations"
Автор: Anil Kumar Pugalia
Дата публикации: May 1, 2011
Перевод: Н.Ромоданов
Дата перевода: июнь 2012 г.

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

Итак, как, по-вашему, Светлана могла бы решить эту проблему? Ясно, что с помощью Пагса. Разве это не очевидно? В нашей предыдущей статье мы видели, что Светлана была озадачена тем, что она не могла прочитать какие-нибудь данные даже после их записи в файл символьного устройства /dev/mynull. Вдруг раздался звонок — не у нее в голове, а реальный - в дверь. И конечно, это был Пагс.

"Как ты здесь оказался?" - воскликнула Светлана.

"Я увидел в твиттере твое сообщение. Это здорово, что ты самостоятельно написала полностью свой первый символьный драйвер. Это удивительно. Итак, на какой стадии ты сейчас находишься?" - спросил Пагс.

"Я скажу тебе только при условии, что ты не будешь вмешиваться" — ответила Светлана.

Пагс улыбнулся: "Хорошо, я только дам тебе совет".

"И о советах тоже - только если я попрошу совета! Я попытаюсь разобраться с работой файловых операций с символьным устройством" — сказала Светлана.

Пагс оживился и сказал: "У меня есть идея. Почему бы тебе не разобраться с ними, а затем объяснить, что ты о них поняла?"

Светлана поняла, что эта идея хорошая. Она применила команду tail к журналу dmesg для того, чтобы проследить за данными, выдаваемыми командой printk из драйвера. Для того, чтобы было удобнее отслеживать работу с файловыми операциями my_open, my_close, my_read и my_write, она также открыла на своей консоли код своего null драйвера.

static int my_open(struct inode *i, struct file *f)
{
    printk(KERN_INFO "Driver: open()\n");
    return 0;
}
static int my_close(struct inode *i, struct file *f)
{
    printk(KERN_INFO "Driver: close()\n");
    return 0;
}
static ssize_t my_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
    printk(KERN_INFO "Driver: read()\n");
    return 0;
}
static ssize_t my_write(struct file *f, const char __user *buf, size_t len, loff_t *off)
{
    printk(KERN_INFO "Driver: write()\n");
    return len;
}

Из того, что мы узнали ранее, возврат значения из функций my_open() и my_close() тривиален, типы возвращаемых значений - int, и обе функции возвращают нулевое значение, что означает успешное завершение.

Но, типы возвращаемых значений обоих функций my_read() и my_write() не int, а - ssize_t. При дальнейшем исследовании заголовков ядра, оказалось, что возвращаемое значение должно быть словом со знаком. Итак, если возвращается отрицательное число , то обычно это ошибка. Но неотрицательное возвращаемое значение будет иметь дополнительный смысл. Для операции чтения, оно будет указывать количество читаемых байтов, а для операции записи, оно будет указывать количество записываемых байтов.

Чтение файла устройства

Чтобы разобраться в этом подробнее, следует заново рассмотреть порядок выполнения операций. Давайте сначала возьмем операцию чтения. Когда пользователь выполняет чтение из файла устройства /dev/mynull, этот системный вызов поступает в слой виртуальной файловой системы (VFS), находящийся в ядре. VFS декодирует пару <major, minor> и выясняет, что нужно перенаправить системный вызов в функцию драйвера my_read(), которая зарегистрирована в виртуальной системе. Так что с этой точки зрения функция my_read() вызывается у нас, писателей драйверов устройств, как запрос на чтение. И, следовательно, возвращаемое значение будет указывать лицу, сделавшему запрос (например, пользователю), сколько байтов они получают при запросе на чтение.

В нашем примере null-драйвера мы возвратили ноль - это означает, что доступных байтов данных нет или что, другими словами, был достигнут конец файла. И, следовательно, когда читается файл устройства, то независимо от того, что в него было записано, результат будет отсутствовать.

"Хм ... Так что, если я поменяю значение на 1, то получу ли я какие-нибудь данные?" - спросил для проверки Пагс.

Светлана помолчала, посмотрела на параметры функции my_read()и утвердительно ответила, но с оговоркой — передаваемые данные были бы мусором, поскольку функция my_read() на самом деле не заносит данные в буфер buf (переменная — буфер, которая является вторым параметром функции my_read() и указывается пользователем). На самом деле, функция my_read() должна записать данные в буфер buf в соответствие со значением len (третий параметр функции), количеством байтов, запрашиваемых пользователем.

Если более конкретно, число байтов, записываемых в буфер buf, должно быть меньше или равно значению len, а количество записанных байтов должно быть передано обратно в качестве возвращаемого значения. Нет, это не опечатка - в операции чтения писатели драйверов устройств "записывают" данные в буфер, который предоставляется пользователем. Мы (возможно) читаем данные из соответствующего устройства, а затем записываем эти данные в пользовательский буфер, так что пользователь может его прочитать. "Это ты очень умно сделала" - с сарказмом сказал Пагс.

Запись в файл устройства

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

"Ага! Вот почему все мои операции записи в /dev/mynull выполнялись успешно, но, в действительности ничего не читалось и не записывалось" — воскликнула Светлана, обрадовавшись тому, что она полностью разобралась со всей последовательностью действий с файлами устройств.

Сохранение последнего символа

Поскольку Светлана не дала Пагсу никакой возможности ее поправить, Пагс решил ее подзадорить. "Хорошо. Кажется, ты полностью разобралась с основами чтения/записи, так вот у меня к тебе есть задачка. Можешь ли ты изменить функции my_read() и my_write() таким образом, чтобы всякий раз, когда я буду читать /dev/mynull, я получал бы последний символ, записанный в /dev/mynull?".

Светлана уверенно взялась за задачку и изменила my_read() и my_write() следующим образом, добавив статическую глобальную символьную переменную:

static char c;
 
static ssize_t my_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
    printk(KERN_INFO "Driver: read()\n");
    buf[0] = c;
    return 1;
}
static ssize_t my_write(struct file *f, const char __user *buf, size_t len, loff_t *off)
{
    printk(KERN_INFO "Driver: write()\n");
    c = buf[len – 1];
    return len;
}

"Почти, но что, если пользователь указал неверный буфер, или что, если пользовательский буфер окажется выгруженным. Не вызовет ли прямой доступ из пользовательского пространства к буферу buf лишь аварийную ситуацию и проблемы в ядре?" - напал Пагс.

Светлана, не испугавшись, заглянула в раскрытые ею учебники и выяснила, что просто есть два API, которые в пользовательском пространстве обеспечивают безопасный доступ к буферу, а затем воспользовалась ими. Полностью разобравшись с API, она переписала приведенный выше фрагмент кода следующим образом:

static char c;
 
static ssize_t my_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
    printk(KERN_INFO "Driver: read()\n");
    if (copy_to_user(buf, &c, 1) != 0)
        return -EFAULT;
    else
        return 1;
}
static ssize_t my_write(struct file *f, const char __user *buf, size_t len, loff_t *off)
{
    printk(KERN_INFO "Driver: write()\n");
    if (copy_from_user(&c, buf + len – 1, 1) != 0)
        return -EFAULT;
    else
        return len;
}

Затем Светлана повторила следующие обычные шаги сборки и проверки:

  1. Собрала драйвер (файл .ko) с помощью запуска команды make.
  2. Загрузила драйвер с помощью команды insmod.
  3. Сделала запись в /dev/mynull с помощью команды echo -n "Pugs" > /dev/ mynull.
  4. Выполнила чтение из /dev/mynull с помощью команды /dev/mynul (остановка с помощью нажатия клавиш Ctrl+C).
  5. Выгрузила драйвер с помощью команды rmmod.

При выполнении команды cat над /dev/mynull последовательность символов s будет выдаваться бесконечно, поскольку функция my_read() постоянно считывает последний символ. Поэтому для того, чтобы остановить бесконечное чтение, вмешался Пагс и нажал клавиши Ctrl+C, а затем он попытался объяснить: "Если здесь поменять на 'последний символ читать только один раз', то функция my_read() должна в первый раз возвращать 1, и ноль — со второго раза. Это можно сделать с помощью параметра off (четвертый параметр функции my_read())".

Светлана услужливо кивнула головой, лишь только для укрепления эго Пагса.


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