Выбрать главу

Сообщение состоит из двух частей. Первая часть посвящена установлению соединения (подобно сообщению, которое сгенерировала бы функция open()), вторая — вводу/выводу (эквивалент сообщения, генерируемого функцией fchown()). Никакого эквивалента функции close() здесь нет, поскольку мы выбрали сообщение типа _IO_CONNECT_COMBINE_CLOSE, которое гласит: «Открой указанное имя пути, используй полученный дескриптор файла для обработки остальной части сообщения, а когда закончишь дела или столкнешься с ошибкой, закрой дескриптор».

Написанный вами администратор ресурса даже не заметит, вызвал ли клиент функцию chown() или сначала сделал open(), а потом вызвал fchown() и далее close(). Все это скрыто базовым уровнем библиотеки.

Составные сообщения

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

Предположим, что в клиентском процессе есть два или более потоков, работающих с одним дескриптором файла. Один из потоков в клиенте вызывает функцию lseek(), за которой следует read(). Все так, как мы и предполагаем. А вот если другой клиента попробует выполнить ту же самую последовательность операций с тем же самым дескриптором файла, вот тут у нас начнутся проблемы. Поскольку функции lseek() и read() друг о друге ничего не знают, то возможно, например, что первый поток выполнит lseek(), а затем будет вытеснен вторым потоком. Второй поток выполнит свои lseek() и read(), после чего освободит процессор. Проблема здесь состоит в том, что поскольку эти два потока разделяют один и тот же дескриптор файла, у первого потока теперь получается неправильное смещение lseek(), поскольку оно было изменено функциями lseek() и read() второго потока! Эта проблема проявляется также с дескрипторами файлов, которые дублируются (dup()) между процессами, не говоря уже о сети.

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

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

Давайте взглянем на библиотечный вызов readblock() (из <unistd.h>):

int readblock(int fd, size_t blksize, unsigned block,

 int numblks, void *buff);

(Функция writeblock() описывается аналогично.)

Вы можете вообразить для функции readblock() довольно «простенькую» реализацию:

int readblock(int fd, size_t blksize, unsigned block,

 int numblks, void *buff) {

 lseek(fd, blksize * block, SEEK_SET); // Идем к блоку

 read(fd, buff, blksize * numblks);

}

Очевидно, что от такой реализации в многопоточной среде толку мало. Нам нужно будет как минимум добавить использование мутекса:

int readblock(int fd, size_t blksize, unsigned block,

 int numblks, void *buff) {

 pthread_mutex_lock(&block_mutex);

 lseek(fd, blksize * block, SEEK_SET); // Идем к блоку

 read(fd, buff, blksize * numblks);

 pthread_mutex_unlock(&block_mutex);

}

(Мы здесь предполагаем, что мутекс уже инициализирован.)

Этот код по-прежнему уязвим для «незащищенного» доступа — если некий поток вызовет lseek() на этом файловом дескрипторе без предварительной попытки захвата мутекса, вот у нас уже и ошибка.

Решение проблемы заключается в использовании составного сообщения, аналогично вышеописанному случаю с функцией chown(). В данном случае библиотечная реализация функции readblock() помещает обе операции — lseek() и read() — в единое сообщение и посылает это сообщение администратору ресурсов:

Составное сообщение для функции readblock().

Это работает, потому что передача сообщения является атомарной операцией. С точки зрения клиента, сообщение уходит либо целиком, либо не уходит вообще. Поэтому, вмешательство «незащищенной» функции lseek() становится несущественным — когда администратор ресурсов принимает сообщение с запросом readblock(), он делает это за один прием. (Очевидно, что в этом случае пострадает сама «незащищенная» lseek(), поскольку после отработки readblock() смещение на этом дескрипторе файла будет отличаться от того, которое она хотела установить.)

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

Структуры данных уровня POSIX

К подпрограммам POSIX-уровня относятся три структуры данных. Отметьте, что пока речь идет о базовом уровне, вы можете использовать любые структуры данных, которые пожелаете; соответствия определенной структуре и содержанию требует именно уровень POSIX. Однако, преимущества, которые предоставляет POSIX-уровень, с лихвой окупают вносимые ограничения. Как мы увидим далее, вы также сможете дополнять эти структуры вашим собственным содержанием.

На рисунке приведены эти три структуры данных для случая, когда несколько клиентов используют администратора ресурсов, объявивший два устройства:

Структуры данных — общая схема.

Структурами данных являются:

iofunc_ocb_t — OCB (блок открытого контекста)

Содержит информацию по каждому дескриптору файла.

iofunc_attr_t — атрибутная запись

Содержит информацию по каждому устройству.

iofunc_mount_t — запись точки монтирования

Содержит информацию по каждой точке монтирования.

Когда мы обсуждали таблицы функций установления соединения и ввода/вывода, мы уже видели блоки открытого контекста и атрибутные записи — в таблицах функций ввода/вывода OCB был последним передаваемым параметром. Атрибутная запись передавалась как параметр handle (третий по счету) в функциях установления соединения. Запись точки монтирования обычно представляет собой глобальную структуру и привязывается к атрибутной записи «вручную» (в инициализационном коде, написанном вами для вашего администратора ресурса).

Структура блока открытого контекста (OCB) iofunc_ocb_t

Структура блока открытого контекста (OCB) содержит информацию по каждому дескриптору файла. Это означает, что когда клиент выполняет вызов open() и получает в ответ дескриптор файла (в противоположность коду ошибки), администратор ресурсов создает OCB и связывает его с данным клиентом. Этот OCB будет существовать до тех пор, пока клиент держит данный дескриптор файла открытым. В действительности, OCB и дескриптор файла — всегда согласованная пара. По каждому сообщению ввода/вывода от клиента библиотека администратора ресурсов автоматически ищет нужный OCB и вместе с сообщением передает его нужной функции из таблицы функций ввода/вывода. Это ответ на вопрос, зачем всем функциям ввода/вывода параметр ocb. В конце концов клиент закроет дескриптор файла (применив close()), что заставит администратор ресурса отвязать OCB от дескриптора файла и от клиента. Заметьте, что клиентская функция dup() просто увеличивает счетчик связей. В этом случае OCB отделяется от дескриптора файла и от клиента только тогда, когда значение счетчика связей достигнет нуля (то есть когда число вызовов close() будет соответствовать числу open() и dup()).