Книга: Введение в QNX/Neutrino 2. Руководство по программированию приложений реального времени в QNX Realtime Platform

Возврат элементов каталога

Возврат элементов каталога

В приведенном ранее примере функции io_read() мы уже видели, как происходит возврат данных. Как было упомянуто в описании функции io_read() (в разделе «Алфавитный список функций установления соединения и ввода-вывода»), io_read() можно возвращать и элементы каталога тоже. Поскольку это может понадобиться далеко не всем, я решил рассмотреть этот вопрос здесь отдельно.

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

Если вы дискретно объявляете элементы в пространстве имени путей, и эти элементы не помечены флагом _RESMGR_FLAG_DIR, тогда вам не придется возвращать элементы каталога из функции io_read(). Если рассматривать это как «файловую систему», то ваш объект будет «файлом». Если же, с другой стороны, вы указываете _RESMGR_FLAG_DIR, то будет создан объект типа «каталог». Никто, кроме вас, не знает ничего о содержимом этого каталога, поэтому вы должны будете предоставить эти данные. Это и есть ответ на вопрос, почему функции io_read() может понадобиться возвращать элементы каталогов.

Вообще говоря…

Вообще говоря, возврат элементов каталога — это почти то же самое, что и возврат «сырых» данных, за исключением того, что:

• вы должны возвратить целое число структур типа struct dirent;

• эти структуры struct dirent должны быть заполнены.

Первый пункт означает, что вы не можете возвратить, например, семь с половиной структур struct dirent. Если восемь структур не вписываются в выделенное пространство, то вы должны будете возвратить только семь элементов.

Второй пункт достаточно очевиден. Он упомянут здесь только потому, что заполнение структуры struct dirent может быть несколько «хитрее», чем «сырой» подход к данным в случае с «обычной» io_read().

Структура struct dirent и ее друзья

Давайте взглянем на структуру struct dirent, поскольку это именно то, что возвращает функция io_read() в случае чтения каталога. Мы также вкратце рассмотрим клиентские вызовы, имеющие дело с элементами каталогов, поскольку у них со структурой struct dirent существует ряд интересных взаимоотношений.

Чтобы работать с каталогами, клиент использует функции closedir(), opendir(), readdir(), rewinddir(), seekdir() и telldir().

Обратите внимание на сходство с «нормальными» функций для файлов (и совпадение применяемых типов сообщений):

Функция для работы с каталогами Функция для работы с файлами Сообщение
closedir() close() IO_CLOSE_DUP
opendir() open() _IO_CONNECT
readdir() read() _IO_READ
rewinddir() lseek() _IO_LSEEK
seekdir() lseek() _IO_LSEEK
telldir() tell() _IO_LSEEK

Если мы на мгновение вообразим, что функции opendir() и closedir() будут обработаны для нас автоматически, мы сможем сконцентрироваться только на сообщениях типа _IO_READ и _IO_LSEEK и на соответствующих им функциях.

Смещения

Сообщение _IO_LSEEK и соответствующая ему функция применяются для «поиска» (или «перемещения») в пределах файла. С каталогом происходит та же история. Вы можете переместиться к «первому» элементу каталога (как явно задав смещение функции seekdir(), так и вызвав функцию rewinddir()) переместиться к любому произвольному элементу (используя функцию seekdir()), или же узнать свою текущую позицию в списке элементов каталога (вызвав функцию telldir()).

Однако, «хитрость» при работе с каталогами состоит в том, что смещения задаете вы сами, и управление ими тоже всецело лежит на вас. Это означает, что вы можете назначать смещения элементов в каталоге равными как «0», «2» и так далее, так и «0», «64», «128» и так далее. Единственно, что здесь необходимо предусмотреть — чтобы обработчики io_lseek() и io_read() одинаково трактовали эти смещения.

В приведенном ниже примере мы предположим, что используется простой подход с номерами «0», «1», «2», и т.д. (Вы могли бы использовать «0», «64», «128», и т.д., если бы эти числа соответствовали, например, неким смещениям на носителе. Выбор за вами!)

Содержимое

Ну вот, остается «просто» заполнить struct dirent «содержимым» нашего каталога. Структура struct dirent выглядит так (взято из <dirent.h>):

struct dirent {
 ino_t    d_ino;
 off_t    d_offset;
 uint16_t d_reclen;
 uint16_t d_namelen;
 char     d_name[1];
};

Коротко о ее полях:

d_ino «Индексный дескриптор» («inode») — уникальный для точки монтирования порядковый номер, который не может быть нулевым (нуль указывал бы на то, что элемент, соответствующий данному индексному дескриптору, является свободным/пустым).
d_offset Смещение в каталоге, о котором мы только что говорили. В нашем примере это будут обычные числа типа «0», «1», «2», и т.д.
d_reclen Размер структуры struct dirent целиком, включая любые добавляемые в нее расширения. Заполнители для выравнивания при вычислении размера учитываются.
d_namelen Число символов в поле d_name, не включая признак конца строки NULL.
d_name Имя элемента каталога, которое должно завершаться признаком конца строки — NULL.

При возврате структур типа struct dirent код возврата, который передается клиенту, представляет собой число возвращенных байт.

Пример

Давайте для примера создадим администратора каталогового ресурса /dev/atoz. Этот администратор зарегистрирует «файлы» с именами от /dev/atoz/a до /dev/atoz/z, чтобы команда cat, примененная к любому из них, возвращала соответствующие их именам заглавные буквы. Чтобы понять, как это должно работать, вот пример командно-строковой сессии:

# cd /dev
# ls
atoz  null  ptyp2 socket ttyp0 ttyp3
enet0 ptyp0 ptyp3 text   ttyp1 zero
mem   ptyp1 shmem tty    ttyp2
# ls -ld atoz
dr-xr-xr-x 1 root 0 26 Sep 05 07:59 atoz
# cd atoz
# ls
a  e  i  m  q  u  y
b  f  j  n  r  v  z
c  g  k  o  s  w
d  h  l  p  t  x
# ls -l e
-r--r--r-x 1 root 0 1 Sep 05 07:59 e
# cat m
M# cat q
Q#

Приведенный пример показывает, что в каталоге /dev есть каталог atoz, и что к нему можно применить команду ls и выполнить в него cd. Данный каталог /dev/atoz имеет размер «26» — мы так задали в нашей программе. Сменив текущий каталог на atoz и выполнив еще одну ls, получаем его содержимое — файлы с именами от а до z. Выполнив ls для отдельного файла — в данном случае для файла e — мы видим, что файл доступен по чтению всем (часть «-r--r--r--») и имеет размер, равный 1 байту. Наконец, выполнение нескольких cat показывает, что файлы действительно имеют заявленное содержимое. (Отметим, что поскольку файлы содержат только один байт и не содержат символа новой строки, после вывода символа строка не переводится, и приглашение командного интерпретатора оказывается на той же самой строке, что и вывод cat.)

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

main() и декларации

Основная функция; здесь мы все инициализируем и запускаем наш администратор ресурса.

my_open()

Обработчик сообщения _IO_CONNECT.

my_read()

Обработчик сообщения _IO_READ.

my_read_dir() и my_read_file()

Выполняют фактическую работу функции my_read().

dirent_size() и dirent_fill()

Сервисные функции для работы со структурой struct dirent.

Заметьте, что при том, что разбит на короткие секции, перемежаемые текстовыми пояснениями, архив с полным исходным текстом в виде единого файла можно взять на веб-сайте компании PARSE Software Devices (http://www.parse.com/), он называется atoz.c.

main() и декларации

Первый приведенный раздел программы представляет собой функцию main() и ряд деклараций. Для удобства объявлен макрос ALIGN(), который используется функциями dirent_size() и dirent_fill() для выравнивания.

Массив atoz_attrs содержит атрибутные записи, используемые в этом примере для «файлов». Мы объявляем массив из NUM_ENTS элементов, потому что у нас есть NUM_ENTS (то есть 26) файлов — от «а» до «z». Атрибутная запись, применяемая непосредственно для каталога (то есть для /dev/atoz, объявлена в теле функции main() и называется просто attr. Обратите внимание на различное содержимое у этих двух типов атрибутных записей:

Файловая атрибутная запись:

 Маркируется как обычный файл (константа S_IFREG) с режимом доступа 0444 (это означает, что доступ по чтению имеет каждый, но доступа по записи нет ни у кого). Размер равен «1» — файл содержит только один байт, а именно прописную букву, соответствующую своему имени. Индексные дескрипторы (inodes) этих файлов пронумерованы от «1» до «26» включительно (было бы удобнее взять числа от «0» до «25», но цифра «0» зарезервирована).

Каталоговая атрибутная запись:

Маркируется как файл типа «каталог» (константа S_IFDIR) с режимом доступа 0555 (это означает, что доступ по чтению и поиску имеет каждый, но доступа по записи нет ни у кого). Размер определен как «26» — это просто число, взятое по количеству элементов в каталоге. Индексный дескриптор выбран равным «27» — это число заведомо не используется больше ни в одной атрибутной записи.

Обратите внимание, что мы переопределили только поле open структуры connect_func и поле read структуры io_func. Для всех остальных полей сохранены POSIX-значения по умолчанию.

Наконец, обратите внимание, как мы создали имя /dev/atoz, используя resmgr_attach(). Наиболее важным здесь является то, что мы применили флаг _RESMGR_FLAG_DIR, который сообщает администратору процессов, что он может разрешать запросы на эту точку монтирования и ниже.

/*
 * atoz.с
 *

 * /dev/atoz с использованием библиотеки администратора ресурсов
*/
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <errno.h>
#include <dirent.h>
#include <limits.h>
#include <sys/iofunc.h>
#include <sys/dispatch.h>
#define ALIGN(x) (((x) +3) & ~3)
#define NUM_ENTS 26
static iofunc_attr_t atoz_attrs[NUM_ENTS];
int main (int argc, char **argv) {
 dispatch_t *dpp;
 resmgr_attr_t resmgr_attr;
 resmgr_context_t *ctp;
 resmgr_connect_funcs_t connect_func;
 resmgr_io_funcs_t io_func;
 iofunc_attr_t attr;
 int i;
 // Создать структуру диспетчеризации
 if ((dpp = dispatch_create()) == NULL) {
  perror("Ошибка dispatch_createn");
  exit(EXIT_FAILURE);
 }
 // Инициализировать структуры данных
 memset(&resmgr_attr, 0, sizeof(resmgr_attr));
 resmgr_attr.nparts_max = 1;
 resmgr_attr.msg_max_size = 2048;
 // Назначить обработчики по умолчанию
 iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_func,
  _RESMGR_IO_NFUNCS, &io_func);
 // Создать и инициализировать атрибутную запись для
 // каталога...
 iofunc_attr_init(&attr, S_IFDIR | 0555, 0, 0);
 attr.inode = NUM_ENTS + 1; // 1-26 зарезервированы для
                            // файлов от «a» до «z»
 attr.nbytes = NUM_ENTS; // 26 элементов в каталоге
 // ...и для имен от «a» до «z»
 for (i = 0; i < NUM_ENTS; i++) {
  iofunc_attr_init(&atoz_attrs[i], S_IFREG | 0444, 0, 0);
  atoz_attrs[i].inode = i + 1;
  atoz_attrs[i].nbytes = 1;
 }
 // Добавить наши функции; нам интересны только io_open
 // и io_read
 connect_func.open = my_open;
 io_func.read = my_read;
 // Зарегистрировать префикс
 if (resmgr_attach(dpp, &resmgr_attr, "/dev/atoz",
  _FTYPE_ANY, _RESMGR_FLAG_DIR, &connect_func,
  &io_func, &attr) == -1) {
  perror("Ошибка resmgr_attachn");
  exit(EXIT_FAILURE);
 }
 // Выделить контекст
 ctp = resmgr_context_alloc(dpp);
 // Ждать сообщений в вечном цикле
 while (1) {
  if ((ctp = resmgr_block(ctp)) == NULL) {
   perror("Ошибка resmgr_blockn");
   exit(EXIT_FAILURE);
  }
  resmgr_handler(ctp);
 }
}

my_open()

При том, что функция my_open() очень невелика, в ней есть ряд критических мест. Обратите внимание, как мы принимаем решение о том, был ли ресурс открыт как «файл» или как «каталог», на основе только длины имени пути. Мы можем себе это позволить, потому что знаем, что других каталогов, кроме основного, в этом администраторе ресурсов нет. Если вы захотите расположить ниже вашей точки монтирования дополнительные каталоги, вам придется применить более сложный механизм анализа поля path структуры msg. В нашем простом примере, если в имени пути ничего нет, то мы знаем, что это каталог. Также обратите внимание на чрезвычайно упрощенную проверку корректности имени пути: мы просто проверяем, что у нас действительно только один символ, и что он лежит в диапазоне от «a» до «z» включительно. Опять же, в случае более сложного администратора ресурсов вам пришлось бы выполнять синтаксический анализа имени, следующего за зарегистрированной точкой монтирования.

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

Все, что мы должны сделать — это решить, какую атрибутную запись мы хотим связать с OCB, создаваемым функцией по умолчанию: либо каталоговую (в этом случае мы передаем attr), либо одну из 26 имеющихся у нас файловых (тогда мы передаем соответствующий элемент atoz_attrs). Это ключевой момент, поскольку обработчик, который вы помещаете в слот open в таблице функций установления соединения, действует как «швейцар» по отношению ко всем последующим запросам к вашему администратору ресурса.

static int my_open(resmgr_context_t *ctp, io_open_t *msg,
 iofunc_attr_t *attr, void *extra) {
 if (msg->connect.path[0] == 0) {
  // Каталог (/dev/atoz)
  return (iofunc_open_default(ctp, msg, attr, extra));
 } else if (msg->connect.path[1] == 0 &&
  (msg->connect.path[0] >= 'a' &&
  msg->connect.path[0] <= 'z')) { // Файл (/dev/atoz/[a-z])
  return
   (iofunc_open_default(ctp, msg, atoz_attrs +
    msg->connect.path[0] - 'a', extra));
 } else {
  return (ENOENT);
 }
}

my_read()

В функции my_read(), чтобы решить, какие операции надо выполнить, мы анализируем поле mode атрибутной записи. Если макрос S_ISDIR() говорит, что это каталог, мы вызываем функцию my_read_dir(); если макрос S_ISREG() говорит, что это файл, мы вызываем функцию my_read_file(). (Отметим, что если мы не можем разобрать, что это такое, мы возвращаем клиенту EBADF, указывая ему этим, что что-то здесь не так.)

Приведенный ниже код ничего не знает о наших специальных устройствах, да ему, собственно, и дела до них нет — он просто принимает решение на основе стандартных общеизвестных данных.

static int my_read(resmgr_context_t *ctp, io_read_t *msg,
 iofunc_ocb_t *ocb) {
 int sts;
 // Использовать вспомогательную функцию для проверки
 // корректности
 if ((sts =
  iofunc_read_verify(ctp, msg, ocb, NULL)) != EOK) {
   return (sts);
 }
 // Решить, надо ли читать «файл» или «каталог»
 if (S_ISDIR(ocb->attr->mode)) {
  return (my_read_dir(ctp, msg, ocb));
 } else if (S_ISREG(ocb->attr->mode)) {
  return (my_read_file(ctp, msg, ocb));
 } else {
  return (EBADF);
 }
}

my_read_dir()

Вот тут-то все веселье и начинается. С точки зрения высокого уровня, мы выделяем буфер (он называется reply_msg), в котором собираемся разместить результат операции. Затем мы используем dp, чтобы «прогуляться» по буферу, заполняя его по ходу дела элементами struct dirent. Вспомогательная подпрограмма dirent_size() применяется, чтобы определить, есть ли у нас место в буфере для еще одного элемента. Вспомогательная подпрограмма dirent_fill() кладет элемент в буфер. (Отметим, что эти подпрограммы не входят в библиотеку администратора ресурсов; мы обсудим их ниже.)

На первый взгляд этот код может показаться неэффективным; мы используем функцию sprintf() для создания двухбайтового имени файла (символ имени файла и признак конца строки NULL) в буфере длиной _POSIX_PATH_MAX (то есть 256) байт. Это делается для того, чтобы сделать код по возможности универсальным.

Наконец, обратите внимание, что мы используем поле offset в OCB для указания, для какого конкретного файла мы в данный момент генерируем структуру struct dirent. Это означает, что мы также должны корректировать поле offset всякий раз, когда возвращаем данные.

Возврат данных клиенту осуществляется «обычным» способом при помощи функции MsgReply(). Заметьте, что поле состояния (status) функции MsgReply() используется для указания числа отправленных клиенту байт.

static int my_read_dir(resmgr_context_t *ctp,
 io_read_t *msg, iofunc_ocb_t *ocb) {
 int nbytes;
 int nleft;
 struct dirent *dp;
 char *reply_msg;
 char fname[_POSIX_PATH_MAX];
 // Выделить буфер для ответа
 reply_msg = calloc(1, msg->i.nbytes);
 if (reply_msg == NULL) {
  return (ENOMEM);
 }
 // Назначить выходной буфер
 dp = (struct dirent *)reply_msg;
 // Осталось «nleft» байт
 nleft = msg->i.nbytes;
 while (ocb->offset < NUM_ENTS) {
  // Создать имя файла
  sprintf(fname, "%с", ocb->offset + "a");
  // Проверить, насколько велик результат
  nbytes = dirent_size(fname);
  // Есть место?
  if (nleft - nbytes >= 0) {
   // Заполнить элемент каталога и увеличить указатель
   dp =
    dirent_fill(dp, ocb->offset + 1, ocb->offset, fname);
   // Увеличить смещение OCB
   ocb->offset++;
   // Учесть, сколько байт мы использовали
   nleft -= nbytes;
  } else {
   // Места больше нет, остановиться
   break;
  }
 }
 // Возвращаемся обратно к клиенту
 MsgReply(ctp->rcvid, (char*)dp - reply_msg, reply_msg,
  (char*)dp — reply_msg);
 // Освободить буфер
 free(reply_msg);
 // Сказать библиотеке, что мы уже ответили сами
 return (_RESMGR_NOREPLY);
}

my_read_file()

В функции my_read_file() мы видим код, почти аналогичный простому примеру функции чтения, который приведен выше в данном разделе. Единственная странная вещь, которую мы здесь делаем — поскольку мы знаем, что возвращается всегда только один байт данных, значит, если параметр nbytes не равен нулю, то он должен быть равен единице (и ничему другому). Таким образом, мы можем создавать данные, подлежащие возврату, непосредственным заполнением символьной переменной string. Обратите внимание, как мы используем поле inode атрибутной записи для определения, какие данные возвращать. Это обычный прием для администраторов, обслуживающих несколько ресурсов. Дополнительным трюком было бы расширить атрибутную запись (мы говорили об этом в разделе «Расширение атрибутной записи») и хранить непосредственно в ней либо сами данные, либо указатель на них.

static int my_read_file(resmgr_context_t *ctp,
 io_read_t *msg, iofunc_ocb_t *ocb) {
 int nbytes;
 int nleft;
 char string;
 // Тут нет никаких xtype...
 if ((msg->i.xtype & _IO_XTYPE_MASK) != _IO_XTYPE_NONE) (
  return (ENOSYS);
 }
 // Выяснить, сколько байт осталось...
 nleft = ocb->attr->nbytes — ocb->offset;
 // ...и сколько мы можем возвратить клиенту
 nbytes = min(nleft, msg->i.nbytes);
 if (nbytes) {
  // Создать ответную строку
  string = ocb->attr->inode - 1 + "A";
  // Возвратить ее клиенту
  MsgReply(ctp->rcvid, nbytes, &string + ocb->offset,
   nbytes);
  // Обновить флаги и смещение
  ocb->attr->flags |=
   IOFUNC_ATTR_ATIME | IOFUNC_ATTR_DIRTY_TIME;
  ocb->offset += nbytes;
 } else {
  // Возвращать нечего, индицировать конец файла
  MsgReply(ctp->rcvid, EOK, NULL, 0);
 }
 // Уже ответили сами
 return (_RESMGR_NOREPLY);
}

dirent_size()

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

int dirent_size(char *fname) {
return (ALIGN(sizeof(struct dirent) - 4 + strlen(fname)));
}

dirent_fill()

И, наконец, вспомогательная подпрограмма dirent_fill() применяется для помещения передаваемых ей значений (а именно — полей inode, offset и fname) в также передаваемый ей элемент каталога. В порядке щедрости она также возвращает указатель на адрес (с учетом выравнивания), с которого должен начинаться следующий элемент каталога.

struct dirent* dirent_fill(struct dirent *dp, int inode,
 int offset, char *fname) {
 dp->d_ino = inode;
 dp->d_offset = offset;
 strcpy(dp->d_name, fname);
 dp->d_namelen = strlen(dp->d_name);
 dp->d_reclen =
 ALIGN(sizeof(struct dirent) - 4 + dp->d_namelen);
 return ((struct dirent*)((char*)dp + dp->d_reclen));
}

Оглавление книги


Генерация: 1.135. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз