<?xml version="1.0" encoding="windows-1251"?>
<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">
 <stylesheet type="text/css">
               section section title { page-break-before: auto }
  image + p { page-break-before: avoid; margin-bottom: 1em; text-align: center; }
  cite { font-style: normal }
  subtitle { font-style: normal }
 </stylesheet>
 <description>
  <title-info>
   <genre>comp_programming</genre>
   <genre>comp_osnet</genre>
   <author>
    <first-name>Роб</first-name>
    <last-name>Кёртен</last-name>
    <home-page>http://www.parse.com/~rk/</home-page>
    <email>rk@parse.com</email>
   </author>
   <book-title>Введение в QNX/Neutrino 2. Руководство по программированию приложений реального времени в QNX Realtime Platform</book-title>
   <annotation>
    <p>Книга "Введение в QNX/Neutrino 2» откроет перед вами в мельчайших подробностях все секреты ОСРВ нового поколения от компании QNX Software Systems Ltd (QSSL) — QNX/Neutrino 2. Книга написана в непринужденной манере, легким для чтения и понимания стилем, и поможет любому, от начинающих программистов до опытных системотехников, получить необходимые начальные знания для проектирования надежных систем реального времени, от встраиваемых управляющих приложений до распределенных сетевых вычислительных систем</p>
    <p>В книге подробно описаны основные составляющие ОС QNX/Neutrino и их взаимосвязи. В частности, уделено особое внимание следующим темам:</p>
    <p>• обмен сообщениями: принципы функционирования и основы применения;</p>
    <p>• процессы и потоки: базовые концепции, предостережения и рекомендации;</p>
    <p>• таймеры: организация периодических событий в программах;</p>
    <p>• администраторы ресурсов: все, что относится к программированию драйверов устройств;</p>
    <p>• прерывания: рекомендации по эффективной обработке.</p>
    <p>В книге представлено множество проверенных примеров кода, подробных разъяснений и рисунков, которые помогут вам детально вникнуть в и излагаемый материал. Примеры кода и обновления к ним также можно найти на веб-сайте автора данной книги, www.parse.com.</p>
   </annotation>
   <date>2001</date>
   <lang>ru</lang>
   <src-lang>en</src-lang>
   <translator>
    <first-name>А.</first-name>
    <middle-name>Н.</middle-name>
    <last-name>Алексеев</last-name>
   </translator>
  </title-info>
  <src-title-info>
   <genre>comp_programming</genre>
   <genre>comp_osnet</genre>
   <author>
    <first-name>Rob</first-name>
    <last-name>Krten</last-name>
    <home-page>http://www.parse.com/~rk/</home-page>
    <email>rk@parse.com</email>
   </author>
   <book-title>Getting started with QNX Neutrino 2 - a guide for realtime programmers</book-title>
   <date></date>
   <lang>en</lang>
  </src-title-info>
  <document-info>
   <author>
    <nickname>honorato bonafe</nickname>
   </author>
   <program-used>OOoFBTools-2.5 (ExportToFB21), FictionBook Editor Release 2.6.6</program-used>
   <date value="2013-04-28">28.04.2013</date>
   <id>OOoFBTools-2013-3-17-14-8-17-351</id>
   <version>1.0</version>
   <history>
    <p>1.0 djvu-&gt;fb2</p>
   </history>
  </document-info>
  <publish-info>
   <book-name>Введение в QNX/Neutrino: Руководство по программированию приложений реального времени в QNX Realtime Platform</book-name>
   <publisher>Петрополис</publisher>
   <city>Санкт-Петербург</city>
   <year>2001</year>
   <isbn>5-94656-025-9</isbn>
  </publish-info>
 </description>
 <body>
  <title>
   <p>Введение в QNX/Neutrino 2</p>
   <p>Руководство по программированию приложений реального времени в QNX Realtime Platform</p>
  </title>
  <section>
   <title>
    <p>Предисловие</p>
   </title>
   <p>Впервые взглянув на черновик этой книги, я подумал, что это будет трудное чтение, потому что сам много лет провел в разработке QNX/Neutrino. Но я ошибался! Я нашел книгу простой, понятной и занимательной — все дело в стиле Роба, сочетающем философию QNX («Почему все именно так, как оно есть») с полезными общими приемами, применимыми к любому проекту, связанному с задачами реального времени. Эта книга будет полезна как для читателей, никогда прежде не слышавших о Neutrino, так и для специалистов, которые активно используют ее в своих проектах.</p>
   <p>Для тех, кто никогда не использовал QNX/Neutrino, книга представляет собой превосходное учебное пособие о том, как это делать. Поскольку Роб сам вышел из среды QNX2 и QNX4, его книга также будет очень полезна для специалистов, которые уже имели дело с QNX, поскольку ОС этого семейства имеют много общего.</p>
   <p>Что до меня самого, то я впервые познакомился с QNX в середине 80-х, когда работал в страховой компании. Изначально там применялся IBM-овский мэйнфрейм, но компания хотела сократить время на расчеты квот для корпоративного страхования; для этого в компании решили применить сеть из 8-мегагерцовых 80286, работающих под управлением QNX2. Было решено распределить данные в прозрачной сети QNX, обеспечив тем самым доступ к файлам данных по всем заказчикам с любой QNX-машины. Клиент/серверная идеология QNX наделила систему такой грацией, что я влюбился в эту ОС с первого взгляда.</p>
   <p>Я был приглашен работать в QSSL в начале 1991 года, когда была еще только-только выпущена QNX4. Она разрабатывалась в соответствии с техническими условиями только что утвержденной спецификации POSIX 1003.1, которые должны были сделать перенос общедоступных программ из UNIX проще, чем это было в QNX2, и подчинить ОС единому стандарту.</p>
   <p>Спустя несколько лет мы стали задумываться о создании операционной системы следующего поколения. Группа из менее чем 15 разработчиков стала проводить локальные совещания, обсуждая всё то, что мы хотели бы сделать иначе, а также то, что могло нам понадобиться в будущем. Мы хотели обеспечить поддержку новых спецификаций POSIX и облегчить написание драйверов. Мы также не собирались ограничиваться процессорами серии x86 и «ремонтировать то, что работает».</p>
   <p>Все фундаментальные идеи, которые Дэн Додж и Гордон Белл вложили в QNX изначально, действуют в QNX/Neutrino и по сей день — обмен сообщениями, микроядерная архитектура, предсказуемое время реакции, и т.д. Усложняла разработку QNX/Neutrino цель сделать ее более модульной, чем QNX4 (например, мы хотели создать полнофункциональное ядро, с которым можно было бы просто скомпоновать приложение, что позволило бы применять его в «более встраиваемых» приложениях по сравнению с QNX4). В 1994 году мы с Дэном Доджем начали работу над новой версией ядра и администратора процессов.</p>
   <p>Те из вас, кто долго имел дело с QNX, знают, что от такой задачи как написание драйвера устройства для QNX2 волосы встают дыбом. Приходилось быть очень осторожным! В действительности, большинство разработчиков просто брали поставляемый с QNX2 исходный текст драйвера спулера и аккуратно прилаживали его под свои нужды. Лишь немногие пытались писать драйверы дисковых устройств, поскольку это требовало специализированных знаний из области ассемблера. Из-за этого практически никому не удавалось довести свои драйверы для QNX2 до конца. В QNX4 написание драйверов было <emphasis>значительно</emphasis> упрощено сведением всех стандартных операций ввода/вывода к четко определенному интерфейсу обмена сообщениями. Когда вы вызывали <emphasis>open()</emphasis>, сервер получал сообщение типа «открыть ресурс». Когда вы вызывали <emphasis>read()</emphasis>, сервер получал сообщение типа «читать данные». Главный выигрыш механизма обмена сообщениями в QNX4 состоял в том, что он развязывал серверы от клиентуры. Помнится, когда я впервые увидел бета-версию QNX 3.99 (пре-релиз QNX4), я подумал: «Вот это да! Как изящно все сделано!» Я был настолько очарован этим, что немедленно написал драйвер файловой системы для QNX2 с использованием этого нового механизма — все вдруг стало так просто!</p>
   <p>Администратор процессов QNX/Neutrino был разработан с учетом трех основных независимых функций: управление пространством имен путей, создание и управление процессами и управление памятью. Он также поддерживал несколько дополнительных сервисов (<code><strong>/dev/null</strong></code>, <code><strong>/dev/zero</strong></code>, образная файловая система, и <emphasis>т.д</emphasis>.), каждый из которых работал независимо, но все они разделяли общую схему обработки сообщений. Мы нашли эту схему настолько полезной, что решили выделить ее код в отдельную служебную библиотеку. Так появилась библиотека администратора ресурсов (или, как Роб любит ее называть, приводя меня в тихий ужас, «библиотека резмаггера». :-).</p>
   <p>(«Resmgr» является стандартным, но труднопроизносимым сокращением от «resource manager». Роб, очевидно, решил упростить произношение и добавить гласных — так из «администратора ресурсов» (resource manager) получился «резервный индийский крокодил» (resmugger). Аналогично Роб, кстати, в свое время поступил и со своей фамилией, сделав из «крещеного» (Krten) «занавеску» (curtain) — <emphasis>прим. ред</emphasis>. :-)</p>
   <p>Мы также обнаружили, что большинство администраторов ресурсов должны предоставлять своим устройствам или файловым системам семантику POSIX, поэтому поверх библиотеки администратора ресурсов был написан еще один дополнительный уровень — семейство функций <emphasis>iofunc*()</emphasis>. Это позволяет любому человеку писать администраторы ресурсов, автоматически наследующие функциональность POSIX — без каких-либо дополнительных усилий. Примерно в это время Роб писал курсы по QNX/Neutrino, и ему был нужен минимальный пример администратора ресурсов, <code><strong>/dev/null</strong></code>. Его основной слайд гласил: «Все, что от вас требуется — это написать обработчики вызовов <emphasis>read()</emphasis> и <emphasis>write()</emphasis>, и перед вами готовый <code><strong>/dev/null</strong></code>!» Я расценил это как вызов и убрал даже это требование — базированная на библиотеке администратора ресурсов реализация <code><strong>/dev/null</strong></code> теперь укладывается в примерно полдюжины вызовов. Поскольку эта библиотека поставляется с QNX/Neutrino, теперь каждый может писать POSIX-совместимые администраторы ресурсов с минимальными усилиями.</p>
   <p>Однако, при том, что концепция администратора ресурсов была значительным шагом в эволюции QNX/Neutrino и обеспечивала мощный фундамент для операционной системы, новорожденная ОС требовала большего. Файловые системы, модули совместимости (например, TCP/IP) и устройства общего назначения, (последовательный интерфейс, консоли) разрабатывались параллельно. В результате огромной работы, в начале 1996 года вышла QNX/Neutrino 1.00. В течение последующих нескольких лет к работе над QNX/Neutrino стали привлекать все больше и больше специалистов отдела исследований и разработки (R&amp;D) компании. Мы дополнили систему поддержкой SMP, многоплатформенностью (x86, PowerPC и MIPS) (на момент перевода также добавлена поддержка ARM, StrongARM и SuperH-4 — <emphasis>прим.ред</emphasis>.) и интерфейсом диспетчеризации (он позволяет комбинировать администраторы ресурсов и другие средства межзадачного взаимодействия) — все это описано в этой книге.</p>
   <p>В августе 1999 года была официально выпущена QNX/Neutrino 2.00 — как раз к моменту выхода книги Роба! :-)</p>
   <p>Я думаю, что это издание должно быть настольной книгой каждого, кто пишет программы для QNX/Neutrino.</p>
   <cite>
    <text-author>Питер Ван Дер Вин (Peter van der Veen),</text-author>
    <text-author>С борта самолета где-то между Оттавой и Сан-Хосе, Сентябрь 1999 г.</text-author>
   </cite>
  </section>
  <section>
   <title>
    <p>Введение</p>
   </title>
   <section>
    <p>Спустя несколько лет после того, как я приобщился к компьютерам, вышел в продажу первый IBM PC. Я был, наверное, одним из первых в Оттаве, кто купил этот ящик. В нем было 16Кб ОЗУ и не было видеокарты — неопытный продавец просто не знал, что без видеокарты машина будет абсолютно бесполезной. Впрочем, несмотря на бесполезность, на ящике было красиво написано «IBM» (а тогда такое можно было увидеть только на мэйнфреймах и им подобных), и это уже само по себе выглядело достаточно внушительно. Когда я наконец накопил денег на видеокарту, я смог даже запустить БЕЙСИК на телевизоре родителей. Для меня тогда все это было вершиной компьютерной технологии — особенно модем с акустической связью на 300 бод! А теперь представьте себе мою досаду, когда мне позвонил мой друг Пол Транли и сказал: «Эй, залогинься ко мне на компьютер?» Я подумал про себя: «А у <emphasis>него</emphasis>-то откуда VAX?» — поскольку из всех известных мне машин, на которые можно было «залогиниться», VAX была единственной, которая влезла бы в его дом. Я позвонил. Это был PC, работающий под загадочной операционной системой по имени «QUNIX», с номером версии меньше 1.00. Но там можно было сделать «login» — я был в шоке!</p>
    <p>Что меня всегда поражало в операционных системах семейства QNX — это небольшой объем требуемой памяти, эффективность и абсолютная элегантность реализации. Я часто за едой развлекал (или утомлял, что более вероятно) приглашенных на ужин гостей своими баснями о программах, параллельно выполнявшихся на моей машине в подвале. Те, кто понимал в компьютерах, начинали прикидывать, какой у меня огромный диск, откуда у меня такой «неограниченный» объем ОЗУ, и т.п. После ужина я тащил их вниз, на мой этаж и показывал им свой простенький PC с 8Мб ОЗУ и винчестером на 70 Мб. На некоторых это действовало очень впечатляюще. Тем, на которых не действовало, я показывал, сколько ОЗУ и дискового пространства было еще <emphasis>доступно</emphasis>, при том что большую часть этого дискового пространства занимали мои собственные данные, которые я накопил за годы работы.</p>
    <p>Прошли годы, и я имел счастье поработать во многих компаниях, большинство их которых так или иначе занимались разработкой под QNX (телекоммуникации, управление производством, драйверы устройств видеозахвата, и т.д.), и где основным требованием была простота — как идеи, так и воплощения. Мне думается, что это требование вытекало из хорошего понимания идеологии QNX главными инженерами проектов — если в основе проекта лежит стройная, изящная архитектура, то велика и вероятность того, что и весь проект в целом будет стройным и изящным (если, конечно, проблема сама по себе не корявая).</p>
    <p>В ноябре 1995 года мне улыбнулось счастье работать непосредственно на QNX Software Systems Limited (QSSL), разрабатывая учебные материалы для двух курсов по QNX/ Neutrino, а затем и преподавая эти курсы в течение более чем трех последующих лет.</p>
    <p>Именно последние 19 или около того лет моей работы дали мне вдохновение и смелость написать мою первую книгу, <emphasis>«Введение в QNX 4: Руководство по программированию приложений реального времени»</emphasis>, которая была издана в мае 1998 года. В данной, новой книге по QNX/Neutrino я надеюсь изложить ряд накопленных мной на личном опыте концепций и идей, чтобы дать вам четкое, фундаментальное восприятие того, как работает QNX/Neutrino, и как ее можно эффективно применять. Хочется верить, что после прочтения этой книги в вашей голове вдруг включится лампочка, и вы воскликнете: «Ага! Так вот почему они сделали это именно так!»</p>
   </section>
   <section>
    <title>
     <p>Немного истории</p>
    </title>
    <p>Компания QSSL, разработавшая операционную систему QNX, была создана в 1980 году Дэном Доджом и Гордоном Беллом (оба — выпускники университета Ватерлоо, расположенного в Онтарио, Канада). Сначала компания называлась Quantum Software Systems Limited, а ее продукт назывался «QUNIX» («Quantum UNIX»). После вежливого письма адвокатов компании</p>
    <p>AT&amp;T (которой в то время принадлежала торговая марка «UNIX»), имя продукта изменили на «QNX». Спустя некоторое время изменили и название самой компании — на «QNX Software Systems Limited» — поскольку в те дни казалось, что у всех и у каждого и у их собак были компании по имени «Quantum что-то» или как-нибудь в этом духе.</p>
    <p>Первый программный продукт, получивший коммерческий успех, назывался просто «QNX» и работал на процессорах 8088 серии. Затем, в начале 80-х, была выпущена операционная система «QNX2» (QNX, версия 2). Она до сих пор успешно применяется во многих ответственных приложениях. Примерно в 1991 году появилась новая операционная система, «QNX4», с улучшенной поддержкой 32-разрядных операций и стандарта POSIX. И, наконец, в 1995 году была заявлена новая модификация ОС семейства QNX, называемая QNX/Neutrino.</p>
    <p>(Несмотря на то, что термином «Neutrino» часто называют саму ОС (сам Роб, кстати, тоже грешит этим), это не так. Neutrino — имя микроядра, а не всей ОС в целом; QNX/Neutrino была названа так, потому что является версией QNX, основанной на микроядре Neutrino. Впоследствии, после выхода пакета QNX Realtime Platform, чтобы не вносить путаницы, ОС QNX/Neutrino стали называть просто QNX6. — <emphasis>прим. ред</emphasis>.)</p>
   </section>
   <section>
    <title>
     <p>Для кого эта книга?</p>
    </title>
    <p>Данная книга подойдет любому желающему получить фундаментальное понимание ключевых особенностей QNX/Neutrino и принципов ее функционирования. Из этой книги смогут почерпнуть многое даже читатели с небольшим компьютерным образованием (хотя обсуждение в каждой главе, по мере продвижения вперед, становится все более и более техническим). Даже бывалые хакеры смогут почерпнуть из этой книги кое-какие интересные приемы, особенно касательно двух фундаментальных черт QNX/Neutrino — обмена сообщениями и структурной организации драйверов.</p>
    <p>Я попытался объяснять сложный материал в легкой для чтения «диалоговой» манере, предвидя некоторые резонные вопросы, которые могли бы возникать по ходу дела, и отвечая на них с примерами и рисунками. Поскольку книга не требует глубокого понимания языка Си, но знание его определенно даст преимущество, в тексте книги есть также и непосредственно примеры программ.</p>
   </section>
   <section>
    <title>
     <p>Что содержит эта книга?</p>
    </title>
    <section>
     <p>Данная книга призвана рассказать читателю, что представляет из себя и как работает QNX/Neutrino. Главы книги содержат описание состояний процессов, потоков, алгоритмов диспетчеризации, обмена сообщениями, модульной концепции построения ОС, и так далее. Если вы ранее никогда не применяли QNX/Neutrino, но знакомы с операционными системами реального времени, то вам, возможно, захочется уделить особое внимание главам, посвященным обмену сообщениями и администраторам ресурсов, так как именно эти концепции составляют основу QNX/Neutrino.</p>
    </section>
    <section>
     <title>
      <p>Процессы и потоки</p>
     </title>
     <p>В этой главе представлено описание процессов и потоков в QNX/ Neutrino, диспетчеризации, системы приоритетов, и дано понятие о реальном времени. Вы узнаете о состояниях потоков и алгоритмах диспетчеризации, которые применяются в QNX/ Neutrino, а также изучите функции, применяемые для управления диспетчеризацией, создания процессов и потоков, а также изменения свойств процессов и потоков, которые уже выполняются. Вы увидите, как в QNX/Neutrino реализована поддержка SMP и все вытекающие из этого как преимущества, так и подводные камни.</p>
     <p>В разделе «Диспетчеризация и реальный мир» обсуждается, диспетчеризуются потоки в работающей системе, и что может вызвать перепланирование.</p>
    </section>
    <section>
     <title>
      <p>Обмен сообщениями</p>
     </title>
     <p>В данной главе вы ознакомитесь с наиболее яркой и фундаментальной особенностью QNX/Neutrino — принципом обмена сообщениями. Вы изучите, что такое обмен сообщениями, как его применять для общения потоков между собой, и как обмениваться сообщениями по сети. Также в этой главе рассмотрен ряд дополнительных вопросов, включая извечное проклятие систем реального времени — инверсию приоритетов. </p>
     <cite>
      <p><image l:href="#img_1.png"/>Это одна из самых важных глав в книге!</p>
     </cite>
    </section>
    <section>
     <title>
      <p>Часы, таймеры и периодические уведомления</p>
     </title>
     <p>В этой главе вы изучите системные часы, таймеры, и как заставить таймеры посылать вам сообщения. В ней также много практических советов и изобилие примеров кода.</p>
    </section>
    <section>
     <title>
      <p>Прерывания</p>
     </title>
     <p>В этой главе вы научитесь писать обработчики прерываний для QNX/Neutrino и узнаете, как обработчики прерываний влияют на диспетчеризацию потоков.</p>
    </section>
    <section>
     <title>
      <p>Администраторы ресурсов</p>
     </title>
     <p>В этой главе вы изучите все, что относится к администраторам ресурсов в QNX/Neutrino (также известным как «драйверы устройств» и «администраторы ввода-вывода»). Перед написанием своего собственного администратора ресурса вам необходимо будет внимательно изучить главу «Обмен сообщениями». В главе также приведены исходные тексты нескольких готовых администраторов ресурсов.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Администраторы ресурсов — еще один важный компонент любой системы на базе QNX/Neutrino.</p>
     </cite>
    </section>
    <section>
     <title>
      <p>Переход с QNX4 на QNX/Neutrino</p>
     </title>
     <p>Неоценимое руководство для всех, кто намерен переносить свои приложения из QNX4 в QNX/Neutrino или писать программы для обеих платформ сразу. (QNX4 — операционная система предыдущего поколения от компании QSSL, а также тема моей предыдущей книги — «Введение в QNX4».) Даже если вы разрабатываете новое приложение, у вас может быть необходимость поддерживать QNX4 и QNX/Neutrino одновременно — если это так, то эта глава поможет вам избежать стандартных подводных камней и написать программу так, чтобы она была переносима в обе операционные системы.</p>
    </section>
    <section>
     <title>
      <p>Скорая помощь</p>
     </title>
     <p>Куда обращаться, если вы зашли в тупик, нашли ошибку или когда вам просто нужен добрый совет.</p>
    </section>
    <section>
     <title>
      <p>Глоссарий</p>
     </title>
     <p>Здесь дается толкование ряда используемых в книге терминов.</p>
    </section>
    <section>
     <title>
      <p>Другие источники информации</p>
     </title>
     <p>В дополнение к специализированному интерфейсу ядра, в QNX/Neutrino также реализованы многие промышленные стандарты. Это позволяет вам подкармливать ваших любимых издателей, покупая литературу по стандартным функциях ANSI, POSIX, TCP/IP и т.д.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Источники информации в Интернет</p>
    </title>
    <section>
     <p>Веб-сайты:</p>
     <p><code><strong>http://www.parse.com/</strong></code></p>
     <p>Веб-сайт компании PARSE Software Devices. Информацию об опечатках в данной книге и примеры кода из нее доступны по адресу: <code><strong>http://www.parse.com/book_v3/index.html</strong></code>.</p>
     <p><code><strong>http://www.qnx.com/</strong></code></p>
     <p>Сайт компании QSSL; здесь вы найдете всю самую свежую информацию о QNX/Neutrino. (QSSL сейчас зарегистрировала еще несколько URL в домене <code><strong>qnx.com</strong></code>, см. <code><strong>get.qnx.com</strong></code>, <code><strong>qdn.qnx.com</strong></code>, <code><strong>betas.qnx.com</strong></code>, <code><strong>partners.qnx.com</strong></code> — <emphasis>прим. ред</emphasis>.)</p>
     <p><code><strong>http://search.yahoo.com/bin/search?p=QNX</strong></code></p>
     <p>Ищите на Yahoo! Это ссылка на Интернет-каталог QNX-ресурсов.</p>
     <p>FTP-сайты:</p>
     <p><code><strong>ftp://ftp.parse.com</strong></code></p>
     <p>FTP-сайт компании PARSE Software Devices. Здесь можно скачать примеры исходных текстов, приведенных в этой книге, в удобном архивном формате.</p>
     <p><code><strong>ftp://ftp.qnx.com</strong></code></p>
     <p>Сайт с официальными обновлениями QNX, демо-версиями программ третьих сторон и свободно-распространяемыми программами для QNX.</p>
     <p>Телеконференции USENET:</p>
     <p><code><strong>comp.os.qnx</strong></code></p>
     <p>Телеконференция по QNX (главным образом QNX4, но поток информации по QNX/Neutrino постоянно увеличивается).</p>
     <subtitle>QUICS</subtitle>
     <p>QNX Users Interactive Conferencing System — интерактивная система телеконференций службы технической поддержки QSSL Используйте клиента <code><strong>telnet</strong></code>, чтобы подключиться к <code><strong>quics.qnx.com</strong></code>:</p>
     <p><code><strong>telnet quics.qnx.com</strong></code></p>
     <p>Там вы сможете создать себе учетную запись QUICS, а затем использовать <code><strong>tin</strong></code> (архаичная программа чтения новостей — <emphasis>прим. ред</emphasis>.) для участия в телеконференциях.</p>
     <p>Вы также можете обратиться к QUICS через Интернет, на www.qnx.com.</p>
     <p>(Справедливости ради следует отметить, что эта информация устарела — пока готовился перевод данной книги, QSSL изменила структуру технической поддержки. Старая добрая QUICS теперь — достояние истории; на ее место пришла более современная веб-ориентированная QNX Developers Network (QDN) — см. <code><strong>http://qdn.qnx.com</strong></code>, <code><strong>nntp://inn.qnx.com</strong></code>, <code><strong>nntp://nntp.qnx.com</strong></code> — прим. ред.)</p>
    </section>
    <section>
     <title>
      <p>О компании PARSE Software Devices</p>
     </title>
     <p>Компания PARSE Software Devices была основана как организация, занимающаяся исследованиями и разработкой, выполняющая заказные работы и предоставляющая консультационные услуги для международного сообщества разработчиков. Наши основные направления:</p>
     <p>• системная архитектура и проектирование;</p>
     <p>• системы реального времени и встраиваемые системы;</p>
     <p>• системное программирование;</p>
     <p>• телефония/телекоммуникации/системы передачи данных;</p>
     <p>• обучение персонала.</p>
     <p>За информацией о заказных работах обращайтесь в компанию PARSE Software Devices по адресу <code><strong>info@parse.com</strong></code>.</p>
     <p>Готовятся к выпуску новые книги — пожалуйста, пошлите запрос по по адресу <code><strong>books@parse.com</strong></code>, чтобы подписаться на нашу информационную рассылку. Отсутствие спама гарантируется. :-)</p>
     <p>Отметим также, что данная книга доступна для корпоративного использования компаниями класса OEM, а также он-лайн — обращайтесь по адресу <code><strong>books@sparse.com</strong></code> для получения дополнительной информации.</p>
     <subtitle>Об авторе</subtitle>
     <p>Роб Кёртен выполнял (в основном контрактные) работы в области встраиваемых систем в течение более чем 13 лет, и занимался системным программированием на протяжении более 18 лет. За период работ по трехлетнему контракту с QSSL он разработал и преподавал учебные курсы «Программирование задач реального времени для ядра Neutrino» и «Написание администраторов ресурсов». Он также написал прототип администратора сети QNX/Neutrino (<code><strong>npi-qnet</strong></code>), а также часть учебного пособия «Построение встраиваемых систем» («Building Embedded Systems», поставляется в комплекте документации к QNX/Neutrino — <emphasis>прим. ред.</emphasis>).</p>
     <p>Предыдущая книга Роба, <emphasis>«Введение в QNX 4: Руководство по программированию приложений реального времени»</emphasis> была удостоена Почетной премии («Award of Merit») Общества технических коммуникаций (Society for Technical Communications; <code><strong>http://www.stc.org</strong></code>).</p>
     <p>Недавно он выполнял контрактную работу по заказу компании Cisco Systems Inc., в которой он разрабатывал системную архитектуру (проектирование и программирование) семейства продуктов Cisco GSR-12000 (Gigabit Switch Router — гигабитный коммутирующий маршрутизатор).</p>
     <p>Роб имеет широкий круг интересов — от компьютерной музыки и графики до виртуальных файловых систем. Он также <emphasis>заядлый</emphasis> коллекционер машин серии PDP-8. Если у вас есть что-нибудь от PDP-8 — детали, документация или еще что — пожалуйста, пришлите ему весточку на <code><strong>rk@parse.com</strong></code>? Вы также можете посмотреть его домашнюю страничку по адресу <code><strong>http://www.parse.com/~rk/</strong></code>, чтобы увидеть, что он из себя представляет (на этой неделе :-).</p>
     <subtitle>О Крисе Херборте</subtitle>
     <p>По истечении почти четырех лет работы в технической издательской группе QSSL Крис решил, что пришло время создать что-нибудь свое. Объединив свои навыки технического писателя, редактора и программиста, он создал компанию Arcane Dragon Software (что-то типа «Программного обеспечения таинственного дракона» — <emphasis>прим. ред</emphasis>.).</p>
     <p>Компания Arcane Dragon Software (<code><strong>http://home.beoscentral.com/chrish/ads/</strong></code>) предоставляет следующие услуги:</p>
     <p>• написание и редактирование технической литературы;</p>
     <p>• программирование на С, С++ и Python для BeOS (см. <code><strong>http://www.be.com</strong></code>), QNX4, QNX/Neutrino и Linux;</p>
     <p>• создание интерфейсов пользователя.</p>
     <p>Крис — лауреат премии BeOS Masters Outstanding Contribution Award («За выдающиеся заслуги», одной из двух вообще когда- либо врученных) и обладатель трех высших наград Сообщества технических коммуникаций (две Почетных премии («Award of Merit») и одна Премия за мастерство («Award of Excellence»). Он также был техническим редактором книги Роба Кёртена <emphasis>«Введение в ОС QNX 4»</emphasis> (издательство PARSE), книги Мартина Броуна <emphasis>«BeOS: Перенос UNIX-приложений»</emphasis> (издательство Morgan-Kauffman) и книги Скота Хакера <emphasis>«Библия BeOS»</emphasis> (издательство Peachpit).</p>
    </section>
   </section>
   <section>
    <title>
     <p>Выражение признательности</p>
    </title>
    <p>Появление данной книги было бы невозможным без помощи и поддержки моих коллег, которые щедро одаривали меня своими многочисленными предложениями и комментариями. Это: Люк Базинет (Luc Bazinet), Джеймс Чанг (James Chang), Дэн Додж (Dan Dodge), Дейв Донахо (Dave Donaho), Мария Годфри (Maria Godfrey), Майк Хантер (Mike Hunter), Прадип Кафейл (Pradeep Kathail), Стив Марш (Steve Marsh), Дэнни Н. Прайэри (Danny N. Priarie) и Эндрю Вернон (Andrew Vernon).</p>
    <p>Особую благодарность я хотел бы выразить Брайену Стечеру (Brian Stecher), который терпеливо и внимательно рассмотрел не менее трех черновых вариантов данной книги, а также Питеру Ван Дер Вину (Peter van der Veen), который провел много ночей в моем доме (был подкуплен пивом и пиццей), выдавая мне тайны функционирования администраторов ресурсов QNX/Neutrino.</p>
    <p>Спасибо Ким Фрейзер (Kim Fraser) за уже вторую прекрасную обложку для моей книги.</p>
    <p>Отдельное спасибо Джону Острандеру (John Ostander) за его превосходные предложения по грамматике и внимательное чтение корректуры :-).</p>
    <p>И, конечно, особую благодарность я хочу выразить моему редактору, Крису Херборту — за то, что он нашел время редактировать эту книгу, помогать мне иногда с применением мрачных SGML/LaTeX, умудряясь при этом еще делать дюжину вещей одновременно! (<emphasis>«Ну я же тебя просил напомнить мне, чтобы я не делал так больше!» — цитата из Криса</emphasis>.)</p>
    <p>Я также хотел бы выразить глубокую благодарность за поддержку и понимание моей жене Кристине за то, что она каждый раз терпела мое многочасовое торчание в подвале с полнейшим ее игнорированием!</p>
   </section>
   <section>
    <title>
     <p>Типографские соглашения</p>
    </title>
    <p>В тексте данной книги для обеспечения различимости технической терминологии используется ряд типографских соглашений. В целом, примененные здесь стандарты оформлении текстового материала соответствуют таковым в публикациях документов POSIX. Ниже в таблице приведены образцы принятых типографских соглашений.</p>
    <table>
     <tr align="left">
      <th align="left" valign="top">Тип текста</th>
      <th align="left" valign="top">Пример оформления</th>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Тексты программ</td>
      <td align="left" valign="top"><code>if (stream == NULL)</code></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Опции команд</td>
      <td align="left" valign="top"><code><strong>-lR</strong></code></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Команды</td>
      <td align="left" valign="top"><code><strong>make</strong></code></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Переменные окружения</td>
      <td align="left" valign="top"><strong>PATH</strong></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Файлы и имена путей</td>
      <td align="left" valign="top"><code><strong>/dev/null</strong></code></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Имена функций</td>
      <td align="left" valign="top"><emphasis>exit()</emphasis></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Комбинации клавиш</td>
      <td align="left" valign="top">Ctrl-Alt-Del</td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Клавиатурный ввод</td>
      <td align="left" valign="top"><code>Текст, который вы набираете</code></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Клавиши</td>
      <td align="left" valign="top">Enter</td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Вывод программ</td>
      <td align="left" valign="top"><code>login:</code></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Именованные константы</td>
      <td align="left" valign="top">NULL</td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Типы данных</td>
      <td align="left" valign="top"><code><strong>unsigned short</strong></code></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Литералы</td>
      <td align="left" valign="top"><code>0xFF, "message string"</code></td>
     </tr>
     <tr align="left">
      <td align="left" valign="top">Имена переменных</td>
      <td align="left" valign="top"><emphasis>stdin</emphasis></td>
     </tr>
    </table>
    <cite>
     <p><image l:href="#img_1.png"/>Этот значок указывает на что-либо важное или полезное в тексте книги.</p>
    </cite>
   </section>
  </section>
  <section>
   <title>
    <p>Глава 1</p>
    <p>Процессы и потоки</p>
   </title>
   <section>
    <title>
     <p>Основные понятия о процессах и потоках</p>
    </title>
    <section>
     <p>Прежде, чем мы начнем обсуждать потоки, процессы, кванты времени и другие замечательные «концепции диспетчеризации», давайте поговорим об аналогиях.</p>
     <p>Сначала я хотел бы проиллюстрировать, как функционируют потоки и процессы. На мой взгляд, лучший способ (о глубинном изучении систем реального времени сейчас речь не идет) — это вообразить поведение наших потоков и процессов в некоторой привычной для нас обстановке.</p>
    </section>
    <section>
     <title>
      <p>Процесс как жилой дом</p>
     </title>
     <p>Давайте используем для построения аналогий о процессах и потоках объект, который мы используем повседневно — наш собственный дом.</p>
     <p>Дом реально представляет собой контейнер с некоторыми атрибутами (общая площадь дома, число спален, и т.д.).</p>
     <p>Если рассматривать жилой дом с этой точки зрения, он ничего не делает сам по себе. Дом — пассивный объект, в этом он аналогичен процессу. Поговорим об этом вкратце.</p>
    </section>
    <section>
     <title>
      <p>Потоки как обитатели дома</p>
     </title>
     <p>Люди, живущие в доме, суть активные объекты — они живут в комнатах, просматривают телепрограммы, готовят пищу, принимают душ, и т.д. Скоро мы поймем, что потоки функционируют аналогично.</p>
     <subtitle>Однопоточность</subtitle>
     <p>Если вы когда-либо жили в одиночестве, вы знаете, каково это — вы можете делать в доме все, что вы пожелаете и когда вы пожелаете, потому что в доме больше никого нет. Если вы пожелаете включить стерео, принять душ, приготовить обед, что угодно — вы просто идете и делаете это.</p>
     <subtitle>Многопоточность</subtitle>
     <p>Ситуация в корне изменится, если вы введете в дом еще одного человека. Скажем, вы женитесь. Теперь у вас есть супруга, живущая в этом же доме вместе с вами. Теперь уже вы не сможете попасть в душ в любой момент времени — придется каждый раз сначала проверять, нет ли там вашей супруги.</p>
     <p>Если вы оба — взрослые и ответственные люди, о вопросах безопасности обычно можно не беспокоиться. Вы будете уверены в том, что другой совершеннолетний человек будет уважать ваши правила, принципы и жизненное пространство и не попробует тайком поджечь кухню, и т.д.</p>
     <p>А если теперь добавить в дом несколько детей — тут все станет еще интереснее.</p>
    </section>
    <section>
     <title>
      <p>Назад к процессам и потокам</p>
     </title>
     <p>Так же как и дом занимает некоторый участок земли в жилом массиве, так и процесс занимает некоторый объем памяти компьютера. Аналогично тому, как и обитатели в доме могут свободно войти в любую комнату, в которую пожелают, потоки в процессах все вместе имеют общий доступ к этой памяти. Если поток получает доступ к некоему объекту (мама покупает игрушку), все другие потоки немедленно получают к нему доступ, потому что этот объект существует в общем адресном пространстве — в доме. Аналогично, если процесс распределяет для себя память, эта память становится доступной для всех потоков. Хитрость здесь состоит в том, что необходимо знать, должна ли эта память быть доступной для всех потоков в процессе. Если это так, то доступ потоков к ней придется синхронизировать. Если это не так, то будем считать, что эта память относится к одному конкретному потоку. В этом случае, поскольку только один поток имеет доступ к этой памяти, можно считать, что синхронизация не потребуется — не будет же этот поток сам ставить себе подножки!</p>
     <p>Из нашего повседневного опыта мы знаем, что вещи не так просты, как кажутся. Теперь, когда мы рассмотрели основные характеристики (резюме: любой объект является разделяемым!) давайте обратимся к более интересным ситуациям и выясним, чем же они так интересны.</p>
     <p>На рисунке, представленном ниже, показано, как мы в дальнейшем будем представлять потоки и процессы. Процесс здесь — это круг, отображающий «контейнерную» концепцию (адресное пространство), а три ломаных линии — это потоки. Вы найдете найдете подобные иллюстрации далее во всех разделах этой книги.</p>
     <image l:href="#img_2.png"/>
     <p>Процесс как контейнер потоков.</p>
    </section>
    <section>
     <title>
      <p>Взаимное исключение</p>
     </title>
     <p>Если вы хотите принять душ, и в доме есть еще кто-то, и этот кто-то уже в ванной, вам придется подождать. Как же поток функционирует в аналогичной ситуации?</p>
     <p>Потоки используют то, что мы называем взаимным исключением (mutual exclusion). Означает это в значительной степени то, о чем вы и подумали — несколько потоков являются взаимно исключающими, когда речь идёт идет об определенном ресурсе.</p>
     <p>Если вы хотите принять душ, это значит, что вы хотите получить эксклюзивный доступ к ванной комнате. Для этого вы должны сначала войти в ванную, а затем закрыть ее дверь изнутри. Если при этом данной ванной комнатой попытается воспользоваться кто-либо другой, его остановит запертая дверь. После того как вы закончили свои дела в ванной, вы откроете дверь и этим позволите еще кому-либо получить доступ в душ.</p>
     <p>Именно так и поступает поток. Поток использует объект, называемый мутексом (сокращенно от MUTual Exclusion — взаимное исключение). Этот объект подобен замку в двери: как только поток заблокирует мутекс, никакой другой поток не сможет получить доступ к мутексу до тех пор, пока владеющий мутексом поток его не разблокирует — иными словами, мутекс будет удерживать другие потоки, подобно дверному замку.</p>
     <p>Другая интересная параллель, которая проявляется как с мутексами, так и по аналогии с дверными замками, состоит в том, что мутекс является действительно «рекомендательной» блокировкой. Если поток не подчиняется правилам использования мутексов, то такая защита бессмысленна. В нашей аналогии с жилым домом эта ситуация подобна тому, как кто-либо вломился бы в ванную комнату через одну из стен, игнорируя соглашение о запертой двери.</p>
    </section>
    <section>
     <title>
      <p>Приоритеты</p>
     </title>
     <p>А что если ванная комната в настоящее время заперта, и множество людей ожидают момента, чтобы ею воспользоваться? Очевидно, все они располагаются вне ее, ожидая, когда же тот, кто в ней находится, наконец выйдет. Закономерный вопрос: «А что произойдет, когда дверь откроется? Кто должен войти следующим?»</p>
     <p>Можно предположить, что было бы «справедливым» позволить войти следующим тому, кто ожидает более длительное время. Или было бы «справедливо» позволить войти в ванную следующим тому, кто бы был, например, самый старший по возрасту, или самый высокий, или самый главный. Имеется множество способов определить то, что признавать «справедливым».</p>
     <p>Применительно к потокам, мы решаем эту проблему с учетом только двух факторов: приоритета и продолжительности ожидания.</p>
     <p>Предположим, что одновременно два человека оказываются у запертой двери в ванную комнату. Одного из них уже «поджимает» время (он опаздывает на совещание), в то время как другой тоже опаздывает, но не так уж сильно. Разве не имело бы смысл позволить тому, кого поджимает время, войти в ванную следующим? Разумеется, имело бы. Остается единственный вопрос о том, как вы принимаете решение о том, кто более «важен» в такой ситуации. Это можно сделать, например, назначив приоритет (давайте использовать номера приоритетов такие, какие приняты в QNX/Neutrino: для рассматриваемой версии QNX/Neutrino номер 1 — самый низкий, номер 63 — самый высокий). Людям в доме, которые имеют неотложные дела, следовало бы дать более высокий приоритет, а тем, у которых таких дел нет, — более низкий. Так же дела обстоят и с потоками. Если бы на момент разблокировки мутекса в ожидании находилось множество потоков, мы бы отдали этот мутекс ожидающему потоку с наивысшим приоритетом. Предположим, однако, что оба человека имеют тот же самый приоритет. Что делать? Хорошо, в этом случае было бы «справедливо» позволить человеку, который ожидал более длительное время, войти следующим. Это было бы не только «справедливо», но и так же, как это делает ядро в QNX/ Neutrino. В случае, когда в ожидании находится группа потоков, мы выстраиваем их сначала по приоритету, а уже в пределах каждого приоритета — по продолжительности ожидания.</p>
     <p>Мутекс, конечно же, не единственное средство синхронизации из тех, которые нам доведется встретить. Давайте же рассмотрим и некоторые другие тоже.</p>
    </section>
    <section>
     <title>
      <p>Семафоры</p>
     </title>
     <p>Давайте переместимся из ванной комнаты на кухню, так как это социально адаптированное помещение для одновременного обитания более чем одного человека. На кухне вы можете не пожелать, чтобы все и каждый находились бы там одновременно. В действительности вы бы, вероятно, пожелали ограничить число людей на кухне (поваров, например).</p>
     <p>Скажем, вы не хотите, чтобы на кухне находилось одновременно более двух человек. Смогли бы вы это реализовать с помощью мутекса? В пределах принятого определения — нет. Почему нет? Это действительно очень интересная проблема в нашей аналогии с домом. Давайте разобьем возникшую проблему на части и проанализируем ситуацию поэтапно.</p>
     <subtitle>Семафор с единичным счетчиком</subtitle>
     <p>В ванной комнате возможна одна из двух ситуаций, каждая из которых характеризуется двумя жестко взаимосвязанными состояниями:</p>
     <p>• дверь открыта, и в ванной никого нет;</p>
     <p>• дверь закрыта, и в помещении находится один человек.</p>
     <p>Здесь никакая другая комбинация состояний невозможна — в пустом помещении дверь не может быть никем заперта изнутри (иначе как мы бы ее тогда открыли?), и дверь не может быть открыта кем-либо вне ванной (иначе как бы мы тогда обеспечили приватность использования?). Это и есть пример семафора с единичным значением счетчика — в помещении может находиться не более одного человека, или, иными словами, только один поток может использовать семафор.</p>
     <p>Ключевым здесь (прошу прощения за каламбур) является подход к определению замка. В типовой ванной комнате вы сможете запереть и отпереть дверь только изнутри — снаружи средств для этого не предусмотрено. В действительности это означает что блокировка мутекса — это атомарная операция, и невозможна ситуация, в которой, пока вы находитесь в процессе блокировки мутекса, его заблокирует некоторый другой поток, так что в результате вы оба стали бы владельцами этого мутекса. В наше аналогии с жилым домом это не так очевидно — хотя бы потому, что люди гораздо умнее, чем нули и единицы.</p>
     <p>Что нам действительно потребуется на кухне, так это замок другого типа.</p>
     <subtitle>Семафор с не-единичным счетчиком</subtitle>
     <p>Предположим, что мы установили в двери на кухне обычный, открываемый ключом замок. Принцип работы этого замка заключается в том, что если у вас есть ключ, вы можете отпереть дверь и войти. Любой, кто использует этот замок, должен быть согласен с тем, что, войдя, он немедленно запрет дверь изнутри, чтобы любому, кто находится вне кухни, для входа всегда требовался бы ключ.</p>
     <p>Ну вот, теперь управлять количеством людей, которых мы пожелали бы одновременно видеть на кухне, становится весьма легким делом — достаточно просто повесить на дверь снаружи несколько ключей. Напоминаем, что кухня должна быть всегда закрыта! Когда кто-либо пожелает попасть на кухню, он увидит, что на двери кухни висит ключ. Если это так, он возьмет этот ключ, откроет им дверь, войдет внутрь и этим же ключом закроет дверь изнутри.</p>
     <p>Поскольку человек, входящий на кухню, должен взять ключ с собой (без этого он просто не сможет закрыть дверь изнутри), получается, что, ограничивая число висящих снаружи ключей, мы можем непосредственно управлять количеством людей, которым позволено быть на кухне в любой заданный момент времени.</p>
     <p>При операциях с потоками подобный механизм реализуется путем применения семафоров. «Простые» семафоры работают точно так же, как и мутексы. Вы либо являетесь владельцем мутекса — в этом случае вы имеете доступ к ресурсу, — или нет — тогда вы не имеете доступа. Семафор, описанный выше в аналогии с доступом на кухню, является семафором со счетчиком. Такой семафор отслеживает состояние своего внутреннего счетчика обращений (т.е. число ключей, доступных потокам).</p>
    </section>
    <section>
     <title>
      <p>Семафор в роли мутекса</p>
     </title>
     <p>Мы только что задали себе вопрос: «Смогли бы мы реализовать блокировку со счетом с помощью мутекса?» Ответ был отрицательный. А если наоборот? Смогли бы мы использовать семафор в качестве мутекса?</p>
     <p>Да, смогли бы. В действительности в некоторых операционных системах так все и делается — никаких мутексов, одни семафоры! Зачем тогда вообще беспокоиться о мутексах?</p>
     <p>Для того чтобы ответить на этот вопрос, рассмотрим ситуацию в нашей аналогии с ванной комнатой. Как строитель вашего дома реализовал мутекс? Я подозреваю, что в вашем доме нет ключей, которые вешались бы на двери снаружи.</p>
     <p>Мутексы — это семафоры «специального назначения». Если вы пожелаете, чтобы в определенном месте программы выполнялся только один поток, эффективнее всего было бы реализовать это при помощи мутекса.</p>
     <p>Позже мы рассмотрим и другие способы синхронизации потоков — объекты, которые называются условными переменными (condvar), барьерами (barrier) и ждущими блокировками (sleepon).</p>
     <cite>
      <p><image l:href="#img_1.png"/>Чтобы не возникло путаницы, необходимо также иметь в виду, что мутекс имеет и другие свойства (например наследование приоритетов), отличающие его от семафора.</p>
     </cite>
    </section>
   </section>
   <section>
    <title>
     <p>Роль ядра</p>
    </title>
    <section>
     <p>Наша аналогия с процессами в жилом доме прекрасна для объяснения концепций синхронизации, но бесполезна при анализе одной очень важной проблемы. В доме у нас было много потоков, работающих одновременно. Однако в реальной жизненной ситуации обычно имеется только один процессор, так что только один объект может реально работать в одно и то же время.</p>
    </section>
    <section>
     <title>
      <p>Одиночный процессор</p>
     </title>
     <p>Давайте рассмотрим, что происходит в реальном мире, и особенно в ситуации «экономии», где в системе есть только один процессор. В этом случае, поскольку имеется только один процессор, в любой заданный момент времени может выполняться только один поток. Ядро решает (с учетом ряда правил, которые мы кратко рассмотрим), какой поток должен выполняться, и запускает его. </p>
    </section>
    <section>
     <title>
      <p>Несколько процессоров — симметричная мультипроцессорная система (SMP)</p>
     </title>
     <p>Если вы покупаете систему, в которой имеется множество идентичных процессоров, совместно использующих одну и ту же память и устройства, это означает, что у вас есть блок SMP. (SMP расшифровывается как «Symmetrical Multi-Processor» — «симметричный мультипроцессор»; с помощью слова «симметричный» подчеркивается, что все центральные процессоры, применяемые в системе, являются идентичными.) В таком случае число потоков, которые могут работать одновременно, ограничено количеством процессоров. (Кстати, в случае с одним процессором была та же самая ситуация!) Поскольку каждый процессор может одновременно обрабатывать только один поток, в ситуации с применением множества процессоров несколько потоков могут работать одновременно. Давайте пока абстрагируемся от числа процессоров в системе — при проектировании системы бывает полезно считать, что несколько потоков могут выполняться одновременно, даже если это и не происходит в реальной ситуации. Несколько позже в разделе «На что следует обратить внимание при использовании SMP» мы рассмотрим кое-какие неочевидные особенности симметричного мультипроцессирования.</p>
    </section>
    <section>
     <title>
      <p>Ядро в роли арбитра</p>
     </title>
     <p>Так кто же определяет, который из потоков должен выполняться в данный момент времени? Этим занимается ядро.</p>
     <p>Ядро определяет, который из потоков должен использовать процессор в данный момент времени и переключает контекст на этот поток. Давайте посмотрим, что ядро при этом делает с процессором.</p>
     <p>Процессор имеет несколько регистров (точное их число зависит от принадлежности процессора к серии, например, сравните процессор x86 с процессором MIPS, а характерный представитель серии, например, процессор 80486 — с процессором Pentium). В тот момент, когда поток выполняется, информация о нем хранится в указанных регистрах (например, данные о размещении программы в памяти).</p>
     <p>Когда же ядро принимает решение о том, что должен выполняться другой поток, оно должно сделать следующее:</p>
     <p>1. Сохранить текущее состояние регистров активного потока и другую контекстную информацию.</p>
     <p>2. Записать в регистры информацию для нового потока, а также загрузить новый контекст.</p>
     <p>Как ядро принимает решение о том, что должен выполняться другой поток? Оно анализирует, действительно ли в данный момент времени данный поток готов к использованию процессора. Когда мы обсуждали, например, мутексы, мы говорили о состояниях блокировки (это происходило в тех случаях, когда поток пытался завладеть мутексом, уже принадлежащим другому потоку, и поэтому блокировался). Таким образом, с точки зрения ядра мы имеем один поток, который может использовать процессор, и другой поток, которые не может этого делать, потому что он заблокирован в ожидании мутекса. В этом случае ядро предоставляет процессор потоку, который готов к работе, а другой поток заносит в свой внутренний список (чтобы можно было отслеживать отслеживал запрос потока на мутекс).</p>
     <p>Очевидно, это не очень-то интересная ситуация. Предположим, что готовы к выполнению сразу несколько потоков. Вспомним, не мы ли делегировали доступ к мутексу на основе приоритета и продолжительности ожидания? Ядро тоже использует подобную схему для определения того, который из потоков должен работать следующим. При этом играют роль два фактора: приоритет и дисциплина диспетчеризации. Рассмотрим их по очереди.</p>
     <subtitle>Концепция приоритетов</subtitle>
     <p>Рассмотрим два готовых к выполнению потока. Если эти поток имеют различные приоритеты, то весьма прост — ядро отдает процессор потоку с высшим приоритетом. Приоритеты в QNX/ Neutrino пронумерованы от единицы (самый низкий) и далее, в единичным дискретом — так же, как это было упомянуто в обсуждении получения мутекса. Заметьте, что нулевой приоритет использовать нельзя — он зарезервирован для «холостого» (idle) потока (на профессиональном жаргоне часто называемого «холодильником» — <emphasis>прим. ред</emphasis>.). (Если вы захотите узнать минимальное или максимальное значение приоритета, определенное для вашей системы, используйте функции <emphasis>sched_get_priority_min()</emphasis> и <emphasis>sched_get_priority_max()</emphasis> — они описаны в <code><strong>&lt;sched.h&gt;</strong></code>. В данной книге мы будем предполагать, что приоритет 1 является самым низким, а 63 самым высоким.</p>
     <p>Если другой поток с более высоким приоритетом вдруг становится готов к выполнению, ядро немедленно переключит контекст на поток с более высоким приоритетом. Это называется <emphasis>вытеснением—</emphasis> поток с высшим приоритетом вытесняет поток с низшим приоритетом. Когда поток с высшим приоритетом заканчивает свою работу, и ядро переключает контекст обратно на поток с низшим приоритетом, который выполнялся ранее, мы называем это <emphasis>возобновлением—</emphasis> ядро возобновляет работу предыдущего потока.</p>
     <p>Теперь предположим, что не один, а два потока готовы к выполнению и имеют один и тот же приоритет.</p>
     <subtitle>Дисциплины диспетчеризации</subtitle>
     <p>Предположим, что в данное время выполняется один из потоков, Рассмотрим правила, которые используются ядром при принятии решения о переключении контекста в такой ситуации. (Разумеется, все это обсуждение в действительности применимо только к потокам с одинаковыми приоритетами — как только будет готов к выполнению поток с высшим приоритетом, процессор будет отдан ему. В этом вся суть приоритетов в операционной системе реального времени.)</p>
     <p>Ядро QNX/Neutrino поддерживает две дисциплины диспетчеризации: карусельную, она же RR (Round Robin), и FIFO (First In — First Out).</p>
     <subtitle>Диспетчеризация FIFO</subtitle>
     <p>При диспетчеризации FIFO процессор предоставляется потоку на столько времени, сколько ему необходимо. Это означает, что если один поток занят длительными вычислениями, и никакой другой поток с более высоким приоритетом не готов к выполнению, то этот поток потенциально может выполняться <emphasis>вечно</emphasis>. А как же потоки с тем же приоритетом? Они будут заблокированы тоже. (То, что в этот же момент потоки с более низким приоритетом будут заблокированы, должно быть очевидно.)</p>
     <p>Если работающий поток завершает свою работу или добровольно уступает процессор, ядро анализирует состояние других потоков того же самого приоритета на готовность их к выполнению. Если таковых не имеется, то ядро анализирует потоки с более низким приоритетом, готовые к выполнению. Заметьте, что выражение «добровольно уступить процессор» может означать одну из двух возможных ситуаций. Если поток переходит в режим ожидания, блокируется на семафоре, и т.д., тогда — <emphasis>да</emphasis>, может выполняться поток с более низким приоритетом (как описано выше). Но существует также специальная функция <emphasis>sched_yield()</emphasis> (базированная на системном вызове <emphasis>SchedYield()</emphasis>), по которому процессор передается только другому потоку с тем же самым приоритетом — если бы был готов к выполнению поток с высшим приоритетом, у потока с низшим приоритетом все равно не было бы никаких шансов получить управление. Если поток вызывает функцию <emphasis>sched_yield()</emphasis>, но никакой другой поток с таким же самым приоритетом не готов к выполнению, первоначальный поток продолжает работу. В реальности, функция <emphasis>sched_yield()</emphasis> применяется для того, чтобы дать шанс другому потоку с таким же самым приоритетом получить доступ к процессору.</p>
     <p>На рисунке, приведенном ниже, мы видим три потока, размещенных в двух различных процессах:</p>
     <image l:href="#img_3.png"/>
     <p>Три потока в двух различных процессах.</p>
     <p>Если мы предположим, что потоки «А» и «В» находятся в состоянии READY («готов»), что поток «С» блокирован (возможно, в ожидании мутекса), а другой поток «D» (не показан) в настоящее время выполняется, то очередь готовности, которую поддерживает ядро QNX/Neutrino, будет выглядеть следующим образом:</p>
     <image l:href="#img_4.png"/>
     <p>Два потока в очереди готовности, один блокирован, один выполняется.</p>
     <p>На рисунке иллюстрируется внутренняя очередь готовности, которую использует ядро при принятии решения о том, кого запланировать на выполнение следующим. Заметьте, что поток «С» не находится в очереди готовности, потому что он блокирован, и поток «D» также не находится в этой очереди, потому что он уже выполняется.</p>
     <subtitle>Карусельная диспетчеризация (RR)</subtitle>
     <p>Дисциплина RR (карусельная диспетчеризация) аналогична дисциплине диспетчеризации FIFO, за исключением того, что поток не будет работать бесконечно, если имеется другой поток с тем же самым приоритетом. Поток будет работать только в течение предопределенного кванта времени (который фиксирован и не может быть изменен). Вы можете узнать величину кванта времени, используя функцию <emphasis>sched_rr_get_interval()</emphasis>.</p>
     <p>Когда ядро запускает на обработку поток с дисциплиной диспетчеризации RR, оно засекает время. Если поток не блокируется в течение выделенного ему кванта времени, квант времени истечет. Тогда ядро проверяет наличие другого готового к выполнению потока с тем же самым приоритетом. Если такой поток обнаруживается, то ядро активирует его. Если такого потока нет, то ядро снова ставит на выполнение предыдущий поток (то есть ядро выделяет потоку для работы еще один квант времени).</p>
     <subtitle>Постулаты</subtitle>
     <p>Давайте сделаем сводку правил диспетчеризации (для одиночного процессора) и отсортируем их в порядке важности:</p>
     <p>• только один поток может выполняться в данный момент времени;</p>
     <p>• всегда должен выполняться поток с наивысшим авторитетом;</p>
     <p>• поток должен работать до тех пор, пока он не блокируется иди не завершается;</p>
     <p>• поток, диспетчеризуемый по дисциплине карусельного типа (RR), должен работать в течение выделенного ему кванта времени, после чего ядро обязано его перепланировать (при необходимости).</p>
     <p>Для систем с несколькими процессорами, приведенные выше правила остаются такими же, за исключением того, что несколько процессоров могут одновременно выполнять несколько потоков. Порядок, в котором потоки выполняются (то есть последовательность, в которой потоки ставятся на выполнение в многопроцессорной системе), определяется точно так же, как для одиночного процессора — в любой момент времени будет выполняться готовый к выполнению поток с наивысшим приоритетом. Если существует другой готовый к выполнению поток с более высоким приоритетом, и имеется доступный процессор, то этот поток будет выполняться на следующем процессоре, и так далее. Если имеющегося числа потоков недостаточно для того, чтобы загрузить все процессоры по такому принципу, то нет проблем — «неактивные» процессоры будут выполнять «холостой» поток (его приоритет равен нулю, то есть ниже, чем приоритет любого пользовательского потока) Если для того, чтобы обработать всю очередь, недостаточно процессоров, тогда только N потоков с наивысшим приоритетом будут выполняться, где N — число доступных процессоров. Другие потоки будут готовы к выполнению, но в действительности выполняться не будут. Отметим, что вопросы диспетчеризации потоков в симметричной мультипроцессорной системе все еще исследуются, так что возможно, что этот порядок может измениться в будущем.</p>
     <image l:href="#img_5.png"/>
     <p>Схема алгоритма диспетчеризации.</p>
    </section>
    <section>
     <title>
      <p>Состояния потоков</p>
     </title>
     <p>Несколько раз небрежно упомянув о «выполнении», «готовности» и «блокировке», давайте теперь формализуем эти состояния потока.</p>
     <subtitle>Выполнение (RUNNING)</subtitle>
     <p>Состояние выполнения (RUNNING) в QNX/Neutrino означает, что поток активно использует ресурсы процессора. В системе SMP будет осуществляться выполнение множества потоков, а в системе с единственным процессором будет осуществляться выполнение одного потока.</p>
     <subtitle>Готовность (READY)</subtitle>
     <p>Состояние готовности (READY) означает, что этот поток может быть поставлен на выполнение немедленно, но не выполняется, потому что в данный момент времени активен другой поток (с таким же или более высоким приоритетом). Если бы два потока были готовы к выполнению, один из них с приоритетом 10, а другой — с приоритетом 7, то поток с приоритетом 10 был бы переведен в состояние выполнения (RUNNING), а поток с приоритетом 7 — в состояние готовности (READY).</p>
     <subtitle>Блокированные состояния (BLOCKED)</subtitle>
     <p>Что называется блокированным состоянием? Проблема здесь состоит в том, что блокированных состояний существует несколько. Реально в QNX/Neutrino имеется более дюжины блокированных состояний.</p>
     <p>Почему так много? Потому что ядро отслеживает причину, по которой поток заблокирован.</p>
     <p>Мы уже ознакомились с двумя типами блокирующих состояний: когда поток заблокирован в ожидании мутекса, этот поток находится в состоянии блокировки по мутексу (MUTEX). Когда поток заблокирован, ожидая семафор, он находится в состоянии блокировки по семафору (SEM). Эти состояния просто указывают, в очереди на какой ресурс поток заблокирован.</p>
     <p>Если по мутексу заблокировано несколько потоков, ядро не уделит им никакого внимания <emphasis>до тех пор</emphasis>, пока поток, который владеет мутексом, не освободит его. Как только это произойдет, один из блокированных потоков будет переведен в состояние готовности (READY), и ядро при необходимости примет решение о перепланировании.</p>
     <p>Почему «при необходимости»? У потока, который только что освободил мутекс, вполне могут быть и другие дела, и он может иметь более высокий приоритет, чем все остальные ожидающие процессор потоки. В этом случае мы следуем второму правилу, которое гласит: «всегда должен выполняться поток с наивысшим приоритетом», что означает, что порядок диспетчеризации не изменяется — поток с наивысшим приоритетом продолжает работать.</p>
     <subtitle>Полный список состояний потоков</subtitle>
     <p>Ниже представлен полный список блокированных состояний с краткими пояснениями. Этот список, кстати, есть в заголовочном файле <code><strong>&lt;sys/QNX/Neutrino.h&gt;</strong></code>, только там эти состояния снабжены префиксом «STATE_» (например, состояние READY данной таблицы там будет звучать как STATE_READY).</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Если состояние потока:</th>
       <th align="left" valign="top">To это значит, что:</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">DEAD</td>
       <td align="left" valign="top">Поток «мертв», ядро ожидает освобождения занятых им ресурсов. (В классических UNIX системах это состояние также называют «zombie» — «зомби» — <emphasis>прим. ред</emphasis>.)</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">RUNNING</td>
       <td align="left" valign="top">Поток выполняется.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">Поток не выполняется, но готов к работе (работает один или более потоков с более высокими или равными приоритетами).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">STOPPED</td>
       <td align="left" valign="top">Поток приостановлен (по сигналу SIGSTOP</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">SEND</td>
       <td align="left" valign="top">Поток ожидает приема своего сообщения сервером.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">RECEIVE</td>
       <td align="left" valign="top">Поток ожидает сообщение от клиента.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">REPLY</td>
       <td align="left" valign="top">Поток ожидает от сервера ответ на свое сообщение.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">STACK</td>
       <td align="left" valign="top">Поток ожидает распределения дополнительного стекового пространства.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">WAITPAGE</td>
       <td align="left" valign="top">Поток ожидает устранения администратором процессов повреждения на странице.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">SIGSUSPEND</td>
       <td align="left" valign="top">Поток ожидает сигнал.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">SIGWAITINFO</td>
       <td align="left" valign="top">Поток ожидает сигнал.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">NANOSLEEP</td>
       <td align="left" valign="top">Поток «спит» (приостановлен на определенный период времени).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">MUTEX</td>
       <td align="left" valign="top">Поток ожидает захват мутекса.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">CONDVAR</td>
       <td align="left" valign="top">Поток ожидает соблюдения условия условной переменной.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">JOIN</td>
       <td align="left" valign="top">Поток ожидает завершения другого потока.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">INTR</td>
       <td align="left" valign="top">Поток ожидает прерывание.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">SEM</td>
       <td align="left" valign="top">Поток ожидает захват семафора.</td>
      </tr>
     </table>
     <p>Важно помнить о том, что когда поток блокирован, независимо от <emphasis>состояния блокировки</emphasis>, он не потребляет ресурсы процессора. Наоборот, единственным состоянием, в котором поток потребляет ресурсы процессора, является состояние выполнения (RUNNING).</p>
     <p>Мы рассмотрим блокированные состояния SEND (блокировка по передаче), RECEIVE (блокировка по приему) и REPLY (блокировка по ответу) в главе «Обмен сообщениями». Состояние NANOSLEEP связано с применением функций типа <emphasis>sleep()</emphasis>, которые мы рассмотрим в главе «Часы, таймеры и периодические уведомления». Состояние INTR связано с использованием функции <emphasis>InterruptWait()</emphasis>, которую мы изучим в главе «Прерывания». Большинство всех прочих состояний обсуждается в данной главе.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Процессы и потоки</p>
    </title>
    <section>
     <p>Вернемся к нашим рассуждениям о потоках и процессах, но на сей раз с точки зрения перспективы их применения в системах реального времени. Затем мы рассмотрим вызовы функций, которые применяются при работе с потоками и процессами.</p>
     <p>Мы знаем, что процесс может содержать один или больше потоков. (Процесс с нулевым числом потоков не был бы способен что-либо <emphasis>делать</emphasis>: если в доме никого нет, выполнять какую-либо полезную работу просто некому.) В операционной системе QNX/Neutrino допускается один или более процессов. (Аналогично — QNX/Neutrino с нулевым количеством процессов просто не сможет ничего сделать.)</p>
     <p>Что же делают все эти процессы и потоки? В конечном счете, они формируют систему — собрание потоков и процессов, реализующих определенную цель.</p>
     <p>На самом высоком уровне абстракции система состоит из множества процессов. Каждый процесс ответственен за обеспечение служебных функций определенного характера, независимо от того, является ли он элементом файловой системы, драйвером дисплея, модулем сбора данных, модулем управления или чем-либо еще.</p>
     <p>В пределах каждого процесса может быть множество потоков. Число потоков варьируется. Один разработчик ПО, используя только единственный поток, может реализовать те же самые функциональные возможности, что и другой, использующий пять потоков. Некоторые задачи сами по себе приводят к многопоточности и дают относительно простые решения, другие в силу своей природы, являются однопоточными, и свести их к многопоточной реализации достаточно трудно.</p>
     <p>Проблемы разработки ПО с применением потоков могли легко стать темой отдельной книги. Здесь же мы изложим только основы этой проблемы.</p>
    </section>
    <section>
     <title>
      <p>Почему процессы?</p>
     </title>
     <p>Почему же не взять просто один процесс с множеством потоков? В то время как некоторые операционные системы вынуждают вас программировать только в таком варианте, возникает ряд преимуществ при разделении объектов на множество процессов.</p>
     <p>К таким преимуществам относятся:</p>
     <p>• возможность декомпозиции задачи и модульной организации решения;</p>
     <p>• удобство сопровождения;</p>
     <p>• надежность.</p>
     <p>Концепция разделения задачи на части, т.е., на несколько независимых задач, является очень мощной. И именно такая концепция лежит в основе QNX/Neutrino. Операционная система QNX/Neutrino состоит из множества независимых модулей, каждый из которых наделен некоторой зоной ответственности. Эти модули независимы и реализованы в отдельных процессах. Разработчики из QSSL использовали эту удобную особенность для отдельной разработки модулей, независимых друг от друга. Единственная возможная установить зависимость этих модулей друг от друга — наладить между ними информационную связь с помощью небольшого количества строго определенных интерфейсов.</p>
     <p>Это естественно ведет к упрощению сопровождения программных продуктов, благодаря незначительному числу взаимосвязей. Поскольку каждый модуль четко определен, и устранять неисправности в одном таком модуле будет гораздо проще — тем более, что он не связан с другими.</p>
     <p>Тем не менее, наиболее важным моментом является надежность. Процесс, точно так же, как и жилой дом, имеет некоторые четкие «границы». Человек, живущий в доме, точно знает, когда он в доме, а когда — нет. Поток наделен в этом смысле пониманием, что если у него есть доступ к памяти в пределах процесса, он может функционировать. Если он переступит границы адресного пространства процесса, он будет уничтожен. Это означает, что два потока, работающие в различных процессах, изолированы один от другого.</p>
     <image l:href="#img_6.png"/>
     <p>Защита памяти.</p>
     <p><emphasis>Адресные пространства процессов</emphasis> устанавливаются и поддерживаются модулем администратора процессов QNX/ Neutrino. При запуске процесса администратор процессов распределяет ему некоторый объем памяти и активирует его потоки. Отведенная данному процессу память помечается как принадлежащая ему.</p>
     <p>Это означает, что если в данном процессе имеются есть несколько потоков, и ядру необходимо переключить контекст между ними, это можно сделать очень эффективно, поскольку не нужно изменять адресное пространство, достаточно просто сменить рабочий поток. Если, однако, мы должны переключиться на другой поток в другом процессе, тут уже включается в работу администратор процессов и переключает адресное пространство. Пусть вас не беспокоят возникающие при этом дополнительные издержки — под управлением QNX/Neutrino все это осуществляется очень быстро.</p>
    </section>
    <section>
     <title>
      <p>Запуск процесса</p>
     </title>
     <p>Теперь обратим внимание на функции, предназначенные для работы с потоками и процессами. Любой поток может осуществить запуск процесса; единственные налагаемые здесь ограничения вытекают из основных принципов защиты (правила доступа к файлу, ограничения на привилегии и т.д.). По всей вероятности, вам уже доводилось запускать процессы — либо из системного сценария, либо из командного интерпретатора, или из программы от своего имени.</p>
     <subtitle>Запуск процесса из командной строки</subtitle>
     <p>Например, при запуске процесса из командного интерпретатора вы можете ввести командную строку:</p>
     <p><code>$ program1</code></p>
     <p>Это предписывает командному интерпретатору запустить программу <code><strong>program1</strong></code> и ждать завершения ее работы. Или, вы могли набрать:</p>
     <p><code>$ program2 &amp;</code></p>
     <p>Это предписывает командному интерпретатору запустить программу <code><strong>program2</strong></code> без ожидания ее завершения. В таком случае говорят, что программа <code><strong>program2</strong></code> работает в фоновом режиме.</p>
     <p>Если вы пожелаете скорректировать приоритет программы до ее запуска, вы можете применить команду <code><strong>nice</strong></code> — точно так же, как в UNIX:</p>
     <p><code>$ nice program3</code></p>
     <p>Это предписывает командному интерпретатору запустить программу <code><strong>program3</strong></code> с заниженным приоритетом.</p>
     <p>Или нет?</p>
     <p>Если посмотреть, что происходит в действительности, то мы велели командному интерпретатору выполнить программу, называемую <code><strong>nice</strong></code>, с обычным приоритетом. Команда <code><strong>nice</strong></code> затем занизила свой собственный приоритет (отсюда и имя программы «nice» — «благовоспитанная») и затем запустила программу <code><strong>program3</strong></code> с этим заниженным приоритетом.</p>
     <subtitle>Запуск процесса из программы</subtitle>
     <p>Нас обычно не заботит тот факт, что командный интерпретатор создает процессы — это просто подразумевается. В некоторых прикладных задачах можно положиться на сценарии командного интерпретатора (пакеты команд, записанные в файл), которые сделают эту работу за вас, но в ряде других случаев вы пожелаете создавать процессы самостоятельно.</p>
     <p>Например, в большой мультипроцессорной системе вы можете пожелать, чтобы одна главная программа выполнила запуск всех других процессов вашего приложения на основании некоторого конфигурационного файла. Другим примером может служить необходимость запуска процессов по некоторому событию.</p>
     <p>Рассмотрим некоторые из функций, которые QNX/Neutrino обеспечивает для запуска других процессов (или подмены одного процесса другим):</p>
     <p>• <emphasis>system()</emphasis>;</p>
     <p>• семейство функций <emphasis>exec()</emphasis>;</p>
     <p>• семейство функций <emphasis>spawn()</emphasis>;</p>
     <p>• <emphasis>fork()</emphasis>;</p>
     <p>• <emphasis>vfork()</emphasis>.</p>
     <p>Какую из этих функций применять, зависит от двух требований: переносимости и функциональности. Как обычно, между этими двумя требованиями возможен компромисс.</p>
     <p>Обычно при всех запросах на создание нового процесса происходит следующее. Поток в первоначальном процессе вызывает одну из вышеприведенных функций. В конечном итоге, функция заставит администратор процессов создать адресное пространство для нового процесса. Затем ядро выполнит запуск потока в новом процессе. Этот поток выполнит несколько инструкций и вызовет функцию <emphasis>main()</emphasis>. (Конечно, в случае вызова функции <emphasis>fork()</emphasis> или <emphasis>vfork()</emphasis> новый поток начнет выполнение в новом процессе с возврата из <emphasis>fork()</emphasis> или <emphasis>vfork()</emphasis>, соответственно — как иметь с этим дело, мы вкратце рассмотрим ниже).</p>
     <subtitle>Запуск процесса с помощью вызова функции <emphasis>system()</emphasis></subtitle>
     <p>Функция <emphasis>system()</emphasis> — самая простая функция; она получает на вход одну командную строку, такую же, которую вы набрали бы в ответ на подсказку командного интерпретатора, и выполняет ее.</p>
     <p>Фактически, для обработки команды, которую вы желаете выполнить, функция <emphasis>system()</emphasis> запускает копию командного интерпретатора.</p>
     <p>Редактор, который я использую при написании данной книги, использует вызов <emphasis>system()</emphasis>. При редактировании мне может понадобиться выйти в командный интерпретатор, проверить некоторые фрагменты программ, и затем снова вернуться в редактор. Все это необходимо сделать, не потеряв позицию курсора в тексте. В этом редакторе я, к примеру, могу дать команду «:!pwd» для отображения текущего рабочего каталога. Редактор при этом выполнит следующий код:</p>
     <p><code>system("pwd");</code></p>
     <p>Подходит ли функция <emphasis>system()</emphasis> для всех дел в Поднебесной? Конечно же, нет. Однако, ее применение может быть очень полезно для множества задач, требующих создания процессов.</p>
     <subtitle>Запуск процесса с помощью вызова функций <emphasis>exec()</emphasis> и <emphasis>spawn()</emphasis></subtitle>
     <p>Давайте рассмотрим ряд других функций создания процессов.</p>
     <p>Следующие функции создания процессов, которые следует рассмотреть, принадлежат к семействам <emphasis>exec()</emphasis> и <emphasis>spawn()</emphasis>. Прежде, чем мы обратимся к подробностям их применения, рассмотрим суть различий между этими двумя группами функций.</p>
     <p>Семейство функций <emphasis>exec()</emphasis> подменяет текущий процесс другим. Я подразумеваю под этим то, что когда процесс вызывает функцию семейства <emphasis>exec()</emphasis>, этот процесс прекращает выполнение текущей программы и начинает выполнять другую. Идентификатор процесса (PID) при этом не меняется, просто процесс преобразуется в другую программу. Что произойдет с потоками в данном процессе? Мы вернемся к этой теме после того, как рассмотрим функцию <emphasis>fork()</emphasis>.</p>
     <p>С другой стороны, семейство функций <emphasis>spawn()</emphasis> так не делает. Вызов функции семейства <emphasis>spawn()</emphasis> создает другой процесс (с новым идентификатором), который соответствует программе, указанной в аргументах функции.</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Spawn</th>
       <th align="left" valign="top">POSIX</th>
       <th align="left" valign="top">Exec</th>
       <th align="left" valign="top">POSIX</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawn()</emphasis></td>
       <td align="left" valign="top">Да</td>
       <td align="left" valign="top"/>
       <td align="left" valign="top"/>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnl()</emphasis></td>
       <td align="left" valign="top">Нет</td>
       <td align="left" valign="top"><emphasis>execl()</emphasis></td>
       <td align="left" valign="top">Да</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnle()</emphasis></td>
       <td align="left" valign="top">Нет</td>
       <td align="left" valign="top"><emphasis>execle()</emphasis></td>
       <td align="left" valign="top">Да</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnlp()</emphasis></td>
       <td align="left" valign="top">Нет</td>
       <td align="left" valign="top"><emphasis>execlp()</emphasis></td>
       <td align="left" valign="top">Да</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnlpe()</emphasis></td>
       <td align="left" valign="top">Нет</td>
       <td align="left" valign="top"><emphasis>execlpe()</emphasis></td>
       <td align="left" valign="top">Нет</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnp()</emphasis></td>
       <td align="left" valign="top">Да</td>
       <td align="left" valign="top"/>
       <td align="left" valign="top"/>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnv()</emphasis></td>
       <td align="left" valign="top">Нет</td>
       <td align="left" valign="top"><emphasis>execv()</emphasis></td>
       <td align="left" valign="top">Да</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnve()</emphasis></td>
       <td align="left" valign="top">Нет</td>
       <td align="left" valign="top"><emphasis>execve()</emphasis></td>
       <td align="left" valign="top">Да</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnvp()</emphasis></td>
       <td align="left" valign="top">Нет</td>
       <td align="left" valign="top"><emphasis>execvp()</emphasis></td>
       <td align="left" valign="top">Да</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>spawnvpe()</emphasis></td>
       <td align="left" valign="top">Нет</td>
       <td align="left" valign="top"><emphasis>execvpe()</emphasis></td>
       <td align="left" valign="top">Нет</td>
      </tr>
     </table>
     <p>Рассмотрим различные варианты функций exec() и spawn(). В таблице, представленной ниже, вы увидите, что некоторые функции из них предусмотрены POSIX, а некоторые — нет. Конечно, для максимальной переносимости, следует использовать только POSIX-совместимые функции.</p>
     <p>При том, что названия функций могут показаться малопонятными, в их суффиксах есть логика.</p>
     <table>
      <tr align="left">
       <td align="left" valign="top">Суффикс:</td>
       <td align="left" valign="top">Смысл:</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>l</emphasis> (нижний регистр «L»)</td>
       <td align="left" valign="top">Список аргументов определяется через список параметров, заданный непосредственно в самом вызове и завершаемый нулевым аргументом NULL.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>е</emphasis></td>
       <td align="left" valign="top">Указывается окружение.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>p</emphasis></td>
       <td align="left" valign="top">Если не указано полное имя пути программы, для ее поиска используется переменная окружения PATH.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>v</emphasis></td>
       <td align="left" valign="top">Список аргументов определяется через указатель на вектор (массив) аргументов.</td>
      </tr>
     </table>
     <p>Список аргументов здесь — список аргументов командной строки, передаваемых программе.</p>
     <p>Заметьте, что в библиотеке языка Си функции <emphasis>spawnlp()</emphasis>, <emphasis>spawnvp()</emphasis> и <emphasis>spawnlpe()</emphasis> все вызывают функцию <emphasis>spawnvpe()</emphasis>, которая, в свою очередь, вызывает POSIX-функцию <emphasis>spawnp()</emphasis>. Функции <emphasis>spawnle()</emphasis>, <emphasis>spawnv()</emphasis> и <emphasis>spawnl()</emphasis> все в конечном счете вызывают функцию <emphasis>spawnve()</emphasis>, которая затем вызывает POSIX-функцию <emphasis>spawn()</emphasis>. И, наконец, POSIX-функция <emphasis>spawnp()</emphasis> вызывает POSIX-функцию <emphasis>spawn()</emphasis>. Таким образом, в основе всех возможностей семейства <emphasis>spawn()</emphasis> лежит сам вызов <emphasis>spawn()</emphasis>.</p>
     <p>Рассмотрим теперь различные варианты функций <emphasis>spawn()</emphasis> и <emphasis>exec()</emphasis> более подробно так, чтобы вы смогли получить навык свободного использования различных суффиксов. Затем мы перейдем непосредственно к рассмотрению вызова функции <emphasis>spawn()</emphasis>.</p>
     <subtitle><emphasis>Суффикс «l»</emphasis></subtitle>
     <p>Например, если я хочу вызвать команду <code><strong>ls</strong></code> с аргументами <code><strong>-t</strong></code>, <code><strong>-r</strong></code>, и <code><strong>-l</strong></code> (означает — «сортировать выходные данные по времени в обратном порядке и показывать выходные данные в длинном формате»), я мог бы определить это в программе так:</p>
     <p><code>/* Вызвать ls и продолжить выполнение */</code></p>
     <p><code>spawnl(P_WAIT, "/bin/ls", "/bin/ls", "-t", "-r", "-l",</code></p>
     <p><code> NULL);</code></p>
     <empty-line/>
     <p><code>/* Заменить себя на ls */</code></p>
     <p><code>execl(P_WAIT, "/bin/ls", "/bin/ls", "-t", "-r", "-l",</code></p>
     <p><code> NULL);</code></p>
     <p>Или, вариант с применением суффикса v:</p>
     <p><code>char *argv[] = {</code></p>
     <p><code> "/bin/ls",</code></p>
     <p><code> "-t",</code></p>
     <p><code> "-r",</code></p>
     <p><code> "-l",</code></p>
     <p><code> NULL</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>/* Вызвать ls и продолжить выполнение */</code></p>
     <p><code>spawnv(P_WAIT, "/bin/ls", argv);</code></p>
     <empty-line/>
     <p><code>/* Заменить себя на ls */</code></p>
     <p><code>execv(P_WAIT, "/bin/ls", "/bin/ls", argv);</code></p>
     <p>Почему именно такой выбор? Он дан для удобства восприятия. У вас может быть синтаксический анализатор, уже встроенный в вашу программу, и может быть удобно сразу оперировать массивами строк. В этом случае я бы рекомендовал применять варианты с суффиксом «v». Или вам может понадобиться запрограммировать вызов программы, когда вам известно, где он находится и какие имеет параметры. В этом случае, зачем вам утруждать себя созданием массива строк, когда вы знаете точно, какие нужны аргументы? Просто передайте их варианту функции с суффиксом «l».</p>
     <p>Отметим, что мы передаем реальное имя пути программы (<emphasis>/bin/ls</emphasis>), а затем имя программы <emphasis>еще раз</emphasis> в качестве первого аргумента. Это делается для поддержки программ, которые ведут себя по-разному в зависимости от того, под каким именем они были вызваны.</p>
     <p>Например, GNU-утилиты компрессии и декомпрессии (<strong>gzip</strong> и <strong>gunzip</strong>) фактически привязаны к одному и тому же исполняемому модулю. Когда исполняемый модуль стартует, он анализирует аргумент <strong>argv[0]</strong> (передаваемый функции <emphasis>main()</emphasis>) и принимает решение, следует ли выполнять компрессию или декомпрессию.</p>
     <subtitle><emphasis>Суффикс «е»</emphasis></subtitle>
     <p>Варианты с суффиксом «<strong>е</strong>» передают программе окружение. Окружение — это только своего рода «контекст», в котором работает программа. Например, у вас может быть программа проверки орфографии, у которой есть эталонный словарь. Вместо описания каждый раз в командной строке местоположения словаря вы могли бы сделать это в окружении:</p>
     <p><code>$ export DICTIONARY=/home/rk/.dict</code></p>
     <p><code>$ spellcheck document.1</code></p>
     <p>Команда <code><strong>export</strong></code> предписывает командному интерпретатору создать новую переменную окружения (в нашем случае <code><strong>DICTIONARY</strong></code>) и присвоить ей значение (<code><strong>/home/rk/.dict</strong></code>).</p>
     <p>Если вы когда-либо хотели бы использовать различные словари, вы были бы должны изменить среду до выполнения программы. Это просто сделать из оболочки:</p>
     <p><code>$ export DICTIONARY=/home/rk1.altdict</code></p>
     <p><code>$ spellcheck document.1</code></p>
     <p>Но как сделать это из ваших собственных программ? Для того чтобы применять «e»-версии функций <emphasis>spawn()</emphasis> и <emphasis>exec()</emphasis>, вам следует определить массив строк, представляющих собой окружение:</p>
     <p><code>char *env[] = {</code></p>
     <p><code> "DICTIONARY=/home/rk/.altdict",</code></p>
     <p><code> NULL</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>// Запуск проверки в отдельном процессе:</code></p>
     <p><code>spawnle(P_WAIT, "/usr/bin/spellcheck",</code></p>
     <p><code> "/usr/bin/spellcheck", "documents.1", NULL, env);</code></p>
     <empty-line/>
     <p><code>// Запуск проверки вместо себя:</code></p>
     <p><code>execle("/usr/bin/spellcheck", "/usr/bin/spellcheck",</code></p>
     <p><code> "document.1", NULL, env);</code></p>
     <subtitle><emphasis>Суффикс «p»</emphasis></subtitle>
     <p>Версии с суффиксом «p» будут искать исполняемый модуль программы в списке каталогов, приведенном в переменной окружения <strong>PATH</strong>. Вы, вероятно, отметили, что во всех примерах местоположение исполняемых модулей строго определено — <code><strong>/bin/ls</strong></code> и <code><strong>/usr/bin/spellcheck</strong></code>. А как быть с другими исполняемыми модулями? Если вы не хотите сразу определить точный путь к нужной программе, было бы лучше сделать так, чтобы места поиска исполняемых модулей вашей программе сообщил пользователь. Стандартная системная переменная <strong>PATH</strong> для этого и предназначена. Ниже приведено ее значение для минимальной системы:</p>
     <p><code>PATH=/proc/boot:/bin</code></p>
     <p>Это сообщает командному интерпретатору, что когда я набираю команду, он в первую очередь должен просмотреть каталог <code><strong>/proc/boot/</strong></code>, и если не сможет найти команду там, то должна просмотреть каталог бинарных файлов <code><strong>/bin</strong></code>. Переменная <strong>PATH</strong> представляет собой разделяемый двоеточиями список каталогов для поиска команд. К переменной <strong>PATH</strong> вы можете добавлять столько элементов, сколько пожелаете, но имейте в виду, что при поиске файла будут проанализированы все элементы (в приведенной последовательности).</p>
     <p>Если вы не знаете путь к выполнимой программе, вы можете использовать варианты с суффиксом «p».</p>
     <p>Например:</p>
     <p><code>// Использование явного пути:</code></p>
     <p><code>execl("/bin/ls", "/bin/ls", "-l", "-t", "-r", NULL);</code></p>
     <empty-line/>
     <p><code>// Поиск пути в PATH:</code></p>
     <p><code>execp("ls", "ls", "-l", "-t", "-r", NULL) ;</code></p>
     <p>Если функция <emphasis>execl()</emphasis> не сможет найти <code><strong>ls</strong></code> в <code><strong>/bin</strong></code>, она завершится с ошибкой. Функция <emphasis>execlp()</emphasis> просмотрит все каталоги, указанные в <strong>PATH</strong>, в поисках <code><strong>ls</strong></code>, и завершится с ошибкой только в том случае, если не сможет найти ls ни в одном из этих каталогов. Это также прекрасная вещь для многоплатформенной поддержки — вашей программе не обязательно знать имена каталогов, принятых на разных машинах, она просто выполнит поиск.</p>
     <p>А что произойдет, если сделать так?</p>
     <p><code>execlp("/bin/ls", "ls", "-l", "-t", "-r", NULL);</code></p>
     <p>Выполняет ли этот вызов поиск в окружении? Нет. Вы явно указали <emphasis>execlp()</emphasis> имя пути, что отменяет правило поиска в <strong>PATH</strong>. Если <code><strong>ls</strong></code> не будет найдена в <code><strong>/bin</strong></code> (а здесь это будет именно так), то никаких других попыток поиска не выполняется — эта ситуация подобна варианту с использованием функции <emphasis>execl()</emphasis>).</p>
     <p>Опасно ли смешивать явный путь с простым именем команды (например, указывать путь как <code><strong>/bin/ls</strong></code>, а имя — как <code><strong>ls</strong></code> вместо <code><strong>/bin/ls</strong></code>)? Обычно нет, потому что:</p>
     <p>• значительное число программ так или иначе игнорирует <code><strong>argv[0]</strong></code>;</p>
     <p>• те программы, поведение которых зависит от их имени, обычно вызывают функцию <emphasis>basename()</emphasis>, которая удаляет каталоговую часть <code><strong>argv[0]</strong></code> и возвращает только имя.</p>
     <p>Единственная обоснованная причина использования полного имени пути в качестве первого параметра заключается в том, что программа может выводить диагностические сообщения, содержащие этот первый параметр, который немедленно укажет вам, откуда она была вызвана. Это может быть важно, если копии программы располагаются в нескольких каталогах из перечисленных в <strong>PATH</strong>.</p>
     <p>Функции семейства <emphasis>spawn()</emphasis> имеют дополнительный параметр; во всех приведенных выше примерах я всегда указывал P_WAIT. Имеются четыре флага, которые вы можете придать функции <emphasis>spawn()</emphasis>, чтобы изменить ее поведение:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top">P_WAIT</td>
       <td align="left" valign="top">Вызывающий процесс (ваша программа) будет блокирован до тех пор, пока вновь созданный процесс не отработает и не завершится.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">P_NOWAIT</td>
       <td align="left" valign="top">Вызывающая программа не будет блокирована на время выполнения вновь созданной. Это позволяет вам запустить программу в фоновом режиме и продолжать выполнение, пока она делает свое дело.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">P_NOWAITO</td>
       <td align="left" valign="top">Аналогично P_NOWAIT за исключением того, что устанавливается флаг SPAWN_NOZOMBIE. Это означает, что вы не должны беспокоить себя вызовом функции <emphasis>waitpid()</emphasis> для очистки кода завершения процесса.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">P_OVERLAY</td>
       <td align="left" valign="top">Этот флаг превращает вызов функции <emphasis>spawn()</emphasis> в соответствующей вызов <emphasis>exec()</emphasis>! Ваша программа преобразуется в указанную программу без изменения идентификатора процесса ID. Вообще-то, если вы хотите сделать именно так, то, наверное, будет более корректно использовать вызов <emphasis>exec()</emphasis>, поскольку это избавит будущих читателей ваших исходных текстов от необходимости искать P_OVERLAY в справочном руководстве по библиотеке языка Си!</td>
      </tr>
     </table>
     <subtitle><emphasis>Просто spawn()</emphasis></subtitle>
     <p>Как мы упомянули выше, все функции семейства <emphasis>spawn()</emphasis>, в конечном счете, вызывают базовую функцию <emphasis>spawn()</emphasis>. Ниже приведен прототип функции <emphasis>spawn()</emphasis>:</p>
     <p><code>#include &lt;spawn.h&gt;</code></p>
     <p><code>pid_t spawn(const char *path, int fd_count,</code></p>
     <p><code> const int fd_map[], const struct inheritance *inherit,</code></p>
     <p><code> char* const argv[], char* const envp[]);</code></p>
     <p>Мы можем не обращать внимание на параметры <emphasis>path</emphasis>, <emphasis>argv</emphasis>, и <emphasis>envp</emphasis> — мы уже рассмотрели их выше как местоположение исполняемого модуля (<emphasis>path</emphasis>), вектор параметров (<emphasis>argv</emphasis>) и окружение (<emphasis>envp</emphasis>).</p>
     <p>Параметры <emphasis>fd_count</emphasis> и <emphasis>fd_map</emphasis> идут вместе. Если вы задаете нуль в <emphasis>fd_count</emphasis>, тогда <emphasis>fd_map</emphasis> игнорируется, и это означает, что вновь создаваемый процесс унаследует от родительского все дескрипторы файлов (кроме тех, которые модифицированы флагом FD_CLOEXEC функции <emphasis>fcntl()</emphasis>). Если параметр <emphasis>fd_count</emphasis> имеет ненулевое значение, то он задает число дескрипторов файлов, содержащихся в <emphasis>fd_map</emphasis>, и будут унаследованы только они.</p>
     <p>Параметр <emphasis>inherit</emphasis> — это указатель на структуру, которая содержит набор флагов, маски сигналов, и т.д. Для получения более подробной информации об этом вам следует обратиться за помощью к справочному руководству по библиотеке языка Си.</p>
     <subtitle>Запуск процесса при помощи функции <emphasis>fork()</emphasis></subtitle>
     <p>Предположим, что вы решили создать новый процесс, который был бы идентичен работающему в настоящее время процессу, и сделать это так, чтобы эти два процесса выполнялись одновременно. Вы могли бы решить эту проблему с помощью функции <emphasis>spawn()</emphasis> (и параметра P_NOWAIT), передав вновь создаваемому процессу достаточно информации о точном состоянии вашего процесса, чтобы новый процесс мог настроить себя сам. Однако, такой подход может оказаться чрезвычайно сложным, потому что описание «текущего состояния» процесса может потребовать большого количества данных.</p>
     <p>Существует более простой способ — применение функции <emphasis>fork()</emphasis> которая просто копирует текущий процесс. У результирующего процесса как код, так и данные полностью совпадают с таковыми для родительского процесса.</p>
     <p>Конечно же, невозможно создать процесс, который во всем был бы идентичен родительскому. Почему? Наиболее очевидное различие между этими двумя процессами должно быть в идентификаторе процесса — мы не можем создать два процесса с одним и тем же идентификатором. Если вы посмотрите документацию на функцию <emphasis>fork()</emphasis> в справочном руководстве по библиотеке Си, вы увидите, что между этими двумя процессами будет иметь место ряд различий. Внимательно изучите этот список, чтоб быть уверенным в корректном применении функции <emphasis>fork()</emphasis>.</p>
     <p>Если после ветвления по <emphasis>fork()</emphasis> получаются два одинаковых процесса, то как же их различить? Когда вы вызываете <emphasis>fork()</emphasis>, вы тем самым создаете другой процесс, выполняющий тот же самый код и в том же самом местоположении (то есть оба процесса ввернутся из вызова <emphasis>fork()</emphasis>), что и родительский. Рассмотрим пример программы:</p>
     <p><code>int main (int argc, char **argv) {</code></p>
     <p><code> int retval;</code></p>
     <p><code> printf("Это определенно родительский процесс\n");</code></p>
     <p><code> fflush(stdout);</code></p>
     <p><code> retval = fork();</code></p>
     <p><code> printf("Кто это сказал?\n");</code></p>
     <p><code> return (EXIT_SUCCESS);</code></p>
     <p><code>}</code></p>
     <p>После вызова <emphasis>fork()</emphasis> оба процесса выполнят второй вызов <emphasis>printf()</emphasis>! Если вы запустите эту программу на выполнение, она выведет на экран примерно следующее:</p>
     <p><code>Это определенно родительский процесс</code></p>
     <p><code>Кто это сказал?</code></p>
     <p><code>Кто это сказал?</code></p>
     <p>Иными словами, оба процесса выведут вторую строку.</p>
     <p>Существует только один способ различить эти два процесса — он заключается в использовании возвращаемого функцией <emphasis>fork()</emphasis> значения, размещенного в <emphasis>retval</emphasis>. Во вновь созданном дочернем процессе <emphasis>retval</emphasis> будет иметь нулевое значение, а в родительском она будет содержать идентификатор дочернего.</p>
     <p>Китайская грамота, да? Проясним этот момент еще одним фрагментом программы:</p>
     <p><code>printf("PID родителя равен %d\n", getpid());</code></p>
     <p><code>fflush(stdout);</code></p>
     <empty-line/>
     <p><code>if (child_pid = fork()) {</code></p>
     <p><code> printf("Это родитель, PID сына %d\n", child_pid);</code></p>
     <p><code>} else {</code></p>
     <p><code> printf("Это сын, PID %d\n", getpid());</code></p>
     <p><code>}</code></p>
     <p>Эта программа выведет на экран примерно следующее:</p>
     <p><code>PID родителя равен 4496</code></p>
     <p><code>Это родитель, PID сына 8197</code></p>
     <p><code>Это сын, PID 8197</code></p>
     <p>Таким образом, после применения функции <emphasis>fork()</emphasis> вы можете определить, в каком процессе находитесь («отец» это или «сын»), анализируя значение, возвращаемое функцией <emphasis>fork()</emphasis>.</p>
     <subtitle>Запуск процесса с помощью функции <emphasis>vfork()</emphasis></subtitle>
     <p>Применение функции <emphasis>vfork()</emphasis> по сравнению с обычной <emphasis>fork()</emphasis> позволяет существенно сэкономить на ресурсах, поскольку она делает разделяемым адресное пространство родителя.</p>
     <p>Функция <emphasis>vfork()</emphasis> создает «сына», но затем приостанавливает родительский поток до тех пор, пока «сын» не вызовет функцию <emphasis>exec()</emphasis> или не завершится (с помощью <emphasis>exit()</emphasis> или его друзей). В дополнение к этому, функция <emphasis>vfork()</emphasis> будет работать в системах с физической моделью памяти, в то время как функция <emphasis>fork()</emphasis> не сможет, потому что нуждается в создании такого же адресного пространства, а это в физической модели памяти просто невозможно.</p>
     <subtitle>Создание процесса и потоки</subtitle>
     <p>Предположим, что у вас есть процесс, и вы еще не создали никаких потоков (т.е., вы работаете с одним потоком — тем, который вызвал функцию <emphasis>main()</emphasis>). Если вызвать функцию <emphasis>fork()</emphasis>, то будет создан другой процесс, и тоже с одним потоком.</p>
     <p>Это был простейший пример.</p>
     <p>Теперь предположим, что в вашем процессе вы вызвали <emphasis>pthread_create()</emphasis> для создания другого потока. Если вы теперь вызовете функцию <emphasis>fork()</emphasis>, она возвратит ENOSYS (что означает, что функция не поддерживается)! Почему так?</p>
     <p>Вы можете верить этому или нет, но это POSIX-совместимая ситуация. POSIX утверждает, что функция <emphasis>fork()</emphasis> может возвращать ENOSYS. На самом же деле происходит вот что: Си-библиотека QNX/Neutrino не рассчитана на ветвление процесса с потоками. Когда вы вызываете <emphasis>pthread_create()</emphasis>, эта функция устанавливает флаг, сигнализирующий что-то типа «не позволяйте этому процессу применять <emphasis>fork()</emphasis>, потому что механизм ветвления в данном случае не определен». Затем, при вызове <emphasis>fork()</emphasis>, этот флаг проверяется и, если он установлен, это принуждает <emphasis>fork()</emphasis> возвратить значение ENOSYS.</p>
     <p>Такая реализация была сделана преднамеренно, и причина этого кроется в потоках и мутексах. Если бы этого ограничения не было (и оно может быть снято в будущих версиях QNX/ Neutrino), то вновь созданный процесс, как и предполагается, имел бы то же самое число потоков, что и исходный. Однако, тут возникает сложность, потому что некоторые из исходных потоков могут являться владельцами мутексов. Поскольку вновь создаваемый процесс имеет ту же область данных, что и исходный, библиотека должна была бы отслеживать, какие мутексы принадлежат каким потокам в исходном процессе, и затем дублировать принадлежность мутексов в новом процессе. Это не является невозможным: есть функция, называемая <emphasis>pthread_atfork()</emphasis>, которая обеспечивает процессу возможность обрабатывать такие ситуации. Однако, на момент написания этой книги функциональные возможности <emphasis>pthread_atfork()</emphasis> используются не всеми мутексами в Си-библиотеке QNX/Neutrino.</p>
     <subtitle>Так что же использовать?</subtitle>
     <p>Очевидно, если вы переносите в QNX/Neutrino программу из другой ОС, вы пожелаете использовать те же механизмы, что и в исходной программе. Я бы посоветовал избегать в новом коде применения функции <emphasis>fork()</emphasis>, и вот почему:</p>
     <p>• функция <emphasis>fork()</emphasis> не работает с несколькими потоками — см. выше;</p>
     <p>• при работе с <emphasis>fork()</emphasis> в условиях многопоточности вы должны будете зарегистрировать обработчик <emphasis>pthread_atfork()</emphasis> и локировать каждый мутекс по отдельности перед собственно ветвлением, а это усложнит структуру программы;</p>
     <p>• дочерние процессы, созданные <emphasis>fork()</emphasis>, копируют все открытые дескрипторы файлов. Как мы увидим позже в главе «Администратор ресурсов», это требует много дополнительных усилий, которые может быть совершенно напрасными, если дочерний процесс затем сразу сделает <emphasis>exec()</emphasis> и тем самым закроет все открытые дескрипторы.</p>
     <p>Выбор между семействами функций <emphasis>vfork()</emphasis> и <emphasis>spawn()</emphasis> сводится к переносимости, а также того, что должны делать родительский и дочерний процесс. Функция <emphasis>vfork()</emphasis> задержит выполнение до тех пор, пока дочерний процесс не вызовет <emphasis>exec()</emphasis> или не завершится, тогда как семейство <emphasis>spawn()</emphasis> может позволить работать обоим процессам одновременно. Впрочем, в разных ОС поведение функции <emphasis>vfork()</emphasis> может несколько отличаться.</p>
    </section>
    <section>
     <title>
      <p>Запуск потока</p>
     </title>
     <p>Теперь, когда мы знаем, как запустить другой процесс, давайте рассмотрим, как осуществить запуск другого потока.</p>
     <p>Любой поток может создать другой поток в том же самом процессе; на это не налагается никаких ограничений (за исключением объема памяти, конечно!) Наиболее общий путь реализации этого — использование вызова функций POSIX <emphasis>pthread_create()</emphasis>:</p>
     <p><code>#include &lt;pthread.h&gt;</code></p>
     <p><code>int pthread_create(pthread_t *thread,</code></p>
     <p><code> const pthread_attr_t *attr,</code></p>
     <p><code> void*(*start_routine)(void*), void *arg);</code></p>
     <p>Функция <emphasis>pthread_create()</emphasis> имеет четыре аргумента :</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>thread</emphasis></td>
       <td align="left" valign="top">указатель на <code><strong>pthread_t</strong></code>, где хранится идентификатор потока</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>attr</emphasis></td>
       <td align="left" valign="top">атрибутная запись</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>start_routine</emphasis></td>
       <td align="left" valign="top">подпрограмма, с которой начинается поток</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>arg</emphasis></td>
       <td align="left" valign="top">параметр, который передается подпрограмме <emphasis>start_routine</emphasis></td>
      </tr>
     </table>
     <p>Отметим, что указатель <emphasis>thread</emphasis> и атрибутная запись (<emphasis>attr</emphasis>) — необязательные элементы, вы может передавать вместо них NULL.</p>
     <p>Параметр <emphasis>thread</emphasis> может использоваться для хранения идентификатора вновь создаваемого потока. Обратите внимание, что в примерах, приведенных ниже, мы передадим NULL, обозначив этим, что мы не заботимся о том, какой идентификатор будет иметь вновь создаваемый поток.</p>
     <p>Если бы нам было до этого дело, мы бы сделали так:</p>
     <p><code>pthread_t tid;</code></p>
     <p><code>pthread_create(&amp;tid, ...</code></p>
     <p><code>printf("Новый поток имеет идентификатор %d\n", tid);</code></p>
     <p>Такое применение совершенно типично, потому что вам часто может потребоваться знать, какой поток выполняет какой участок кода.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Небольшой тонкий момент. Новый поток может начать работать еще до присвоения значения параметру <emphasis>tid</emphasis>. Это означает, что вы должны внимательно относиться к использованию <emphasis>tid</emphasis> в качестве глобальной переменной. В примере, приведенном выше, все будет корректно, потому что вызов <emphasis>pthread_create()</emphasis> отработал до использования <emphasis>tid</emphasis>, что означает, что на момент использования <emphasis>tid</emphasis> имел корректное значение.</p>
     </cite>
     <p>Новый поток начинает выполнение с функции <emphasis>start_routine()</emphasis>, с параметром <emphasis>arg</emphasis>.</p>
     <subtitle>Атрибутная запись потока</subtitle>
     <p>Когда вы осуществляете запуск нового потока, он может следовать ряду четко определенных установок по умолчанию, или же вы можете явно задать его характеристики.</p>
     <p>Прежде, чем мы перейдем к обсуждению задания атрибутов потока, рассмотрим тип данных <code><strong>pthread_attr_t</strong></code>:</p>
     <p><code>typedef struct {</code></p>
     <p><code> int flags;</code></p>
     <p><code> size_t   stacksize;</code></p>
     <p><code> void     *stackaddr;</code></p>
     <p><code> void     (*exitfunc)(void *status);</code></p>
     <p><code> int      policy;</code></p>
     <p><code> struct   sched_param param;</code></p>
     <p><code> unsigned guardsize;</code></p>
     <p><code>} pthread_attr_t;</code></p>
     <p>В основном эти поля используются как:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>flags</emphasis></td>
       <td align="left" valign="top">Неисчисляемые (булевы) характеристики потока — например, создается поток как «обособленный» или «синхронизирующий».</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>stacksize</emphasis>, <emphasis>stackaddr</emphasis> и <emphasis>guardsize</emphasis></td>
       <td align="left" valign="top">Параметры стека.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>exitfunc</emphasis></td>
       <td align="left" valign="top">Функция, выполняемая перед завершением потока.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>policy</emphasis> и <emphasis>param</emphasis></td>
       <td align="left" valign="top">Параметры диспетчеризации.</td>
      </tr>
     </table>
     <p>Доступны следующие функции:</p>
     <p>Управление атрибутами</p>
     <p><emphasis>pthread_attr_destroy()</emphasis></p>
     <p><emphasis>pthread_attr_init()</emphasis></p>
     <p>Флаги (булевы характеристики)</p>
     <p><emphasis>pthread_attr_getdetachstate()</emphasis></p>
     <p><emphasis>pthread_attr_setdetachstate()</emphasis></p>
     <p><emphasis>pthread_attr_getinheritsched()</emphasis></p>
     <p><emphasis>pthread_attr_setinheritsched()</emphasis></p>
     <p><emphasis>pthread_attr_getscope()</emphasis></p>
     <p><emphasis>pthread_attr_setscope()</emphasis></p>
     <p>Параметры стека</p>
     <p><emphasis>pthread_attr_getguardsize()</emphasis></p>
     <p><emphasis>pthread_attr_setguardsize()</emphasis></p>
     <p><emphasis>pthread_attr_getstackaddr()</emphasis></p>
     <p><emphasis>pthread_attr_setstackaddr()</emphasis></p>
     <p><emphasis>pthread_attr_getstacksize()</emphasis></p>
     <p><emphasis>pthread_attr_setstacksize()</emphasis></p>
     <p>Параметры диспетчеризации</p>
     <p><emphasis>pthread_attr_getschedparam()</emphasis></p>
     <p><emphasis>pthread_attr_setschedparam()</emphasis></p>
     <p><emphasis>pthread_attr_getschedpolicy()</emphasis></p>
     <p><emphasis>pthread_attr_setschedpolicy()</emphasis></p>
     <p>Список выглядит довольно большим (18 функций), но в действительности нас будет заботить применение только примерно половины функций из этого списка, потому что все эти они сгруппированы по парам «get» — «set», т.е. в каждой паре есть функция как получения параметров (get), так и их установки (set) — за исключением функций <emphasis>pthread_attr_init()</emphasis> и <emphasis>pthread_attr_destroy()</emphasis>.</p>
     <p>Прежде чем мы исследуем назначения атрибутов, следует отметить одно обстоятельство. Вы обязаны вызвать <emphasis>pthread_attr_init()</emphasis> для инициализации атрибутной записи до момента ее использования, задействовать ее с помощью соответствующей функции (функций) <emphasis>pthread_attr_set*()</emphasis> и только затем вызвать функцию <emphasis>pthread_create()</emphasis> для создания потока. Изменение атрибутной записи после того, как поток уже создан, не будет иметь никакого действия.</p>
     <subtitle>Администрирование атрибутов потока</subtitle>
     <p>Перед использованием атрибутной записи для ее инициализации следует вызвать функцию <emphasis>pthread_attr_init()</emphasis>:</p>
     <p><code>...</code></p>
     <p><code>pthread_attr_t attr;</code></p>
     <p><code>...</code></p>
     <p><code>pthread_attr_init(&amp;attr);</code></p>
     <p>Вы можете также вызывать <emphasis>pthread_attr_destroy()</emphasis> для «деинициализации» атрибутной записи потока, но так обычно никто не делает (если не требуется жесткой POSIX-совместимости).</p>
     <p>В приведенных ниже описаниях значения по умолчанию помечены комментарием «(по умолчанию)».</p>
     <subtitle>Атрибут потока «flags» (флаги)</subtitle>
     <p>Три функции — <emphasis>pthread_attr_setdetachstate()</emphasis>, <emphasis>pthread_attr_setinheritsched()</emphasis> и <emphasis>pthread_attr_setscope()</emphasis> — определяют, создается ли поток как «синхронизирующий» («joinable») или как «обособленный» (detached), наследует ли поток атрибуты диспетчеризации от создающего потока или использует атрибуты диспетчеризации, указанные в функциях <emphasis>pthread_attr_setschedparam()</emphasis> и <emphasis>pthread_attr_setschedpolicy()</emphasis>, и, наконец, имеет ли поток масштаб «системы» или «процесса».</p>
     <p>Для создания «синхронизирующего» потока (это значит, что с завершением этого потока можно синхронизировать другой поток при помощи функции <emphasis>pthread_join()</emphasis>), используется вызов:</p>
     <p><emphasis>(по умолчанию)</emphasis></p>
     <p><code>pthread_attr_setdetachstate(&amp;attr,</code></p>
     <p><code> PTHREAD_CREATE_JOINABLE);</code></p>
     <p>Чтобы создать поток, синхронизация с завершением которого невозможна (такой поток называют «обособленным»), надо было бы сделать так:</p>
     <p><code>pthread_attr_setdetachstate(&amp;attr,</code></p>
     <p><code> PTHREAD_CREATE_DETACHED);</code></p>
     <p>Если вы желаете, чтобы поток унаследовал атрибуты диспетчеризации от потока, его создающего (то есть имел бы ту же самую дисциплину диспетчеризации и тот же самый приоритет, что и родитель), вам следует сделать так:</p>
     <p><emphasis>(по умолчанию)</emphasis></p>
     <p><code>pthread_attr_setinheritsched(&amp;attr, PTHREAD_INHERIT_SCHED);</code></p>
     <p>Для создания потока, который использует атрибуты диспетчеризации, указанные в непосредственно в атрибутной записи (это делается при помощи функций <emphasis>pthread_attr_setsetschedparam()</emphasis> и <emphasis>pthread_attr_setschedpolicy()</emphasis>), вызов выглядел бы следующим образом:</p>
     <p><code>pthread_attr_setinheritsched(&amp;attr,</code></p>
     <p><code> PTHREAD_EXPLICIT_SCHED);</code></p>
     <p>И наконец, функция <emphasis>pthread_attr_setscope()</emphasis>. Вам не придется ее вызывать никогда. Почему? Потому что QNX/Neutrino поддерживает для потоков только масштаб системы, и соответствующее значение устанавливается по умолчанию, когда вы инициализируете атрибут. (Масштаб системы означает, что за обладание ресурсами все потоки в системе конкурируют друг с другом; масштаб процесса же означает, что потоки конкурируют за процессор только в пределах «своего» процесса, а диспетчеризацию процессов выполняет ядро).</p>
     <p>Если вам необходимо вызвать эту функцию, вы можете сделать это только следующим образом:</p>
     <p><emphasis>(по умолчанию)</emphasis></p>
     <p><code>pthread_attr_setscope(&amp;attr, PTHREAD_SCOPE_SYSTEM);</code></p>
     <subtitle>Атрибуты потока «stack» (параметры стека)</subtitle>
     <p>Прототипы функций установки параметров стека в атрибутах потока выглядят следующим образом:</p>
     <p><code>int pthread_attr_setguardsize(pthread_attr_t *attr,</code></p>
     <p><code> size_t gsize);</code></p>
     <empty-line/>
     <p><code>int pthread_attr_setstackaddr(pthread_attr_t *attr,</code></p>
     <p><code> void *addr);</code></p>
     <empty-line/>
     <p><code>int pthread_attr_setstacksize(pthread_attr_t *attr,</code></p>
     <p><code> size_t ssize);</code></p>
     <p>Все эти три функции имеют в качестве первого параметра атрибутную запись, вторые параметры перечислены ниже:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>gsize</emphasis></td>
       <td align="left" valign="top">Размер «области защиты».</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>addr</emphasis></td>
       <td align="left" valign="top">Адрес стека, если последний вами предусмотрен.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>ssize</emphasis></td>
       <td align="left" valign="top">Размер стека.</td>
      </tr>
     </table>
     <p>Область защиты — это область памяти, расположенная сразу после стека, которую поток не может использовать для записи. Если это происходит (а это означает, что стек вот-вот переполнится), потоку будет послан SIGSEGV. Если размер области защиты равен 0, это означает, что области защиты не предусматривается. Это также подразумевает, что проверка стека на переполнение выполняться не будет. Если размер области защиты отличен от нуля, то это устанавливает его по меньшей мере в общесистемное значение по умолчанию (которое вы можете получить по запросу <emphasis>sysconf()</emphasis>, указав ему константу _SC_PAGESIZE). Заметьте, что ненулевой минимально возможный размер области защиты составляет одну страницу (например, 4 Кб для процессора x86). Также отметьте, что страница защиты не занимает никакой физической памяти, это уловка с применением виртуальной адресации (MMU). Параметр <emphasis>addr</emphasis> представляет собой адрес стека, если вы его задаете явно. Вы можете задать вместо него NULL, что будет значить, что система будет должна сама распределить (и освободить!) стек для потока. Преимущество явного определения стека для потока состоит в том, что вы сможете делать «посмертный» (после аварийного завершения) анализ глубины стека. Это достигается распределением области стека и заполнением ее некоторой «подписью» (например, многократно повторяемой строкой «STACK»), после чего поток запускается на выполнение. По завершении работы потока вы сможете проанализировать область стека и посмотреть, на какую глубину поток затер в ней вашу «подпись», и тем самым определить максимальную глубину стека, использованную потоком в данном конкретном сеансе выполнения.</p>
     <p>Параметр <emphasis>ssize</emphasis> определяет размер стека. Если вы явно задаете адрес области стека в параметре <emphasis>addr</emphasis>, то параметр ssize должен задавать размер этой области. Если вы не задаете адрес области стека в параметре <emphasis>addr</emphasis> (то есть передаете вместо адреса NULL), то параметр ssize сообщает системе, стек какого размера следует распределить. Если вы укажете для параметра <emphasis>ssize</emphasis> значение 0 (ноль), система выберет размер стека, заданный по умолчанию. Очевидно, что задавать 0 в качестве параметра <emphasis>ssize</emphasis> и при этом явно указывать адрес стека, используя параметр <emphasis>addr</emphasis> — порочная практика, поскольку в действительности вы тем самым заявляете: «вот указатель на объект, который имеет некоторый заданный по умолчанию размер». Проблема здесь заключается в том, что между размером объекта и передаваемым значением нет никакой связи.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Если стек назначается с помощью параметра <emphasis>attr</emphasis>, данный поток не будет защищен от переполнения этого стека (то есть область защиты будет отсутствовать).</p>
     </cite>
     <subtitle>Атрибуты потока «scheduling» (диспетчеризация)</subtitle>
     <p>Наконец, если вы определяете PTHREAD_EXPLICIT_SCHED для функции <emphasis>pthread_attr_setinheritsched()</emphasis>, тогда вам необходимо будет как-то определить дисциплину диспетчеризации и приоритет для потока, который вы намерены создать.</p>
     <p>Это выполняется с помощью двух функций:</p>
     <p><code>int pthread_attr_setschedparam(pthread_attr_t *attr,</code></p>
     <p><code> const struct sched_param *param);</code></p>
     <empty-line/>
     <p><code>int pthread_attr_setschedpolicy(pthread_attr_t *attr,</code></p>
     <p><code> int policy);</code></p>
     <p>С параметром <emphasis>policy</emphasis> все просто — это либо SCHED_FIFO, либо SCHED_RR, либо SCHED_OTHER.</p>
     <cite>
      <p><image l:href="#img_1.png"/>В рассматриваемой версии QNX/Neutrino параметр SCHED_OTHER интерпретируется как SCHED_RR (карусельная диспетчеризация).</p>
     </cite>
     <p>Параметр <emphasis>param</emphasis> — структура, которая содержит единственный элемент: <emphasis>sched_priority</emphasis>. Задайте этот параметр путем прямого присвоения ему значения желаемого приоритета.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Стандартная ошибка, которой следует избегать, заключается в задании PTHREAD_EXPLICIT_SCHED и затем определением только дисциплины диспетчеризации. Проблема состоит в том, что в инициализированной атрибутной записи значение <emphasis>param.sched_priority</emphasis> есть 0 (ноль). Это тот же самый приоритет, что и у «холостого» потока (IDLE), что означает, что создаваемый вами поток будет конкурировать за процессор с «холостым» потоком.</p>
      <p>Плавали, знаем. :-)</p>
      <p>На том, что QSSL зарезервировала нулевой приоритет только для «холостого» потока, уже «прокололось» немало программистов. Поток с нулевым приоритетом просто не сможет выполняться.</p>
     </cite>
     <subtitle>Несколько примеров</subtitle>
     <p>Давайте рассмотрим ряд примеров. Будем считать, что в обсуждаемой программе подключены нужные заголовочные файлы (<code><strong>&lt;pthread.h&gt;</strong></code> и <code><strong>&lt;sched.h&gt;</strong></code>), а также что поток, который предстоит создать, называется <emphasis>new_thread()</emphasis>, и для него существуют все необходимые прототипы и определения.</p>
     <p>Самый обычный способ создания потока — просто оставить везде значения по умолчанию:</p>
     <p><code>pthread_create(NULL, NULL, <emphasis>new_thread</emphasis>, NULL);</code></p>
     <p>В вышеупомянутом примере мы создали наш новый поток со значениями параметров по умолчанию и передали ему NULL в качестве его единственного параметра (третий NULL в указанном выше вызове <emphasis>pthread_create()</emphasis>).</p>
     <p>Вообще говоря, вы можете передавать вашему новому потоку что угодно через параметр <emphasis>arg</emphasis>. Например, число 123:</p>
     <p><code>pthread_create(NULL, NULL, <emphasis>new_thread</emphasis>, (void*)123);</code></p>
     <p>Более сложный пример — создание «обособленного» (detached) потока с диспетчеризацией карусельного типа (RR) и приоритетом 15:</p>
     <p><code>pthread_attr_t attr;</code></p>
     <empty-line/>
     <p><code>// Инициализировать атрибутную запись</code></p>
     <p><code>pthread_attr_init(&amp;attr);</code></p>
     <empty-line/>
     <p><code>// Установить «обособленность»</code></p>
     <p><code>pthread_attr_setdetachstate(&amp;attr,</code></p>
     <p><code> PTHREAD_CREATE_DETACHED);</code></p>
     <empty-line/>
     <p><code>// Отменить наследование по умолчанию (INHERIT_SCHED)</code></p>
     <p><code>pthread_attr_setinheritsched(&amp;attr,</code></p>
     <p><code> PTHREAD_EXPLICIT_SCHED);</code></p>
     <p><code>pthread_attr_setschedpolicy(&amp;attr, SCHED_RR);</code></p>
     <p><code>attr.param.sched_priority = 15;</code></p>
     <empty-line/>
     <p><code>// И, наконец, создать поток</code></p>
     <p><code>pthread_create(NULL, &amp;attr, new_thread, NULL);</code></p>
     <p>Для того чтобы увидеть, как «выглядит» многопоточная программа, можно запустить из командного интерпретатора команду <code><strong>pidin</strong></code>. Скажем, нашу программу зовут <code><strong>spud</strong></code>. Если мы выполняем <code><strong>pidin</strong></code> один раз до создания программой spud потоков, и еще раз — после того, как <code><strong>spud</strong></code> создала два потока (тогда всего их будет три), то вот как примерно будет выглядеть вывод (я укоротил вывод <code><strong>pidin</strong></code> для демонстрации только того, что относится к <code><strong>spud</strong></code>):</p>
     <p><code># pidin</code></p>
     <p><code>pid   tid name prio STATE Blocked</code></p>
     <p><code>12301   1 spud  10r READY</code></p>
     <empty-line/>
     <p><code># pidin</code></p>
     <p><code>pid  tid name prio STATE Blocked</code></p>
     <p><code>12301  1 spud  10r READY</code></p>
     <p><code>12301  2 spud  10r READY</code></p>
     <p><code>12301  3 spud  10r READY</code></p>
     <p>Вы можете видеть, что процесс <code><strong>spud</strong></code> (идентификатор процесса 12301) имеет три потока (столбец «tid» в таблице). Эти три поток» выполняются с приоритетом 10, с диспетчеризацией карусельного (RR) типа (обозначенной как «r» после цифры 10). Все три процесса находятся в состоянии готовности (READY), т. е. готовы использовать процессор, но в настоящее время не выполняются (поскольку в данный момент выполняется другой поток с более высоким приоритетом).</p>
     <p>Теперь, когда мы знаем все о создании потоков, давайте рассмотрим, как и где мы можем этим воспользоваться.</p>
     <subtitle>Где хороша многопоточность</subtitle>
     <p>Существует два класса задач, где можно было бы эффективно применять многопоточность.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Потоки подобны перегруженным операторам в языке Си++. Поначалу может показаться хорошей идеей перегрузить каждый оператор какой-либо дополнительной интересной функцией, но это сделает программу трудной для восприятия. Аналогичная ситуация с потоками. Вы могли бы создать множество потоков, но это усложнит ваш код и сделает программу малопонятной, а значит, сложной в сопровождении. Разумное же применение потоков, наоборот, внесет в программу дополнительную функциональную ясность.</p>
     </cite>
     <p>Применение потоков хорошо там, где можно выполнять операции параллельно — например, в ряде математических задач (графика, цифровая обработка сигналов, и т.д.). Потоки также прекрасны там, где программа должна выполнять несколько независимых функций, при этом использующих общие данные — например, веб-сервер, который обслуживает несколько клиентов одновременно. Эти два класса задач мы здесь и рассмотрим.</p>
     <subtitle>Потоки в математических операциях</subtitle>
     <p>Предположим, что мы имеем графическую программу, выполняющую алгоритм трассировки луча. Каждая строка растра на экране зависит от содержимого основной базы данных (которая описывает генерируемую картинку). Ключевым моментом здесь является то, что <emphasis>каждая строка растра не зависит от остальных</emphasis>. Это обстоятельство (независимость строк растра) автоматически приводит к программированию данной задачи как многопоточной.</p>
     <p>Ниже приведен однопоточный вариант:</p>
     <p><code>int main (int argc, char **argv) {</code></p>
     <p><code> int x1;</code></p>
     <empty-line/>
     <p><code> ... // Выполнить инициализации</code></p>
     <empty-line/>
     <p><code> for (x1 = 0; x1 &lt; num_x_lines; x1++) {</code></p>
     <p><code>  do_one_line(x1);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> ... // Вывести результат</code></p>
     <empty-line/>
     <p><code>}</code></p>
     <p>Здесь мы видим, что программа итеративно по всем значениям рассчитает необходимые растровые строки.</p>
     <p>В многопроцессорных системах эта программа будет использовать только один из процессоров. Почему? Потому что мы не указали операционной системе выполнять что-либо параллельно. Операционная система не настолько умна, чтобы посмотреть на программу и сказать: «Эй, секундочку! У нас ее 4 процессора, и похоже, что у нас тут несколько независимых потоков управления. Запущу-ка я это на всех 4 процессорах сразу!»</p>
     <p>Так что это дело разработчика (ваше дело!) — сообщить QNX/Neutrino, какие разделы программы следует выполнять параллельно. Проще всего это можно было бы сделать так:</p>
     <p><code>int main (int argc, char **argv) {</code></p>
     <p><code> int x1;</code></p>
     <empty-line/>
     <p><code> ... // Выполнить инициализации</code></p>
     <empty-line/>
     <p><code> for (x1 = 0; x1 &lt; num_x_lines; x1++) {</code></p>
     <p><code>  pthread_create(NULL, NULL, do_one_line, (void*)x1);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> ... // Вывести результат</code></p>
     <empty-line/>
     <p><code>}</code></p>
     <p>С таким упрощением связано множество проблем. Первая из них (и самая незначительная) состоит в том, что функцию <emphasis>do_one_line()</emphasis> придется модифицировать так, чтобы она могла в качестве своего аргумента принимать значение типа <code><strong>void*</strong></code> вместо <code><strong>int</strong></code>. Это можно легко исправить с помощью оператора приведения типа (typecast).</p>
     <p>Вторая проблема несколько сложнее. Скажем, что разрешающая способность дисплея, для которой вы рассчитывали картинку, была равна 1280&#215;1024. Нам пришлось бы создать 1280 потоков! В общем-то, для QNX/Neutrino это не проблема — QNX/Neutrino позволяет создавать до 32767 потоков в одном процессе! Однако, каждый поток должен иметь свой уникальный стек. Если ваш стек имеет разумный размер (скажем 8 Кб), эта программа израсходует под стек 1280&#215;8 Кб (10 мегабайт!) ОЗУ. И ради чего? В вашей системе есть только 4 процессора. Это означает, что только 4 из этих 1280 потоков будут работать одновременно, а другие 1276 потоков будут ожидать доступа к процессору. (В действительности, в данном случае пространство под стек будет выделяться только по мере необходимости. Но тем не менее, это все равно расходование ресурсов впустую — есть ведь еще и другие издержки.)</p>
     <p>Более красивым способом решения этой задачи было бы разбить ее на 4 части (по одной подзадаче на каждый процессор), и обрабатывать каждую часть как отдельный поток:</p>
     <p><code>int num_lines_per_cpu;</code></p>
     <p><code>int num_cpus;</code></p>
     <empty-line/>
     <p><code>int main (int argc, char **argv) {</code></p>
     <p><code> int cpu;</code></p>
     <empty-line/>
     <p><code> ... // Выполнить инициализации</code></p>
     <empty-line/>
     <p><code> // Получить число процессоров</code></p>
     <p><code> num_cpus = _syspage_ptr-&gt;num_cpu;</code></p>
     <p><code> num_lines_per_cpu = num_x_lines / num_cpus;</code></p>
     <p><code> for (cpu = 0; cpu &lt; num_cpus; cpu++) {</code></p>
     <p><code>  pthread_create(NULL, NULL, do_one_batch, (void*)cpu);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <empty-line/>
     <p><code> ... // Вывести результат</code></p>
     <empty-line/>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>void* do_one_batch(void *c) {</code></p>
     <p><code> int cpu = (int)c;</code></p>
     <p><code> int x1;</code></p>
     <p><code> for (x1 = 0; x1 &lt; num_lines_per_cpu; x1++) {</code></p>
     <p><code>  do_line_line(x1 + cpu * num_lines_per_cpu);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>Здесь мы запускаем только <emphasis>num_cpus</emphasis> потоков. Каждый поток будет выполняться на отдельном процессоре. А поскольку мы имеем дело с небольшим числом потоков, мы тем самым не засоряем память ненужными стеками. Обратите внимание, что мы получили число процессоров путем разыменования глобальной переменной — указателя на системную страницу <emphasis>_syspage_ptr</emphasis>. (Дополнительную информацию относительно системной страницы можно найти в книге «Building Embedded Systems» (поставляется в комплекте документации по QNX/ Neutrino — <emphasis>прим. ред.</emphasis>) или в заголовочном файле <code><strong>&lt;sys/syspage.h&gt;</strong></code>).</p>
     <subtitle>Программирование для одного или нескольких процессоров</subtitle>
     <p>Последняя программа в первую очередь интересна тем, что будет корректно функционировать в системе с одиночным процессором тоже. Просто будет создан только один поток, который и выполнит всю работу. Дополнительные издержки (один стек) с лихвой окупаются гибкостью программы, умеющей работать быстрее в многопроцессорной системе.</p>
     <subtitle>Синхронизация по отношению к моменту завершения потока</subtitle>
     <p>Я уже упоминал, что с приведенным выше упрощенным примером программы связана масса проблем. Так вот, еще одна связанная с ним проблема состоит в том, что функция <emphasis>main()</emphasis> сначала запускает целый букет потоков, а затем отображает результаты. Но как функция узнает, когда уже можно выводить результаты?</p>
     <p>Заставлять <emphasis>main()</emphasis> заниматься опросом, закончены ли вычисления, противоречит самому замыслу ОС реального времени.</p>
     <p><code>int main (int argc, char **argv) {</code></p>
     <p><code> ...</code></p>
     <p><code> // Запустить потоки, как раньше</code></p>
     <p><code> while (num_lines_completed &lt; num_x_lines) {</code></p>
     <p><code>  sleep(1);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>He вздумайте писать такие программы!</p>
     <p>Для решения этой задачи существуют два изящных решения: применение функций <emphasis>pthread_join()</emphasis> и <emphasis>barrier_wait()</emphasis>.</p>
     <subtitle>«Присоединение» (joining)</subtitle>
     <p>Самый простой метод синхронизации — это «присоединение» потоков. Реально это действие означает ожидание завершения.</p>
     <p>Присоединение выполняется одним потоком, ждущим завершения другого потока. Ждущий поток вызывает <emphasis>pthread_join()</emphasis>:</p>
     <p><code>#include &lt;pthread.h&gt;</code></p>
     <empty-line/>
     <p><code>int pthread_join(pthread_t thread, void **value_ptr);</code></p>
     <p>Функции <emphasis>pthread_join()</emphasis> передается идентификатор потока, к которому вы желаете присоединиться, а также необязательный аргумент <emphasis>value_ptr</emphasis>, который может быть использован для сохранения возвращаемого присоединяемым потоком значения (Вы можете передать вместо этого параметра NULL, если это значение для вас не представляет интереса — в данном примере мы так и сделаем).</p>
     <p>Где нам брать идентификатор потока? Мы игнорировали его в функции <emphasis>pthread_create()</emphasis>, передав NULL в качестве первого параметра. Давайте исправим нашу программу:</p>
     <p><code>int num_lines_per_cpu;</code></p>
     <p><code>int num_cpus;</code></p>
     <empty-line/>
     <p><code>int main(int argc, char **argv) {</code></p>
     <p><code> int cpu;</code></p>
     <p><code> pthread_t *thread_ids;</code></p>
     <empty-line/>
     <p><code> ... // Выполнить инициализации</code></p>
     <empty-line/>
     <p><code> thread_ids = malloc(sizeof(pthread_t) * num_cpus);</code></p>
     <p><code> num_lines_per_cpu = num_x_lines / num_cpus;</code></p>
     <p><code> for (cpu = 0; cpu &lt; num_cpus; cpu++) {</code></p>
     <p><code>  pthread_create(</code></p>
     <p><code>   &amp;thread_ids[cpu], NULL, do_one_batch, (void*)cpu);</code></p>
     <p><code> }</code></p>
     <p><code> // Синхронизироваться с завершением всех потоков</code></p>
     <p><code> for (cpu = 0; cpu &lt; num_cpus; cpu++) {</code></p>
     <p><code>  pthread_join(thread_ids[cpu], NULL);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> ... // Вывести результат</code></p>
     <empty-line/>
     <p><code>}</code></p>
     <p>Обратите внимание, что на этот раз мы передали функции <emphasis>pthread_create()</emphasis> в качестве первого аргумента указатель на <code><strong>pthread_t</strong></code>. Там и будет сохранен идентификатор вновь созданного потока. После того как первый цикл <code><strong>for</strong></code> завершится, у нас будет num_cpu работающих потоков, плюс поток, выполняющий <emphasis>main()</emphasis>. Потребление ресурсов процессора потоком <emphasis>main()</emphasis> нас мало интересует — этот поток потратит все свое время на ожидание.</p>
     <p>Ожидание достигается применением функции <emphasis>pthread_join()</emphasis> к каждому из наших потоков. Сначала мы ждем завершения потока <code><strong>thread_ids[0]</strong></code>. Когда он завершится, функция <emphasis>pthread_join()</emphasis> разблокируется. Следующая итерация цикла <code><strong>for</strong></code> заставит нас ждать завершения потока <code><strong>thread_ids[1]</strong></code>, и так далее для всех <emphasis>num_cpus</emphasis> потоков.</p>
     <p>В этот момент возникает законный вопрос: «А что если потоки завершат работу в обратном порядке?» Другими словами, если имеются 4 процессора, и по какой-либо причине поток, выполняющийся на последнем процессоре (с номером 3), завершит работу первым, затем завершится поток, выполняющийся на процессоре с номером 2, и так далее? Вся прелесть приведенной схемы заключается в том, что ничего плохого не произойдет.</p>
     <p>Первое, что произойдет — это то, что <emphasis>pthread_join()</emphasis> блокируется на <code><strong>thread_ids[0]</strong></code>. Тем временем пусть завершится поток <code><strong>thread_ids[3]</strong></code>. Это не окажет абсолютно никакого воздействия на поток <emphasis>main()</emphasis>, который будет по-прежнему ждать завершения первого потока. Затем, пусть завершит работу поток <code><strong>thread_ids[2]</strong></code>. По-прежнему, никаких последствий. И так далее — пока не завершит работу поток <code><strong>thread_ids[0]</strong></code>.</p>
     <p>В этот момент <emphasis>pthread_join()</emphasis> разблокируется, и мы немедленно переходим к следующей итерации цикла <code><strong>for</strong></code>. Вторая итерация цикла for применит <emphasis>pthread_join()</emphasis> к потоку <code><strong>thread_ids[1]</strong></code>, который не будет блокирован, и итерация завершится немедленно. Почему? Потому что поток, идентифицированный как <code><strong>thread_ids[1]</strong></code>, уже завершился. Поэтому наш цикл for просто «проскочит» остальные потоки и завершится. В этот момент мы будем знать, что вычислительные потоки синхронизированы, и теперь мы можем выводить результаты отображение.</p>
     <subtitle>Применение барьера</subtitle>
     <p>Когда мы говорили о синхронизации функции <emphasis>main()</emphasis> по моменту завершения рабочих потоков (в параграфе «Синхронизация по отношению к моменту завершения потока», см. выше), мы упомянули два метода синхронизации: один метод с применением функции <emphasis>pthread_join()</emphasis>, который мы только что рассмотрели, и метод с применением барьера.</p>
     <p>Возвращаясь к нашей аналогии с процессами в жилом доме, предположим, что семья пожелала где-нибудь отдохнуть на природе. Водитель садится в микроавтобус и запускает двигатель. И ждет. Водитель будет ждать до тех пор, пока <emphasis>все</emphasis> члены семьи не сядут в машину, и только затем можно будет ехать — не можем же мы кого-нибудь оставить!</p>
     <p>Точно так происходит и в нашем примере с выводом графики на дисплей. Основной поток должен дождаться того момента, когда все рабочие потоки завершат работу, и только затем можно начинать следующую часть программы.</p>
     <p>Однако, отметьте для себя одну важную отличительную особенность. С применением функции <emphasis>pthread_join()</emphasis> мы ожидаем завершения потоков. Это означает, что на момент ее разблокирования потоков нет больше с нами; они закончили работу и завершились.</p>
     <p>В случае с барьером, мы ждем «встречи» определенного числа потоков у барьера. Затем, когда заданное число потоков достигнуто, мы их всех разблокируем (заметьте, что потоки при этом продолжат выполнять свою работу).</p>
     <p>Сначала барьер следует создать при помощи функции <emphasis>barrier_init()</emphasis>:</p>
     <p><code>#include &lt;sync.h&gt;</code></p>
     <empty-line/>
     <p><code>int barrier_init(barrier_t *barrier, const barrier_attr_t *attr, int count);</code></p>
     <p>Эта функция создает объект типа «барьер» по переданному ей адресу (указатель на барьер хранится в параметре barrier) и назначает ему атрибуты, которые определены в <emphasis>attr</emphasis> (мы будем использовать NULL, чтобы установить значения по умолчанию). Число потоков, которые должны вызывать функцию <emphasis>barrier_wait()</emphasis>, передается в параметре <emphasis>count</emphasis>.</p>
     <p>После того как барьер создан, каждый из потоков должен будет вызвать функцию <emphasis>barrier_wait()</emphasis>, чтобы сообщить, что он отработал:</p>
     <p><code>#include &lt;sync.h&gt;</code></p>
     <empty-line/>
     <p><code>int barrier_wait(barrier_t *barrier);</code></p>
     <p>После того как поток вызвал <emphasis>barrier_wait()</emphasis>, он будет блокирован до тех пор, пока число потоков, указанное первоначально в параметре count функции <emphasis>barrier_init()</emphasis>, не вызовет функцию <emphasis>barrier_wait()</emphasis> (они также будут блокированы). После того как нужное число потоков выполнит вызов функции <emphasis>barrier_wait()</emphasis>, все эти потоки будут разблокированы «одновременно».</p>
     <p>Вот пример:</p>
     <p><code>/*</code></p>
     <p><code>* barrier1.c</code></p>
     <p><code>*/</code></p>
     <empty-line/>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;time.h&gt;</code></p>
     <p><code>#include &lt;sync.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>barrier_t barrier; // Объект типа «барьер»</code></p>
     <empty-line/>
     <p><code>void* thread1(void *not_used) {</code></p>
     <p><code> time_t now;</code></p>
     <p><code> char buf[27];</code></p>
     <p><code> time(&amp;now);</code></p>
     <p><code> printf("Поток 1, время старта %s", ctime_r(&amp;now, buf));</code></p>
     <empty-line/>
     <p><code> // Выполнить вычисления</code></p>
     <p><code> // (вместо этого просто сделаем sleep)</code></p>
     <p><code> sleep(20);</code></p>
     <empty-line/>
     <p><code> barrier_wait(&amp;barrier);</code></p>
     <empty-line/>
     <p><code> // После этого момента все потоки уже завершатся</code></p>
     <p><code> time(&amp;now);</code></p>
     <p><code> printf("Барьер в потоке 1, время срабатывания %s",</code></p>
     <p><code>  ctime_r(&amp;now, buf));</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>void* thread2(void *not_used) {</code></p>
     <p><code> time_t now;</code></p>
     <p><code> char buf[27];</code></p>
     <p><code> time(&amp;now);</code></p>
     <p><code> printf("Поток 2, время старта %s", ctime_r(&amp;now, buf));</code></p>
     <empty-line/>
     <p><code> // Выполнить вычисления</code></p>
     <p><code> // (вместо этого просто сделаем sleep)</code></p>
     <p><code> sleep(40);</code></p>
     <empty-line/>
     <p><code> barrier_wait(&amp;barrier);</code></p>
     <empty-line/>
     <p><code> // После этого момента все потоки уже завершатся</code></p>
     <p><code> time(&amp;now);</code></p>
     <p><code> printf("Барьер в потоке 2, время срабатывания %s",</code></p>
     <p><code>  ctime_r(&amp;now, buf));</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>main() // Игнорировать аргументы</code></p>
     <p><code>{</code></p>
     <p><code> time_t now;</code></p>
     <p><code> char buf[27];</code></p>
     <empty-line/>
     <p><code> // Создать барьер со значением счетчика 3</code></p>
     <p><code> barrier_init(&amp;barrier, NULL, 3);</code></p>
     <p><code> // Создать два потока, thread1 и thread2</code></p>
     <p><code> pthread_create(NULL, NULL, thread1, NULL);</code></p>
     <p><code> pthread_create(NULL, NULL, thread2, NULL);</code></p>
     <empty-line/>
     <p><code> // Сейчас выполняются оба потока</code></p>
     <p><code> // Ждать завершения</code></p>
     <p><code> time(&amp;now);</code></p>
     <p><code> printf("main(): ожидание у барьера, время %s",</code></p>
     <p><code>  ctime_r(&amp;now, buf));</code></p>
     <empty-line/>
     <p><code> barrier_wait(&amp;barrier);</code></p>
     <empty-line/>
     <p><code> // После этого момента все потоки уже завершатся</code></p>
     <p><code> time(&amp;now);</code></p>
     <p><code> printf("Барьер в main(), время срабатывания %s",</code></p>
     <p><code>  ctime_r(&amp;now, buf));</code></p>
     <p><code>}</code></p>
     <p>Основной поток создал объект типа «барьер» и инициализировал его значением счетчика, равным числу потоков (включая себя!), которые должны «встретиться» у барьера, прежде чем он «прорвется». В нашем примере этот индекс был равен 3 — один для потока <emphasis>main()</emphasis>, один для потока <emphasis>thread1()</emphasis> и один для потока <emphasis>thread2()</emphasis>. Затем, как и прежде, стартуют потоки вычисления графики (в нашем случае это потоки <emphasis>thread1()</emphasis> и <emphasis>thread2()</emphasis>). Для примера вместо приведения реальных алгоритмов графических вычислений мы просто временно «усыпили» потоки, указав в них <code><strong>sleep(20)</strong></code> и <code><strong>sleep(40)</strong></code>, чтобы имитировать вычисления. Для осуществления синхронизации основной поток (<emphasis>main()</emphasis>) просто блокирует сам себя на барьере, зная, что барьер будет разблокирован только после того, как рабочие потоки аналогично присоединятся к нему.</p>
     <p>Как упоминалось ранее, с функцией <emphasis>pthread_join()</emphasis> рабочие потоки для синхронизации главного потока с ними должны умереть. В случае же с барьером потоки живут и чувствуют себя вполне хорошо. Фактически, отработав, они просто разблокируются по функции <emphasis>barrier_wait()</emphasis>. Тонкость здесь в том, что вы обязаны предусмотреть, что эти потоки должны делать дальше! В нашем примере с графикой мы не дали им никакого задания для них — просто потому что мы так придумали алгоритм. В реальной жизни вы могли бы захотеть, например, продолжить вычисления.</p>
     <subtitle>Несколько потоков при одиночном процессоре</subtitle>
     <p>Предположим, что мы слегка изменили наш пример так, чтобы можно было проиллюстрировать, почему иногда хорошо иметь несколько потоков даже в системе с одиночным процессором.</p>
     <p>В таком модифицированном примере один узел на сети ответственен за вычисление строк растра (как и в примере с графикой, рассмотренном выше). Однако, когда строка рассчитана, ее данные должны быть отправлены по сети другому узлу, который выполняет функцию отображения. Ниже приведена соответствующая модифицированная функция <emphasis>main()</emphasis> (на основе первоначального примера без потоков):</p>
     <p><code>int main(int argc, char **argv) {</code></p>
     <p><code> int x1;</code></p>
     <empty-line/>
     <p><code> ... // выполнить инициализации</code></p>
     <empty-line/>
     <p><code> for (x1 = 0; x1 &lt; num_x_lines; x1++) {</code></p>
     <p><code>  do _one_line(x1); // Область «С» на схеме</code></p>
     <p><code>  tx_one_line_wait_ack(x1); // Области «X» и «W» на схеме</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>Обратите внимание на то, что мы исключили отображающую часть программы и вместо этого добавили функцию <emphasis>tx_one_line_wait_ack()</emphasis>. Далее предположим, что мы имеем дело с достаточно медленной сетью, но процессор в действительности не занимается передачей данных — он просто отдает их некоторым аппаратным средствам, которые уже сами позаботятся об их передаче. Функция <emphasis>tx_one_line_wait_ack()</emphasis> потребует немного процессорного времени на то, чтобы обеспечить передачу данных аппаратным средствам, и после этого, пока не получит подтверждения о получении данных от удаленного узла, не будет потреблять процессорное время вообще.</p>
     <p>Ниже представлена диаграмма, иллюстрирующая загрузку процессора в данном случае (графические вычисления на ней обозначены как «С», передача — как «X», а ожидание подтверждения — как «W»).</p>
     <image l:href="#img_7.png"/>
     <p>Последовательное выполнение, один процессор.</p>
     <p>Минуточку! Мы тратим впустую драгоценные секунды, ожидая, пока аппаратура сделает свое дело!</p>
     <p>Если мы сделали бы это в многопоточном варианте, мы смогли бы добиться более эффективного использования процессора, так?</p>
     <image l:href="#img_8.png"/>
     <p>Многопоточное выполнение, один процессор</p>
     <p>Это уже намного лучше, потому что теперь, даже при том, второй поток затрачивает немного времени на ожидание, мы добились уменьшения суммарного времени вычислений.</p>
     <p>Если бы в нашем примере тратилось T<sub>compute</sub> единиц времени на вычисления, T<sub>tx</sub> — на передачу и T<sub>wait</sub> — на ожидание аппарату средств, тогда для первого случая в нашем примере общие затраты времени на обработку были бы равны:</p>
     <p>(T<sub>compute</sub> + T<sub>tx</sub> + T<sub>wait</sub>) &#8729; <emphasis>num_x_lines</emphasis>,</p>
     <p>тогда как затраты времени при использовании двух потоков были бы равны:</p>
     <p>(T<sub>compute</sub> + T<sub>tx</sub>) &#8729; <emphasis>num_x_lines</emphasis> + T<sub>wait</sub>,</p>
     <p>что меньше на величину:</p>
     <p>T<sub>wait</sub> &#8729; (<emphasis>num_x_lines</emphasis> – 1),</p>
     <p>в предположении, конечно, что T<sub>wait</sub> &#8804; T<sub>compute</sub>.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Отметим, что мы изначально будем ограничены интервалом времени, равным:</p>
      <p>T<sub>compute</sub> + T<sub>tx</sub> &#8729; <emphasis>num_x_lines</emphasis>,</p>
      <p>потому что мы должны будем завершить по меньшей мере одно полное вычисление, а также еще и передать данные. Иными словами, мы можем использовать многопоточность для распараллеливания вычислений, но аппаратный ресурс для передачи данных у нас все равно есть только один.</p>
     </cite>
     <p>А если бы мы разработали вариант системы с четырьмя потоками и выполнили это в SMP-системе с четырьмя процессорами, это выглядело бы примерно так:</p>
     <image l:href="#img_9.png"/>
     <p>Четыре потока, четыре процессора.</p>
     <p>Обратите внимание, насколько каждый из этих четырех центральных процессоров недоиспользован (см. незаштрихованные прямоугольники в строках «Загрузка»). На представленном выше рисунке имеются две интересные зоны. Когда все четыре потока стартуют одновременно, все они вычисляются. К сожалению, когда потоки заканчивают вычисления, они начинают конкурировать за право обладания аппаратными средствами передачи данных (зоны «X» на диаграмме смещены одна относительно другой, поскольку, имея только один передающий ресурс, можно вести только одну передачу одновременно). Это дает нам небольшую аномалию на начальном этапе. После того как потоки отработали этот этап, они оказываются естественным образом синхронизированы по отношению к работе аппаратных средств, так как время передачи данных намного меньше, чем &#188; времени вычислительного цикла. Если игнорировать эту небольшую аномалию в работе системы на начальном этапе, значения временных интервалов в данной системе можно оценить по формуле:</p>
     <p>(T<sub>compute</sub> + T<sub>tx</sub> + T<sub>wait</sub>) &#8729; <emphasis>num_x_lines</emphasis> / <emphasis>num_cpus</emphasis></p>
     <p>Из этой формулы следует, что применение четырех потоков на четырех процессорах обеспечивает сокращение затрат времени приблизительно в 4 раза по сравнению с аналогичным временем в модели с единственным потоком, т.е. по сравнению с данным! примера, с которого мы начали обсуждение этой проблемы.</p>
     <p>Суммируя все то, что мы узнали из анализа примера с использованием многопоточного варианта с одиночным процессором, в идеале мы желали бы иметь больше потоков, чем процессоров, чтобы дополнительные потоки могли «подобрать» время простоя процессоров, которое естественным образом возникает из интервалов ожидания подтверждения (а также из интервалов ожидания, связанных с конкуренцией за передатчик) В этом случае у нас бы получилось примерно вот что: (см. рис. «Восемь потоков, четыре процессора»).</p>
     <image l:href="#img_10.png"/>
     <p>Восемь потоков, четыре процессора.</p>
     <p>На этом рисунке предполагается следующее:</p>
     <p>• потоки 5, 6, 7 и 8 привязаны к процессорам 1, 2, 3, и 4 (для упрощения);</p>
     <p>• передача данных выполняется с более высоким приоритетов чем вычислительные операции;</p>
     <p>• прервать передачу нельзя.</p>
     <p>Из диаграммы видно, что хоть мы теперь и имеем в два раза больше потоков, чем процессоров, мы по-прежнему сталкиваемся с временными интервалами, в течение которых процессоры «недоиспользованы». На рисунке показаны три таких интервала времени. Эти интервалы обозначены числами, соответствующими номеру процессора, и указаны на временных диаграммах загрузки процессоров в строках «Загрузка»:</p>
     <p>1. Поток 1 ожидает подтверждения (состояние «W»), при этом поток 5 завершил вычисления и ждет доступности передатчика.</p>
     <p>2. Потоки 2 и 6 ожидают подтверждения.</p>
     <p>3. Поток 3 ожидает подтверждения, при этом поток 7 завершил вычисления и ждет доступности передатчика.</p>
     <p>Этот пример для нас — важный урок. Бессмысленно просто увеличивать количество процессоров в надежде, что все ваши дела пойдут быстрее, поскольку имеются также и ограничивающие факторы. В некоторых случаях эти ограничивающие факторы определяются просто конструкцией материнской платы мультипроцессорной системы, то есть структурой подсистемы разрешения конфликтов за устройства в память, когда несколько процессоров пытаются обратиться по одному и тому же адресу. В нашем случае обратите внимание, что строка «Использование порта передачи данных» стала все больше заполняться. Если бы мы просто увеличили число процессоров, то в конечном счете столкнулись бы с проблемами, связанными с тем, что соответствующие потоки простаивали бы в ожидании передатчика.</p>
     <p>В любом случае, используя потоки-«мусорщики» для сбора неиспользованных ресурсов процессоров, мы сможем обеспечить намного более эффективное использование процессоров. Это время приближенно оценивается по формуле:</p>
     <p>(T<sub>compute</sub> + T<sub>tx</sub> + T<sub>wait</sub>) &#8729; <emphasis>num_x_lines</emphasis> / <emphasis>num_cpus</emphasis></p>
     <p>При выполнении только вычислений мы ограничены только количеством процессоров; ни один процессор не будет простаивать в ожидании подтверждения. Впрочем, это был бы идеальный случай. Как вы видели из диаграммы, реально периодически возникают временные интервалы, когда один процессор простаивает. Также, как отмечалось ранее, мы в любом случае ограничены по скорости значением:</p>
     <p>T<sub>compute</sub> + T<sub>tx</sub> &#8729; <emphasis>num_x_lines</emphasis>.</p>
     <subtitle>На что обратить внимание при использовании симметричного мультипроцессора (SMP)</subtitle>
     <p>При том, что в общем случае вы можете запросто «игнорировать», работаете вы с SMP-архитектурой или с одиночным процессором, есть ряд обстоятельств, которые определенно добавят вам головной боли. К сожалению, это могут быть такие маловероятные события, которые могут проявиться не на этапе разработки, а на этапе его испытаний, в демонстрационных версиях или даже, что самое неприятное, на стадии эксплуатации. Так вот, следование ряду принципов «защитного программирования» избавит вас от связанной с этими проблемами нервотрепки.</p>
     <p>Вот краткий перечень того, что следует четко помнить, имея дело с SMP-системой:</p>
     <p>• Потоки действительно могут работать и работают параллельно — ни в коем случае не доверяйте при их синхронизации таким механизмам как диспетчеризация FIFO или система приоритетов.</p>
     <p>• Потоки могут также выполняться одновременно с обработчиками прерываний (ISR) — это означает, что вам нужно будет не только защитить поток от обработчика прерываний, но и наоборот — обработчик прерываний от потока. Подробнее об этом см. в главе 4, «Прерывания».</p>
     <p>• Некоторые операции, которые по вашему мнению должны быть атомарными, в действительности таковыми не являются — это зависит от операции и от процессора. Отметим из такого списка операции типа «чтение- модификация-запись» (например, <code><strong>++</strong></code>, <code><strong>--</strong></code>, <code><strong>&amp;=</strong></code>, т.д.). См. файл <code><strong>&lt;atomic.h&gt;</strong></code> для анализа возможных замен. (Заметьте, что это не проблема SMP в чистом виде; код для вышеупомянутых операции может выполняться не как атомарный на большинстве RISC-процессоров).</p>
     <subtitle>Потоки в независимых ситуациях</subtitle>
     <p>Ранее в разделе «Где хороша многопоточность» говорилось о том, что потокам также находят применение там, где имеет место обработка информации по множеству независимых алгоритмов с разделяемыми структурами данных. При этом, строго говоря, вы могли бы использовать несколько <emphasis>процессов</emphasis> (с одним потоком каждый), явно разделяющих данные, но в некоторых случаях вместо этого гораздо удобнее использовать один многопоточный процесс. Давайте рассмотрим, почему и где здесь можно использовать потоки.</p>
     <p>В наших примерах будем отталкиваться от стандартной модели «ввод-обработка-вывод». В наиболее общем случае одна часть этой модели ответственна за получение откуда-либо входных данных, другая часть — за обработку этих данных и преобразование их в некоторые выходные данные (или управляющие воздействия), третья часть — за отправку полученных выходных данных куда надо.</p>
     <subtitle>Несколько процессов</subtitle>
     <p>Давайте, во-первых, осмыслим, что мы будем иметь в случае нескольких однопоточных процессов. Для нашей модели у нас было бы три процесса — процесс «ввода», процесс «обработки» и процесс «вывода»:</p>
     <image l:href="#img_11.png"/>
     <p>Система 1: Несколько операций, несколько процессов.</p>
     <p>В таком виде наша модель в высшей степени абстрактна, но и в такой же степени «слабо связана». Процесс «ввода» не имеет никакой реальной связи ни с процессом «обработки», ни с процессом «вывода» — он просто отвечает за сбор входных данных и передачу их как-нибудь на следующий этап («этап обработки»).</p>
     <p>Мы могли бы сказать то же самое о процессах «обработки» и «вывода» — они также не имеют никакой реальной связи друг с другом. Также здесь предполагается, что обмен данными («ввод — обработка» и «обработка — вывод») осуществляется по некоторому стандартному протоколу (например, через программные каналы, очереди сообщений POSIX, обмен сообщениями QNX/Neutrino — что угодно).</p>
     <subtitle>Несколько процессов с разделяемой памятью</subtitle>
     <p>В зависимости от объема потока данных, мы можем пожелать оптимизировать характер связей. Самый простой путь состоит в том, чтобы связать три процесса «теснее». Попробуем теперь вместо использования универсального протокола соединения выбрать схему с разделяемой памятью (на диаграмме толстые стрелки указывают потоки данных; тонкие стрелки — потоки управления):</p>
     <image l:href="#img_12.png"/>
     <p>Система 2: Несколько операций, буферы разделяемой памяти между процессами.</p>
     <p>В данной схеме мы «<emphasis>подтянули</emphasis>» связь так, чтобы в результате обеспечить более быстрый и более эффективный обмен данными. В то же время, мы здесь по-прежнему можем применять универсальный протокол для передачи «управляющей» информации, поскольку предполагается, что по сравнению с потоком данных ее не так много.</p>
     <subtitle>Несколько потоков</subtitle>
     <p>Система с наиболее тесными связями представлена на следующей схеме:</p>
     <image l:href="#img_13.png"/>
     <p>Система 3: Несколько операций, несколько потоков.</p>
     <p>Здесь мы наблюдаем один процесс с тремя потоками. Все три потока неявно разделяют области данных. Обмен управляющей информацией может быть реализован аналогично предыдущим примерам или с помощью ряда примитивов синхронизации потоков (мы уже имели дело с мутексами, барьерами и семафорами — скоро рассмотрим и другие).</p>
     <subtitle>Сравнение</subtitle>
     <p>Давайте теперь сравним эти три метода по ряду критериев и взвесим все «за» и «против».</p>
     <p>В системе 1 связь была самой слабой. Это имеет то преимущество, что каждый из трех процессов может быть легко (то есть при помощи командной строки, в противоположность перекомпиляции/переработке) заменен другим модулем. Это следует из самой природы модели, потому что «единицей модульности» здесь является сам функциональный модуль. Система 1 является также единственной, которая из всех трех может быть распределена по узлам сети QNX/Neutrino. Поскольку информационные связи здесь абстрагированы до некоторого универсального протокола, очевидно, что эти три процесса могут быть выполнены на любой машине в сети. Это может быть очень мощным фактором масштабируемости в Вашем проекте — вам может понадобиться расширить свою сеть до сотен узлов, либо разделенных географически, либо как-то иначе — например, для совместимости с другими аппаратными средствами.</p>
     <p>Однако, как только мы переходим к применению разделяемой памяти, мы теряем способность распределять модули по сети. QNX/Neutrino не поддерживает распределенные объекты разделяемой памяти. Таким образом, в Системе 2 мы реально ограничили себя выполнением всех трех процессов на одной и той же машине. Мы не потеряли способность легкой замены или исключения модулей, потому что модули все еще представляют собой отдельные процессы, управляемые командной строкой. Но мы добавили ограничение, в соответствии с которым все заменяемые компоненты должны соответствовать модели с разделяемой памятью.</p>
     <p>В системе 3 мы теряем все отмеченные ранее проектные возможности. Мы определенно не можем выполнять различные потоки одного процесса на различных узлах (хотя при этом мы можем выполнять их на различных процессорах в SMP-системе). Также мы потеряли наши возможности переконфигурации — теперь нам обязательно понадобится механизм явного доопределения, который из алгоритмов «ввода», «обработки» и «вывода» мы должны использовать (эту проблему можно решить с помощью разделяемых объектов, также известных как динамические библиотеки — DLL).</p>
     <p>Так почему же я должен проектировать свою систему, используя многопоточность, как в Системе 3? Почему бы мне для обеспечения максимальной универсальности не выбрать Систему 1?</p>
     <p>Ну, даже при том, что Система 3 является наиболее ригидной, она, скорее всего, окажется самой быстродействующей. В ней не будет переключений контекста между потоками в различных процессах, мне не придется настраивать разделяемую память, а также применять абстрактные методы синхронизации типа программных каналов, очередей сообщений POSIX или обмен сообщениями QNX/Neutrino для обеспечения доставки данных или управляющей информации — я смогу использовать базовые примитивы синхронизации потоков на уровне ядра. Другим преимуществом является то, что при запуске системы, состоящей из одного процесса (с тремя потоками), я могу быть уверен, что все, что мне понадобится далее, уже загружено с носителя (то есть потом не выяснится что-то типа «Опа! А нужного-то драйвера на диске и нету...») И, наконец, Система 3 также, скорее всего, будет наиболее компактной, потому что не придется использовать три отдельных копии информации, характерной для процессов (например, дескрипторы файлов).</p>
     <p>Мораль: знайте, какое решение сулит какие выгоды и какие потери, и применяйте то, что будет оптимальным для вашего конкретного проекта.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Дополнительно о синхронизации</p>
    </title>
    <section>
     <p>Мы уже обсудили:</p>
     <p>• мутексы;</p>
     <p>• семафоры;</p>
     <p>• барьеры.</p>
     <p>Давайте теперь завершим нашу дискуссию о синхронизации, обсудив следующее:</p>
     <p>• блокировки чтения/записи (reader/writer locks);</p>
     <p>• ждущие блокировки (sleepons);</p>
     <p>• условные переменные (condition variables);</p>
     <p>• дополнительные сервисы QNX/QNX/Neutrino.</p>
    </section>
    <section>
     <title>
      <p>Блокировки чтения/записи</p>
     </title>
     <p>Блокировки чтения/записи применяются точно в соответствии с их названием: несколько «читателей» могут использовать ресурс в отсутствие «писателей», или один «писатель» может использовать ресурс в отсутствие «читателей» и других «писателей».</p>
     <p>Эта ситуация возникает достаточно часто для того, чтобы создать отдельный примитив синхронизации специально для этих целей.</p>
     <p>У вас будет часто возникать ситуация разделения структуры данных группой потоков. Очевидно, что в любой момент времени только один поток может записывать данные в эту структуру. Если бы запись велась более чем одним потоком одновременно, одни потоки могли бы записать свои данные поверх данных других потоков. Для предотвращения таких ситуаций поток-«писатель» должен эксклюзивно получить блокировку чтения/записи («rwlock»), обозначив этим, что он и только он имеет доступ к структуре данных. Заметьте, что это исключительное право доступа «строго контролируется на добровольных началах» — обеспечение того, чтобы все потоки, которые пользуются указанной областью данных, синхронизировались с использованием блокировок чтения/ записи, зависит только от вас.</p>
     <p>С «читателями» ситуация противоположная. Поскольку считывание области данных — неразрушающая операция, любое число потоков может считывать данные (даже если ту же часть данных в этот момент считывает другой поток). Сложным моментом здесь является то, что никто не должен производить запись в область данных, из которой в этот момент ведется чтение. В противном случае, считывающие потоки могут быть «введены в заблуждение» — например, поток мог бы считать часть данных, затем быть вытесненным потоком-«писателем» затем возобновиться и продолжить считывание данных, но уже обновленных! Это может закончиться нарушением целостности данных.</p>
     <p>Давайте рассмотрим вызовы, которые вы могли бы использовать при применении блокировок чтения/записи.</p>
     <p>Первые два вызова используются для инициализации внутренних областей памяти для rwlock-блокировок (чтения/записи):</p>
     <p><code>int pthread_rwlock_init(pthread_rwlock_t *<emphasis>lock</emphasis>,</code></p>
     <p><code> const pthread_rwlockattr_t *<emphasis>attr</emphasis>);</code></p>
     <empty-line/>
     <p><code>int pthread_rwlock_destroy(pthread_rwlock_t *<emphasis>lock</emphasis>);</code></p>
     <p>Функция <emphasis>pthread_rwlock_init()</emphasis> принимает аргумент lock (типа <code><strong>pthread_rwlock_t</strong></code>) и инициализирует его атрибутами, указанными в параметре attr. В нашем примере мы применим атрибут NULL, что будет означать «применить значения по умолчанию». Более подробно об этом см. документацию на функции:</p>
     <p><emphasis>pthread_rwlockattr_init()</emphasis>;</p>
     <p><emphasis>pthread_rwlockattr_destroy()</emphasis>;</p>
     <p><emphasis>pthread_rwlockattr_getpshared()</emphasis>;</p>
     <p><emphasis>pthread_rwlockattr_setpshared()</emphasis>.</p>
     <p>Когда мы закончим свои дела с блокировкой чтения/записи, её следует уничтожить функцией <emphasis>pthread_rwlock_destroy()</emphasis>.</p>
     <p>Никогда не используйте блокировку, которая либо уже уничтожена, либо еще не инициализирована.</p>
     <p>Далее, мы должны выбрать блокировку подходящего типа. Как отмечалось выше, в основном применяются два режима блокировки: «читателю» желательно иметь «неэксклюзивный» доступ, а для «писателю» — «эксклюзивный». Для упрощения имен, функции названы по именам своих пользователей:</p>
     <p><code>int pthread_rwlock_rdlock(pthread_rwlock_t *<emphasis>lock</emphasis>);</code></p>
     <p><code>int pthread_rwlock_tryrdlock(pthread_rwlock_t *<emphasis>lock</emphasis>);</code></p>
     <p><code>int pthread_rwlock_wrlock(pthread_rwlock_t* <emphasis>lock</emphasis>);</code></p>
     <p><code>int pthread_rwlock_trywrlock(pthread_rwlock_t *<emphasis>lock</emphasis>);</code></p>
     <p>Существует четыре функции блокировки, а не две, как вы могли бы предположить. Очевидно, «предполагаемыми» функциями были <emphasis>pthread_rwlock_rdlock()</emphasis> и <emphasis>pthread_rwlock_wrlock()</emphasis>, используемые «читателями» и «писателями», соответственно.</p>
     <p>Это — собственно блокирующие вызовы: если блокировка для выбранной операции недоступна, поток будет блокирован. Когда блокировка становится доступной в соответствующем режиме, поток будет разблокирован, из чего он сможет предположить, что теперь можно спокойно обращаться к защищенному блокировкой ресурсу.</p>
     <p>Иногда, тем не менее, поток может не захотеть блокироваться, желая вместо этого просто узнать, доступна ли нужная блокировка. Для этого и существуют версии функций, содержащие в имени «try» («проверка»). Важно отметить, что «проверочные» версии получат блокировку, если она доступна, но если нет, тогда они не будут блокированы, а только возвратят код ошибки. Причина, по которой они должны получать блокировку, если она доступна, очень проста. Предположим, что поток хочет получить блокировку на чтение, но не хочет ждать, если блокировка окажется недоступной. Поток вызывает функцию <emphasis>pthread_rwlock_tryrdlock()</emphasis>, и оказывается, что блокировка доступна. Если бы функция <emphasis>pthread_rwlock_tryrdlock()</emphasis> не захватывала доступную блокировку немедленно, могли бы произойти неприятные вещи — наш поток мог бы быть, к примеру, вытеснен другим потоком, а тот, в свою очередь, мог бы блокировать нужный нам ресурс. Поскольку первому потоку фактически не была предоставлена блокировка, после возобновления ему придется вызывать <emphasis>pthread_rwlock_rdlock()</emphasis>, и вот теперь он будет заблокирован, поскольку ресурс более недоступен. Иными словами, в такой ситуации даже поток, не желающий блокироваться и поэтому вызывающий «проверочную» версию, по-прежнему может быть заблокирован!</p>
     <p>Наконец, независимо от того, как блокировка нами применялась, нам необходим способ ее освобождения:</p>
     <p><code>int pthread_rwlock_unlock(pthread_rwlock_t* <emphasis>lock</emphasis>);</code></p>
     <p>После того как поток выполнил нужную операцию с ресурсом, он освобождает блокировку, вызывая функцию <emphasis>pthread_rwlock_unlock()</emphasis>. Если блокировка теперь становится доступной в режиме, который запрошен и ожидается другим потоком, то этот ждущий поток будет переведен в состояние готовности (READY).</p>
     <p>Отметим, что мы не смогли бы реализовать такую форму синхронизации только с помощью мутекса. Мутекс рассчитан только на один поток, что было бы хорошо в случае записи (чтобы только один поток мог использовать ресурс в определенный момент времени), но оплошал бы в случае считывания, потому что не допустил бы к ресурсу более чем одного «читателя». Семафор также был бы бесполезен, потому что нельзя было бы отличить два режима доступа — применение семафора могло бы обеспечить доступ нескольких «читателей», но если бы семафором попытался завладеть «писатель», его вызов ничем бы не отличался от вызова «читателей», что вызвало бы некрасивую ситуацию с множеством «читателей» и множеством же «писателей»!</p>
    </section>
    <section>
     <title>
      <p>Ждущие блокировки</p>
     </title>
     <p>Другая типовая ситуация в многопоточных программах — это потребность заставить поток «ждать чего-либо». Этим «чем- либо» может являться фактически что угодно! Например, когда доступны данные от устройства, или когда конвейерная лента находится в нужной позиции, или когда данные сохранены на диск, и т.д. Еще одна хитрость этой ситуации состоит в том, что одного и того же события могут ожидать несколько потоков.</p>
     <p>Для таких целей мы могли бы использовать либо условную переменную (condition variable), о которой речь ниже, либо, что гораздо проще, ждущую блокировку (sleepon).</p>
     <p>Для применения ждущих блокировок надо выполнить несколько операций. Рассмотрим сначала вызовы, а затем вернемся к использованию ждущих блокировок.</p>
     <p><code>int pthread_sleepon_lock(void);</code></p>
     <empty-line/>
     <p><code>int pthread_sleepon_unlock(void);</code></p>
     <empty-line/>
     <p><code>int pthread_sleepon_broadcast(void *addr);</code></p>
     <empty-line/>
     <p><code>int pthread_sleepon_signal(void *addr);</code></p>
     <empty-line/>
     <p><code>int pthread_sleepon_wait(void *addr);</code></p>
     <cite>
      <p><image l:href="#img_1.png"/>He дайте префиксу <emphasis>pthread_</emphasis> себя обмануть. Эти функции не предусмотрены стандартами POSIX.</p>
     </cite>
     <p>Как было отмечено ранее, потоку может быть необходимо ждать какого-нибудь события. Наиболее очевидный выбор из представленного выше списка функций — это функция <emphasis>pthread_sleepon_wait()</emphasis>. Но сначала поток должен проверить, надо ли ждать. Давайте приведем пример. Один поток представляет собой поток-«поставщик», который получает данные от неких аппаратных средств. Другой поток — поток-«потребитель» и он неким образом обрабатывает поступающие данные. Рассмотрим сначала поток-«потребитель»:</p>
     <p><code>volatile int data_ready = 0;</code></p>
     <empty-line/>
     <p><code>consumer() {</code></p>
     <p><code> while (1) {</code></p>
     <p><code>  while (!data_ready) {</code></p>
     <p><code>   // wait</code></p>
     <p><code>  }</code></p>
     <p><code>  // Обработать данные</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>«Потребитель» вечно находится в своем главном обрабатывающем цикле (<code><strong>while(1)</strong></code>). Первое, что он проверяет — это флаг <emphasis>data_ready</emphasis>. Если этот флаг равен 0, это означает, что данных нет, и их надо ждать. Впоследствии поток-«производитель» должен будет как-то «разбудить» его, и тогда поток-«потребитель» должен будет повторно проверить состояние флага <emphasis>data_ready</emphasis>. Положим, что происходит именно это. Поток-«потребитель» анализирует состояние флага и определяет, что флаг равен 1, то есть данные теперь доступны. Поток-«потребитель» переходит к обработке поступивших данных, после чего он должен снова проверить, не поступили ли новые данные, и так далее.</p>
     <p>Здесь мы можем столкнуться с новой проблемой. Как «потребителю» сбрасывать флаг <emphasis>data_ready</emphasis> согласованно с «производителем»? Очевидно, нам понадобится некоторая форма монопольного доступа к флагу, чтобы в любой момент времени только один из этих потоков мог модифицировать его. Метод, который применен в данном случае, заключается в применения мутекса, но это внутренний мутекс библиотеки ждущих блокировок, так что мы сможем обращаться к нему только с помощью двух функций: <emphasis>pthread_sleepon_lock()</emphasis> и <emphasis>pthread_sleepon_unlock()</emphasis>. Давайте модифицируем наш поток-«потребитель»:</p>
     <p><code>consumer() {</code></p>
     <p><code> while (1) {</code></p>
     <p><code>  pthread_sleepon_lock();</code></p>
     <p><code>  while (!data_ready) {</code></p>
     <p><code>   // WAIT</code></p>
     <p><code>  }</code></p>
     <p><code>  // Обработать данные</code></p>
     <p><code>  data_ready = 0;</code></p>
     <p><code>  pthread_sleepon_unlock();</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>Здесь мы добавили «потребителю» установку и снятие блокировки. Это означает, что потребитель может теперь <emphasis>надежно</emphasis> проверять флаг <emphasis>data_ready</emphasis>, не опасаясь гонок, а также надежно его устанавливать.</p>
     <p>Великолепно! А как насчет собственно процесса ожидания? Как мы и предполагали ранее, там действительно применяется вызов функции <emphasis>pthread_sleepon_wait()</emphasis>. Вот второй while-цикл:</p>
     <p><code>while (!data_ready) {</code></p>
     <p><code> pthread_sleepon_wait(&amp;data_ready);</code></p>
     <p><code>}</code></p>
     <p>Функция <emphasis>pthread_sleepon_wait()</emphasis> в действительности выполняет три действия:</p>
     <p>1. Разблокирует мутекс библиотеки ждущих блокировок.</p>
     <p>2. Выполняет собственно операцию ожидания.</p>
     <p>3. Снова блокирует мутекс библиотеки ждущих блокировок.</p>
     <p>Причина обязательной разблокировки/блокировки мутекса библиотеки проста: поскольку суть мутекса состоит в обеспечении взаимного исключения доступа к флагу <emphasis>data_ready</emphasis>, мы хотим запретить потоку-«производителю» изменять флаг <emphasis>data_ready</emphasis>, пока мы его проверяем. Но если мы не разблокируем флаг впоследствии, то поток-«производитель» не сможет его установить, чтобы сообщить нам о доступности данных! Операция повторной блокировки выполняется автоматически исключительно для удобства, чтобы вызвавший функцию <emphasis>pthread_sleepon_wait()</emphasis> поток не беспокоился о состоянии блокировки после «пробуждения».</p>
     <p>Давайте перейдем теперь к потоку-«производителю» и рассмотрим, как он использует библиотеку ждущих блокировок. Вот его полная реализация:</p>
     <p><code>producer() {</code></p>
     <p><code> while (1) {</code></p>
     <p><code>  // Ждать прерывания от оборудования...</code></p>
     <p><code>  pthread_sleepon_lock();</code></p>
     <p><code>  data_ready = 1;</code></p>
     <p><code>  pthread_sleepon_signal(&amp;data_ready);</code></p>
     <p><code>  pthread_sleepon_unlock();</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>Как вы видите, поток-«производитель» также блокирует мутекс, чтобы получить монопольный доступ к флагу <emphasis>data_ready</emphasis> перед его установкой.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Клиента «пробуждает» не установка флага <emphasis>data_ready</emphasis> в единицу (1), а вызов функции <emphasis>pthread_sleepon_signal()</emphasis>!</p>
     </cite>
     <p>Давайте рассмотрим происходящее в подробностях. Определим состояния «потребителя» и «производителя» следующим образом:</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Состояние</th>
       <th align="left" valign="top">Означает</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">CONDVAR</td>
       <td align="left" valign="top">ожидание соответствующей ждущей блокировке условной переменной</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">MUTEX</td>
       <td align="left" valign="top">ожидание мутекса</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">состояние готовности, т.е., готов выполняться или уже выполняется</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">INTERRUPT</td>
       <td align="left" valign="top">ожидание прерывания от аппаратных средств</td>
      </tr>
     </table>
     <table>
      <tr align="left">
       <th align="left" valign="top">Действие</th>
       <th align="left" valign="top">Владелец мутекса</th>
       <th align="left" valign="top">Состояние «потребителя»</th>
       <th align="left" valign="top">Состояние «производителя»</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«потребитель» блокирует мутекс</td>
       <td align="left" valign="top">«потребитель»</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«потребитель» проверяет флаг <emphasis>data_ready</emphasis></td>
       <td align="left" valign="top">«потребитель»</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">потребитель вызывает функцию <emphasis>pthread_sleepon_wait()</emphasis></td>
       <td align="left" valign="top">«потребитель»</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">функция <emphasis>pthread_sleepon_wait()</emphasis> разблокирует мутекс</td>
       <td align="left" valign="top">мутекс свободен</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">функция <emphasis>pthread_sleepon_wait()</emphasis> блокируется</td>
       <td align="left" valign="top">мутекс свободен</td>
       <td align="left" valign="top">CONDVAR</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">пауза до прерывания</td>
       <td align="left" valign="top">мутекс свободен</td>
       <td align="left" valign="top">CONDVAR</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">аппаратные средства генерируют данные</td>
       <td align="left" valign="top">мутекс свободен</td>
       <td align="left" valign="top">CONDVAR</td>
       <td align="left" valign="top">READY</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«производитель» блокирует мутекс</td>
       <td align="left" valign="top">«производитель»</td>
       <td align="left" valign="top">CONDVAR</td>
       <td align="left" valign="top">READY</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«производитель» устанавливает флаг <emphasis>data_ready</emphasis></td>
       <td align="left" valign="top">«производитель»</td>
       <td align="left" valign="top">CONDVAR</td>
       <td align="left" valign="top">READY</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«производитель» вызывает <emphasis>pthread_sleepon_signal()</emphasis></td>
       <td align="left" valign="top">«производитель»</td>
       <td align="left" valign="top">CONDVAR</td>
       <td align="left" valign="top">READY</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«потребитель» «пробуждается», функция <emphasis>pthread_sleepon_wait()</emphasis> пытается заблокировать мутекс</td>
       <td align="left" valign="top">«производитель»</td>
       <td align="left" valign="top">MUTEX</td>
       <td align="left" valign="top">READY</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«производитель» разблокирует мутекс</td>
       <td align="left" valign="top">мутекс свободен</td>
       <td align="left" valign="top">MUTEX</td>
       <td align="left" valign="top">READY</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«потребитель» получает мутекс</td>
       <td align="left" valign="top">«потребитель»</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">READY</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«потребитель» обрабатывает данные</td>
       <td align="left" valign="top">«потребитель»</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">READY</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«производитель» ждет новых данных от аппаратуры</td>
       <td align="left" valign="top">«потребитель»</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">пауза («потребитель» обрабатывает полученные данные)</td>
       <td align="left" valign="top">«потребитель»</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«потребитель» завершает обработку и разблокирует мутекс</td>
       <td align="left" valign="top">мутекс свободен</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">«потребитель» возвращается в начало цикла и блокирует мутекс</td>
       <td align="left" valign="top">«потребитель»</td>
       <td align="left" valign="top">READY</td>
       <td align="left" valign="top">INTERRUPT</td>
      </tr>
     </table>
     <p>Последняя строка в таблице повторяет первую — мы совершили один полный цикл.</p>
     <p>Каково назначение флага <emphasis>data_ready</emphasis>? Он служит для двух целей:</p>
     <p>• Он является флагом состояния — посредником между «потребителем» и «производителем», указывающим на состояние системы. Если флаг установлен в состояние 1, это означает, что данные доступны для обработки; если этот флаг установлено в состояние 0, это означает, что данных нет, и поток-потребитель должен быть заблокирован.</p>
     <p>• Он выполняет функцию «места, где происходит синхронизация со ждущей блокировкой». Более формально говоря, адрес переменной <emphasis>data_ready</emphasis> используется как уникальный идентификатор объекта, по которому осуществляется ждущая блокировка. Мы запросто могли бы применить «<code><strong>(void*)12345</strong></code>» вместо «<code><strong>&amp;data_ready</strong></code>» — библиотеке ждущих блокировок все равно, что это за идентификатор, лишь бы он был уникален и корректно использовался. Использование же в качестве идентификатора адреса переменной есть надежный способ сгенерировать уникальный номер, поскольку не бывает же двух переменных с одинаковым адресом!</p>
     <p>• К обсуждению различий между функциями <emphasis>pthread_sleepon_signal()</emphasis> и <emphasis>pthread_sleepon_broadcast()</emphasis> мы еще вернемся в разговоре об условных переменных.</p>
    </section>
    <section>
     <title>
      <p>Условные переменные</p>
     </title>
     <p>Условные переменные (или «condvars») очень похожи на ждущие блокировки, которые мы рассматривали выше. В действительности, ждущие блокировки — это надстройка над механизмом условных переменных, и именно поэтому в таблице, иллюстрировавшей использование ждущих блокировок, у нас встречалось состояние CONDVAR. Функция <emphasis>pthread_cond_wait()</emphasis> точно так же освобождает мутекс, ждет, а затем повторно блокирует мутекс, аналогично функции <emphasis>pthread_sleepon_wait()</emphasis>.</p>
     <p>Давайте опустим вступление и обратимся к нашему примеру о «производителе» и «потребителе» из раздела о ждущих блокировках, но вместо ждущих блокировок будем использовать условные переменные. А затем уже обсудим вызовы.</p>
     <p><code>/*</code></p>
     <p><code> * cp1.c</code></p>
     <p><code>*/</code></p>
     <empty-line/>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;pthread.h&gt;</code></p>
     <empty-line/>
     <p><code>int data_ready = 0;</code></p>
     <p><code>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;</code></p>
     <p><code>pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;</code></p>
     <empty-line/>
     <p><code>void* consumer(void *notused){</code></p>
     <p><code> printf("Это поток-потребитель...\n");</code></p>
     <p><code> while (1) {</code></p>
     <p><code>  pthread_mutex_lock(&amp;mutex);</code></p>
     <p><code>  while (!data_ready) {</code></p>
     <p><code>   pthread_cond_wait(&amp;condvar, &amp;mutex);</code></p>
     <p><code>  }</code></p>
     <p><code>  // Обработать данные</code></p>
     <p><code>  printf("Потребитель: получил данные от производителя\n");</code></p>
     <p><code>  data_ready = 0;</code></p>
     <p><code>  pthread_cond_signal(&amp;condvar);</code></p>
     <p><code>  pthread_mutex_unlock(&amp;mutex);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>void* producer (void *notused) {</code></p>
     <p><code> printf("Это поток-производитель...\n");</code></p>
     <p><code> while (1) {</code></p>
     <p><code>  // Получить данные от оборудования</code></p>
     <p><code>  // (мы имитируем это при помощи sleep(1))</code></p>
     <p><code>  sleep(1);</code></p>
     <p><code>  printf("Производитель: получил данные от h/w\n");</code></p>
     <p><code>  pthread_mutex_lock(&amp;mutex);</code></p>
     <p><code>  while (data_ready) {</code></p>
     <p><code>   pthread_cond_wait(&amp;condvar, &amp;mutex);</code></p>
     <p><code>  }</code></p>
     <p><code>  data_ready = 1;</code></p>
     <p><code>  pthread_cond_signal(&amp;condvar);</code></p>
     <p><code>  pthread_mutex_unlock(&amp;mutex);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>main() {</code></p>
     <p><code> printf(</code></p>
     <p><code>  "Начало примера с производителем и потребителем...\n");</code></p>
     <p><code> // Создать поток-производитель и поток-потребитель</code></p>
     <p><code> pthread_create(NULL, NULL, producer, NULL);</code></p>
     <p><code> pthread_create(NULL, NULL, consumer, NULL);</code></p>
     <p><code> // Дать потокам немного повыполняться</code></p>
     <p><code> sleep(20);</code></p>
     <p><code>}</code></p>
     <p>Этот пример в значительной степени похож на программу с применением ждущей блокировки, с небольшими отличиями (мы добавили несколько вызовов <emphasis>printf()</emphasis>, а также функцию <emphasis>main()</emphasis>, чтобы программа могла работать!) Первое отличие, которое бросается в глаза, — здесь использован новый тип данных, <code><strong>pthread_cond_t</strong></code>. Это просто декларация для условной переменной; мы назвали нашу условную переменную <emphasis>condvar</emphasis>.</p>
     <p>Следующее, что видно из примера, — это то, что структура «потребителя» идентична таковой в предыдущем примере с ждущей блокировкой. Мы заменили функции <emphasis>pthread_sleepon_lock()</emphasis> и <emphasis>pthread_sleepon_unlock()</emphasis> на стандартные мутекс-ориентированные версии (<emphasis>pthread_mutex_lock()</emphasis> и <emphasis>pthread_mutex_unlock()</emphasis>). Функция <emphasis>pthread_sleepon_wait()</emphasis> была заменена на функцию <emphasis>pthread_cond_wait()</emphasis>.</p>
     <p>Основное различие здесь состоит в том, что библиотека ждущих блокировок имеет скрытый внутренний мутекс, а при использовании условных переменных мутекс передается явно. Последний способ дает нам больше гибкости.</p>
     <p>И, наконец, обратите внимание на то, что мы использовали функцию <emphasis>pthread_cond_signal()</emphasis> вместо функции <emphasis>pthread_sleepon_signal()</emphasis> (опять же, с явной передачей мутекса).</p>
     <subtitle>Функции <emphasis>phtread*_signal()</emphasis> и <emphasis>pthread*_broadcast()</emphasis></subtitle>
     <p>В разделе о ждущих блокировках мы обещали обсудить различие между функциями <emphasis>pthread_sleepon_broadcast()</emphasis> и <emphasis>pthread_sleepon_signal()</emphasis>. Заодно поговорим и о различии между двумя аналогичными функциями, имеющими отношение к условным переменным: <emphasis>pthread_cond_signal()</emphasis> и <emphasis>pthread_cond_broadcast()</emphasis>.</p>
     <p>В двух словах, функция в варианте «signal» разблокирует только один поток. Например, если бы несколько потоков находилось в ожидании по функции «wait», и некий поток вызвал бы функцию <emphasis>pthread*_signal()</emphasis>, то был бы разблокирован только один из ждущих потоков. Который из них? Тот, у которого наивысший приоритет. Если имеется два или более потоков с одинаковым приоритетом, порядок «пробуждения» будет не определен. Применение же варианта <emphasis>pthread*_broadcast()</emphasis> приведет к тому что будут разблокированы все ожидающие потоки.</p>
     <p>Разблокировать все потоки может показаться излишним. Но с другой стороны, разблокировать только один (причем случайный поток тоже не совсем корректно.</p>
     <p>Поэтому мы должны думать, где имеет смысл использовать какой вариант. Очевидно, что если у вас только один ждущий поток, как у нас и было во всех вариантах «потребителя», функция <emphasis>pthread*_signal()</emphasis> прекрасно справится — будет разблокирован один поток, и как раз тот, который нужно (потому что других просто нет).</p>
     <p>В ситуации с несколькими потоками в первую очередь следует выяснить: а почему они ждут? Обычно на этот вопрос есть два ответа:</p>
     <p>• все потоки рассматриваются как эквивалентные и реально образуют пул доступных потоков, готовых к обработке некоторого запроса;</p>
     <p>• все потоки являются уникальными, и каждый из них ждет соблюдения своего специфического условия.</p>
     <p>В первом случае мы можем представить себе, что код всех потоков имеет примерно следующий вид:</p>
     <p><code>/*</code></p>
     <p><code> * cv1.c</code></p>
     <p><code>*/</code></p>
     <empty-line/>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;pthread.h&gt;</code></p>
     <empty-line/>
     <p><code>pthread_mutex_t mutex_data = PTHREAD_MUTEX_INITIALIZER;</code></p>
     <p><code>pthread_cond_t cv_data = PTHREAD_COND_INITIALIZER;</code></p>
     <p><code>int data;</code></p>
     <empty-line/>
     <p><code>thread1() {</code></p>
     <p><code> for (;;) {</code></p>
     <p><code>  pthread_mutex_lock(&amp;mutex_data);</code></p>
     <p><code>  while (data == 0) {</code></p>
     <p><code>   pthread_cond_wait(&amp;cv_data, &amp;mutex_data);</code></p>
     <p><code>  }</code></p>
     <p><code>  // Сделать что-нибудь</code></p>
     <p><code>  pthread_mutex_unlock(&amp;mutex_data);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>В этом случае абсолютно неважно, который именно из потоков получит данные — главное, чтобы хотя бы один сделал это и произвел над этими данными необходимые действия.</p>
     <p>Однако, если ваш код подобен приведенному ниже, все будет несколько по-иному:</p>
     <p><code>/*</code></p>
     <p><code> * cv2.c</code></p>
     <p><code>*/</code></p>
     <empty-line/>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;pthread.h&gt;</code></p>
     <empty-line/>
     <p><code>pthread_mutex_t mutex_xy = PTHREAD_MUTEX_INITIALIZER;</code></p>
     <p><code>pthread_cond_t cv_xy = PTHREAD_COND_INITIALIZER;</code></p>
     <p><code>int x, y;</code></p>
     <p><code>int isprime(int);</code></p>
     <empty-line/>
     <p><code>thread1() {</code></p>
     <p><code> for (;;) {</code></p>
     <p><code>  pthread_mutex_lock(&amp;mutex_xy);</code></p>
     <p><code>  while ((x &gt; 7) &amp;&amp; (y != 15)) {</code></p>
     <p><code>   pthread_cond_wait(&amp;cv_xy, &amp;mutex_xy);</code></p>
     <p><code>  }</code></p>
     <p><code>  // Сделать что-нибудь</code></p>
     <p><code>  pthread_mutex_unlock(&amp;mutex_xy);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>thread2() {</code></p>
     <p><code> for (;;) {</code></p>
     <p><code>  pthread_mutex_lock(&amp;mutex_xy);</code></p>
     <p><code>  while (!isprime(x)) {</code></p>
     <p><code>   pthread_cond_wait(&amp;cv_xy, &amp;mutex_xy);</code></p>
     <p><code>  }</code></p>
     <p><code>  // Сделать что-нибудь</code></p>
     <p><code>  pthread_mutex_unlock(&amp;smutex_xy);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>thread3() {</code></p>
     <p><code> for (;;) {</code></p>
     <p><code>  pthread_mutex_lock(&amp;mutex_xy);</code></p>
     <p><code>  while (x != y) {</code></p>
     <p><code>   pthread_cond_wait(&amp;cv_xy, &amp;mutex_xy);</code></p>
     <p><code>  }</code></p>
     <p><code>  // Сделать что-нибудь</code></p>
     <p><code>  pthread_mutex_unlock(&amp;mutex_xy);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>В этом случае пробуждение одного потока ничего не даст! Здесь мы обязаны «разбудить» все три потока, чтобы каждый из них проверил соблюдение своего условия.</p>
     <p>Это в полной мере отражает второй вариант ответа на наш вопрос «а почему они ждут?» Так как все потоки все ждут соблюдения различных условий (поток <emphasis>thread1()</emphasis> ждет, пока значение x не станет меньше или равно 7, или пока значение у не станет равным 15, поток <emphasis>thread2()</emphasis> ждет, пока значение x не станет простым числом, а поток <emphasis>thread3()</emphasis> ждет, пока x не станет равным у), у нас нет никакого выбора, кроме как «разбудить» все потоки «одновременно».</p>
     <subtitle>Ждущие блокировки в сравнении с условными переменными</subtitle>
     <p>Ждущие блокировки имеют одно основное преимущество в сравнении с условными переменными. Предположим, что вам надо синхронизировать множество объектов. Используя условные переменные, вы бы ассоциировали с каждым объектом отдельную условную переменную — если бы у вас было <emphasis>M</emphasis> объектов, вы, скорее всего, определили бы <emphasis>M</emphasis> условных переменных. При применении же ждущих блокировок соответствующие им условные переменные создаются динамически по мере постановки потоков на ожидание, поэтому в этом случае на <emphasis>M</emphasis> объектов и <emphasis>N</emphasis> блокированных потоков у вас было бы максимум <emphasis>N</emphasis>, а не <emphasis>M</emphasis> условных переменных.</p>
     <p>Однако, условные переменные более универсальны, чем ждущие блокировки, и вот почему:</p>
     <p>1. Ждущие блокировки в любом случае основаны на условных переменных.</p>
     <p>2. Мутексы ждущих блокировок скрыты в библиотеке; условные переменные позволяют вам задавать его явно.</p>
     <p>Первый пункт сам по себе достаточно убедителен. :-) Второй, однако, имеет еще и практический смысл. Когда мутекс скрыт в библиотеке, это означает, что он может быть только один на процесс, независимо от числа потоков в этом процессе или от количества переменных. Это может быть сильно ограничивающим фактором, особенно если принять во внимание, что вам придется использовать один-единственный мутекс для синхронизации доступа всех имеющихся потоков в процессе ко всем нужным им переменным!</p>
     <p>Намного лучшая схема состоит в применении нескольких мутексов — по одному на каждый набор данных — и явно сопоставлять им условные переменные по мере необходимости. Как мощь, так и опасность этого подхода заключаются в том, что ни на этапе компиляции, ни на этапе выполнения не будет производиться никаких проверок, и вам придется самим следить за:</p>
     <p>• блокировкой мутексов перед доступом к соответствующим переменным;</p>
     <p>• применением правильного мутекса для каждой переменной;</p>
     <p>• применением правильной условной переменной для соответствующих мутекса и переменной (данных).</p>
     <p>Самый простой путь решения этих проблем — грамотно проектировать и тщательно проверять, а также заимствовать приемы объектно-ориентированного программирования (например, встраивать мутексы в структуры данных, создавать для обращения к структурам данных специализированные подпрограммы, и т.д.). Разумеется, то, в какой степени вы примените первый, второй, или оба варианта, будет зависеть не только от вашего стиля программирования, но и от требований производительности.</p>
     <p>Ключевыми моментами при использовании условных переменных являются:</p>
     <p>1. Мутексы следует использовать для проверки и изменения переменных.</p>
     <p>2. Условные переменные следует использовать в качестве «точки встречи».</p>
     <p>Ниже представлена иллюстрация этого:</p>
     <image l:href="#img_14.png"/>
     <p>Связь мутексов и условных переменных по схеме «один к одному»</p>
     <p>Одно интересное замечание. Поскольку никаких проверок не выполняется, вы можете, например, связать один набор переменных с мутексом «MutexABC», другой — с мутексом «MutexDEF», и сопоставить <emphasis>обоим</emphasis> наборам переменных одну и ту же условную переменную «CondvarABCDEF»:</p>
     <empty-line/>
     <image l:href="#img_15.png"/>
     <p>Связь мутексов и условных переменных по схеме «один ко многим».</p>
     <p>Это весьма полезное свойство. Поскольку мутекс должен использоваться для «проверки и изменения» всегда, это подразумевает, что я должен буду выбрать правильный мутекс всякий раз, когда мне понадобится доступ к некоей переменной. Вполне логично — если я, скажем, проверяю переменную «С», то, очевидно, мне потребуется заблокировать мутекс «MutexABC». А что если я хочу изменить переменную «E»? Хорошо, перед этим я должен буду захватить мутекс «MutexDEF». Затем я ее изменяю и сообщаю об этом другим потокам через условную переменную «CondvarABCDEF», после чего освобождаю мутекс.</p>
     <p>А теперь смотрите, что происходит. Толпа потоков, ждавших на условии «CondvarABCDEF», вдруг резко «просыпается» (по функции <emphasis>pthread_cond_wait()</emphasis>). Их функции ожидания немедленно пытаются повторно захватить мутекс. Критическим моментом здесь является то, что мутексов два. (В зависимости от того, изменения какой переменной поток ждал, его функция ожидания попытается захватить либо MutexABC, либо MutexDEF — <emphasis>прим. ред</emphasis>.) Это означает, что в SMP-системе возникли бы две <emphasis>конкурирующие очереди</emphasis> потоков, и в каждой потоки будут проверять как бы независимые переменные, используя при этом независимые мутексы. Круто, да?</p>
    </section>
    <section>
     <title>
      <p>Дополнительные сервисы QNX/Neutrino</p>
     </title>
     <p>QNX/Neutrino позволяет делать еще ряд изящных вещей. POSIX утверждает, что с мутексом должны работать потоки одного и того же процесса, но позволяет в соответствующей реализации эту концепцию расширять. В QNX/Neutrino это расширение сводится к тому, что мутекс может использоваться потоками <emphasis>различных процессов</emphasis>. Чтобы понять, почему это работает, вспомните: то, что мы рассматриваем как «операционную систему», реально состоит из двух частей — ядра, которое занимается диспетчеризацией, и администратора процессов, который, наряду со всем остальным, заботится о защите памяти и «процессах». Мутекс — всего-навсего объект синхронизации потоков. Поскольку ядро работает только с потоками, то реально ему все равно, какие потоки работают в каких процессах, это уже забота администратора.</p>
     <p>Итак, если вы установили область разделяемой памяти между двумя процессами и разместили в ней мутекс, ничто не мешает вам с его помощью синхронизировать потоки в двух (или более!) процессах — функции <emphasis>pthread_mutex_lock()</emphasis> и <emphasis>pthread_mutex_unlock()</emphasis> будут работать точно так же.</p>
    </section>
    <section>
     <title>
      <p>Пулы потоков</p>
     </title>
     <p>Другое существенное дополнение в QNX/Neutrino — это понятие пула потоков. Вы будете часто обращать внимание в ваших программах на то обстоятельство, что вам хотелось бы иметь несколько потоков и управлять их поведением в определенных пределах. Например, для сервера вы можете решить, что первоначально в ожидании сообщения от клиента должен быть блокирован только один поток. Когда этот поток получит сообщение и пойдет обслуживать запрос, вы можете принять решение о том, что хорошо было бы создать другой поток и блокировать его в ожидании на случай поступления другого запроса — тогда этот запрос будет кому обработать. И так далее. Через некоторое время, когда все запросы будут обслужены, у вас может оказаться большое число потоков, бездействующих в ожидании. Чтобы не расходовать ресурсы впустую, вам, возможно, захочется уничтожить некоторые из этих «лишних» потоков.</p>
     <p>Подобные операции в жизни — обычное дело, и для задач такого рода QNX/Neutrino предоставляет для этого специальную библиотеку.</p>
     <cite>
      <p><image l:href="#img_1.png"/>В более ранних (до 2.00) версиях QNX/Neutrino была предусмотрена подобная функциональность, но она была скрыта в библиотеке администратора ресурсов. В версии 2.00 эти функции были вынесены из библиотеки администратора ресурсов в отдельную библиотеку. Мы еще вернемся к функциям работы с пупами потоков в главе «Администраторы ресурсов».</p>
     </cite>
     <p>В рамках данного обсуждения важно понять, что следует различать два режима потоков в пулах:</p>
     <p>• режим блокирования;</p>
     <p>• режим обработки.</p>
     <p>В режиме блокирования поток обычно вообще не использует ресурсы процессора. В типовом сервере это соответствует ситуации, когда поток ждет сообщения. Противоположностью этого режима является режим обработки, в котором поток может как использовать, так и не использовать ресурсы процессора — это зависит от структуры процесса. Чуть позже мы рассмотрим функции работы с пулами потоков, и вы увидите, что они дают возможность управлять количеством как блокированных, так и обрабатывающих потоков.</p>
     <p>Для работы с пулами потоков в QNX/Neutrino предусмотрены следующие функции:</p>
     <p><code>#include &lt;sys/dispatch.h&gt;</code></p>
     <empty-line/>
     <p><code>thread_pool_t *thread_pool_create(</code></p>
     <p><code> thread_pool_attr_t *attr, unsigned flags);</code></p>
     <empty-line/>
     <p><code>int thread_pool_destroy(thread_pool_t *pool);</code></p>
     <empty-line/>
     <p><code>int thread_pool_start(void *pool);</code></p>
     <p>Как видно из имен функций, вы в первую очередь создаете пул потоков, используя функцию <emphasis>thread_pool_create()</emphasis>, а затем запускаете этот пул при помощи функции <emphasis>thread_pool_start()</emphasis>. Когда вы закончили свои дела с пулом потоков, вы можете использовать функцию <emphasis>thread_pool_destroy()</emphasis> для его уничтожения. Заметьте, что функция <emphasis>thread_pool_destroy()</emphasis> может вам вообще не понадобиться — например, когда ваша программа суть сервер, который работает «вечно».</p>
     <p>Итак, первая функция, на которую следует обратить внимание — это функция <emphasis>thread_pool_create()</emphasis>. У нее два параметра: <emphasis>attr</emphasis> и <emphasis>flags</emphasis>. Параметр <emphasis>attr</emphasis> — атрибутная запись, которая определяет рабочие параметры пула потоков (см. <code><strong>&lt;sys/dispatch.h&gt;</strong></code>):</p>
     <p><code>typedef struct _thread_pool_attr {</code></p>
     <p><code> // Функции и дескриптор пула потоков</code></p>
     <p><code> THREAD_POOL_HANDLE_T *<emphasis>handle</emphasis>;</code></p>
     <p><code> THREAD_POOL_PARAM_T *(*block_func)</code></p>
     <p><code>  (THREAD_POOL_PARAM_T *ctp);</code></p>
     <p><code> void (*<emphasis>unblock_func</emphasis>)(THREAD_POOL_PARAM_T *ctp);</code></p>
     <p><code> int (*handler_func) (THREAD_POOL_PARAM_T *ctp);</code></p>
     <p><code> THREAD_POOL_PARAM_T *(*<emphasis>context_alloc</emphasis>)</code></p>
     <p><code>  (THREAD_POOL_HANDLE_T *handle);</code></p>
     <p><code> void *(*<emphasis>context_free</emphasis>)(THREAD_POOL_PARAM_T *ctp);</code></p>
     <p><code> // Параметры пула потоков</code></p>
     <p><code> pthread_attr_t *<emphasis>attr</emphasis>;</code></p>
     <p><code> unsigned short <emphasis>lo_water</emphasis>;</code></p>
     <p><code> unsigned short <emphasis>increment</emphasis>;</code></p>
     <p><code> unsigned short <emphasis>hi_water</emphasis>;</code></p>
     <p><code> unsigned short <emphasis>maximum</emphasis>;</code></p>
     <p><code>} thread_pool_attr_t;</code></p>
     <p>Я разбил определение типа <code><strong>thread_pool_attr_t</strong></code> на два раздела, один из которых содержит функции и дескриптор для потоков в пуле, а в другом — рабочие параметры пула.</p>
     <subtitle>Управление числом потоков</subtitle>
     <p>Сначала проанализируем «параметры пула потоков», чтобы понять, как можно управлять числом потоков в пуле и их атрибутами. Имейте в виду, что здесь мы будем говорить о «режиме блокирования» и «режиме обработки» (далее, когда мы будем рассматривать функции исходящих вызовов (callout functions), мы увидим, как эти эти режимы соотносятся).</p>
     <p>Приведенный ниже рисунок иллюстрирует связи между параметрами <emphasis>lo_water, hi_water</emphasis> и <emphasis>maximum</emphasis>.</p>
     <image l:href="#img_16.png"/>
     <p>Жизненный цикл потока в пуле потоков.</p>
     <p>(Заметьте, что как «CA» здесь обозначается функция <emphasis>context_alloc()</emphasis>, как «CF» — функция <emphasis>context_free()</emphasis>, как «режим блокирования» — функция <emphasis>block_func()</emphasis>, а как «режим обработки» — функция <emphasis>handler_func()</emphasis>.</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>attr</emphasis></td>
       <td align="left" valign="top">Это атрибутная запись, которая применяется при создании потока. Мы уже обсуждали эту структуру ранее (в разделе «Атрибутная запись потока»). Вспомните — это та самая структура, которая задает характеристики нового потока: приоритет, размер стека, и т.д.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>lo_water</emphasis></td>
       <td align="left" valign="top">(От «Low watermark», буквально — «нижняя ватерлиния» — <emphasis>прим. ред</emphasis>.) Этот параметр задает минимальное количество потоков, которые должны находиться в режиме блокирования. В типовом сервере это было бы количество потоков, например, ждущих запроса. Если число ждущих потоков меньше, чем значение параметра <emphasis>lo_water</emphasis>, (например, потому что мы только что приняли сообщение, и один из ждущих потоков переключился на его обработку), тогда создается дополнительно еще <emphasis>increment</emphasis> потоков. Это представлено на рисунке в виде первого этапа, обозначенного как «создание потока».</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>increment</emphasis></td>
       <td align="left" valign="top">(Буквально — «приращение» — <emphasis>прим. ред</emphasis>.) Этот параметр определяет, сколько потоков должны быть созданы сразу, если число потоков, находящихся в режиме блокирования, становится меньше значения параметра <emphasis>lo_water</emphasis>. В выборе значения для этого параметра вы бы наиболее вероятно начали со значения 1 (единица). Это означало бы, что если бы число потоков в режиме блокирования стало бы меньше значения параметра <emphasis>lo_water</emphasis>, то пулом потоков был бы создан дополнительно ровно один поток. Для более тонкой настройки параметра <emphasis>increment</emphasis> можно понаблюдать за поведением процесса и определить, может ли этому параметру понадобиться принимать значения, отличные от единицы. Например, если ваш процесс периодически получает «всплески» запросов, то из того, что число потоков, находящихся в режиме блокирования, упало ниже значения <emphasis>lo_water</emphasis>, можно было бы сделать вывод как раз о таком «всплеске» и принять решение о создании более чем одного резервного потока.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>hi_water</emphasis></td>
       <td align="left" valign="top">(От «high watermark», буквально — «верхняя ватерлиния» — прим. ред.) Этот параметр указывает верхний предел числа потоков, которые могут быть в режиме блокирования одновременно. По мере завершения своих операций по обработке данных, потоки обычно будут возвращаться в режим блокирования. Однако, у библиотеки поддержки пулов потоков есть внутренний счетчик числа потоков, находящихся в режиме блокирования, и если его значение превышает значение параметра hi_water, библиотека автоматически уничтожит поток, который вызвал переполнение (то есть тот поток, который только что завершил обработку и намеревался возвратиться в режим блокирования). Это показано на рисунке раздвоением стрелки, исходящей из блока «режим обработки» — одна стрелка ведет к «режиму блокирования», а вторая — к блоку операции «CF» и далее на уничтожение потока. Таким образом, сочетание параметров <emphasis>lo_water</emphasis> и <emphasis>hi_water</emphasis> позволяет вам четко определять диапазон числа потоков, одновременно находящихся в режиме блокирования.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>maximum</emphasis></td>
       <td align="left" valign="top">Параметр указывает на максимальное число потоков, которые вообще могут работать одновременно в результате действий библиотеки поддержки пулов потоков. Например, при создании новых потоков в случае их нехватки (когда число блокированных потоков падает ниже границы <emphasis>lo_water</emphasis>) общее количество потоков было бы ограничено параметром <emphasis>maximum</emphasis>.</td>
      </tr>
     </table>
     <p>Другой ключевой параметр, предназначенный для управления потоками, — это параметр <emphasis>flags</emphasis>, передаваемый функции <emphasis>thread_pool_create()</emphasis>. Он может принимать одно из следующих значений:</p>
     <p>POOL_FLAG_EXIT_SELF</p>
     <p>Не делать возврат из функции <emphasis>thread_pool_start()</emphasis> и не включать вызывающий поток в пул.</p>
     <p>POOL_FLAG_USE_SELF</p>
     <p>Не делать возврат из функции <emphasis>thread_pool_start()</emphasis>, но включить вызывающий поток в пул.</p>
     <p>0</p>
     <p>Функция <emphasis>thread_pool_start()</emphasis> возвратится, новые потоки будут создаваться по мере необходимости.</p>
     <p>Приведенное описание может показаться суховатым. Давайте рассмотрим пример.</p>
     <p>В управляющей структуре пула потоков сконцентрируем наше внимание только на значениях параметров <emphasis>lo_water</emphasis>, <emphasis>increment</emphasis> и <emphasis>maximum</emphasis>:</p>
     <p><code>/*</code></p>
     <p><code> * tp1.с</code></p>
     <p><code> *</code></p>
     <p><code> * Пример с пулами потоков (1)</code></p>
     <p><code> *</code></p>
     <p><code>*/</code></p>
     <empty-line/>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;stdlib.h&gt;</code></p>
     <p><code>#include &lt;errno.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <p><code>#include &lt;sys/dispatch.h&gt;</code></p>
     <empty-line/>
     <p><code>char *progname = "tp1";</code></p>
     <empty-line/>
     <p><code>void tag (char *name) {</code></p>
     <p><code> time_t t;</code></p>
     <p><code> char buffer[BUFSIZ];</code></p>
     <p><code> time(&amp;t);</code></p>
     <p><code> strftime(buffer, BUFSIZ, "%T ", localtime(&amp;t));</code></p>
     <p><code> printf("%s %3d %-20.20s: ", buffer, pthread_self(), name);</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>THREAD_POOL_PARAM_T* blockfunc(</code></p>
     <p><code> THREAD_POOL_PARAM_T *ctp) {</code></p>
     <p><code> tag("blockfunc");</code></p>
     <p><code> printf("ctp %p\n", ctp);</code></p>
     <p><code> tag("blockfunc");</code></p>
     <p><code> printf("sleep (%d);\n", 15 * pthread_self());</code></p>
     <p><code> sleep(pthread_self() * 15);</code></p>
     <p><code> tag("blockfunc");</code></p>
     <p><code> printf("Выполнили sleep\n");</code></p>
     <p><code> tag("blockfunc");</code></p>
     <p><code> printf("Возвращаем 0x%08X\n",</code></p>
     <p><code>  0x10000000 + pthread_self());</code></p>
     <p><code> return((void*)(0x10000000 + pthread_self()));</code></p>
     <p><code> // Передано handlerfunc</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>THREAD_POOL_PARAM_T* contextalloc(</code></p>
     <p><code> THREAD_POOL_HANDLE_T *handle) {</code></p>
     <p><code> tag("contextalloc");</code></p>
     <p><code> printf("handle %p\n", handle);</code></p>
     <p><code> tag("contextalloc");</code></p>
     <p><code> printf("Возвращаем 0x%08X\n",</code></p>
     <p><code>  0x20000000 + pthread_self());</code></p>
     <p><code> return ((void*)(0x20000000 + pthread_self()));</code></p>
     <p><code> // Передано blockfunc</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>void contextfree(THREAD_POOL_PARAM_T *param) {</code></p>
     <p><code> tag("contextfree");</code></p>
     <p><code> printf("param %p\n", param);</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>void unblockfunc(THREAD_POOL_PARAM_T *ctp) {</code></p>
     <p><code> tag("unblockfunc");</code></p>
     <p><code> printf("ctp %p\n", ctp);</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>int handlerfunc(THREAD_POOL_PARAM_T *ctp) {</code></p>
     <p><code> static int i = 0;</code></p>
     <p><code> tag("handlerfunc");</code></p>
     <p><code> printf("ctp %p\n", ctp);</code></p>
     <p><code> if (i++ &gt; 15) {</code></p>
     <p><code>  tag("handlerfunc");</code></p>
     <p><code>  printf("Более 15 операций, возвращаем 0\n");</code></p>
     <p><code>  return (0);</code></p>
     <p><code> }</code></p>
     <p><code> tag("handlerfunc");</code></p>
     <p><code> printf("sleep (%d)\n", pthread_self() * 25);</code></p>
     <p><code> sleep(pthread_self() * 25);</code></p>
     <p><code> tag("handlerfunc");</code></p>
     <p><code> printf("Выполнили sleep\n");</code></p>
     <p><code> tag("handlerfunc");</code></p>
     <p><code> printf("Возвращаем 0x%08X\n",</code></p>
     <p><code>  0x30000000 + pthread_self());</code></p>
     <p><code> return (0x30000000 + pthread_self());</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>main() {</code></p>
     <p><code> thread_pool_attr_t tp_attr;</code></p>
     <p><code> void *tpp;</code></p>
     <p><code> memset(&amp;tp_attr, 0, sizeof(tp_attr));</code></p>
     <p><code> tp_attr.handle = (void*)0x12345678;</code></p>
     <p><code>  // Передано contextalloc</code></p>
     <p><code> tp_attr.block_func = blockfunc;</code></p>
     <p><code> tp_attr.unblock_func = unblockfunc;</code></p>
     <p><code> tp_attr.context_alloc = contextalloc;</code></p>
     <p><code> tp_attr.context_free = contextfree;</code></p>
     <p><code> tp_attr.handler_func = handlerfunc;</code></p>
     <p><code> tp_attr.lo_water = 3;</code></p>
     <p><code> tp_attr.hi_water = 7;</code></p>
     <p><code> tp_attr.increment = 2;</code></p>
     <p><code> tp_attr.maximum = 10;</code></p>
     <p><code> if ((tpp =</code></p>
     <p><code>  thread_pool_create(&amp;tp_attr, POOL_FLAG_USE_SELF)) ==</code></p>
     <p><code>   NULL) {</code></p>
     <p><code>  fprintf(stderr,</code></p>
     <p><code>   "%s: Ошибка thread_pool_create, errno %s\n",</code></p>
     <p><code>   progname, strerror(errno));</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> thread_pool_start(tpp);</code></p>
     <p><code> fprintf(stderr,</code></p>
     <p><code>  "%s: возврат из thread_pool_start; errno %s\n",</code></p>
     <p><code>  progname, strerror(errno));</code></p>
     <p><code> sleep(3000);</code></p>
     <p><code> exit(EXIT_FAILURE);</code></p>
     <p><code>}</code></p>
     <p>После установки параметров мы вызываем функцию <emphasis>thread_pool_create()</emphasis> для создания пула потоков. Эта функция возвращает указатель на управляющую структуру пула потоков (<emphasis>tpp</emphasis>), который мы проверяем на равенство NULL (что указало бы на ошибку). И, наконец, мы вызываем функцию <emphasis>thread_pool_start()</emphasis>, передав ей эту самую управляющую структуру <emphasis>tpp</emphasis>.</p>
     <p>Я указал флаг POOL_FLAG_USE_SELF, что означает, что поток, вызвавший функцию <emphasis>thread_pool_start()</emphasis>, будет рассматриваться как доступный для ввода в пул. Таким образом, на момент старта пула в нем есть только один поток. Поскольку значение параметра <emphasis>lo_water</emphasis> равно 3, библиотека немедленно создаст еще <emphasis>increment</emphasis> потоков (в нашем случае — 2). С этого момента в пуле будет три (3) потока, и все они будут находиться в режиме блокирования. Условие по параметру <emphasis>lo_water</emphasis> удовлетворено, потому что число потоков в режиме блокирования действительно не меньше <emphasis>lo_water</emphasis>, условие по параметру <emphasis>hi_water</emphasis> удовлетворено, потому что число потоков в режиме блокирования действительно не больше <emphasis>hi_water</emphasis>; и, наконец, также удовлетворено условие по параметру maximum, потому что общее число потоков не превышает его значения. Допустим теперь, что один из потоков, находящихся в режиме блокирования, разблокируется (например, в серверном приложении — при получении сообщения). Это означает, что один из трех потоков перейдет из режима блокирования в режим обработки. Счетчик блокированных потоков уменьшится, и его значение упадет ниже значения параметра <emphasis>lo_water</emphasis>. Это переключит триггер <emphasis>lo_water</emphasis> и заставит библиотеку создать ещё <emphasis>increment</emphasis> (2) потоков. Таким образом, у нас будет всего 5 потоков (4 в режиме блокирования, и 1 — в режиме обработки).</p>
     <p>Пусть далее разблокируется еще несколько потоков. Давайте предположим, что на этот момент еще ни один из потоков, находящихся в режиме обработки, еще не завершил свои дела. Ниже приведена таблица, в которой иллюстрируется весь процесс, начиная с исходного состояния:</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Событие</th>
       <th align="left" valign="top">Режим обработки</th>
       <th align="left" valign="top">Режим блокирования</th>
       <th align="left" valign="top">Всего потоков</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Исходное состояние</td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">1</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Срабатывание триггера <emphasis>lo_water</emphasis></td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">3</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">3</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Срабатывание триггера <emphasis>lo_water</emphasis></td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">4</td>
       <td align="left" valign="top">5</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">5</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">5</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Срабатывание триггера <emphasis>lo_water</emphasis></td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">4</td>
       <td align="left" valign="top">7</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">4</td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">7</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">5</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">7</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Срабатывание триггера <emphasis>lo_water</emphasis></td>
       <td align="left" valign="top">5</td>
       <td align="left" valign="top">4</td>
       <td align="left" valign="top">9</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">6</td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">9</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">9</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Срабатывание триггера <emphasis>lo_water</emphasis></td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">8</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">9</td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Разблокирование</td>
       <td align="left" valign="top">10</td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">10</td>
      </tr>
     </table>
     <p>Видно, что библиотека проверяет параметр <emphasis>lo_water</emphasis>, и по мере необходимости увеличивает число потоков на значение параметра <emphasis>increment</emphasis>, но только до тех пор, пока число потоков не достигнет предельного значения — параметра <emphasis>maximum</emphasis> (именно поэтому число в столбце «Всего потоков» никогда не превышает 10, даже когда условие по параметру <emphasis>lo_water</emphasis> перестает выполняться).</p>
     <p>Это означает, что однажды наступает момент, когда потоков в режиме блокирования больше не остается. Предположим теперь, что потоки, находящиеся в режиме обработки, завершают свои дела. Посмотрим, что при этом произойдет с триггером параметра <emphasis>hi_water</emphasis>.</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Событие</th>
       <th align="left" valign="top">Режим обработки</th>
       <th align="left" valign="top">Режим блокирования</th>
       <th align="left" valign="top">Всего потоков</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">9</td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">8</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">6</td>
       <td align="left" valign="top">4</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">5</td>
       <td align="left" valign="top">5</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">4</td>
       <td align="left" valign="top">6</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">8</td>
       <td align="left" valign="top">10</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Срабатывание триггера <emphasis>hi_water</emphasis></td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">9</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">8</td>
       <td align="left" valign="top">9</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Срабатывание триггера <emphasis>hi_water</emphasis></td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">9</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Завершение обработки</td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">8</td>
       <td align="left" valign="top">8</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Срабатывание триггера <emphasis>hi_water</emphasis></td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">7</td>
      </tr>
     </table>
     <p>Обратите внимание, что с потоками ничего не происходит до тех пор, пока число блокированных потоков не превышает значение <emphasis>hi_water</emphasis>. Реализация здесь такова: как только поток завершает обработку, он проверяет число блокированных на данный момент потоков, и если их слишком много (то есть больше, чем предусмотрено параметром <emphasis>hi_water</emphasis>), то «совершает самоубийство». Удобство использования параметров <emphasis>lo_water</emphasis> и <emphasis>hi_water</emphasis> в управляющих структурах состоит в том, что ими вы фактически задаете «эффективный диапазон» числа потоков, в пределах которого всегда доступно достаточное число потоков, и потоки без необходимости не создаются и не уничтожаются. В нашем случае, после выполнения действий, перечисленных в вышеупомянутых таблицах, мы имеем систему, которая способна обрабатывать до 4 запросов одновременно без необходимости в создании дополнительных потоков (7-4 = 3, что соответствует значению параметра <emphasis>lo_ water</emphasis>).</p>
     <subtitle>Функции работы с пулами потоков</subtitle>
     <p>Теперь, когда мы достаточно хорошо владеем методикой управления числом потоков в пуле, давайте обратимся к другим элементам атрибутной записи пула потоков:</p>
     <p><code>// Функции и дескриптор пула потоков</code></p>
     <p><code>THREAD_POOL_HANDLE_T *handlе;</code></p>
     <empty-line/>
     <p><code>THREAD_POOL_PARAM_T *(*block_func)(</code></p>
     <p><code> THREAD_POOL_PARAM_T *ctp);</code></p>
     <empty-line/>
     <p><code>void (*unblock_func)(THREAD_POOL_PARAM_T *ctp);</code></p>
     <empty-line/>
     <p><code>int (*handler_func)(THREAD_POOL_PARAM_T *ctp);</code></p>
     <empty-line/>
     <p><code>THREAD_POOL_PARAM_T *(*context_alloc)(</code></p>
     <p><code> THREAD_POOL_HANDLE_T *handle);</code></p>
     <empty-line/>
     <p><code>void (*context_free)(THREAD_POOL_PARAM_T *ctp);</code></p>
     <p>Повторно обратимся к рисунку «Жизненный цикл пула потоков». Из рисунка видно, что при создании потока каждый раз вызывается функция <emphasis>context_alloc()</emphasis>. (Аналогично, при уничтожении потока вызывается функция <emphasis>context_tree()</emphasis>). Элемент атрибутной записи с именем <emphasis>handler</emphasis> передается функции <emphasis>context_alloc()</emphasis> в качестве ее единственного параметра. Функция <emphasis>context_alloc()</emphasis> ответственна за индивидуальные настройки потока и возвращает указатель на контекст (списках параметров называемый <emphasis>ctp</emphasis>). Заметьте, что содержание этого указателя — исключительно ваша забота; библиотеке абсолютно все равно, что вы в него поместите.</p>
     <p>Теперь, когда контекст создан функцией <emphasis>context_alloc()</emphasis>, вызывается функция <emphasis>block_func()</emphasis> для перевода потока в режим блокирования. Заметьте, что функция <emphasis>block_func()</emphasis> получает на вход результат работы функции <emphasis>context_alloc()</emphasis>. После того как функция <emphasis>block_func()</emphasis> разблокируется, она возвращает указатель на контекст, который библиотека передает функции <emphasis>handler_func()</emphasis>. Функция <emphasis>handler_func()</emphasis> отвечает за выполнение «работы» — например, в типовом варианте именно она обрабатывает сообщение от клиента. На данный момент принято, что функция <emphasis>handler_func()</emphasis> должна возвращать нуль — ненулевые значения зарезервированы QSSL для будущего функционального расширения. Функция <emphasis>unblock_func()</emphasis> также в настоящее время зарезервирована, поэтому просто оставьте там NULL.</p>
     <p>Возможно, ситуацию немного прояснит приведенный ниже пример псевдокода (он основан все на том же рисунке «Жизненный цикл потока в пуле потоков»):</p>
     <p><code>FOREVER DO</code></p>
     <p><code> IF (#threads &lt; lo_water) THEN</code></p>
     <p><code>  IF (#threads &lt; maximum) THEN</code></p>
     <p><code>   create new thread</code></p>
     <p><code>   context = (*context_alloc)(handle);</code></p>
     <p><code>  ENDIF</code></p>
     <p><code> ENDIF</code></p>
     <p><code> retval = (*block_func)(context);</code></p>
     <p><code> (*handler_func)(retval);</code></p>
     <p><code> IF (#threads &gt; hi_water) THEN</code></p>
     <p><code>  (*context_free)(context)</code></p>
     <p><code>  kill thread</code></p>
     <p><code> ENDIF</code></p>
     <p><code>DONE</code></p>
     <p>Отметим, что приведенная выше программа излишне упрощена. Ее назначение состоит только в том, чтобы продемонстрировать вам поток данных по параметрам <emphasis>ctp</emphasis> и <emphasis>handler</emphasis> и дать вам некоторое представление об алгоритмах, которые обычно применяются для управления числом потоков.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Диспетчеризация и реальный мир</p>
    </title>
    <section>
     <p>До настоящего момента мы обсуждали дисциплины диспетчеризации и состояния потоков, но практически ничего не сказали относительно того, почему и когда происходит собственно перепланирование. Существует распространенное заблуждение, что перепланирование «просто случается», безо всяких реальных причин. И в общем-то, для проектирования это довольно полезная абстракция! Однако, очень важно понимать, почему происходит перепланирование. Вспомним рисунок «Схема алгоритма диспетчеризации» (в разделе «Роль ядра»).</p>
     <p>Перепланирование может иметь только три причины:</p>
     <p>• аппаратное прерывание;</p>
     <p>• системный вызов;</p>
     <p>• сбой (исключение).</p>
    </section>
    <section>
     <title>
      <p>Перепланирование по аппаратному прерыванию</p>
     </title>
     <p>Перепланирование из-за аппаратного прерывания можно разделить на две категории: </p>
     <p>• по прерыванию от таймеров;</p>
     <p>• по прерыванию от других аппаратных средств.</p>
     <p>Часы реального времени генерируют периодические прерывания для ядра, организуя перепланирование во времени.</p>
     <p>Например, если вы производите вызов <code><strong>sleep(10)</strong></code>, часы реального времени сгенерируют некоторое число прерываний; по каждому прерыванию ядро увеличивает значение системных часов. Когда системные часы покажут, что 10 секунд истекли, ядро перепланирует ваш поток, переведя его в состояние готовности (READY). (Мы рассмотрим этот вопрос более подробно в главе «Часы, таймеры и периодические уведомления»).</p>
     <p>Другие потоки могут ожидать аппаратные прерывания от внешних устройств, таких как последовательный порт, жесткий диск или аудио платы. В этом случае они блокируются в ядре, ожидающем аппаратное прерывание. Поток будет переупорядочен ядром только после того, как ядро сгенерирует «событие».</p>
    </section>
    <section>
     <title>
      <p>Перепланирование по системным вызовам</p>
     </title>
     <p>Если поток делает системный вызов, перепланирование выполняется немедленно и может рассматриваться как асинхронное в отношении прерываний таймера и других прерываний.</p>
     <p>Например, выше мы приводили пример вызова функции <code><strong>sleep(10)</strong></code>. Это библиотечная функция языка Си, в конечном счете она транслируется в системный вызов. В тот же самый момент ядро приняло решение о перепланировании, чтобы удалить ваш поток из очереди готовности по соответствующему приоритету и поставить на выполнение другой поток, находящийся в состоянии готовности (READY).</p>
     <p>Системных вызов, вызывающи процесс обязательного перепланирования, очень много. Большинство их них достаточно очевидны. Перечислим некоторые из них:</p>
     <p>• функции таймера (например, <emphasis>sleep()</emphasis>);</p>
     <p>• функции обмена сообщениями (например, <emphasis>MsgSendv()</emphasis>);</p>
     <p>• примитивы работы с потоками (например, <emphasis>pthread_cancel()</emphasis> или <emphasis>pthread_join()</emphasis>).</p>
    </section>
    <section>
     <title>
      <p>Перепланирование по исключительным ситуациям</p>
     </title>
     <p>Последняя из вышеперечисленных причин перепланирования — это сбой процессора (CPU fault), который является исключительной ситуацией (exception) — чем-то средним между аппаратным прерыванием и системным вызовом. Исключительные ситуации асинхронны в отношении ядра (подобно прерыванию), но синхронны с вызывающими их пользовательскими программами (подобно вызову ядра — например, такая исключительная ситуация как деление на ноль). Все рассуждения, относящиеся к перепланированию по прерываниям от аппаратных средств и по системным вызовам, относятся и к исключительным ситуациям тоже.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Резюме</p>
    </title>
    <p>Операционная система QNX/Neutrino предлагает богатые возможности диспетчеризации потоков — минимальных диспетчеризуемых единиц. Процесс в QNX/Neutrino определяется как минимальная единица, способная обладать ресурсами (например, областями памяти), и может содержать один или более потоков.</p>
    <p>С потоками можно применять любые из следующих методов синхронизации:</p>
    <p>• мутексы (mutexes) — владеть мутексом в заданный момент времени может только один поток;</p>
    <p>• семафоры (semaphores) — владеть семафором позволяется некоторому фиксированному числу потоков;</p>
    <p>• ждущие блокировки (sleepons) — позволяют нескольким потокам блокироваться на нескольких объектах, динамически назначая блокированным потокам соответствующие условные переменные;</p>
    <p>• условные переменные (condvars) — подобны ждущим блокировкам, за исключением того, что за распределение условных переменных отвечает программист;</p>
    <p>• присоединение (joining) — обеспечивает синхронизацию потока по отношению к завершению другого потока;</p>
    <p>• барьеры (barriers) — позволяют потокам ждать, пока определенное число потоков не встретится в определенной точке.</p>
    <p>Отметим, что мутексы, семафоры и условные переменные могут использоваться между потоками как в том же самом, так и в разных процессах, ждущие же блокировки могут применяться только между потоками одного и того же процесса (потому что системный мутекс библиотеки ждущих блокировок «скрыт» в адресном пространстве процесса).</p>
    <p>Наряду с синхронизацией, потоки можно диспетчеризовать (используя приоритеты и различные дисциплины диспетчеризации), и они автоматически могут выполняться как в однопроцессорном блоке, так и в системе с архитектурой SMP.</p>
    <p>Всякий раз, когда мы говорим о «создании процесса» (обычно как о средстве переноса однопоточного кода), мы действительно создаем адресное пространство с одним работающим в нем потоком — этот поток стартует по вызову функции <emphasis>main()</emphasis> или функций <emphasis>atfork()</emphasis> или <emphasis>vfork()</emphasis>, в зависимости от реализации.</p>
   </section>
  </section>
  <section>
   <title>
    <p>Глава 2</p>
    <p>Обмен сообщениями</p>
   </title>
   <section>
    <title>
     <p>Введение в обмен сообщениями</p>
    </title>
    <section>
     <p>В данной главе мы рассмотрим наиболее характерную отличительную особенность QNX/Neutrino — механизм <emphasis>обмена сообщениями</emphasis>. Обмен сообщениями в QNX/Neutrino — ключевой механизм, глубоко интегрированный с микроядерной архитектурой этой операционной системы и обеспечивающий ей ее модульность.</p>
    </section>
    <section>
     <title>
      <p>Микроядро и обмен сообщениями</p>
     </title>
     <p>Одним из основных преимуществ QNX/Neutrino является то, что данная операционная система является масштабируемой. Под «масштабируемостью» здесь подразумевается, что данная система может быть адаптирована к работе как в крошечных встраиваемых системах с ограниченными ресурсами, так и в больших сетях симметричных многопроцессорных систем (SMP), т.е. систем, ресурсы которых практически неограничены.</p>
     <p>В операционной системе QNX/Neutrino такой уровень универсальности достигается разнесением различных сервисов по отдельным модулям. Таким образом, вы имеете возможность включить в конечную систему только те компоненты, которые вам действительно необходимы. Используя многопоточность, вы также упрощаете своему проекту «масштабируемость вверх» для использования его в SMP-системах (в данной главе мы рассмотрим еще ряд полезных применений для потоков, которые мы не обсуждали ранее).</p>
     <p>Эта концепция была изначально заложена во все ОС семейства QNX и соблюдается по сей день. Основным принципом построения этих систем является микроядерная архитектура, когда модули, которые в традиционной операционной системе были бы включены в состав ядра, рассматриваются как необязательные компоненты.</p>
     <image l:href="#img_17.png"/>
     <p>Модульная архитектура QNX/Neutrino.</p>
     <p>Какие из модулей применить в проекте — это уже решать проектировщику, то есть вам. В вашем проекте необходима файловая система? Если да, включите ее в проект. Если нет — можете не включать. Вам необходим драйвер последовательного порта? Каков бы ни был ваш ответ, он не повлияет на предыдущее решение касательно файловой системы, равно как и не будет от него зависеть.</p>
     <p>В процессе работы системы вы также имеете возможность изменять ее состав. Вы можете динамически удалять любые компоненты из работающей системы или добавлять их, в любой произвольный момент времени. Вы спросите, существуют ли какие-либо ограничения относительно «драйверов»? Нет, не существуют — драйвер в QNX/Neutrino является стандартной пользовательской программой, которая разве что выполняет определенные действия с оборудованием. Мы обсудим, как писать такие программы, в главе «Администраторы ресурсов».</p>
     <p>Ключом к реализации всего этого является обмен сообщениями. Вместо встраивания модулей ОС непосредственно в ядро и обеспечения между ними некоего «специального» взаимодействия, в QNX/Neutrino модули общаются друг с другом посредством обмена сообщениями. Ядро в основном отвечает только за служебные функции на уровне потоков (например, за диспетчеризацию потоков). На самом деле, обмен сообщениями используется не только для трюков с динамической инсталляцией и деинсталляцией компонентов — на нем основаны большинство всех остальных служебных функций (например, распределение памяти выполняется путем отправки специализированного сообщения администратору процессов). Впрочем, конечно, некоторые сервисы реализуются непосредственно через системные вызовы.</p>
     <p>Рассмотрим открытие файла и запись в него блока данных. Это реализуется с помощью ряда сообщений, посылаемых приложением такому опциональному компоненту ОС как файловая система. Сообщение указывает файловой системе открыть файл, затем другое сообщение указывает ей записать в него некие данные. И не волнуйтесь — обмен сообщениями в QNX/Neutrino происходит очень быстро.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Обмен сообщениями и модель «клиент/сервер»</p>
    </title>
    <p>Представьте, что приложение читает данные из файловой системы. На языке QNX это значит, что данное приложение — это <emphasis>клиент</emphasis>, запрашивающий данные у <emphasis>сервера</emphasis>.</p>
    <p>Модель «клиент/сервер» позволяет говорить о нескольких рабочих состояниях процессов, связанных с обменом сообщениями (мы говорили о них в главе «Процессы и потоки»). Первоначально сервер ждет от кого-нибудь сообщение. В этот момент сервер, как говорят, должен быть в состоянии блокировки по приему (recieve-blocked) (оно также может обозначаться как RECV). Ниже приведен пример вывода программы <code><strong>pidin</strong></code>:</p>
    <p><code>pid tid name      prio  STATE   Blocked</code></p>
    <p><code>4   1   devc-pty  10r   RECEIVE       1</code></p>
    <p>В приведенном примере сервер псевдотерминалов (называемый <code><strong>devc-pty</strong></code>) имеет идентификатор процесса 4, содержит один поток с идентификатором потока 1, выполняется с приоритетом 10, подчиняется диспетчеризации карусельного типа (RR) и находится в состоянии блокировки по приему, ожидая сообщения по каналу с идентификатором 1 (к «каналам» мы еще скоро вернемся).</p>
    <image l:href="#img_18.png"/>
    <p>Смена состояний сервера.</p>
    <p>По получении сообщения сервер переходит в состояние готовности (READY) и становится способен выполнять работу. Если оказывается, что из всех процессов, находящихся в данный момент в состоянии готовности (READY), наш сервер имеет наивысший приоритет, он получит процессор и сможет выполнить какие-то действия. Поскольку это сервер, он анализирует поступившее сообщение и решает, что с ним делать. В некоторый момент времени сервер завершит обработку сообщения и затем «ответит» клиенту.</p>
    <p>Перейдем теперь к клиенту. Изначально клиент работал самостоятельно, пока не решил послать сообщение. Клиент переключается при этом из состояния готовности (READY) в состояние либо блокировки по передаче (send-blocked), либо блокировки по приему (recieve-blocked), в зависимости от состояния сервера, которому было послано сообщение.</p>
    <image l:href="#img_19.png"/>
    <p>Смена состояний клиента</p>
    <p>Скорее всего, вам чаще придется иметь дело с состоянием блокировки по приему (reply-blocked), чем с состоянием блокировки по передаче (send-blocked). Состояние блокировки по приему (reply-blocked) реально означает следующее:</p>
    <cite>
     <p>Сервер принял сообщение и теперь обрабатывает его. В некоторый момент времени сервер завершит обработку и ответит клиенту. Клиент блокирован в ожидании этого ответа от сервера.</p>
    </cite>
    <p>Сравните с состоянием блокировки по передаче (send-blocked):</p>
    <cite>
     <p>Сервер все еще не принял сообщение — вероятно, потому что был занят обработкой другого, ранее поступившего сообщения. Когда сервер возвратится в состояние «приема» вашего (клиентского) сообщения, вы перейдете из состояния блокировки по передаче (send-blocked) в состояние блокировки по ответу (reply- blocked).</p>
    </cite>
    <p>На практике, если вы наблюдаете процесс, блокированный по передаче (send-blocked), это означает одно из двух:</p>
    <p>1. Вы запечатлели момент, когда сервер был занят обслуживанием некоего клиента и в это время получил еще один запрос.</p>
    <p>Это нормальная ситуация — вы можете проверить это, повторно выполнив <code><strong>pidin</strong></code>. На сей раз вы, вероятно, сможете увидеть, что этот процесс уже более не блокирован по передаче.</p>
    <p>2. В сервере проявилась какая-то внутренняя ошибка, и он больше не воспринимает запросы.</p>
    <p>Когда это произойдет, вы сможете увидеть множество процессов, блокированных по передаче на этом сервере. Чтобы проверить это, выполните <code><strong>pidin</strong></code> снова и посмотрите, есть ли изменения в состоянии клиентских процессов.</p>
    <p>Ниже приведен пример, в котором показан клиент в состоянии блокировки по ответу (reply-blocked) и сервер, по которому он блокирован:</p>
    <p><code>  pid tid name                 prio STATE   Blocked</code></p>
    <p><code>    1   1 /nto/x86/sys/procnto   0f READY</code></p>
    <p><code>    1   2 /nto/x86/sys/procnto  10r RECEIVE       1</code></p>
    <p><code>    1   3 /nto/x86/sys/procnto  10r NANOSLEEP</code></p>
    <p><code>    1   4 /nto/x86/sys/procnto  10r RUNNING</code></p>
    <p><code>    1   5 /nto/x86/sys/procnto  15r RECEIVE       1</code></p>
    <p><code>16426   1 esh                   10r REPLY         1</code></p>
    <p>В примере показано, что программа <code><strong>esh</strong></code> (встраиваемый командный интерпретатор) передала сообщение процессу с номером 1 (это ядро и администратор процессов, <code><strong>procnto</strong></code>) и теперь ждет ответа.</p>
    <p>Ну вот, теперь вы знаете основы обмена сообщениями в архитектуре «клиент/сервер».</p>
    <p>Не исключено, что вы сейчас думаете: «Так что, получается, чтобы открыть файл или записать данные, мне придется писать специализированные вызовы обмена сообщениями QNX/ Neutrino?!»</p>
    <p>Нет, вам не придется программировать обмен сообщениями непосредственно — разве что если вам будет нужно копнуть совсем вглубь (об этом несколько позже). Действительно, позвольте мне показать Вам некоторую программу клиента, который делает передачу сообщений:</p>
    <p><code>#include &lt;fcntl.h&gt;</code></p>
    <p><code>#include &lt;unistd.h&gt;</code></p>
    <empty-line/>
    <p><code>int main(void) {</code></p>
    <p><code> int fd;</code></p>
    <p><code> fd = open("filename", O_WRONLY);</code></p>
    <p><code> write(fd, "Это обмен сообщениями\n", 24);</code></p>
    <p><code> close(fd);</code></p>
    <p><code> return (EXIT_SUCCESS);</code></p>
    <p><code>}</code></p>
    <p>Видите? Обычная Си-программа, никаких хитростей.</p>
    <p>Собственно обмен сообщениями реализован в Си-библиотеке QNX/Neutrino. Вы просто выдаете вызовы по стандарту POSIX 1003.1 и вызовы функций ANSI Си и Си-библиотека делает за Вас всю работу, связанную с обменом сообщениями.</p>
    <p>В приведенном выше примере вызываются три функции, и посылаются три различных сообщения:</p>
    <p>• <emphasis>open()</emphasis> — передала сообщение «open» («открыть»);</p>
    <p>• <emphasis>write()</emphasis> — передала сообщение «write» («записать»);</p>
    <p>• <emphasis>close()</emphasis> — передала сообщение «close» («закрыть»).</p>
    <p>Мы обсудим сами сообщения более подробно, когда мы будем изучать администраторы ресурсов (в главе «Администраторы ресурсов»), а пока что единственное, что нам надо знать об этом — это сам факт, что были переданы сообщения различных типов.</p>
    <p>Давайте на мгновение отвлечемся и сравним этот подход с тем, как бы это работало в традиционной операционной системе.</p>
    <p>Клиентская программа осталась бы такой же — различия были бы скрыты в Си-библиотеке, поставляемой производителем программного обеспечения. В такой системе функция <emphasis>open()</emphasis> сделала бы системный вызов, ядро затем обратилось бы непосредственно к файловой системе, которая, в свою очередь, выполнила некоторые действия и возвратила бы дескриптор файла. Вызовы функций <emphasis>write()</emphasis> и <emphasis>close()</emphasis> работали бы аналогично</p>
    <p>Итак? Есть ли преимущества в способе, который предлагает QNX/Neutrino? «Оставайтесь с нами!»</p>
   </section>
   <section>
    <title>
     <p>Распределенный обмен сообщениями</p>
    </title>
    <p>Предположим, что мы пожелали изменить приведенный выше пример, чтобы можно было «поговорить» с другим узлом сети. Вы, наверное, думаете, что для этого придется вызывать специальные функции, чтобы «попасть в сеть». Вот сетевой вариант нашей программы:</p>
    <p><code>#include &lt;fcntl.h&gt;</code></p>
    <p><code>#include &lt;unistd.h&gt;</code></p>
    <empty-line/>
    <p><code>int main(void) {</code></p>
    <p><code> int fd;</code></p>
    <p><code> fd = open("/net/wintermute/home/rk/filename", O_WRONLY);</code></p>
    <p><code> write(fd, "Это обмен сообщениями\n", 24);</code></p>
    <p><code> close(fd);</code></p>
    <p><code> return (EXIT_SUCCESS);</code></p>
    <p><code>}</code></p>
    <p>Вы будете правы, если скажете, что в обеих версиях программы почти одинаковы. Так и есть.</p>
    <p>В традиционной ОС функция <emphasis>open()</emphasis> библиотеки Си вызывает ядро, которое анализирует имя файла и говорит: «Опа! Это не на нашем узле…» Ядро затем вызывает сетевую файловую систему NFS, которая уже определяет, где в действительности находится файл <code><strong>/net/wintermute/home/rk/filename</strong></code>. Затем, NFS вызывает сетевой драйвер и посылает сообщение ядру на узле <code><strong>wintermute</strong></code>, которое повторяет весь процесс, описанный нами в нашем первоначальном примере. Заметьте, что в этом случае оказываются вовлеченными две файловые системы, одна из которых — сетевая файловая система (NFS) клиента, а вторая — удаленная. К сожалению, в зависимости от реализации как удаленной файловой системы, так и NFS, некоторые операции (например, блокировки файлов) могут работать некорректно из-за неполной совместимости.</p>
    <p>В QNX/Neutrino функция <emphasis>open()</emphasis> Си-библиотеки создает точно такое же сообщение, какое она создала бы для локальной файловой системы, и посылает его файловой системе узла <code><strong>wintermute</strong></code>. Локальная и удаленная файловые системы при этом абсолютно одинаковы.</p>
    <p>Это и есть еще одна фундаментальная особенность QNX/Neutrino: распределенные операции выполняются в ней абсолютно «непринужденно», поскольку потребности клиентов изначально абстрагированы от служебных функций, обеспечиваемых серверами, благодаря механизму обмена сообщениями.</p>
    <p>В традиционном ядре действует «двойной стандарт», когда локальные сервисы реализуются одним способом, а удаленные (сетевые) — совершенно другим.</p>
   </section>
   <section>
    <title>
     <p>Что это означает для вас</p>
    </title>
    <section>
     <p>Обмен сообщениями в QNX/Neutrino элегантно реализован и распределен по сети. И что? Что с этого нам, программистам?</p>
     <p>Ну, в первую очередь это означает, что ваши программы унаследуют от ОС те же характеристики — сделать их распределенными будет гораздо проще, чем в других ОС. Но самое полезное, на мой взгляд, преимущество заключается в том, что эта схема обеспечивает модульную структуру программного обеспечения, тем самым значительно упрощая процесс отладки и тестирования.</p>
     <p>Вам, вероятно, доводилось участвовать в больших проектах, когда множество людей разрабатывают различные фрагменты целевого программного обеспечения. Разумеется, при этом кто-то опережает график, а кто-то запаздывает.</p>
     <p>У таких проектов проблемы чаще всего возникают на двух этапах: на первоначальном, при распределении отдельных частей проекта между конкретными исполнителями, а также на этапе тестирования и интеграции, когда невозможно провести комплексные испытания системы из-за недоступности всех необходимых компонентов.</p>
     <p>С использованием принципа обмена сообщениями развязать друг от друга отдельные компоненты проекта становится очень просто, что ведет к значительному упрощению как самого проекта, так и технологии тестирования. Если говорить об этом в терминах существующих парадигм, данный подход очень похож на концепции, применяемые в объектно-ориентированном программировании (ООП).</p>
     <p>К чему все это сводится? К тому, что тестирование можно выполнять поэтапно. Вы сможете написать простенькую программку, которая посылает сообщения вашему серверному процессу, а поскольку его входы и выходы являются (или должны быть!) хорошо задокументированными, то вы сможете сразу определить, работает он или нет. Черт возьми, можно даже создать типовые тестовые наборы, включить их в комплект для регрессивного тестирования и выполнять его в автоматическом режиме!</p>
    </section>
    <section>
     <title>
      <p>Философия QNX/Neutrino</p>
     </title>
     <p>Принципы обмена сообщениями лежат в самой основе философии QNX/Neutrino. Понимание смысла и приемов применения обмена сообщениями будет ключом к наиболее эффективному использованию ОС. Прежде чем углубиться в детали, давайте рассмотрим немного теории.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Обмен сообщениями и многопоточность</p>
    </title>
    <section>
     <p>При том, что модель «клиент/сервер» проста для понимания и очень широко используется, существуют две вариации на данную тему. Первая — многопоточная реализация (об этом речь в данной главе), вторая — так называемая модель «сервер/субсервер», иногда полезная и в обычных разработках, но в полной мере раскрывающая свои преимущества при проектировании распределенных систем. Сочетание этих двух концепций предоставляет колоссальную мощь, особенно в сетях симметричных мультипроцессорных систем!</p>
     <p>Как мы уже обсуждали в главе «Процессы и потоки», QNX/Neutrino позволяет реализовать множество потоков в одном и том же процессе. Какие преимущества это нам даст в сочетании с механизмом обмена сообщениями?</p>
     <p>Ответ здесь довольно прост. Мы можем стартовать пул потоков (используя функции семейства <emphasis>thread_pool_*()</emphasis>, о которых мы говорили в разделе «Процессы и потоки»), каждый из которых сможет обрабатывать сообщения от клиентов:</p>
     <image l:href="#img_20.png"/>
     <p>Обслуживание клиентов различными потоками сервера</p>
     <p>С этой точки зрения нам абсолютно все равно, которому именно потоку достанется на обработку отправленное клиентом сообщение — главное, чтобы работа была выполнена. Это имеет ряд преимуществ. Способность обслуживать нескольких клиентов отдельными потоками обработки является очень мощной концепцией по сравнению с применением лишь одного потока. Главное преимущество состоит в том, что задача распараллеливания обработки перелагается на ядро, избавляя сам сервер от необходимости реализовывать параллельную обработку самостоятельно.</p>
     <p>Когда несколько потоков работают на машине с единственным процессором, это значит, что все эти потоки будут конкурировать друг с другом за процессорное время.</p>
     <p>Однако, в SMP-блоке мы можем сделать так, чтобы потоки конкурировали не за один, а за несколько процессоров, используя при этом одну и ту же общую область данных. Это означает, что здесь мы будем ограничены исключительно числом доступных процессоров.</p>
    </section>
    <section>
     <title>
      <p>Модель «сервер/субсервер»</p>
     </title>
     <p>Давайте теперь рассмотрим модель «сервер/субсервер», а затем наложим ее на модель многопоточности.</p>
     <p>В соответствием с моделью «сервер/субсервер», сервер по- прежнему обеспечивает обслуживание клиентуры, но поскольку обслуживание запросов может занимать слишком много времени, мы должны быть способны поставить запрос на обработку и при этом не потерять способность обрабатывать новые запросы, продолжающие поступать от других клиентов.</p>
     <p>Если бы мы попытались реализовать эту задачу с применением традиционной однопоточной модели «клиент/сервер», то после получения одного запроса и начала его обработки мы потеряли бы способность воспринимать другие запросы — нам приходилось бы периодически прекращать обработку, проверять, есть ли еще запросы, помещать таковые (если они есть) в очередь заданий и затем продолжать обработку, уже распыляя внимание на обработку всевозможных заданий, находящихся в очереди. Не очень-то эффективно. Фактически мы здесь дублируем работу ядра путем реализации дополнительного квантования времени между заданиями!</p>
     <p>Представьте себе, каково вам было бы делать все это самому. Вы сидите за своим рабочим столом в офисе, и кто-то приносит вам полную папку заданий на выполнение. Вы начинаете их выполнять. Вы по уши заняты работой, и тут в дверном проеме появляется кто-то еще, с еще одним списком не менее первоочередных заданий. Теперь у вас на рабочем столе имеете два списка неотложных дел, и вы продолжаете работать, стараясь выполнить все. Вы тратите несколько минут на работы из одного списка, потом переключаете внимание на другой, и так далее, периодически посматривая на дверной проем, на случай если кто- нибудь принесет еще.</p>
     <p>В такой ситуации было бы гораздо разумнее как раз применить модель «сервер/субсервер». В соответствии с этой моделью, у нас есть сервер, который создает ряд других процессов (субсерверов). Каждый из этих субсерверов посылает сообщение серверу, но сервер не отвечает им, пока не получит запрос от клиента. Затем сервер передает запрос клиента одному из субсерверов, отвечая на его сообщение заданием, которое субсервер обязан выполнить. Этот процесс показан на приведенном ниже рисунке. Отметьте для себя направления стрелок — они соответствуют направлениям передачи сообщений!</p>
     <image l:href="#img_21.png"/>
     <p>Модель «сервер/субсервер».</p>
     <p>Если бы вы делали что-то подобное, вы бы, скорее всего, наняли дополнительно несколько служащих. Эти служащие все пришли бы к вам (как субсерверы посылают сообщение серверу — отсюда направление стрелок на рисунке) в поисках, чего бы такого сделать. Первоначально у вас могло и не быть для них никакой работы — в таком случае их запросы остались бы без ответа. Но теперь, когда кто-нибудь принесет вам кипу бумаг, вы скажете одному из ваших подчиненных: «Это тебе!» — и подчиненный пойдет заниматься делом. Аналогично, по мере поступления других заданий вы и далее будете делегировать их остальным подчиненным.</p>
     <p>Хитрость этой модели заключается в том, что она является управляемой по ответу (reply-driven) — выполнение задания начинается с вашего ответа (reply) субсерверу. Стандартная же модель «клиент/сервер» является управляемой по запросу (send-driven), поскольку работа начинается с передачи сообщения серверу.</p>
     <p>Но почему клиенты приходят именно <emphasis>к вам</emphasis> в офис, а не в офисы нанятых вами работников? Почему именно вы распределяете работу? Ответ довольно прост: вы — координатор, ответственный за определенную задачу, и ваша обязанность — гарантировать ее выполнение. Ваши клиенты, заказывающие вам работу, знают Вас, но не знают ни имен, ни местонахождения ваших (возможно, временных) работников.</p>
     <p>Как вы, вероятно, и подозревали, концепцию единого многопоточного сервера и модель «сервер/субсервер» можно комбинировать. Главная хитрость при этом будет в определении того, какие части задачи лучше было бы распределить по машинам в сети (обычно это касается компонентов системы, которые не генерируют большого трафика), а какие — по процессорам SMP-архитектур (чаще всего это элементы, требующие наличия разделяемой области данных).</p>
     <p>Зачем можно было бы комбинировать эти два метода? Используя подход «сервер/субсервер», мы сможем распределять работу между узлами сети. Это в действительности означает, что мы ограничены только числом доступных в сети машин (ну, и полосой пропускания сети, разумеется).</p>
     <p>Объединение этот подход с принципом распределения потоков по различным процессорам в архитектурах SMP, мы получим «вычислительный кластер», где центральный «арбитр» распределяет работу (в модели «сервер/субсервер») между блоками SMP, объединенными в сеть. </p>
    </section>
    <section>
     <title>
      <p>Несколько примеров</p>
     </title>
     <p>Рассмотрим теперь несколько примеров применения каждого метода.</p>
     <subtitle>Режим с управлением по запросу (send-driven) — модель «клиент/сервер»</subtitle>
     <p>Файловая система, последовательные порты, консоли и звуковые платы — все это примеры применения модели «клиент/сервер». Прикладная программа на языке Си берет на себя роль клиента и посылает запросы этим серверам. Серверы выполняют работу и отвечают клиентам.</p>
     <p>Некоторые из этих «обычных» серверов, однако, в действительности могут быть серверами, управляемыми по ответу (reply-driven)! Это возможно, например, в случае, когда по отношению к конечному клиенту они выглядят как стандартные серверы, а вот работу выполняют по методике «сервер/ субсервер». То есть я имею в виду, что клиент по-прежнему посылает сообщение тому, кого считает «серверным процессом», а тот просто передает работу другому процессу (субсерверу).</p>
     <subtitle>Режим с управлением по ответу (reply-driven) — модель «сервер/субсервер»</subtitle>
     <p>Один из наиболее популярных примеров программы, управляемой по ответу (reply-driven), — это программа фрактальной графики, распределенная по сети. Ведущая программа делит экран на несколько зон — например, на 64 зоны. При старте ведущей программе задается список узлов, которые могут участвовать в работе. Затем ведущая программа запускает рабочие программы (субсерверы), по одной на каждый узел, и ждет от них сообщений. </p>
     <p>Затем ведущая программа по очереди берет «незаполненные» зоны (из имеющихся 64) и передает задачу фрактальных вычислений программе-исполнителю на другом узле, отвечая ей на ее сообщение. Когда рабочая программа завершит вычисления, она посылает результаты обратно ведущей, которая выводит их на экран.</p>
     <p>Поскольку программа-исполнитель передала результаты ведущей программе путем отправки ей сообщения, она теперь снова готова получить от нее ответ с новым заданием. Ведущая программа так и делает до тех пор, пока все 64 зоны на экране не будут заполнены.</p>
     <subtitle>Важная тонкость</subtitle>
     <p>Поскольку ведущая программа отвечает за распределение работы между программами-исполнителям, она не может себе позволить быть заблокированной! При традиционном подходе с управлением по запросу (send-driven) ведущая программа должна была бы создать программу-исполнителя и послать ей сообщение. К сожалению, при этом ведущая программа не сможет получить ответ до тех пор, пока программа-исполнитель не выполнит свою работу, а значит, не сможет и передать сообщение другой программе-исполнителю. Это сразу сводит на нет все преимущества наличия нескольких рабочих программ на разных узлах.</p>
     <image l:href="#img_22.png"/>
     <p>Один ведущий, несколько исполнителей</p>
     <p>Решение этой проблемы заключается в том, чтобы исполнители при старте запросили ведущего, есть ли для них работа, послав ему сообщение. Напомним еще раз, что направление стрелок на рисунке указывает направление передачи. Теперь исполнители ждут ответа от ведущего. Когда какой-нибудь клиент «заказывает работу» ведущему, тот отвечает одному или более из исполнителей, что указывает им выйти из ожидания и начать выполнение. Это позволяет исполнителям заботиться о своих делах самостоятельно, а ведущий сохраняет возможность отвечать на новые запросы, поскольку не блокируется в ожидании ответа от исполнителей.</p>
     <subtitle>Многопоточный сервер</subtitle>
     <p>С позиции клиента многопоточные серверы неотличимы от однопоточных. Фактически, разработчик сервера может запросто «включить многопоточность», запустив еще один или несколько потоков.</p>
     <p>В любом случае, сервер может по-прежнему использовать несколько процессоров в SMP-системе, даже если «клиент» у него только один.</p>
     <p>Что это означает? Давайте вернемся к примеру о фрактальной графике. Когда субсервер получает от сервера запрос на «вычисления», ему ничто не мешает запустить несколько потоков и начать обработку данного запроса на нескольких процессорах сразу. На самом деле, чтобы приложение лучше масштабировалось в сетях, в которых есть как мультипроцессоры SMP, так и однопроцессорные машины, сервер и субсервер могут сначала обменяться информацией о том, сколько у субсервера имеется в распоряжении процессоров. Это даст серверу знать, сколько запросов субсервер может обслужить одновременно. Тогда сервер сможет перенаправлять многопроцессорным субсерверам больше запросов, чем однопроцессорным, равномерно распределяя нагрузку между вычислительными мощностями.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Применение обмена сообщениями</p>
    </title>
    <section>
     <p>Теперь, когда мы рассмотрели базовые концепции обмена сообщениями и выяснили, что он используется даже в таких обычных повседневных вещах как библиотека языка Си, давайте рассмотрим кое-какие детали.</p>
     <subtitle>Архитектура и структура</subtitle>
     <p>Мы рассуждали о «клиентах» и «серверах». Я также использовал три ключевые выражения:</p>
     <p>• «Клиент <emphasis>посылает</emphasis> (sends) сообщение серверу»;</p>
     <p>• «Сервер <emphasis>принимает</emphasis> (receives) сообщение от клиента»;</p>
     <p>• «Сервер <emphasis>отвечает</emphasis> (replies) клиенту».</p>
     <p>Я преднамеренно использовал именно эти выражения, потому что они в точности соответствуют действительным именам функций, которые используются для передачи сообщений в QNX/Neutrino.</p>
     <p>Ниже приводится (в алфавитном порядке) полный список функций QNX/Neutrino, относящихся к обмену сообщениями:</p>
     <p>• <emphasis>ChannelCreate()</emphasis>, <emphasis>ChannelDestroy()</emphasis>;</p>
     <p>• <emphasis>ConnectAttach()</emphasis>, <emphasis>ConnectDetach()</emphasis>;</p>
     <p>• <emphasis>MsgDeliverEvent()</emphasis>;</p>
     <p>• <emphasis>MsgError()</emphasis>;</p>
     <p>• <emphasis>MsgRead()</emphasis>, <emphasis>MsgReadv()</emphasis>;</p>
     <p>• <emphasis>MsgRecieve()</emphasis>, <emphasis>MsgRecievePulse()</emphasis>, <emphasis>MsgRecievev()</emphasis>;</p>
     <p>• <emphasis>MsgReply()</emphasis>, <emphasis>MsgReplyv()</emphasis>;</p>
     <p>• <emphasis>MsgSend()</emphasis>, <emphasis>MsgSendc()</emphasis>, <emphasis>MsgSendsv()</emphasis>, <emphasis>MsgSendsvnc()</emphasis>, <emphasis>MsgSendvs()</emphasis>, <emphasis>MsgSendvsnc()</emphasis>, <emphasis>MsgSendv()</emphasis>, <emphasis>MsgSendvnc()</emphasis>;</p>
     <p>• <emphasis>MsgWrite()</emphasis>, <emphasis>MsgWritev()</emphasis>.</p>
     <p>Пусть вас не приводит в замешательство размер списка функций. Вы запросто сможете писать приложения «клиент/сервер», используя лишь небольшое подмножество этого списка, просто по мере углубления в детали вы поймете, что некоторые из вышеперечисленных функций могут оказаться очень полезными в определенных случаях.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Минимальный полезный набор функций включает в себя функции <emphasis>ChannelCreate()</emphasis>, <emphasis>ConnectAttach()</emphasis>, <emphasis>MsgReply()</emphasis>, <emphasis>MsgSend()</emphasis> и <emphasis>MsgRecieve()</emphasis>.</p>
     </cite>
     <p>Разобьем обсуждение на две части: отдельно обсудим функции, которые применяются на стороне клиента, и отдельно — те, что применяются на стороне сервера.</p>
    </section>
    <section>
     <title>
      <p>Клиент</p>
     </title>
     <p>Клиент, который желает послать запрос серверу, блокируется до тех пор, пока сервер не завершит обработку запроса. Затем, после завершения сервером обработки запроса, клиент разблокируется, чтобы принять «ответ».</p>
     <p>Это подразумевает обеспечение двух условий: клиент должен «уметь» сначала установить соединение с сервером, а потом обмениваться с ним данными с помощью сообщений — как в одну сторону (запрос — «send»), так и в другую (ответ — «reply»).</p>
     <subtitle>Установление соединения</subtitle>
     <p>Итак, рассмотрим теперь функции по порядку. Первое, что мы должны сделать — это установить соединение. Это мы сделаем с помощью функции <emphasis>ConnectAttach()</emphasis>, описанной следующим образом:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int ConnectAttach(int nd, pid_t pid, int chid,</code></p>
     <p><code> unsigned index, int flags);</code></p>
     <p>Функции <emphasis>ConnectAttach()</emphasis> передаются три идентификатора: идентификатор <emphasis>nd</emphasis> — дескриптор узла (Node Descriptor), идентификатор <emphasis>pid</emphasis> — идентификатор процесса (process ID) и идентификатор <emphasis>chid</emphasis> — идентификатор канала (channel ID).</p>
     <p>Вместе эти три идентификатора, которые обычно записываются в виде «ND/PID/CHID», однозначно идентифицируют сервер, с которым клиент желает соединиться. Аргументы <emphasis>index</emphasis> и <emphasis>flags</emphasis> мы здесь просто проигнорируем (установим их в ноль).</p>
     <p>Итак, предположим, что мы хотим подсоединиться к процессу, находящемуся на нашем узле и имеющего идентификатор 77, по каналу с идентификатором 1. Ниже приведен пример программы для выполнения этого:</p>
     <p><code>int coid;</code></p>
     <p><code>coid = ConnectAttach(0, 77, 1, 0, 0);</code></p>
     <p>Можно видеть, что присвоением идентификатору узла (<emphasis>nd</emphasis>) нулевого значения мы сообщаем ядру о том, что мы желаем установить соединение на локальном узле.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Как я узнал, что соединиться надо с процессом 77 и по каналу 1? К этому мы скоро вернемся (см. ниже «Поиск сервера по ND/PID/CHID»).</p>
     </cite>
     <p>С этого момента у меня есть идентификатор соединения — небольшое целое число, которое однозначно идентифицирует соединение моего клиента с конкретным сервером по заданному каналу.</p>
     <p>Я смогу применять этот идентификатор для отправки запросов серверу сколько угодно раз. Выполнив все, для чего предназначалось соединение, я смогу уничтожить его с помощью функции:</p>
     <p><code>ConnectDetach(coid);</code></p>
     <p>Итак, давайте рассмотрим, как я воспользуюсь этим на практике.</p>
     <subtitle>Передача сообщений (sending)</subtitle>
     <p>Передача сообщения со стороны клиента осуществляется применением какой-либо функции из семейства <emphasis>MsgSend*()</emphasis>.</p>
     <p>Мы рассмотрим это на примере простейшей из них — <emphasis>MsgSend()</emphasis>:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int MsgSend(int coid, const void *smsg, int sbytes,</code></p>
     <p><code> void *rmsg, int rbytes);</code></p>
     <p>Аргументами функции <emphasis>MsgSend()</emphasis> являются :</p>
     <p>• идентификатор соединения с целевым сервером (<emphasis>coid</emphasis>);</p>
     <p>• указатель на передаваемое сообщение (<emphasis>smsg</emphasis>);</p>
     <p>• размер передаваемого сообщения (<emphasis>sbytes</emphasis>);</p>
     <p>• указатель на буфер для ответного сообщения (<emphasis>rmsg</emphasis>);</p>
     <p>• размер ответного сообщения (<emphasis>rbytes</emphasis>);</p>
     <p>Что может быть проще!</p>
     <p>Передадим сообщение процессу с идентификатором 77 по каналу 1:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>char *smsg = "Это буфер вывода";</code></p>
     <p><code>char rmsg[200];</code></p>
     <p><code>int coid;</code></p>
     <empty-line/>
     <p><code>// Установить соединение</code></p>
     <p><code>coid = ConnectAttach(0, 77, 1, 0, 0);</code></p>
     <p><code>if (coid == -1) {</code></p>
     <p><code> fprintf(stderr, "Ошибка ConnectAttach к 0/77/1!\n");</code></p>
     <p><code> perror(NULL);</code></p>
     <p><code> exit(EXIT_FAILURE);</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>// Послать сообщение</code></p>
     <p><code>if (MsgSend(</code></p>
     <p><code> coid, smsg, strlen(smsg) + 1, rmsg, sizeof(rmsg)) == -1) {</code></p>
     <p><code> fprintf (stderr, "Ошибка MsgSend\n");</code></p>
     <p><code> perror(NULL);</code></p>
     <p><code> exit (EXIT_FAILURE);</code></p>
     <p><code>}</code></p>
     <p><code>if (strlen(rmsg) &gt; 0) {</code></p>
     <p><code> printf("Процесс с ID 77 возвратил \"%s\"\n", rmsg);</code></p>
     <p><code>}</code></p>
     <p>Предположим, что процесс с идентификатором 77 был действительно активным сервером, ожидающим сообщение именно такого формата по каналу с идентификатором 1. После приема сообщения сервер обрабатывает его и в некоторый момент времени выдает ответ с результатами обработки. В этот момент функция <emphasis>MsgSend()</emphasis> должна возвратить ноль (0), указывая этим, что все прошло успешно. Если бы сервер послал нам в ответ какие-то данные, мы смогли бы вывести их на экран с помощью последней строки в программе (с тем предположением, что обратно мы получаем корректную ASCIIZ-строку).</p>
    </section>
    <section>
     <title>
      <p>Сервер</p>
     </title>
     <p>Теперь, когда мы рассмотрели клиента, перейдем к серверу. Клиент использовал функцию <emphasis>ConnectAttach()</emphasis> для создания соединения с сервером, а затем использовал функцию <emphasis>MsgSend()</emphasis> для передачи сообщений.</p>
     <subtitle>Создание канала</subtitle>
     <p>Под этим подразумевается, что сервер должен создать канал — то, к чему присоединялся клиент, когда вызывал функцию <emphasis>ConnectAttach()</emphasis>. Обычно сервер, однажды создав канал, приберегает его «впрок».</p>
     <p>Канал создается с помощью функции <emphasis>ChannelCreate()</emphasis> и уничтожается с помощью функции <emphasis>ChannelDestroy()</emphasis>:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int ChannelCreate(unsigned flags);</code></p>
     <p><code>int ChannelDestroy(int chid);</code></p>
     <p>Мы еще вернемся к обсуждению аргумента <emphasis>flags</emphasis> (в разделе «Флаги каналов», см. ниже), а покамест будем использовать для него значение 0 (ноль). Таким образом, для создания канала сервер должен сделать так:</p>
     <p><code>int chid;</code></p>
     <p><code>chid = ChannelCreate(0);</code></p>
     <p>Теперь у нас есть канал. В этом пункте клиенты могут подсоединиться (с помощью функции <emphasis>ConnectAttach()</emphasis>) к этому каналу и начать передачу сообщений:</p>
     <image l:href="#img_23.png"/>
     <p>Связь между каналом сервера и клиентским соединением.</p>
     <subtitle>Обработка сообщений</subtitle>
     <p>В терминах обмена сообщениями, сервер отрабатывает схему обмена в два этапа — этап «приема» (receive) и этап «ответа» (reply).</p>
     <image l:href="#img_24.png"/>
     <p>Взаимосвязь функций клиента и сервера при обмене сообщениями.</p>
     <p>Обсудим сначала два простейших варианта соответствующих функций, <emphasis>MsgReceive()</emphasis> и <emphasis>MsgReply()</emphasis>, а далее посмотрим, какие есть варианты.</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int MsgReceive(int chid, void *rmsg, int rbytes,</code></p>
     <p><code> struct _msg_info *info);</code></p>
     <p><code>int MsgReply(int rcvid, int status, const void *msg,</code></p>
     <p><code> int nbytes);</code></p>
     <p>Посмотрим, как соотносятся параметры:</p>
     <image l:href="#img_25.png"/>
     <p>Поток данных при обмене сообщениями.</p>
     <p>Как видно из рисунка, имеются четыре элемента, которые мы должны обсудить:</p>
     <p>1. Клиент вызывает функцию <emphasis>MsgSend()</emphasis> и указывает ей на буфер передачи (указателем smsg и длиной <emphasis>sbytes</emphasis>). Данные передаются в буфер функции <emphasis>MsgReceive()</emphasis> на стороне сервера, по адресу <emphasis>rmsg</emphasis> и длиной <emphasis>rbytes</emphasis>. Клиент блокируется.</p>
     <p>2. Функция <emphasis>MsgReceive()</emphasis> сервера разблокируется и возвращает идентификатор отправителя <emphasis>rcvid</emphasis>, который будет впоследствии использован для ответа. Теперь сервер может использовать полученные от клиента данные.</p>
     <p>3. Сервер завершил обработку сообщения и теперь использует идентификатор отправителя <emphasis>rcvid</emphasis>, полученный от функции <emphasis>MsgReceive()</emphasis>, передавая его функции <emphasis>MsgReply()</emphasis>. Заметьте, что местоположение данных для передачи функции <emphasis>MsgReply()</emphasis> задается как указатель на буфер (<emphasis>smsg</emphasis>) определенного размера (<emphasis>sbytes</emphasis>). Ядро передает данные клиенту.</p>
     <p>4. Наконец, ядро передает параметр <emphasis>sts</emphasis>, который используется функцией <emphasis>MsgSend()</emphasis> клиента как возвращаемое значение. После этого клиент разблокируется.</p>
     <p>Вы, возможно, заметили, что для каждой буферной передачи указываются два размера (в случае запроса от клиента клиента это <emphasis>sbytes</emphasis> на стороне клиента и <emphasis>rbytes</emphasis> на стороне сервера; в случае ответа сервера это <emphasis>sbytes</emphasis> на стороне сервера и <emphasis>rbytes</emphasis> на стороне клиента). Это сделано для того, чтобы разработчики каждого компонента смогли определить размеры своих буферов — из соображений дополнительной безопасности.</p>
     <p>В нашем примере размер буфера функции <emphasis>MsgSend()</emphasis> совпадал с длиной строки сообщения. Давайте теперь рассмотрим, что происходит в сервере и как размер используется там.</p>
     <subtitle>Структура сервера</subtitle>
     <p>Вот общая структура сервера:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>...</code></p>
     <empty-line/>
     <p><code>void server(void) {</code></p>
     <p><code> int rcvid; // Указывает, кому надо отвечать</code></p>
     <p><code> int chid; // Идентификатор канала</code></p>
     <p><code> char message[512]; // Достаточно велик</code></p>
     <p><code> // Создать канал</code></p>
     <p><code> chid = ChannelCreate(0);</code></p>
     <p><code> // Выполняться вечно — для сервера это обычное дело</code></p>
     <p><code> while (1) {</code></p>
     <p><code>  // Получить и вывести сообщение</code></p>
     <p><code>  rcvid = MsgReceive(chid, message, sizeof(message), NULL);</code></p>
     <p><code>  printf("Получил сообщение, rcvid %X\n", rcvid);</code></p>
     <p><code>  printf("Сообщение такое: \"%s\".\n", message);</code></p>
     <p><code>  // Подготовить ответ — используем тот же буфер</code></p>
     <p><code>  strcpy(message, "Это ответ");</code></p>
     <p><code>  MsgReply(rcvid, EOK, message, sizeof(message));</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>Как видно из программы, функция <emphasis>MsgReceive()</emphasis> сообщает ядру о том, что она может обрабатывать сообщения размером вплоть до <code><strong>sizeof(message)</strong></code> (или 512 байт). Наш клиент (представленный выше) передал только 28 байт (длина строки). На приведенном ниже рисунке это и показано:</p>
     <image l:href="#img_26.png"/>
     <p>Передача меньшего объема данных, чем предполагается.</p>
     <p>Ядро реально передает <emphasis>минимум</emphasis> из двух указанных размеров. В нашем случае ядро передало бы 28 байт, сервер бы разблокировался и отобразил сообщение клиента. Оставшиеся 484 байта (из буфера длиной 512 байт) остались бы нетронутыми.</p>
     <p>Аналогичная ситуация с функцией <emphasis>MsgReply()</emphasis>. Функция <emphasis>MsgReply()</emphasis> информирует, что собирается передать 512 байт, но функция <emphasis>MsgSend()</emphasis> определила, что может принять максимум 200 байт. Ядро опять передает минимум. В этом случае 200 байтов, которые клиент может принять, ограничивают размер передачи. (Один интересный аспект здесь состоит в том, что когда сервер передаст данные, то если клиент не примет их целиком, как в нашем примере, их уже нельзя будет вернуть — они будут потеряны.).</p>
     <cite>
      <p><image l:href="#img_1.png"/>Имейте в виду, что такое «урезание» является стандартным и ожидаемым поведением.</p>
      <p>Когда мы будем обсуждать обмен сообщениями по сети, вы увидите, что в количестве передаваемых данных есть кое-какое «ага». Мы проанализируем это далее в разделе «Особенности обмена сообщениями в сети».</p>
     </cite>
    </section>
    <section>
     <title>
      <p>Иерархический принцип обмена (send-иерархия)</p>
     </title>
     <p>В обмене сообщениями есть одна вещь, которая, возможно, не является очевидной — это необходимость следовать строгой иерархии обмена. Означает это то, что два потока никогда не должны посылать сообщения друг другу; наоборот, они должны быть организованы так, что каждый поток занимал свой «уровень иерархии», и все потоки данного уровня должны посылать сообщения только потокам более низкого уровня, а не своего или высшего. Проблема с наличием двух потоков, которые посылают сообщения друг другу, заключается в том, что в конечном счете вы столкнетесь с проблемой взаимной блокировки (deadlock), когда оба потока ожидают друг от друга ответ на соответствующие сообщения. Поскольку эти потоки блокированы, то они никогда не будут поставлены на выполнение, а значит, не смогут дать друг другу ответ, и вы в результате получите два (а то и более!) зависших потока.</p>
     <p>Способ назначения потокам уровней иерархии заключается в том, чтобы разместить наиболее удаленную клиентуру на самом верхнем уровне и работать оттуда. Например, если у вас есть графический интерфейс пользователя, который использует некоторый сервер баз данных, который, в свою очередь, использует файловую систему, а файловая система использует блок-ориентированный драйвер файловой системы, то у вас получается естественная иерархия процессов.</p>
     <p>Передачи (sends) при обмене сообщениями будут направлены от клиента (графического интерфейса пользователя) вниз к серверам нижнего уровня; ответы на сообщения (replies) будут иметь встречное направление.</p>
     <p>При том, что это работает в большинстве случаев, вы можете столкнуться и с ситуацией, когда вам придется нарушить иерархию обмена. Это никогда не следует выполнять простым нарушением иерархии, направляя сообщения «против течения» — для этого существует функция <emphasis>MsgDeliverEvent()</emphasis>, о которой речь несколько позже.</p>
    </section>
    <section>
     <title>
      <p>Идентификаторы отправителя, каналы и другие параметры</p>
     </title>
     <p>Мы с вами пока не обсуждали различные параметры, используемые в ранее рассмотренных примерах, чтобы можно было сконцентрировать внимание на самих принципах обмена сообщениями. Теперь поговорим об этих параметрах более подробно.</p>
     <subtitle>Дополнительно о каналах</subtitle>
     <p>В приведенном выше примере с сервером мы видели, что сервер создал один-единственный канал. Конечно, можно было создать больше, но обычно серверы так не делают. (Наиболее очевидный пример сервера с двумя каналами — это администратор штатной сети <code><strong>qnet</strong></code> — вот уж определенно эксцентричный образец программного обеспечения !)</p>
     <p>Оказывается, что в действительности нет большой необходимости в создании нескольких каналов. Главное назначение канала состоит в том, чтобы четко указать серверу, где «слушать» на предмет входящих сообщений, и четко указать клиентам, куда передавать сообщения (через соответствующие соединения). Единственная ситуация, когда вам могло бы понадобиться использовать несколько каналов в сервере, — это если бы хотели реализовать сервер, предоставляющий различные услуги (или различные классы услуг) в зависимости от того, по какому каналу было принято сообщение. Второй канал мог бы применяться, например, для отправки сообщений типа «импульс», пробуждающих субсерверы — это гарантировало бы развязку этого сервиса от служебных функций, предоставляемых обычными сообщениями по первому каналу.</p>
     <p>В предыдущем параграфе я утверждал, что вы могли бы использовать в сервере пул потоков, готовый принимать сообщения от клиентов, и что реально не имеет значения, который именно из потоков в пуле получит запрос. Это еще один аспект «канальной абстракции». В предыдущих версиях QNX (особенно в QNX4), клиент мог передать сообщение серверу, определяя его идентификатором узла (node ID) и идентификатором процесса (process ID) на этом узле. Поскольку QNX4 — однопоточная ОС, никакого беспорядка с тем, кому передается сообщение, в ней быть не могло. Однако, стоит ввести понятие потока, и встает дополнительная проблема адресации потоков в процессе (ведь именно потоки собственно предоставляют сервисы). Поскольку поток — вещь преходящая, в действительности для клиента не имеет смысла подключаться к четко определенному потоку в четко определенном процессе на четко определенном узле. К тому же, а что если нужный поток занят? Мы тогда должны были бы обеспечить клиенту возможность выбрать «незанятый поток из некоторого пула потоков, предоставляющих нужный сервис».</p>
     <p>Так вот, для этого и существуют каналы. Канал — это «адрес» некоторого «пула потоков, предоставляющих нужный сервис». Суть здесь заключается в том, что вызвать функцию <emphasis>MsgReceive()</emphasis> по одному и тому же каналу могут несколько потоков одновременно. Все они будут блокированы, но входящее сообщение будет передано только одному из них.</p>
     <subtitle>Кто послал сообщение?</subtitle>
     <p>Довольно часто серверу необходимо знать, кто послал ему сообщение. Для этого есть ряд причин, например:</p>
     <p>• учет клиентов;</p>
     <p>• управление доступом;</p>
     <p>• определение контекстных связей;</p>
     <p>• выбор типа сервиса;</p>
     <p>• и т.д.</p>
     <p>Сделать так, чтобы клиент передавал серверу эту информацию с каждым сообщением, было бы излишне громоздким (да и давало бы лишние лазейки в системе защиты). Поэтому существует специальная структура, заполняемая ядром всякий раз, когда функция <emphasis>MsgReceive()</emphasis> разблокируется, приняв сообщение. Эта структура имеет тип <code><strong>struct _msg_info</strong></code> и содержит в себе следующее:</p>
     <p><code>struct _msg_info {</code></p>
     <p><code> int nd;</code></p>
     <p><code> int srcnd;</code></p>
     <p><code> pid_t pid;</code></p>
     <p><code> int32_t chid;</code></p>
     <p><code> int32_t scoid;</code></p>
     <p><code> int32_t coid;</code></p>
     <p><code> int32_t msglen;</code></p>
     <p><code> int32_t tid;</code></p>
     <p><code> int16_t priority;</code></p>
     <p><code> int16_t flags;</code></p>
     <p><code> int32_t srcmsglen;</code></p>
     <p><code>};</code></p>
     <p>Вы передаете все это функции <emphasis>MsgReceive()</emphasis> в качестве последнего параметра. Если вы передаете NULL, то не произойдет ничего. (Информацию все равно можно будет потом получить с помощью вызова функции <emphasis>MsgInfo()</emphasis> — она не теряется!)</p>
     <p>Давайте взглянем на поля этой структуры:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>nd</emphasis>, <emphasis>srcnd</emphasis>, <emphasis>pid</emphasis> и <emphasis>tid</emphasis></td>
       <td align="left" valign="top">Это дескриптор узла, идентификатор процесса и идентификатор потока клиента. (Заметьте, что <emphasis>nd </emphasis>— это дескриптор принимающего узла для режима передачи, a <emphasis>srcnd</emphasis> — это дескриптор передающего узла для режима приема. Для этого имеется очень серьезное основание ;-), которое мы рассмотрим ниже в разделе «Несколько замечаний о дескрипторах узлов»).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>priority</emphasis></td>
       <td align="left" valign="top">Приоритет потока, пославшего сообщение.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>chid</emphasis>, <emphasis>coid</emphasis></td>
       <td align="left" valign="top">Идентификатор канала, по которому сообщение было передано, и идентификатор использованного при этом соединения.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>scoid</emphasis></td>
       <td align="left" valign="top">Идентификатор соединения с сервером. Это внутренний идентификатор, который применяется ядром для маршрутизации сообщения от сервера назад к клиенту Вам не нужно ничего знать об этом идентификаторе, кроме одного любопытно факта, что это будет небольшое целое число, которое уникально идентифицирует клиента.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>flags</emphasis></td>
       <td align="left" valign="top">Содержит различные битовые флаги: _NTO_MI_ENDIAN_BIG, _NTO_MI_ENDIAN_DIFF, _NTO_MI_NET_CRED_DIRTY и _NTO_MI_UNBLOCK_REQ. Биты _NTO_MI_ENDIAN_BIG и _NTO_MI_ENDIAN_DIFF сообщат вам о порядке байт в слове для отправившей сообщение машины (в случае, если сообщение пришло через сеть от машины с другим порядком байт), бит _NTO_MI_NET_CRED_DIRTY зарезервирован для внутреннего использования, значение бита _NTO_MI_UNBLOCK_REQ мы рассмотрим в разделе «Использование бита _NTO_MI_UNBLOCK_REQ», см. ниже.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>msglen</emphasis></td>
       <td align="left" valign="top">Число принятых байт.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>srcmsglen</emphasis></td>
       <td align="left" valign="top">Длина исходного сообщения в байтах, как оно было отправлено клиентом. Это число может превышать значение <emphasis>msglen</emphasis> — например, в случае приема меньшего количества данных, чем было послано. Заметьте, что это поле действительно только в том случае, если установлен бит _NTO_CHF_SENDER_LEN в переданном функции <emphasis>ChannelCreate()</emphasis> (для канала, по которому было получено данное сообщение) параметре <emphasis>flags</emphasis>.</td>
      </tr>
     </table>
     <subtitle>Идентификатор отправителя (receive ID), он же клиентский жетон (client cookie)</subtitle>
     <p>В примере программы, представленном выше, отметьте следующее:</p>
     <p><code>rcvid = MsgReceive(...);</code></p>
     <p><code>...</code></p>
     <p><code>MsgReply(rcvid, ...);</code></p>
     <p>Это — ключевой фрагмент, потому что именно в нем иллюстрируется привязка приема сообщения от клиента к последующему ответу этому конкретному клиенту. Идентификатор отправителя — это целое число, которое действует как жетон («magic cookie»), который вы получаете от клиента и обязаны хранить, если вы желаете впоследствии взаимодействовать с этим клиентом. Что произойдет, если вы его потеряете? Его больше нет. Функция <emphasis>MsgSend()</emphasis> клиента не разблокируется, пока вы (конкретный сервер) живы, или пока не произошел тайм-аут обмена сообщениями (и даже в этом случае все не так просто; см. функцию <emphasis>TimerTimeout()</emphasis> в справочном руководстве по библиотеке Си и обсуждение о применения в главе «Часы, таймеры и периодические уведомления», раздел «Тайм-ауты ядра»).</p>
     <cite>
      <p><image l:href="#img_1.png"/>Не пытайтесь извлечь из значения идентификатора отправителя какой-либо конкретный смысл — он может измениться в будущих версиях операционной системы. Единственное, что нужно знать — что он уникален, то есть у вас никогда не будет двух различных клиентов с одним и тем же идентификатором отправителя (иначе ядро просто не сможет их различить, когда вы вызовете <emphasis>MsgReply()</emphasis>).</p>
      <p>Отметим также, что за исключением одного частного случая (с применением функции <emphasis>MsgDeliverEvent()</emphasis>, которую мы рассмотрим позже), после вызова функции <emphasis>MsgReply()</emphasis> соответствующий идентификатор отправителя перестает иметь смысл.</p>
     </cite>
     <p>Таким образом, мы плавно переходим к функции <emphasis>MsgReply()</emphasis>.</p>
     <subtitle>Ответ клиенту</subtitle>
     <p>Функция <emphasis>MsgReply()</emphasis> принимает в качестве параметров идентификатор отправителя, код возврата, указатель на сообщение и размер этого сообщения. Мы только что обсудили идентификатор отправителя — он уникально идентифицирует того, кому должно быть отправлено ответное сообщение. Код возврата указывает, какой код должна возвратить функция <emphasis>MsgSend()</emphasis> клиента. Наконец, указатель на сообщение и размер указывают на местоположение и размер (необязательного!) ответного сообщения, которое следует отправить.</p>
     <p>Функция <emphasis>MsgReply()</emphasis> может показаться очень простой (и так оно и есть), но рассмотреть ее применение было бы полезно.</p>
     <subtitle>А можно и не отвечать</subtitle>
     <p>Однако, вы вовсе не обязаны обязательно ответить клиенту перед приемом новых сообщений от других клиентов с помощью функции <emphasis>MsgReceive()</emphasis>! Это положение можно с успехом использовать в множестве различных сценариев.</p>
     <p>В типовом драйвере устройства клиент может выдать запросом, который не будет обслужен в течение продолжительного времени. Например, клиент может запросить драйвер аналого-цифрового преобразователя (АЦП): «Сходи-ка принеси мне данные за следующие 45 секунд.» Драйвер АЦП не может себе позволить вывесить табличку «Закрыто» на целых 45 секунд, потому что другим клиентам тоже может срочно что-нибудь понадобиться — например, данные по другому каналу, информация о состоянии, и т.п.</p>
     <p>В соответствии со своей архитектурой, драйвер АЦП просто поставит в очередь полученный от функции <emphasis>MsgReceive()</emphasis> идентификатор отправителя, осуществит запуск 45-секундного процесса накопления данных и снова вернется к обработке клиентских запросов. По истечении этого 45-секундного интервала, когда данные накоплены, драйвер АЦП сможет найти идентификатор отправителя, связанный с данным запросом, и ответить нужному клиенту.</p>
     <p>Вам также может понадобиться задержаться с ответом клиенту в случае модели «сервер/субсервер» (то есть некоторые клиенты — на самом деле субсерверы). Вы можете просто запомнить идентификаторы ищущих работу субсерверов и сохранить их до поры до времени. Когда работа для субсерверов появится, тогда и только тогда вы ответите субсерверу, указав, что именно он должен сделать.</p>
     <subtitle>Ответ без данных или с кодом ошибки (<emphasis>errno</emphasis>)</subtitle>
     <p>Когда дело наконец доходит до ответа клиенту, вы совершенно не обязаны передавать ему какие-либо данные. Это может использоваться в двух случаях.</p>
     <p>Вы можете отправить клиенту ответ без данных, если единственная цель ответа — разблокировать клиента. Скажем, клиент желает быть блокированным до некоторого события, а до какого именно — ему знать не обязательно. В этом случае функции <emphasis>MsgReply()</emphasis> не потребуется никаких данных, достаточно будет только идентификатора отправителя:</p>
     <p><code>MsgReply(rcvid, EOK, NULL, 0);</code></p>
     <p>Такой вызов разблокирует клиента (но не передаст ему никаких данных) и возвратит код EOK («успешное завершение»).</p>
     <p>Как вариант, вы можете при желании возвратить клиенту код ошибки. Вы не сможете сделать это с помощью функции <emphasis>MsgReply()</emphasis>, вместо нее для этого используется функция <emphasis>MsgError()</emphasis>:</p>
     <p><code>MsgError(rcvid, EROFS);</code></p>
     <p>В приведенном выше примере сервер обнаруживает, что клиент пытается записать данные в файловую систему, предназначенную только для чтения, и вместо данных возвращает клиенту код ошибки (<emphasis>errno</emphasis>) EROFS.</p>
     <p>Еще одним поводом ответить клиенту без данных (и соответствующие вызовы мы вскоре рассмотрим) может быть то, что данные уже переданы ранее (с помощью функции <emphasis>MsgWrite()</emphasis>), и больше никаких данных нет.</p>
     <p>Почему применяются два типа вызовов? Они немного различны. В то время как обе функции <emphasis>MsgError()</emphasis> и <emphasis>MsgReply()</emphasis> разблокируют клиента, функция <emphasis>MsgError()</emphasis> при этом не передаст никаких данных, заставит функцию <emphasis>MsgSend()</emphasis> клиента возвратить -1 и установит переменную <emphasis>errno</emphasis> на стороне клиента в значение, переданное функции <emphasis>MsgError()</emphasis> в качестве второго аргумента.</p>
     <p>С другой стороны, функция <emphasis>MsgReply()</emphasis> может передавать данные (как видно из ее третьего и четвертого параметров) и заставляет функцию <emphasis>MsgSend()</emphasis> клиента возвратить значение, переданное <emphasis>MsgReply()</emphasis> в качестве второго аргумента. Переменная <emphasis>errno</emphasis> клиента остается нетронутой.</p>
     <p>В общем случае, если вам нужно только сообщить о результатах действия («прошло/не прошло»), лучше применять функцию <emphasis>MsgError()</emphasis>. Если бы вы возвращали данные, здесь была бы необходима функция <emphasis>MsgReply()</emphasis>. Обычно, когда вы возвращаете данные, вторым параметром функции <emphasis>MsgReply()</emphasis> будет положительное целое число, указывающее на число возвращаемых байт.</p>
     <subtitle>Определение идентификаторов узла, процесса и канала (ND/PID/CHID) нужного сервера</subtitle>
     <p>Ранее мы отметили, что для соединения с сервером функции <emphasis>ConnectAttach()</emphasis> необходимо указать дескриптор узла (Node Descriptor — ND), идентификатор процесса (process ID — PID), а также идентификатор канала (Channel ID — CHID). До настоящего момента мы не обсуждали, как именно клиент находит эту информацию.</p>
     <p>Если один процесс создает другой процесс, тогда это просто — вызов создания процесса возвращает идентификатор вновь созданного процесса. Создающий процесс может либо передать собственные PID и CHID вновь созданному процессу в командной строке, либо вновь созданный процесс может вызвать функцию <emphasis>getppid()</emphasis> для получения идентификатора родительского процесса, и использовать некоторый «известный» идентификатор канала.</p>
     <p>А что если у нас два совершенно чужих процесса? Это возможно, например, в том случае, если сервер создан некоей третьей стороной, а вашему приложению нужно уметь общаться с этим сервером. Реально мы должны найти ответ на вопрос: «Как сервер объявляет о своем местонахождении?»</p>
     <p>Существует множество способов сделать это; мы рассмотрим только три из них, в порядке возрастания «элегантности»:</p>
     <p>1. Открыть файла с известным именем и сохранить в нем ND/PID/CHID. Такой метод является традиционным для серверов UNIX, когда сервер открывает файл (например, <code><strong>/etc/httpd.pid</strong></code>), записывает туда свой идентификатор процесса в виде строки ASCII и предполагают, что клиенты откроют этот файл прочитают из него идентификатор.</p>
     <p>2. Использовать для объявления идентификаторов ND/PID/CHID глобальные переменные. Такой способ обычно применяется в многопоточных серверах, которые могут посылать сообщение сами себе. Этот вариант по самой своей природе является очень редким.</p>
     <p>3. Занять часть пространства имен путей и стать администратором ресурсов. Мы поговорим об этом в главе «Администраторы ресурсов».</p>
     <p>Первый подход относительно прост, но он чреват «загрязнением файловой системы», когда в каталоге <code><strong>/etc</strong></code> лежит куча файлов <code><strong>*.pid</strong></code>. Поскольку файлы устойчивы (имеется в виду, что они выживают после смерти создающего их процесса и перезагрузки машины), очевидного способа стереть эти файлы не существует — разве что использовать этакую программную «старуху с косой», постоянно проверяющую, не пора ли прибрать кого-то из них.</p>
     <p>Имеется и другая связанная с этим подходом проблема. Поскольку процесс, который создал файл, может умереть, не удалив этот файл, то вы не сможете узнать, жив ли еще этот процесс, пока не попробуете передать ему сообщение. И это ещё не самое страшное — еще хуже, если комбинация ND/PID/CHID указанная в файле, оказывается настолько старой, что может быть повторно использована другой программой! Получив «чужое» сообщение, эта программа в лучшем случае его проигнорирует его, а ведь может и предпринять некорректные действия. Так что такой подход исключается.</p>
     <p>Второй подход, где мы используем глобальные переменные для объявления значений ND/PID/CHID, не является общим решением проблемы, поскольку в нем предполагается способность клиента обратиться к этим глобальным переменным. А поскольку для этого требуется использование разделяемой памяти, это не будет работать в сети! Так что этот метод обычно используется либо в небольших тестовых программах, либо в очень специфичных случаях, но всегда в контексте многопоточной программы.</p>
     <p>Что реально происходит, так это то, что один поток в программе является клиентом, а другой поток — сервером. Поток-сервер создает канал и затем размещает идентификатор канала в глобальной переменной (идентификаторы узла и процесса являются одинаковыми для всех потоков в процессе, так что объявлять их не обязательно). Поток-клиент затем берет этот идентификатор канала и выполняет по нему функцию <emphasis>ConnectAttach()</emphasis>.</p>
     <p>Третий подход — сделать сервер администратором ресурса — является определенно самым прозрачным и поэтому рекомендуемым общим решением. Механизм того, как это делается, изложен в главе «Администраторы ресурсов», а пока все, что вы должны об этом знать — это то, что сервер регистрирует некое имя пути как свою «область ответственности», а клиенты обращаются к нему обычным вызовом функции <emphasis>open()</emphasis>.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Не сочту лишним подчеркнуть:</p>
      <p>Файловые дескрипторы POSIX в QNX/Neutrino реализованы через идентификаторы соединений, то есть дескриптор файла уже является идентификатором соединения! Органичность этой схемы в том, что поскольку дескриптор файла, возвращаемый функцией <emphasis>open()</emphasis>, фактически является идентификатором соединения, клиенту не нужно выполнять какие-либо дополнительные действия, чтобы использовать это соединение. Например, когда клиент после вызова <emphasis>open()</emphasis> вызывает функцию <emphasis>read()</emphasis>, передавая ей полученный дескриптор, это с минимальными накладными расходами транслируется в функцию <emphasis>MsgSend()</emphasis>.</p>
     </cite>
     <subtitle>А что насчет приоритетов?</subtitle>
     <p>А что произойдет, если сообщение серверу передадут одновременно два процесса с разными приоритетами?</p>
     <cite>
      <p><image l:href="#img_1.png"/>Сообщения всегда доставляются в порядке приоритетов.</p>
      <p>Если два процесса посылают сообщения «одновременно», первым доставляется сообщение от процесса с высшим приоритетом.</p>
      <p>Если оба процесса имеют одинаковый приоритет, то сообщения будут доставлены в порядке отправки (поскольку в машине с одним процессором не бывает ничего одновременного, и даже в SMP-блоке будет присутствовать некий порядок, поскольку процессоры будут конкурировать между собой за доступ к ядру).</p>
     </cite>
     <p>Мы еще вернемся к анализу других тонкостей этой проблемы чуть позже в этой главе, когда будем говорить о проблеме инверсии приоритетов.</p>
     <subtitle>Чтение и запись данных</subtitle>
     <p>До настоящего времени мы обсуждали основные примитивы обмена сообщениями. Как я и упоминал ранее, это минимум, который необходимо знать. Однако существует еще несколько дополнительных функций, которые делают нашу жизнь значительно проще.</p>
     <p>Рассмотрим пример, в котором для обеспечения обмена сообщениями между клиентом и сервером нам понадобились бы и другие функции.</p>
     <p>Клиент вызывает <emphasis>MsgSend()</emphasis> для передачи неких данных серверу. После вызова <emphasis>MsgSend()</emphasis> клиент блокируется. Теперь он ждет, чтобы сервер ему ответил.</p>
     <p>Интересные события разворачиваются на стороне сервера. Сервер вызывает функцию <emphasis>MsgReceive()</emphasis> для приема сообщения от клиента. В зависимости от того, как вы спроектировали вашу систему сообщений, сервер может знать, а может и не знать, насколько велико сообщение клиента. Как сервер может не знать, каков реальный размер сообщения? Возьмем наш пример с файловой системой. Предположим, что клиент делает так:</p>
     <p><code>write(fd, buf, 16);</code></p>
     <p>Это сработает так, как и ожидается, если сервер вызовет <emphasis>MsgReceive()</emphasis> с размером буфера, скажем, 1024 байта. Так как наш клиент послал небольшое сообщение (28 байт), никаких проблем не будет.</p>
     <p>А что если клиент отправит сообщение, превышающее по размеру 1024 байт — скажем, 1 мегабайт? Например, так:</p>
     <p><code>write(fd, buf, 1000000);</code></p>
     <p>Как сервер мог бы обработать это сообщение поизящнее? Мы могли, к примеру, сказать, что клиенту не позволяется записывать более чем n байт. Тогда функции <emphasis>write()</emphasis> в клиентской Си-библиотеке пришлось бы разбивать каждый «длинный» запрос на несколько запросов по <emphasis>n</emphasis> байт каждый. Неуклюже.</p>
     <p>Другая проблема в этом примере заключается в вопросе «А каково должно быть <emphasis>n</emphasis>?»</p>
     <p>Как вы видите, этот подход имеет следующие основные недостатки:</p>
     <p>• Все функции, которые применяются для обмена сообщениями ограниченного размера, должны быть модифицированы в Си- библиотеке так, чтобы функция передавала запросы в виде серии пакетов. Это само по себе немалый объем работы. Также это может иметь ряд неожиданных побочных эффектов при работе в мнопоточной среде — что если первая часть сообщения от одного потока передана, и тут его вытесняет другой поток клиента и посылает свое собственное сообщение. Что будет с прерванным потоком тогда?</p>
     <p>• Все серверы должны быть готовы к обработке сообщения максимально возможного размера. Это означает, что все серверы должны будут иметь значительные области данных, или Си-библиотека будет должна разделять большие запросы на несколько меньших, ухудшая тем самым быстродействие.</p>
     <p>К счастью, эта проблема довольно просто обходится, причем даже с дополнительным выигрышем.</p>
     <p>Здесь будут особенно полезны функции <emphasis>MsgRead()</emphasis> и <emphasis>MsgWrite()</emphasis>. Важно при этом помнить, что клиент блокирован — это означает, что он не собирается изменять данные, пока сервер их анализирует.</p>
     <cite>
      <p><image l:href="#img_1.png"/>В многопоточном клиенте теоретически возможно, что в область данных заблокированного по серверу клиентского потока залезет другой поток. Такая ситуация рассматривается как некорректная (ошибка проектирования), поскольку серверный поток предполагает, что он имеет монопольный доступ к области данных клиента, пока тот заблокирован.</p>
     </cite>
     <p>Функция <emphasis>MsgRead()</emphasis> описана так:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int MsgRead(int rcvid, void *msg, int nbytes, int offset);</code></p>
     <p>Функция <emphasis>MsgRead()</emphasis> позволяет Вашему серверу считать <emphasis>nbytes</emphasis> байт данных из адресного пространства заблокированного клиента, начиная со смещения <emphasis>offset</emphasis> от начала клиентского буфера, в буфер, указанный параметром <emphasis>msg</emphasis>. Сервер не блокируется, а клиент не разблокируется. Функция <emphasis>MsgRead()</emphasis> возвращает число байтов, которые были фактически считаны, или возвращает -1, если произошла ошибка.</p>
     <p>Итак, давайте подумаем, как бы мы использовали эти возможности в нашем примере с вызовом <emphasis>write()</emphasis>. Библиотечная функция <emphasis>write()</emphasis> создает сообщение с заголовком и посылает его серверу файловой системы <code><strong>fs-qnx4</strong></code>. Сервер принимает небольшую часть сообщения с помощью <emphasis>MsgReceive()</emphasis>, анализирует его и принимает решение, где разместить остальную часть сообщения — например, где-то в уже выделенном буфере дискового кэша.</p>
     <p>Давайте рассмотрим пример.</p>
     <image l:href="#img_27.png"/>
     <p>Пример отправки сообщения серверу fs-qnx4 с непрерывным представлением данных.</p>
     <p>Итак, клиент решил переслать файловой системе 4Кб данных. (Отметьте для себя, что Си-библиотека добавила к сообщению перед данными небольшой заголовок — чтобы потом можно было узнать, к какому типу принадлежал этот запрос. Мы еще вернемся к этому вопросу, когда будем говорить о составных сообщениях, а также — еще более детально — когда будем анализировать работу администраторов ресурсов.) Файловая система считывает только те данные (заголовок), которые будут ей необходимы для того, чтобы выяснить тип принятого сообщения:</p>
     <p><code>// Часть заголовков, вымышлены для примера</code></p>
     <p><code>struct _io_write {</code></p>
     <p><code> uint16_t type;</code></p>
     <p><code> uint16_t combine_len;</code></p>
     <p><code> int32_t nbytes;</code></p>
     <p><code> uint32_t xtype;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> uint16_t type;</code></p>
     <p><code> struct _io_read io_read;</code></p>
     <p><code> struct _io_write io_write;</code></p>
     <p><code> ...</code></p>
     <p><code>} header_t;</code></p>
     <empty-line/>
     <p><code>header_t header; // Объявить заголовок</code></p>
     <empty-line/>
     <p><code>rcvid = MsgReceive(chid, &amp;header, sizeof(header), NULL);</code></p>
     <p><code>switch (header.type) {</code></p>
     <p><code> ...</code></p>
     <p><code>case _IO_WRITE:</code></p>
     <p><code> number_of_bytes = header.io_write.nbytes;</code></p>
     <p><code> ...</code></p>
     <p>Теперь сервер <code><strong>fs-qnx4</strong></code> знает, что в адресном пространстве клиента находится 4Кб данных (сообщение известило его об этом через элемент структуры <emphasis>nbytes</emphasis>), и что эти данные надо передать в буфер кэша. Теперь сервер <code><strong>fs-qnx4</strong></code> может сделать так:</p>
     <p><code>MsgRead(rcvid, cache_buffer[index].data,</code></p>
     <p><code> cache_buffer[index].size, sizeof(header.io_write));</code></p>
     <p>Обратите внимание, что операции приема сообщения задано смещение <code><strong>sizeof(header.io_write)</strong></code> — это сделано для того, чтобы пропустить заголовок, добавленный клиентской библиотекой. Мы предполагаем здесь, что <code><strong>cache_buffer[index].size</strong></code> (размер буфера кэша) равен 4096 (или более) байт.</p>
     <p>Для записи данных в адресное пространство клиента есть аналогичная функция:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int MsgWrite(int rcvid, const void *msg, int nbytes,</code></p>
     <p><code> int offset);</code></p>
     <p>Применение функции <emphasis>MsgWrite()</emphasis> позволяет серверу записать данные в адресное пространство клиента, начиная со смещения <emphasis>offset</emphasis> байт от начала указанного клиентом приемного буфера. Эта функция наиболее полезна в случаях, где сервер ограничен в ресурсах, а клиент желает получить от него значительное количество информации.</p>
     <p>Например, в системе сбора данных клиент может выделить 4-мегабайтный буфер и приказать драйверу собрать 4 мегабайта данных. Драйверу вовсе не обязательно держать под боком здоровенный буфер просто так, на случай если кто-то вдруг неожиданно запросит передачу большого массива данных.</p>
     <p>Драйвер может иметь буфер размером 128Кб для обмена с аппаратурой посредством DMA, а сообщение пересылать в адресное пространство клиента по частям, используя функцию <emphasis>MsgWrite()</emphasis> (разумеется, каждый раз увеличивая смещение на 128Кб). Когда будет передан последний фрагмент, можно будет вызывать <emphasis>MsgReply()</emphasis>.</p>
     <image l:href="#img_28.png"/>
     <p>Передача нескольких фрагментов сообщения с помощью функции <emphasis>MsgWrite()</emphasis></p>
     <p>Отметим, что функция <emphasis>MsgWrite()</emphasis> позволяет вам записать различные компоненты данных в различные места, а затем либо просто разбудить клиента вызовом <emphasis>MsgReply()</emphasis>:</p>
     <p><code>MsgReply(rcvid, EOK, NULL, 0);</code></p>
     <p>либо сделать это после записи заголовка в начало клиентского буфера:</p>
     <p><code>MsgReply(rcvid, EOK, &amp;header, sizeof(header));</code></p>
     <p>Это довольно изящный трюк для записи неизвестного количества данных, когда вы узнаете, сколько данных нужно было записать, только когда запись уже закончена. Главное — если вы будете использовать второй метод, с записью заголовка после записи данных, не забудьте зарезервировать место под заголовок в начале клиентского буфера!</p>
    </section>
    <section>
     <title>
      <p>Составные сообщения</p>
     </title>
     <p>До сих пор мы демонстрировали только обмен сообщениями, когда данные передаются из одного буфера в адресном пространстве клиента в другой буфер в адресном пространстве сервера (и наоборот — в случае ответа на сообщение).</p>
     <p>При том, что данный подход вполне приемлем для большинства приложений, его применение далеко не всегда эффективно. Вспомните: наша функция <emphasis>write()</emphasis> из Си-библиотеки берет переданный ей буфер и добавляет в его начало небольшой заголовок. Используя то, что мы уже изучили ранее, вы могли бы ожидать, что реализация <emphasis>write()</emphasis> в Си-библиотеке может выглядеть примерно так (это не реальный код!):</p>
     <p><code>ssize_t write(int fd, const void *buf, size_t nbytes) {</code></p>
     <p><code> char *newbuf;</code></p>
     <p><code> io_write_t *wptr;</code></p>
     <p><code> int nwritten;</code></p>
     <p><code> newbuf = malloc(nbytes + sizeof(io_write_t));</code></p>
     <p><code> // Заполнить write_header</code></p>
     <p><code> wptr = (io_write_t*)newbuf;</code></p>
     <p><code> wptr-&gt;type = _IO_WRITE;</code></p>
     <p><code> wptr-&gt;nbytes = nbytes;</code></p>
     <p><code> // Сохранить данные от клиента</code></p>
     <p><code> memcpy(newbuf + sizeof(io_write_t), buf, nbytes);</code></p>
     <p><code> // Отправить сообщение серверу</code></p>
     <p><code> nwritten =</code></p>
     <p><code>  MsgSend(fd, newbuf, nbytes + sizeof(io_write_t),</code></p>
     <p><code>  newbuf, sizeof(io_write_t));</code></p>
     <p><code> free(newbuf);</code></p>
     <p><code> return(nwritten);</code></p>
     <p><code>}</code></p>
     <p>Понимаете, что произошло? Несколько неприятных вещей:</p>
     <p>• Функция <emphasis>write()</emphasis> теперь должна быть способна выделить память под буфер достаточно большого размера как для данных клиента (которые могут быть довольно значительными по объему), так и для заголовка. Размер заголовка не имеет значения — в этом случае он был равен 12 байтам.</p>
     <p>• Мы были должны скопировать данные дважды: в первый раз — при использовании функции <emphasis>memcpy()</emphasis>, и затем еще раз, снова — уже при осуществлении передачи сообщения.</p>
     <p>• Мы должны были предусмотреть указатель на тип <code><strong>io_write_t</strong></code> и установить его на начало буфера, вместо использования обычных механизмов доступа (впрочем, это незначительный недостаток).</p>
     <p>Поскольку ядро намерено копировать данные в любом случае, было бы хорошо, если бы мы смогли сообщить ему о том, что одна часть данных (заголовок) фиксирована по некоторому адресу, а другая часть (собственно данные) фиксирована где- нибудь еще, без необходимости самим вручную собирать буферы из частей и копировать данные.</p>
     <p>На наше счастье, в QNX/Neutrino реализован механизм, который позволяет нам сделать именно так! Механизм этот называется IOV (i/o vector), или «вектор ввода/вывода».</p>
     <p>Давайте для начала рассмотрим некоторую программу, а затем обсудим, что происходит с применением такого вектора.</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>ssize_t write(int fd, const void *buf, size_t nbytes) {</code></p>
     <p><code> io_write_t whdr;</code></p>
     <p><code> iov_t iov[2];</code></p>
     <p><code> // Установить IOV на обе части:</code></p>
     <p><code> SETIOV(iov + 0, &amp;whdr, sizeof(whdr));</code></p>
     <p><code> SETIOV(iov + 1, buf, nbytes);</code></p>
     <p><code> // Заполнить io_write_t</code></p>
     <p><code> whdr.type = _IO_WRITE;</code></p>
     <p><code> whdr.nbytes = nbytes;</code></p>
     <p><code> // Отправить сообщение серверу</code></p>
     <p><code> return (MsgSendv(coid, iov, 2, iov, 1));</code></p>
     <p><code>}</code></p>
     <p>Прежде всего, обратите внимание на то, что не применяется никакой функции <emphasis>malloc()</emphasis> и никакой функции <emphasis>memcpy()</emphasis>. Затем обратим внимание на тип применяемого вектора IOV — <code><strong>iov_t</strong></code>. Это структура, которая содержит два элемента — адрес и длину. Мы определили массив из двух таких структур и назвали его <emphasis>iov</emphasis>.</p>
     <p>Определение типа вектора <code><strong>iov_t</strong></code> содержится в <code><strong>&lt;sys/neutrino.h&gt;</strong></code> и выглядит так:</p>
     <p><code>typedef struct iovec {</code></p>
     <p><code> void *iov_base;</code></p>
     <p><code> size_t iov_len;</code></p>
     <p><code>} iov_t;</code></p>
     <p>Мы заполняем в этой структуре пары «адрес — длина» для заголовка операции записи (первая часть) и для данных клиента (вторая часть). Существует удобная макрокоманда, <emphasis>SETIOV()</emphasis>, которая выполняет за нас необходимые присвоения. Она формально определена следующим образом:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>#define SETIOV(_iov, _addr, _len) \</code></p>
     <p><code> ((_iov)-&gt;iov_base = (void *)(_addr), \</code></p>
     <p><code> (_iov)-&gt;iov_len = (_len))</code></p>
     <p>Макрос <emphasis>SETIOV()</emphasis> принимает вектор <code><strong>iov_t</strong></code>, а также адрес и данные о длине, которые подлежат записи в вектор IOV.</p>
     <p>Также отметим, что как только мы создаем IOV для указания на заголовок, мы сможем выделить стек для заголовка без использования <emphasis>malloc()</emphasis>. Это может быть и хорошо, и плохо — это хорошо, когда заголовок невелик, потому что вы хотите исключить головные боли, связанные с динамическим распределением памяти, но это может быть плохо, когда заголовок очень велик, потому что тогда он займет слишком много стекового пространства. Впрочем, заголовки обычно невелики.</p>
     <p>В любом случае, вся важная работа выполняется функцией <emphasis>MsgSendv()</emphasis>, которая принимает почти те же самые аргументы, что и функция <emphasis>MsgSend()</emphasis>, которую мы использовали в предыдущем примере:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int MsgSendv(int coid, const iov_t *siov, int sparts,</code></p>
     <p><code> const iov_t *riov, int rparts);</code></p>
     <p>Давайте посмотрим на ее аргументы:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>coid</emphasis></td>
       <td align="left" valign="top">Идентификатор соединения, по которому мы передаем — как и при использовании функции <emphasis>MsgSend()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sparts</emphasis> и <emphasis>rparts</emphasis></td>
       <td align="left" valign="top">Число пересылаемых и принимаемых частей, указанных параметрами вектора <code><strong>iov_t</strong></code>; в нашем примере мы присваиваем аргументу <emphasis>sparts</emphasis> значение 2, указывая этим, что пересылаем сообщение из двух частей, а аргументу <emphasis>rparts</emphasis> — значение 1, указывая этим, что мы принимаем ответ из одной части.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>siov</emphasis> и <emphasis>riov</emphasis></td>
       <td align="left" valign="top">Эти массивы значений типа <code><strong>iov_t</strong></code> указывают на пары «адрес — длина», которые мы желаем переслать. В вышеупомянутом примере мы выделяем <emphasis>siov</emphasis> из двух частей, указывая ими на заголовок и данные клиента, и <emphasis>riov</emphasis> из одной части, указывая им только на заголовок.</td>
      </tr>
     </table>
     <image l:href="#img_29.png"/>
     <p>Как ядро видит составное сообщение.</p>
     <p>Ядро просто прозрачно копирует данные из каждой части вектора IOV из адресного пространства клиента в адресное пространство сервера (и обратно, при ответе на сообщение). Фактически, при этом ядро выполняет операцию фрагментации/дефрагментации сообщения (scatter/gather).</p>
     <p>Несколько моментов, которые необходимо запомнить:</p>
     <p>• Число фрагментов ограничено значением 2<sup>31</sup> (больше, чем вам придется использовать!); число 2 в нашем примере — типовое значение.</p>
     <p>• Ядро просто копирует данные, указанные вектором IOV, из одного адресного пространства в другое.</p>
     <p>• <emphasis>Вектор-источник и вектор-приемник не должны совпадать</emphasis>.</p>
     <p>Почему последний пункт так важен? Для того чтобы ответить, рассмотрим все подробнее. Со стороны клиента, скажем, мы выдали:</p>
     <p><code>write(fd, buf, 12000);</code></p>
     <p>в результате чего был создан вектор IOV из двух частей:</p>
     <p>• заголовок (12 байт);</p>
     <p>• данные (12000 байт);</p>
     <p>На стороне сервера (скажем, сервера файловой системы <code><strong>fs-qnx4</strong></code>) мы имеем блоки памяти кэша до 4Кб каждый, и мы хотели бы эффективно принять сообщение непосредственно в эти блоки. В идеале мы бы написали что-то типа:</p>
     <p><code>// Настроить структуру IOV для приема:</code></p>
     <p><code>SETIOV(iov + 0, &amp;header, sizeof(header.io_write));</code></p>
     <p><code>SETIOV(iov + 1, &amp;cache_buffer[37], 4096);</code></p>
     <p><code>SETIOV(iov + 2, &amp;cache_buffer[16], 4096);</code></p>
     <p><code>SETIOV(iov + 3, &amp;cache_buffer[22], 4096);</code></p>
     <p><code>rcvid = MsgReceivev(chid, iov, 4, NULL);</code></p>
     <p>Эта программа делает в значительной степени то, что вы и предполагаете: она задает вектор IOV из 4 частей, первая из которых указывает на заголовок, а следующие три части — на блоки кэш-памяти с номерами 37, 16 и 22. (Предположим, что именно эти блоки случайно оказались доступными в данный момент.) Ниже это иллюстрируется графически.</p>
     <image l:href="#img_30.png"/>
     <p>Распределение непрерывных данных по отдельным буферам.</p>
     <p>Затем осуществляется вызов функции <emphasis>MsgReceivev()</emphasis>, и ей указывается, что мы намерены принять сообщение по указанному каналу (параметр <emphasis>chid</emphasis>), и что вектор IOV для этой операции состоит из 4 частей.</p>
     <p>(Кроме возможности работать с векторами IOV, функция <emphasis>MsgReceivev()</emphasis> действует аналогично функции <emphasis>MsgReceive()</emphasis>.)</p>
     <p>Опа! Мы сделали ту же самую ошибку, которую уже делали к раньше, когда знакомились с функцией <emphasis>MsgReceive()</emphasis>. Как мы узнаем, сообщение какого типа мы собираемся принять и сколько в нем данных, пока не примем все сообщение целиком?</p>
     <p>Мы сможем решить эту проблему тем же способом, что и прежде:</p>
     <p><code>rcvid = MsgReceive(chid, &amp;header, sizeof(header), NULL);</code></p>
     <p><code>switch (header.message_type) {</code></p>
     <p><code> ...</code></p>
     <p><code>case _IO_WRITE:</code></p>
     <p><code> number_of_bytes = header.io_write.nbytes;</code></p>
     <p><code> // Выделить / найти элемент кэша</code></p>
     <p><code> // Заполнить элементами кэша 3-элементный IOV</code></p>
     <p><code> MsgReadv(rcvid, iov, 3, sizeof(header.io_write));</code></p>
     <p>Здесь мы вызываем «предварительную» <emphasis>MsgReceive()</emphasis> (отметьте, что тут мы не используем ее векторную форму, поскольку для сообщения, состоящего из одной части, в ней просто нет необходимости), определяем тип сообщения и затем продолжаем считывать данные из адресного пространства клиента (начиная со смещения <code><strong>sizeof(header.io_write)</strong></code>) в кэш-буферы, определенные трехэлементным вектором IOV.</p>
     <p>Обратите внимание, что мы перешли от вектора IOV, состоящего из 4 частей (как в первом примере), к вектору IOV из 3 частей. Дело в том, что в первом примере первый из четырех элементов вектора IOV отводился под заголовок, который на этот раз мы считали непосредственно при помощи функции <emphasis>MsgReceive()</emphasis>, а последние три элемента аналогичны трехэлементному вектору из второго примера — они определяют место, куда мы хотим записать данные.</p>
     <p>Можно представить, как мы ответили бы на запрос чтения:</p>
     <p>1. Найти элементы кэша, которые соответствуют запрашиваемым данным.</p>
     <p>2. Заполнить вектора IOV ссылками на них.</p>
     <p>3. Применить функцию <emphasis>MsgWritev()</emphasis> (или <emphasis>MsgReplyv())</emphasis> для передачи данных клиенту.</p>
     <p>Отметим, что если данные начинаются не непосредственно с начала блока кэша (или другой структуры данных), то в этом нет никакой проблемы. Просто сместите первый вектор IOV на точку начала данных и соответственно откорректируйте поле размера.</p>
     <subtitle>Как насчет других версий?</subtitle>
     <p>Все функции обмена сообщениями, кроме функций семейства <emphasis>MsgSend*()</emphasis>, имеют одинаковую общую форму: если имя функции имеет суффикс «v», значит, она принимает в качестве аргументов вектор IOV и число его частей; в противном случае, она принимает указатель и длину.</p>
     <p>Семейство <emphasis>MsgSend*()</emphasis> содержит четыре основных варианта реализации функций с точки зрения буферов источника и адресата, плюс два варианта собственно системного вызова — итого восемь.</p>
     <p>В нижеприведенной таблице сведены данные о вариантах функций семейства <emphasis>MsgSend*()</emphasis>.</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Функция</th>
       <th align="left" valign="top">Буфер передачи</th>
       <th align="left" valign="top">Буфер приема</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>MsgSend()</emphasis></td>
       <td align="left" valign="top">линейный</td>
       <td align="left" valign="top">линейный</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>MsgSendnc()</emphasis></td>
       <td align="left" valign="top">линейный</td>
       <td align="left" valign="top">линейный</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>MsgSendsv()</emphasis></td>
       <td align="left" valign="top">линейный</td>
       <td align="left" valign="top">IOV</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>MsgSendsvnc()</emphasis></td>
       <td align="left" valign="top">линейный</td>
       <td align="left" valign="top">IOV</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>MsgSendvs()</emphasis></td>
       <td align="left" valign="top">IOV</td>
       <td align="left" valign="top">линейный</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>MsgSendvsnc()</emphasis></td>
       <td align="left" valign="top">IOV</td>
       <td align="left" valign="top">линейный</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>MsgSendv()</emphasis></td>
       <td align="left" valign="top">IOV</td>
       <td align="left" valign="top">IOV</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>MsgSendvnc()</emphasis></td>
       <td align="left" valign="top">IOV</td>
       <td align="left" valign="top">IOV</td>
      </tr>
     </table>
     <p>Под линейным буфером я подразумеваю, что передается единый буфер типа <code><strong>void*</strong></code> вместе с его длиной. Это легко запомнить: суффикс «v» означает «вектор», и он находится на том же самом месте, что и соответствующий параметр — первым или вторым, в зависимости от того, какой буфер — передачи или приема — объявляется векторным.</p>
     <p>Хмм. Получается, что функции <emphasis>MsgSendsv()</emphasis> и <emphasis>MsgSendsvnc()</emphasis> идентичны? Да, по части параметров именно так оно и есть. Различие заключается в том, является функция точкой завершения (cancellation point) или нет. Версии с суффиксом «nc» (<emphasis>«no cancellation» — прим. ред.</emphasis>) не являются точками завершения, в то время как версии без этого суффикса — являются. (Дополнительную информацию относительно точек завершения и завершаемости (cancelability) вообще можно найти в справочном руководстве по Си-библиотеке в главе, посвященной <emphasis>pthread_cancel()</emphasis>.)</p>
     <subtitle>Реализация</subtitle>
     <p>Вероятно, вы уже подозревали, что все варианты функций <emphasis>MsgRead()</emphasis>, <emphasis>MsgReceive()</emphasis>, <emphasis>MsgSend()</emphasis> и функций <emphasis>MsgWrite()</emphasis> тесно связаны между собой. (Единственное исключение — функция <emphasis>MsgReceivePulse()</emphasis>; мы ее вкратце рассмотрим.)</p>
     <p>Которые из этих функций следует применять? В общем-то вопрос этот является чисто философским. Что до меня лично, то я предпочитаю комбинировать.</p>
     <p>Если мы посылаем или принимаем только одноэлементные сообщения, то зачем нам все эти проблемы с настройкой векторов IOV?</p>
     <p>Накладные расходы (кстати, незначительные) по загрузке процессора обычно не зависят от того, настраиваете ли вы все сами или оставляете это ядру или библиотеке. Подход с использованием одноэлементных сообщений избавляет ядро от необходимости манипуляций с адресным пространством и поэтому работает несколько быстрее.</p>
     <p>Следует ли вам применять функции, использующие IOV? Конечно! Используйте их всегда, когда вам приходится самостоятельно программировать обмен многоэлементными сообщениями. <emphasis>Никогда</emphasis> непосредственно не копируйте данные при передаче многоэлементных сообщений, даже если для этого потребуется всего несколько строк программы. Это перегрузит систему попытками минимизировать число реальных операций копирования данных туда-сюда; передача указателей происходит намного быстрее, чем копирование данных из буфера в буфер.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Сообщения типа «импульс» (pulse)</p>
    </title>
    <section>
     <p>Все сообщения, которые мы обсуждали до настоящего времени, блокируют клиента. Как только клиент вызывает функцию <emphasis>MsgSend()</emphasis>, для него наступает тихий час. Клиент отдыхает до тех пор, пока сервер не ответит на сообщение.</p>
     <p>Однако есть ситуации, где отправитель сообщения не может себе позволить блокироваться. Мы рассмотрим некоторые из них в главах «Прерывания» и «Часы, таймеры и периодические уведомления», а сейчас мы должны понять концепцию данной проблемы.</p>
     <p>Механизм, который обеспечивает отправку сообщения без блокирования, называют «импульсом» (pulse). Импульс — это миниатюрное сообщение, которое:</p>
     <p>• может перенести 40 бит полезной информации (8-битный код и 32 бита данных);</p>
     <p>• является неблокирующим для отправителя;</p>
     <p>• может быть получено точно так же, как и сообщение другого типа;</p>
     <p>• ставится в очередь, если получатель не заблокирован в ожидании сообщения.</p>
    </section>
    <section>
     <title>
      <p>Прием импульса</p>
     </title>
     <p>Прием импульса выполняется очень просто: короткое, четко определенное сообщение передается функции <emphasis>MsgReceive()</emphasis>, <emphasis>как будто поток отправил обычное стандартное сообщение</emphasis>. Единственное различие состоит в том, что вы не сможете применить функцию <emphasis>MsgReply()</emphasis> к такому сообщению, поскольку, кроме всего прочего, общая идея импульса состоит в том, что это сообщение по своей сути является асинхронным. В данном разделе мы рассмотрим другую функцию, <emphasis>MsgReceivePulse()</emphasis>, применение которой полезно при обработке импульсов.</p>
     <p>Единственно что забавляет при работе с импульсами — это то, что идентификатор отправителя, который возвращается функцией <emphasis>MsgReceive()</emphasis> при их приеме, имеет нулевое значение. Это верный индикатор того, что принятое сообщение является импульсом, а не стандартным сообщением клиента. В коде серверов вы будете часто видеть фрагменты, подобные представленному ниже:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>rcvid = MsgReceive(chid, ...);</code></p>
     <p><code>if (rcvid == 0) { // Это импульс</code></p>
     <p><code> // Определить тип импульса</code></p>
     <empty-line/>
     <p><code> // Обработать его</code></p>
     <p><code>} else { // Это обычное сообщение</code></p>
     <p><code> // Определить тип сообщения</code></p>
     <empty-line/>
     <p><code> // Обработать его</code></p>
     <p><code>}</code></p>
     <subtitle>Что внутри импульса?</subtitle>
     <p>Итак, вы принимаете сообщение с нулевым идентификатором отправителя. Что у него внутри? Вот фрагмент заголовочного файла <code><strong>&lt;sys/neutrino.h&gt;</strong></code>:</p>
     <p><code>struct _pulse {</code></p>
     <p><code> _uint16      type;</code></p>
     <p><code> _uint16      subtype;</code></p>
     <p><code> _int8        code;</code></p>
     <p><code> _uint8       zero[3];</code></p>
     <p><code> union sigval value;</code></p>
     <p><code> _int32       scoid;</code></p>
     <p><code>};</code></p>
     <p>Элементы <emphasis>type</emphasis> и <emphasis>subtype</emphasis> равны нулю (это еще один признак того, что перед нами импульс); содержимое элементов <emphasis>code</emphasis> и <emphasis>value</emphasis> определяется отправителем. В общем случае элемент <emphasis>code</emphasis> будет указывать на причину, по которой был отправлен импульс, а параметр <emphasis>value</emphasis> будет содержать 32 бита данных, ассоциируемых с данным импульсом. Эти два поля и есть те самые 40 бит контента; другие поля пользователем не настраиваются.</p>
     <p>Ядро резервирует отрицательные значения параметра <emphasis>code</emphasis>, оставляя 127 значений для программистов — для использования по своему усмотрению.</p>
     <p>Элемент <emphasis>value</emphasis> в действительности является элементом типа <code><strong>union</strong></code>:</p>
     <p><code>union sigval {</code></p>
     <p><code> int sival_int;</code></p>
     <p><code> void *sival_ptr;</code></p>
     <p><code>};</code></p>
     <p>Поэтому (в развитие примера с сервером, представленного выше) вы часто будете видеть программу, подобную этой:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>rcvid = MsgReceive(chid, ...</code></p>
     <p><code>if (rcvid == 0) { // Импульс</code></p>
     <p><code> // Определить тип импульса</code></p>
     <p><code> switch (msg.pulse.code) {</code></p>
     <p><code> case MY_PULSE_TIMER:</code></p>
     <p><code>  // Сработал один из наших таймеров,</code></p>
     <p><code>  // надо что-то делать...</code></p>
     <p><code>  break;</code></p>
     <p><code> case MY_PULSE_HWINT:</code></p>
     <p><code>  // Импульс получен от обработчика прерывания.</code></p>
     <p><code>  // Надо заглянуть в поле «value»...</code></p>
     <p><code>  val = msg.pulse.value.sival_int;</code></p>
     <p><code>  // Сделать что-нибудь по этому поводу...</code></p>
     <p><code>  break;</code></p>
     <p><code> case _PULSE_CODE_UNBLOCK:</code></p>
     <p><code>  // Это импульс от ядра, разблокирующий клиента</code></p>
     <p><code>  // Сделать что-нибудь по этому поводу...</code></p>
     <p><code>  break;</code></p>
     <p><code>   //и так далее...</code></p>
     <p><code> }</code></p>
     <p><code>} else { // Обычное сообщение</code></p>
     <p><code> // Определить тип сообщения</code></p>
     <p><code> // Обработать его</code></p>
     <p><code>}</code></p>
     <p>В этой программе предполагается, конечно, что вы описали структуру <emphasis>msg</emphasis> так, чтобы она содержала элемент «<code><strong>struct _pulse pulse;</strong></code>», и что определены константы MY_PULSE_TIMER и MY_PULSE_HWINT. Код импульса _PULSE_CODE_UNBLOCK — один из тех самых отрицательных кодов, зарезервированных для ядра, как это было упомянуто выше. Вы можете найти полный список этих кодов (а также краткое описание поля <emphasis>value</emphasis>) в <code><strong>&lt;sys/neutrino.h&gt;</strong></code>.</p>
     <subtitle>Функция <emphasis>MsgReceivePulse()</emphasis></subtitle>
     <p>Функции <emphasis>MsgReceive()</emphasis> и <emphasis>MsgReceivev()</emphasis> могут принимать либо стандартное сообщение, либо импульс. Однако, возможны ситуации, когда вы пожелаете принимать только импульсы. Лучшим примером этого является ситуация с сервером, когда вы приняли запрос от клиента на выполнение чего-нибудь, но не можете выполнить этот запрос сразу (возможно, из-за длительной операции, связанной с аппаратными средствами). В таких случаях следует, как правило, настроить аппаратные средства (или таймер, или что-нибудь еще) на передачу вам импульса всякий раз, когда происходит некое значительное событие.</p>
     <p>Если вы напишете ваш сервер по стандартной схеме «ждать сообщения в бесконечном цикле», вы можете оказаться в ситуации, когда один клиент посылает вам запрос, а потом, пока вы ожидаете импульса, который должен сигнализировать об отработке запроса, приходит запрос от другого клиента. Вообще говоря, это как раз то что нужно — в конце концов, мы хотели иметь способность одновременно обслуживать множество клиентов. Однако, у вас могут быть веские основания отказать клиенту в обслуживании — например, если обслуживание клиента слишком ресурсоемко, и надо ограничить численность одновременно обрабатываемой клиентуры.</p>
     <p>В таком случае вам потребуется обеспечить возможность «выборочного» приема только импульсов. Тут-то и становится актуальной функция <emphasis>MsgReceivePulse()</emphasis>:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int MsgReceivePulse(int chid, void *rmsg, int rbytes,</code></p>
     <p><code> struct _msg_info *info);</code></p>
     <p>Видно, что ее параметры те же, что и у функции <emphasis>MsgReceive()</emphasis> — идентификатор канала, буфер (и его размер), и параметр <emphasis>info</emphasis> — мы обсуждали его в параграфе «Кто послал сообщение?» Заметьте, что параметр <emphasis>info</emphasis> не применяется в импульсах. Вы можете спросить, почему он представлен в списке параметров. Ответ незамысловат: так было проще сделать. Просто передайте NULL!</p>
     <p>Функция <emphasis>MgsReceivePulse()</emphasis> способна принимать только импульсы. Так, если бы у вас был канал с множеством потоков, блокированных на нем с помощью функции <emphasis>MsgReceivePulse()</emphasis> (и ни одного потока, блокированного на нем с помощью функции <emphasis>MsgReceive()</emphasis>), и некий клиент попытался бы отправить вашему серверу сообщение, то этот клиент остался бы заблокированным по передаче (Send-blocked) до тех пор, пока какой-либо поток сервера не вызовет <emphasis>MsgReceive()</emphasis>. Тем временем функция <emphasis>MsgReceivePulse()</emphasis> будет спокойно принимать импульсы.</p>
     <p>Единственное, что можно гарантировать при совместном применении функций <emphasis>MsgReceivePulse()</emphasis> и <emphasis>MsgReceive()</emphasis>, — что функция <emphasis>MsgReceivePulse()</emphasis> обеспечит прием исключительно импульсов. Функция <emphasis>MsgReceive()</emphasis> сможет принимать как импульсы, так и обычные сообщения! Это происходит потому, что применение функции <emphasis>MsgReceivePulse()</emphasis> зарезервировано специально для случаев, где нужно исключить получение сервером обычных сообщений.</p>
     <p>Это немного вводит в замешательство. Так как функция <emphasis>MsgReceive()</emphasis> может принимать и обычные сообщения, и импульсы, а функция <emphasis>MsgReceivePulse()</emphasis> может принимать только импульсы, то как быть с сервером, в котором применяются обе функции? Общий ответ такой. У вас есть пул потоков, выполняющих <emphasis>MsgReceive()</emphasis>. Этот пул потоков (один или более потоков — это зависит от числа клиентов, которое вы хотели бы обслуживать одновременно) отвечает за обработку запросов от клиентов.</p>
     <p>Поскольку вы пытаетесь управлять численностью потоков- обработчиков, и некоторым из этих потоков может понадобиться блокироваться в ожидании импульса (например, от оборудования или от другого потока), вы блокируете поток-обработчик при помощи функции <emphasis>MsgReceivePulse()</emphasis>. Функция <emphasis>MsgReceivePulse()</emphasis> принимает только импульсы, а значит, ее применение гарантирует, что пока вы ждете импульса, к вам ненароком не просочится никакой клиентский запрос.</p>
    </section>
    <section>
     <title>
      <p>Функция <emphasis>MsgDeliverEvent()</emphasis></p>
     </title>
     <p>Как было упомянуто выше в параграфе «Иерархический принцип обмена», существуют ситуации, когда приходится нарушать естественное направление передач.</p>
     <p>Такой случай возможен, когда у вас есть клиент, который посылает серверу сообщение и при этом не хочет блокироваться, а результат может быть доступен только через некоторое время. Конечно, вы могли бы частично решить эту проблему путем применения многопоточных клиентов, выделяя в клиенте отдельный поток для блокирующих вызовов сервера, но это не всегда с успехом работает в больших системах, поскольку при большом количестве серверов количество ждущих потоков было бы слишком велико. Но допустим, вы не хотите здесь использовать многопоточность, а вместо этого вам нужно, чтобы сервер ответил клиенту сразу, и чем-то вроде «Заказ принят; я скоро вернусь». Здесь, поскольку сервер ответил, клиент теперь свободен продолжать свою работу. После того как сервер отработает запрос клиента, ему потребуется как-то сказать клиенту «Проснись, вот твой заказ.» Очевидно, как мы это уже видели при анализе иерархического принципа обмена, сервер не должен передавать сообщения клиенту, потому что если клиент в это же время отправит сообщение серверу, это может вызывать взаимную блокировку. Так как же сервер может послать сообщение клиенту без нарушения иерархического принципа?</p>
     <p>В действительности это составная операция. Вот как это работает:</p>
     <p>1. Клиент создает структуру типа <code><strong>struct sigevent</strong></code> и заполняет ее.</p>
     <p>2. Клиент посылает сообщение серверу, в котором запрашивает: «Сделай для меня то-то, ответ дай сразу же, а по окончании работы уведоми меня об этом при помощи структуры <code><strong>struct sigevent</strong></code> — структуру прилагаю».</p>
     <p>3. Сервер принимает сообщение (которое включает в себя структуру <code><strong>struct sigevent</strong></code>), сохраняет структуру <code><strong>struct sigevent</strong></code> и идентификатор отправителя и немедленно отвечает клиенту.</p>
     <p>4. Теперь клиент выполняется — как и сервер.</p>
     <p>5. Когда сервер завершает работу, он использует функцию <emphasis>MsgDeliverEvent()</emphasis>, чтобы сообщить об этом клиенту.</p>
     <p>Мы рассмотрим более подробно структуру <code><strong>struct sigevent</strong></code> в главе «Часы, таймеры и периодические уведомления», в параграфе «Как заполнить структуру <code><strong>struct sigevent</strong></code>», а здесь мы только предположим, что структура <code><strong>struct sigevent</strong></code> — это «черный ящик», который содержит некоторое событие, используемое сервером для уведомления клиента.</p>
     <p>Поскольку сервер хранит клиентские <code><strong>struct sigevent</strong></code> и идентификатор отправителя, он теперь сервер может вызвать функцию <emphasis>MsgDeliverEvent()</emphasis>, чтобы доставить событие клиенту, как клиент того и желал:</p>
     <p><code>int MsgDeliverEvent(int rcvid, const struct sigevent *event);</code></p>
     <p>Обратите внимание, что функция <emphasis>MsgDeliverEvent()</emphasis> принимает два параметра — идентификатор отправителя (<emphasis>rcvid</emphasis>) и доставляемое событие (<emphasis>event</emphasis>). Сервер никогда не изменяет и даже не читает событие! Этот момент важен, потому что это позволяет серверу доставлять события вне зависимости от их выбранного клиентом типа, без какой бы то ни было специальной обработки на стороне сервера.</p>
     <p>Идентификатор <emphasis>rcvid</emphasis> — это идентификатор отправителя, который сервер получил от клиента. Заметьте, что это определенно особый случай. Обычно, после того как сервер ответил клиенту, идентификатор отправителя прекращает иметь значение (потому что клиент уже разблокирован, и сервер не может разблокировать его заново или считать/записать данные, и т.п.). Но в нашем случае, идентификатор отправителя содержит только информацию для ядра, какому клиенту должно быть доставлено событие. Вызывая <emphasis>MsgDeliverEvent()</emphasis>, сервер не блокируется — для сервера это неблокирующий вызов. Ядро доставляет событие клиенту, после чего тот выполняет какие бы то ни было соответствующие действия.</p>
    </section>
    <section>
     <title>
      <p>Флаги канала</p>
     </title>
     <p>Когда мы вначале книги изучали сервер (в параграфе «Сервер»), мы упомянули, что функция <emphasis>ChannelCreate()</emphasis> принимает параметр <emphasis>flags</emphasis> (флаги); правда, тогда мы вместо этого параметра передавали нуль.</p>
     <p>Теперь пришло время более подробно изучить назначение параметра <emphasis>flags</emphasis>. Рассмотрим только некоторые из возможных его значений:</p>
     <p>_NTO_CHF_FIXED_PRIORITY</p>
     <p>Принимающий поток не изменит приоритет в зависимости от приоритета отправителя. (Мы поговорим о проблемах приоритетов более подробно в разделе «Наследование приоритетов»). Обычно (то есть если этот флаг не установлен) приоритет принимающего сообщение потока изменяется на приоритет потока- отправителя.</p>
     <p>_NTO_CHF_UNBLOCK</p>
     <p>Ядро посылает импульс всякий раз, когда поток клиента пытается разблокироваться. Чтобы клиент мог разблокироваться, сервер должен ему ответить. Мы обсудим это ниже, потому что это имеет некоторые интересные последствия — как для клиента, так и для сервера.</p>
     <p>_NTO_CHF_THREAD_DEATH</p>
     <p>Ядро посылает импульс всякий раз, когда блокированный на этом канале поток «умирает». Это полезно для серверов, которые желают поддержать фиксированную популяцию потоков в пуле, т. е. количество потоков, доступных для обслуживания запросов.</p>
     <p>_NTO_CHF_DISCONNECT</p>
     <p>Ядро посылает импульс всякий раз после того, как уничтожается последнее из имевшихся соединений сервера с некоторым клиентом.</p>
     <p>_NTO_CHF_SENDER_LEN</p>
     <p>Ядро доставляет серверу, наряду с остальной информацией, размер клиентского сообщения.</p>
     <subtitle>Флаг _NTO_CHF_UNBLOCK</subtitle>
     <p>Присмотримся к флагу _NTO_CHF_UNBLOCK. Этот флаг имеет несколько особенностей при его применении, интересных и для клиента, и для сервера.</p>
     <p>Обычно (то есть когда сервер не устанавливает флаг _NTO_CHF_UNBLOCK), когда клиент хочет разблокироваться от <emphasis>MsgSend()</emphasis> (или <emphasis>MsgSendv()</emphasis>, <emphasis>MsgSendvs()</emphasis> или другой функции этого семейства), клиент просто берет и разблокируется. Клиент может пожелать разблокироваться по приему сигнала или по тайм-ауту ядра (см. функцию <emphasis>TimerTimeout()</emphasis> в Справочном руководстве по Си-библиотеке, а также главу «Часы, таймеры и периодические уведомления»). Неприятный аспект этого заключается в том, что сервер понятия не имеет, что клиент уже разблокирован и больше не ожидает ответа.</p>
     <p>Давайте предположим, что у вас многопоточный сервер, и все потоки заблокированы с помощью функции <emphasis>MsgReceive()</emphasis>. Клиент посылает сообщение серверу, и один из потоков сервера принимает его. Клиент блокируется, поток же сервера активно обрабатывает запрос. Но прежде, чем поток сервера сможет ответить клиенту, клиент разблокируется из своего <emphasis>MsgSend()</emphasis> (предположим, что по причине приема сигнала).</p>
     <p>Не забывайте: поток сервера по-прежнему обрабатывает поступивший от клиента запрос. Но так как клиент теперь разблокирован (например, его вызов <emphasis>MsgSend()</emphasis> возвратил EINTR), он теперь может послать серверу другой запрос. Вследствие особенности архитектуры серверов в QNX/Neutrino, очередное сообщение от этого клиента принял бы другой поток сервера, но идентификатор отправителя-то остается тем же самым! Сервер не сумеет различить эти два запроса, и когда первый поток сервера завершает обработку первого запроса и отвечает клиенту, фактически он отвечает на второе сообщение, а не на первое. Итак, первый поток сервера отвечает на второе сообщение клиента.</p>
     <p>Это плохо уже само по себе; но давайте заглянем еще на шаг вперед. Теперь второй поток завершает свою работу по обработке запроса и пробует ответить клиенту. Но поскольку первый поток сервера уже ответил этому клиенту, а значит, этот клиент уже разблокирован, то попытка второго потока сервера ответить клиенту возвратится с ошибкой.</p>
     <p>Эта проблема встречается только в многопоточных серверах, потому что в однопоточном сервере его единственный поток был бы по-прежнему занят обработкой первого запроса клиента. Это означает, что даже если бы клиент разблокировался и снова послал сообщение серверу, он перешел бы в SEND- блокированное состояние (а не в REPLY-блокированное состояние), позволив тем самым серверу закончить обработку первого запроса, ответить клиенту (что привело бы к ошибке, потому что клиент более не находится в REPLY-блокированном состоянии) и лишь затем принять второе сообщение. Здесь реальная проблема состоит в том, что сервер выполняет лишнюю операцию — обработку первого запроса. Операция же эта является абсолютно бесполезной, поскольку клиент больше не ожидает ее результатов.</p>
     <p>Решение данной проблемы (в случае многопоточного сервера) заключается в том, что сервер должен при создании канала указать вызову <emphasis>ChannelCreate()</emphasis> флаг _NTO_CHF_UNBLOCK. Этот флаг скажет ядру: «Сообщи мне импульсом, когда клиент попробует разблокироваться, но не позволяй ему это делать! Я разблокирую клиента сам».</p>
     <p>Ключевым моментом здесь является то, что этот флаг сервера изменяет поведение клиентов, не позволяя им разблокироваться до тех пор, пока им это не разрешит сервер.</p>
     <p>В однопоточном сервере происходит следующее:</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Действие</th>
       <th align="left" valign="top">Состояние клиента</th>
       <th align="left" valign="top">Состояние сервера</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Клиент посылает запрос серверу</td>
       <td align="left" valign="top">Блокирован</td>
       <td align="left" valign="top">Обработка</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Клиент получает сигнал</td>
       <td align="left" valign="top">Блокирован</td>
       <td align="left" valign="top">Обработка</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Ядро передает импульс серверу</td>
       <td align="left" valign="top">Блокирован</td>
       <td align="left" valign="top">Обработка (первого сообщения)</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">Сервер завершает обработку первого запроса и отвечает клиенту</td>
       <td align="left" valign="top">Разблокирован, получены корректные данные</td>
       <td align="left" valign="top">Обработка (импульса)</td>
      </tr>
     </table>
     <p>Это не помогло клиенту разблокироваться, когда он должен был это сделать, но зато обеспечило, чтобы сервер не запутался. В подобном примере сервер мог вообще проигнорировать импульс, отправленный ему ядром. Это нормально — поскольку сделано предположение, что позволить клиенту быть заблокированным до тех пор, пока сервер не подготовит данные для него, безопасно.</p>
     <p>Если вы хотите, чтобы сервер среагировал каким-то действием на посланный ядром импульс, то существует два способа реализации этого:</p>
     <p>• Создать еще один поток в сервере, который «слушал» бы канал на предмет импульсов от ядра. Этот второй поток будет отвечать за отмену операции, выполняемой первым потоком. Отвечать клиенту может любой из этих двух потоков.</p>
     <p>• Не выполнять задание клиента в потоке непосредственно, а поставить его в очередь заданий. Это обычно делается в приложениях, где сервер целенаправленно направляет задания клиента в очередь, и сервер является управляемым по событиям. Обычно одно из получаемых сервером сообщений указывает на то, что работа клиента завершена, и что пора отвечать. Когда в этом случае приходит импульс от ядра, сервер отменяет выполняемую для данного клиента работу и отвечает.</p>
     <p>Какой из методов вам выбирать — это будет зависеть от типа работы, которую выполняет сервер. В первом случае сервер активно выполняет работу для клиента, так что у вас просто не будет иного выбора, чем применить второй поток, который слушал бы импульсы от ядра, сообщающие о разблокировании (далее — «импульсы разблокирования» — <emphasis>прим. ред</emphasis>.). Конечно, вы могли бы также организовать программный опрос в пределах потока для проверки, не пришел ли импульс, но программный опрос обычно удручает.</p>
     <p>Во втором случае работу делает не сам сервер, а кто-то другой — возможно, оборудование, которому приказано «сходи и набери данных». При таком варианте поток сервера будет в любом случае блокирован по функции <emphasis>MsgReceive()</emphasis>, ожидая от оборудования признака завершения операции.</p>
     <p>В обоих случаях сервер <emphasis>обязан</emphasis> ответить клиенту, иначе клиент останется заблокированным.</p>
     <subtitle>Проблема синхронизации</subtitle>
     <p>Но даже если вы используете флаг _NTO_CHF_UNBLOCK, как это описано выше, остается еще одна проблема синхронизации. Предположим, что несколько потоков вашего сервера заблокированы по функции <emphasis>MsgReceive()</emphasis> в ожиданий сообщения или импульса, и клиент посылает серверу сообщение. Один из потоков разблокируется и начнет обрабатывать запрос клиента. В процессе этого клиент вдруг пожелает разблокироваться, и ядро сгенерирует предупреждающий об этом импульс (импульс разблокирования). Другой поток сервера примет этот импульс. Фактически здесь мы имеем гонки потоков — первый поток на момент получения вторым импульса мог быть уже почти готов ответить клиенту. Если ответит второй поток (тот, который получил импульс), то есть шанс, что клиент разблокируется и передаст серверу еще одно сообщение. При этом первый поток сервера получает шанс завершить работу по первому запросу и ответить полученными данными на второй запрос:</p>
     <image l:href="#img_31.png"/>
     <p>Путаница в многопоточном сервере.</p>
     <p>Также возможен такой вариант: поток, получивший импульс, готовится ответить клиенту, а в это время отвечает первый поток. Получается то же самое, что и раньше — первый поток разблокирует клиента, клиент передает второй запрос, второй поток (тот, который получил импульс) разблокирует клиента по второму запросу.</p>
     <p>Здесь мы имеем ситуацию с двумя параллельными потоками обработки (один вызван сообщением клиента и один — импульсом). Обычно в таких ситуациях применяются мутексы.</p>
     <p>К сожалению, это привело бы к проблеме — мутекс нужно было бы захватить немедленно после вызова <emphasis>MsgReceive()</emphasis> и освободить перед вызовом <emphasis>MsgReply()</emphasis>. Это, конечно, будет работать, но это войдет в противоречие с самим предназначением импульса разблокирования! (Сервер мог бы либо получить сообщение и игнорировать импульс разблокирования, пока не ответит клиенту, либо получить импульс разблокирования и отменить второй запрос клиента.)</p>
     <p>Многообещающим решением (но в конечном счете все равно обреченным на провал) выглядит применение «мелкозернистого» мутекса, то есть мутекса, который захватывается и освобождается только в небольших областях потока управления (как, собственно, и предполагается использовать мутекс — вместо блокирования целого раздела, как это было предложено выше). Можно было бы организовать в сервере флаг «Мы уже ответили?», сбрасывая его при приеме сообщения и снова устанавливая после ответа на него. Непосредственно перед ответом на сообщение проверяется состояние этого флага. Если флаг указывает на то, что ответ уже произведен, то отвечать не надо. Мутекс при этом следовало бы захватывать и освобождать только в областях проверки и установки/сброса флага.</p>
     <p>К сожалению, это не будет работать, потому что мы далеко не всегда имеем дело с двумя потоками управления — не всегда же клиент будет получать сигнал в процессе обработки запроса, порождая тем самым импульс разблокирования. Ниже приведен сценарий, где предложенная схема не сработает:</p>
     <p>• Клиент передает сообщение серверу; после этого клиент блокируется, а сервер переключается в режим обработки.</p>
     <p>• Поскольку сервер принял запрос от клиента, флаг ответа сбрасывается в 0, указывая этим, что мы все еще должны ответить.</p>
     <p>• Сервер отвечает клиенту в нормальном режиме (потому что флаг был установлен в 0) и устанавливает флаг в 1, указывая этим, что если прибудет импульс разблокирования, то его следует игнорировать.</p>
     <p>• (Проблемы начинаются здесь.) Клиент посылает серверу второе сообщение и почти немедленно после этого получает сигнал; ядро передает серверу импульс разблокирования.</p>
     <p>• Поток сервера, который принял сообщение, намеревался захватить мутекс для проверки состояния флага, но еще не успел это сделать, поскольку был вытеснен.</p>
     <p>• Другой поток сервера получает импульс разблокирования, и поскольку флаг по-прежнему находится в состоянии 1 с момента последней установки, игнорирует этот импульс.</p>
     <p>• Первый поток сервера захватывает мутекс и сбрасывает флаг.</p>
     <p>• К этому моменту событие разблокирования клиента потеряно.</p>
     <p>Даже если вы усовершенствуете флаг, задав для него большее количество состояний (таких как «импульс получен», «дан ответ на импульс», «сообщение получено», «дан ответ на сообщение»), у вас по-прежнему останется проблема гонок при синхронизации, потому что не существует атомарной операции, связывавшей бы флаг ответа и функции приема сообщения и ответа на него. (Именно это и определяет суть проблемы — небольшие окна во времени, одно после <emphasis>MsgReceive()</emphasis>, но до сброса флага, и второе — после того, как флаг установлен, но до вызова <emphasis>MsgReply()</emphasis>.) Единственный способ обойти проблему состоит в том, чтобы переложить работу по отслеживанию состояния флага на ядро.</p>
     <subtitle>Применение флага _NTO_MI_UNBLOCK_REQ</subtitle>
     <p>К счастью, ядро отслеживает для нас состояние этого флага — под это отведен один бит в информационной структуре сообщения (это структура типа <code><strong>struct _msg_infо</strong></code>, которая передается функции <emphasis>MsgReceive()</emphasis> в качестве последнего параметра, и которую можно также впоследствии получить по идентификатору отправителя, вызвав функцию <emphasis>MsgInfo()</emphasis>).</p>
     <p>Этот флаг называется _NTO_MI_UNBLOCK_REQ и устанавливается в 1, если клиент желает разблокироваться (например, получив сигнал).</p>
     <p>Это означает, что в многопоточном сервере у вас будет поток- обработчик, выполняющий работу для клиента, и еще один поток, который будет получать сообщения разблокирования (или другие; но сконцентрируемся пока только на сообщениях разблокирования). Когда от клиента приходит сообщение разблокирования, установите для себя флаг, чтобы дать вашей программе знать о желании потока разблокироваться.</p>
     <p>Существуют две ситуации, которые необходимо рассмотреть:</p>
     <p>• поток-обработчик заблокирован;</p>
     <p>• поток-обработчик активен (выполняется).</p>
     <p>Если поток-обработчик блокирован (например, в ожидании ресурса), то поток, получивший сообщение разблокирования, должен его разбудить. Когда поток-обработчик активизируется, он должен проверить состояние флага _NTO_MI_UNBLOCK_REQ и, если флаг установлен, дать ответ о ненормальном завершении. Если флаг сброшен, то поток может спокойно выполнять все, что ему необходимо для нормальной обработки запроса.</p>
     <p>В противном случае, если поток-обработчик активен, он должен периодически проверять «флаг, выставляемый в его отношении» потоком, принимающим сообщение разблокирования, и если флаг установлен в 1, он должен ответить клиенту с кодом ошибки. Заметьте, что это всего-навсего оптимизация: в неоптимизированном случае поток-обработчик постоянно вызывал бы функцию <emphasis>MsgInfo()</emphasis> по идентификатору отправителя и проверял бит _NTO_MI_UNBLOCK_REQ самостоятельно.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Обмен сообщениями в сети</p>
    </title>
    <section>
     <cite>
      <p><image l:href="#img_1.png"/>Прозрачный обмен сообщениями в сети не поддерживается в версии QNX/Neutrino 2.00, но это намечено к реализации в более поздних версиях данной ОС. (Поддержка этого механизма реализована в QNX/Neutrino, начиная с версии 2.11, и присутствует в QNX Realtime Platform, начиная с релиза 6.1.0 — <emphasis>прим. ред</emphasis>.) Я привожу рассмотрение этого вопроса в данной книге по двум причинам: 1) Когда этот механизм будет реализован, им можно будет воспользоваться. 2) Это настолько изящно, что грех не рассказать об этом!</p>
     </cite>
     <p>Чтобы не вносить излишней путаницы, до сих пор я избегал вопроса о применении обмена сообщениями в сети, хотя реально это основополагающий фактор гибкости QNX/Neutrino!</p>
     <p>Все, что вы узнали из книги до этого момента, применимо и к передаче сообщений по сети.</p>
     <p>Ранее в этой главе я демонстрировал пример:</p>
     <p><code>#include &lt;fcntl.h&gt;</code></p>
     <p><code>#include &lt;unistd.h&gt;</code></p>
     <empty-line/>
     <p><code>int main (void) {</code></p>
     <p><code> int fd;</code></p>
     <p><code> fd = open("/net/wintermute/home/rk/filename", O_WRONLY);</code></p>
     <p><code> write(fd, "Это обмен сообщениями\n", 24);</code></p>
     <p><code> close(fd);</code></p>
     <p><code> return(EXIT_SUCCESS);</code></p>
     <p><code>}</code></p>
     <p>В то время, я говорил, что это был пример «обмена сообщениями в сети». Клиент соединяется с сервером, определяемым тройкой ND/PID/CHID (и который оказывается на другом узле), а сервер выполняет на своем канале <emphasis>MsgReceive()</emphasis>. Клиент и сервер в данном случае абсолютно аналогичны клиенту и серверу в варианте с локальным узлом. Собственно, прекратить читать книгу можно прямо здесь — в передаче сообщений по сети нет ничего хитрого. На вам, наверное, любопытно как все это происходит? Читайте дальше!</p>
     <p>Теперь, когда мы уже рассмотрели в подробностях особенности локального обмена сообщениями, мы можем более углубленно обсудить, как осуществляется передача сообщений в сети. И хотя это обсуждение может показаться сложным, на самом деле все сводится к двум этапам: этапу разрешения имен и этапу собственно передачи сообщений.</p>
     <p>Вот рисунок, иллюстрирующий эти этапы:</p>
     <image l:href="#img_32.png"/>
     <p>Обмен сообщениями в сети. Отметьте, что модуль qnet разделен на две части.</p>
     <p>На данном рисунке наш узел называется <code><strong>magenta</strong></code>, а целевой узел по аналогии с примером называется <code><strong>wintermute</strong></code>.</p>
     <p>Рассмотрим взаимодействия, которые происходят, когда программа-клиент использует <code><strong>qnet</strong></code>, чтобы обратиться к серверу через сеть:</p>
     <p>1. Функции <emphasis>open()</emphasis> клиента было предписано открыть файл с именем, которое начинается с <code><strong>/net</strong></code>. (Имя <code><strong>/net</strong></code> — имя по умолчанию, объявляемое администратором <code><strong>qnet</strong></code> — см. документацию по QNX/Neutrino, раздел <code><strong>npi-qnet</strong></code>). Клиент понятия не имеет, кто именно отвечает за конкретное имя пути, поэтому он соединяется с администратором процессов (шаг 1), чтобы выяснить, кому принадлежит ресурс. Это выполняется автоматически и не зависит от того, передаем ли мы сообщения по сети или нет. Поскольку все ресурсы, имена которых начинаются с <code><strong>/net</strong></code>, принадлежат администратору <code><strong>qnet</strong></code>, администратор процессов отвечает клиенту, что относительно этого имени пути надо спросить администратор <code><strong>qnet</strong></code>.</p>
     <p>2. Клиент теперь посылает сообщение потоку администратора <code><strong>qnet</strong></code>, надеясь, что тот будет способен обработать запрос. Однако администратор <code><strong>qnet</strong></code> на этом узле не может предоставить клиенту конечный сервис, поэтому он сообщает клиенту, что тот должен обратиться к администратору процессов на узле <code><strong>wintermute</strong></code>. (Это делается специальным перенаправляющим сообщением, в котором содержатся ND/PID/CHID сервера, к которому надо обратиться взамен.) Это перенаправление также автоматически обрабатывается клиентской библиотекой.</p>
     <p>3. Клиент соединяется с администратором процессов на узле <code><strong>wintermute</strong></code>. Это включает в себя отправку сообщения другому узлу с помощью драйверного потока <code><strong>qnet</strong></code>. Процесс <code><strong>qnet</strong></code> клиентского узла получает сообщение и транспортирует его через сетевую среду удаленному <code><strong>qnet</strong></code>, который, в свою очередь, доставляет его администратору процессов на узле <code><strong>wintermute</strong></code>. Администратор процессов этого узла разрешает остальную часть имени пути (в нашем примере это <code><strong>/home/rk/filename</strong></code>) и отвечает перенаправляющим сообщением. Это сообщение передается обратно — через <code><strong>qnet</strong></code> сервера по сетевой среде к <code><strong>qnet</strong></code> клиента и, наконец, самому клиенту. Поскольку в этом сообщении содержатся ND/PID/CHID нужного сервера, предоставляющего конечный сервис, клиент теперь знает, к кому обращаться (в нашем примере это администратор удаленной файловой системы).</p>
     <p>4. Теперь клиент посылает запрос непосредственно нужному серверу. Маршрут следования сообщения здесь идентичен описанному в предыдущем пункте, за исключением того, что на этот раз связь с сервером осуществляется напрямую, а не через администратор процессов.</p>
     <p>После того как пройдены этапы 1 и 3, все дальнейшие коммуникации осуществляются аналогично этапу 4. В вышеприведенном примере все сообщения типа <emphasis>open()</emphasis>, <emphasis>read()</emphasis> и <emphasis>close()</emphasis> идут по маршруту, описанном в этапе 4. Заметьте, что вся последовательность рассмотренных событий была запущена вызовом <emphasis>open()</emphasis>, но само сообщение <emphasis>open()</emphasis> все равно дошло до сервера-адресата так, как это описано этапом 4.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Для особо любопытных: на самом деле я пропустил в изложении один этап. На этапе 2, когда клиент спрашивает <code><strong>qnet</strong></code> об узле <code><strong>wintermute</strong></code>, <code><strong>qnet</strong></code> должен сначала выяснить, кто такой wintermute. Это может привести к еще одной сетевой транзакции для разрешения имени узла. Приведенный выше рисунок корректен, если предположить, что <code><strong>qnet</strong></code> заранее знал про узел с именем <code><strong>wintermute</strong></code>.</p>
     </cite>
     <p>Мы еще вернемся к сообщениям, используемым функциями <emphasis>open()</emphasis>, <emphasis>read()</emphasis> и <emphasis>close()</emphasis> (а также другими функциями) в главе «Администраторы ресурсов».</p>
    </section>
    <section>
     <title>
      <p>Особенности обмена сообщениями в сети</p>
     </title>
     <p>Итак, как только соединение установлено, все дальнейшие операции обмена сообщениями осуществляются в соответствии с этапом 4, как указано на рисунке. Это может привести вас к ошибочному представлению, что передача сообщений по сети идентична локальной. К сожалению, это не так. Вот список отличий:</p>
     <p>• более длительные задержки;</p>
     <p>• функция <emphasis>ConnectAttach()</emphasis> возвращает признак успешного соединения независимо от того, является ли узел доступным или нет — реальный признак ошибки проявляется только при первой попытке передать сообщение;</p>
     <p>• функция <emphasis>MsgDeliverEvent()</emphasis> не обеспечивает достаточной надежности;</p>
     <p>• функции <emphasis>MsgReply()</emphasis>, <emphasis>MsgRead()</emphasis>, <emphasis>MsgWrite()</emphasis> являются блокирующими вызовами (в локальном варианте они не являлись таковыми);</p>
     <p>• функция <emphasis>MsgReceive()</emphasis> не будет принимать все данные, посланные клиентом; сервер будет должен вызывать функцию <emphasis>MsgRead()</emphasis> для получения окончательных остальных данных.</p>
     <subtitle>Более длительные времена задержки</subtitle>
     <p>Поскольку передача сообщений теперь выполняется в некоторой среде, а не прямым копированием «память-память» под управлением ядра, можно смело ожидать, что затраты времени на передачу сообщения будут существенно выше (100-Мбитный Ethernet в сравнении с 128-битным динамическим ОЗУ с тактированием 100 МГц будет ориентировочно на один или два порядка медленнее). В дополнение к этому также будут сказываться накладные расходы протокола и повторные попытки передачи в сбойных сетях.</p>
     <subtitle>Воздействие на функцию <emphasis>ConnectAttach()</emphasis></subtitle>
     <p>Когда вы вызываете функцию <emphasis>ConnectAttach()</emphasis>, вы задаете ей идентификаторы ND, PID и CHID. Все, что при этом происходит в QNX/Neutrino, заключается в возврате ядром идентификатора соединения драйверному потоку <code><strong>qnet</strong></code>, изображенному выше на рисунке. Поскольку никакого сообщения еще не отправлено, вы не имеете информации о том, доступен ли узел, к которому вы только что подсоединились, или нет. В обычном случае это не проблема, потому что большинство клиентов не будет самостоятельно вызывать <emphasis>ConnectAttach()</emphasis> и скорее воспользуется библиотечной функцией <emphasis>open()</emphasis>, которая перед передачей сообщения «open» сама вызывает <emphasis>ConnectAttach()</emphasis>. Это практически немедленно дает информацию о доступности удаленного узла.</p>
     <subtitle>Воздействие на функцию <emphasis>MsgDeliverEvent()</emphasis></subtitle>
     <p>Когда сервер вызывает функцию <emphasis>MsgDeliverEvent()</emphasis> локально, ответственность за доставку события целевому потоку ложится на ядро. В сетевом варианте сервер также может вызывать функцию <emphasis>MsgDeliverEvent()</emphasis>, но на этот раз ядро доставит «заготовку» этого события администратору <code><strong>qnet</strong></code>, возлагая на него ответственность за доставку этой «заготовки» удаленному <code><strong>qnet</strong></code>, который уже доставит реальное событие клиенту. Так вот, на стороне сервера с этим вызовом могут возникнуть проблемы, потому что он не является блокирующим. Это означает, после вызова <emphasis>MsgDeliverEvent()</emphasis> сервер продолжает выполняться, и поздно уже оглядываться и говорить «Знаете, очень не хочется вас огорчать, но помните тот вызов <emphasis>MsgDeliverEvent()</emphasis>? Так вот, он не сработал...»</p>
     <subtitle>Воздействие на функции <emphasis>MsgReply()</emphasis>, <emphasis>MsgRead()</emphasis> и <emphasis>MsgWrite()</emphasis></subtitle>
     <p>Чтобы уберечь функции <emphasis>MsgReply()</emphasis>, <emphasis>MsgRead()</emphasis> и <emphasis>MsgWrite()</emphasis> от вышеупомянутой проблемы <emphasis>MsgDeliverEvent()</emphasis>, эти функции при использовании их в сети преобразуются в блокирующие вызовы. В локальном случае они бы просто передали данные и разблокировались; в сети же мы должны либо удостовериться, что данные были доставлены клиенту (в случае <emphasis>MsgReply()</emphasis>), либо собственно передать данные по сети клиенту или от него (в случае двух других функций).</p>
     <subtitle>Воздействие на функцию <emphasis>MsgReceive()</emphasis></subtitle>
     <p>Функция <emphasis>MsgReceive()</emphasis> (при использовании в сети) тоже оказывается под влиянием. На момент разблокирования функции <emphasis>MsgReceive()</emphasis> на стороне сервера <code><strong>qnet</strong></code> может еще не успеть передать все данные клиента. Это делается из соображений производительности.</p>
     <p>В структуре <code><strong>struct _msg_infо</strong></code>, передаваемой функции <emphasis>MsgReceive()</emphasis> в качестве последнего параметра (мы подробно рассматривали эту структуру в параграфе «Кто послал сообщение?»), есть два флага:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>msglen</emphasis></td>
       <td align="left" valign="top">Указывает на фактическое количество данных, переданное функцией <emphasis>MsgReceive()</emphasis> (<code><strong>qnet</strong></code> любит передавать по 8Кб за один раз).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>srcmsglen</emphasis></td>
       <td align="left" valign="top">Указывает на количество данных, которое клиент хотел передать (определяется клиентом).</td>
      </tr>
     </table>
     <p>Таким образом, если бы клиент желал передать 1 мегабайт данных по сети, <emphasis>MsgReceive()</emphasis> сервера разблокировалась бы, установив параметр <emphasis>msglen</emphasis> в значение 8192 (указывая, что 8192 байта доступны в буфере); параметр <emphasis>srcmsglen</emphasis> при этом равнялся бы 1048576 (указывая, что клиент пытался переслать 1 мегабайт данных).</p>
     <p>Затем сервер использует <emphasis>MsgRead()</emphasis> для получения остальной части данных из адресного пространства клиента.</p>
    </section>
    <section>
     <title>
      <p>Несколько замечаний о дескрипторах узлов</p>
     </title>
     <p>Еще одна любопытная вещь, которой мы еще не касались в обсуждениях обмена сообщениями, — это дескрипторы узлов, для краткости обозначаемые «ND» (сокр. от Node Descriptor — <emphasis>прим. ред</emphasis>.).</p>
     <p>Вспомните: в наших примерах мы использовали символьные имена узлов, например, <code><strong>/net/wintermute</strong></code>. В QNX4 (предыдущая версия QNX до появления QNX/Neutrino) вся работа в сети была основана на концепции идентификатора узла, небольшого целого числа, уникально определяющего узел сети. Таким образом, в терминах QNX4 мы говорили бы что-то вроде «узел 61», или «узел 1», и это отражалось бы и на вызовах функций тоже.</p>
     <p>При работе в QNX/Neutrino все узлы внутренне представляются 32-разрядными числами, но эти числа не являются уникальными в сети! Я имею в виду, что узел <code><strong>wintermute</strong></code> может думать об узле <code><strong>spud</strong></code> как об узле с дескриптором 7, в то время как сам узел <code><strong>spud</strong></code> может думать, что дескриптор 7 соответствует узлу <code><strong>magenta</strong></code>. Поясню подробнее, чтобы дать полную картину происходящего. В приведенной ниже таблице сведены примерные дескрипторы узлов, которые могли бы использоваться для описания трех узлов: <code><strong>wintermute</strong></code>, <code><strong>spud</strong></code> и <code><strong>foobar</strong></code> (не путать с аббревиатурой FUBAR — <emphasis>прим. ред</emphasis>. :-):</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Узел</th>
       <th align="left" valign="top"><code>wintermute</code></th>
       <th align="left" valign="top"><code>spud</code></th>
       <th align="left" valign="top"><code>foobar</code></th>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><strong>wintermute</strong></td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">4</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><strong>spud</strong></td>
       <td align="left" valign="top">4</td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">6</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><strong>foobar</strong></td>
       <td align="left" valign="top">5</td>
       <td align="left" valign="top">7</td>
       <td align="left" valign="top">0</td>
      </tr>
     </table>
     <p>Обратите внимание, что каждый узел считает свой собственный дескриптор нулевым. Также отметьте, что для узла <code><strong>spud</strong></code> оба узла <code><strong>wintermute</strong></code> и <code><strong>foobar</strong></code> имеют дескриптор 7. Однако, для узла <code><strong>foobar</strong></code> узел <code><strong>wintermute</strong></code> имеет дескриптор 4, а узел <code><strong>spud</strong></code> — 6. Как я и упоминал раньше, эти номера не уникальны в сети, но они уникальны на каждом узле. Вы можете относиться к ним, как к файловым дескрипторам — два процесса, когда обращаются к одному и тому же файлу, могут иметь для него как одинаковый дескриптор, так и нет — все зависит от того, кто, когда и который файл открывает.</p>
     <p>К счастью, вам не надо беспокоиться о дескрипторах узлов по ряду причин:</p>
     <p>• Большинство осуществляемых вами операций обмена сообщениями «с внешним миром» будут реализовываться с помощью вызовов функций высокого уровня (таких как функция <emphasis>open()</emphasis>, приведенная в примере выше).</p>
     <p>• Дескрипторы узлов не кэшируются — предполагается, что получив дескриптор, вы используете немедленно и забудете про него.</p>
     <p>• Существует ряд библиотечных функций, предназначенных для преобразования имени пути (например, <code><strong>/net/magenta</strong></code>) в дескриптор узла.</p>
     <p>Чтобы работать с дескрипторами узлов, вам понадобится подключить файл <code><strong>&lt;sys/netmgr.h&gt;</strong></code>, потому что он содержит прототипы семейства функций <emphasis>netmgr_*()</emphasis>.</p>
     <p>Для преобразования строки в дескриптор узла используется функция <emphasis>netmgr_strtond()</emphasis>. После получения дескриптора узла его следует сразу же применить в вызове функции <emphasis>ConnectAttach()</emphasis>. Не пытайтесь сохранять его какой-либо структуре данных! Веским основанием для этого является то, что администратор сети может решить повторно использовать дескриптор после отключения всех соединений с узлом.</p>
     <p>Так что если вы получили дескриптор «7» для узла <code><strong>/net/magenta</strong></code>, подсоединились к нему, передали сообщение и затем отсоединились, то существует возможность того, что администратор сети заново назначит дескриптор «7» другому узлу.</p>
     <p>Поскольку дескрипторы узлов в сети не уникальны, возникает вопрос: «А как передавать эти штуки по сети?» Очевидно, взгляды узла <code><strong>magenta</strong></code> и узла <code><strong>wintermute</strong></code> на дескриптор «7» будут радикально отличаться. Существуют два способа решения этой проблемы:</p>
     <p>• Не передавать по сети дескрипторы узлов и пользоваться символьными именами (например, <code><strong>/net/wintermute</strong></code>).</p>
     <p>• Применять функцию <emphasis>netmgr_remote_nd()</emphasis>.</p>
     <p>Первый метод хорош как универсальное решение. Второй метод достаточно удобен на практике.</p>
     <p><code>int netmgr_remote_nd(int remote_nd, int local_nd);</code></p>
     <p>Эта функция принимает два параметра, где <emphasis>remote_nd</emphasis> — дескриптор узла целевой машины, a <emphasis>local_nd</emphasis> — дескриптор узла, который нужно преобразовать из точки зрения локальной машины в точку зрения целевой. Результатом является дескриптор узла, корректный с точки зрения заданной удаленной машины.</p>
     <p>Например, пусть <code><strong>wintermute</strong></code> — имя нашей локальной машины. У нас есть дескриптор узла «7», который является корректным на нашей локальной машине и указывает на узел <code><strong>magenta</strong></code>. Мы хотели бы выяснить, какой дескриптор узла использует узел <code><strong>magenta</strong></code> для связи с нашим узлом:</p>
     <p><code>int remote_nd;</code></p>
     <p><code>int magenta_nd;</code></p>
     <p><code>magenta_nd = netmgr_strtond("/net/magenta", NULL);</code></p>
     <p><code>printf("ND узла magenta — %d\n", magenta_nd);</code></p>
     <p><code>remote_nd = netmgr_remote_nd(magenta_nd, ND_LOCAL_NODE);</code></p>
     <p><code>printf("С точки зрения узла magenta, наш ND — %d\n",</code></p>
     <p><code> remote_nd);</code></p>
     <p>Это программа могла бы вывести что-то вроде следующего:</p>
     <p><code>ND узла magenta - 7</code></p>
     <p><code>С точки зрения узла magenta, наш ND — 4</code></p>
     <p>Это говорит о том, что на узле <code><strong>magenta</strong></code> нашему узлу соответствует дескриптор «4». (Обратите внимание на использование специальной константы ND_LOCAL_NODE, которая в действительности равна нулю, для указания на «локальный узел»).</p>
     <p>Теперь вернемся к тому, о чем мы говорили в разделе «Кто послал сообщение?»). Параметр <code><strong>struct _msg_info</strong></code> содержит, среди всего прочего, два дескриптора узлов:</p>
     <p><code>struct _msg_info {</code></p>
     <p><code> int nd;</code></p>
     <p><code> int srcnd;</code></p>
     <p><code> ...</code></p>
     <p><code>};</code></p>
     <p>Мы определили в описании для этих двух полей, что:</p>
     <p>• <emphasis>nd</emphasis> — дескриптор принимающего узла с точки зрения передающего;</p>
     <p>• <emphasis>srcnd</emphasis> — дескриптор передающего узла с точки зрения принимающего.</p>
     <p>Так, для приведенного выше примера, где узел <code><strong>wintermute</strong></code> — локальный, а узел <code><strong>magenta</strong></code> — удаленный, когда узел <code><strong>magenta</strong></code> посылает нам (узлу <code><strong>wintermute</strong></code>) сообщение, эти поля заполняются следующим образом:</p>
     <p>• <emphasis>nd</emphasis> равен 7;</p>
     <p>• <emphasis>srcnd</emphasis> равен 4.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Наследование приоритетов</p>
    </title>
    <section>
     <p>Одним из интересных моментов в операционных системах реального времени является феномен инверсии приоритетов.</p>
     <p>Инверсия приоритетов наблюдается, например, в случае, когда поток с низким приоритетом потребляет все процессорное время, в то время как в состоянии готовности находится поток с более высоким приоритетом.</p>
     <p>Вы, наверное, сейчас думаете: «Минуточку! Вы утверждали ранее, что поток с более высоким приоритетом будет всегда вытеснять поток с более низким приоритетом! Как же такое может быть?»</p>
     <p>Вы абсолютно правы. Поток с более высоким приоритетом всегда будет вытеснять поток с более низким приоритетом. Но при этом все-таки может произойти кое-что интересное. Давайте рассмотрим сценарий с тремя потоками (в трех различных процессах, для простоты рассмотрения), где «L» — поток с низким приоритетом, «Н» — поток с высоким приоритетом, и «S» — сервер. На рисунке показаны все три потока со своими приоритетами.</p>
     <empty-line/>
     <image l:href="#img_33.png"/>
     <p>Три потока с различными приоритетами.</p>
     <p>В данный момент выполняется поток Н. Потоку сервера S, имеющему наивысший приоритет, пока делать нечего, так что он находится в режиме ожидания и блокирован на функции <emphasis>MsgReceive()</emphasis>. Поток L и хотел бы работать, но его приоритет ниже, чем у потока Н, который выполняется в данный момент. Все как вы и предполагали, да?</p>
     <p>А теперь представьте себе, что поток Н принял решение «прикорнуть» на 100 миллисекунд — возможно, чтобы подождать медленное оборудование. Теперь выполняется поток L.</p>
     <p>Вот тут-то все интересное и начинается.</p>
     <p>В пределах своего нормального функционирования поток L посылает сообщение потоку сервера S, принуждая этим сервер S перейти в состояние READY и (поскольку поток S имеет высший приоритет из всех готовых к выполнению потоков) начать выполняться. К великому сожалению, сообщение, которое поток L направил к потоку сервера S, было сформулировано так: «Вычислить значение Пи с точностью до 50 знаков после запятой».</p>
     <p>Очевидно, это займет более чем 100 миллисекунд. Поэтому, когда 100 миллисекунд сна потока Н истекут, поток Н перейдет в состояние READY — угадайте, что дальше? Поток Н не активизируется, постольку в состоянии READY находится поток S, имеющий более высокий приоритет!</p>
     <p>Что здесь произошло? Произошло то, что поток с низким приоритетом «отстранил» от работы поток с более высоким приоритетом путем передачи процессора потоку с еще более высоким приоритетом. Это явление называется инверсией приоритетов.</p>
     <p>Чтобы научиться не допускать таких вещей, мы должны поговорить о наследовании приоритетов. Простой вариант реализации наследования приоритета — заставить сервер S унаследовать приоритет клиентского потока:</p>
     <image l:href="#img_34.png"/>
     <p>Блокированные потоки.</p>
     <p>При таком сценарии по истечении 100-миллисекундного интервала бездействия потока Н этот поток переходит в состояние READY и немедленно ставится на выполнение как имеющий наивысший приоритет.</p>
     <p>Неплохо; однако, здесь есть еще один тонкий момент.</p>
     <p>Предположим, что потоку Н вдруг становится нужно выполнить какие-то вычисления — например, найти 5034-е по порядку простое число. Он посылает сообщение потоку сервера S и блокируется.</p>
     <p>Однако, в данный момент S по-прежнему вычисляет значение Пи, находясь на приоритете 5! В нашей выбранной для примера системе наверняка достаточно других потоков, имеющих приоритет выше, чем 5, которым тоже нужен процессор. Это автоматически значит, что процессорного времени на вычисление значения Пи у S остается не так уж и много.</p>
     <p>Это еще одна форма инверсии приоритетов. В этом случае поток с низким приоритетом помешал потоку с более высоким приоритетом получить доступ к ресурсу. Сравните это с первой формой инверсии приоритета, где поток с низким приоритетом реально потреблял ресурсы процессора — в рассматриваемом сейчас случае этот поток не дает более приоритетному потоку доступа к ресурсам процессора, но сам при этом непосредственно их не потребляет.</p>
     <p>К счастью, данная проблема решается тоже достаточно просто. Достаточно увеличить приоритет сервера так, чтобы он был равен наивысшему из приоритетов всех заблокированных клиентов:</p>
     <image l:href="#img_35.png"/>
     <p>Повышение приоритета сервера.</p>
     <p>Здесь мы немного «обделяем» другие потоки, позволяя заданию потока L выполняться с приоритетом выше, чем он сам, но зато гарантируем, что поток Н получит свою заслуженную порцию процессорного времени.</p>
    </section>
    <section>
     <title>
      <p>Так в чем тут хитрость?</p>
     </title>
     <p>Никакой хитрости нет, QNX/Neutrino делает все для вас автоматически.</p>
     <cite>
      <p><image l:href="#img_1.png"/>В QNX/Neutrino этот механизм реализован только на один уровень вглубь, так что если клиент посылает серверу сообщение, а этот сервер, в свою очередь, передает это сообщение другому серверу, то второй сервер наследует <emphasis>нормальный</emphasis> приоритет первого, а не приоритет, который первый унаследовал от клиента. Это означает, что если на первом сервере блокируется поток с более высоким приоритетом, то соответственно повышен будет приоритет только первого сервера (а поскольку первый сервер в этот момент блокирован на втором, приоритет которого уже не повышается и остается прежним, толку от этого не будет абсолютно никакого). Будьте внимательны!</p>
     </cite>
     <p>Однако, и здесь есть еще одна тонкость. Как обеспечить возврат приоритета на тот уровень, который был до изменения?</p>
     <p>Ваш сервер работает, обслуживает запросы клиентуры и автоматически регулирует свой приоритет каждый раз, когда ему приходится разблокироваться из функции <emphasis>MsgReceive()</emphasis>. Но когда он должен восстанавливать прежнее значение приоритета, которое было до вызова <emphasis>MsgReceive()</emphasis>?</p>
     <p>Рассмотрим два варианта развития событий.</p>
     <p>• После обслуживания клиента сервер выполняет еще какие-то дополнительные действия. Это он должен сделать на своем приоритете, а не на приоритете клиента.</p>
     <p>• После обслуживания клиента сервер немедленно вызывает <emphasis>MsgReceive()</emphasis> снова для обработки следующего запроса.</p>
     <p>В первом случае для сервера было бы некорректно работать на приоритете клиента, поскольку он больше не делает для этого клиента никакой работы. Решение здесь очень простое. Используйте функцию <emphasis>pthread_setschedparam()</emphasis> (мы ее обсуждали в главе «Процессы и потоки») для возврата приоритету нужного значения.</p>
     <p>Что касательно второго случая, то ответ достаточно прост. Кому какое дело?</p>
     <p>Подумайте об этом. Какая разница, станет сервер RECEIVE-блокированным на приоритете 29 или на приоритете 2?</p>
     <p>Главное — что он RECEIVE-блокирован! А коль скоро в этом состоянии он не расходует процессорное время, его приоритет является несущественным. Как только функция <emphasis>MsgReceive()</emphasis> разблокирует сервер, сервером будет унаследован приоритет нового клиента, и все будет работать как полагается.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Резюме</p>
    </title>
    <p>Обмен сообщениями представляет собой чрезвычайно мощную концепцию и является одним из основополагающих принципов, на которых построена QNX/Neutrino (как и все предыдущие версии QNX).</p>
    <p>С помощью механизма обмена сообщениями клиент и сервер обмениваются информацией (между потоками в пределах одного процесса, между потоками в различных процессах на том же самом узле или между потоками в различных процессах на различных узлах сети). Клиент посылает сообщение и блокируется до тех пор, пока сервер не примет сообщение, не обработает его и не ответит на него.</p>
    <p>Основные преимущества передачи сообщений:</p>
    <p>• Содержание сообщения не зависит от местоположения адресата (локально или удаленно в сети).</p>
    <p>• Сообщения обеспечивают четкую границу развязки клиентов и серверов.</p>
    <p>• Неявные автоматические механизмы синхронизации и соблюдения очередности сообщений упрощают проектирование ваших приложений.</p>
   </section>
  </section>
  <section>
   <title>
    <p>Глава 3</p>
    <p>Часы, таймеры и периодические уведомления</p>
   </title>
   <section>
    <title>
     <p>Часы и таймеры</p>
    </title>
    <section>
     <p>Пришло время рассмотреть все, что относится ко времени в QNX/Neutrino. Мы увидим, как и почему мы должны использовать таймеры, а также рассмотрим теоретические положения, которые этому сопутствуют. Далее мы обсудим способы опроса и настройки часов реального времени.</p>
     <p>Давайте рассмотрим типовую техническую систему — скажем, автомобиль. В этом автомобиле у нас есть ряд программ, большинство из которых выполняются с различными приоритетами. Некоторые из этих программ необходимы для обеспечения реакции на внешние события (например, тормоза или радиоприемник), другие же должны срабатывать периодически (например, система диагностики).</p>
    </section>
    <section>
     <title>
      <p>Периодические процессы</p>
     </title>
     <p>Так как же обеспечивается «периодическая» работа системы диагностики? Можно вообразить себе некоторый процесс, выполняемый процессором нашего автомобиля и делающий нечто подобное следующему:</p>
     <p><code>// Процесс диагностики</code></p>
     <empty-line/>
     <p><code>int main(void) // Игнорируем аргументы</code></p>
     <p><code>{</code></p>
     <p><code> for (;;) {</code></p>
     <p><code>  perform_diagnostics();</code></p>
     <p><code>  sleep(15);</code></p>
     <p><code> }</code></p>
     <p><code> // Сюда мы не дойдем</code></p>
     <p><code> return (EXIT_SUCCESS);</code></p>
     <p><code>}</code></p>
     <p>Видно, что процесс диагностики выполняется бесконечно. Он запускает цикл работ по диагностике, затем «засыпает» на 15 секунд, потом «просыпается», и все повторяется заново.</p>
     <p>Если оглянуться назад в мрачные и смутные однозадачные времена, когда один процессор обслуживал одного пользователя программы такого сорта реализовывались путем выполнения функцией <emphasis>sleep()</emphasis> активного ожидания. Для этого вам было необходимо узнать быстродействие вашего процессора и написать свою собственную функцию <emphasis>sleep()</emphasis>, например:</p>
     <p><code>void sleep(int nseconds) {</code></p>
     <p><code> long i;</code></p>
     <p><code> while (nseconds--) {</code></p>
     <p><code>  for (i = 0; i &lt; CALIBRATED_VALUE; i++);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>В те дни, поскольку в машине не выполнялось никаких других задач, такие программы не составляли большой проблемы, поскольку никакой другой процесс не беспокоило, что вы используете своей функцией <emphasis>sleep()</emphasis> все 100% ресурсов процессора.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Даже в наши дни мы иногда отдаем все 100% ресурсов процессора, чтобы отмерить время. В частности, функция <emphasis>nanospin()</emphasis> применяется для отсчета времени с очень большой точностью, но делает это за счет монопольного захвата процессора на своем приоритете. Пользуйтесь с осторожностью!</p>
     </cite>
     <p>Если вы должны были реализовать некоторое подобие «многозадачного режима», то это обычно делалось путем применения процедуры прерывания, которая либо срабатывала от аппаратного таймера, либо выполнялась в пределах периода «активного ожидания», оказывая при этом некоторое воздействие на калибровку отсчета времени. Это обычно не вызывало беспокойства.</p>
     <p>К счастью, в решении этих проблем мы уже ушли далеко вперед. Вспомните параграф «Диспетчеризация и реальный мир» (глава «Процессы и потоки»), там описываются причины, по которым ядро выполняет перепланирование потоков. Причины могут быть следующие:</p>
     <p>• аппаратное прерывание;</p>
     <p>• системный вызов;</p>
     <p>• сбой (исключение).</p>
     <p>В данной главе мы подробно проанализируем две первые причины из вышеуказанного списка — аппаратные прерывания и системные вызовы.</p>
     <p>Когда поток вызывает функцию <emphasis>sleep()</emphasis>, код, содержащийся в Си-библиотеке, в конечном счете делает системный вызов. Этот вызов приказывает ядру отложить выполнение данного потока на заданный интервал времени. Ядро удаляет поток из рабочей очереди и включает таймер.</p>
     <p>Все это время ядро принимает регулярно поступающие аппаратные прерывания таймера. Положим для определенности, что эти аппаратные прерывания происходят ровно каждые 10 миллисекунд.</p>
     <p>Давайте немного переформулируем это утверждение: каждый раз, когда такое прерывание обслуживается соответствующей подпрограммой обработки прерывания (ISR) ядра, это значит, что истек очередной 10-миллисекундный интервал. Ядро отслеживает время суток путем увеличения специальной внутренней переменной на значение, соответствующее 10 миллисекундам, с каждым вызовом обработчика прерывания.</p>
     <p>Так что, реализуя 15-секундный таймер, ядро в действительности выполняет следующее:</p>
     <p>• Устанавливает переменную в текущее время плюс 15 секунд.</p>
     <p>• В обработчике прерываний сравнивает эту переменную с текущим временем.</p>
     <p>• Когда текущее время станет равным (или больше) данной переменной, поток снова ставится в очередь готовности.</p>
     <p>При использовании множества параллельно работающих таймеров — например, когда необходимо активизировать несколько потоков в различные моменты времени — ядро просто ставит запросы в очередь, отсортировав таймеры по возрастанию их времени истечения (в голове очереди при этом окажется таймер с минимальным временем истечения). Обработчик прерывания будет анализировать только переменную, расположенную в голове очереди.</p>
    </section>
    <section>
     <title>
      <p>Источники прерываний таймера</p>
     </title>
     <p>На этом мы, пожалуй, закончим наш краткий экскурс по стране таймеров и перейдем к вещам, которые уже не так очевидны.</p>
     <p>Откуда возникают прерывания таймера? На рисунке ниже приведены аппаратные компоненты (и некоторые характерные для PC значения параметров), отвечающие за генерацию этих прерываний.</p>
     <image l:href="#img_36.png"/>
     <p>Источники прерываний таймера в PC.</p>
     <p>Из рисунка видно, что в PC используется высокочастотный аппаратный генератор синхроимпульсов (МГц-диапазона). Высокочастотный меандр делится при помощи аппаратного счетчика (на рисунке — микросхема Intel 82C54), который понижает частоту импульсов до сотен килогерц или сотен герц (диапазон, в котором их уже может обработать ISR). ISR таймера входит в состав ядра и взаимодействует непосредственно с его кодом и внутренними структурами данных. В процессорах архитектуры не-x86 (MIPS, PowerPC) тоже происходит подобная последовательность событий; в некоторых микросхемах аппаратный таймер может быть непосредственно встроен в процессор.</p>
     <p>Отметим, что импульсы высокой частоты делятся на целочисленный делитель. Это означает, что результирующий период импульсов не будет точно равен 10 миллисекундам, потому что исходный период 10 миллисекундам не кратен. Поэтому ISR ядра из вышеприведенного примера будет реально вызываться по истечении каждых 9.9999296004 миллисекунд.</p>
     <p>Большое дело, скажете вы, ну и что? Ну ладно, для нашего 15-секундного счетчика это годится. 15 секунд — это 1500 отсчетов таймера; расчеты показывают, что погрешность будет в районе 106 микросекунд:</p>
     <p>15 с – 1500 * 9.9999296004 мс =</p>
     <p> = 15000 мс – 14999.8944006 мс =</p>
     <p> = 0.1055994 мс =</p>
     <p> = 105.5994 мкс</p>
     <p>К сожалению, продолжая наши математические выкладки, приходим к выводу, что при таких раскладах погрешность составляет 608 миллисекунд в день, что равняется приблизительно 18.5 секунд в месяц, или почти 3.7 минут в год!</p>
     <p>Можно предположить, что при использовании делителей другого типа ошибка может быть либо меньше, либо больше, в зависимости от погрешности округления. К счастью, ядро это знает и вводит соответствующие поправки.</p>
     <p>Ключевой момент всей этой истории состоит в том, что независимо от красивого округленного значения, реальное значение выбирается в сторону ускорения отсчета.</p>
    </section>
    <section>
     <title>
      <p>Разрешающая способность отсчета времени</p>
     </title>
     <p>Пусть отсчеты времени таймера генерируются чуть чаще, чем раз в 10 миллисекунд. Смогу ли я надежно обеспечить ожидание длительностью в 3 миллисекунды?</p>
     <p>Не-а.</p>
     <p>Подумайте, что происходит в ядре. Мы вызываем стандартную библиотечную функцию <emphasis>delay()</emphasis> для задержки на 3 миллисекунды. Ядро должно присвоить внутренней переменной ISR какое-то значение. Если оно присвоит ей значение текущего времени, то это будет означать, что таймер уже истек, и надо активизироваться немедленно. Если оно присвоит ей значение на один отсчет больше текущего времени, это будет означать, что надо активизироваться на следующем отсчете (т.е. с задержкой <emphasis>вплоть до</emphasis> 10 миллисекунд).</p>
     <subtitle>Достижение более высокой точности</subtitle>
     <p>Мораль: не следует рассчитывать на то, что разрешающая способность ваших таймеров будет лучше, чем у системного отсчета.</p>
     <p>В QNX/Neutrino у приложений есть возможность программной подстройки аппаратного делителя и ядра вместе с ним (чтобы ядро знало, с какой частотой вызывается ISR таймера). Мы поговорим об этом далее в разделе «Опрос и установка часов реального времени».</p>
    </section>
    <section>
     <title>
      <p>Флуктуации отсчета времени</p>
     </title>
     <p>Существует еще одно явление, которое вы должны принимать во внимание. Предположим, что разрешающая способность у вас равна 10 миллисекундам, а вы желаете сформировать задержку длительностью в 20 миллисекунд.</p>
     <p>Всегда ли вы можете быть уверены, что от момента вызова функции <emphasis>delay()</emphasis> до возврата из нее пройдет ровно 20 миллисекунд?</p>
     <p>Никогда.</p>
     <p>На это есть две серьезные причины. Первая причина довольно проста: при блокировании поток изымается из очереди готовности. Это означает, что процессор может перейти к другому потоку вашего приоритета. Когда ваши 20 миллисекунд истекут, ваш поток будет помещен в конец очереди готовности по этому приоритету и будет таким образом оставлен на милость потока, выполняющегося в данный момент. Это относится также к обработчикам прерываний и к потокам более высокого приоритета — то, что ваш поток перешел в состояние READY, еще не означает, что ему сразу предоставят процессор.</p>
     <p>Вторая причина несколько более хитрая. Чтобы понять ее смысл, посмотрите на нижеприведенный рисунок.</p>
     <image l:href="#img_37.png"/>
     <p>Флуктуации отсчета времени.</p>
     <p>Проблема здесь состоит в том, что ваш запрос является асинхронным по отношению к источнику отсчетов. У вас нет никакой возможности синхронизировать аппаратный таймер с вашим запросом. Поэтому в итоге вы получите интервал задержки где-то в диапазоне от 20 до 30 мс — в зависимости от того, в какой момент между отсчетами аппаратных часов возник ваш запрос.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Это очень важный момент. Флуктуации отсчета времени — одна из печальных жизненных реалий. Единственный способ избавиться от этой проблемы заключается в увеличении разрешающей способности так, чтобы получающиеся погрешности укладывались в пределы установленных допусков. (Как это делается, мы рассмотрим ниже, в разделе «Опрос и установка часов реального времени».). Имейте в виду, что флуктуации проявляются только на первом отсчете таймера — задержка на 100 секунд, реализуемая с помощью таймера с разрешением в 10 мс, попадет в интервал между 100 и 100.01 секундами.</p>
     </cite>
    </section>
    <section>
     <title>
      <p>Типы таймеров</p>
     </title>
     <p>Таймер, работу которого мы только что обсудили, называют <emphasis>относительным таймером</emphasis>. Для такого таймера период ожидания задается относительно текущего времени. Если бы вы пожелали задержать выполнение вашего потока до 12 часов 4 минут 33 секунд EDT (Eastern Daylight Time — восточное поясное время — <emphasis>прим. ред</emphasis>.) 20 января 2005 года, вам пришлось бы сначала рассчитать точное число секунд от «сейчас» до выбранного вами момента и включить относительный таймер с задержкой на это число секунд. Поскольку это довольно часто встречающаяся операция, в QNX/Neutrino реализованы абсолютные таймеры, которые обеспечивают задержку до заданного времени (а не на заданное время, как в случае относительного таймера).</p>
     <p>А что если вы захотите сделать что-нибудь полезное, пока поток ожидает наступления установленной даты? Или делать что-либо и получать «синхроимпульс» каждые 27 секунд? Здесь нельзя просто так позволить себе спать!</p>
     <p>Как мы уже обсуждали в главе «Процессы и потоки», вы можете просто запустить другой поток, и пусть он выполняет работу, пока ваш поток спит. Однако, поскольку мы говорим сейчас о таймерах, посмотрим, как это можно сделать другим способом.</p>
     <p>В зависимости от выбранной цели, вы можете сделать это с помощью либо периодического, либо однократного таймера. <emphasis>Периодический таймер</emphasis> — это таймер, который срабатывает периодически, уведомляя поток (снова и снова), что истек некоторый временной интервал. <emphasis>Однократный таймер </emphasis>— это таймер, который срабатывает только один раз.</p>
     <p>Реализация этих таймеров в ядре основана на том же самом принципе, что и в случае с таймером задержки из нашего первого примера. Ядро запоминает абсолютное значение времени (если вы укажете сделать именно так) и хранит его. Обработчик прерываний таймера сравнивает сохраненное значение времени с текущим.</p>
     <p>Однако, вместо удаления из очереди на выполнение после системного вызова, на этот раз ваш поток продолжит работу. И в момент, когда суточное время достигнет заданного вами и хранимого в памяти момента времени, ядро уведомит ваш поток о том, что назначенное время пришло.</p>
    </section>
    <section>
     <title>
      <p>Схема уведомления</p>
     </title>
     <p>Как получить уведомление о тайм-ауте? При использовании таймера задержки вы получаете уведомление просто посредством возвращения в состояние READY.</p>
     <p>При использовании периодически и однократных таймеров у вас появляется выбор:</p>
     <p>• послать импульс;</p>
     <p>• послать сигнал:</p>
     <p>• создать поток.</p>
     <p>Импульсы мы уже обсуждали главе «Обмен сообщениями»; сигналы — стандартный для UNIX механизм. Здесь же мы кратко рассмотрим уведомления при помощи создания потока.</p>
     <subtitle>Как заполнять структуру struct sigevent</subtitle>
     <p>Независимо от выбранной вами схемы уведомления, вам обязательно придется заполнять структуру <code><strong>struct sigevent</strong></code>. Давайте вкратце посмотрим, как это делается.</p>
     <p><code>struct sigevent {</code></p>
     <p><code> int sigev_notify;</code></p>
     <p><code> union {</code></p>
     <p><code>  int  sigev_signo;</code></p>
     <p><code>  int  sigev_coid;</code></p>
     <p><code>  int  sigev_id;</code></p>
     <p><code>  void (*sigev_notify_function)(union sigval);</code></p>
     <p><code> };</code></p>
     <p><code> union sigval sigev_value;</code></p>
     <p><code> union {</code></p>
     <p><code>  struct {</code></p>
     <p><code>   short sigev_code;</code></p>
     <p><code>   short sigev_priority;</code></p>
     <p><code>  };</code></p>
     <p><code>  pthread_attr_t *sigev_notify_attributes;</code></p>
     <p><code> };</code></p>
     <p><code>};</code></p>
     <cite>
      <p><image l:href="#img_1.png"/>Обратите внимание, что в приведенной декларации используются неименованные объединения и структуры. Внимательное изучение файла заголовка покажет вам, как этот трюк проходит с компиляторами, не поддерживающими такую особенность. По существу там есть директива <code><strong>#define</strong></code>, которая заставляет именованные объединения и структуры выглядеть неименованными. Подробнее см. <code><strong>&lt;sys/siginfo.h&gt;</strong></code>.</p>
     </cite>
     <p>Первое поле, которое вы должны заполнить, — это элемент <emphasis>sigev_notify</emphasis>, который определяет выбранный вами тип уведомления:</p>
     <p>SIGEV_PULSE</p>
     <p>Будет передан импульс.</p>
     <p>SIGEV_SIGNAL, SIGEV_ SIGNAL _CODE или SIGEV_SIGNAL_THREAD</p>
     <p>Будет передан сигнал.</p>
     <p>SIGEV_UNBLOCK</p>
     <p>В данном случае не используется; предназначен для тайм-аутов ядра (см. ниже в разделе «Тайм-ауты ядра»).</p>
     <p>SIGEV_INTR</p>
     <p>В данном случае не используется; предназначен для прерываний (см. главу «Прерывания»),</p>
     <p>SIGEV_THREAD</p>
     <p>Будет создан поток.</p>
     <p>Поскольку мы намерены использовать структуру <code><strong>struct sigevent</strong></code> для таймеров, нас будут интересовать только такие значения <emphasis>sigev_notify</emphasis> как SIGEV_PULSE, SIGEV_SIGNAL* и SIGNAL_THREAD; остальные мы рассмотрим в соответствующих их применению разделах.</p>
     <subtitle>Уведомление при помощи импульса</subtitle>
     <p>Чтобы передать импульс при срабатывании таймера, присвойте полю <emphasis>sigev_notify</emphasis> значение SIGEV_PULSE и обеспечьте немного дополнительной информации:</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Поле</th>
       <th align="left" valign="top">Значение и смысл</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_coid</emphasis></td>
       <td align="left" valign="top">Идентификатор соединения (connection ID), по каналу которого которому будет передан импульс.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_value</emphasis></td>
       <td align="left" valign="top">32-разрядное значение (данные импульса — см. параграф «Что внутри импульса?», глава «Обмен сообщениями» — <emphasis>прим. ред</emphasis>.), которое будет передано по заданному полем <emphasis>sigev_coid</emphasis> соединению.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_code</emphasis></td>
       <td align="left" valign="top">8-разрядное значение (код импульса — см. параграф «Что внутри импульса?», глава «Обмен сообщениями» — <emphasis>прим. ред</emphasis>.), которое будет передано по заданному полем <emphasis>sigev_coid</emphasis> соединению.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_priority</emphasis></td>
       <td align="left" valign="top">Приоритет доставки импульса. Нулевое значение не допускается — слишком уж много людей пострадало от переключения на нулевой приоритет после получения импульса, а поскольку на этом приоритете приходится конкурировать за процессор со спецпроцессом IDLE, много процессорного времени там точно не светит :-).</td>
      </tr>
     </table>
     <p>Отметим, что <emphasis>sigev_coid</emphasis> может описывать соединение на любом канале (обычно, хотя и не обязательно, этот канал связан с процессом, который инициирует событие).</p>
     <subtitle>Уведомление при помощи сигнала</subtitle>
     <p>Чтобы передать сигнал, укажите в поле <code><strong>sigev_notify</strong></code> одно из нижеперечисленных значений:</p>
     <p>SIGEV_SIGNAL</p>
     <p>Процессу будет передан обычный сигнал.</p>
     <p>SIGEV_SIGNAL_CODE</p>
     <p>Процессу будет передан сигнал, содержащий 8-битный код.</p>
     <p>SIGEV_SIGNAL_THREAD</p>
     <p>Сигнал, содержащий 8-битный код, будет передан определенному потоку.</p>
     <p>При выборе уведомления типа SIGEV_SIGNAL* нужно будет заполнить ряд дополнительных полей:</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Поле</th>
       <th align="left" valign="top">Значение и смысл</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_signo</emphasis></td>
       <td align="left" valign="top">Номер сигнала для передачи (берется из <code><strong>&lt;signal.h&gt;</strong></code>, например, SIGALRM).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_code</emphasis></td>
       <td align="left" valign="top">8-разрядный код (для уведомления типа SIGEV_SIGNAL_CODE или SIGEV_SIGNAL_THREAD).</td>
      </tr>
     </table>
     <subtitle>Уведомление созданием потока</subtitle>
     <p>Для создания потока по срабатыванию таймера установите поле <emphasis>sigev_notify</emphasis> в значение SIGEV_THREAD и заполните следующие поля:</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Поле</th>
       <th align="left" valign="top">Значение и смысл</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_notify_function</emphasis></td>
       <td align="left" valign="top">Адрес функции, возвращающей <code><strong>void*</strong></code> и принимающей <code><strong>void*</strong></code>, которая будет вызвана при возникновении события.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_value</emphasis></td>
       <td align="left" valign="top">Значение, которое будет передано функции <emphasis>sigev_notify_function()</emphasis> в качестве параметра.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sigev_notify_attributes</emphasis></td>
       <td align="left" valign="top">Атрибутная запись потока (см. главу «Процессы и потоки», параграф «Атрибутная запись потока»).</td>
      </tr>
     </table>
     <p>Этот тип уведомления воистину страшен. Если ваш таймер будет срабатывать слишком часто, и при этом будут готовы к выполнению потоки с более высоким приоритетом, чем вновь создаваемые, то у вас быстро вырастет огромная куча заблокированных потоков, и они съедят все ресурсы вашей машины. Пользуйтесь этим типом уведомления с осторожностью!</p>
     <subtitle>Общие приемы программирования уведомлений</subtitle>
     <p>В файле <code><strong>&lt;sys/siginfo.h&gt;</strong></code> есть ряд удобных макросов упрощения заполнения полей в структурах:</p>
     <p><emphasis>SIGEV_SIGNAL_INIT(eventp, signo)</emphasis></p>
     <p>Установите <emphasis>eventp</emphasis> в SIGEV_SIGNAL и впишите соответствующий номер сигнала <emphasis>signo</emphasis>.</p>
     <p><emphasis>SIGEV_SIGNAL_CODE_INIT(eventp, signo, value, code)</emphasis></p>
     <p>Установите поле <emphasis>eventp</emphasis> в SIGEV_SIGNAL_CODE, укажите номер сигнала в <emphasis>signo</emphasis>, а также задайте значения полей <emphasis>value</emphasis> и <emphasis>code</emphasis>.</p>
     <p><emphasis>SIGEV_SIGNAL_THREAD_INIT(eventp, signo, value, code)</emphasis></p>
     <p>Установите <emphasis>eventp</emphasis> в SIGEV_SIGNAL_THREAD, укажите номер сигнала в <emphasis>signo</emphasis>, а также задайте значения полей <emphasis>value</emphasis> и <emphasis>code</emphasis>.</p>
     <p><emphasis>SIGEV_PULSE_INIT(eventp, coid, priority, code, value)</emphasis></p>
     <p>Установите eventp в SIGEV_SIGNAL_PULSE, укажите идентификатор соединения в <emphasis>coid</emphasis>, а также параметры <emphasis>priority</emphasis>, <emphasis>code</emphasis> и <emphasis>value</emphasis>. Отметьте, что для <emphasis>priority</emphasis> есть специальное значение SIGEV_PULSE_PRIO_INHERIT, которое предотвращает изменение приоритета принимающего потока.</p>
     <p><emphasis>SIGEV_UNBLOCK_INIT(eventp)</emphasis></p>
     <p>Установите <emphasis>eventp</emphasis> в SIGEV_UNBLOCK.</p>
     <p><emphasis>SIGEV_INTR_INIT(eventp)</emphasis></p>
     <p>Установите <emphasis>eventp</emphasis> в SIGEV_INTR.</p>
     <p><emphasis>SIGEV_THREAD_INIT(eventp, func, attributes)</emphasis></p>
     <p>Задайте значения <emphasis>eventp</emphasis>, функции потока <emphasis>func</emphasis> и атрибутной записи <emphasis>attributes</emphasis>.</p>
     <subtitle>Уведомление при помощи импульса</subtitle>
     <p>Предположим, что вы разрабатываете сервер, который будет обречен провести большую часть своей жизни в RECEIVE-блокированном состоянии, ожидая сообщение. Идеальным вариантом здесь было бы принять специальное сообщение, указывающее, что момент, которого мы так долго ждали, наконец настал.</p>
     <p>Как раз при таком сценарии и надо использовать импульсы в качестве схемы уведомления. В разделе «Применение таймеров», представленном ниже, я приведу пример кода, который можно использовать для периодического получения импульсов.</p>
     <p>Предположим, что, с другой стороны, вы выполняете некоторую работу, но не желаете, чтобы она продолжалась вечно. Например, вы ожидаете возврата из некоторой функции, но не можете точно предсказать, сколько времени на это потребуется.</p>
     <p>В этом случае оправданным выбором является использование уведомления при помощи сигнала — возможно, даже с обработчиком. (Другой вариант, который мы обсудим позже, заключается в использовании тайм-аутов ядра; см. также параграф «_NTO_CHF_UNBLOCK» в главе «Обмен сообщениями»). В параграфе «Применение таймеров», представленном ниже, мы рассмотрим пример, использующий сигналы.</p>
     <p>Если вы вообще не собираетесь принимать сообщения, то использование сигнала и функции <emphasis>sigwait()</emphasis> является более экономной альтернативой созданию канала для принятия импульсного сообщения.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Применение таймеров</p>
    </title>
    <section>
     <p>Изучив все красоты теории, давайте теперь переключим наше внимание на конкретные образцы кода, чтобы посмотреть, что можно сделать при помощи таймеров.</p>
     <p>Чтобы работать с таймером, вам потребуется:</p>
     <p>1. Создать объект типа «таймер».</p>
     <p>2. Выбрать схему уведомления (сигнал, импульс или создание потока) и создать структуру уведомления (<code><strong>struct sigevent</strong></code>).</p>
     <p>3. Выбрать нужный тип таймера (относительный или абсолютный, и однократный или периодический).</p>
     <p>4. Запустить таймер.</p>
     <p>Давайте теперь рассмотрим все это по порядку.</p>
    </section>
    <section>
     <title>
      <p>Создание таймера</p>
     </title>
     <p>Первый этап — это создание таймера с помощью функции <emphasis>timer_create()</emphasis>:</p>
     <p><code>#include &lt;time.h&gt;</code></p>
     <p><code>#include &lt;sys/siginfo.h&gt;</code></p>
     <empty-line/>
     <p><code>int timer_create(clockid_t clock_id,</code></p>
     <p><code> struct sigevent *event, timer_t *timerid);</code></p>
     <p>Аргумент <emphasis>clock_id</emphasis> сообщает функции <emphasis>timer_create()</emphasis>, на какой временном базисе вы формируете таймер. Это вещь из области POSIX — стандарт утверждает, что на различных платформах вы можете использовать различные типы временных базисов, но любая платформа должна, по меньшей мере, поддерживать базис CLOCK_REALTIME. В QNX/Neutrino есть три базиса:</p>
     <p>• CLOCK_REALTIME</p>
     <p>• CLOCK_SOFTTIME</p>
     <p>• CLOCK_MONOTONIC</p>
    </section>
    <section>
     <title>
      <p>Сигнал, импульс или поток?</p>
     </title>
     <p>Оставим пока на время варианты CLOCK_SOFTTIME и CLOCK_MONOTONIC, поскольку они еще пока (на момент написания книги — <emphasis>прим. ред</emphasis>.) не реализованы. Втором параметром является указатель на структуру <code><strong>struct sigevent</strong></code>. Эта структура применяется для того, чтобы сообщить ядру о типе события, которое таймер должен сгенерировать при срабатывании. Мы уже обсуждали порядок заполнения <code><strong>struct sigevent</strong></code>, когда говорили о выборе схемы уведомления.</p>
     <p>Итак, мы вызываем функцию <emphasis>timer_create()</emphasis> с временным базисом CLOCK_REALTIME и указателем на структуру <code><strong>struct sigevent</strong></code>, и ядро создает объект типа «таймер» (он возвращается в последнем аргументе). Этот объект представляет собой небольшое целое число, которое является номером таймера в таблице таймеров ядра. Считайте его просто «дескриптором».</p>
     <p>На этот момент никаких событий пока не происходит. Вы просто создали таймер, но ведь вы еще не включали его.</p>
    </section>
    <section>
     <title>
      <p>Какой таймер выбрать?</p>
     </title>
     <p>Создав таймер, теперь вы должны решить, какого типа будет этот таймер. Это осуществляется путем комбинирования аргументов функции <emphasis>timer_settime()</emphasis>, которая обычно применяется для собственно запуска таймера:</p>
     <p><code>#include &lt;time.h&gt;</code></p>
     <empty-line/>
     <p><code>int timer_settime(timer_t timerid, int flags,</code></p>
     <p><code> struct itimerspec *value, struct itimerspec *oldvalue);</code></p>
     <p>Аргумент <emphasis>timerid</emphasis> — это число, которое вы получите обратно по вызову функции <emphasis>timer_create()</emphasis>. Вы можете создать множество таймеров, а затем вызывать <emphasis>timer_settime()</emphasis> для них по отдельности, когда вам это будет необходимо.</p>
     <p>С помощью аргумента <emphasis>flags</emphasis> вы определяете тип таймера — абсолютный или относительный.</p>
     <p>Если вы передаете константу TIMER_ABSTIME, получается абсолютный таймер, как вы и могли бы предположить. Затем вы передаете реальные дату и время срабатывания таймера.</p>
     <p>Если вы передаете нуль, таймер предполагается относительным.</p>
     <p>Давайте посмотрим, как определяется время. Вот ключевые фрагменты двух структур данных из <code><strong>&lt;time.h&gt;</strong></code>:</p>
     <p><code>struct timespec {</code></p>
     <p><code> long tv_sec, tv_nsec;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct itimerspec {</code></p>
     <p><code> struct timespec it_value, it_interval;</code></p>
     <p><code>};</code></p>
     <p>В структуре <code><strong>struct itimerspec</strong></code> есть два поля:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>it_value</emphasis></td>
       <td align="left" valign="top">однократно используемое значение</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>it_interval</emphasis></td>
       <td align="left" valign="top">перезагружаемое значение</td>
      </tr>
     </table>
     <p>Параметр <emphasis>it_value</emphasis> задает либо интервал времени от настоящего момента до момента срабатывания таймера (в случае относительного таймера), либо собственно время срабатывания (в случае абсолютного таймера). После срабатывания таймера значение величины <emphasis>it_interval</emphasis> задает относительное время для повторной загрузки таймера, чтобы он мог сработать снова. Заметим, что задание для <emphasis>it_interval</emphasis> нулевого значения преобразует данный таймер в однократный. Вы можете предположить, что чтобы создать «исключительно периодический» таймер, вам следует установить параметр <emphasis>it_interval</emphasis> в значение интервала перезагрузки, а параметр <emphasis>it_value</emphasis> — в нуль. К сожалению, последнее неверно — установка параметра it_value в нуль выключает таймер. Если вы хотите создать «исключительно периодический» таймер, присвойте <emphasis>it_value</emphasis> и <emphasis>it_interval</emphasis> одинаковые значения и создайте таймер как относительный. Такой таймер сработает один раз (с задержкой <emphasis>it_value</emphasis>), а затем будет циклически перезагружаться с задержкой <emphasis>it_interval</emphasis>.</p>
     <p>Оба параметра <emphasis>it_value</emphasis> и <emphasis>it_interval</emphasis> фактически являются структурами типа <code><strong>struct timespec</strong></code> — еще одного POSIX-объекта. Эта структура позволяет вам обеспечить разрешающую способность на уровне долей секунд. Первый ее элемент, <emphasis>tv_sec</emphasis>, — это число секунд, второй элемент, <emphasis>tv_nsec</emphasis>, — число наносекунд в текущей секунде. (Это означает, что никогда не следует устанавливать параметр <emphasis>tv_nsec</emphasis> в значение, превышающее 1 миллиард — это будет подразумевать смещение на более чем 1 секунду).</p>
     <p>Несколько примеров:</p>
     <p><code>it_value.tv_sec = 5;</code></p>
     <p><code>it_value.tv_nsec = 500000000;</code></p>
     <p><code>it_interval.tv_sec = 0;</code></p>
     <p><code>it_interval.tv_nsec = 0;</code></p>
     <p>Это сформирует однократный таймер, который сработает через 5,5 секунды. (5,5 секунд складывается из 5 секунд и 500,000,000 наносекунд.)</p>
     <p>Мы предполагаем здесь, что этот таймер используется как относительный, потому что если бы это было не так, то его время срабатывания уже давно бы его истекло (5.5 секунд с момента 00:00 по Гринвичу, 1 января 1970).</p>
     <p>Другой пример:</p>
     <p><code>it_value.tv_sec = 987654321;</code></p>
     <p><code>it_value.tv_nsec = 0;</code></p>
     <p><code>it_interval.tv_sec = 0;</code></p>
     <p><code>it_interval.tv_nsec = 0;</code></p>
     <p>Данная комбинация параметров сформирует однократный таймер, который сработает в четверг, 19 апреля 2001 года в 00:25:21 по EDT. (Существует множество функций, которые помогут вам преобразовать воспринимаемый человеком интервал времени в «число секунд, истекшее с 00:00:00 по Гринвичу, 1 января 1970 года». См. функции <emphasis>time()</emphasis>, <emphasis>asctime()</emphasis>, <emphasis>ctime()</emphasis>, <emphasis>mktime()</emphasis>, <emphasis>strftime()</emphasis>, и т.д.).</p>
     <p>В данном примере мы предполагаем, что это абсолютный таймер, поскольку в противном случае ждать пришлось бы достаточно долго (987654321 секунд — приблизительно 31.3 года).</p>
     <p>Отметьте, что в двух приведенных выше примерах я говорил: «мы предполагаем». В коде функции timer<emphasis>_settime()</emphasis> нет никаких проверок на правильность аргументов! Вы должны самостоятельно доопределить, является таймер абсолютным или относительным. Что до ядра, то оно будет просто счастливо запланировать какое-нибудь событие на 31.3 года вперед.</p>
     <p>И еще один пример:</p>
     <p><code>it_value.tv_sec = 1;</code></p>
     <p><code>it_value.tv_nsec = 0;</code></p>
     <p><code>it_interval.tv_sec = 0;</code></p>
     <p><code>it_interval.tv_nsec = 500000000;</code></p>
     <p>Если предположить, что это относительный таймер, он сработает через одну секунду и далее каждые полсекунды. Не существует никаких требований какого бы то ни было подобия значений интервала перезагрузки значениям задержки однократного срабатывания.</p>
    </section>
    <section>
     <title>
      <p>Сервер с периодическими импульсами</p>
     </title>
     <p>Первое, что следует рассмотреть, — это сервер, который желает получать периодические сообщения. Типовыми применениями такой схемы являются:</p>
     <p>• поддерживаемые сервером тайм-ауты клиентских запросов;</p>
     <p>• внутренние периодические события серверов.</p>
     <p>Конечно, есть и другие, специализированные, применения для таких вещей — например, периодические подтверждения готовности узлов сети («я жив»), запросы на повторную передачу, и т.п.</p>
     <subtitle>Поддерживаемые сервером тайм-ауты</subtitle>
     <p>В таком сценарии сервер предоставляет клиенту некоторую услугу, и клиент способен задать тайм-аут. Это может использоваться в самых разнообразных приложениях. Например, вы можете сказать серверу «выдай мне данные за 15 секунд» или «дай мне знать, когда истекут 10 секунд», или «жди прихода данных, но в течение не более чем 2 секунд».</p>
     <p>Все это — примеры поддерживаемых сервером тайм-аутов. Клиент посылает сообщение серверу и блокируется. Сервер принимает периодические сообщения от таймера (раз в секунду, реже или чаще) и подсчитывает, сколько этих сообщений он получил. Когда число сообщений о тайм-аутах превышает время ожидания, указанное клиентом, сервер отвечает клиенту с сообщением о тайм-ауте или, возможно, с данными, которые он успел накопить на данный момент — это зависит от того, как структурированы отношения клиента и сервера.</p>
     <p>Ниже приведен полный пример сервера, который принимает одно из двух сообщений от клиентуры и сообщения о тайм-ауте в виде импульса. Первое клиентское сообщение говорит серверу: «Дай мне знать, есть ли для меня данные, но не блокируй меня более чем на 5 секунд». Второе клиентское сообщение говорит: «Вот, возьми данные». Сервер должен позволить нескольким клиентам блокироваться на себе в ожидании данных, и поэтому обязан сопоставить клиентам тайм-ауты. Тут-то и нужен импульс; он информирует сервер: «Истекла одна секунда».</p>
     <p>Чтобы программа не выглядела излишне громоздкой, перед каждым из основных разделов я прерываю исходный текст небольшими пояснениями. Скачать эту программу вы можете на FTP-сайте компании PARSE (<code><strong>ftp://ftp.parseftp.parse.com/pub/book_v3.tar.gz</strong></code>), файл называется <code><strong>time1.с</strong></code>.</p>
     <subtitle><emphasis>Декларации</emphasis></subtitle>
     <p>В первом разделе программы определяются различные именованные константы и структуры данных. В нем также подключаются все необходимые заголовочные файлы. Оставим это без комментариев. :-)</p>
     <p><code>/*</code></p>
     <p><code> * time1.c</code></p>
     <p><code> *</code> </p>
     <p><code> * Пример сервера, получающего периодические сообщения</code></p>
     <p><code> * от таймера</code></p>
     <p><code> * и обычные сообщения от клиента.</code></p>
     <p><code> *</code> </p>
     <p><code> * Иллюстрирует использование функций таймера с импульсами.</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;stdlib.h&gt;</code></p>
     <p><code>#include &lt;time.h&gt;</code></p>
     <p><code>#include &lt;signal.h&gt;</code></p>
     <p><code>#include &lt;errno.h&gt;</code></p>
     <p><code>#include &lt;unistd.h&gt;</code></p>
     <p><code>#include &lt;sys/siginfo.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>// Получаемые сообщения</code></p>
     <empty-line/>
     <p><code>// Сообщения</code></p>
     <p><code>#define MT_WAIT_DATA 2 // Сообщение от клиента</code></p>
     <p><code>#define MT_SEND_DATA 3 // Сообщение от клиента</code></p>
     <empty-line/>
     <p><code>// Импульсы</code></p>
     <p><code>#define CODE_TIMER 1 // Импульс от таймера</code></p>
     <empty-line/>
     <p><code>// Отправляемые сообщения</code></p>
     <p><code>#define MT_OK       0 // Сообщение клиенту</code></p>
     <p><code>#define MT_TIMEDOUT 1 // Сообщение клиенту</code></p>
     <empty-line/>
     <p><code>// Структура сообщения</code></p>
     <p><code>typedef struct {</code></p>
     <p><code> int messageType; // Содержит сообщение от клиента и</code></p>
     <p><code>                  // клиенту</code></p>
     <p><code> int messageData; // Опциональные данные, зависят от</code></p>
     <p><code>                  // сообщения</code></p>
     <p><code>} ClientMessageT;</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> ClientMessageT msg;  // Сообщение может быть</code></p>
     <p><code>                      // либо обычным,</code></p>
     <p><code> struct _pulse pulse; // либо импульсом</code></p>
     <p><code>} MessageT;</code></p>
     <empty-line/>
     <p><code>// Таблица клиентов</code></p>
     <p><code>#define MAX_CLIENT 16 // Максимум клиентов</code></p>
     <p><code>                      // одновременно</code></p>
     <empty-line/>
     <p><code>struct {</code></p>
     <p><code> int in_use;  // Элемент используется?</code></p>
     <p><code> int rcvid;   // Идентификатор</code></p>
     <p><code>              // отправителя клиента</code></p>
     <p><code> int timeout; // Оставшийся клиенту</code></p>
     <p><code>              //тайм-аут</code></p>
     <p><code>} clients[MAX_CLIENT]; // Таблица клиентов</code></p>
     <empty-line/>
     <p><code>int chid; // Идентификатор канала</code></p>
     <p><code>          // (глобальный)</code></p>
     <empty-line/>
     <p><code>int debug = 1; // Режим отладки, 1 ==</code></p>
     <p><code>               // вкл, 0 == выкл</code></p>
     <empty-line/>
     <p><code>char *progname = "time1.c";</code></p>
     <empty-line/>
     <p><code>// Предопределенные прототипы</code></p>
     <p><code>static void setupPulseAndTimer(void);</code></p>
     <p><code>static void gotAPulse(void);</code></p>
     <p><code>static void gotAMessage(int rcvid, ClientMessageT *msg);</code></p>
     <subtitle><emphasis>main()</emphasis></subtitle>
     <p>Следующий раздел кода является основным и отвечает за следующее:</p>
     <p>• создание канала с помощью функции <emphasis>ChannelCreate()</emphasis>;</p>
     <p>• вызов подпрограммы <emphasis>setupPulseAndTimer()</emphasis> (для настройки периодического таймера, срабатывающего раз в секунду и использующего импульс в качестве способа доставки события;</p>
     <p>• и, наконец, бесконечный цикл ожидания импульсов и сообщений и их обработки.</p>
     <p>Обратите внимание на проверку значения, возвращаемого <emphasis>MsgReceive()</emphasis> — нуль указывает, что был принят импульс (здесь мы не делаем никакой дополнительной проверки, <emphasis>наш</emphasis> ли это импульс), ненулевое значение говорит о том, что было принято сообщение.</p>
     <p>Обработка импульсов и сообщений выполняется функциями <emphasis>gotAPulse()</emphasis> и <emphasis>gotAMessage()</emphasis>.</p>
     <p><code>int main void) // Игнорировать аргументы</code></p>
     <p><code>               // командной строки</code></p>
     <p><code>{</code></p>
     <p><code> int rcvid; // PID отправителя</code></p>
     <p><code> MessageT msg; // Само сообщение</code></p>
     <p><code> if ((chid = ChannelCreate(0)) == -1) {</code></p>
     <p><code>  fprintf(stderr, "%s: не удалось создать канал!\n",</code></p>
     <p><code>   progname);</code></p>
     <p><code>  perror(NULL);</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> // Настроить импульс и таймер</code></p>
     <p><code> setupPulseAndTimer();</code></p>
     <p><code> // Прием сообщений</code></p>
     <p><code> for(;;) {</code></p>
     <p><code>  rcvid = MsgReceive(chid, &amp;msg, sizeof(msg), NULL));</code></p>
     <p><code>  // Определить, от кого сообщение</code></p>
     <p><code>  if (rcvid == 0) {</code></p>
     <p><code>   // Здесь неплохо бы еще проверить поле «code»...</code></p>
     <p><code>   gotAPulse();</code></p>
     <p><code>  } else {</code></p>
     <p><code>   gotAMessage(rcvid, &amp;msg.msg);</code></p>
     <p><code>  }</code></p>
     <p><code> }</code></p>
     <p><code> // Сюда мы никогда не доберемся</code></p>
     <p><code> return (EXIT_SUCCESS);</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>setupPulseAndTimer()</emphasis></subtitle>
     <p>В функции <emphasis>setupPulseAndTimer()</emphasis> вы видите код, в котором определяется тип таймера и схема уведомления. Когда мы рассуждали о таймерных функциях выше, я говорил, что таймер может выдать сигнал или импульс, либо создать поток. Решение об этом принимается именно здесь, в функции <emphasis>setupPulseAndTimer()</emphasis>. Обратите внимание, что здесь мы использовали макроопределение SIGEV_PULSE_INIT(). Используя это макроопределение, мы реально присвоили элементу sigev_notify значение SIGEV_PULSE. (Если бы мы использовали одно из макроопределений семейства SIGEV_SIGNAL*_INIT(), мы получили бы уведомление при помощи соответствующего сигнала). Отметьте, что при настройке импульса мы с помощью вызова <emphasis>ConnectAttach()</emphasis> устанавливаем соединение с самим собой и даем ему уникальный код (здесь — константа CODE_TIMER; мы ее определили сами)</p>
     <p>Последний параметр в инициализации структуры события — это приоритет импульса; здесь мы выбрали SIGEV_PULSE_PRIO_INHERIT (константа, равная -1). Это предписывает ядру не изменять приоритет принимающего импульс потока.</p>
     <p>В конце описания функции мы вызываем <emphasis>timer_create()</emphasis> для создания таймера в ядре, после чего настраиваем его на срабатывание через одну секунду (поле <emphasis>it_value</emphasis>) и на периодическую перезагрузку односекундными интервалами (поле <emphasis>it_interval</emphasis>). Отметим, что таймер включается только по вызову <emphasis>timer_settime()</emphasis>, а не при его создании.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Схема уведомления по типу SIGEV_PULSE — расширение, свойственное только QNX/Neutrino. Концепция импульсов в POSIX отсутствует.</p>
     </cite>
     <p><code>/*</code></p>
     <p><code> * setupPulseAndTimer</code></p>
     <p><code> *</code></p>
     <p><code> * Эта подпрограмма отвечает за настройку импульса, чтобы</code></p>
     <p><code> * тот отправлял сообщение с кодом MT_TIMER.</code></p>
     <p><code> * Затем устанавливается</code></p>
     <p><code> * периодический таймер с периодом в одну секунду.</code></p>
     <p><code>*/</code></p>
     <p><code>void setupPulseAndTimer(void) {</code></p>
     <p><code> timer_t timerid; // Идентификатор таймера</code></p>
     <p><code> struct sigevent event; // Генерируемое событие</code></p>
     <p><code> struct itimerspec timer; // Структура данных</code></p>
     <p><code>                          // таймера</code></p>
     <p><code> int coid; // Будем соединяться с</code></p>
     <p><code>           // собой</code></p>
     <p><code> // Создать канал к себе</code></p>
     <p><code> coid = ConnectAttach(0, 0, chid, 0, 0);</code></p>
     <p><code> if (coid == -1) {</code></p>
     <p><code>  fprintf(stderr, "%s: ошибка ConnectAttach!\n", progname);</code></p>
     <p><code>  perror(NULL);</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> // Установить, какое событие мы хотим сгенерировать</code></p>
     <p><code> // - импульс</code></p>
     <p><code> SIGEV_PULSE_INIT(&amp;event, coid, SIGEV_PULSE_PRIO_INHERIT,</code></p>
     <p><code>  CODE_TIMER, 0);</code></p>
     <p><code> // Создать таймер и привязать к событию</code></p>
     <p><code> if (timer_create(CLOCK_REALTIME, &amp;event, &amp;timerid) ==</code></p>
     <p><code>  -1) {</code></p>
     <p><code>  fprintf(stderr,</code></p>
     <p><code>   "%s: не удалось создать таймер, errno %d\n",</code></p>
     <p><code>   progname, errno);</code></p>
     <p><code>  perror(NULL);</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> // Настроить таймер (задержка 1 с, перезагрузка через</code></p>
     <p><code> // 1 с) ...</code></p>
     <p><code> timer.it_value.tv_sec = 1;</code></p>
     <p><code> timer.it_value.tv_nsec = 0;</code></p>
     <p><code> timer.it_interval.tv_sec = 1;</code></p>
     <p><code> timer.it_interval.tv_nsec = 0;</code></p>
     <p><code> // ...и запустить его!</code></p>
     <p><code> timer_settime(timerid, 0, &amp;timer, NULL);</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>gotAPulse()</emphasis></subtitle>
     <p>В функции <emphasis>gotAPulse()</emphasis> вы можете видеть, как мы реализовали способность сервера обеспечивать тайм-ауты для клиентов. Мы последовательно просматриваем список клиентуры и, поскольку мы знаем, что импульс выдается один раз в секунду, просто уменьшаем число секунд, которое остается клиенту до тайм-аута. Если эта величина достигает нулевого значения, мы отвечаем этому клиенту сообщением «Извините, тайм-аут» (тип сообщения MT_TIMEDOUT). Обратите внимание, что мы подготавливаем это сообщение заранее (вне цикла <code><strong>for</strong></code>), а затем посылаем его по мере необходимости. Этот прием — по существу вопрос стиля: если вы предполагаете отвечать часто, возможно, имело бы смысл выполнить настройку однажды и загодя. Если же множество ответов не ожидается, то имело бы больший смысл делать настройки по мере необходимости.</p>
     <p>Если значение оставшегося времени еще не достигло нуля, мы не делаем ничего — клиент по-прежнему заблокирован в ожидании сообщения.</p>
     <p><code>/*</code></p>
     <p><code> * gotAPulse</code></p>
     <p><code> *</code></p>
     <p><code> * Эта подпрограмма отвечает за обработку тайм-аутов.</code></p>
     <p><code> * Она проверяет список клиентов на предмет тайм-аута</code></p>
     <p><code> * и отвечает соответствующим сообщением тем клиентам,</code></p>
     <p><code> * у которых тайм-аут произошел.</code></p>
     <p><code>*/</code></p>
     <p><code>void gotAPulse(void) {</code></p>
     <p><code> ClientMessageT msg;</code></p>
     <p><code> int i;</code></p>
     <p><code> if (debug) {</code></p>
     <p><code>  time_t now;</code></p>
     <p><code>  time(&amp;now);</code></p>
     <p><code>  printf("Получен импульс, время %s", ctime(&amp;now));</code></p>
     <p><code> }</code></p>
     <p><code> // Подготовить ответное сообщение</code></p>
     <p><code> msg.messageType = MT_TIMEDOUT;</code></p>
     <p><code> // Просмотреть список клиентов</code></p>
     <p><code> for (i = 0; i &lt; MAX_CLIENT; i++) {</code></p>
     <p><code>  // Элемент используется?</code></p>
     <p><code>  if (clients[i].in_use) {</code></p>
     <p><code>   // Тайм-аут?</code></p>
     <p><code>   if (--clients[i].timeout == 0) {</code></p>
     <p><code>    // Ответить</code></p>
     <p><code>    MsgReply(clients[i].rcvid, EOK, &amp;msg, sizeof(msg));</code></p>
     <p><code>    // Освободить элемент</code></p>
     <p><code>    clients[i].in_use = 0;</code></p>
     <p><code>   }</code></p>
     <p><code>  }</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>gotAMessage()</emphasis></subtitle>
     <p>В функции <emphasis>gotAMessage()</emphasis> вы видите другую половину заданной функциональности, где мы добавляем клиента в список клиентуры, ожидающей данные (если получено сообщение типа MT_WAIT_DATA), или сопоставляем клиента с сообщением, которое было только что получено (если это сообщение типа MT_SEND_DATA). Заметьте, что для простоты мы здесь не реализуем очередь клиентов, находящихся в ожидании передачи данных, получатель для которых еще не доступен — это вопрос управления очередями, оставьте его для себя в качестве упражнения.</p>
     <p><code>/*</code></p>
     <p><code> * gotAMessage</code></p>
     <p><code> *</code> </p>
     <p><code> * Эта подпрограмма вызывается при каждом приеме</code></p>
     <p><code> * сообщения. Проверяем тип</code></p>
     <p><code> * сообщения (либо «жду данных», либо «вот данные»),</code></p>
     <p><code> * и действуем</code></p>
     <p><code> * соответственно. Для простоты предположим, что данные</code></p>
     <p><code> * никогда не ждут.</code></p>
     <p><code> * Более подробно об этом см. в тексте.</code></p>
     <p><code>*/</code></p>
     <p><code>void gotAMessage(int rcvid, ClientMessageT *msg) {</code></p>
     <p><code> int i;</code></p>
     <p><code> // Определить тип сообщения</code></p>
     <p><code> switch (msg-&gt;messageType) {</code></p>
     <p><code> // Клиент хочет ждать данных</code></p>
     <p><code> case MT_WAIT_DATA:</code></p>
     <p><code>  // Посмотрим, есть ли пустое место в таблице клиентов</code></p>
     <p><code>  for (i = 0; i &lt; MAX_CLIENT; i++) {</code></p>
     <p><code>   if (!clients[i].in_use) {</code></p>
     <p><code>    // Нашли место - пометить как занятое,</code></p>
     <p><code>    // сохранить rcvid</code></p>
     <p><code>    // и установить тайм-аут</code></p>
     <p><code>    clients[i].in_use = 1;</code></p>
     <p><code>    clients[i].rcvid = rcvid;</code></p>
     <p><code>    clients[i].timeout = 5;</code></p>
     <p><code>    return;</code></p>
     <p><code>   }</code></p>
     <p><code>  }</code></p>
     <p><code>  fprintf(stderr,</code></p>
     <p><code>   "Таблица переполнена, сообщение от rcvid %d"</code></p>
     <p><code>   " игнорировано, клиент заблокирован\n", rcvid);</code></p>
     <p><code>  break;</code></p>
     <p><code>  // Клиент с данными</code></p>
     <p><code> case MT_SEND_DATA:</code></p>
     <p><code>  // Посмотрим, есть ли другой клиент, которому можно ответить</code></p>
     <p><code>  // данными от этого клиента</code></p>
     <p><code>  for (i = 0; i &lt; MAX CLIENT; i++) {</code></p>
     <p><code>   if (clients[i].in_use) {</code></p>
     <p><code>    // Нашли - использовать полученное сообщение</code></p>
     <p><code>    // в качестве ответного</code></p>
     <p><code>    msg-&gt;messageType = MT_OK;</code></p>
     <p><code>    // Ответить ОБОИМ КЛИЕНТАМ!</code></p>
     <p><code>    MsgReply(clients[i].rcvid, EOK, msg, sizeof(*msg));</code></p>
     <p><code>    MsgReply(rcvid, EOK, msg, sizeof(*msg));</code></p>
     <p><code>    clients[i].in_use = 0;</code></p>
     <p><code>    return;</code></p>
     <p><code>   }</code></p>
     <p><code>  }</code></p>
     <p><code>  fprintf(stderr,</code></p>
     <p><code>   "Таблица пуста, сообщение от rcvid %d игнорировано,"</code></p>
     <p><code>   " клиент заблокирован\n", rcvid);</code></p>
     <p><code>  break;</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <subtitle>Примечания</subtitle>
     <p>Несколько общих замечаний по тексту программы:</p>
     <p>• Если сообщение с данными прибывает, когда либо никто не ждет, либо список ожидающих клиентов переполнен, в стандартный поток ошибок выводится сообщение, но клиенту при этом мы не отвечаем ничего. Это означает, что ряд клиентов может оказаться в REPLY-блокированном состоянии навсегда — идентификаторы отправителей мы потеряли, а значит, и ответ им дать не можем.</p>
     <p>Это сделано намеренно. Вы можете изменить это, добавив соответственно сообщения MT_NO_WAITERS и MT_NO_SPACE, которыми можно было бы отвечать всякий раз при обнаружении ошибок данного типа.</p>
     <p>• Когда клиент-обработчик ждет, а клиент-поставщик пересылает ему данные, мы отвечаем обоим клиентам. Это критично, поскольку мы должны разблокировать обоих клиентов.</p>
     <p>• Мы повторно использовали буфер клиента-поставщика для обоих ответов. Этот прием программирования — опять же, вопрос стиля: в большом приложении у вас, вероятно, было бы много типов возвращаемых значений, и вы могли бы и не захотеть повторно использовать одни и те же буферы.</p>
     <p>• В приведенном примере используется «щербатый» массив фиксированной длины с флагом «элемент задействован» (<code><strong>clients[i].in_use</strong></code>). Поскольку моей целью здесь является отнюдь не демонстрация хитростей программирования односвязных списков, я использовал простейший для понимания вариант. В конечном же программном продукте, разумеется, имело бы смысл использовать динамический список.</p>
     <p>• Когда функция <emphasis>MsgReceive()</emphasis> получает импульс, наше решение относительно того, действительно ли это «наш» импульс, фактически является весьма слабо аргументированным — мы просто предполагаем (согласно комментариям), что все входящие импульсы имеют тип CODE_TIMER. Опять же, в конечном продукте следовало бы проверять значение кода импульса и сообщать о наличии каких-либо аномалий.</p>
     <p>Отметим, что в приведенном примере демонстрируется только один способ реализации тайм-аутов клиентуры. Позже, в этой же главе (в разделе «Тайм-ауты ядра») мы поговорим о тайм-аутах ядра. Это еще один способ делать почти то же самое, только управление на этот раз осуществляется клиентом, а не таймером.</p>
     <subtitle>Внутренние периодические события серверов</subtitle>
     <p>Здесь мы имеем несколько другое применение для периодических сообщений о тайм-аутах, когда эти сообщения предназначены сервером исключительно для внутреннего использования и не имеют никакого отношения к клиенту вообще.</p>
     <p>Например, некоторые аппаратные средства могут требовать, чтобы сервер опрашивал их периодически — например, такое может быть в случае сетевого соединения: сервер должен периодически проверять, является ли данное подключение доступным, и это не зависит от команд клиентуры.</p>
     <p>Другой вариант — если, например, в аппаратных средствах предусмотрен таймер «выключения по неактивности». Например, если длительное пребывание какого-то аппаратного модуля во включенном состоянии может приводить к неоправданным затратам электроэнергии, то если его никто не использует в течение, скажем, 10 секунд, его можно было бы выключить (или переключить в режим низкого энергопотребления — <emphasis>прим. ред</emphasis>.). Опять же, к клиенту это не имеет никакого отношения (за исключением того, что запрос от клиента отменит режим ожидания) — это просто функция, которую сервер должен уметь предоставлять «своим» аппаратным средствам.</p>
     <p>Код в этом случае сильно бы напоминал приведенный выше пример, за исключением того, что вместо списка ожидающих клиентов у вас была бы только одна переменная тайм-аута. С каждым событием от таймера ее значение уменьшалось бы, но пока оно больше нуля, ничего бы не происходило. Когда оно стало бы равным нулю, это вызывало бы отключение аппаратных средств (или какое-либо другое соответствующее действие).</p>
     <p>Единственный трюк здесь заключается в том, что всякий раз, когда поступает сообщение от клиента, использующего данные аппаратные средства, вы должны восстановить первоначальное значение этой переменной, поскольку обращение к ресурсу должно сбрасывать «обратный отсчет». И наоборот, аппаратным средствам может потребоваться определенный промежуток времени «на разогрев» после включения. В этом случае после выключения аппаратных средств вам придется при поступлении запроса от клиента организовать еще один таймер, чтобы «придержать» запрос до того момента, пока аппаратные средства не станут готовы.</p>
    </section>
    <section>
     <title>
      <p>Таймеры, посылающие сигналы</p>
     </title>
     <p>На настоящий момент мы уже рассмотрели практически все, что относится к таймерам, за исключением одного небольшого момента. Мы обеспечивали отправку импульса, но у нас также есть возможность посылать POSIX-сигналы. Давайте посмотрим, как это делается:</p>
     <p><code>timer_create(CLOCK_REALTIME, NULL, &amp;timerid);</code></p>
     <p>Это простейший способ создать таймер, который будет посылать вам сигнал. Он обеспечивает выдачу сигнала SIGALRM при срабатывании таймера. Если бы мы предоставили <code><strong>struct sigevent</strong></code>, мы могли бы определить, какой именно сигнал мы хотим получить:</p>
     <p><code>struct sigevent event;</code></p>
     <p><code>SIGEV_SIGNAL_INIT(&amp;event, SIGUSR1);</code></p>
     <p><code>timer_create(CLOCK_REALTIME, &amp;event, &amp;timerid);</code></p>
     <p>Это обеспечит нам выдачу сигнала SIGUSR1 вместо SIGALRM.</p>
     <p>Сигналы таймера перехватываются обычными обработчиками сигналов, здесь нет ничего необычного.</p>
    </section>
    <section>
     <title>
      <p>Таймеры, создающие потоки</p>
     </title>
     <p>Если вы хотите по каждому срабатыванию таймера создавать новый поток, то вы можете это сделать с помощью <code><strong>struct sigevent</strong></code> и всех остальных таймерных штук, которые мы только что обсудили:</p>
     <p><code>struct sigevent event;</code></p>
     <p><code>SIGEV_THREAD_INIT(&amp;event, maintenance_func, NULL);</code></p>
     <p>Однако, пользоваться этим надо очень осторожно, потому что если вы определите слишком короткий интервал, вы можете просто утонуть в создаваемых потоках. Они просто поглотят все ресурсы вашего процессора и оперативной памяти.</p>
    </section>
    <section>
     <title>
      <p>Опрос и установка часов реального времени, и кое-что еще</p>
     </title>
     <p>Независимо от применения таймеров, вы можете также опрашивать и устанавливать часы реального времени, а также и плавно подстраивать их. Для этих целей можно использовать следующие функции:</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Функция</th>
       <th align="left" valign="top">Тип</th>
       <th align="left" valign="top">Описание</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>ClockAdjust()</emphasis></td>
       <td align="left" valign="top">QNX/Neutrino</td>
       <td align="left" valign="top">Плавная регулировка времени</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>ClockCycles()</emphasis></td>
       <td align="left" valign="top">QNX/Neutrino</td>
       <td align="left" valign="top">Опрос с высоким разрешением</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>clock_getres()</emphasis></td>
       <td align="left" valign="top">POSIX</td>
       <td align="left" valign="top">Выборка базового разрешения</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>clock_gettime()</emphasis></td>
       <td align="left" valign="top">POSIX</td>
       <td align="left" valign="top">Получение текущего времени суток</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>ClockPeriod()</emphasis></td>
       <td align="left" valign="top">QNX/Neutrino</td>
       <td align="left" valign="top">Получение/установка базового разрешения</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>clock_settime()</emphasis></td>
       <td align="left" valign="top">POSIX</td>
       <td align="left" valign="top">Установка текущего времени суток</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>ClockTime()</emphasis></td>
       <td align="left" valign="top">QNX/Neutrino</td>
       <td align="left" valign="top">Получение/установка текущего времени суток</td>
      </tr>
     </table>
     <subtitle>Опрос и установка</subtitle>
     <p>Функции <emphasis>clock_gettime()</emphasis> и <emphasis>clock_settime()</emphasis> являются POSIX-функциями, основанными на системном вызове <emphasis>ClockTime()</emphasis>. Эти функции могут применяться для получения и установки текущего времени суток. К сожалению, установка здесь является «жесткой», то есть независимо от того, какое время вы указываете в буфере, оно немедленно делается текущим. Это может иметь пугающие последствия, особенно когда получается, что время «повернуло вспять», потому что устанавливаемое время оказалось меньше «реального». Вообще настройка часов таким способом должна выполняться только при включении питания или когда время сильно не соответствует «реальному».</p>
     <p>Если нужна плавная корректировка текущего времени, ее можно реализовать с помощью функции <emphasis>ClockAdjust()</emphasis>:</p>
     <p><code>int ClockAdjust(clockid_t id,</code></p>
     <p><code> const struct _clockadjust *new,</code></p>
     <p><code> const struct _clockadjust *old);</code></p>
     <p>Параметрами здесь являются источник синхроимпульсов (всегда используйте CLOCK_REALTIME) и параметры <emphasis>new</emphasis> и <emphasis>old</emphasis>. Оба эти параметра являются необязательными и могут быть заданы как NULL. Параметр <emphasis>old</emphasis> просто возвращает текущую корректировку. Работа по корректировке часов управляется параметром <emphasis>new</emphasis>, который является указателем на структуру, содержащую два элемента, <emphasis>tick_nsec_inc</emphasis> и <emphasis>tick_count</emphasis>. Действует функция <emphasis>ClockAdjust()</emphasis> очень просто — каждые <emphasis>tick_count</emphasis> отсчетов системных часов к существующему значению системного времени добавляется корректировка <emphasis>tick_nsec_inc</emphasis>. Это означает, что чтобы передвинуть время вперед («догоняя» реальное), вы задаете для <emphasis>tick_nsec_inc</emphasis> положительное значение. Заметьте, что не надо переводить время назад — вместо этого, если ваши часы спешат, задайте для <emphasis>tick_nsec_inc</emphasis> небольшое отрицательное значение, и ваши часы соответственно замедлят ход. Таким образом, вы немного замедляете часы, пока их показания не будут соответствовать действительности. Существует эмпирическое правило, гласящее, что не следует корректировать системные часы значением, превышающим 10% от базового разрешения вашей системы (см. функцию <emphasis>ClockPeriod()</emphasis> и ее друзей, о них мы поговорим в следующем параграфе).</p>
     <subtitle>Регулировка разрешающей способности</subtitle>
     <p>Как мы и говорили на протяжении всей этой главы, нельзя сделать ничего с большей точностью, чем принятая в системе базовая разрешающая способность по времени. Напрашивается вопрос: а как настроить эту базовую разрешающую способность? Для этого вы можете использовать следующую функцию:</p>
     <p><code>int ClockPeriod(clockid_t id,</code></p>
     <p><code> const struct _clockperiod *new,</code></p>
     <p><code> struct _clockperiod *old, int reserved);</code></p>
     <p>Как и в случае с описанной выше функцией <emphasis>ClockAdjust()</emphasis>, с помощью параметров <emphasis>new</emphasis> и <emphasis>old</emphasis> вы получаете и/или устанавливаете значения базовой разрешающей способности по времени. Параметры <emphasis>new</emphasis> и <emphasis>old</emphasis> являются указателями на структуры типа <code><strong>struct _clockperiod</strong></code>, которые, в свою очередь, содержат два элемента — <emphasis>nsec</emphasis> и <emphasis>fract</emphasis>. На настоящий момент элемент <emphasis>fract</emphasis> должен быть равен нулю (это число фемтосекунд (миллиардная доля микросекунды — <emphasis>прим. ред</emphasis>.); нам, вероятно, это еще не скоро потребуется). Параметр <emphasis>nsec</emphasis> указывает, сколько наносекунд содержится в интервале между двумя базовыми отсчетами времени. Значение этого интервала времени по умолчанию — 10 миллисекунд, поэтому значение <emphasis>nsec</emphasis> (если вы используете функцию для получения базового разрешения) будет приблизительно равно 10 миллионам наносекунд. (Как мы уже упоминали ранее в разделе «Источники прерываний таймера», это не будет в точности равняться 10 миллисекундам.)</p>
     <p>При этом вы можете, конечно, не стесняться и попробовать назначить базовой разрешающей способности какое-нибудь смехотворно малое значение, но тут вмешается ядро и эту вашу попытку пресечет. В общем случае, в большинстве систем допускаются значения от 1 миллисекунды до сотен микросекунд.</p>
     <subtitle>Точные временные метки</subtitle>
     <p>Существует одна система отсчета времени, которая не подчиняется описанным выше правилам «базовой разрешающей способности по времени». Некоторые процессоры оборудованы встроенным высокочастотным (высокоточным) счетчиком, к которому QNX/Neutrino обеспечивает доступ при помощи функции <emphasis>ClockCycles()</emphasis>. Например, в процессоре Pentium, работающем с частотой 200 МГц, этот счетчик увеличивается тоже с частотой в 200 МГц, и поэтому он может обеспечить вам значение времени с точностью до 5 наносекунд. Это особенно полезно, когда вы хотите точно выяснить, сколько времени затрачивается на выполнение конкретного фрагмента кода (в предположении, конечно, что он не будет вытеснен). В этом случае вы должны вызвать функцию <emphasis>ClockCycles()</emphasis> перед началом вашего фрагмента и после его окончания, а потом просто подсчитать разность полученных отсчетов. Более подробно это описано в руководстве по Си-библиотеке.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Тайм-ауты ядра</p>
    </title>
    <section>
     <p>QNX/Neutrino позволяет вам получать тайм-ауты по всем блокированным состояниям. Мы обсуждали эти состояния в главе «Процессы и потоки» в разделе «Состояния потоков». Наиболее часто у вас может возникнуть потребность в этом при обмене сообщениями: клиент, посылая сообщение серверу, не желает ждать ответа «вечно». В этом случае было бы удобно использовать тайм-аут ядра. Тайм-ауты ядра также полезны в сочетании с функцией <emphasis>pthread_join()</emphasis>: завершения потока тоже не всегда хочется долго ждать.</p>
     <p>Ниже приводится декларация для функции <emphasis>TimerTimeout()</emphasis>, которая является системным вызовом, ответственным за формирование тайм-аутов ядра.</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int TimerTimeout(clockid_t id, int flags,</code></p>
     <p><code> const struct sigevent *notify,</code></p>
     <p><code> const uint64_t *ntime, uint64_t *otime);</code></p>
     <p>Видно, что функция <emphasis>TimerTimeout()</emphasis> возвращает целое число (индикатор удачи/неудачи; 0 означает, что все в порядке, -1 — что произошла ошибка, и ее код записан в <emphasis>errno</emphasis>). Источник синхроимпульсов (CLOCK_REALTIME, и т.п.) указывается в <emphasis>id</emphasis>, параметр <emphasis>flags</emphasis> задает соответствующее состояние (или состояния). Параметр <emphasis>notify</emphasis> всегда должен быть событием уведомления типа SIGEV_UNBLOCK; параметр <emphasis>ntime</emphasis> указывает относительное время, спустя которое ядро должно сгенерировать тайм-аут. Параметр <emphasis>otime</emphasis> показывает предыдущее значение тайм-аута и в большинстве случаев не используется (вы можете передать вместо него NULL).</p>
     <cite>
      <p><image l:href="#img_1.png"/>Важно отметить, что тайм-ауты «взводятся» функцией <emphasis>TimerTimeout()</emphasis>, а запускаются по входу в одно из состояний, указанных в параметре <emphasis>flags</emphasis>. Сбрасывается тайм-аут при возврате из любого системного вызова. Это означает, что вы должны заново «взводить» тайм-аут перед каждым системным вызовом, к которому вы хотите его применить. Сбрасывать тайм-аут после системного вызова не надо — это выполняется автоматически.</p>
     </cite>
    </section>
    <section>
     <title>
      <p>Тайм-ауты ядра и функция <emphasis>pthread_join()</emphasis></p>
     </title>
     <p>Самый простой пример для рассмотрения — это использование тайм-аута с функцией <emphasis>pthread_join()</emphasis>. Вот как это можно было бы сделать:</p>
     <p><code>/*</code></p>
     <p><code> * tt1.c</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;pthread.h&gt;</code></p>
     <p><code>#include &lt;inttypes.h&gt;</code></p>
     <p><code>#include &lt;errno.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>#define SEC_NSEC 1000000000LL // В одной секунде</code></p>
     <p><code>                              // 1 биллион наносекунд</code></p>
     <empty-line/>
     <p><code>void* long_thread(void *notused) {</code></p>
     <p><code> printf("Этот поток выполняется более 10 секунд\n");</code></p>
     <p><code> sleep(20);</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>int main(void) // Игнорировать аргументы</code></p>
     <p><code>{</code></p>
     <p><code> uint64_t timeout;</code></p>
     <p><code> struct sigevent event;</code></p>
     <p><code> int rval;</code></p>
     <p><code> pthread_t thread_id;</code></p>
     <p><code> // Настроить событие — это достаточно сделать однажды</code></p>
     <p><code> // Либо так, либо event.sigev_notify = SIGEV_UNBLOCK:</code></p>
     <p><code> SIGEV_UNBLOCK_INIT(&amp;event);</code></p>
     <p><code> // Создать поток</code></p>
     <p><code> pthread_create(&amp;thread_id, NULL, long_thread, NULL);</code></p>
     <p><code> // Установить тайм-аут 10 секунд</code></p>
     <p><code> timeout = 10LL * SEC_NSEC;</code></p>
     <p><code> TimerTimeout(CLOCK_REALTIME, _NTO_TIMEOUT_JOIN, &amp;event,</code></p>
     <p><code>  &amp;timeout, NULL);</code></p>
     <p><code> rval = pthread_join(thread_id, NULL);</code></p>
     <p><code> if (rval == ETIMEDOUT) {</code></p>
     <p><code>  printf("Истекли 10 секунд, поток %d все еще"</code></p>
     <p><code>   " выполняется!\n",</code></p>
     <p><code>   thread_id);</code></p>
     <p><code> }</code></p>
     <p><code> sleep(5);</code></p>
     <p><code> TimerTimeout(СLOCK_REALTIME, _NTO_TIMEOUT_JOIN, &amp;event,</code></p>
     <p><code>  &amp;timeout, NULL);</code></p>
     <p><code> rval = pthread_join(thread_id, NULL);</code></p>
     <p><code> if (rval == ETIMEDOUT) {</code></p>
     <p><code>  printf("Истекли 25 секунд, поток %d все еще выполняется"</code></p>
     <p><code>   " (нехорошо)!\n",</code></p>
     <p><code>   thread_id);</code></p>
     <p><code> } else {</code></p>
     <p><code>  printf("Поток %d завершен (как и ожидалось!)\n",</code></p>
     <p><code>   thread_id);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>Мы применили макроопределение <emphasis>SIGEV_UNBLOCK_INIT()</emphasis> для инициализации структуры события, но можно было установить <emphasis>sigev_notify</emphasis> в SIGEV_UNBLOCK и «вручную». Можно было даже сделать еще более изящно, передав NULL вместо <code><strong>struct sigevent</strong></code> — функция <emphasis>TimerTimeout()</emphasis> понимает это как знак, что нужно использовать SIGEV_UNBLOCK.</p>
     <p>Если поток (заданный в <emphasis>thread_id</emphasis>) остается работающим более 10 секунд, то системный вызов завершится по тайм-ауту — функция <emphasis>pthread_join()</emphasis> возвратится с ошибкой, установив <emphasis>errno</emphasis> в ETIMEDOUT.</p>
     <p>Вы можете использовать и другую «стенографию», указав NULL в качестве значения тайм-аута (параметр <emphasis>ntime</emphasis> в декларации выше), что предпишет ядру не блокироваться в данном состоянии. Этот прием можно использовать для организации программного опроса. (Хоть программный опрос и считается дурным тоном, его можно весьма эффективно использовать в случае с <emphasis>pthread_join()</emphasis>, периодически проверяя, завершился ли нужный поток. Если нет, можно пока сделать что-нибудь другое.)</p>
     <p>Ниже представлен пример программы, в которой демонстрируется неблокирующий вызов <emphasis>pthread_join()</emphasis>:</p>
     <p><code>int pthread_join_nb(int tid, void **rval) {</code></p>
     <p><code> TimerTimeout(CLOCK_REALTIME, _NTO_TIMEOUT_JOIN,</code></p>
     <p><code>  NULL, NULL, NULL);</code></p>
     <p><code> return (pthread_join(tid, rval));</code></p>
     <p><code>}</code></p>
     <subtitle>Тайм-ауты ядра при обмене сообщениями</subtitle>
     <p>Все становятся несколько сложнее, когда вы используете тайм-ауты ядра при обмене сообщениями. Вспомните главу «Обмен сообщениями», раздел «Обмен сообщениями и модель «клиент/сервер») — на момент отправки клиентом сообщения сервер может как ожидать его, так и нет. Это означает, что клиент может заблокироваться как по передаче (если сервер еще не принял сообщение), так и по ответу (если сервер принял сообщение, но еще не ответил). Основной смысл здесь в том, что вы должны предусмотреть оба блокирующих состояния в параметре <emphasis>flags</emphasis> функции <emphasis>TimerTimeout()</emphasis>, потому что клиент может оказаться в любом из них.</p>
     <p>Чтобы задать несколько состояний, сложите их операцией ИЛИ (OR):</p>
     <p><code>TimerTimeout(... _NTO_TIMEOUT_SEND | _NTO_TIMEOUT_REPLY,</code></p>
     <p><code> ...);</code></p>
     <p>Это вызовет тайм-аут всякий раз, когда ядро переведет клиента в состояние блокировки по передаче (SEND) или по ответу (REPLY). В тайм-ауте SEND-блокировки нет ничего особенного — сервер еще не принял сообщение, значит, ничего для этого клиента он не делает. Это значит, что если ядро генерирует тайм-аут для SEND-блокированного клиента, сервер об этом информировать не обязательно. Функция <emphasis>MsgSend()</emphasis> клиента возвратит признак ETIMEDOUT и обработка тайм-аута завершится.</p>
     <p>Однако, как было упомянуто в главе «Обмен сообщениями» (параграф «_NTO_CHF_UNBLOCK»), если сервер уже принял сообщение клиента, и клиент желает разблокироваться, для сервера существует два варианта реакции. Если сервер не указал флаг _NTO_CHF_UNBLOCK на канале, по которому было принято сообщение, клиент будет разблокирован немедленно, и сервер не получит об этом никакого оповещения. У большинства серверов, которые мне доводилось встречать, флаг _NTO_CHF_UNBLOCK был всегда установлен. В этом случае ядро посылает серверу импульс, а клиент остается заблокированным до тех пор, пока сервер ему не ответит! Как было показано в вышеупомянутом разделе главы «Обмен сообщениями», это сделано для того, чтобы сервер мог узнать о запросе клиента на разблокирование и выполнить по этому поводу какие-то действия.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Резюме</p>
    </title>
    <p>Мы рассмотрели функции QNX/Neutrino, ответственные за манипулирование временем, включая таймеры и их применение, а также тайм-ауты ядра. Относительные таймеры обеспечивают генерацию событий «через определенное число секунд», в то время как абсолютные таймеры генерируют события «в определенное время». Таймеры (и, вообще говоря, структура <code><strong>struct sigevent</strong></code>) могут обеспечить как выдачу импульса или сигнала, так и создание потока.</p>
    <p>Ядро создает таймеры, сохраняя абсолютное время, представляющее последующее «событие», в отсортированной очереди и сравнивая текущее время (при помощи обработчика прерываний таймера) с значением, расположенным в голове этой очереди. Когда текущее время становится больше или равно времени, хранящемуся в головном элементе очереди, очередь просматривается на предмет дополнительных совпадений, после чего ядро диспетчеризует события или потоки (в зависимости типа элемента очереди) и, возможно, производит перепланирование.</p>
    <p>Для обеспечения поддержки функций энергосбережения вы обязаны отключать периодические таймеры, когда в них нет необходимости, иначе энергосбережения как такового не произойдет — система будет все время думать, что у нее есть работа для периодического выполнения.</p>
   </section>
  </section>
  <section>
   <title>
    <p>Глава 4</p>
    <p>Прерывания</p>
   </title>
   <section>
    <title>
     <p>QNX/Neutrino и прерывания</p>
    </title>
    <section>
     <p>В данной главе мы рассмотрим прерывания, как с ними работать в QNX/Neutrino, их воздействие на диспетчеризацию и режим реального времени, а также некоторые стратегии их использования.</p>
     <p>Первый вопрос, который приходит на ум: «А что такое прерывание?»</p>
     <p>Прерывание — это в точности то, что определяется этим словом — прерывание того, что происходит в данный момент, и переход к выполнению другой задачи.</p>
     <p>Например, предположим, что вы сидите за своим рабочим столом и выполняете задание «А». Вдруг звонит телефон — Чрезвычайно Уважаемый Клиент (ЧУК) нуждается в вашем незамедлительном ответе на некий важный вопрос. После того как вы ответите на этот вопрос, вы сможете возвратиться к заданию «А»; впрочем, возможно, что этот ЧУК изменит ваши приоритеты, и вам придется отложить задание «А» и немедленно приступить к заданию «Б».</p>
     <p>Давайте теперь рассмотрим это в проекции на QNX/Neutrino.</p>
     <p>В любой момент времени процессор занят обработкой готового к выполнению потока с наивысшим приоритетом (этот поток будет находиться в состоянии RUNNING («выполняется»). Чтобы вызвать прерывание, подключенная к шине компьютера аппаратура выставляет сигнал на линии прерывания (в нашей аналогии это был телефонный звонок).</p>
     <p>Как только сигнал прерывания выставлен, ядро переключается на участок кода, который настраивает окружение для выполнения <emphasis>подпрограммы обработки прерывания</emphasis> (Interrupt Service Routine — ISR) — кода, который определяет, что должно происходить при обнаружении прерывания.</p>
     <p>Интервал времени от момента установки аппаратурой сигнала прерывания до выполнения первой инструкции обработчика прерываний называют <emphasis>временем реакции на прерывание</emphasis>. Время реакции на прерывание измеряется в микросекундах. Различные процессоры характеризуются различными временами реакции прерывание; это зависит от быстродействия процессора, архитектуры кэша, быстродействия памяти, и, конечно, от эффективности операционной системы.</p>
     <p>В нашей аналогии, если вы, например, слушаете музыку в наушниках и не слышите телефонного звонка, вам потребуется больше времени, чтобы обратить внимание на это «прерывание». В QNX/Neutrino может происходить то же самое, поскольку существует инструкция процессора, которая блокирует прерывания (для процессоров x86 это инструкция <code><strong>cli</strong></code>). Процессор не будет обращать внимание на какие бы то ни было прерывания до тех пор, пока они не будут разблокированы (инструкция <code><strong>sti</strong></code> для семейства x86).</p>
     <cite>
      <p><image l:href="#img_1.png"/>Чтобы избежать процессорно-зависимых вызовов на ассемблере, QNX/Neutrino обеспечивает четыре функции: <emphasis>InterruptEnable()</emphasis> и <emphasis>InterruptDisable()</emphasis>, и <emphasis>InterruptLock()</emphasis> и <emphasis>InterruptUnlock()</emphasis>. Эти функции принимают на себя все заботы о низкоуровневых деталях всех поддерживаемых платформ.</p>
     </cite>
     <p>Обработчик прерывания (ISR) обычно выполняет минимально возможный объем работы и завершается (в нашей аналогии это был бы краткий разговор по телефону с ЧУКом — не заставлять же заказчика ждать на линии несколько часов, пока мы сделаем работу! Достаточно сказать: «Не беспокойтесь, все будет сделано!»). Когда обработчик прерывания (ISR) завершается, он может либо сообщить ядру, что ничего больше делать не надо (это означает, что обработчик прерываний полностью завершил обработку события), либо что ядро должно выполнить некоторое действие, вследствие которого некий поток может переключиться в состояние READY («готов»).</p>
     <p>В нашей аналогии сообщение ядру о том, что прерывание полностью обработано, подобно сообщению клиенту ответа на поставленный вопрос — после этого можно спокойно вернуться к тому, что мы делали раньше, зная, что вопрос клиента отработан.</p>
     <p>Сообщение ядру о том, что требуется выполнить некоторое действие, подобно убеждению заказчика, что вы работаете над его проблемой и дополнительно сообщите, когда она будет решена. Трубка теперь повешена, но телефон может зазвонить опять.</p>
    </section>
    <section>
     <title>
      <p>Подпрограмма обработки прерывания</p>
     </title>
     <p>Обработчик прерывания (ISR) представляет собой фрагмент кода, ответственный за очистку источника прерывания.</p>
     <p>Это ключевой момент, особенно с учетом того, что прерывание имеет приоритет <emphasis>выше, чем приоритет любой программы</emphasis>. Это означает, что время, затрачиваемое на выполнение обработчика прерывания, может оказать серьезное воздействие на диспетчеризацию потоков. Время выполнения ISR должно быть минимальным. Давайте исследуем этот вопрос несколько подробнее.</p>
     <subtitle>Очистка источника прерываний</subtitle>
     <p>Аппаратное устройство, которое сгенерировало прерывание, будет удерживать сигнал прерывания до тех пор, пока не удостоверится в том, что прерывание успешно обработано. Поскольку аппаратура не умеет читать мысли, программа должна сообщить ей, что отреагировала на вызвавшую прерывание причину. Обычно это выполняется путем чтения регистра состояния из определенного порта ввода/вывода или блока данных из определенного адресного пространства памяти.</p>
     <p>При любом событии обычно есть некоторая форма подтверждения между аппаратными средствами и программным обеспечением, чтобы сбросить сигнал прерывания. (Впрочем, иногда подтверждение не предусматривается — например, когда аппаратные средства генерируют прерывание с полной уверенностью, что программное обеспечение обязательно его обработает.)</p>
     <p>Поскольку прерывание выполняется с более высоким приоритетом, чем любой программный поток, мы должны потратить как можно меньше времени на непосредственное выполнение обработчика прерывания, чтобы свести воздействие на диспетчеризацию к минимуму. Если очистка источника прерывания выполняется простым считыванием регистра и возможно, записью полученного значения в глобальную переменную, тогда наша задача проста.</p>
     <p>Обработка подобного рода выполняется обработчиком прерываний (ISR) последовательного порта. Аппаратура последовательного порта генерирует прерывание по приему символа. Обработчик считывает регистр, содержащий символ, записывает этот символ в кольцевой буфер. Сделано. Общее время на обработку: единицы микросекунд. Ну, собственно, так и должно быть. Представьте, что произошло бы, если бы вы принимали символы со скоростью 115 Кбод (примерно по символу каждые 100 микросекунд); если бы вы затрачивали на обработку прерывания что-то около 100 микросекунд, у вас бы больше ни на что не осталось времени!</p>
     <cite>
      <p><image l:href="#img_1.png"/>Не поймите меня неправильно. ISR последовательного порта может выполняться несколько дольше, потому что в нем еще предусмотрен опрос устройства на предмет наличия дополнительных символов в очереди.</p>
     </cite>
     <p>Понятно, что минимизацию времени, затрачиваемого на обработку прерывания, можно трактовать как «повышение качества обслуживания клиента». В нашей аналогии это минимизация времени занятости телефонной линии, чтобы другие клиенты не услышали сигнал «занято».</p>
     <p>А что если обработка слишком сложна? Есть два варианта развития событий:</p>
     <p>• Затраты времени на очистку источника прерывания невелики, но надо много чего сделать с оборудованием (клиент задал нам короткий вопрос, но на подготовку ответа требуется значительное время).</p>
     <p>• Затраты времени на очистку источника прерывания достаточно велики (клиент долго и запутанно объясняет свою проблему).</p>
     <p>В первом случае мы бы захотели очистить источник прерывания как можно быстрее, а затем приказать ядру переложить работу с медленной аппаратурой на некий поток. Преимущество такой схемы состоит в том, что ISR проводит на сверхвысоком приоритете минимальное количество времени, а остальная часть работы выполняется потоками на обычных приоритетах. Это подобно ситуации, когда вы подходите к телефону (сверхвысокий приоритет), а затем передаете фактическую работу одному из своих помощников. Далее в данной главе мы рассмотрим, как ISR предписывает ядру запланировать кого-то еще.</p>
     <p>Второй случай достаточно уродливый. Если ISR не очистит источник прерывания на момент своего завершения, ядро немедленно будет повторно прервано программируемым контроллером прерываний (Programmable Interrupt Controller — PIC; в процессорах серии x86 серии это микросхема Intel 8259 или ей эквивалентная).</p>
     <cite>
      <p><image l:href="#img_1.png"/>Специально для любителей контроллеров прерываний — мы вскоре кратко рассмотрим прерывания, активные как по уровню, так и по фронту.</p>
     </cite>
     <p>Таким образом, ISR так и будет работать все время и не даст активизироваться никакому потоку, который мог бы выполнить обработку.</p>
     <p>И какой же ущербный кусок железа может требовать продолжительного времени на очистку источника прерывания? Базовый контроллер дисковода PC удерживает сигнал прерывания на шине до тех пор, пока вы не прочитаете ряд его регистров состояния. К сожалению, данные в этих регистрах не всегда бывают доступны немедленно, и приходится опрашивать регистры на предмет поступления данных. Это может занять порядка миллисекунды — для компьютера это очень много!</p>
     <p>Чтобы решить эту проблему, надо временно <emphasis>маскировать прерывания</emphasis> — явно приказать контроллеру PIC игнорировать прерывания от определенного источника прерываний, пока вы не прикажете ему сделать обратное. В этом случае, даже при активном сигнале прерывания, контроллер PIC будет игнорировать его и ничего не скажет процессору. Это позволит вашему ISR запланировать поток, чтобы вынести работу с аппаратурой за пределы обработчика прерываний. Когда ваш поток закончит передачу данных от аппаратных средств, он может приказать контроллеру PIC демаскировать это прерывание. Это позволяет снова распознавать прерывания от данного аппаратного модуля. В нашей аналогии это подобно переводу звонка ЧУКа на вашего помощника.</p>
     <subtitle>Передача работы потоку</subtitle>
     <p>Как сделать так, чтобы ISR приказал ядру запланировать поток для выполнение некоторой работы? (Или, наоборот, как сказать ядру, что ему <emphasis>не следует</emphasis> так поступать?)</p>
     <p>Ниже приведен псевдокод типового ISR:</p>
     <p><code>FUNCTION ISR</code></p>
     <p><code>BEGIN</code></p>
     <p><code> определить источник прерывания</code></p>
     <p><code> очистить источник прерывания</code></p>
     <p><code> IF надо передать работу потоку THEN</code></p>
     <p><code>  RETURN (событие);</code></p>
     <p><code> ELSE</code></p>
     <p><code>  RETURN (NULL);</code></p>
     <p><code> END IF</code></p>
     <p><code>END</code></p>
     <p>Трюк здесь заключается в том, что вместо пустого указателя (NULL) можно возвратить некое событие (типа <code><strong>struct sigevent</strong></code>, мы говорили об этой структуре в главе «Часы, таймеры и периодические уведомления»).</p>
     <p>Отметим, что событие, которое вы возвращаете, должно продолжать существовать даже после того, как будет освобожден стек ISR (потому что локальные переменные хранятся в стеке — <emphasis>прим. ред</emphasis>.). Это означает, что событие должно либо быть описано вне ISR, либо передаваться из области устойчивых данных при помощи параметра ISR <emphasis>area</emphasis>, либо быть быть описано в пределах ISR как статическое. Это ваш выбор. Если вы возвращаете событие, ядро доставляет его потоку при возврате из вашего ISR. Поскольку событие «предупреждает» поток (путем передачи ему импульса, как мы говорили в главе «Обмен сообщениями», или сигнала), это может заставить ядро выполнить перепланирование потоков, желающих получить процессор. Если ваш ISR возвращает NULL, это оповещает ядро, что в дополнительных действиях на уровне потоков нет необходимости, и перепланирования не произойдет — будет продолжать выполняться поток, вытесненный вашим ISR.</p>
    </section>
    <section>
     <title>
      <p>Активность прерываний по уровню и по фронту</p>
     </title>
     <p>Недостает еще одного фрагмента мозаики. Большинство контроллеров прерываний могут быть запрограммированы на чувствительность либо к уровню сигнала прерывания, либо к его фронту.</p>
     <p>В режиме чувствительности по уровню считается, что сигнал прерывания выставлен, когда соответствующая линия шины находится в активном состоянии (это соответствует отметке «1» на рисунке ниже).</p>
     <image l:href="#img_38.png"/>
     <p>Выставление сигнала прерывания в режиме чувствительности по уровню.</p>
     <p>Из рисунка видно, что работа с прерыванием контроллером дисковода в таком режиме привела бы к вышеупомянутой проблеме. Каждый раз при завершении ISR ядро сообщает контроллеру прерываний: «Порядок, это прерывание обработано. Сообщи мне, когда оно возникнет снова.» (отметка «2» на рисунке). Говоря техническим языком, ядро посылает контроллеру сигнал EOI (End Of Interrupt — «конец прерывания»). Контроллер PIC анализирует линию прерывания и если она все еще активна, он немедленно прерывает ядро заново (отметка «3»).</p>
     <p>Мы могли бы обойти эту проблему, перепрограммировав контроллер прерываний в режим чувствительности по фронту.</p>
     <p>В этом режиме прерывания распознаются контроллером только по переднему фронту сигнала.</p>
     <image l:href="#img_39.png"/>
     <p>Выставление сигнала прерывания в режиме чувствительности по фронту.</p>
     <p>Здесь, даже если обработчик прерываний не очищает источник прерывания, после передачи ядром контроллеру сигнала EOI контроллер не может заново прервать ядро, потому что другого переднего фронта на линии прерывания после передачи EOI не будет. Для распознавания следующего прерывания на данной линии ее сначала будет необходимо деактивировать (отметка «4»), а затем активировать вновь (отметка «1»).</p>
     <p>Похоже, что все наши проблемы решены! Будем использовать режим чувствительности по фронту и жить счастливо. Но, к сожалению, у режима чувствительности по фронту тоже есть свои проблемы.</p>
     <p>Предположим, что ваш ISR не очистил источник прерывания. Когда ядро выдаст контроллеру сигнал EOI, аппаратные средства будут по-прежнему удерживать сигнал прерывания в активном состоянии. Однако, поскольку контроллер работает в режиме чувствительности по фронту, все последующие прерывания от этого устройства он не увидит.</p>
     <p>Что же это за добрый парень, который так пишет ISR, чтобы тот забыл очистить источник прерывания? К сожалению, готовых рецептов здесь нет. Представьте себе ситуацию, когда два устройства (например адаптер SCSI и адаптер Ethernet), разделяющих одну и ту же линию прерывания на позволяющей это шине. (Сейчас вы скажете: «Да ну, какой придурок будет так делать?!» Ну, это иногда случается, особенно когда не хватает свободных прерываний...)</p>
     <p>В этом случае одному и тому же вектору прерывания соответствовали бы два ISR (это, кстати, допустимо), и ядро при получении прерывания по этой линии вызывало бы их каждый раз поочередно.</p>
     <p>Разделяемые прерывания без перекрытия.</p>
     <p>В этом случае, поскольку только одно из аппаратных устройств (плата SCSI) было активно, когда отработал связанный с ним обработчик корректно очистил источник прерывания (этап 2.) Отметьте, что ядро вызывает ISR для платы Ethernet (этап 3) независимо ни от чего — оно просто не знает, какое конкретное устройство требовало обслуживания, поэтому всегда отрабатывает всю цепочку.</p>
     <p>А теперь представьте себе другой случай:</p>
     <image l:href="#img_40.png"/>
     <p>Разделяемые прерывания с перекрытием.</p>
     <p>Это как раз та самая проблемная ситуация.</p>
     <p>Устройство Ethernet запрашивает прерывание первым. Это приводит к тому, что выставляется сигнал прерывания (передний фронт импульса распознается контроллером прерываний), и ядро вызывает первый в очереди обработчик прерывания (драйвер SCSI; этап 1). Обработчик драйвера SCSI смотрит на свои аппаратные средства и говорит: «Не, это не мое. Ладно, забудь» (этап 2). Затем ядро вызывает следующий обработчик прерывания в очереди, соответствующий плате Ethernet (этап 3). Обработчик драйвера Ethernet смотрит на свои аппаратные средства и восклицает: «О! Мои аппаратные средства запросили прерывание! Надо очистить источник». И тут, как назло, как раз в процессе очистки устройство SCSI генерирует прерывание (этап 4).</p>
     <p>Когда ISR платы Ethernet завершит очистку источника прерываний (этап 5), сигнал прерывания по-прежнему останется выставлен вследствие возникшего прерывания от устройства SCSI. Однако, контроллер прерываний, запрограммированный на режим чувствительности по фронту, реагирует на переход группового сигнала прерывания из неактивного состояния в активное. А этого не будет, потому что ядро уже вызвало оба обработчика прерываний и теперь ждет другое прерывание от контроллера.</p>
     <p>В этом случае подходящим решением был бы режима чувствительности по уровню, потому что когда ISR Ethernet завершится, и ядро выдаст контроллеру сигнал EOI, контроллер сможет распознать, что сигнал прерывания все еще активен, и прервет ядро заново. Тогда ядро снова прошло бы по всей цепочке ISR, и на этот раз обработкой занялся бы ISR драйвера SCSI.</p>
     <p>Выбор режима чувствительности зависит от типа аппаратных средств и стартового кода. Некоторые аппаратные средства поддерживают только либо один, либо другой режим. Аппаратные средства, которые поддерживают оба режима, могут быть запрограммированы на тот или иной режим стартовым кодом. За окончательным ответом обращайтесь к документации по BSP (Board Support Package — пакет поддержки платы), поставляемого с вашей системой.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Написание обработчиков прерываний</p>
    </title>
    <section>
     <p>Давайте посмотрим, как настроить обработчики прерываний — вызовы, характеристики и кое-какие стратегии реализации.</p>
    </section>
    <section>
     <title>
      <p>Подключение обработчиков прерываний</p>
     </title>
     <p>Для подключения к источнику прерывания воспользуйтесь функцией <emphasis>InterruptAttach()</emphasis> или <emphasis>InterruptAttachEvent()</emphasis>.</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int InterruptAttachEvent(int intr,</code></p>
     <p><code> const struct sigevent *event, unsigned flags);</code></p>
     <empty-line/>
     <p><code>int InterruptAttach(int intr,</code></p>
     <p><code> const struct sigevent* (*handler)(void *area, int id),</code></p>
     <p><code> const void *area, int size, unsigned flags);</code></p>
     <p>Параметр <emphasis>intr</emphasis> определяет, к какому прерыванию вы хотите подключить обработчик.</p>
     <p>Передаваемые значения определяются стартовым кодом, который перед запуском QNX/Neutrino, среди прочего, инициализирует контроллер обработки прерываний. (В документации по QNX/ Neutrino приводится подробная информация о стартовом коде; см. Справочник по утилитам, главы <code><strong>startup-*</strong></code>, например, <code><strong>startup-p5064</strong></code>.)</p>
     <p>Уже здесь функции <emphasis>InterruptAttach()</emphasis> и <emphasis>InterruptAttachEvent()</emphasis> различаются. Рассмотрим сначала функцию <emphasis>InterruptAttachEvent()</emphasis>, как более простую. Затем вернемся к рассмотрению функции <emphasis>InterruptAttach()</emphasis>.</p>
     <subtitle>Подключение с помощью функции <emphasis>InterruptAttachEvent()</emphasis></subtitle>
     <p>Функция <emphasis>InterruptAttachEvent()</emphasis> принимает два дополнительных аргумента: event, который является указателем на структуру <code><strong>struct sigevent</strong></code>, описывающую генерируемое событие, а также <emphasis>flags</emphasis>. Функция <emphasis>InterruptAttachEvent()</emphasis> сообщает ядру, что при обнаружении прерывания должно быть сгенерировано событие <emphasis>event</emphasis>, после чего данный уровень прерываний должен быть демаскирован. Отметьте, что за то, как интерпретировать событие и какой поток перевести в состояние READY, отвечает ядро, при обнаружении прерывания.</p>
     <subtitle>Подключение с помощью функции <emphasis>InterruptAttach()</emphasis></subtitle>
     <p>При использовании функции <emphasis>InterruptAttach()</emphasis> мы определяем другой набор параметров. Параметр <emphasis>handler</emphasis> — это адрес функции, которую надо вызвать. Как видно из прототипа, функция <emphasis>handler()</emphasis> возвращает структуру <code><strong>struct sigevent</strong></code> (указывающую на тип события, которое следует сгенерировать) и принимает два параметра. Первый передаваемый параметр — <emphasis>area</emphasis>, тот самый, который передается функции <emphasis>InterruptAttach()</emphasis>. Второй параметр, <emphasis>id</emphasis>, — идентификатор прерывания, его также возвращает <emphasis>InterruptAttach()</emphasis>. Он применяется для идентификации прерывания, а также для маскирования, демаскирования, блокировки и деблокировки прерывания. Четвертый параметр <emphasis>InterruptAttach()</emphasis>, <emphasis>size</emphasis>, указывает размер (в байтах) области данных, которая передается в параметре <emphasis>area</emphasis>. И, наконец, пятый параметр — <emphasis>flags</emphasis> — тот же самый параметр <emphasis>flags</emphasis>, что и у <emphasis>InterruptAttachEvent()</emphasis>; мы скоро к нему вернемся.</p>
     <subtitle>Теперь, когда вы подключились к прерыванию</subtitle>
     <p>Допустим, что вы уже вызвали функцию <emphasis>InterruptAttachEvent()</emphasis> или <emphasis>InterruptAttach()</emphasis>.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Поскольку подключение к прерываниям — не та вещь, которую вы хотели бы позволять кому попало, QNX/Neutrino позволяет делать это только потокам, у которых есть право «привилегированного ввода/вывода» («I/O privity») (см. функцию <emphasis>ThreadCtl()</emphasis> в справочном руководстве по Си-библиотеке QNX/Neutrino). Получить это право могут только потоки, которые выполняются под идентификатором пользователя root или которые установили свой идентификатор пользователя в root при помощи <emphasis>setuid()</emphasis>. Следовательно, реально эти права есть только у root. (Что, кстати, вполне логично — зачем несуперпользовательскому процессу привилегированный ввод/вывод? — <emphasis>прим. ред</emphasis>.)</p>
     </cite>
     <p>Ниже приведен фрагмент программы, в котором реализовано подключение ISR к вектору аппаратного прерывания, идентифицируемого константой HW_SERIAL_IRQ:</p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <empty-line/>
     <p><code>int InterruptID;</code></p>
     <empty-line/>
     <p><code>const struct sigevent* intHandler(void *arg, int id) {</code></p>
     <p><code> ...</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>int main (int argc, char **argv) {</code></p>
     <p><code> interruptID =</code></p>
     <p><code>  InterruptAttach(HW_SERIAL_IRQ, intHandler, sevent,</code></p>
     <p><code>   sizeof(event), 0);</code></p>
     <p><code> if (interruptID == -1 {</code></p>
     <p><code>  fprintf(stderr, "%s: ошибка подключения к IRQ %d\n",</code></p>
     <p><code>   progname, W_SERIAL_IRQ);</code></p>
     <p><code>  perror(NULL);</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> ...</code></p>
     <p><code> return (EXIT_SUCCESS);</code></p>
     <p><code>}</code></p>
     <p>Теперь, если по данному вектору произойдет прерывание, наш обработчик будет подвергнут диспетчеризации. При вызове функции <emphasis>InterruptAttach()</emphasis> ядро демаскирует указанный источник прерываний на уровне контроллера (если это прерывание еще не демаскировано, что имело бы место в случае многочисленных обработчиков одного и то же прерывания).</p>
    </section>
    <section>
     <title>
      <p>Отключение обработчика прерывания</p>
     </title>
     <p>Когда вы закончили с обработчиком прерывания, вы можете пожелать уничтожить связь между ним и вектором:</p>
     <p><code>int InterruptDetach(int id);</code></p>
     <p>Я сказал «можете», потому что обрабатывающие прерывания потоки, как правило, используются в серверах, а серверы обычно не завершаются. Это часто ведет к предрассудку, что хорошо организованному серверу никогда не понадобится самостоятельно вызывать <emphasis>InterruptDetach()</emphasis>. К тому же, при смерти потока или процесса ОС автоматически отключит все связанные с ним обработчики прерываний. Таким образом, если программа просто дойдет до конца <emphasis>main()</emphasis>, вызовет <emphasis>exit()</emphasis> или завершится по SIGSEGV, все ее ISR будут автоматически отключены от соответствующих векторов прерываний. (Впрочем, вы, вероятно, пожелаете сделать это несколько изящнее, запретив соответствующему устройству генерацию прерываний. Если же прерывание разделяемое, и его используют другие устройства, то здесь двух вариантов быть не может вообще — вы просто обязаны «убрать за собой», иначе у вас либо больше не будет прерываний (в режиме чувствительности по фронту), либо пойдет постоянный поток запросов на прерывание (в режиме чувствительности по уровню).</p>
     <p>Продолжая вышеприведенный пример, если бы мы захотели отключиться от прерывания, то мы использовали бы следующий код:</p>
     <p><code>void terminateInterrupts(void) {</code></p>
     <p><code> InterruptDetach(interruptID);</code></p>
     <p><code>}</code></p>
     <p>Если это последний ISR, связанный с данным вектором прерывания, то ядро автоматически произведет маскирование источника прерывания на уровне контроллера, чтобы таких прерываний больше не возникало.</p>
    </section>
    <section>
     <title>
      <p>Параметр <emphasis>flags</emphasis></p>
     </title>
     <p>Последний параметр, <emphasis>flags</emphasis>, управляет различными дополнительными опциями:</p>
     <p>_NTO_INTR_FLAGS_END</p>
     <p>Указывает, что данный обработчик должен сработать после всех других обработчиков данного прерывания (если они есть).</p>
     <p>_NTO_INTR_FLAGS_PROCESS</p>
     <p>Указывает на то, что данный обработчик связан с процессом, а не с потоком. Что из этого вытекает, так это условие автоматического отключения обработчика. Если вы определяете этот флаг, обработчик будет автоматически отключен от источника прерывания при завершении процесса. Если этот флаг не определен, обработчик прерывания будет отключен от источника, когда завершится поток, подключивший его.</p>
     <p>_NTO_INTR_FLAGS_TRK_MSK</p>
     <p>Указывает, что ядро должно отследить, сколько раз данное прерывание было маскировано. Это приводит к несколько большей загрузке ядра, но это необходимо для корректного демаскирования источника прерываний при завершении потока или процесса.</p>
    </section>
    <section>
     <title>
      <p>Обработчик прерывания</p>
     </title>
     <p>Давайте рассмотрим собственно обработчик прерывания. В первом примере применим <emphasis>InterruptAttach()</emphasis>, а затем рассмотрим аналогичный случай, только с применением функции <emphasis>InterruptAttachEvent()</emphasis>.</p>
     <subtitle>Применение функции <emphasis>InterruptAttach()</emphasis></subtitle>
     <p>В продолжение примера приведем функцию <emphasis>intHandler()</emphasis> — наш обработчик прерывания. Она отвечает за микросхему последовательного порта 8250 (допустим, что она генерирует прерывание HW_SERIAL_IRQ).</p>
     <p><code>/*</code></p>
     <p><code> * int1.c</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <p><code>#define REG_RX   0</code></p>
     <p><code>#define REG_II   2</code></p>
     <p><code>#define REG_LS   5</code></p>
     <p><code>#define REG_MS   6</code></p>
     <p><code>#define IIR_MASK 0x07</code></p>
     <p><code>#define IIR_MSR  0x00</code></p>
     <p><code>#define IIR_THE  0x02</code></p>
     <p><code>#define IIR_RX   0x04</code></p>
     <p><code>#define IIR_LSR  0x06</code></p>
     <p><code>#define IIR_MASK 0x07</code></p>
     <empty-line/>
     <p><code>volatile int serial_msr; // Сохраненное значение</code></p>
     <p><code>                         // регистра состояния модема</code></p>
     <p><code>volatile int serial_rx; // Сохраненное значение</code></p>
     <p><code>                        // регистра приема</code></p>
     <p><code>volatile int serial_lsr; // Сохраненное значение</code></p>
     <p><code>                         // регистра состояния линии</code></p>
     <empty-line/>
     <p><code>static int base_reg = 0x2f8;</code></p>
     <p><code>const struct sigevent* intHandler(void *arg, int id) {</code></p>
     <p><code> int iir;</code></p>
     <p><code> struct sigevent *event = (struct sigevent*)arg;</code></p>
     <empty-line/>
     <p><code> /*</code></p>
     <p><code>  * Определить (и очистить) источник прерывания</code></p>
     <p><code>  * чтением регистра идентификации прерывания</code></p>
     <p><code> */</code></p>
     <p><code> iir = in8(base_reg + REG_II) &amp; IIR_MASK;</code></p>
     <p><code> /* Нет прерывания? */</code></p>
     <p><code> if (iir &amp; 1) {</code></p>
     <p><code>  /* Значит, нет и события */</code></p>
     <p><code>  return (NULL);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> /*</code></p>
     <p><code>  * Выяснить, что вызвало прерывание, и определить, надо ли</code></p>
     <p><code>  * потоку что-нибудь с этим делать.</code></p>
     <p><code>  * (Константы основаны на строении регистра</code></p>
     <p><code>  * идентификации прерывания 8250.)</code></p>
     <p><code> */</code></p>
     <p><code> switch (iir) {</code></p>
     <p><code> case IIR_MSR:</code></p>
     <p><code>  serial_msr = in8(base_reg + REG_MS);</code></p>
     <p><code>  /* Разбудить поток */</code></p>
     <p><code>  return (event);</code></p>
     <p><code>  break;</code></p>
     <p><code> case IIR_THE:</code></p>
     <p><code>  /* Ничего не делать */</code></p>
     <p><code>  break;</code></p>
     <p><code> case IIR_RX:</code></p>
     <p><code>  /* Считать символ */</code></p>
     <p><code>  serial_rx = in8(base_reg + REG_RX);</code></p>
     <p><code>  break;</code></p>
     <p><code> case IIR_LSR:</code></p>
     <p><code>  /* Сохранить регистр состояния линии */</code></p>
     <p><code>  serial_lsr = in8(base_reg + REG_LS);</code></p>
     <p><code>  break;</code></p>
     <p><code> default:</code></p>
     <p><code>  break;</code></p>
     <p><code> }</code></p>
     <p><code> /* Никого не беспокоить */</code></p>
     <p><code> return (NULL);</code></p>
     <p><code>}</code></p>
     <p>Первое, что бросается в глаза, — что все переменные, к которым обращается ISR, должны быть объявлены как <code><strong>volatile</strong></code>. В с единственным процессором это делается не для блага обработчиков прерываний, а для облегчения жизни потокам, которые могут быть прерваны обработчиком прерывания в любой момент. Конечно, в многопроцессорной ЭВМ обработчики прерываний вполне могли бы выполняться одновременно с кодом потоков, и в таких случаях надо быть предельно осторожными с вещами подобного рода.</p>
     <p>С помощью ключевого слова <code><strong>volatile</strong></code> мы указываем компилятору не кэшировать значения этих переменных, поскольку они могут быть изменены в любой точке выполнения программы.</p>
     <p>Следующее, на что мы обращаем внимание — это прототип самого обработчика прерывания. Он обозначен как <code><strong>const struct sigevent*</strong></code>. Это говорит о том, что подпрограмма <emphasis>intHandler()</emphasis> возвращает указатель на <code><strong>struct sigevent</strong></code>. Это стандарт для всех подпрограмм обработки прерываний.</p>
     <p>Наконец, обратите внимание на то, что решение, передавать или не передавать событие потоку, принимает сам обработчик. Здесь мы генерируем событие только в случае прерывания по изменению регистра состояния модема (MSR) (событие определяется переменной <emphasis>event</emphasis>, которая передается обработчику прерывания в момент его подключения). Во всех других случаях мы игнорируем прерывание (и обновляем кое-какие глобальные переменные); однако, источник прерывания мы очищаем во всех случаях. Это выполняется считыванием порта ввода/вывода с помощью вызова <emphasis>in8()</emphasis>.</p>
     <subtitle>Применение функции <emphasis>InterruptAttachEvent()</emphasis></subtitle>
     <p>Если бы мы должны были переписать вышеприведенную программу с применением функции <emphasis>InterruptAttachEvent()</emphasis>, это бы выглядело так:</p>
     <p><code>/*</code></p>
     <p><code> * Фрагмент int2.c</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <p><code>#define HW_SERIAL_IRQ 3</code></p>
     <p><code>#define REG_RX   0</code></p>
     <p><code>#define REG_II   2</code></p>
     <p><code>#define REG_LS   5</code></p>
     <p><code>#define REG_MS   6</code></p>
     <p><code>#define IIR_MASK 0x07</code></p>
     <p><code>#define IIR_MSR  0x00</code></p>
     <p><code>#define IIR_THE  0x02</code></p>
     <p><code>#define IIR_RX   0x04</code></p>
     <p><code>#define IIR_LSR  0x06</code></p>
     <p><code>#define IIR_MASK 0x07</code></p>
     <empty-line/>
     <p><code>static int base_reg = 0x2f8;</code></p>
     <empty-line/>
     <p><code>int main(int arcgc, char **argv) {</code></p>
     <p><code> int intId; // Идентификатор прерывания</code></p>
     <p><code> int iir; // Регистр идентификации</code></p>
     <p><code>          // прерывания</code></p>
     <p><code> int serial_msr; // Сохраненное значение</code></p>
     <p><code>                  // регистра состояния модема</code></p>
     <p><code> int serial_rx; // Сохраненное значение регистра</code></p>
     <p><code>                 // приема</code></p>
     <p><code> int serial_lsr; // Сохраненное значение</code></p>
     <p><code>                  // регистра состояния линии</code></p>
     <p><code> struct sigevent event;</code></p>
     <p><code> // Обычная настройка main()...</code></p>
     <p><code> // Настроить событие</code></p>
     <p><code> intId = InterruptAttachEvent(HW_SERIAL_IRQ, &amp;event, 0);</code></p>
     <p><code> for (;;) {</code></p>
     <p><code>  // Ждать события от прерывания</code></p>
     <p><code>  // (можно было использовать MsgReceive)</code></p>
     <p><code>  InterruptWait(0, NULL);</code></p>
     <p><code>  /*</code></p>
     <p><code>   * Определить (и очистить) источник прерывания</code></p>
     <p><code>   * чтением регистра идентификации прерывания</code></p>
     <p><code>  */</code></p>
     <p><code>  iir = in8(base_reg + REG_II) &amp; IIR_MASK;</code></p>
     <p><code>  // Демаскировать прерывание, чтобы оно</code></p>
     <p><code>  // могло сработать снова</code></p>
     <p><code>  InterruptUnmask(HW_SERIAL_IRQ, intId);</code></p>
     <p><code>  /* Нет прерывания? */</code></p>
     <p><code>  if (iir &amp; 1) {</code></p>
     <p><code>   /* Ждать нового */</code></p>
     <p><code>   continue;</code></p>
     <p><code>  }</code></p>
     <p><code>  /*</code></p>
     <p><code>   * Выяснить, что вызвало прерывание,</code></p>
     <p><code>   * и надо ли что-то с этим делать</code></p>
     <p><code>  */</code></p>
     <p><code>  switch (iir) {</code></p>
     <p><code>  case IIR_MSR:</code></p>
     <p><code>   serial_msr = in8(base_reg + REG_MS);</code></p>
     <p><code>   /*</code></p>
     <p><code>    * Выполнить какую-нибудь обработку...</code></p>
     <p><code>   */</code></p>
     <p><code>   break;</code></p>
     <p><code>  case IIR_THE:</code></p>
     <p><code>   /* He делать ничего */</code></p>
     <p><code>   break;</code></p>
     <p><code>  case IIR_RX:</code></p>
     <p><code>   /* Считать символ */</code></p>
     <p><code>   serial_rx = in8(base_reg + REG_RX);</code></p>
     <p><code>   break;</code></p>
     <p><code>  case IIR_LSR:</code></p>
     <p><code>   /* Запомнить регистр состояния линии */</code></p>
     <p><code>   serial_lsr = in8(base_reg + REG_LS);</code></p>
     <p><code>   break;</code></p>
     <p><code>  }</code></p>
     <p><code> }</code></p>
     <p><code> /* Сюда мы не доберемся */</code></p>
     <p><code> return (0);</code></p>
     <p><code>}</code></p>
     <p>Обратите внимание, что функция <emphasis>InterruptAttachEvent()</emphasis> возвращает идентификатор прерывания (небольшое целое число). Мы сохранили это значение в переменной <emphasis>intId</emphasis>, чтобы впоследствии смогли с его помощью демаскировать прерывание. После того как мы подключились к прерыванию, мы должны ждать его возникновения. Поскольку в данном случае мы применили функцию <emphasis>InterruptAttachEvent()</emphasis>, при каждом возникновении прерывания мы будем получать соответствуют предварительно созданное событие. Сравните это с предыдущим случаем, где применялась <emphasis>InterruptAttach()</emphasis> — там решение о том, посылать событие или нет, принимал сам обработчик. При использовании функции <emphasis>InterruptAttachEvent()</emphasis> ядро понятия не имеет, было ли аппаратное прерывание «существенным» для нас или нет — оно просто каждый раз генерирует событие, затем маскирует соответствующее прерывание и предоставляет нам самим возможность решать, насколько это важно и что с этим делать.</p>
     <p>В примере с <emphasis>InterruptAttach()</emphasis> мы принимали решение путем возврата либо <code><strong>struct sigevent</strong></code>, чтобы указать, что что-то должно произойти, либо константы NULL. Обратите внимание на изменения в программе в варианте с <emphasis>InterruptAttachEvent()</emphasis>:</p>
     <p>• «Работа ISR» теперь выполняется потоком — в функции <emphasis>main()</emphasis>.</p>
     <p>• Теперь мы должны всегда демаскировать источник прерывания после получения нашего события (потому что ядро маскирует его для нас).</p>
     <p>• Если прерывание для нас является несущественным, мы не будем ничего предпринимать и просто пойдем дальше по программному циклу в ожидании другого прерывания.</p>
     <p>• Если прерывание существенно для нас, мы непосредственно обрабатываем его (см. блок операторов <code><strong>case IIR_MSR</strong></code>).</p>
     <p>Где в программе очищать источник прерывания — это зависит от ваших аппаратных средств и от выбранной схемы уведомления. При использовании SIGEV_INTR в сочетании с <emphasis>InterruptWait()</emphasis> ядро не ставит в очередь более одного уведомления; при использовании же SIGEV_PULSE в сочетании с <emphasis>MsgReceive()</emphasis> поставлены в очередь будут все. Если вы используете сигналы (например, в сочетании с SIGEV_SIGNAL), вы сами определяете, ставить их в очередь или нет. Одни аппаратные средства требуют очистки источника прерываний до начала чтения данных, другие — нет, и можно читать данные из них при выставленном сигнале прерывания.</p>
     <p>Такой сценарий как ISR, возвращающий SIGEV_THREAD, повергает меня в дикий ужас. Настоятельно рекомендую по возможности избегать этого «приема».</p>
     <p>В вышеприведенном примере с программированием последовательного порта мы приняли решение использовать функцию <emphasis>InterruptWait()</emphasis>, которая ставит в очередь только одно событие. Аппаратура последовательного порта может выставить еще одно прерывание сразу после чтения нами регистра идентификации прерываний (IIR), но это нормально, потому что в очередь будет поставлен максимум один SIGEV_INTR. Мы подберем это уведомление на следующей итерации цикла <code><strong>for</strong></code>.</p>
     <subtitle>Различия между <emphasis>InterruptAttach()</emphasis> и <emphasis>InterruptAttachEvent()</emphasis></subtitle>
     <p>Напрашивается естественный вопрос: «Когда и какую функцию выбирать?»</p>
     <p>Наиболее очевидное преимущество <emphasis>InterruptAttachEvent()</emphasis> состоит в том, что применять ее значительно проще, чем <emphasis>InterruptAttach()</emphasis> — поскольку нет никакого ISR, его и отлаживать не надо. Другое преимущество состоит в том, что поскольку в пространстве ядра ничего не выполняется (a ISR делал бы именно так), нет никакой опасности разрушить систему — если вы столкнетесь с ошибкой программирования, то пострадает конкретный процесс, а не система в целом. Однако, применение этой функции будет более или менее эффективным по сравнению с <emphasis>InterruptAttach()</emphasis> в зависимости от того, чего вы пытаетесь достичь. Этот вопрос достаточно сложен, и сводить его к нескольким словам (типа «быстрее» или «лучше») было бы неправильно. Давайте рассмотрим несколько возможных сценариев.</p>
     <p>Вот что происходит, когда мы используем <emphasis>InterruptAttach()</emphasis>:</p>
     <image l:href="#img_41.png"/>
     <p>Поток управления при использовании <emphasis>InterruptAttach()</emphasis>.</p>
     <p>Выполняющийся поток («Поток 1») прерывается, и мы переключаемся в ядро. Ядро сохраняет контекст «Потока 1». Затем ядро смотрит, кто ответственен за обработку данного прерывания и решает, что это «ISR». Ядро настраивает контекст для «ISR» и передает ему управление. «ISR» опрашивает аппаратуру и решает возвратить <code><strong>struct sigevent</strong></code>. Ядро отмечает возвращаемое событие, выясняет, кто должен его обработать, и переводит их в состояние READY. Это может привести к планированию ядром другого потока, «Потока 2».</p>
     <p>Теперь давайте сопоставим это с тем, что будет происходить при использовании <emphasis>InterruptAttachEvent()</emphasis>:</p>
     <image l:href="#img_42.png"/>
     <p>Поток управления при использовании <emphasis>InterruptAttachEvent()</emphasis>.</p>
     <p>В этом случае путь обслуживания прерываний намного короче. Мы выполнили одно переключение контекста от выполнявшегося потока («Поток 1») в ядро. Вместо второго переключения контекста в ISR ядро просто «притворилось», что получило от ISR <code><strong>struct sigevent</strong></code> и среагировало на него, запланировав «Поток 2».</p>
     <p>Теперь вы думаете: «Великолепно! Забудем про <emphasis>InterruptAttach()</emphasis> и будем использовать простую функцию <emphasis>InterruptAttachEvent()</emphasis>.»</p>
     <p>Это не такая хорошая идея, как кажется на первый взгляд, потому что вы можете и не захотеть просыпаться от каждого прерывания, генерируемого аппаратурой! Вернитесь к примеру, который приведен выше — событие там возвращалось только тогда, когда изменялся регистр состояния модема, а не по приему символа, изменению регистра состояния линии или опустошению буфера передачи.</p>
     <p>В этом случае, особенно если бы последовательный порт принимал символы (которые вы хотели бы проигнорировать), вы бы потратили много времени впустую на перепланирование своего потока — и только ради того, чтобы он проанализировал состояние последовательного порта и принял решение, что ничего делать не надо. В данном случае все бы выглядело примерно так:</p>
     <image l:href="#img_43.png"/>
     <p>Поток управления при использовании <emphasis>InterruptAttachEvent()</emphasis> с излишним перепланированием.</p>
     <p>Происходящее по сути заключается в том, что вы вызываете переключение контекста для перехода к «Потоку 2», он опрашивает аппаратуру и понимает, что делать ничего не требуется, и это влечет за собой еще одно лишнее переключение контекста обратно в «Поток 1».</p>
     <p>Вот что произошло бы, если бы вы применили функцию <emphasis>InterruptAttach()</emphasis>, но не пожелали планировать другой поток (т.е. просто вернулись обратно):</p>
     <image l:href="#img_44.png"/>
     <p>Поток управления при использовании <emphasis>InterruptAttach()</emphasis> без перепланирования потоков.</p>
     <p>Ядро знает, что выполнялся «Поток 1», и что ISR не сказал ему что-либо сделать, поэтому после прерывания оно может смело вернуть управление «Потоку 1».</p>
     <p>Для справки: вот что делает функция <emphasis>InterruptAttachEvent()</emphasis> (это не реальный исходный текст, поскольку функция <emphasis>InterruptAttachEvent()</emphasis> в действительности связывает с ядром структуру данных — она не реализована как отдельная вызываемая функция!):</p>
     <p><code>// «Внутренний» обработчик</code></p>
     <p><code>static const struct sigevent*</code></p>
     <p><code>internalHandler(void *arg, int id) {</code></p>
     <p><code> struct sigevent *event = arg;</code></p>
     <p><code> InterruptMask(intr, id);</code></p>
     <p><code> return (arg);</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>int InterruptAttachEvent(int intr,</code></p>
     <p><code>const struct sigevent *event, unsigned flags) {</code></p>
     <p><code> static struct sigevent static_event;</code></p>
     <p><code> memcpy(&amp;static_event, event, sizeof(static_event));</code></p>
     <p><code> return</code></p>
     <p><code>  (InterruptAttach(intr, internalHandler, &amp;static_event,</code></p>
     <p><code>   sizeof(*event), flags));</code></p>
     <p><code>}</code></p>
     <subtitle>Что выбрать?</subtitle>
     <p>Так какую функцию применять? От редко возникающих прерываний почти всегда можно отмахнуться применением <emphasis>InterruptAttachEvent()</emphasis>. Поскольку прерывания будут происходить редко, даже лишние перепланирования потоков значительного воздействия на общую производительность системы не окажут. Единственный момент, когда это проявится — это если на данном прерывании будут «сидеть» еще и другие устройства; в этом случае, поскольку функция <emphasis>InterruptAttachEvent()</emphasis> маскирует источник прерывания, то это прерывание останется заблокированным до тех пор, пока источник не будет демаскирован обратно. Если при этом первое устройство требует много времени на обслуживание, остальным придется все это время ждать демаскирования. По большому счету, это проблема аппаратной организации системы — не следует размещать медленные устройства на одной линии прерывания с быстрыми.</p>
     <p>При выборе функции для более часто возникающих прерываний требуется учесть множество факторов:</p>
     <p>• Ненужные прерывания — если их число будет существенным, лучше применять <emphasis>InterruptAttach()</emphasis> и отфильтровывать их прямо в ISR, Возьмем, например, тот же случай с последовательным устройством. Поток может выдать команду: «Дай мне 64 байта». Если ISR запрограммирован с учетом того, что пока не будут приняты все 64 байта, ничего полезного не произойдет, все возникающие в процессе приема прерывания будут отфильтрованы. ISR возвратит событие только после окончания приема всех 64 байт.</p>
     <p>• Время реакции — если ваши аппаратные средства чувствительны к интервалу времени от момента выставления запроса на прерывание до отработки ISR, вам следует использовать <emphasis>InterruptAttach()</emphasis>, чтобы свести это время к минимуму. Это сработает, потому что диспетчеризация ISR в ядре выполняется очень быстро.</p>
     <p>• Буферизация — если ваша аппаратура имеет встроенные средства буферизации, вы можете обойтись функцией <emphasis>InterruptAttachEvent()</emphasis> и очередью из единственного события, как в случае с комбинацией SIGEV_INTR и <emphasis>InterruptWait()</emphasis>. Этот метод позволяет прерываниям возникать с такой частотой, как им захочется» при этом позволяя вашему потоку выбирать значения из буфера с такой скоростью, с какой он сможет. Поскольку данные буферизуются на аппаратном уровне, никаких проблем со временем реакции на прерывание не будет.</p>
    </section>
    <section>
     <title>
      <p>Функции, которые может вызывать ISR</p>
     </title>
     <p>Следующий вопрос, за который следует взяться, — это список функций, которые может вызывать ISR.</p>
     <p>Небольшое отступление. Исторически, причина основных затруднений при написании обработчиков прерываний заключалась (и в большинстве других операционных систем до сих пор заключается) в том, что ISR работают в особом окружении.</p>
     <p>Одна из конкретных причин, усложняющих написание ISR, состоит в том, что с точки зрения ядра ISR на самом деле не является «полноправным» потоком. С позиции ядра это, если хотите, такой таинственный «аппаратный» поток. Это означает, что ISR не имеет права делать никаких манипуляций «на уровне потока» — таких как, например, обмен сообщениями, синхронизация, системные вызовы, дисковый ввод/вывод, и т.д.</p>
     <p>Не усложняет ли это написание ISR? Конечно. И поэтому решение заключается в том, чтобы в самом теле обработчика выполнять минимум работы, а все остальное делать уже на уровне потока, где есть доступ ко всем сервисам.</p>
     <p>Ваши цели при написании ISR должны заключаться в следующем:</p>
     <p>• считать переменчивую (в оригинале было «transient» — <emphasis>прим. ред</emphasis>.) информацию;</p>
     <p>• очистить источник прерывания;</p>
     <p>• возможно, запланировать поток, который сделает реальную работу.</p>
     <p>Такая «архитектура « держится на том, что QNX/Neutrino обеспечивает очень быстрые времена переключения контекста. Вы знаете, что сможете быстро переключиться в ваш обработчик для выполнения работы, критичной по времени. Вы также знаете, что когда обработчик возвратит событие для запуска потока, то поток тоже активизируется очень быстро. И именно эта философия «ничего не делайте в теле ISR» делает обработчики прерываний в QNX/Neutrino столь простыми!</p>
     <p>Итак, какие же вызовы можно использовать в теле ISR? Вот официальный список:</p>
     <p>• функции семейства <emphasis>atomic_*()</emphasis> (например, <emphasis>atomic_set()</emphasis>);</p>
     <p>• функции семейства <emphasis>mem*()</emphasis> (типа <emphasis>memcpy()</emphasis>);</p>
     <p>• большинство функций семейства <emphasis>str*()</emphasis> (типа <emphasis>strcmp()</emphasis>). Остерегайтесь, однако, потому что не все эти функции являются безопасными — например, <emphasis>strdup()</emphasis> вызывает <emphasis>malloc()</emphasis>, в которой используется мутекс, а это запрещено.</p>
     <p>Вообще, что касательно строковых функций, перед их использованием надо индивидуально смотреть их описание в руководстве по Си-библиотеке;</p>
     <p>• <emphasis>InterruptMask()</emphasis>;</p>
     <p>• <emphasis>InterruptUnmask()</emphasis>;</p>
     <p>• <emphasis>InterruptLock()</emphasis>;</p>
     <p>• <emphasis>InterruptUnlock()</emphasis>;</p>
     <p>• <emphasis>InterruptDisable()</emphasis>;</p>
     <p>• <emphasis>InterruptEnable()</emphasis>;</p>
     <p>• <emphasis>in*()</emphasis> и <emphasis>out*()</emphasis>.</p>
     <p>Основное эмпирическое правило формулируется примерно так: «Не используйте ничего, что требует большого объема стека или больших затрат времени, и не используйте ничего, что делает системные вызовы». Требование по стековому пространству проистекает из того факта, что ISR имеют очень ограниченный объем стека.</p>
     <p>Список функций, безопасных для применения в ISR, имеет реальный смысл — например, если вам потребуется скопировать область памяти, хорошим выбором будет применение функций типа <emphasis>mem*()</emphasis> и <emphasis>str*()</emphasis>. Скорее всего, вам потребуется читать регистры аппаратных средств (например, чтобы сохранить какие-либо значения или очистить источник прерывания), тогда вам пригодятся функции ввода/вывода из семейств <emphasis>in*()</emphasis> и <emphasis>out*()</emphasis>.</p>
     <p>А как насчет ошарашивающего выбора функций семейства <emphasis>Interrupt*()</emphasis>? Давайте рассмотрим их попарно.</p>
     <p><emphasis>InterruptMask() и InterruptUnmask()</emphasis></p>
     <p>Эти функции ответственны за маскирование источника прерывания на уровне контроллера; это предохраняет прерывания от передачи процессору. Обычно эти функции применяются, когда вы хотите доделать работу в потоке, но не можете очистить источник прерывании непосредственно в теле ISR. В этом случае ISR должен вызвать <emphasis>InterruptMask()</emphasis>, а поток, после завершения работы, — <emphasis>InterruptUnmask()</emphasis>.</p>
     <p>Имейте в виду, что число вызовов <emphasis>InterruptUnmask()</emphasis> должно соответствовать числу вызовов <emphasis>InterruptMask()</emphasis> — чтобы прерывание продолжало работать, вы обязаны демаскировать его ровно столько раз, сколько раз оно было маскировано.</p>
     <p>Заметьте, между прочим, что функция <emphasis>InterruptAttachEvent()</emphasis> выполняет <emphasis>InterruptMask()</emphasis> автоматически (в ядре), поэтому ваш обрабатывающий прерывание поток должен вызывать <emphasis>InterruptUnmask()</emphasis>.</p>
     <p><emphasis>InterruptLock() и InterruptUnlock()</emphasis></p>
     <p>Эти функции используются для блокировки (<emphasis>InterruptLock()</emphasis>) и деблокировки (<emphasis>InterruptUnlock()</emphasis>) прерываний в одно- или многопроцессорной системе. Вам может понадобиться заблокировать прерывания, например, чтобы защитить поток от ISR (или, дополнительно, в SMP-системе — защитить ISR от потока). Когда вы сделаете нужные манипуляции с критическими данными, вы сможете деблокировать прерывания обратно. Отметьте, что данные функции рекомендованы к применению вместо известных вам функций <emphasis>InterruptDisable()</emphasis> и <emphasis>InterruptEnable()</emphasis>, потому что корректно работают в SMP-системах. По сравнению со «старыми» функциями, проверка на многопроцессорность вносит дополнительные издержки, но в однопроцессорной системе ими можно пренебречь, поэтому я рекомендую вам всегда использовать <emphasis>InterruptLock()</emphasis> и <emphasis>InterruptUnlock()</emphasis>.</p>
     <p><emphasis>InterruptDisable() и InterruptEnable()</emphasis></p>
     <p>Не используйте эти функции в новых проектах. Исторически, эти функции применялись для вызова инструкций <code><strong>cli</strong></code> и <code><strong>sti</strong></code> в процессорах серии x86, когда QNX/Neutrino еще не была многоплатформенной ОС.</p>
     <p>С тех пор функции были модернизированы для работы со всеми типами процессоров, но чтобы не огорчать SMP-системы, используйте лучше функции <emphasis>InterruptLock()</emphasis> и <emphasis>InterruptUnlock()</emphasis>.</p>
     <p>Еще одна вещь, которую не вредно будет повторить, заключается в том, что в SMP-системе возможно одновременное выполнение ISR и другого потока.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Резюме</p>
    </title>
    <p>При работе с прерываниями принимайте во внимание следующие положения:</p>
    <p>• Не оставайтесь в обработчике прерывания слишком долго — выполняйте в нем минимальный объем работы. Это поможет сократить время реакции на прерывание и упростить отладку.</p>
    <p>• Применяйте функцию <emphasis>InterruptAttach()</emphasis> только тогда, когда нужно обращаться к аппаратным средствам непосредственно после прерывания, в противном случае избегайте ее.</p>
    <p>• Применяйте функцию <emphasis>InterruptAttachEvent()</emphasis> во всех других случаях. Ядро запланирует поток (на основе события, которое вы передадите) для обработки возникшего прерывания.</p>
    <p>• Защищайте переменные, используемые как в обработчиках прерываний (при использовании <emphasis>InterruptAttach()</emphasis>), так и в потоках, путем вызова <emphasis>InterruptLock()</emphasis> и <emphasis>InterruptUnlock()</emphasis>.</p>
    <p>• Объявляйте переменные, используемые в качестве посредников между потоками и обработчиками прерывании, как <code><strong>volatile</strong></code>, чтобы компилятор не кэшировал их «просроченные» значения, уже измененные обработчиком прерывания.</p>
   </section>
  </section>
  <section>
   <title>
    <p>Глава 5</p>
    <p>Администраторы ресурсов</p>
   </title>
   <section>
    <title>
     <p>Что такое администратор ресурсов?</p>
    </title>
    <section>
     <p>В данной главе мы рассмотрим все, что вы должны знать для самостоятельного написания <emphasis>администратора ресурса</emphasis>.</p>
     <p>Администратор ресурса — это просто программа с рядом четко определенных характеристик. Эта программа по-разному называется в различных операционных системах — «драйвер», «устройство», «драйвер устройства», «администратор ввода/вывода», «файловая система», и т.п. Однако, во всех случаях предназначение этой программы (мы будем называть ее просто «администратором ресурса») заключается в том, чтобы предоставить абстрактную форму некоего сервиса.</p>
     <p>Также, поскольку QNX/Neutrino является POSIX-совместимой ОС, основу предоставляемой абстракции составляют спецификации POSIX.</p>
    </section>
    <section>
     <title>
      <p>Примеры администраторов ресурсов</p>
     </title>
     <p>Прежде чем уйти в тонкости проблемы, давайте проанализируем пару примеров и увидим, как в них «абстрагируются» сервисы. Рассмотрим реальный аппаратный блок (последовательный порт) и кое-что более абстрактное (файловую систему).</p>
     <subtitle>Последовательный порт</subtitle>
     <p>В типовой системе обычно существует какой-нибудь способ программирования обмена информацией по последовательному интерфейсу типа RS-232. Этот интерфейс составляют ряд аппаратных устройств, включая микросхему UART (Universal Asynchronous Receiver Transmitter — универсальный асинхронный приемопередатчик), которая умеет преобразовывать параллельные данные от центрального процессора в последовательный поток и обратно.</p>
     <p>В этом случае сервисом, предоставляемым соответствующим администратором ресурса, будет возможность передачи и приема символьных данных через последовательный порт.</p>
     <p>Мы говорим, что имеет место «абстрагирование» сервиса, потому что клиентская программа (та, которая непосредственно использует сервис) не знает (да и незачем ей) о микросхеме UART и ее реализации. Все, что знает клиентская программа, — что для передачи символа она должна вызвать функцию <emphasis>fprintf()</emphasis> а для приема символов — функцию <emphasis>fgets()</emphasis>. Обратите внимание, что взаимодействия с последовательным портом мы использовали стандартные функции POSIX.</p>
     <subtitle>Файловая система</subtitle>
     <p>В качестве другого примера администратора ресурса рассмотрим файловую систему. Она состоит из ряда взаимодействующих модулей: собственно файловой системы, драйвера блочного ввода/вывода и дискового драйвера.</p>
     <p>Предлагаемый здесь сервис состоит в способности считывать и записывать символы на некоторый носитель. Абстракция здесь та же самая, что и в предыдущем примере с последовательным портом — клиентская программа по-прежнему может использовать <emphasis>те же самые</emphasis> вызовы функций (например, <emphasis>fprintf()</emphasis> и <emphasis>fgets()</emphasis>) для доступа к носителю. Фактически, клиент действительно не знает или даже не должен знать, с каким конкретно администратором ресурсов он имеет дело.</p>
    </section>
    <section>
     <title>
      <p>Характеристики администраторов ресурсов</p>
     </title>
     <p>Как мы увидели в приведенных выше примерах, ключом к универсальности администраторов ресурсов является возможность использования стандартных функций POSIX — мы ведь не использовали никакие «специальные» функции, когда общались с последовательным портом. А если вам понадобится сделать нечто «особенное», характерное только для применяемого вами устройства? Например, операция настройки скорости обмена по последовательному порту в бодах специфична для администратора последовательного порта, но абсолютно бессмысленна для администратора файловой системы. Аналогично, операция по позиционированию в файле с помощью функции <emphasis>lseek()</emphasis> имеет смысл для файловой системы, но является несодержательной для последовательного порта. В POSIX эта проблема решается просто. Некоторые функции — например, <emphasis>lseek()</emphasis> — при попытке применить их к устройству, которое их не поддерживает, просто возвращают код ошибки. Для реализации функций, специфичных для каждого устройства, в POSIX предусмотрена функция управления устройствами, <emphasis>devctl()</emphasis>. Если устройство не понимает команду, передаваемую ему посредством <emphasis>devctl()</emphasis>, оно просто возвращают код ошибки, аналогично устройствам, которые не понимают функцию <emphasis>lseek()</emphasis>.</p>
     <p>Поскольку мы уже упомянули функции <emphasis>lseek()</emphasis> и <emphasis>devctl()</emphasis> как общеупотребительные, следует заметить, что администраторы ресурсов обычно поддерживают весь спектр функций, работающих с дескрипторами файлов (или <code><strong>FILE* stream</strong></code>).</p>
     <p>Это естественно приводит нас к выводу о том, что администраторы ресурсов будут работать почти исключительно с вызовами дескриптор-ориентированных функций. Поскольку QNX/Neutrino — операционная система, организованная на основе обмена сообщениями, из этого следует, что вызовы POSIX-функций транслируются в сообщения, которые затем пересылаются администраторам ресурсов.</p>
     <p>Именно эта трансляция вызовов POSIX в сообщения позволяет нам отвязать клиентуру от администраторов ресурсов. Все, что должен уметь делать администратор ресурса, — это обрабатывать ряд строго определенных сообщений. Все, что должен уметь делать клиент, — это генерировать эти самые строго определенные сообщения, которые администратор ресурса ожидает принимать и обрабатывать.</p>
     <cite>
      <p>Поскольку взаимодействие между клиентурой и администраторами ресурсов основано на обмене сообщениями, имеет смысл делать этот «передаточный уровень» как можно «тоньше». Например, когда клиент выполняет функцию <emphasis>open()</emphasis> и получает в ответ дескриптор файла, этот дескриптор фактически является идентификатором соединения! Данный идентификатор соединения (он же дескриптор файла) используется затем функциями клиентской Си-библиотеки (например, функцией <emphasis>read()</emphasis>) при создании и отправке сообщения для администратора ресурсов.</p>
     </cite>
    </section>
   </section>
   <section>
    <title>
     <p>Взгляд со стороны клиента</p>
    </title>
    <section>
     <p>Мы уже намекнули, что ожидает клиент. Он ожидает интерфейс на основе файловых дескрипторов с применением стандартных функций POSIX.</p>
     <p>В действительности «под колпаком» происходит еще кое-что.</p>
     <p>Например, как в действительности клиент соединяется с соответствующим администратором ресурса? Что происходит в случае объединённых файловых систем (когда несколько файловых систем ответственны за то же самое пространство имен)? Как обрабатываются каталоги?</p>
    </section>
    <section>
     <title>
      <p>Поиск сервера</p>
     </title>
     <p>Первое, что делает клиент, — это вызывает <emphasis>open()</emphasis>, чтобы получить дескриптор файла. (Заметьте, что если клиент вместо этого вызывает функцию более высокого уровня — например, <emphasis>fopen()</emphasis> — утверждение остается справедливым, поскольку <emphasis>fopen()</emphasis> в конечном счете вызывает <emphasis>open()</emphasis>).</p>
     <p>Реализация функции <emphasis>open()</emphasis> в Си-библиотеке создает сообщение, которое затем пересылается администратору процессов (<code><strong>procnto</strong></code>).</p>
     <p>Администратор процессов отвечает за поддержание информации о пространстве имен путей. Данная информация представляет собой древовидную структуру имен путей, с которой связаны дескрипторы узлов (node descriptors), идентификаторы процессов (process IDs), идентификаторы каналов (channel IDs) и обработчики (handles):</p>
     <image l:href="#img_45.png"/>
     <p>Пространство имен путей в QNX/Neutrino.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Отметьте, что на представленном выше рисунке и в последующих описаниях для обозначения администратора ресурса, который реализует файловую систему QNX4, я использовал имя <code><strong>fsys-qnx4</strong></code>. В действительности все немного сложнее, потому что драйверы файловой системы представляют собой группы связанных между собой динамических библиотек (DLL), так что никакой программы с именем <code><strong>fsys-qnx4</strong></code> на самом деле не существует; мы просто используем это имя в качестве «заполнителя» для компонента файловой системы.</p>
     </cite>
     <p>Давайте предположим, что клиент вызывает функцию <emphasis>open()</emphasis>:</p>
     <p><code>fd = open("/dev/ser1", O_WRONLY);</code></p>
     <p>Реализация функции <emphasis>open()</emphasis> в клиентской Си-библиотеке создает сообщение и пересылает его администратору процессов. Это сообщение гласит: «Хочу открыть <code><strong>/dev/ser1</strong></code>. К кому мне обратиться по этому вопросу?»</p>
     <image l:href="#img_46.png"/>
     <p>Первая стадия разрешения имени.</p>
     <p>Администратор процессов принимает запрос и просматривает дерево имен на предмет соответствия (давайте предположим здесь, что нам необходимо точное соответствие). Имя пути «<code><strong>/dev/ser1</strong></code>» вполне подойдет, и администратор процессов может ответить клиенту: «Нашел <code><strong>/dev/ser1</strong></code>. За обработку отвечает канал 1 процесса 44 на узле 0, спроси его!»</p>
     <p>Не забывайте: мы все еще в клиентском коде <emphasis>open()</emphasis>!</p>
     <p>Функция <emphasis>open()</emphasis> создает другое сообщение и соединяется с указанным процессом (PID 44) на указанном узле (NID 0 означает локальный узел) по заданному каналу (CHID 1), помещая обработчик (handle) непосредственно в сообщение. Это воистину «сообщение установки соединения» — то самое сообщение которое клиентская функция <emphasis>open()</emphasis> использует для установления связи с администратором ресурса (3 стадия на рисунке ниже) Когда администратор ресурса получает сообщение установки соединения, он анализирует его и проверяет на корректность. Например, вы могли бы попытаться применить операцию записи к администратору ресурса, который реализует файловую систему с доступом только для чтения — в этом случае вы бы получили обратно признак ошибки (в данном случае — EROFS). В нашем примере, однако, администратор последовательного порта смотрит на запрос (мы указали там O_WRONLY, что для последовательного порта абсолютно кошерно) и отвечает признаком EOK (4 стадия на рисунке ниже).</p>
     <image l:href="#img_47.png"/>
     <p>Сообщение _IO_CONNECT.</p>
     <p>Затем, наконец, клиентская функция <emphasis>open()</emphasis> возвращает клиенту корректный дескриптор файла.</p>
     <p>На самом деле этот дескриптор файла представляет собой идентификатор соединения, который мы только что использовали для отправки сообщения администратору ресурса! Если бы администратор ресурса <emphasis>не</emphasis> ответил признаком EOK, мы бы сообщили клиенту, что произошла ошибка (<emphasis>open()</emphasis> возвратила бы -1 и установила код ошибки в <emphasis>errno</emphasis>).</p>
    </section>
    <section>
     <title>
      <p>Поиск администратора процессов</p>
     </title>
     <p>Теперь, когда мы знаем основные этапы поиска конкретного администратора ресурса, осталось раскрыть тайну поиска администратора процесса, с которого все начинается. На самом деле все очень просто. По определению, администратору процессов соответствует дескриптор узла 0 (то есть текущий узел), идентификатором процесса 1 и идентификатор канала 1. Так что администратор процессов всегда идентифицируется триплетом ND/PID/CHID, равным 0/1/1.</p>
    </section>
    <section>
     <title>
      <p>Обработка каталогов</p>
     </title>
     <p>Пример, рассмотренный выше, относился к администратору последовательного порта. Мы также высказывали предположение, что хотим точного соответствия имен путей при поиске по дереву. Это предположение справедливо только наполовину — все соответствия имен путей, о которых мы будем говорить в этой главе, основаны на <emphasis>полном</emphasis> соответствии <emphasis>компонента</emphasis> имени пути, но вовсе не обязательно имени пути <emphasis>целиком</emphasis>. Давайте вкратце это поясним.</p>
     <p>Предположим, у меня есть код, который делает следующее:</p>
     <p><code>fp = fopen("/etc/passwd", "r");</code></p>
     <p>Напомним, что функция <emphasis>fopen()</emphasis> в конечном счете вызывает функцию <emphasis>open()</emphasis>, так что реально мы имеем функцию <emphasis>open()</emphasis>, запрашивающую имя пути <code><strong>/etc/passwd</strong></code>. Но такого имени на рисунке нет:</p>
     <image l:href="#img_45.png"/>
     <p>Пространство имен путей в QNX/Neutrino.</p>
     <p>Однако, из рисунка видно, что модуль <code><strong>fs-qnx4</strong></code> зарегистрировал свою тройку ND/PID/CHID для имени пути «<code><strong>/</strong></code>». Хоть это и не показано на рисунке, файловая система <code><strong>fs-qnx4</strong></code> зарегистрировалась как «<emphasis>администратор каталога</emphasis>», сказав администратору процессов, что будет отвечать за «<code><strong>/</strong></code>» и все то, что расположено «ниже». «<emphasis>Администраторы устройств</emphasis>» (например, администратор последовательного порта) так не делают. Установив флаг каталога, <code><strong>fs-qnx4</strong></code> получает возможность обработать запрос для имени пути «<code><strong>/etc/passwd</strong></code>», потому что это имя начинается с «<code><strong>/</strong></code>», а значит, есть совпадение!</p>
     <p>А что произошло бы, если бы мы попытались сделать так?</p>
     <p><code>fd = open("/dev/ser1/9600.8.1.n", O_WRONLY);</code></p>
     <p>Ну, поскольку у администратора последовательного порта не установлен флаг каталога, администратор процессов увидит эта и скажет: «Опаньки, извините, <code><strong>/dev/ser1</strong></code> — не каталог. В обработке отказано». Запрос прямо здесь и заканчивается — администратор процессов даже не возвращает функции <emphasis>open()</emphasis> четверку ND/PID/CHID/handle.</p>
     <cite>
      <p>Из параметров функции <emphasis>open()</emphasis> в примере выше видно, что может показаться заманчивой идеей позволить некоторым «традиционным» устройствам открываться с дополнительными параметрами, указываемыми после «обычного» имени. Однако, эмпирическое правило здесь такое: если это пройдет на совещании по организации проекта, тогда вперед. Некоторые из моих студентов, услышав это от меня, заявляют: «Так я и есть сам себе комитет по проектным решениям!» На что я обычно отвечаю. «Пистолет у вас есть. Прострелите себе ногу. :-)»</p>
     </cite>
    </section>
    <section id="_bookmark641">
     <title>
      <p>Объединенные файловые системы</p>
     </title>
     <p id="___temp_view_cursor_for_clear_format__1">Взгляните повнимательнее на уже знакомый нам рисунок.</p>
     <image l:href="#img_45.png"/>
     <p>Пространство имен путей в QNX/Neutrino.</p>
     <p>Обратите внимание, что ответственными за префикс «<code><strong>/</strong></code>» объявили себя как файловая система <code><strong>fs-qnx4</strong></code>, так и администратор процессов. Это нормально, и беспокоиться тут не о чем. Мало того, иногда это оказывается очень даже неплохой идеей. Рассмотрим один такой случай.</p>
     <image l:href="#img_50.png"/>
     <p>Файловые системы с перекрытием</p>
     <p>Предположим, что у вас очень медленное сетевое соединение, и вы смонтировали поверх него сетевую файловую систему. Вы замечаете, что некоторый файлы используются достаточно часто, и хотели бы, чтобы эти файлы неким волшебным способом «кешировались» на вашей машине, но увы и ах, проектировщики сетевой файловой системы это почему-то не предусмотрели. И тогда вы решаете самостоятельно написать кеширующую файловую систему (назовем ее, например, <code><strong>fs-cache</strong></code>) и поместить ее поверх сетевой файловой системы. Вот как это будет смотреться с позиции клиента:</p>
     <p>Обе файловые системы, <code><strong>fs-nfs</strong></code> (сетевая файловая система) и ваша кэшированная файловая система (<code><strong>fs-cache</strong></code>) регистрируются под одним и тем же префиксом, «<code><strong>/nfs</strong></code>» уже упомянули выше, в QNX/Neutrino это нормально и абсолютно законно.</p>
     <p>Предположим, что ваша система только что стартовала, и в вашей кэшированной файловой системе еще ничего нет. Клиентская программа пробует открыть какой-нибудь файл — скажем <code><strong>/nfs/home/rk/abc.txt</strong></code>. Ваша кэшированная файловая система находится «перед» сетевой файловой системой (я потом покажу вам, как это сделать, когда мы будем обсуждать реализацию администратора ресурса).</p>
     <p>Клиентский вызов <emphasis>open()</emphasis> выполняет свои обычные действия:</p>
     <p>1. Спрашивает администратор процессов: «К кому обратиться по поводу файла <code><strong>/nfs/home/rk/abc.txt</strong></code>?»</p>
     <p>2. Получает ответ от администратора процессов: «Поговори сначала с <code><strong>fs-cache</strong></code>, а потом с <code><strong>fs-nfs</strong></code>».</p>
     <p>Обратите внимание, что здесь администратор процессов возвращает <emphasis>две</emphasis> четверки ND/PID/CHID/handle — одну для файловой системы <code><strong>fs-cach</strong></code>e и одну для файловой системы <code><strong>fs-nfs</strong></code>. Это критично.</p>
     <p>Далее функция <emphasis>open()</emphasis> делает следующее:</p>
     <p>1. Направляет сообщение файловой системе <code><strong>fs-cache</strong></code>. «Я бы хотел открыть файл <code><strong>/nfs/home/rk/abc.txt</strong></code> на чтение, пожалуйста.»</p>
     <p>2. Получает ответ от файловой системы <code><strong>fs-cache</strong></code>: «Сожалею, но я никогда о таком не слышала.»</p>
     <p>Здесь становится ясно, что с администратором файловой системы <code><strong>fs-cache</strong></code> клиентской функции <emphasis>open()</emphasis> не повезло. Файл не существует! Однако, вызов <emphasis>open()</emphasis> знает, что он получил список из двух четверок ND/PID/CHID/handle, и поэтому пробует второй вариант:</p>
     <p>1. Направляет сообщение файловой системе <code><strong>fs-nfs</strong></code>: «Я бы хотел открыть файл <code><strong>/nfs/home/rk/abc.txt</strong></code> на чтение, пожалуйста.»</p>
     <p>2. От файловой системы приходит ответ: «Запросто, никаких проблем!»</p>
     <p>Теперь, после того как у функции <emphasis>open()</emphasis> есть EOK («никаких проблем»), она возвращает дескриптор файла. Все дальнейшие операции клиент выполняет непосредственно с администратором сетевой файловой системы <code><strong>fs-nfs</strong></code>.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Имя пути разрешается только один раз — во время вызова функции <emphasis>open()</emphasis>. Это означает, что как только мы успешно открыли нужный администратор ресурса, все дальнейшие вызовы, работающие с дескрипторами файлов, будут идти через него.</p>
     </cite>
     <p>Так когда же вступает в игру наша кеширующая файловая система <code><strong>fs-cache</strong></code>? Ну, допустим, пользователь закончил считывание файла (файл теперь загружен в текстовый редактор). Когда файл понадобится сохранить, произойдет та же самая последовательность действий, но возникнет один любопытный поворот:</p>
     <p>1. Сообщение администратору процессов: «С кем я должен переговорить насчет файла <code><strong>/nfs/home/rk/abc.txt</strong></code>?»</p>
     <p>2. Ответ администратора процессов: «Поговори сначала с <code><strong>fs-cache</strong></code>, а затем с <code><strong>fs-nfs</strong></code>».</p>
     <p>3. Сообщение <code><strong>fs-cache</strong></code>: «Мне хотелось бы открыть файл <code><strong>/nfs/home/rk/abc.txt</strong></code> на запись, пожалуйста».</p>
     <p>4. Ответ от <code><strong>fs-cache</strong></code>: «Запросто, нет проблем».</p>
     <p>Обратите внимание на то, что на 3 этапе мы открыли файл на запись, а не на чтение, как в первый раз. Поэтому не удивительно, что <code><strong>fs-cache</strong></code> на этот раз разрешает эту операцию (этап 4).</p>
     <p>Еще более интересные события происходят, когда мы повторно пытаемся прочитать этот файл:</p>
     <p>1. Сообщение администратору процессов: «С кем я должен переговорить насчет файла <code><strong>/nfs/home/rk/abc.txt</strong></code>?»</p>
     <p>2. Ответ администратора процессов: «Поговори сначала с <code><strong>fs-cache</strong></code>, а затем с <code><strong>fs-nfs</strong></code>».</p>
     <p>3. Сообщение <code><strong>fs-cache</strong></code>: «Мне хотелось бы открыть файл <code><strong>/nfs/home/rk/abc.txt</strong></code> на <emphasis>чтение</emphasis>, пожалуйста».</p>
     <p>4. Ответ от <code><strong>fs-cache</strong></code>: «Запросто, нет проблем».</p>
     <p>Да-да, на этот раз <code><strong>fs-cache</strong></code> обработала запрос на чтение!</p>
     <p>Мы опустили несколько деталей, но для восприятия базовых идей они не так важны. Очевидно, кеширующая файловая система должна предусматривать некоторый способ отправки данных по сети на «реальный» носитель. Она также должна уметь перед отправкой данных клиенту проверять, не изменился ли файл (чтобы клиент не получил устаревшие данные). К тому же, кеширующая файловая система вполне могла бы сама обработать первый запрос на чтение, загрузив данные из сетевой файловой системы в свой кэш. И так далее.</p>
     <subtitle>Объединенные файловые системы (UFS — Unioned File Systems) и объединенные точки монтирования (UMP — Unioned Mount Points)</subtitle>
     <p>Дабы не путать понятия, сделаем небольшой экскурс в терминологию. Основное различие между объединенной файловой системой (UFS) и объединенной точкой монтирования (UMP) заключается в том, что UFS ориентирована на файлы, а UMP — на точки монтирования. В вышеупомянутой кеширующей файловой системе у нас была UFS, потому что оба администратора могли получить доступ к файлу вне зависимости от глубины его размещения файла в дереве каталогов. Давайте для примера рассмотрим другой администратор ресурса (назовем его «<code><strong>foobar</strong></code>»), отвечающий за путь «<code><strong>/nfs/other</strong></code>». В UFS-системе процесс <code><strong>fs-cache</strong></code> был бы способен кэшировать файлы и оттуда тоже, присоединившись к «<code><strong>/nfs</strong></code>». В случае с UMP, что принято в QNX/Neutrino по умолчанию, поскольку там все основано на соответствии самого длинного префикса, запросы смог бы обрабатывать только администратор <code><strong>foobar</strong></code>.</p>
    </section>
    <section>
     <title>
      <p>Резюме о клиенте</p>
     </title>
     <p>На этом с клиентом все. Перечислим ключевые моменты, которые следует запомнить:</p>
     <p>• Клиент обычно налаживает связь с администратором ресурса с помощью вызова <emphasis>open()</emphasis> (или <emphasis>fopen()</emphasis>).</p>
     <p>• После того как запрос клиента разрешился в конкретный администратор ресурса, мы его больше не меняем.</p>
     <p>• Все дальнейшие клиентские сообщения в этом сеансе основываются на дескрипторах файлов (или <code><strong>FILE* stream</strong></code>) — например, <emphasis>read()</emphasis>, <emphasis>lseek()</emphasis>, <emphasis>fgets()</emphasis>, и т.п.</p>
     <p>• Сеанс прекращается, когда клиент закрывает дескриптор файла или поток (или завершается по какой-либо причине).</p>
     <p>Все вызовы, основанные на дескрипторах файлов, транслируются в сообщения.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Взгляд со стороны администратора ресурсов</p>
    </title>
    <section>
     <p>Давайте теперь посмотрим на вещи с позиции администратора ресурса. Перво-наперво администратор ресурса должен сообщить администратору процессов, что он берет на себя ответственность за некоторую часть пространства имен путей (то есть <emphasis>зарегистрироваться</emphasis>). Затем он должен принимать сообщения от клиентуры и их обрабатывать. Очевидно, не так тут все просто.</p>
     <p>Давайте кратко рассмотрим функции, реализуемые администраторами ресурсов, а затем уже углубимся в детали.</p>
    </section>
    <section>
     <title>
      <p>Регистрация префикса</p>
     </title>
     <p>Администратор ресурса должен сообщить администратору процессов, что одно или более имя пути теперь является его <emphasis>доменом ответственности</emphasis> — иными словами, что он готов обрабатывать клиентские запросы, относящиеся к этим именам путей.</p>
     <p>Администратор последовательного порта способен обрабатывать (допустим, так) четыре последовательных порта. В этом случае он должен зарегистрировать у администратора процесса четыре различных имени пути: <code><strong>/dev/ser1</strong></code>, <code><strong>/dev/ser2</strong></code>, <code><strong>/dev/ser3</strong></code> и <code><strong>/dev/ser4</strong></code>. В результате этого в дереве имен путей у администратора процессов появятся еще четыре элемента, по одному на каждый из последовательных портов. Четыре элемента — это неплохо. А что если бы администратор последовательного порта обрабатывал, например, новомодную мультипортовую плату на 256 портов? Регистрация 256 <emphasis>отдельных</emphasis> префиксов (то есть от <code><strong>/dev/ser1</strong></code> до <code><strong>/dev/ser256</strong></code>) привела бы к появлению в дереве имен путей администратора процессов 256 различных элементов! Администратор процессов не оптимизирован для поиска по этому дереву — он предполагает, что элементы в нем, конечно, есть, но не сотни же там этих элементов.</p>
     <p>Как правило, не следует регистрировать больше чем несколько дюжин отдельных префиксов, потому что поиск по дереву является линейным. Число 256 портов определенно больше. В таких случаях что мультипортовый администратор ресурсов должен сделать, так это зарегистрировать каталогоподобный префикс, например, <code><strong>/dev/multiport</strong></code>. Это займет только один элемент в дереве имен путей. Клиент открывает последовательный порт, скажем, порт <code><strong>57</strong></code>:</p>
     <p><code>fp = fopen("/dev/multiport/57", "w");</code></p>
     <p>Администратор процессов разрешает это в четверку ND/PID/CHID/handle для мультипортового администратора; решать, насколько корректен при этом остаток имени («57») — это уже дело самого администратора ресурса. В этом примере, предположив, что часть имени пути после точки монтирования хранится в переменной <emphasis>path</emphasis>, администратор ресурса мог бы выполнить проверку <emphasis>очень</emphasis> простым способом:</p>
     <p><code>devnum = atoi(path);</code></p>
     <p><code>if ((devnum &lt;= 0) || (devnum &gt;= 256)) {</code></p>
     <p><code> // Неправильный номер устройства</code></p>
     <p><code>} else {</code></p>
     <p><code> // Правильный номер устройства</code></p>
     <p><code>}</code></p>
     <p>Этот будет однозначно быстрее, чем поиск, выполняемый администратором процессов, — хотя бы потому что администратор процессов по сути своей намного более универсален, чем наш администратор ресурса.</p>
    </section>
    <section>
     <title>
      <p>Обработка сообщений</p>
     </title>
     <p>Как только мы зарегистрировали один или более префиксов, мы должны быть готовы принимать сообщения от клиентов. Это делается «обычным» способом с помощью функции <emphasis>MsgReceive()</emphasis>. Существуют менее 30 четко определенных типов сообщений, которые администратор ресурса должен быть способен обработать. Однако, для упрощения обсуждения и реализации их условно делят на две группы:</p>
     <p>Сообщения установления соединения (connect messages)</p>
     <p> Всегда содержат имя пути; либо являются однократными, либо устанавливают контекст для последующих сообщений ввода/вывода.</p>
     <p>Сообщения ввода/вывода (I/O messages)</p>
     <p> Всегда базируются на сообщениях установления соединения; выполняют всю последующую работу.</p>
     <subtitle>Сообщения установления соединения</subtitle>
     <p>Сообщения установления соединения всегда содержат имя пути. Прекрасным примером функции, генерирующей сообщение установления соединения, является уже не раз упомянутая нами функция <emphasis>open()</emphasis>. В этом случае обработчик (handle) сообщения установления соединении устанавливает контекст для последующих сообщений ввода/вывода. (В конце-то концов, после <emphasis>open()</emphasis> мы все-таки собираемся делать что-то наподобие <emphasis>read()</emphasis>.).</p>
     <p>Примером «однократного» сообщения установления соединения может быть сообщение, сгенерированное в результате вызова <emphasis>rename()</emphasis>. Здесь никакого контекста не предполагается — обработчик в администраторе ресурса изменяет имя указанного файла на новое, и все.</p>
     <subtitle>Сообщения ввода/вывода</subtitle>
     <p>Сообщения ввода/вывода возникают только после соответствующего сообщения установления соединения и ссылаются на установленный им контекст. Как уже упоминалось ранее при обсуждении сообщений установления соединения, идеальный пример — вызов функции <emphasis>open()</emphasis>, за которым следует <emphasis>read()</emphasis>.</p>
     <subtitle>На самом деле групп сообщений три</subtitle>
     <p>Кроме сообщений об установлении соединения и сообщений ввода/вывода, есть еще и «другие» сообщения, которые администратор ресурсов может принимать и обрабатывать. Но поскольку они не являются в полной мере «административными», покамест отложим их обсуждение и вернемся к ним позже.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Библиотека администратора ресурсов</p>
    </title>
    <section>
     <p>Прежде чем лезть в глубины организации администраторов ресурсов, познакомимся сначала с библиотекой администратора ресурсов, разработанной QSSL. Отметим, что в действительности эта «библиотека» состоит из нескольких четко различимых частей:</p>
     <p>• функции пула потоков (мы обсуждали их в главе «Процессы и потоки», в параграфе «Пулы потоков»);</p>
     <p>• интерфейс диспетчеризации;</p>
     <p>• функции администратора ресурсов;</p>
     <p>• вспомогательные функции POSIX-библиотеки.</p>
     <p>При том, что можно было бы, конечно, писать администраторы ресурсов «с нуля» (как это делалось в QNX4), эта овчинка часто не стоит такой выделки.</p>
     <p>Просто для демонстрации практичности библиотечного подхода — вот код однопоточной версии администратора «<code><strong>/dev/null</strong></code>»:</p>
     <p><code>/*</code></p>
     <p><code> * resmgr1.c</code></p>
     <p><code> *</code> </p>
     <p><code> * /dev/null на основе библиотеки администратора ресурсов</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;stdlib.h&gt;</code></p>
     <p><code>#include &lt;stddef.h&gt;</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <p><code>#include &lt;sys/dispatch.h&gt;</code></p>
     <empty-line/>
     <p><code>int main(int argc, char **argv) {</code></p>
     <p><code> dispatch_t             *dpp;</code></p>
     <p><code> resmgr_attr_t          resmgr_attr;</code></p>
     <p><code> resmgr_context_t       *ctp;</code></p>
     <p><code> resmgr_connect_funcs_t connect_func;</code></p>
     <p><code> resmgr_io_funcs_t      io_func;</code></p>
     <p><code> iofunc_attr_t          attr;</code></p>
     <p><code> // Создать структуру диспетчеризации</code></p>
     <p><code> if ((dpp = dispatch_create()) == NULL) {</code></p>
     <p><code>  perror("Ошибка dispatch_create\n");</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> // Инициализировать структуры данных</code></p>
     <p><code> memset(&amp;resmgr_attr, 0, sizeof(resmgr_attr));</code></p>
     <p><code> resmgr_attr.nparts_max = 1;</code></p>
     <p><code> resmgr_attr.msg_max_size = 2048;</code></p>
     <p><code> // Назначить вызовам обработчики по умолчанию</code></p>
     <p><code> iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &amp;connect_func,</code></p>
     <p><code>  _RESMGR_IO_NFUNCS, &amp;io_func);</code></p>
     <p><code> iofunc_attr_init(&amp;attr, S_IFNAM | 0666, 0, 0);</code></p>
     <p><code> // Зарегистрировать префикс в пространстве имен путей</code></p>
     <p><code> if (resmgr_attach(dpp, &amp;resmgr_attr,</code></p>
     <p><code>  "/dev/mynull", _FTYPE_ANY,</code></p>
     <p><code>  0, &amp;connect_func, &amp;io_func, &amp;attr) == -1) {</code></p>
     <p><code>  perror("Ошибка resmgr_attach\n");</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> ctp = resmgr_context_alloc(dpp);</code></p>
     <p><code> // Ждать сообщений в вечном цикле</code></p>
     <p><code> while (1) (</code></p>
     <p><code>  if ((ctp = resmgr_block(ctp)) == NULL) {</code></p>
     <p><code>   perror("Ошибка resmgr_block\n");</code></p>
     <p><code>   exit(EXIT_FAILURE);</code></p>
     <p><code>  }</code></p>
     <p><code>  resmgr_handler(ctp);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>И все! Полнофункциональный администратор ресурса <code><strong>/dev/null</strong></code> реализуется всего несколькими вызовами функций!</p>
     <p>Если бы пришлось писать аналогичный по функциональности администратор (то есть с поддержкой функций <emphasis>stat()</emphasis>, <emphasis>chown()</emphasis>, <emphasis>chmod()</emphasis>, и т.д.) «с нуля», то вам пришлось бы перелопатить сотни, если не тысячи строк Си-кода.</p>
    </section>
    <section>
     <title>
      <p>Реально все это за вас делает библиотека</p>
     </title>
     <p>Как вариант начального знакомства с библиотекой, давайте посмотрим, что делают вызовы, использованные в администраторе ресурсов <code><strong>/dev/null</strong></code>:</p>
     <p><emphasis>dispatch _create()</emphasis></p>
     <p>Создает структуру диспетчеризации; она будет использоваться для блокирования по приему сообщения.</p>
     <p><emphasis>iofunc_attr_init()</emphasis></p>
     <p>Инициализирует используемую устройством атрибутную запись. Мы обсудим атрибутные записи в подробностях несколько позже, а вкратце так: атрибутная запись содержит информацию об устройстве, и на каждое имя устройства имеется по одной атрибутной записи.</p>
     <p><emphasis>iofunc_func_init()</emphasis></p>
     <p>Инициализирует две структуры данных, <emphasis>cfuncs</emphasis> и <emphasis>ifuncs</emphasis>, которые содержат соответственно указатели на функции установления соединения и функции ввода/вывода. Это, пожалуй, самый «магический» вызов, поскольку именно он назначает подпрограммы обработки сообщений, привязывая их к структурам данных. Заметьте, что никакого <emphasis>кода</emphasis> обработки сообщений установления соединения или сообщений ввода/вывода, генерируемых функциями <emphasis>read()</emphasis>, <emphasis>stat()</emphasis> или им подобными, в администраторе нет. Дело в том, что библиотека содержит для всех сообщений готовые POSIX-обработчики по умолчанию, и как раз функция <emphasis>iofunc_func_init()</emphasis>-то и привязывает их к двум передаваемым ей таблицам.</p>
     <p><emphasis>resmgr_attach()</emphasis></p>
     <p>Создает канал, который администратор ресурса будет использовать для приема сообщений, и говорит администратору процессов, что мы намерены отвечать за «<code><strong>/dev/null</strong></code>». Параметров тут много, но к этой головной боли мы вернемся несколько позже. Сейчас же важно отметить, что именно здесь связываются воедино дескриптор диспетчера (<emphasis>dpp</emphasis>), имя пути (строка «<code><strong>/dev/null</strong></code>») и обработчики функций установления соединения (<emphasis>cfuncs</emphasis>) и ввода/вывода (<emphasis>ifuncs</emphasis>).</p>
     <p><emphasis>resmgr_context_alloc()</emphasis></p>
     <p>Выделяет внутренний контекстный блок администратора ресурса. Мы рассмотрим этот блок в подробностях несколько позже, а вкратце — он содержит информацию, относящуюся к обрабатываемому сообщению.</p>
     <p><emphasis>resmgr_block()</emphasis></p>
     <p>Это блокирующий вызов администратора ресурса — функция, с помощью которой мы ожидаем сообщение от клиента.</p>
     <p><emphasis>resmgr_handler()</emphasis></p>
     <p>После того как сообщение от клиента получено, для его обработки вызывается эта функция.</p>
    </section>
    <section>
     <title>
      <p>За кулисами библиотеки</p>
     </title>
     <p>Вы уже видели, что наша программа ответственна за предоставление основного рабочего цикла приема сообщений:</p>
     <p><code>while (1) {</code></p>
     <p><code> // Здесь ждем сообщения</code></p>
     <p><code> if ((ctp = resmgr_block(ctp)) == NULL) {</code></p>
     <p><code>  perror("Unable to resmgr_block\n");</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> // Обработать сообщение</code></p>
     <p><code> resmgr_handler(ctp);</code></p>
     <p><code>}</code></p>
     <p>Это очень удобно, поскольку позволяет вам помещать точки останова на принимающей функции и перехватывать сообщения (например, с помощью отладчика) в процессе работы.</p>
     <p>Библиотека осуществляет все магические манипуляции внутри функции <emphasis>resmgr_handler()</emphasis>, потому что это как раз то самое место, где сообщение анализируется и обрабатывается в соответствии с таблицами функций установления соединения и ввода/вывода, о которых мы уже говорили ранее.</p>
     <p>В действительности, библиотека состоит из двух взаимодействующих уровней — базового, который обеспечивает «сырую» функциональность администратора ресурсов, и уровня POSIX, который содержит вспомогательные функции POSIX и обработчики по умолчанию. Сейчас мы кратко обрисуем эти два уровня, а затем в разделе «Структура администратора ресурсов» рассмотрим все в подробностях.</p>
     <subtitle>Базовый уровень</subtitle>
     <p>Самый нижний, базовый, уровень состоит из функций, имена которых начинаются с «<emphasis>resmgr_</emphasis>». Этот класс функций относится к низкоуровневым механизмам функционирования администратора ресурсов.</p>
     <p>Здесь я только кратко приведу описание этих функций, которые являются доступными и которые мы будем использовать. Затем я отправлю вас к изучению QSSL документации для получения более подробных сведений об этих функциях.</p>
     <p>К функциям базового уровня относятся:</p>
     <p><emphasis>resmgr_msgreadv()</emphasis> и <emphasis>resmgr_msgread()</emphasis></p>
     <p>Считывают данных из адресного пространства клиента при помощи обмена сообщениями.</p>
     <p><emphasis>resmgr_msgwritev()</emphasis> и <emphasis>resmgr_msgwrite()</emphasis></p>
     <p>Записывают данные в адресное пространство клиента при помощи обмена сообщениями.</p>
     <p><emphasis>resmgr_open_bind()</emphasis></p>
     <p>Связывает контекст с запросом на установление соединения, поступившим от соответствующей клиентской функции. Этот контекст далее будет использоваться функциями ввода/вывода.</p>
     <p><emphasis>resmgr_attach()</emphasis></p>
     <p>Создает канал и связывает воедино имя пути, дескриптор диспетчера, функции установления соединения, функции ввода/вывода и другие параметры. Посылает сообщение администратору процессов для регистрации имени пути (префикса).</p>
     <p><emphasis>resmgr_detach()</emphasis></p>
     <p>Противоположна функции <emphasis>resmgr_attach()</emphasis>. Уничтожает связь между именем пути и администратором ресурса.</p>
     <p><emphasis>pulse_attach()</emphasis></p>
     <p>Связывает код импульса с функцией. Поскольку приема сообщений реализуется библиотекой, это удобный способ «перехватывать управление» для обработки импульсов.</p>
     <p><emphasis>pulse_detach()</emphasis></p>
     <p>Отвязывает код импульса от функции.</p>
     <p>В дополнение к функциям, перечисленным выше, есть также множество функций, посвященных интерфейсу диспетчеризации.</p>
     <p>Одна функция из вышеупомянутого списка заслуживает особого упоминания — функция <emphasis>resmgr_open_bind()</emphasis>. Данная функция, когда приходит сообщение установления соединения (обычно это происходит в результате клиентского вызова <emphasis>open()</emphasis> или <emphasis>fopen()</emphasis>), создает некую контекстную информацию, чтобы она была готов к моменту прихода сообщения ввода/вывода. Почему ее не быт администраторе <code><strong>/dev/null</strong></code>? Потому что POSIX-функции обработки сообщений, принятые по умолчанию, сами вызывают для нас эту функцию. Если бы мы обрабатывали все сообщения самостоятельно, нам, конечно, пришлось бы вызвать данную функцию.</p>
     <p>Функция <emphasis>resmgr_open_bind()</emphasis> не только формирует контекстный блок для последующих сообщений ввода/вывода, но также инициализирует и другие структуры данных, используемые непосредственно библиотекой администратора ресурсов.</p>
     <p>Остальные функции из вышеупомянутого списка достаточно интуитивны, так что отложим их обсуждение до тех пор, пока не дойдем до их явного применения.</p>
     <subtitle>Уровень POSIX</subtitle>
     <p>Второй уровень библиотеки администратора ресурсов является POSIX-уровнем. Как и в случае с базовым уровнем, вы могли бы написать администратор ресурсов и без использования этих функций, но это отняло бы уйму трудов! Прежде чем обсуждать функции уровня POSIX в подробностях, мы должны рассмотреть ряд структур данных базового уровня, приходящие от клиентуры сообщения, а также общую структуру и сферу ответственности администратора ресурсов.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Написание администратора ресурсов</p>
    </title>
    <section>
     <p>Теперь, когда мы знаем основы, — как выглядит мир глазами клиента, в каком цвете видит все администратор ресурсов, и что из себя представляют оба уровня библиотеки — пришло время сконцентрироваться на деталях.</p>
     <p>В этом разделе мы рассмотрим следующие темы:</p>
     <p>• структуры данных;</p>
     <p>• структуру администратора ресурсов;</p>
     <p>• структуры данных POSIX-уровня;</p>
     <p>• подпрограммы обработки сообщений;</p>
     <p>• и, конечно, множество примеров.</p>
     <p>Постарайтесь запомнить приведенную ниже «большую картинку» — на ней изображено практически все, что имеет отношение к администратору ресурсов:</p>
     <image l:href="#img_51.png"/>
     <p>Архитектура администратора ресурсов — общая схема.</p>
    </section>
    <section>
     <title>
      <p>Структуры данных</p>
     </title>
     <p>Первое, в чем следует разобраться, — это структуры данных, которые управляют работой библиотеки:</p>
     <p>• управляющая структура <code><strong>resmgr_attr_t</strong></code></p>
     <p>• таблица функций установления соединения <code><strong>resmgr_connect_funcs_t</strong></code></p>
     <p>• таблица функций ввода-вывода <code><strong>resmgr_io_funcs_t</strong></code> и еще одна внутренняя структура данных библиотеки:</p>
     <p>• внутренний блок контекста <code><strong>resmgr_context_t</strong></code></p>
     <p>Позже мы рассмотрим такие типы данных как блок открытого контекста (OCB), атрибутную запись (attributes structure) и запись точки монтирования (mount structure), которые используются POSIX-уровнем библиотеки.</p>
     <subtitle>Управляющая структура <code>resmgr_attr_t</code></subtitle>
     <p>Управляющая структура (типа <code><strong>resmgr_attr_t</strong></code>) передается функции <emphasis>resmgr_start()</emphasis>, которая несколько ее элементов и отвечает за основной цикл приема сообщений.</p>
     <p>Управляющая структура (взято из <code><strong>&lt;sys/dispatch.h&gt;</strong></code>) содержит следующее:</p>
     <p><code>typedef struct _resmgr_attr {</code></p>
     <p><code> unsigned <emphasis>flags</emphasis>;</code></p>
     <p><code> unsigned <emphasis>nparts_max</emphasis>;</code></p>
     <p><code> unsigned <emphasis>msg_max_size</emphasis>;</code></p>
     <p><code> int      (*<emphasis>other_func</emphasis>)(resmgr_context_t *ctp, void *msg);</code></p>
     <p><code>} resmgr_attr_t;</code></p>
     <subtitle>Обработчик сообщений <code><emphasis>other_func</emphasis></code></subtitle>
     <p>Вообще говоря, использования этого элемента следует избегать. Этот элемент, если не равен NULL, указывает на подпрограмму, которая должна быть вызвана, если принято сообщение, не распознанное библиотекой. Хоть это и можно было бы использовать для реализации «нестандартных» сообщений, но это нехорошая практика (применяйте либо обработчики _IO_DEVCTL, либо _IO_MSG — см. ниже). Если вы хотите обрабатывать входящие импульсы, рекомендую для этого применять функцию <emphasis>pulse_attach()</emphasis>.</p>
     <p>Так что оставьте у этого элемента значение NULL.</p>
     <subtitle>Параметры, задающие размеры структур данных</subtitle>
     <p>Эти два параметра используются для управления размерами областей памяти, используемых при обмене сообщениями.</p>
     <p>Параметр <emphasis>nparts_max</emphasis> управляет размером динамически выделяемого вектора ввода/вывода (элемент <emphasis>iov</emphasis> в структуре типа <code><strong>resmgr_context_t</strong></code> — контекстном блоке библиотеки администратора ресурсов, см. ниже). Обычно этот параметр подстраивают, когда некоторые из функций-обработчиков возвращают более чем одноэлементный вектор ввода-вывода (IOV). Отметим, что этот параметр применяется только к исходящим сообщениям на поступающие сообщения не влияет.</p>
     <p>Параметр <emphasis>msg_max_size</emphasis> управляет размером буферного пространства, которое библиотека администратора ресурсов должна выделить под входящее сообщение. Библиотека администратора ресурсов установит этот параметр в значение как минимум соответствующее наибольшему заголовку принимаемого сообщения. Это гарантирует, что когда будет вызвана функция- обработчик, ей будет передан полный заголовок сообщения. Отметим, однако, что присутствие в буфере следующих за заголовком данных (если таковые имеются) не гарантируется, даже если параметр <emphasis>msg_max_size</emphasis> задан достаточно большим. (Размеры буферов обсуждаются в параграфе «Внутренний контекстный блок <code><strong>resmgr_context_t</strong></code>», см. ниже).</p>
     <subtitle>Параметр <code><emphasis>flags</emphasis></code></subtitle>
     <p>Этот параметр дает библиотеке администратора ресурсов дополнительную информацию. В нашем случае мы передадим просто нуль (0). Другие значения этого параметра можно найти в справочном руководстве по Си-библиотеке, в разделе, посвященном функции <emphasis>resmgr_attach()</emphasis>.</p>
     <subtitle>Таблица функций установления соединения <code>resmgr_connect_funcs_t</code></subtitle>
     <p>Когда библиотека администратора ресурсов принимает сообщение, она проверяет тип сообщения и смотрит, что можно сделать. Базовый уровень библиотеки содержит две таблицы, которые определяют это поведение. Это таблица типа <code><strong>resmgr_connect_funcs_t</strong></code>, которая содержит список обработчиков сообщений установления соединения, а также таблица типа <code><strong>геsmgr_io_funсs_t</strong></code>, которая содержит аналогичный список обработчиков сообщений ввода/вывода — ее мы рассмотрим несколько позже.</p>
     <p>Когда придет время заполнить таблицы функций установления соединения и ввода/вывода, рекомендуется сначала воспользоваться функцией <emphasis>iofunc_func_init()</emphasis>, чтобы инициализировать таблицы функциями по умолчанию, определенными на уровне POSIX. Тогда, если вам потребуется заменить обработчик какого-либо сообщения, вы просто подставляете вместо POSIX-обработчика по умолчанию свою собственную функцию. Мы увидим это в разделе «Подстановка своих собственных функций». А сейчас давайте рассмотрим собственно таблицу функций установления соединения (взято из <code><strong>&lt;sys/resmgr.h&gt;</strong></code>):</p>
     <p><code>typedef struct _resmgr_connect_funcs {</code></p>
     <p><code> unsigned nfuncs;</code></p>
     <p><code> int (*open)(ctp, io_open_t *msg, handle, void *extra);</code></p>
     <p><code> int (*unlink)(ctp, io_unlink_t *msg, handle,</code></p>
     <p><code>  void *reserved);</code></p>
     <p><code> int (*rename)(ctp, io_rename_t *msg, handle,</code></p>
     <p><code>  io_rename_extra_t *extra);</code></p>
     <p><code> int (*mknod)(ctp, io_mknod_t *msg, handle,</code></p>
     <p><code>  void *reserved);</code></p>
     <p><code> int (*readlink)(ctp, io_readlink_t *msg, handle,</code></p>
     <p><code>  void *reserved);</code></p>
     <p><code> int (*link)(ctp, io_link_t *msg, handle,</code></p>
     <p><code>  io_link_extra_t *extra);</code></p>
     <p><code> int (*unblock)(ctp, io_pulse_t *msg, handle,</code></p>
     <p><code>  void *reserved);</code></p>
     <p><code> int (*mount) (ctp, io_mount_t *msg, handle,</code></p>
     <p><code>  io_mount_extra_t *extra);</code></p>
     <p><code>} resmgr_connect_funcs_t;</code></p>
     <p>Заметьте, что я сократил прототипы, опустив тип первого параметра, <emphasis>ctp</emphasis> (<code><strong>resmgr_context_t*</strong></code>), и третьего, handle (<code><strong>RESMGR_HANDLE_T</strong></code>).</p>
     <p>Полный прототип для, например, функции <emphasis>open()</emphasis> в действительности имеет вид:</p>
     <p><code>int (*open)(resmgr_context_t *<emphasis>ctp</emphasis>, io_open_t *<emphasis>msg</emphasis>,</code></p>
     <p><code> RESMGR_HANDLE_T *<emphasis>handle</emphasis>, void *<emphasis>extra</emphasis>);</code></p>
     <p>Первый элемент структуры (<emphasis>nfuncs</emphasis>) указывает, насколько она велика (то есть сколько в ней содержится элементов). Для приведенной выше структуры он должен быть равен 8, поскольку элементов в ней восемь (от <emphasis>open()</emphasis> до <emphasis>mount()</emphasis>). Этот элемент нужен главным образом для того, чтобы позволить QSSL обновлять данную библиотеку без каких бы то ни было вредных последствий для вашего кода. Предположим, к примеру, что вы компилировали свой код со значением 8, а затем QSSL обновила библиотеку, и параметр стал равен 9. Библиотека могла бы себе сказать: «Ага! Пользователь библиотеки был скомпилирован с расчетом на 8 функций, а их у нас теперь 9. Надо бы назначить 9-й функции обработчик по умолчанию.» Текущее значение параметра <emphasis>nfuncs</emphasis> хранится в заголовочном файле <code><strong>&lt;sys/resmgr.h&gt;</strong></code> в виде именованной константы _RESMGR_CONNECT_NFUNCS. Используйте эту константу при заполнении таблицы функций установления соединения вручную (хотя <emphasis>лучше всего</emphasis> применять для этого функцию <emphasis>iofunc_func_init()</emphasis>).</p>
     <p>Отметим, что формат у всех прототипов один и тот же. Первый параметр, <emphasis>ctp</emphasis>, указывает на структуру <code><strong>resmgr_context_t</strong></code>. Это внутренний контекстный блок, используемый библиотекой администратора ресурсов и который изменять не следует (за исключением одного поля, к обсуждению которого мы еще вернемся).</p>
     <p>Второй параметр всегда указывает на сообщение. Поскольку функции в таблице предназначены для обработки различных типов сообщений, тип второго параметра в прототипе соответствует типу сообщения, которое данная функция должна обрабатывать.</p>
     <p>Третий параметр — структура типа <code><strong>RESMGR_HANDLE_T</strong></code>, называемая дескриптором (<emphasis>handle</emphasis>). Она используется для идентификации устройства, которому предназначалось сообщение. Мы тоже рассмотрим ее позже, когда будем говорить об атрибутной записи.</p>
     <p>И, наконец, последний параметр является «резервным», или «дополнительным», и используется для функций, которым необходимы какие-либо дополнительные данные. Мы продемонстрируем применение параметра <emphasis>extra</emphasis> по назначению в обсуждении функций-обработчиков сообщений.</p>
     <subtitle>Таблица функций ввода/вывода <code>resmgr_io_funcs_t</code></subtitle>
     <p>Таблица функций ввода/вывода подобна таблице функций установления соединения. Вот она (взято из <code><strong>&lt;sys/resmgr.h&gt;</strong></code>): </p>
     <p><code>typedef struct _resmgr_io_funcs {</code></p>
     <p><code> unsigned nfuncs;</code></p>
     <p><code> int (*read)(ctp, io_read_t *msg, ocb);</code></p>
     <p><code> int (*write)(ctp, io_write_t *msg, ocb);</code></p>
     <p><code> int (*close_ocb)(ctp, void *reserved, ocb);</code></p>
     <p><code> int (*stat)(ctp, io_stat_t *msg, ocb);</code></p>
     <p><code> int (*notify)(ctp, io_notify_t *msg, ocb);</code></p>
     <p><code> int (*devctl)(ctp, io_devctl_t *msg, ocb);</code></p>
     <p><code> int (*unblock)(ctp, io_pulse_t *msg, ocb);</code></p>
     <p><code> int (*pathconf)(ctp, io_pathconf_t *msg, ocb);</code></p>
     <p><code> int (*lseek)(ctp, io_lseek_t *msg, ocb);</code></p>
     <p><code> int (*chmod)(ctp, io_chmod_t *msg, ocb);</code></p>
     <p><code> int (*chown)(ctp, io_chown_t *msg, ocb);</code></p>
     <p><code> int (*utime)(ctp, io_utime_t *msg, ocb);</code></p>
     <p><code> int (*openfd)(ctp, io_openfd_t *msg, ocb);</code></p>
     <p><code> int (*fdinfo)(ctp, io_fdinfo_t *msg, ocb);</code></p>
     <p><code> int (*lock)(ctp, io_lock_t *msg, ocb);</code></p>
     <p><code> int (*space)(ctp, io_space_t *msg, ocb);</code></p>
     <p><code> int (*shutdown)(ctp, io_shutdown_t *msg, ocb);</code></p>
     <p><code> int (*mmap)(ctp, io_mmap_t *msg, ocb);</code></p>
     <p><code> int (*msg)(ctp, io_msg_t *msg, ocb);</code></p>
     <p><code> int (*umount)(ctp, void *msg, ocb);</code></p>
     <p><code> int (*dup)(ctp, io_dup_t *msg, ocb);</code></p>
     <p><code> int (*close_dup)(ctp, io_close_t *msg, ocb);</code></p>
     <p><code> int (*lock_ocb)(ctp, void *reserved, ocb);</code></p>
     <p><code> int (*unlock_ocb)(ctp, void *reserved, ocb);</code></p>
     <p><code> int (*sync)(ctp, io_sync_t *msg, ocb);</code></p>
     <p><code>} resmgr_io_funcs_t;</code></p>
     <p>В этой структуре я тоже сократил прототипы, опустив тип элемента <emphasis>ctp</emphasis> (<code><strong>resmgr_context_t*</strong></code>) и тип последнего элемента, <emphasis>ocb</emphasis> (<code><strong>RESMGR_OCB_T*</strong></code>). Полный прототип, например, для функции <emphasis>read()</emphasis> в действительности имеет вид:</p>
     <p><code>int (*read)(resmgr_context_t *<emphasis>ctp</emphasis>, io_read_t *<emphasis>msg</emphasis>, RESMGR_OCB_T *<emphasis>ocb</emphasis>);</code></p>
     <p>Самый первый элемент структуры (<emphasis>nfuncs</emphasis>) указывает, насколько она велика (то есть сколько элементов она содержит). Текущее значение этого элемента содержится в константе _RESMGR_IO_NFUNCS.</p>
     <p>Отметим, что списки параметров в таблице функций ввода/вывода также довольно однообразны. Первый параметр — <emphasis>ctp</emphasis>, второй параметр — <emphasis>msg</emphasis>, как и у обработчиков из таблицы функций установления соединения.</p>
     <p>Третий параметр, однако, отличается. Этот параметр называется <emphasis>ocb</emphasis>, что расшифровывается как «Open Context Block» — «блок открытого контекста». Этот блок содержит контекст, созданный обработчиком сообщения установления соединения (например, в результате клиентского запроса <emphasis>open()</emphasis>) и доступный функциям ввода/вывода.</p>
     <p>Как уже упоминалось ранее, когда придет время заполнять таблицы функций, рекомендуется пользоваться для этого функцией <emphasis>iofunc_func_init()</emphasis>, чтобы сначала загрузить таблицы POSIX-обработчиками по умолчанию. Если же вам будет нужно переопределить обработчики сообщений определенного типа, вы сможете просто заменить POSIX-обработчики по умолчанию на свои собственные. Мы рассмотрим это в разделе «Подстановка своих собственных функций».</p>
     <subtitle>Внутренний контекстный блок <code>resmgr_context_t</code></subtitle>
     <p>И, наконец, еще одна структура данных используется базовым уровнем библиотеки, чтобы отслеживать кое-какую информацию <emphasis>для себя</emphasis>. Вам не следует изменять содержимое этой структуры, за исключением одного элемента — вектора ввода/вывода <emphasis>iov</emphasis>.</p>
     <p>Вот эта структура данных (взято из <code><strong>&lt;sys/resmgr.h&gt;</strong></code>):</p>
     <p><code>typedef struct _resmgr_context {</code></p>
     <p><code> int                 <emphasis>rcvid</emphasis>;</code></p>
     <p><code> struct _msg_info    <emphasis>info</emphasis>;</code></p>
     <p><code> resmgr_iomsgs_t     *<emphasis>msg</emphasis>;</code></p>
     <p><code> struct _resmgr_ctrl *<emphasis>ctrl</emphasis>;</code></p>
     <p><code> int                 <emphasis>id</emphasis>;</code></p>
     <p><code> int                 <emphasis>status</emphasis>;</code></p>
     <p><code> int                 <emphasis>offset</emphasis>;</code></p>
     <p><code> int                 <emphasis>size</emphasis>;</code></p>
     <p><code> iov_t               <emphasis>iov</emphasis>[1];</code></p>
     <p><code>} resmgr_context_t;</code></p>
     <p>Как и в случае с другими структурами данных, я позволил себе опустить зарезервированные поля.</p>
     <p>Давайте взглянем на ее содержимое.</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>rcvid</emphasis></td>
       <td align="left" valign="top">Идентификатор отправителя, полученный от <emphasis>MsgReceivev()</emphasis>. Указывает, кому вы должны ответить (если вы намерены отвечать самостоятельно).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>info</emphasis></td>
       <td align="left" valign="top">Содержит информационную структуру, возвращаемую функцией <emphasis>MsgReceivev()</emphasis> в основном цикле приема сообщений библиотеки администратора ресурсов. Полезна для получения информации о клиенте, включая дескриптор узла, идентификатор процесса (PID), идентификатор потока и т.д. Подробнее см. документацию по функции <emphasis>MsgReceivev()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>msg</emphasis></td>
       <td align="left" valign="top">Указатель на объединение (union) всех возможных типов сообщений. Практически бесполезен, потому что каждая из ваших функций-обработчиков получает соответствующий элемент объединения вторым параметром.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>ctrl</emphasis></td>
       <td align="left" valign="top">Указатель на управляющую структуру, которую вы передали в самом начале. Опять же, для вас этот параметр не очень полезен, но зато полезен для библиотеки администратора ресурсов.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>id</emphasis></td>
       <td align="left" valign="top">Идентификатор точки монтирования, которой предназначалось сообщение. Когда вы вызывали <emphasis>resmgr_attach()</emphasis>, она вернула вам небольшой целочисленный идентификатор. Это и есть значение <emphasis>id</emphasis>. Отметим, что вы вероятнее всего никогда не будете использовать этот параметр самостоятельно, а будете полагаться вместо этого на атрибутную запись, передаваемую вам обработчиком <emphasis>io_open()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>status</emphasis></td>
       <td align="left" valign="top">Сюда ваша функция-обработчик помещает результат выполнения операции. Отметим, что вы должны всегда использовать макрос <emphasis>_RESMGR_STATUS()</emphasis> для заполнения этого поля. Например, если вы обрабатываете сообщение установления соединения от функции <emphasis>open()</emphasis>, причем ваш администратор ресурса предоставляет доступ «только для чтения», а клиент хотел открыть ресурс на запись, вы возвратите клиенту через <emphasis>errno</emphasis> код EROFS при помощи (обычно) <code><strong>_RESMGR_STATUS(ctp, EROFS)</strong></code>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>offset</emphasis></td>
       <td align="left" valign="top">Текущее смещение (в байтах) в клиентском буфере сообщений. Имеет смысл для базового уровня библиотеки только при чтении составных сообщений функцией <emphasis>resmgr_msgreadv()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>size</emphasis></td>
       <td align="left" valign="top">Этот параметр говорит, сколько байт в буфере сообщения, переданном вашей функции-обработчику, являются достоверными. Это важная цифра, поскольку она указывает на то, требуется ли читать дополнительные данные от клиента (например, если не все данные были считаны базовым уровнем библиотеки), и надо ли выделить память для ответа клиенту (например, для ответа на запрос <emphasis>read()</emphasis>). (Отметим, что в версии 2.00 есть ошибка, из-за которой это поле <emphasis>не заполняется</emphasis> в случае несоставного сообщения установления соединения. Все остальные сообщения обрабатываются корректно. Обходной путь здесь (и только здесь!) заключается в использовании параметра <emphasis>msglen</emphasis> структуры <emphasis>info</emphasis>.)</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>iov</emphasis></td>
       <td align="left" valign="top">Таблица векторов ввода/вывода, в которую вы можете записывать возвращаемые значения, если это необходимо. Например, когда клиент вызывает <emphasis>read()</emphasis>, и у вас вызывается соответствующий обработчик <emphasis>read()</emphasis>, вам может потребоваться возвратить клиенту данные. Можно задать эти данные при помощи массива iov и возвратить что-нибудь типа <code><strong>_RESMGR_NPARTS(2)</strong></code>, указав тем самым (в нашем случае), векторы <code><strong>iov[0]</strong></code> и <code><strong>iov[1]</strong></code> содержат данные для клиента. Заметьте, что массив <emphasis>iov</emphasis> определен как одноэлементный. Однако, заметьте также, что он очень удобно расположен в конце структуры. Фактическое число элементов в массиве <emphasis>iov</emphasis> определяете вы сами, когда присваиваете значение полю <emphasis>nparts_max</emphasis> вышеупомянутой управляющей структуры (см. параграф «Управляющая структура <code><strong>resmgr_attr_t</strong></code>»).</td>
      </tr>
     </table>
    </section>
    <section>
     <title>
      <p>Структура администратора ресурсов</p>
     </title>
     <p>Теперь, когда мы имеем представление о структурах данных, мы можем обсудить взаимодействие между компонентами, которые вам предстоит написать, чтобы ваш администратор ресурсов мог что-нибудь реально <emphasis>сделать</emphasis>.</p>
     <p>Мы рассмотрим:</p>
     <p>• Функцию <emphasis>resmgr_attach()</emphasis> и ее параметры;</p>
     <p>• Подстановку своих собственных функций;</p>
     <p>• Общую схему работы администратора ресурсов;</p>
     <p>• Сообщения, которые должны бы быть сообщениями установления соединения, но таковыми не являются;</p>
     <p>• Составные сообщения.</p>
     <subtitle>Функция <code><emphasis>resmgr_attach()</emphasis></code> и ее параметры</subtitle>
     <p>Как вы уже видели в приведенном выше примере с администратором <code><strong>/dev/null</strong></code>, первое, что вы должны сделать — это зарегистрировать у администратора процессов свою точку монтирования. Это делается с помощью функции <emphasis>resmgr_attach()</emphasis>, которая имеет следующий прототип:</p>
     <p><code>int resmgr_attach(void *<emphasis>dpp</emphasis>, resmgr_attr_t *<emphasis>resmgr_attr</emphasis>,</code></p>
     <p><code> const char *<emphasis>path</emphasis>, enum _file_type <emphasis>file_type</emphasis>,</code></p>
     <p><code> unsigned <emphasis>flags</emphasis>,</code></p>
     <p><code> const resmgr_connect_funcs_t *<emphasis>connect_funcs</emphasis>,</code></p>
     <p><code> const resmgr_io_funcs_t *<emphasis>io_funcs</emphasis>,</code></p>
     <p><code> RESMGR_HANDLE_T *<emphasis>handle</emphasis>);</code></p>
     <p>Давайте исследуем по порядку ее аргументы и посмотрим, как они применяются.</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>dpp</emphasis></td>
       <td align="left" valign="top">Дескриптор диспетчера (dispatch handle). Обеспечивает интерфейсу диспетчеризации возможность управлять приемом сообщений для вашего администратора ресурсов.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>resmgr_attr</emphasis></td>
       <td align="left" valign="top">Управляет характеристиками администратора ресурсов, как обсуждалось ранее.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>path</emphasis></td>
       <td align="left" valign="top">Точка монтирования, которую вы регистрируете. Если вы регистрируете дискретную точку монтирования (как, например, в случае с <code><strong>/dev/null</strong></code> или <code><strong>/dev/ser1</strong></code>), клиент должен указывать ее точно, без каких бы то ни было дополнительных компонентов имени пути в ее конце. Если вы регистрируете каталоговую точку монтирования (как было бы, например, в случае с сетевой файловой системой, монтируемой как <code><strong>/nfs</strong></code>), то соответствие тоже должно быть точным, но с той оговоркой, что в этом случае продолжение имени пути допускается; то, что идет после точки монтирования, будет передано функции установления соединения (например, имя пути <code><strong>/nfs/etc/passwd</strong></code> даст совпадение с точкой монтирования сетевой файловой системой, а «остатком» будет <code><strong>etc/passwd</strong></code>). (Эта особенность, кстати, может пригодиться и там, где на первый взгляд логичнее было бы регистрировать дискретную точку монтирования — см. параграф «Регистрация префикса» раздела «Взгляд со стороны администратора ресурсов» — <emphasis>прим. ред</emphasis>.)</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>file_type</emphasis></td>
       <td align="left" valign="top">Класс администратора ресурсов. См. ниже.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>flags</emphasis></td>
       <td align="left" valign="top">Дополнительные флаги, управляющие поведением вашего администратора ресурсов. Эти флаги выбираются из множества _RESMGR_FLAG_BEFORE, _RESMGR_FLAG_AFTER, _RESMGR_FLAG_DIR и константы 0. Флаги «BEFORE» (букв, «перед») и «AFTER» (букв, «после») указывают на то, что ваш администратор ресурсов хочет зарегистрироваться на данной точке монтирования перед или, соответственно, после других. Эти два флага могут быть полезны, если надо реализовать объединенные файловые системы. Мы вскоре вернемся к взаимосвязи этих флагов. Флаг «DIR.» («каталог») указывает на то, что ваш администратор ресурса хочет обслуживать указанную точку монтирования и все, что находится ниже ее — этот стиль характерен для администратора файловой системы, в противоположность администратору ресурсов, регистрирующему дискретную точку монтирования.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>connect_funcs</emphasis> и <emphasis>io_funcs</emphasis> </td>
       <td align="left" valign="top">Эти параметры являются просто списком функций установления соединения и функций ввода/вывода, которые вы хотите привязать к точке монтирования.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>handle</emphasis></td>
       <td align="left" valign="top">Это «расширяемая» структура (также известная как «атрибутная запись»), описывающая монтируемый ресурс. Например, в случае последовательного порта вы могли бы расширить стандартную атрибутную запись POSIX-уровня информацией о базовом адресе последовательного порта, скорости обмена в бодах, и т.д.</td>
      </tr>
     </table>
     <p>Вы можете вызывать функцию <emphasis>resmgr_attach()</emphasis> столько раз, сколько вам захочется зарегистрировать различных точек монтирования. Вы также можете вызывать функцию <emphasis>resmgr_attach() из тела функций установления соединения или ввода/вывода</emphasis> — эта аккуратная особенность позволяет вам «создавать» устройства «на лету».</p>
     <p>Когда вы определились с точкой монтирования и хотите ее зарегистрировать, вы должны сообщить администратору процессов, хочет ли ваш администратор ресурсов обрабатывать запросы от кого попало или только от клиентуры, которая помечает свои сообщения установления соединения специальными метками. Например, рассмотрим драйвер очередей сообщений POSIX (mqueue). Ему совершенно ни к чему «обычные» вызовы <emphasis>open()</emphasis> от старых добрых клиентов — он просто не будет знать, что с ними делать. Он примет сообщения только от тех клиентов, которые используют POSIX-вызовы <emphasis>mq_open()</emphasis>, <emphasis>mq_receive()</emphasis>, и т.п. Чтобы не позволять администратору процессов даже перенаправлять «обычные» запросы администратору очередей mqueue, у этого администратора в параметре параметр <emphasis>file_type</emphasis> задается значение _TYPE_MQUEUE. Это означает, что когда клиент пытается с помощью администратора процессов выполнить разрешение имени, при этом явно не определив, что хочет поговорить с администратором ресурсов, зарегистрированным как _FTYPE_MQUEUE, администратор процессов не будет даже рассматривать администратор mqueue как возможный вариант.</p>
     <p>Если только вы не делаете что-либо уж очень специфичное, вам лучше всего подойдет значение <emphasis>file_type</emphasis>, равное _FTYPE_ANY, означающее, что ваш администратор ресурсов готов обработать запрос от любого клиента. Полный список именованных констант _FTYPE_* приведен в файле <code><strong>&lt;sys/ftype.h&gt;</strong></code>.</p>
     <p>Что касательно флагов «BEFORE» и «AFTER», тут все становится интереснее. Вы можете задать либо один из этих флагов, либо константу 0.</p>
     <p>Давайте посмотрим, как это работает. Стартуют несколько администраторов ресурсов, в указанном в таблице порядке. В таблице также приведены флаги, указанные каждым из них в параметре <emphasis>flags</emphasis>. Взгляните на получившуюся очередность.</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Администратор</th>
       <th align="left" valign="top">Флаг</th>
       <th align="left" valign="top">Очередность</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">_RESMGR_BEFORE</td>
       <td align="left" valign="top">1</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">_RESMGR_AFTER</td>
       <td align="left" valign="top">1, 2</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">3</td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">1, 3 ,2</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">4</td>
       <td align="left" valign="top">_RESMGR_BEFORE</td>
       <td align="left" valign="top">1, 4, 3, 2</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">5</td>
       <td align="left" valign="top">_RESMGR_AFTER</td>
       <td align="left" valign="top">1, 4, 3, 5, 2</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">6</td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">1, 4, 6, 3, 5, 2</td>
      </tr>
     </table>
     <p>Из таблицы видно, что первый администратор ресурса, явно определивший флаг, далее не сдвигается со своей позиции.(См. таблицу: администратор ресурсов № 1 был первым определившим флаг «BEFORE»; кто бы теперь ни зарегистрировался, он так и останется первым в списке. Аналогично, администратор ресурсов № 2 был первым определившим флаг «AFTER» — и снова, независимо от того, кто еще будет регистрироваться после него, он всегда остается в списке последним.) Если не определен никакой флаг, это действует как флаг «между». Когда стартует администратор ресурсов № 3 (указав нулевой флаг), он помещается в середину очереди. Как и в случае с флагами «BEFORE» и «AFTER», здесь имеет место упорядочивание, в результате чего все вновь регистрирующиеся «средние» администраторы ресурсов располагаются перед уже зарегистрированными «средними».</p>
     <p>Однако, в действительности, только в очень редких случаях вам придется регистрировать более одного, и в еще меньшем числе случаев — более двух администраторов ресурсов при той же самой точке монтирования. Полезный совет: обеспечьте возможность установки флагов непосредственно в командной строке администратора ресурса, чтобы конечный пользователь вашего администратора ресурса мог сам задать, например, флаг «BEFORE» опцией <code><strong>-b</strong></code>, флаг «AFTER» — опцией <code><strong>-а</strong></code>, а нулевой флаг («между») был бы, скажем, установкой по умолчанию.</p>
     <p>Имейте в виду, что данное обсуждение применимо только для администраторов ресурсов, регистрируемых при одной и той же точке монтирования. Монтирование «<code><strong>/nfs</strong></code>» с флагом «BEFORE» и «<code><strong>/disk2</strong></code>» с флагом «AFTER» не будет иметь никакого взаимного влияния. Однако, если вы затем будете монтировать еще одну «<code><strong>/nfs</strong></code>» или «<code><strong>/disk2</strong></code>», вот тогда эти флаги и проявят себя.</p>
     <p>И наконец, функция <emphasis>resmgr_attach()</emphasis> в случае успешного завершения возвращает дескриптор (handle) в виде небольшого целого числа (или -1 при неудаче). Этот дескриптор можно затем применить для того, чтобы убрать данное имя пути из внутренней таблицы имен путей администратора процессов.</p>
     <subtitle>Подстановка своих собственных функций</subtitle>
     <p>Проектируя свой самый первый администратор ресурсов вы, скорее всего, захотите действовать постепенно. Было бы очень досадно написать несколько тысяч строк кода только для того, чтобы понять, что в самом начале была допущена фундаментальная ошибка, и теперь придется либо наспех затыкать дырки (э-э, я хотел сказать — вносить коррективы), либо выкинуть все это и начать заново.</p>
     <p>Чтобы все работало как надо, рекомендуемым подходом здесь является использование функции-инициализатора <emphasis>iofunc_func_init()</emphasis> из уровня POSIX, чтобы заполнить таблицы функций установления соединения и функций ввода/вывода заданными по умолчанию функциями POSIX-уровня. Это значит; что вы можете фактически написать каркас вашего администратора ресурсов, как мы уже делали выше, с помощью всего нескольких вызовов.</p>
     <p>Какую функцию запрограммировать первой — это будет зависеть от того, какой администратор ресурсов вы пишете. Если это администратор файловой системы, отвечающий за точку монтирования и все, что под ней, то вам, скорее всего, лучше всего начать с функции <emphasis>io_open()</emphasis>. С другой стороны, если вы пишете администратор ресурса с дискретной точкой монтирования, который выполняет «традиционные» операции ввода/вывода (то есть вы будете общаться с ним преимущественно вызовами типа <emphasis>read()</emphasis> и <emphasis>write()</emphasis>), то лучшей стартовой позицией для вас были бы функции <emphasis>io_read()</emphasis> и/или <emphasis>io_write()</emphasis>. Если же вы пишете администратор ресурса с дискретной точкой монтирования, но вместо «традиционных» операций ввода/вывода основу его функциональности составляют вызовы типа <emphasis>devctl()</emphasis> или <emphasis>ioctl()</emphasis>, то правильнее было бы начать с <emphasis>io_devctl()</emphasis>.</p>
     <p>Независимо от того, с чего вы начинаете, вам нужно будет удостовериться в том, что ваши функции вызываются так, как вы предполагаете. В данном ключе функции-обработчики POSIX- уровня по умолчанию обладают очень полезным свойством — их можно помещать непосредственно в таблицы функций установления соединения и таблицы функций ввода/вывода.</p>
     <p>Это означает, что если вы захотите что-то дополнительно проконтролировать, просто добавьте дополнительный диагностический вызов <emphasis>printf()</emphasis>, чтобы он сказал что-то типа «Я тут!», а затем делайте «то, что надо сделать» — все очень просто.</p>
     <p>Вот фрагмент администратора ресурсов, который перехватывает функцию <emphasis>io_open()</emphasis>:</p>
     <p><code>// Упреждающая декларация</code></p>
     <p><code>int io_open(resmgr_context_t*, io_open_t*,</code></p>
     <p><code> RESMGR_HANDLE_T*, void*);</code></p>
     <empty-line/>
     <p><code>int main() {</code></p>
     <p><code> // Все как в примере /dev/null,</code></p>
     <p><code> // кроме следующего за этой строкой:</code></p>
     <p><code> iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &amp;cfuncs,</code></p>
     <p><code>  _RESMGR_IO_NFUNCS, &amp;ifuncs);</code></p>
     <p><code> // Добавьте это для перехвата управления:</code></p>
     <p><code> cfuncs.open = io_open;</code></p>
     <p>Если вы описали функцию <emphasis>io_open()</emphasis> корректно, как в этом примере кода, то вы можете вызывать функцию, заданную по умолчанию, из вашей собственной!</p>
     <p><code>int io_open(resmgr_context_t *ctp, io_open_t *msg,</code></p>
     <p><code> RESMGR_HANDLE_T *handle, void *extra) {</code></p>
     <p><code> printf("Мы в io_open!\n");</code></p>
     <p><code> return (iofunc_open_default(ctp, msg, handle, extra));</code></p>
     <p><code>}</code></p>
     <p>Таким образом, вы по-прежнему применяете POSIX-обработчик по умолчанию <emphasis>iofunc_open_default()</emphasis>, но заодно перехватываете управление для вызова <emphasis>printf()</emphasis>.</p>
     <p>Очевидно, что вы могли бы выполнить аналогичные действия для функций <emphasis>io_read()</emphasis>, <emphasis>io_write()</emphasis>, <emphasis>io_devctl()</emphasis> и любых других, для которых есть обработчики POSIX-уровня по умолчанию. Идея, кстати, действительно отличная, потому что такой подход показывает вам, что клиент вызывает ваш администратор ресурса именно так, как вы предполагаете.</p>
     <subtitle>Общая схема работы администратора ресурсов</subtitle>
     <p>Как мы уже намекнули выше в разделах, посвященных краткому рассмотрению клиента и администратора ресурсов, последовательность действий начинается на клиентской стороне с вызова <emphasis>open()</emphasis>. Он транслируется в сообщение установления соединения, которое принимается и обрабатывается функцией администратора ресурсов <emphasis>io_open()</emphasis>.</p>
     <p>Это действительно ключевой момент, потому что функция <emphasis>io_open()</emphasis> выполняет для вашего администратора ресурсов функцию «швейцара». Если «швейцар» посмотрит на сообщение и отклонит запрос, вы <emphasis>не получите</emphasis> никаких запросов на ввод/вывод, потому что у клиента не будет корректного дескриптора файла. И наоборот, если «швейцар» пропустит сообщение, тогда клиент получит корректный дескриптор файла, и логично будет ожидать от него сообщений ввода/вывода.</p>
     <p>Но на самом деле роль функции <emphasis>io_open()</emphasis> гораздо значительнее. Она отвечает не только за проверку, может клиент открыть ресурс или нет, но также за следующее:</p>
     <p>• инициализацию внутренних параметров библиотеки;</p>
     <p>• привязку к запросу контекстного блока;</p>
     <p>• привязку к контекстному блоку атрибутной записи.</p>
     <p>Первые две операции выполняются с помощью функции базового уровня <emphasis>resmgr_open_bind()</emphasis>, а привязка атрибутной записи сводится к простому присваиванию.</p>
     <p>Будучи однажды вызвана, <emphasis>io_open()</emphasis> выпадает из рассмотрения. Клиент может либо прислать сообщение ввода/вывода, либо нет, но в любом случае должен будет однажды завершить «сеанс связи» с помощью сообщения, соответствующего функции <emphasis>close()</emphasis>. Заметьте, что если клиента вдруг постигает внезапная смерть (например, он получает SIGSEGV, или выходит из строя узел, на котором он работает), операционная система автоматически синтезирует сообщение <emphasis>close()</emphasis>, чтобы администратор ресурсов смог корректно завершить сессию. Поэтому вы <emphasis>гарантированно</emphasis> получите сообщение <emphasis>close()</emphasis>!</p>
     <subtitle>Сообщения, которые должны быть сообщениями установления соединения, но таковыми не являются</subtitle>
     <p>Тут есть один интересный момент, который вы, может быть, для себя уже отметили. Прототип клиентской функции <emphasis>chown()</emphasis> имеет вид:</p>
     <p><code>int chown(const char *<emphasis>path</emphasis>, uid_t <emphasis>owner</emphasis>, gid_t <emphasis>group</emphasis>);</code></p>
     <p>Вспомните: сообщение об установлении соединения всегда содержит имя пути и является либо однократным, либо устанавливает контекст для дальнейших сообщений ввода/ вывода.</p>
     <p>Так почему же сообщение, соответствующее клиентской функции <emphasis>chown()</emphasis>, не является сообщением установления соединения? К чему здесь сообщение ввода/вывода, когда в прототипе даже дескриптора файла нет?!</p>
     <p>Ответ простой — чтобы облегчить вам жизнь.</p>
     <p>Представьте себе, что было бы, если бы функции типа <emphasis>chown()</emphasis>, <emphasis>chmod()</emphasis>, <emphasis>stat()</emphasis> и им подобные требовали от администратора ресурсов, чтобы он сначала анализировал имя пути, а затем уже выполнял нужные действия. (Именно так, кстати, все реализовано в QNX4.) Типичные проблемы этого подхода:</p>
     <p>• Каждой функции приходится вызывать процедуру поиска.</p>
     <p>• Для функций, у которых есть также версия, ориентированная на файловый дескриптор, драйвер должен обеспечить две отдельные точки входа: одну для версии с именем пути, и еще одну — версии с дескриптором файла.</p>
     <p>В QNX/Neutrino же происходит следующее. Клиент создает <emphasis>составное сообщение</emphasis> — реально это одно сообщение, но оно включает в себя несколько сообщений администратору ресурсов. Без составных сообщений мы могли бы смоделировать функцию <emphasis>chown()</emphasis> чем-то таким:</p>
     <p><code>int chown(const char *path, uid_t owner, gid_t group) {</code></p>
     <p><code> int fd, sts;</code></p>
     <p><code> if ((fd = open(path, O_RDWR)) == -1) {</code></p>
     <p><code>  return (-1);</code></p>
     <p><code> }</code></p>
     <p><code> sts = fchown(fd, owner, group);</code></p>
     <p><code> close(fd);</code></p>
     <p><code> return (sts);</code></p>
     <p><code>}</code></p>
     <p>где функция <emphasis>fchown()</emphasis> — это версия функции <emphasis>chown()</emphasis>, ориентированная на файловые дескрипторы. Проблема здесь в том, что мы в этом случае используем три вызова функций (а значит, и три отдельных транзакции передачи сообщений) и привносим дополнительные накладные расходы применением функций <emphasis>open()</emphasis> и <emphasis>close()</emphasis> на стороне клиента.</p>
     <p>При использовании составных сообщений в QNX/Neutrino непосредственно клиентским вызовом <emphasis>chown()</emphasis> создается одиночное сообщение, выглядящее примерно так:</p>
     <image l:href="#img_52.png"/>
     <p>Составное сообщение.</p>
     <p>Сообщение состоит из двух частей. Первая часть посвящена установлению соединения (подобно сообщению, которое сгенерировала бы функция <emphasis>open()</emphasis>), вторая — вводу/выводу (эквивалент сообщения, генерируемого функцией <emphasis>fchown()</emphasis>). Никакого эквивалента функции <emphasis>close()</emphasis> здесь нет, поскольку мы выбрали сообщение типа _IO_CONNECT_COMBINE_CLOSE, которое гласит: «Открой указанное имя пути, используй полученный дескриптор файла для обработки остальной части сообщения, а когда закончишь дела или столкнешься с ошибкой, закрой дескриптор».</p>
     <p>Написанный вами администратор ресурса даже не заметит, вызвал ли клиент функцию <emphasis>chown()</emphasis> или сначала сделал <emphasis>open()</emphasis>, а потом вызвал <emphasis>fchown()</emphasis> и далее <emphasis>close()</emphasis>. Все это скрыто базовым уровнем библиотеки.</p>
     <subtitle>Составные сообщения</subtitle>
     <p>Как выясняется, концепция составных сообщений полезна не только для экономии ресурсов вследствие уменьшения числа сообщений (как в случае с <emphasis>chown()</emphasis>, см. выше). Она также критически важна для обеспечения атомарности операций.</p>
     <p>Предположим, что в клиентском процессе есть два или более потоков, работающих с одним дескриптором файла. Один из потоков в клиенте вызывает функцию <emphasis>lseek()</emphasis>, за которой следует <emphasis>read()</emphasis>. Все так, как мы и предполагаем. А вот если другой клиента попробует выполнить ту же самую последовательность операций с тем же самым дескриптором файла, вот тут у нас начнутся проблемы. Поскольку функции <emphasis>lseek()</emphasis> и <emphasis>read()</emphasis> друг о друге ничего не знают, то возможно, например, что первый поток выполнит <emphasis>lseek()</emphasis>, а затем будет вытеснен вторым потоком. Второй поток выполнит свои <emphasis>lseek()</emphasis> и <emphasis>read()</emphasis>, после чего освободит процессор. Проблема здесь состоит в том, что поскольку эти два потока разделяют один и тот же дескриптор файла, у первого потока теперь получается неправильное смещение <emphasis>lseek()</emphasis>, поскольку оно было изменено функциями <emphasis>lseek()</emphasis> и <emphasis>read()</emphasis> второго потока! Эта проблема проявляется также с дескрипторами файлов, которые дублируются (<emphasis>dup()</emphasis>) между процессами, не говоря уже о сети.</p>
     <p>Очевидным решением здесь является заключение <emphasis>lseek()</emphasis> и <emphasis>read()</emphasis> в пределы действия мутекса — когда первый поток захватит мутекс, мы будем знать, что он имеет эксклюзивный доступ к дескриптору. Второй поток должен будет ждать освобождения мутекса, прежде чем он сможет творить свое безобразие с позиционированием дескриптора.</p>
     <p>К сожалению, если кто-то вдруг забудет захватить мутекс <emphasis>перед каждой и любой операцией с дескриптором файла</emphasis>, всегда остается возможность того, что такой «незащищенный» доступ может привести поток к чтению или записи данных не туда, куда надо.</p>
     <p>Давайте взглянем на библиотечный вызов <emphasis>readblock()</emphasis> (из <code><strong>&lt;unistd.h&gt;</strong></code>):</p>
     <p><code>int readblock(int <emphasis>fd</emphasis>, size_t <emphasis>blksize</emphasis>, unsigned <emphasis>block</emphasis>,</code></p>
     <p><code> int <emphasis>numblks</emphasis>, void *<emphasis>buff</emphasis>);</code></p>
     <p>(Функция <emphasis>writeblock()</emphasis> описывается аналогично.)</p>
     <p>Вы можете вообразить для функции <emphasis>readblock()</emphasis> довольно «простенькую» реализацию:</p>
     <p><code>int readblock(int fd, size_t blksize, unsigned block,</code></p>
     <p><code> int numblks, void *buff) {</code></p>
     <p><code> lseek(fd, blksize * block, SEEK_SET); // Идем к блоку</code></p>
     <p><code> read(fd, buff, blksize * numblks);</code></p>
     <p><code>}</code></p>
     <p>Очевидно, что от такой реализации в многопоточной среде толку мало. Нам нужно будет как минимум добавить использование мутекса:</p>
     <p><code>int readblock(int fd, size_t blksize, unsigned block,</code></p>
     <p><code> int numblks, void *buff) {</code></p>
     <p><code> pthread_mutex_lock(&amp;block_mutex);</code></p>
     <p><code> lseek(fd, blksize * block, SEEK_SET); // Идем к блоку</code></p>
     <p><code> read(fd, buff, blksize * numblks);</code></p>
     <p><code> pthread_mutex_unlock(&amp;block_mutex);</code></p>
     <p><code>}</code></p>
     <p>(Мы здесь предполагаем, что мутекс уже инициализирован.)</p>
     <p>Этот код по-прежнему уязвим для «незащищенного» доступа — если некий поток вызовет <emphasis>lseek()</emphasis> на этом файловом дескрипторе без предварительной попытки захвата мутекса, вот у нас уже и ошибка.</p>
     <p>Решение проблемы заключается в использовании составного сообщения, аналогично вышеописанному случаю с функцией <emphasis>chown()</emphasis>. В данном случае библиотечная реализация функции <emphasis>readblock()</emphasis> помещает обе операции — <emphasis>lseek()</emphasis> и <emphasis>read()</emphasis> — в единое сообщение и посылает это сообщение администратору ресурсов:</p>
     <image l:href="#img_53.png"/>
     <p>Составное сообщение для функции <emphasis>readblock()</emphasis>.</p>
     <p>Это работает, потому что передача сообщения является атомарной операцией. С точки зрения клиента, сообщение уходит либо целиком, либо не уходит вообще. Поэтому, вмешательство «незащищенной» функции <emphasis>lseek()</emphasis> становится несущественным — когда администратор ресурсов принимает сообщение с запросом <emphasis>readblock()</emphasis>, он делает это за один прием. (Очевидно, что в этом случае пострадает сама «незащищенная» <emphasis>lseek()</emphasis>, поскольку после отработки <emphasis>readblock()</emphasis> смещение на этом дескрипторе файла будет отличаться от того, которое она хотела установить.)</p>
     <p>А как насчет самого администратора ресурсов? Как он <emphasis>обрабатывает</emphasis> операцию <emphasis>readblock()</emphasis> за один прием? Мы вскоре рассмотрим это, когда будем обсуждать операции, выполняемые для каждого компонента составных сообщений.</p>
    </section>
    <section>
     <title>
      <p>Структуры данных уровня POSIX</p>
     </title>
     <p>К подпрограммам POSIX-уровня относятся три структуры данных. Отметьте, что пока речь идет о <emphasis>базовом уровне</emphasis>, вы можете использовать любые структуры данных, которые пожелаете; соответствия определенной структуре и содержанию требует именно уровень POSIX. Однако, преимущества, которые предоставляет POSIX-уровень, с лихвой окупают вносимые ограничения. Как мы увидим далее, вы также сможете дополнять эти структуры вашим собственным содержанием.</p>
     <p>На рисунке приведены эти три структуры данных для случая, когда несколько клиентов используют администратора ресурсов, объявивший два устройства:</p>
     <image l:href="#img_54.png"/>
     <p>Структуры данных — общая схема.</p>
     <p>Структурами данных являются:</p>
     <p><code><strong>iofunc_ocb_t</strong></code> — OCB (блок открытого контекста)</p>
     <p>Содержит информацию по каждому дескриптору файла.</p>
     <p><code><strong>iofunc_attr_t</strong></code> — атрибутная запись</p>
     <p>Содержит информацию по каждому устройству.</p>
     <p><code><strong>iofunc_mount_t</strong></code> — запись точки монтирования</p>
     <p>Содержит информацию по каждой точке монтирования.</p>
     <p>Когда мы обсуждали таблицы функций установления соединения и ввода/вывода, мы уже видели блоки открытого контекста и атрибутные записи — в таблицах функций ввода/вывода OCB был последним передаваемым параметром. Атрибутная запись передавалась как параметр <emphasis>handle</emphasis> (третий по счету) в функциях установления соединения. Запись точки монтирования обычно представляет собой глобальную структуру и привязывается к атрибутной записи «вручную» (в инициализационном коде, написанном вами для вашего администратора ресурса).</p>
     <subtitle>Структура блока открытого контекста (OCB) <code>iofunc_ocb_t</code></subtitle>
     <p>Структура блока открытого контекста (OCB) содержит информацию по каждому дескриптору файла. Это означает, что когда клиент выполняет вызов <emphasis>open()</emphasis> и получает в ответ дескриптор файла (в противоположность коду ошибки), администратор ресурсов создает OCB и связывает его с данным клиентом. Этот OCB будет существовать до тех пор, пока клиент держит данный дескриптор файла открытым. В действительности, OCB и дескриптор файла — всегда согласованная пара. По каждому сообщению ввода/вывода от клиента библиотека администратора ресурсов автоматически ищет нужный OCB и вместе с сообщением передает его нужной функции из таблицы функций ввода/вывода. Это ответ на вопрос, зачем всем функциям ввода/вывода параметр <emphasis>ocb</emphasis>. В конце концов клиент закроет дескриптор файла (применив <emphasis>close()</emphasis>), что заставит администратор ресурса отвязать OCB от дескриптора файла и от клиента. Заметьте, что клиентская функция <emphasis>dup()</emphasis> просто увеличивает счетчик связей. В этом случае OCB отделяется от дескриптора файла и от клиента только тогда, когда значение счетчика связей достигнет нуля (то есть когда число вызовов <emphasis>close()</emphasis> будет соответствовать числу <emphasis>open()</emphasis> и <emphasis>dup()</emphasis>).</p>
     <p>Как вы, наверное, догадываетесь, в OCB содержатся важные вещи по каждому открытию ресурса и по каждому дескриптору файла. Вот его содержание (взято из <code><strong>&lt;sys/iofunc.h&gt;</strong></code>):</p>
     <p><code>typedef struct _iofunc_ocb {</code></p>
     <p><code> IOFUNC_ATTR_T *<emphasis>attr</emphasis>;</code></p>
     <p><code> int32_t       <emphasis>ioflag</emphasis>;</code></p>
     <p><code> CM НИЖЕ!!!    <emphasis>offset</emphasis>;</code></p>
     <p><code> uint16_t      <emphasis>sflag</emphasis>;</code></p>
     <p><code> uint16_t      <emphasis>flags</emphasis>;</code></p>
     <p><code>} iofunc_ocb_t;</code></p>
     <p>Проигнорируем пока комментарий относительно поля <emphasis>offset</emphasis>; мы вернемся к этому вопросу сразу же после данного обсуждения.</p>
     <p>Поля структуры <code><strong>iofunc_ocb_t</strong></code>:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>attr</emphasis></td>
       <td align="left" valign="top">Указатель на атрибутную запись, связанную с данным блоком OCB. В функциях ввода/вывода вы будете встречать устоявшуюся идиому «<code><strong>ocb-&gt;attr</strong></code>»; она используется для получения доступа к элементам атрибутной записи.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>ioflag</emphasis></td>
       <td align="left" valign="top">Режим открытия, то есть как был открыт ресурс (например, «только для чтения»). Заметьте, что поле <emphasis>ioflag</emphasis> содержит режим открытия (который был передан клиентской функции <emphasis>open()</emphasis>) плюс единица. Например, режим открытия O_RDONLY (значение 0) появится в поле <emphasis>ioflag</emphasis>, как значение, равное единице (1) (константа _READ из <code><strong>&lt;stdio.h&gt;</strong></code>). Это позволяет трактовать два младших бита поля ioflag как флаги разрешения чтения и записи (<code><strong>ioflag &amp; _READ</strong></code> указывает на право доступа по чтению; <code><strong>ioflag &amp; _WRITE</strong></code> — по записи).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>offset</emphasis></td>
       <td align="left" valign="top">Текущее смещение <emphasis>lseek()</emphasis> в данном ресурсе.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>sflag</emphasis></td>
       <td align="left" valign="top">Флаг разделяемого использования (см. <code><strong>&lt;share.h&gt;</strong></code>), используемый с клиентской функцией вызова <emphasis>sopen()</emphasis>. Возможны значения SH_COMPAT, SH_DENYRW, SH_DENYWR, SH_DENYRD, и SH_DENYN</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>flags</emphasis></td>
       <td align="left" valign="top">Системные флаги. В настоящее время поддерживаются два флага: IOFUNC_OCB_PRIVILEGED, указывающий на то, что этот OCB был создан в результате сообщения установления соединения от привилегированного процесса, и IOFUNC_OCB_MMAP, указывающий, используется ли этот OCB функцией <emphasis>mmap()</emphasis> на стороне клиента. На настоящий момент никаких других флагов не определено. Вы можете использовать биты, заданные в IOFUNC_OCB_FLAGS_PRIVATE, по своему собственному усмотрению.</td>
      </tr>
     </table>
     <p>Если вы хотите наряду со «стандартным» OCB сохранить какие-либо дополнительные данные, то будьте покойны — OCB можно «расширять». Мы обсудим это в разделе «Дополнительно».</p>
     <subtitle>Это странное поле <emphasis>offset</emphasis></subtitle>
     <p>Поле <emphasis>offset</emphasis>, скажем так, как минимум любопытно. Посмотрите в <code><strong>&lt;sys/iofunc.h&gt;</strong></code>, как оно реализовано. В зависимости от того, какие у вас заданы флаги препроцессора, вы можете получить одну из шести (!) возможных раскладок поля <emphasis>offset</emphasis>. Но не беспокойтесь особо по поводу реализации — реально есть смысл рассматривать только два случая, в зависимости от того, хотите вы поддерживать 64-разрядные смещения или нет:</p>
     <p>• если да, то поле <emphasis>offset</emphasis> 64-разрядное;</p>
     <p>• если нет (у вас 32-разрядные целые), то поле <emphasis>offset</emphasis> — это младшие 32 бита; старшие 32 бита хранятся в поле <emphasis>offset_hi</emphasis>.</p>
     <p>Для наших целей, если речь не идет о явном противопоставлении 32- и 64-разрядных значений, мы будем предполагать, что все смещения являются 64-разрядными (типа <code><strong>off_t</strong></code>), и платформа знает, что делать с 64-разрядными числами.</p>
     <subtitle>Атрибутная запись <code>iofunc_attr_t</code></subtitle>
     <p>В то время как OCB был определен как структура данных по каждому дескриптору файла, атрибутная запись является структурой данных по каждому устройству. Вы видели, что стандартный OCB типа <code><strong>iofunc_ocb_t</strong></code> имеет элемент, называемый <emphasis>attr</emphasis>, который представляет собой указатель на атрибутную запись. Это сделано для того, чтобы у OCB был доступ к информации об устройстве. Давайте посмотрим на атрибутную запись (взято из <code><strong>&lt;sys/iofunc.h&gt;</strong></code>):</p>
     <p><code>typedef struct _iofunc_attr {</code></p>
     <p><code> IOFUNC_MOUNT_T           *<emphasis>mount</emphasis>;</code></p>
     <p><code> uint32_t                 <emphasis>flags</emphasis>;</code></p>
     <p><code> int32_t                  <emphasis>lock_tid</emphasis>;</code></p>
     <p><code> uint16_t                 <emphasis>lock_count</emphasis>;</code></p>
     <p><code> uint16_t                 <emphasis>count</emphasis>;</code></p>
     <p><code> uint16_t                 <emphasis>rcount</emphasis>;</code></p>
     <p><code> uint16_t                 <emphasis>wcount</emphasis>;</code></p>
     <p><code> uint16_t                 <emphasis>rlocks</emphasis>;</code></p>
     <p><code> uint16_t                 <emphasis>wlocks</emphasis>;</code></p>
     <p><code> struct _iofunc_mmap_list *<emphasis>mmap_list</emphasis>;</code></p>
     <p><code> struct _iofunc_lock_list *<emphasis>lock_list</emphasis>;</code></p>
     <p><code> void                     *<emphasis>list</emphasis>;</code></p>
     <p><code> uint32_t                 <emphasis>list_size</emphasis>;</code></p>
     <p><code> СМ_НИЖЕ!!!               <emphasis>nbytes</emphasis>;</code></p>
     <p><code> СМ_НИЖЕ!!!               <emphasis>inode</emphasis>;</code></p>
     <p><code> uid_t                    <emphasis>uid</emphasis>;</code></p>
     <p><code> gid_t                    <emphasis>gid</emphasis>;</code></p>
     <p><code> time_t                   <emphasis>mtime</emphasis>;</code></p>
     <p><code> time_t                   <emphasis>atime</emphasis>;</code></p>
     <p><code> time_t                   <emphasis>ctime</emphasis>;</code></p>
     <p><code> mode_t                   <emphasis>mode</emphasis>;</code></p>
     <p><code> nlink_t                  <emphasis>nlink</emphasis>;</code></p>
     <p><code> dev_t                    <emphasis>rdev</emphasis>;</code></p>
     <p><code>} iofunc_attr_t;</code></p>
     <p>У полей <emphasis>nbytes</emphasis> и <emphasis>inode</emphasis> такой же набор директив условной компиляции, что и у поля <emphasis>offset</emphasis> в OCB (см. параграф «Это странное поле offset»).</p>
     <p>Заметьте, что некоторые из полей атрибутной записи полезны только для вспомогательных функций POSIX.</p>
     <p>Давайте рассмотрим поля в индивидуальном порядке:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>mount</emphasis></td>
       <td align="left" valign="top">Указатель на необязательную запись точки монтирования (типа <code><strong>iofunc_mount_t</strong></code>). Он применяется аналогично указателю на атрибутную запись, входящему в состав в OCB, за исключением того, что здесь это поле может принимать NULL — в этом случае для записи точки монтирования применяются установки по умолчанию (см. ниже параграф «Запись точки монтирования <code><strong>iofunc_mount_t</strong></code>»). Как уже было упомянуто, запись точки монтирования привязывается к атрибутной записи «вручную» в инициализационном коде вашего администратора ресурсов.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>flags</emphasis></td>
       <td align="left" valign="top">Содержит флаги, которые описывают состояние других полей атрибутной записи. Мы вскоре к ним вернемся.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>lock_tid</emphasis></td>
       <td align="left" valign="top">Для предупреждения проблем синхронизации доступ множества потоков к одной и той же атрибутной записи должен быть взаимно исключающим. Поле <emphasis>lock_tid</emphasis> содержит идентификатор потока (thread ID), которым данная атрибутная запись блокирована в настоящий момент.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>lock_count</emphasis></td>
       <td align="left" valign="top">Указывает, сколько потоков пытаются использовать данную атрибутную запись. Нулевое значение указывает на то, что структура не заблокирована. Значение, большее нуля (единица или более) указывает на то, что данную структуру используют один или более потоков.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>count</emphasis></td>
       <td align="left" valign="top">Указывает на число OCB, которые по какой-либо причине открыли эту атрибутную запись. Например, если у одного клиента есть OCB, открытый на чтение, у другого — другой OCB, открытый на чтение/запись, и оба эти OCB указывают на одну и ту же атрибутную запись, то значение <emphasis>count</emphasis> для нее должно быть равно 2. Это будет указывать на то, что данный ресурс открыт двумя клиентами.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>rcount</emphasis></td>
       <td align="left" valign="top">Число читателей. В примере, приведенном для <emphasis>count</emphasis>, <emphasis>rcount</emphasis> будет также иметь значение 2, потому что ресурс открыт на чтение двумя клиентами.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>wcount</emphasis></td>
       <td align="left" valign="top">Число писателей. В примере, приведенном для <emphasis>count</emphasis>, <emphasis>wcount</emphasis> будет иметь значение 1, потому что ресурс открыт на чтение только одним клиентом.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>rlocks</emphasis></td>
       <td align="left" valign="top">Показывает число OCB, наложивших на данный ресурс блокировки по чтению. Если значение этого поля равно нулю, это означает, что никаких блокировок по чтению нет, но могут быть блокировки по записи.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>wlocks</emphasis></td>
       <td align="left" valign="top">Аналогично <emphasis>rlocks</emphasis>, только для блокировок по записи.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>mmap_list</emphasis></td>
       <td align="left" valign="top">Для внутреннего использования POSIX-функцией <emphasis>iofunc_mmap_default()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>lock_list</emphasis></td>
       <td align="left" valign="top">Для внутреннего использования POSIX-функцией <emphasis>iofunc_lock_default()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>list</emphasis></td>
       <td align="left" valign="top">Зарезервировано.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>list_size</emphasis></td>
       <td align="left" valign="top">Размер области, зарезервированной под поле list.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>nbytes</emphasis></td>
       <td align="left" valign="top">Размер ресурса в байтах. Например, если ресурс описывает конкретный файл, и этот файл имеет размер 7756 байт, то поле <emphasis>nbytes</emphasis> будет содержать значение 7756.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>inode</emphasis></td>
       <td align="left" valign="top">Содержит порядковый номер файла или ресурса; он должен быть уникален для каждой точки монтирования. Значение поля <emphasis>inode</emphasis> никогда не должно быть нулевым, потому что нуль указывает на неиспользуемый файл.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>uid</emphasis></td>
       <td align="left" valign="top">Идентификатор пользователя владельца данного ресурса.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>gid</emphasis></td>
       <td align="left" valign="top">Идентификатор группы владельца данного ресурса.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>mtime</emphasis></td>
       <td align="left" valign="top">Время последней модификации файла, обновленное или как минимум ставшее недействительным вследствие обработки клиентской функции <emphasis>write()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>atime</emphasis></td>
       <td align="left" valign="top">Время последнего доступа к файлу, обновленное или как минимум ставшее недействительным вследствие обработки клиентской функции <emphasis>read()</emphasis>, возвратившей ненулевое количество прочитанных байт.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>ctime</emphasis></td>
       <td align="left" valign="top">Время последнего изменения файла, обновленное или как минимум ставшее недействительным вследствие обработки клиентских функций <emphasis>write()</emphasis>, <emphasis>chown()</emphasis> или <emphasis>chmod()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>mode</emphasis></td>
       <td align="left" valign="top">Режим доступа к файлу. Содержит стандартные значения S_* из <code><strong>&lt;sys/stat.h&gt;</strong></code> (например, S_IFCHR), или восьмеричные значения (например, 0664), указывающие на режим доступа для владельца объекта (owner), группы (group) и всех остальных (other).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>nlink</emphasis></td>
       <td align="left" valign="top">Число связей (линков) файла, возвращаемое клиентским вызовом <emphasis>stat()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>rdev</emphasis></td>
       <td align="left" valign="top">Для специальных символьных устройств это поле состоит из старшего (major) и младшего (minor) кодов устройства (10 младших бит — младший код, старшие 6 бит — старший). Для устройств другого типа это поле содержит номер устройства (подробности см. ниже в параграфе «О номерах устройств, индексных дескрипторах и нашем друге <emphasis>rdev</emphasis>»).</td>
      </tr>
     </table>
     <p>Как и в случае с OCB, вы можете расширять «стандартную» атрибутную запись вашими собственными данными — см. раздел «Дополнительно».</p>
     <subtitle>Запись точки монтирования <code>iofunc_mount_t</code></subtitle>
     <p>Запись точки монтирования содержит информацию, общую для нескольких атрибутных записей.</p>
     <p>Вот содержимое записи точки монтирования (взято из <code><strong>&lt;sys/iofunc.h&gt;</strong></code>):</p>
     <p><code>typedef struct _iofunc_mount {</code></p>
     <p><code> uint32_t <emphasis>flags</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>conf</emphasis>;</code></p>
     <p><code> dev_t          <emphasis>dev</emphasis>;</code></p>
     <p><code> int32_t        <emphasis>blocksize</emphasis>;</code></p>
     <p><code> iofunc_funcs_t *<emphasis>funcs</emphasis>;</code></p>
     <p><code>} iofunc_mount_t;</code></p>
     <p>Поле <emphasis>flags</emphasis> содержит только один флаг — IOFUNC_MOUNT_32BIT. Этот флаг указывает на то, что параметр <emphasis>offset</emphasis> в OCB и параметры nbytes и inode в атрибутной записи являются 32-разрядными. Заметьте, что вы можете определять ваши собственные флаги в поле <emphasis>flags</emphasis>, используя биты, определенные константой IOFUNC_MOUNT_FLAGS_PRIVATE.</p>
     <p>Поле <emphasis>conf</emphasis> содержит следующие флаги:</p>
     <p>IOFUNC_PC_CHOWN_RESTRICTED</p>
     <p>Указывает, что файловая система является «chown-ограниченной», то есть никто, кроме суперпользователя (root), не может применять к файлам операцию <emphasis>chown()</emphasis>.</p>
     <p>IOFUNC_PC_NO_TRUNC</p>
     <p>Указывает на то, что файловая система не выполняет усечение имен.</p>
     <p>IOFUNC_PC_SYNC_IO</p>
     <p>Указывает на то, что файловая система поддерживает синхронные операции ввода-вывода.</p>
     <p>IOFUNC_PC_LINK_DIR</p>
     <p>Указывает на то, что допускается создание/уничтожение связей (linking/unilnking) для каталогов.</p>
     <p>Поле <emphasis>dev</emphasis> содержит номер устройства и описывается ниже в параграфе «О номерах устройств, индексных дескрипторах и нашем друге <emphasis>rdev</emphasis>».</p>
     <p>Поле <emphasis>blocksize</emphasis> описывает типовой для данного устройства размер блока в байтах. Например, для дисковых устройств типовым значением будет 512.</p>
     <p>И наконец, поле <emphasis>funcs</emphasis> указывает на следующую структуру (взято из <emphasis>&lt;sys/iofunc.h&gt;</emphasis>):</p>
     <p><code>typedef struct _iofunc_funcs {</code></p>
     <p><code> unsigned <emphasis>nfuncs</emphasis>;</code></p>
     <p><code> IOFUNC_OCB_T *(*<emphasis>ocb_calloc</emphasis>)(</code></p>
     <p><code>  resmgr_context_t *<emphasis>ctp</emphasis>, IOFUNC_ATTR_T *<emphasis>attr</emphasis>);</code></p>
     <p><code> void (*<emphasis>ocb_free</emphasis>)(IOFUNC_OCB_T *<emphasis>ocb</emphasis>);</code></p>
     <p><code>} iofunc_funcs_t;</code></p>
     <p>Как и в таблицах функций установления соединения и ввода/вывода, поле <emphasis>nfuncs</emphasis> должен содержать текущий размер таблицы. Используйте для этого константу _IOFUNC_NFUNCS.</p>
     <p>Указатели на функции <emphasis>ocb_calloc</emphasis> и <emphasis>ocb_free</emphasis> могут быть заполнены адресами функций, которые следует вызывать всякий раз при создании и уничтожении OCB. Зачем вам могут понадобиться эти функции — мы обсудим это чуть позже, когда будем говорить о расширении OCB.</p>
     <subtitle>О номерах устройств, индексных дескрипторах и нашем друге <emphasis>rdev</emphasis></subtitle>
     <p>Запись точки монтирования содержит поле с именем <emphasis>dev</emphasis>. Атрибутная запись содержит два поля: <emphasis>inode</emphasis> и <emphasis>rdev</emphasis>. Давайте рассмотрим их взаимосвязь на примере традиционной дисковой файловой системы. Файловая система монтируется на блок- ориентированном устройстве (которое представляет собой весь диск целиком). Это блок-ориентированное устройство может называться, скажем, <code><strong>/dev/hd0</strong></code> (первый жесткий диск в системе). На этом диске может быть несколько разделов, один из которых вполне может называться <code><strong>/dev/hd0t77</strong></code> (первый раздел файловой системы QNX на этом конкретном устройстве). И, наконец, на этом разделе может находиться произвольное число файлов, один из которых может иметь имя <code><strong>/hd/spud.txt</strong></code>.</p>
     <p>Поле <emphasis>dev</emphasis> («device number» — «номер устройства») содержит число, уникальное для узла, на котором зарегистрирован данный администратор ресурсов. Поле <emphasis>rdev</emphasis> — значение <emphasis>dev</emphasis> для корневого устройства (root device). И, наконец, поле <emphasis>inode</emphasis> — порядковый номер файла.</p>
     <p>Попробуем соотнести это с нашим примером дисковой системы. В приведенной ниже таблице приведен ряд чисел; взглянем на таблицу, а потом посмотрим, откуда появились эти номера и как они соотносятся.</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Устройство</th>
       <th align="left" valign="top">dev</th>
       <th align="left" valign="top">inode</th>
       <th align="left" valign="top">rdev</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">/dev/hd0</td>
       <td align="left" valign="top">6</td>
       <td align="left" valign="top">2</td>
       <td align="left" valign="top">1</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">/dev/hd0t77</td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">12</td>
       <td align="left" valign="top">77</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">/hd/spud.txt</td>
       <td align="left" valign="top">77</td>
       <td align="left" valign="top">47343</td>
       <td align="left" valign="top">-</td>
      </tr>
     </table>
     <p>Для «сырого» блок-ориентированного устройства <code><strong>/dev/hd0</strong></code> значения <emphasis>dev</emphasis> и <emphasis>inode</emphasis> назначил администратор процессов (значения 6 и 2 в таблице, см. выше). Администратор ресурсов при старте получил для устройства уникальное значение <emphasis>rdev</emphasis> (число 1).</p>
     <p>Для раздела <code><strong>/dev/hd0t77</strong></code> значение <emphasis>dev</emphasis> взялось из значения <emphasis>rdev</emphasis> «сырого» блок-ориентированного устройства (та самая 1). Значение <emphasis>inode</emphasis> было выбрано администратором ресурсов как уникальное в пределах <emphasis>rdev</emphasis>. Так появилась цифра 12. Наконец, значение <emphasis>rdev</emphasis> было также выбрано администратором ресурсов — здесь разработчик администратора выбрал значение 77, потому что оно соответствует типу раздела.</p>
     <p>И наконец, для файла <code><strong>/hd/spud.txt</strong></code>, значение <emphasis>dev</emphasis> (77) было взято из значения <emphasis>rdev</emphasis> для раздела. Значение <emphasis>inode</emphasis> было выбрано администратором ресурса (в случае файла номер выбирается так, чтобы соответствовать некоторому внутреннему представлению файла — конкретное число не имеет значения, лишь бы оно было ненулевым и уникальным в пределах <emphasis>rdev</emphasis>). Отсюда число 47343. Для файла поле <emphasis>rdev</emphasis> не имеет значения.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Функции-обработчики</p>
    </title>
    <section>
     <p>Не все вызовы обработчиков соответствуют клиентским сообщениям. Некоторые из них синтезируются ядром, а некоторые — библиотекой.</p>
     <p>Я организовал этот раздел следующим образом:</p>
     <p>• общие замечания;</p>
     <p>• замечания о функциях установления соединения;</p>
     <p>• алфавитный список сообщений установления соединения и ввода/вывода.</p>
    </section>
    <section>
     <title>
      <p>Общие замечания</p>
     </title>
     <p>Каждой функции-обработчику передается внутренний контекстный блок (параметр <emphasis>ctp</emphasis>), который следует рассматривать как «только для чтения», за исключением поля <emphasis>iov</emphasis>. Как это уже упоминалось в параграфе «Внутренний контекстный блок <code><strong>resmgr_context_t</strong></code>», этот контекстный блок содержит несколько интересных вещей. Также, каждой функции передается указатель на сообщение (аргумент <emphasis>msg</emphasis>). Вы будете активно использовать этот указатель, поскольку он содержит параметры, которыми его для вас заполнил клиентский библиотечный вызов. Функция, которую вы пишете, должна возвращать некоторое значение (все функции описаны как возвращающие <code><strong>int</strong></code>).</p>
     <p>Значения выбираются из следующего списка:</p>
     <p>_RESMGR_NOREPLY</p>
     <p>Указывает библиотеке администратора ресурсов, что она <emphasis>не должна</emphasis> выполнять <emphasis>MsgReplyv()</emphasis> — в предположении, что вы либо уже сделали это самостоятельно в вашей функции-обработчике, либо собираетесь сделать это несколько позже.</p>
     <p>_RESMGR_NPARTS(<emphasis>n</emphasis>)</p>
     <p>Указывает библиотеке администратора ресурсов при выполнении <emphasis>MsgReplyv()</emphasis> возвратить <emphasis>n</emphasis>-элементный вектор ввода/вывода (он располагается в <code><strong>ctp-&gt;iov</strong></code>). Ваша функция ответственна за заполнение поля iov структуры <emphasis>ctp</emphasis> и возврат _RESMGR_NPARTS с корректным числом элементов.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Память под поле <emphasis>iov</emphasis> структуры <emphasis>ctp</emphasis> выделяется динамически, и ее должно быть достаточно, чтобы вместить столько число элементов массива, сколько вы записываете в <emphasis>iov</emphasis>! Детали о настройке поля <emphasis>nparts_max</emphasis> см. выше в разделе «Управляющая структура <code><strong>resmgr_attr_t</strong></code>».</p>
     </cite>
     <p>_RESMGR_DEFAULT</p>
     <p>Это говорит библиотеке администратора ресурсов выполнить <emphasis>низкоуровневую функцию по умолчанию</emphasis> (это другое семейство функций; не путайте их с <emphasis>iofunc_*_default()</emphasis>!). Это возвращаемое значение вам вряд ли когда-нибудь пригодится. В общем случае оно заставляет библиотеку администратора ресурсов возвратить клиенту значение <emphasis>errno</emphasis>, равное ENOSYS, что означает «функция не поддерживается».</p>
     <p>_RESMGR_ERRNO(<emphasis>errno</emphasis>)</p>
     <p>(Устаревшее.) Данное возвращаемое значение использовалось для «инкапсуляции» значения <emphasis>errno</emphasis> в возвращаемое сообщением значение. Например, если бы клиент выдал запрос <emphasis>open()</emphasis> (по записи — <emphasis>прим. ред</emphasis>.) устройству, доступному только для чтения, корректно было бы возвратить код ошибки EROFS. Поскольку данный способ сделать это считается устаревшим, вы можете возвратить код ошибки непосредственно (например, при помощи <code><strong>return (EROFS);</strong></code> вместо громоздкого <code><strong>_RESMGR_ERRNO(EROFS);</strong></code>).</p>
     <p>_RESMGR_PTR(<emphasis>ctp</emphasis>, <emphasis>addr</emphasis>, <emphasis>len</emphasis>)</p>
     <p>Это макрос для удобства. Он берет указатель на контекст <emphasis>ctp</emphasis> и заполняет его первый элемент IOV адресом <emphasis>addr</emphasis> и длиной <emphasis>len</emphasis>, а затем возвращает библиотеке эквивалент <code><strong>_RESMGR_NPARTS(1)</strong></code>. Это может быть полезно для функций, возвращающих одноэлементные IOV.</p>
     <subtitle>Блокировки, разблокировки и обработка составных сообщений</subtitle>
     <p>Мы видели клиентский взгляд на составные сообщения, когда рассматривали функцию <emphasis>readblock()</emphasis> (в параграфе «Составные сообщения»). Клиент мог атомарно создать сообщение, которое содержало бы несколько «подсообщений» администратору ресурсов — в нашем примере это были сообщения, соответствующие функциям <emphasis>lseek()</emphasis> и <emphasis>read()</emphasis>. С точки зрения клиента две (или более) функций были как минимум атомарно <emphasis>переданы</emphasis> (и, вследствие самой сути обмена сообщениями, будут атомарно <emphasis>приняты</emphasis> администратором ресурсов). О чем мы еще не говорили, так это о том, как мы сможем гарантированно обеспечить атомарность <emphasis>обработки</emphasis> этих сообщений.</p>
     <p>Данные рассуждения применимы не только к составным сообщениям, но и ко <emphasis>всем</emphasis> сообщениям, принимаемым библиотекой администратора ресурсов. Первое, что делает библиотека администратора ресурсов, — она блокирует атрибутную запись, соответствующую ресурсу, используемому полученным сообщением. Затем она обрабатывает одно или более «подсообщений», содержащихся в полученном сообщении. Затем она снова разблокирует атрибутную запись.</p>
     <p>Это гарантирует, что поступающие сообщения обрабатываются атомарно, поскольку никакой другой поток администратора ресурсов (в случае многопоточного администратора, конечно) не может «влезть» и изменить ресурс, пока наш поток этот ресурс использует. Без блокировок два клиентских потока могли бы оба выдать то, что, по их мнению, являлось бы атомарным составным сообщением (скажем, пару «<emphasis>lseek()</emphasis> — <emphasis>read()</emphasis>»). Поскольку администратор ресурсов мог выделить на обработку этих сообщений два различных потока, эти потоки могли бы в произвольном порядке вытеснять друг друга, и их <emphasis>lseek()</emphasis> могли бы друг другу помешать. Блокировки же позволяют это предотвратить, потому что каждое сообщение, получающее доступ к ресурсу, обрабатывается целиком и атомарно.</p>
     <p>Блокировка и разблокировка ресурса выполняются вспомогательными функциями по умолчанию (<emphasis>iofunc_lock_ocb_default()</emphasis> и <emphasis>iofunc_unlock_ocb_default()</emphasis>), которые размещаются в таблице функций ввода/вывода в полях <emphasis>lock_ocb</emphasis> и <emphasis>unlock_ocb</emphasis> соответственно. Вы можете, конечно, переназначить эти функции, если хотите выполнить в процессе блокировки и разблокировки какие-либо дополнительные действия.</p>
     <p>Заметьте, что ресурс разблокируется <emphasis>перед</emphasis> вызовом <emphasis>io_close()</emphasis>. Это необходимо, поскольку функция <emphasis>io_close()</emphasis> освободит OCB, что автоматически сделает недействительным указатель на атрибутную запись, а блокировка хранится именно там!</p>
    </section>
    <section>
     <title>
      <p>Замечания о функциях установления соединения</p>
     </title>
     <p>Перед тем как углубиться в отдельные сообщения, однако, есть смысл подчеркнуть, что для всех функции установления соединения структура сообщений идентична (взято из <code><strong>&lt;sys/iomsg.h&gt;</strong></code>, с небольшими изменениями):</p>
     <p><code>struct _io_connect {</code></p>
     <p><code> // Для внутреннего использования</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>subtype</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>file_type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>reply_max</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>entry_max</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>key</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>handle</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>ioflag</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>mode</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>sflag</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>access</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>zero</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>eflag</emphasis>;</code></p>
     <p><code> // Для конечного пользователя</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>extra_type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>extra_len</emphasis>;</code></p>
     <p><code> char <emphasis>path</emphasis>[1];</code></p>
     <p><code>};</code></p>
     <p>Вы заметите, что я разделил структуру <code><strong>struct _io_connect</strong></code> на две части, часть «Для внутреннего использования» и часть «Для конечного пользователя».</p>
     <subtitle>Поля для внутреннего использования</subtitle>
     <p>Первая часть состоит из полей, которые библиотека администратора ресурсов использует для:</p>
     <p>• определения типа сообщения, полученного от клиента;</p>
     <p>• проверки сообщения на достоверность (не является ли оно дезинформацией);</p>
     <p>• отслеживания режима доступа (используется вспомогательными функциями).</p>
     <p>Для простоты я бы рекомендовал вам <emphasis>всегда</emphasis> применять вспомогательные функции (из семейства <emphasis>iofunc_*_default()</emphasis>) во <emphasis>всех</emphasis> функциях установления соединения. Эти функции возвратят вам признак успешного/неудачного завершения, после чего вы сможете использовать в функции установления соединения «поля для конечного пользователя».</p>
     <subtitle>Поля для конечного пользователя</subtitle>
     <p>Вторая половина полей непосредственно относится к вашей реализации функции установления соединения:</p>
     <p><emphasis>path_len</emphasis> и <emphasis>path</emphasis></p>
     <p>Имя пути (и его длина), которые являются <emphasis>операндом</emphasis> (то есть это имя пути, над которым производится действие).</p>
     <p><emphasis>extra_type</emphasis> и <emphasis>extra_len</emphasis></p>
     <p>Дополнительные параметры (имена путей, например), соответствующее данной функции установления соединения.</p>
     <p>Чтобы получить представление о том, как поле <emphasis>path</emphasis> используется в качестве «имени пути, над которым производится действие», давайте рассмотрим что-нибудь типа функции <emphasis>rename()</emphasis>. Эта функция принимает два имени пути: «изначальное» имя пути и «новое» имя пути. Изначальное имя пути передается в <emphasis>path</emphasis>, потому что именно над ним производится операция (это имя файла, подлежащего переименованию). Новое имя пути — аргумент данной операции. Вы увидите, что параметр <emphasis>extra</emphasis>, который передается функции установления соединения, как раз содержит указатель на аргумент операции, в данном случае — на новое имя пути.</p>
     <p>(В принятой реализации новое имя пути располагается в памяти непосредственно следом за изначальным (на которое указывает <emphasis>path</emphasis>) с учетом выравнивания, но вам делать на этот счет ничего не надо — параметр <emphasis>extra</emphasis> уже содержит правильный указатель.)</p>
    </section>
    <section>
     <title>
      <p>Алфавитный список функций установления соединения и ввода/вывода</p>
     </title>
     <p>В данном разделе в алфавитном порядке приведен список точек входа в функции установления соединения и ввода/вывода, которые вы можете заполнять самостоятельно (эти две таблицы затем передаются функции <emphasis>pathname_attach()</emphasis>). Помните, что если вы просто вызываете функцию <emphasis>iofunc_func_init()</emphasis>, все эти точки входа будут заполнены соответствующими значениями по умолчанию; вам следует переопределять конкретные точки входа только в том случае, если вы собираетесь обрабатывать данное конкретное сообщение самостоятельно. Ниже, в разделе «Примеры», вы увидите несколько примеров общеупотребительных функций.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Поначалу все это может показаться излишне запутанным, но отметим, что на самом деле существуют <emphasis>два</emphasis> разблокирующих вызова — один для функций установления соединения и один для функций ввода/вывода. Все вполне корректно, поскольку отражает то, когда происходит разблокирование. Вариант для функций установления соединения применяется тогда, когда ядро разблокирует клиента немедленно после отправки им сообщения установления соединения. Вариант для функций ввода/вывода применяется тогда, когда ядро разблокирует клиента немедленно после отправки им сообщения ввода/вывода.</p>
     </cite>
     <p>Чтобы не путать точки входа в функции-обработчики сообщений с вызовами клиентской Си-библиотеки (например, <emphasis>open()</emphasis>), к именам всех приведенных здесь функций добавлен префикс «<emphasis>io_</emphasis>». Например, обработчик функции установления соединения <emphasis>open()</emphasis> будет называться <emphasis>io_open()</emphasis>.</p>
     <subtitle><emphasis>io_chmod()</emphasis></subtitle>
     <p><code><strong>int io_chmod(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_chmod_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_chmod_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_chmod()</emphasis></p>
     <p>Клиентская функция: <emphasis>chmod()</emphasis>, <emphasis>fchmod()</emphasis></p>
     <p>Сообщения: _IO_CHMOD</p>
     <p>Структура данных:</p>
     <p><code>struct _io_chmod {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> mode_t   <emphasis>mode</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_chmod <emphasis>i</emphasis>;</code></p>
     <p><code>} io_chmod_t;</code></p>
     <p>Описание: Отвечает за изменение режима доступа к ресурсу, указанному в переданном ей параметре <emphasis>ocb</emphasis>, в значение, содержащееся в поле сообщения mode.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_chown()</emphasis></subtitle>
     <p><code><strong>int io_chown(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_chown_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_chown_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_chown()</emphasis></p>
     <p>Клиентская функция: <emphasis>chown()</emphasis>, <emphasis>fchown()</emphasis></p>
     <p>Сообщения: _IO_CHOWN</p>
     <p>Структура данных:</p>
     <p><code>struct _io_chown {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>gid</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>uid</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_chown <emphasis>i</emphasis>;</code></p>
     <p><code>} io_chown_t;</code></p>
     <p>Описание: Ответственна за изменение полей идентификатора пользователя и группы для ресурса, указанному в переданном ей параметре ocb, соответственно в значения <emphasis>uid</emphasis> и <emphasis>gid</emphasis>. Отметим, что чтобы узнать, позволяет ли данная файловая система выполнять <emphasis>chown()</emphasis> кому-либо, кроме суперпользователя (root), надо проверить запись точки монтирования на предмет флага IOFUNC_PC_CHOWN_RESTRICTED, а также поле <emphasis>flags</emphasis> в OCB.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_close_dup()</emphasis></subtitle>
     <p><code><strong>int io_close_dup(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_close_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_close_dup_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_close_dup()</emphasis></p>
     <p>Клиентская функция. <emphasis>close()</emphasis>, <emphasis>fclose()</emphasis></p>
     <p>Сообщения: _IO_CLOSE_DUP</p>
     <p>Структура данных:</p>
     <p><code>struct _io_close {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_close <emphasis>i</emphasis>;</code></p>
     <p><code>} io_close_t;</code></p>
     <p>Описание: Это <emphasis>реальный</emphasis> обработчик клиентских вызовов <emphasis>close()</emphasis> и <emphasis>fclose()</emphasis>. Отметим, что вам почти никогда не придется переназначать эту функцию; оставляйте в таблице функций ввода/вывода значение <emphasis>iofunc_close_dup_default()</emphasis>. Причиной этому служит то, что базовый уровень библиотеки отслеживает число сообщений <emphasis>open()</emphasis>, <emphasis>dup()</emphasis> и <emphasis>close()</emphasis>, выданных по каждому OCB, и синтезирует вызов <emphasis>io_close_ocb()</emphasis> (см. ниже) после получения для данного OCB <emphasis>последнего</emphasis> сообщения <emphasis>close()</emphasis>. Отметим, что идентификаторы отправителей, расположенные в <code><strong>ctp-&gt;rcvid</strong></code>, могут и не совпадать с переданными функции <emphasis>io_open()</emphasis>; однако, совпадение по меньшей мере одного идентификатора гарантируется. «Лишние» идентификаторы отправителей являются результатом (возможно, внутренних) вызовов типа <emphasis>dup()</emphasis>.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_close_ocb()</emphasis></subtitle>
     <p><code><strong>int io_close_ocb(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, void*</strong> <emphasis><strong>reserved</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода (синтезируется библиотекой)</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_close_default()</emphasis></p>
     <p>Вспомогательные функции: Нет</p>
     <p>Клиентская функция: Нет (синтезируется библиотекой)</p>
     <p>Сообщения: Нет (синтезируется библиотекой)</p>
     <p>Структура данных:</p>
     <p><code>// Синтезируется библиотекой</code></p>
     <p><code>struct _io_close {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_close <emphasis>i</emphasis>;</code></p>
     <p><code>} io_close_t;</code></p>
     <p>Описание: Это функция, которая синтезируется базовым уровнем библиотеки, когда для некоего OCB получено последнее сообщение <emphasis>close()</emphasis>. Это то самое место, где вам следует «подчистить» все перед уничтожением OCB. Отметим, что идентификатор отправителя в <code><strong>ctp-&gt;rcvid</strong></code> есть нуль (0), потому что данная функция синтезируется библиотекой и не обязательно соответствует какому-либо конкретному сообщению.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_devctl()</emphasis></subtitle>
     <p><code><strong>int io_devctl(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_devctl_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_devctl_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_devctl()</emphasis></p>
     <p>Клиентская функция: <emphasis>devctl()</emphasis>, <emphasis>ioctl()</emphasis></p>
     <p>Сообщения: _IO_DEVCTL</p>
     <p>Структура данных:</p>
     <p><code>struct _io_devctl {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>dcmd</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>nbytes</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>zero</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_devctl_reply {</code></p>
     <p><code> uint32_t <emphasis>zero</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>ret_val</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>nbytes</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>zero2</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_devctl       <emphasis>i</emphasis>;</code></p>
     <p><code> struct _io_devctl_reply <emphasis>o</emphasis>;</code></p>
     <p><code>} io_devctl_t;</code></p>
     <p>Описание: Выполняет над устройством операцию ввода/вывода, переданную от клиентской функции <emphasis>devctl()</emphasis> в параметре <emphasis>dcmd</emphasis>. Клиент кодирует направление передачи данных двумя старшими разрядами <emphasis>dcmd</emphasis>, указывая этим, как функция <emphasis>devctl()</emphasis> должна передавать данные (поле «to» соответствует биту _POSIX_DEVDIR_TO, поле «from» — биту _POSIX_DEVDIR_FROM):</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Поле «to»</th>
       <th align="left" valign="top">Поле «from»</th>
       <th align="left" valign="top">Значение</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">Передачи данных нет</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">Передача от драйвера клиенту</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">0</td>
       <td align="left" valign="top">Передача от клиента драйверу</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">1</td>
       <td align="left" valign="top">Двунаправленная передача</td>
      </tr>
     </table>
     <p>В случае, когда передачи данных нет, предполагается, что драйвер просто выполняет команду, заданную в <emphasis>dcmd</emphasis>. В случае передачи данных предполагается, что драйвер передает данные клиенту и/или обратно, используя вспомогательные функции <emphasis>resmgr_msgreadv()</emphasis> и <emphasis>resmgr_msgwritev()</emphasis>. Клиент указывает размер передачи в поле <emphasis>nbytes</emphasis>; драйвер должен установить число передаваемых байт в поле <emphasis>nbytes</emphasis> исходящей структуры.</p>
     <p>Отметим, что структуры данных, предназначенные для ввода и вывода, дополнены нулями, чтобы быть выровненными друг относительно друга. Это означает, что неявная область данных начинается в этих структурах с того же самого адреса.</p>
     <p>Если вы используете вспомогательную функцию <emphasis>iofunc_devctl()</emphasis>, то имейте в виду, что если она не сможет сделать что-либо с сообщением <emphasis>devctl()</emphasis>, она возвратит вам константу _RESMGR_DEFAULT. Эта сделано для отделения корректных значений <emphasis>errno</emphasis> от возвращаемого признака «нераспознанная команда». Получив _RESMGR_DEFAULT, базовый уровень библиотеки ответит установкой <emphasis>errno</emphasis> в значение ENOSYS, которое будет транслировано клиентской библиотечной функцией <emphasis>devctl()</emphasis> в значение ENOTTY, «корректное» с точки зрения POSIX.</p>
     <p>Проверка режима открытия и сопоставление его с выполняемой операцией лежит всецело на совести вашей функции — ни в клиентской <emphasis>devctl()</emphasis>, ни в библиотеке администратора ресурсов никаких проверок не выполняется. Например, можно открыть администратор ресурса в режиме «только для чтения», а затем выдать ему посредством <emphasis>devctl()</emphasis> команду «отформатировать жесткий диск» (которая, в общем, является весьма нехилой операцией записи). Так вот, с точки зрения администратора было бы весьма предусмотрительно до выполнения такой операции сначала проверить режим открытия ресурса.</p>
     <p>Отметим, что диапазон доступных пользователю значений <emphasis>dcmd</emphasis> ограничен (значения от 0x0000 до 0x0FFF включительно зарезервированы QSSL). Другие значения можно смело использовать — см. заголовочные файлы с именами <code><strong>&lt;sys/dcmd_*.h&gt;</strong></code>.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>, и буфер приема (с ответными данными, если надо).</p>
     <p>Для примера см. ниже параграф «Простой пример функции <emphasis>io_devctl()</emphasis>».</p>
     <subtitle><emphasis>io_dup()</emphasis></subtitle>
     <p><code><strong>int io_dup(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_dup_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: NULL (обрабатывается базовым уровнем)</p>
     <p>Вспомогательные функции: Нет</p>
     <p>Клиентская функция: <emphasis>dup()</emphasis>, <emphasis>dup2()</emphasis>, <emphasis>fcntl()</emphasis>, <emphasis>fork()</emphasis>, <emphasis>spawn*()</emphasis>, <emphasis>fork()</emphasis></p>
     <p>Сообщения: _IO_DUP</p>
     <p>Структура данных:</p>
     <p><code>struct _io_dup {</code></p>
     <p><code> uint16_t         <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t         <emphasis>combine_len</emphasis>;</code></p>
     <p><code> struct _msg_info <emphasis>info</emphasis>;</code></p>
     <p><code> uint32_t         <emphasis>reserved</emphasis>;</code></p>
     <p><code> uint32_t         <emphasis>key</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_dup <emphasis>i</emphasis>;</code></p>
     <p><code>} io_dup_t;</code></p>
     <p>Описание: Это обработчик сообщений <emphasis>dup()</emphasis>. Как и в случае с <emphasis>io_close_dup()</emphasis>, вы вряд ли будете обрабатывать это сообщение самостоятельно. За вас это сделает базовый уровень библиотеки.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_fdinfo()</emphasis></subtitle>
     <p><code><strong>int io_fdinfo(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_fdinfo_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода при помощи <emphasis>iofunc_func_init()</emphasis> и не изменять данную точку входа.</p>
     <subtitle><emphasis>io_link()</emphasis></subtitle>
     <p><code><strong>int io_link(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_link_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_HANDLE_T*</strong> <emphasis><strong>handle</strong></emphasis><strong>, io_link_extra_t*</strong> <emphasis><strong>extra</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция установления соединения</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: <emphasis>iofunc_link()</emphasis></p>
     <p>Клиентская функция: <emphasis>link()</emphasis></p>
     <p>Сообщения: _IO_CONNECT, подтип IO_CONNECT_LINK</p>
     <p>Структура данных:</p>
     <p><code>struct _io_connect {</code></p>
     <p><code> // Внутренние поля (как описано выше)</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>extra_type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>extra_len</emphasis>;</code></p>
     <p><code> char     <emphasis>path</emphasis>[1];</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_connect_link_reply {</code></p>
     <p><code> uint32_t <emphasis>reserved1</emphasis>[2];</code></p>
     <p><code> uint8_t  <emphasis>eflag</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>reserved2</emphasis>[3];</code></p>
     <p><code> uint32_t <emphasis>umask</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>nentries</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_connect            <emphasis>connect</emphasis>;</code></p>
     <p><code> struct _io_connect_link_reply <emphasis>link_reply</emphasis>;</code></p>
     <p><code>} io_link_t;</code></p>
     <empty-line/>
     <p><code>typedef union _io_link_extra {</code></p>
     <p><code> struct _msg_info             <emphasis>info</emphasis>;</code></p>
     <p><code> void                         *<emphasis>ocb</emphasis>;</code></p>
     <p><code> char                         <emphasis>path</emphasis>[1];</code></p>
     <p><code> struct _io_resmgr_link_extra <emphasis>resmgr</emphasis>;</code></p>
     <p><code>} io_link_extra_t;</code></p>
     <p>Описание: Создает новую связь (линк) с именем, заданным в поле <emphasis>path</emphasis> структуры <emphasis>msg</emphasis>, к уже существующему имени пути, указанному в поле <emphasis>path</emphasis> параметра <emphasis>extra</emphasis> (переданного вашей функции). Для удобства поле <emphasis>ocb</emphasis> параметра <emphasis>extra</emphasis> содержит указатель на OCB существующего имени пути.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_lock()</emphasis></subtitle>
     <p><code><strong>int io_lock(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_lock_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода, используя <emphasis>iofunc_func_init()</emphasis>, и не изменять данную точку входа.</p>
     <subtitle><emphasis>io_lock_ocb()</emphasis></subtitle>
     <p><code><strong>int io_lock_ocb(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, void *</strong><emphasis><strong>reserved</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода (синтезируется библиотекой)</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_lock_ocb_default()</emphasis></p>
     <p>Вспомогательные функции: Нет</p>
     <p>Клиентская функция: Все</p>
     <p>Сообщения: Нет (синтезируются библиотекой)</p>
     <p>Структура данных: Нет</p>
     <p>Описание: Эта функция отвечает за блокировку атрибутной записи, на которую указывает OCB. Это сделано для того, чтобы гарантировать одновременный доступ не более одного потока как к самому OCB, так и к соответствующей атрибутной записи. Функции блокировки (и соответствующие функции разблокировки) синтезируются библиотекой администратора ресурсов до начала обработки сообщения и после ее завершения соответственно. Более подробно это описано выше в параграфе «Составные сообщения». Вы почти никогда не будете использовать этот вызов самостоятельно; вместо этого используйте функцию POSIX-уровня по умолчанию.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_lseek()</emphasis></subtitle>
     <p><code><strong>int io_lseek(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_lseek_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_lseek_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_lseek()</emphasis></p>
     <p>Клиентская функции: <emphasis>lseek()</emphasis>, <emphasis>fseek()</emphasis>, <emphasis>rewinddir()</emphasis></p>
     <p>Сообщения: _IO_LSEEK</p>
     <p>Структура данных:</p>
     <p><code>struct _io_lseek {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> short    <emphasis>whence</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>zero</emphasis>;</code></p>
     <p><code> uint64_t <emphasis>offset</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_lseek <emphasis>i</emphasis>;</code></p>
     <p><code> uint64_t         <emphasis>o</emphasis>;</code></p>
     <p><code>} io_lseek_t;</code></p>
     <p>Описание: Обрабатывает клиентскую функцию <emphasis>lseek()</emphasis>. Отметьте, что администратору ресурса, который обрабатывает каталоги, придется также интерпретировать сообщение _IO_LSEEK для операций с каталогами. Параметры <emphasis>whence</emphasis> и <emphasis>offset</emphasis> передаются от клиентской функции <emphasis>lseek()</emphasis>. После интерпретации параметров <emphasis>whence</emphasis> и <emphasis>offset</emphasis> клиентского сообщения подпрограмма должна скорректировать у OCB параметр <emphasis>offset</emphasis> и затем возвратить новое значение <emphasis>offset</emphasis> или возвратить признак ошибки.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>, а также (не обязательно) текущее смещение.</p>
     <subtitle><emphasis>io_mknod()</emphasis></subtitle>
     <p><code><strong>int io_mknod(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_mknod_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_HANDLE_T *</strong><emphasis><strong>handle</strong></emphasis><strong>, void *</strong><emphasis><strong>reserved</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция установления соединения</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: <emphasis>iofunc_mknod()</emphasis></p>
     <p>Клиентская функция: <emphasis>mknod()</emphasis>, <emphasis>mkdir()</emphasis>, <emphasis>mkfifo()</emphasis></p>
     <p>Сообщения: _IO_CONNECT, подтип _IO_CONNECT_MKNOD</p>
     <p>Структура данных:</p>
     <p><code>struct _io_connect {</code></p>
     <p><code> // Внутренние поля (как описано выше)</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>extra_type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>extra_len</emphasis>;</code></p>
     <p><code> char     <emphasis>path</emphasis>[1];</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_connect_link_reply {</code></p>
     <p><code> uint32_t <emphasis>reserved1</emphasis>[2];</code></p>
     <p><code> uint8_t  <emphasis>eflag</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>reserved2</emphasis>[3];</code></p>
     <p><code> uint32_t <emphasis>umask</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>nentries</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_connect            <emphasis>connect</emphasis>;</code></p>
     <p><code> struct _io_connect_link_reply <emphasis>link_reply</emphasis>;</code></p>
     <p><code>} io_mknod_t;</code></p>
     <p>Описание: Создает новую точку входа в файловую систему. Сообщение выдается для создания файла с именем, указанным в <emphasis>path</emphasis>, и типом, закодированным в поле <emphasis>mode</emphasis> (оно из «внутренних полей» структуры <code><strong>struct _io_connect</strong></code> и здесь не показано).</p>
     <p>Реально это используется только для клиентских функций <emphasis>mkfifo()</emphasis> и <emphasis>mkdir()</emphasis>.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_mmap()</emphasis></subtitle>
     <p><code><strong>int io_mmap(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_mmap_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_mmap_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_mmap()</emphasis></p>
     <p>Клиентская функция: <emphasis>mmap()</emphasis>, <emphasis>munmap()</emphasis>, <emphasis>mmap_device_io()</emphasis>, <emphasis>mmap_device_memory()</emphasis></p>
     <p>Сообщения: _IO_MMAP</p>
     <p>Структура данных:</p>
     <p><code>struct _io_mmap {</code></p>
     <p><code> uint16_t         <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t         <emphasis>combine_len</emphasis>;</code></p>
     <p><code> uint32_t         <emphasis>prot</emphasis>;</code></p>
     <p><code> uint64_t         <emphasis>offset</emphasis>;</code></p>
     <p><code> struct _msg_info <emphasis>info</emphasis>;</code></p>
     <p><code> uint32_t         <emphasis>zero</emphasis>[6];</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_mmap_reply {</code></p>
     <p><code> uint32_t<emphasis> zero</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>flags</emphasis>;</code></p>
     <p><code> uint64_t <emphasis>offset</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>coid</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>fd</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code>struct _io_mmap       <emphasis>i</emphasis>;</code></p>
     <p><code>struct _io_mmap_reply <emphasis>o</emphasis>;</code></p>
     <p><code>} io_mmap_t;</code></p>
     <p>Описание: Позволяет администратору процессов применять к файлам вашего администратора ресурсов операцию <emphasis>mmap()</emphasis>. В общем случае самостоятельно программировать эту функцию не следует (используйте обработчик по умолчанию, предоставляемый <emphasis>iofunc_func_init()</emphasis>), если только вы не хотите ее преднамеренно отключить (например, драйвер последовательного порта мог бы запросто возвратить ENOSYS, поэтому для него эта операция не имеет никакого смысла).</p>
     <p>Данную функцию администратора ресурсов может вызвать только администратор процессов</p>
     <p>Отметим, что побочным результатом вызова этой функции администратором процессов является создание OCB (то есть будет вызвана функция <emphasis>iofunc_ocb_calloc()</emphasis>, но для правильно спроектированного администратора ресурсов это не должно иметь никаких последствий.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_mount()</emphasis></subtitle>
     <p><code><strong>int io_mount(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_mount_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_HANDLE_T*</strong> <emphasis><strong>handle</strong></emphasis><strong>, io_mount_extra_t*</strong> <emphasis><strong>extra</strong></emphasis><strong>)</strong></code></p>
     <p>Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода, используя <emphasis>iofunc_func_init()</emphasis>, и не изменять данную точку входа.</p>
     <subtitle><emphasis>io_msg()</emphasis></subtitle>
     <p><code><strong>int io_msg(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_msg_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: Нет</p>
     <p>Клиентская функция: Нет (создается «вручную» и передается посредством <emphasis>MsgSend()</emphasis>)</p>
     <p>Сообщения: _IO_MSG</p>
     <p>Структура данных:</p>
     <p><code>struct _io_msg {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>mgrid</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>subtype</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_msg <emphasis>i</emphasis>;</code></p>
     <p><code>} io_msg_t;</code></p>
     <p>Описание: Интерфейс _IO_MSG является более общей, но менее переносимой вариацией на тему <emphasis>ioctl()</emphasis> и <emphasis>devctl()</emphasis>. Поле <emphasis>mgrid</emphasis> идентифицирует конкретный администратор — вы не должны выполнять никаких действий по запросам, не соответствующим идентификатору вашего администратора. Поле <emphasis>subtype</emphasis> фактически задает команду, которую клиент хочет выполнить. Любые неявно передаваемые данные следуют за входной структурой. Данные, возвращаемые клиенту, передаются сами по себе; код завершения возвращается через макрос <emphasis>_RESMGR_STATUS</emphasis>. Уникальный «идентификатор администратора» (manager ID) вы можете получить в QSSL.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_notify()</emphasis></subtitle>
     <p><code><strong>int io_notify(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_notify_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: <emphasis>iofunc_notify()</emphasis>, <emphasis>iofunc_notify_remove()</emphasis>, <emphasis>iofunc_notify_trigger()</emphasis></p>
     <p>Клиентская функция: <emphasis>select()</emphasis>, <emphasis>ionotify()</emphasis></p>
     <p>Сообщения: _IO_NOTIFY</p>
     <p>Структура данных:</p>
     <p><code>struct _io_notify {</code></p>
     <p><code> uint16_t        <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t<emphasis>        combine_len</emphasis>;</code></p>
     <p><code> int32_t         <emphasis>action</emphasis>;</code></p>
     <p><code> int32_t         <emphasis>flags</emphasis>;</code></p>
     <p><code> struct sigevent <emphasis>event</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_notify_reply {</code></p>
     <p><code> uint32_t <emphasis>zero</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>flags</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_notify       <emphasis>i</emphasis>;</code></p>
     <p><code> struct _io_notify_reply <emphasis>o</emphasis>;</code></p>
     <p><code>} io_notify_t;</code></p>
     <p>Описание: Данный обработчик отвечает за установку, опрос или удаление обработчика уведомлений. Параметры <emphasis>action</emphasis> (действие) и <emphasis>flags</emphasis> (флаги) определяют тип операции уведомления и условия; параметр event (событие) является структурой типа <code><strong>struct sigevent</strong></code>, которая определяет событие уведомления (если оно есть), которое клиент хочет получить. Событие <emphasis>event</emphasis> клиенту доставляется функцией <emphasis>MsgDeliverEvent()</emphasis> или функцией <emphasis>iofunc_notify_trigger()</emphasis>.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>; флаги возвращается ответным сообщением.</p>
     <subtitle><emphasis>io_open()</emphasis></subtitle>
     <p><code><strong>int io_open(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_open_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_HANDLE_T *</strong><emphasis><strong>handle</strong></emphasis><strong>, void *</strong><emphasis><strong>extra</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция установления соединения</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_open_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_open()</emphasis>, <emphasis>iofunc_ocb_attach()</emphasis></p>
     <p>Клиентская функция: <emphasis>open()</emphasis>, <emphasis>fopen()</emphasis>, <emphasis>sopen()</emphasis> и др.</p>
     <p>Сообщения: _IO_CONNECT, подтипы _IO_CONNECT_COMBINE, _IO_CONNECT_COMBINE_CLOSE и _IO_CONNECT_OPEN.</p>
     <p>Структура данных:</p>
     <p><code>struct _io_connect {</code></p>
     <p><code> // Внутренние поля (как описано выше)</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>extra_type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>extra_len</emphasis>;</code></p>
     <p><code> char     <emphasis>path</emphasis>[1];</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_connect_link_reply {</code></p>
     <p><code> uint32_t <emphasis>reserved1</emphasis>[2];</code></p>
     <p><code> uint8_t  <emphasis>eflag</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>reserved2</emphasis>[3];</code></p>
     <p><code> uint32_t <emphasis>umask</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>nentries</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_connect            <emphasis>connect</emphasis>;</code></p>
     <p><code> struct _io_connect_link_reply <emphasis>link_reply</emphasis>;</code></p>
     <p><code>} io_open_t;</code></p>
     <p>Описание: Это основная точка входа в администратор ресурсов. Она выполняет проверку, действительно ли клиент имеет соответствующие права на открытие файла, привязывает OCB к внутренним структурам библиотеки (посредством функций <emphasis>resmgr_bind_ocb()</emphasis> или <emphasis>iofunc_ocb_attach()</emphasis>) и возвращает <emphasis>errno</emphasis>. Отметим, что для данной функции релевантны не все поля структур ввода и вывода.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_IO_SET_CONNECT_RET</emphasis>.</p>
     <subtitle><emphasis>io_openfd()</emphasis></subtitle>
     <p><code><strong>int io_openfd(resmgx_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_openfd_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_openfd_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_openfd()</emphasis></p>
     <p>Клиентская функция: <emphasis>openfd()</emphasis></p>
     <p>Сообщения: _IO_OPENFD</p>
     <p>Структура данных:</p>
     <p><code>struct _io_openfd {</code></p>
     <p><code> uint16_t         <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t         <emphasis>combine_len</emphasis>;</code></p>
     <p><code> uint32_t         <emphasis>ioflag</emphasis>;</code></p>
     <p><code> uint16_t         <emphasis>sflag</emphasis>;</code></p>
     <p><code> uint16_t         <emphasis>reserved1</emphasis>;</code></p>
     <p><code> struct _msg_info <emphasis>info</emphasis>;</code></p>
     <p><code> uint32_t         <emphasis>reserved2</emphasis>;</code></p>
     <p><code> uint32_t         <emphasis>key</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_openfd <emphasis>i</emphasis>;</code></p>
     <p><code>} io_openfd_t;</code></p>
     <p>Описание: Данная функция аналогична предоставляемому обработчику <emphasis>io_open()</emphasis> — за исключением того, что вместо имени пути передается дескриптор уже открытого файла (в силу передачи вам параметра <emphasis>ocb</emphasis> в вызове функции).</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_pathconf()</emphasis></subtitle>
     <p><code><strong>int io_pathconf(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_pathconf_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_pathconf_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_pathconf()</emphasis></p>
     <p>Клиентская функция: <emphasis>fpathconf()</emphasis>, <emphasis>pathconf()</emphasis></p>
     <p>Сообщения: IO_PATHCONF</p>
     <p>Структура данных:</p>
     <p><code>struct _io_pathconf {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> short    <emphasis>name</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>zero</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_pathconf <emphasis>i</emphasis>;</code></p>
     <p><code>] io_pathconf_t;</code></p>
     <p>Описание: Обработчик этого сообщения отвечает за возврат значения настраиваемого параметра <emphasis>name</emphasis> для ресурса, связанного с данным OCB. Используйте функцию по умолчанию и расширьте ее дополнительными вариантами элемента <emphasis>name</emphasis>, соответствующими вашему устройству.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_IO_SET_PATHCONF_VALUE</emphasis>; флаги возвращаются в ответном сообщении.</p>
     <subtitle><emphasis>io_read()</emphasis></subtitle>
     <p><code><strong>int io_read(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_read_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_read_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_read()</emphasis>, <emphasis>iofunc_read_verify()</emphasis></p>
     <p>Клиентская функция: <emphasis>read()</emphasis>, <emphasis>readdir()</emphasis></p>
     <p>Сообщение: IO_READ</p>
     <p>Структура данных:</p>
     <p><code>struct _io_read {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>nbytes</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>xtype</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_read <emphasis>i</emphasis>;</code></p>
     <p><code>} io_read_t;</code></p>
     <p>Описание: Отвечает за чтение данных из ресурса. Клиент задает число байт, которое он готов прочитать, в элементе <emphasis>nbytes</emphasis>. Вы возвращаете данные, увеличиваете смещение в OCB и обновляете соответствующие поля с информацией о временах доступа.</p>
     <p>Отметим, что элемент <emphasis>xtype</emphasis> может устанавливать для отдельных сообщений флаг переопределения, поэтому его надо проверять. Если вы не поддерживаете никаких расширенных флагов переопределения, вы должны возвратить EINVAL. Далее, в примерах функций <emphasis>io_read()</emphasis> и <emphasis>io_write()</emphasis>, мы рассмотрим обработку одного очень важного (и очень непростого!) флага переопределения, называемого _IO_XTYPE_OFFSET.</p>
     <p>Отметим также, что сообщение _IO_READ приходит не только для обычных файлов, но также и для чтения содержимого каталогов. В варианте с каталогом вы должны гарантированно обеспечить возврат целого (integral) числа элементов <code><strong>struct dirent</strong></code>. За дополнительной информацией по возврату элементов каталога см. пример в параграфе «Возврат элементов каталога» раздела «Дополнительно».</p>
     <p>Чтобы удостовериться, что файл был открыт в режиме, совместимом с операцией чтения, надо вызвать вспомогательную функцию <emphasis>iofunc_read_verify()</emphasis>. Также, следует вызвать функцию <emphasis>iofunc_sync_verify()</emphasis>, чтобы проверить, надо ли синхронизировать данные с носителем.</p>
     <p>Возвращает: Число считанных байтов или код завершения, при помощи вспомогательного макроса <emphasis>_IO_SET_READ_NBYTES</emphasis>, а также собственно данные — ответным сообщением.</p>
     <p>В качестве примера с возвратом только данных см. ниже раздел «Простой пример функции <emphasis>io_read()</emphasis>». Более сложный пример с одновременным возвратом как данных, так и элементов каталогов, см. в параграфе «Возврат элементов каталога» раздела «Дополнительно».</p>
     <subtitle><emphasis>io_readlink()</emphasis></subtitle>
     <p><code><strong>int io_readlink(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_readlink_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_HANDLE_T *</strong><emphasis><strong>handle</strong></emphasis><strong>, void*</strong> <emphasis><strong>reserved</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция установления соединения</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: <emphasis>iofunc_readlink()</emphasis></p>
     <p>Клиентская функция: <emphasis>readlink()</emphasis></p>
     <p>Сообщения: IO_CONNECT, подтип IO_CONNECT_READLINK</p>
     <p>Структура данных:</p>
     <p><code>struct _io_connect {</code></p>
     <p><code> // Внутренние поля (как описано выше)</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>extra_type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>extra_len</emphasis>;</code></p>
     <p><code> char     <emphasis>path</emphasis>[1];</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_connect_link_reply {</code></p>
     <p><code> uint32_t <emphasis>reserved1</emphasis>[2];</code></p>
     <p><code> uint8_t  <emphasis>eflag</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>reserved2</emphasis>[3];</code></p>
     <p><code> uint32_t <emphasis>umask</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>nentries</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_connect            <emphasis>connect</emphasis>;</code></p>
     <p><code> struct _io_connect_link_reply <emphasis>link_reply</emphasis>;</code></p>
     <p><code>} io_open_t;</code></p>
     <p>Описание: Отвечает за чтение содержимого символьной связи (линка), как определено полем <emphasis>path</emphasis> входной структуры. Возвращаемые байты представляют собой содержимое символьной связи; возвращаемый код состояния представляет собой число байт в ответе. Допустимый возврат должен быть сделано только для символьной связи. Все другие доступы должны возвратить код ошибки.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>, и данные в ответном сообщении.</p>
     <subtitle><emphasis>io_rename()</emphasis></subtitle>
     <p><code><strong>int io_rename(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_rename_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_HANDLE_T *</strong><emphasis><strong>handle</strong></emphasis><strong>, io_rename_extra_t*</strong> <emphasis><strong>extra</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция установления соединения</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: <emphasis>iofunc_rename()</emphasis></p>
     <p>Клиентская функция: <emphasis>rename()</emphasis></p>
     <p>Сообщение: _IO_CONNECT, подтип _IO_CONNECT_RENAME</p>
     <p>Структура данных:</p>
     <p><code>struct _io_connect {</code></p>
     <p><code> // internal fields (as described above)</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>extra_type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>extra_len</emphasis>;</code></p>
     <p><code> char     <emphasis>path</emphasis>[1];</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_connect_link_reply {</code></p>
     <p><code> uint32_t <emphasis>reserved1</emphasis>[2];</code></p>
     <p><code> uint8_t  <emphasis>eflag</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>reserved2</emphasis>[3];</code></p>
     <p><code> uint32_t <emphasis>umask</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>nentries</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union _io_rename_extra {</code></p>
     <p><code> char <emphasis>path</emphasis>[1];</code></p>
     <p><code>} io_rename_extra_t;</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_connect            <emphasis>connect</emphasis>;</code></p>
     <p><code> struct _io_connect_link_reply <emphasis>link_reply</emphasis>;</code></p>
     <p><code>} io_rename_t;</code></p>
     <p>Описание: Выполняет операцию переименования, получив на вход первоначальное имя в элементе <emphasis>path</emphasis> и новое имя в поле <emphasis>path</emphasis> переданного параметра <emphasis>extra</emphasis>. Замечание по реализации: для первоначального имени задается имя пути (а не OCB) — это делается специально для случая переименования файла, который является жесткой связью к другому файлу. Если бы был задан OCB, две (или более) жестких связей к одному и тому же файлу различить было бы нельзя.</p>
     <p>Данная функция будет вызываться только для тех двух имен файлов, которые принадлежат одной и той же файловой системе (то есть одному и тому же устройству). Поэтому в проверке случаев, в которых надо было бы возвращать EXDEV, нет никакой необходимости. Это ничуть не мешает вам возвращать EXDEV — например, если вы не хотите выполнять <emphasis>rename()</emphasis> самостоятельно (например, операция переименования из одного каталога в другой может оказаться очень сложной). В случае возврата EXDEV командно-строковая утилита <code><strong>mv</strong></code> выполнит сначала <code><strong>cp</strong></code>, а потом <code><strong>rm</strong></code> (библиотечная функция <emphasis>rename()</emphasis> этого не сделает — она просто установит <emphasis>errno</emphasis> в EXDEV).</p>
     <p>Также перед вызовом этой функции должны быть разрешены все символьные связи (где это применимо), а переданные имена путей должны быть абсолютны и относиться к файловой системе, за которую отвечает данный администратор ресурсов.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_shutdown()</emphasis></subtitle>
     <p><code><strong>int io_shutdown(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_shutdown_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода, используя <emphasis>iofunc_func_init()</emphasis>, и не изменять данную точку входа.</p>
     <subtitle><emphasis>io_space()</emphasis></subtitle>
     <p><code><strong>int io_space(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_space_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: <emphasis>iofunc_space_verify()</emphasis></p>
     <p>Клиентская функция: <emphasis>chsize()</emphasis>, <emphasis>fcntl()</emphasis>, <emphasis>ftruncate()</emphasis>, <emphasis>ltrunc()</emphasis></p>
     <p>Сообщение _IO_SPACE</p>
     <p>Структура данных:</p>
     <p><code>struct _io_space {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>subtype</emphasis>;</code></p>
     <p><code> short    <emphasis>whence</emphasis>;</code></p>
     <p><code> uint64_t <emphasis>start</emphasis>;</code></p>
     <p><code> uint64_t <emphasis>len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_space <emphasis>i</emphasis>;</code></p>
     <p><code> uint64_t         <emphasis>o</emphasis>;</code></p>
     <p><code>} io_space_t;</code></p>
     <p>Описание: Эта функция применяется для выделения или освобождения занимаемого ресурсом пространства. Параметр <emphasis>subtype</emphasis> («подтип») указывает на то, следует ли это пространство выделить (если равен F_ALLOCSP) или освободить (если равен F_FREESP). Комбинация параметров <emphasis>whence</emphasis> («откуда») и <emphasis>start</emphasis> («начало») указывает, где следует начать выделение/ освобождение; элемент <emphasis>len</emphasis> указывает размер операции.</p>
     <p>Возвращает: Число байтов (размер ресурса), посредством вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_stat()</emphasis></subtitle>
     <p><code><strong>int io_stat(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_stat_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_stat_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_stat()</emphasis></p>
     <p>Клиентская функция: <emphasis>stat()</emphasis>, <emphasis>lstat()</emphasis>, <emphasis>fstat()</emphasis></p>
     <p>Сообщения: _IO_STAT</p>
     <p>Структура данных:</p>
     <p><code>struct _io_stat {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>zero</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union (</code></p>
     <p><code> struct _io_stat <emphasis>i</emphasis>;</code></p>
     <p><code> struct stat     <emphasis>o</emphasis>;</code></p>
     <p><code>} io_stat_t;</code></p>
     <p>Описание: Обрабатывает сообщение, запрашивающее информацию о ресурсе, связанном с переданным OCB. Заметьте, что атрибутная запись содержит всю информацию, необходимую для выполнения запроса <emphasis>stat()</emphasis>. Вспомогательная функция <emphasis>iofunc_stat()</emphasis> заполняет структуру <code><strong>struct stat</strong></code>, базированную на атрибутной записи. Эта вспомогательная функция также изменяет сохраненные элементы <emphasis>dev</emphasis>/<emphasis>rdev</emphasis> так, чтобы они были уникальны с точки зрения единичного узла (это используется для выполнения вызовов <emphasis>stat()</emphasis> в отношении файлов по сети). Писать этот обработчик самостоятельно особого смысла нет.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>, и структуру <code><strong>struct stat</strong></code> — в ответном сообщении.</p>
     <subtitle><emphasis>io_sync()</emphasis></subtitle>
     <p><code><strong>int io_sync(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_sync_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_sync_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_sync_verify()</emphasis>, <emphasis>iofunc_sync()</emphasis></p>
     <p>Клиентская функция: <emphasis>fsync()</emphasis>, <emphasis>fdatasync()</emphasis></p>
     <p>Сообщения: _IO_SYNC</p>
     <p>Структура данных:</p>
     <p><code>struct _io_sync {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>сombine_len</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>flag</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_sync <emphasis>i</emphasis>;</code></p>
     <p><code>} io_sync_t;</code></p>
     <p>Описание: Это точка входа команды flush (синхронизация носителя информации с буферами — например, диска с дисковым кэшем — <emphasis>прим. ред</emphasis>.). Вспомогательная функция <emphasis>iofunc_sync()</emphasis> принимает поле <emphasis>flag</emphasis> входного сообщения и возвращает одно из следующих значений, которые указывают, какие действия ваш администратор ресурсов должен выполнить:</p>
     <p>• 0 — не делать ничего;</p>
     <p>• O_SYNC — все, что связано с файлом (включая содержимое файла, элементы каталогов, индексные дескрипторы (inodes), и т.д.) должно присутствовать на носителе и должно быть восстанавливаемым с него.</p>
     <p>• O_DSYNC — присутствовать на носителе и быть восстанавливаемыми с него должны только данные файла.</p>
     <p>Отметим, что эта функция будет вызываться только в том случае, если вы согласились поддерживать функцию <code><strong>sync</strong></code>, установив соответствующий флаг в записи точки монтирования.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_umount()</emphasis></subtitle>
     <p><code><strong>int io_umount(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, void *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода, используя <emphasis>iofunc_func_init()</emphasis>, и не изменять данную точку входа.</p>
     <subtitle><emphasis>io_unblock()</emphasis> [установление соединения]</subtitle>
     <p><code><strong>int io_unblock(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_pulse_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_HANDLE_T *</strong><emphasis><strong>handle</strong></emphasis><strong>, void*</strong> <emphasis><strong>reserved</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция установления соединения (синтезируется ядром и библиотекой)</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: <emphasis>iofunc_unblock()</emphasis></p>
     <p>Клиентская функция: Нет (вызывается ядром вследствие сигнала или тайм-аута)</p>
     <p>Сообщения: Нет (синтезируется библиотекой)</p>
     <p>Структура данных: (см. вариант <emphasis>io_unblock()</emphasis> для ввода/вывода, ниже)</p>
     <p>Описание: Это версия разблокирующего вызова в форме сообщения установления соединения, синтезируемая библиотекой в ответ на импульс от ядра, возникший в результате желания клиента разблокироваться на этапе установления соединения. См. вариант этой функции для ввода/вывода ниже.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <p>См. подробное обсуждение стратегий разблокирования в главе «Обмен сообщениями», параграф «Применение флага _NTO_MI_UNBLOCK_REQ».</p>
     <subtitle><emphasis>io_unblock()</emphasis> [ввод/вывод]</subtitle>
     <p><code><strong>int io_unblock(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_pulse_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода (синтезируется ядром и библиотекой)</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_unblock_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_unblock()</emphasis></p>
     <p>Клиентская функция: Нет (реакция ядра на сигнал или тайм-аут)</p>
     <p>Сообщения: Нет (синтезируется библиотекой)</p>
     <p>Структура данных: указательна структуру, содержащую прерываемое сообщение</p>
     <p>Описание: Это версия разблокирующего вызова для сообщения ввода/вывода, синтезируемая библиотекой в результате импульса от ядра, возникшего вследствие попытки клиента разблокироваться на этапе ввода/вывода. Обработчик <emphasis>io_unblock()</emphasis> для фазы установления соединения почти аналогичен (см. предыдущий параграф).</p>
     <p>Общим для обеих обработчиков разблокировки (как для функций установления соединения, так и для функций ввода/вывода) является желание клиента разблокироваться с разрешения администратора ресурсов. Администратор ресурсов <emphasis>обязан</emphasis> ответить на клиентское сообщение, чтобы разблокировать клиента. (Мы это обсуждали в главе «Обмен сообщениями», когда говорили о флагах функции <emphasis>ChannelCreate()</emphasis> — в частности, о флаге _NTO_CHF_UNBLOCK).</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <p>Подробное обсуждение стратегий разблокирования см. в разделе «Применение флага _NTO_MI_UNBLOCK_REQ» в главе «Обмен сообщениями».</p>
     <subtitle><emphasis>io_unlink()</emphasis></subtitle>
     <p><code><strong>int io_unlink(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_unlink_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_HANDLE_T*</strong> <emphasis><strong>handle</strong></emphasis><strong>, void*</strong> <emphasis><strong>reserved</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция установления соединения</p>
     <p>Обработчик по умолчанию: Нет</p>
     <p>Вспомогательные функции: <emphasis>iofunc_unlink()</emphasis></p>
     <p>Клиентская функция: <emphasis>unlink()</emphasis></p>
     <p>Сообщение: _IO_CONNECT, подтип _IO_CONNECT_UNLINK</p>
     <p>Структура данных:</p>
     <p><code>struct _io_connect {</code></p>
     <p><code> // Внутренние поля (как описано выше)</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>extra_type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>extra_len</emphasis>;</code></p>
     <p><code> char     <emphasis>path</emphasis>[1];</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>struct _io_connect_link_reply {</code></p>
     <p><code> uint32_t <emphasis>reserved1</emphasis>[2];</code></p>
     <p><code> uint8_t  <emphasis>eflag</emphasis>;</code></p>
     <p><code> uint8_t  <emphasis>reserved2</emphasis>[3];</code></p>
     <p><code> uint32_t <emphasis>umask</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>nentries</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>path_len</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_connect            <emphasis>connect</emphasis>;</code></p>
     <p><code> struct _io_connect_link_reply <emphasis>link_reply</emphasis>;</code></p>
     <p><code>} <emphasis>io_unlink_t</emphasis>;</code></p>
     <p>Описание: Отвечает за уничтожение связей (unlinking) файла, имя пути которого передается в поле <emphasis>path</emphasis> структуры входящего сообщения.</p>
     <p>Возвращает: Состояние по применению вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_unlock_ocb()</emphasis></subtitle>
     <p><code><strong>int io_unlock_ocb(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, void *</strong><emphasis><strong>reserved</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода (синтезируется библиотекой)</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_unlock_ocb_default()</emphasis></p>
     <p>Вспомогательные функции: Нет</p>
     <p>Клиентская функция: Все</p>
     <p>Сообщения: Нет (синтезируется библиотекой)</p>
     <p>Структура данных: Нет</p>
     <p>Описание: Действует противоположно вышеописанной функции <emphasis>io_lock_ocb()</emphasis>, т.е. отвечает за разблокирование атрибутной записи, на которую указывает OCB. Эта операция освобождает атрибутную запись, чтобы другие администратора ресурсов могли с ней работать. Подробности см. выше в разделе «Составные сообщения».</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_utime()</emphasis></subtitle>
     <p><code><strong>int io_utime(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_utime_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_utime_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_utime()</emphasis></p>
     <p>Клиентская функция: <emphasis>utime()</emphasis></p>
     <p>Сообщения: _IO_UTIME</p>
     <p>Структура данных:</p>
     <p><code>struct _io_utime {</code></p>
     <p><code> uint16_t       <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t       <emphasis>combine_len</emphasis>;</code></p>
     <p><code> int32_t        <emphasis>cur_flag</emphasis>;</code></p>
     <p><code> struct utimbuf <emphasis>times</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_utime <emphasis>i</emphasis>;</code></p>
     <p><code>} io_utime_t;</code></p>
     <p>Описание: Устанавливает времена последнего доступа и модификации либо в «текущий момент» (если они равны нулю), либо в заданные значения. Заметьте, что согласно правилам POSIX этот обработчик сообщения может быть необходим для модификации флагов <code><strong>IOFUNC_ATTR_*</strong></code> в атрибутной записи. Вам почти никогда не придется самостоятельно использовать этот обработчик; вместо этого вы будете использовать вспомогательную функцию POSIX-уровня.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_RESMGR_STATUS</emphasis>.</p>
     <subtitle><emphasis>io_write()</emphasis></subtitle>
     <p><code><strong>int io_write(resmgr_context_t *</strong><emphasis><strong>ctp</strong></emphasis><strong>, io_write_t *</strong><emphasis><strong>msg</strong></emphasis><strong>,</strong></code></p>
     <p><code><strong> RESMGR_OCB_T *</strong><emphasis><strong>ocb</strong></emphasis><strong>)</strong></code></p>
     <p>Классификация: Функция ввода/вывода</p>
     <p>Обработчик по умолчанию: <emphasis>iofunc_write_default()</emphasis></p>
     <p>Вспомогательные функции: <emphasis>iofunc_write_verify()</emphasis></p>
     <p>Клиентская функция: <emphasis>write()</emphasis>, <emphasis>fwrite()</emphasis>, и т.п.</p>
     <p>Сообщения: _IO_WRITE</p>
     <p>Структура данных:</p>
     <p><code>struct _io_write {</code></p>
     <p><code> uint16_t <emphasis>type</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>combine_len</emphasis>;</code></p>
     <p><code> int32_t  <emphasis>nbytes</emphasis>;</code></p>
     <p><code> uint32_t <emphasis>xtype</emphasis>;</code></p>
     <p><code>};</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> struct _io_write <emphasis>i</emphasis>;</code></p>
     <p><code>} io_write_t;</code></p>
     <p>Описание: Данный обработчик отвечает за получение данных, которые клиент записал в администратор ресурсов. Обработчику передается число байт, которые клиент пытается записать, в элементе <emphasis>nbytes</emphasis>; данные неявно следуют за входной структурой (если параметр <emphasis>xtype</emphasis> не установлен в _IO_XTYPE_OFFSET; см. ниже параграф «Простой пример функции <emphasis>io_write()</emphasis>»!). Согласно реализации, потребуется повторное считывание от клиента части сообщения с данными при помощи функции <emphasis>resmgr_msgreadv()</emphasis> или ей эквивалентной. Код завершения дает число байт, фактически записанных, либо устанавливает признак ошибки в <emphasis>errno</emphasis>.</p>
     <p>Отметьте, что чтобы удостовериться, что файл был открыт в режиме, совместимом с записью, следует вызвать вспомогательную функцию <emphasis>iofunc_write_verify()</emphasis>. Также следует вызывать функцию <emphasis>iofunc_sync_verify()</emphasis> для проверки необходимости синхронизации данных с носителем.</p>
     <p>Возвращает: Код завершения, при помощи вспомогательного макроса <emphasis>_IO_SET_WRITE_NBYTES</emphasis>.</p>
     <p>Пример см. ниже в параграфе «Простой пример функции <emphasis>io_write()</emphasis>».</p>
    </section>
   </section>
   <section>
    <title>
     <p>Примеры</p>
    </title>
    <section>
     <p>Этот раздел — своего рода «кулинарная книга» для программистов. Здесь я приведу ряд готовых примеров, которые вы сможете непосредственно использовать в качестве базиса для ваших проектов. Это не совсем готовые администраторы ресурсов — вы должны будете дополнить их функциями работы с пулами потоков и «каркасом» диспетчеризации (о котором речь ниже), а также удостовериться, что ваши версии функций- обработчиков помещаются в соответствующие таблицы функций <emphasis>после</emphasis> вызова <emphasis>iofunc_func_init()</emphasis>, чтобы корректно переопределить значения по умолчанию!</p>
     <p>Я начну с ряда простых примеров, демонстрирующих базовые функциональные возможности обработчиков различных сообщений, таких как:</p>
     <p>• <emphasis>io_read()</emphasis></p>
     <p>• <emphasis>io_write()</emphasis></p>
     <p>• <emphasis>io_devctl()</emphasis> (без передачи данных)</p>
     <p>• <emphasis>io_devctl()</emphasis> (с передачей данных)</p>
     <p>Затем, в разделе «Дополнительно», мы рассмотрим обработчик <emphasis>io_read()</emphasis>, который обеспечивает возврат элементов каталога.</p>
     <subtitle>Базовый каркас администратора ресурсов</subtitle>
     <p>Приведенный ниже пример можно использовать в качестве шаблона для многопоточного администратора ресурсов. (Шаблон однопоточного администратора ресурсов мы уже рассматривали — это было в разделе «Библиотека администратора ресурсов», когда мы обсуждали администратор <code><strong>/dev/null</strong></code>).</p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;stdlib.h&gt;</code></p>
     <p><code>#include &lt;stddef.h&gt;</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <p><code>#include &lt;sys/dispatch.h&gt;</code></p>
     <empty-line/>
     <p><code>static resmgr_connect_funcs_t connect_func;</code></p>
     <p><code>static resmgr_io_funcs_t io_func;</code></p>
     <p><code>static iofunc_attr_t attr;</code></p>
     <empty-line/>
     <p><code>main(int argc, char **argv) {</code></p>
     <p><code> thread_pool_attr_t pool_attr;</code></p>
     <p><code> thread_pool_t      *tpp;</code></p>
     <p><code> dispatch_t         *dpp;</code></p>
     <p><code> resmgr_attr_t      resmgr_attr;</code></p>
     <p><code> resmgr_context_t   *ctp;</code></p>
     <p><code> int                id;</code></p>
     <p><code> if ((dpp = dispatch_create()) == NULL) {</code></p>
     <p><code>  fprintf(stderr,</code></p>
     <p><code>   "%s: Ошибка выделения контекста диспетчеризации\n",</code></p>
     <p><code>   argv[0]);</code></p>
     <p><code>  return (EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> memset(&amp;pool_attr, 0, sizeof(pool_attr));</code></p>
     <p><code> pool_attr.handle = dpp;</code></p>
     <p><code> pool_attr.context_alloc = resmgr_context_alloc;</code></p>
     <p><code> pool_attr.block_func = resmgr_block;</code></p>
     <p><code> pool_attr.handler_func = resmgr_handler;</code></p>
     <p><code> pool_attr.context_free = resmgr_context_free;</code></p>
     <empty-line/>
     <p><code> // 1) Настроить пул потоков</code></p>
     <p><code> pool_attr.lo_water = 2;</code></p>
     <p><code> pool_attr.hi_water = 4;</code></p>
     <p><code> pool_attr.increment = 1;</code></p>
     <p><code> pool_attr.maximum = 50;</code></p>
     <p><code> if ((tpp =</code></p>
     <p><code>  thread_pool_create(&amp;pool_attr, POOL_FLAG_EXIT_SELF))</code></p>
     <p><code>   == NULL) {</code></p>
     <p><code>  fprintf(stderr,</code></p>
     <p><code>   "%s: Ошибка инициализации пула потоков\n",</code></p>
     <p><code>  argv[0]);</code></p>
     <p><code>  return (EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &amp;connect_func,</code></p>
     <p><code>  _RESMGR_IO_NFUNCS, &amp;io_func);</code></p>
     <p><code> iofunc_attr_init(&amp;attr, S_IFNAM | 0777, 0, 0);</code></p>
     <empty-line/>
     <p><code> // 2) Переопределить функции установления соединения</code></p>
     <p><code> // и функции ввода/вывода, как надо</code></p>
     <p><code> memset(&amp;resmgr_attr, 0, sizeof(resmgr_attr));</code></p>
     <p><code> resmgr_attr.nparts_max = 1;</code></p>
     <p><code> resmgr_attr.msg_max_size = 2048;</code></p>
     <empty-line/>
     <p><code> // 3) Замените «/dev/whatever» на нужный префикс</code></p>
     <p><code> if ((id =</code></p>
     <p><code>  resmgr_attach(dpp, &amp;resmgr_attr, "/dev/whatever",</code></p>
     <p><code>   _FTYPE_ANY,</code></p>
     <p><code>   0, &amp;connect_func, &amp;io_func, &amp;attr)) == -1) {</code></p>
     <p><code>  fprintf(stderr, "%s: Ошибка регистрации префикса\n",</code></p>
     <p><code>   argv[0]);</code></p>
     <p><code>  return (EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <p><code> // Отсюда возврата не будет</code></p>
     <p><code> thread_pool_start(tpp);</code></p>
     <p><code>}</code></p>
     <p>Дополнительную информацию об интерфейсе диспетчеризации (т.е., о функции <emphasis>dispatch_create()</emphasis>), см. в справочном руководстве по Си-библиотеке (С Library Reference).</p>
     <subtitle>Этап 1</subtitle>
     <p>Здесь мы используем функции пула потоков для создания пула, который должен будет обслуживать сообщения в нашем администраторе ресурсов. Вообще говоря, я бы рекомендовал вам начать однопоточного администратора ресурсов, как мы это делали ранее в примере с администратором <code><strong>/dev/null</strong></code>. Как только базовая функциональность у вас заработает, вы сможете <emphasis>затем</emphasis> добавить многопоточность. Вам нужно будет задать параметры <emphasis>lo_water</emphasis>, <emphasis>hi_water</emphasis>, <emphasis>increment</emphasis> и <emphasis>maximum</emphasis> структуры <emphasis>pool_attr</emphasis>, как это было описано в главе «Процессы и потоки» в обсуждениях функций пула потоков.</p>
     <subtitle>Этап 2</subtitle>
     <p>Здесь мы дополняем администратор ресурсов <emphasis>нашими</emphasis> функциями. Эти функции представляют собой функции- обработчики сообщений, о которых мы только что говорили (например, <emphasis>io_read()</emphasis>, <emphasis>io_devctl()</emphasis>, и т.п.). Например, чтобы добавить свой собственный обработчиком для сообщения _IO_READ, указывающий на функцию <emphasis>my_io_read()</emphasis>, мы должны были бы добавить в программу такую строчку: </p>
     <p><code>io_func.io_read = my_io_read;</code></p>
     <p>Это переопределит элемент таблицы, инициализированный вызовом <emphasis>iofunc_func_init()</emphasis> и содержавший функцию POSIX-уровня по умолчанию, заменив его указателем на вашу функцию <emphasis>my_io_read()</emphasis>.</p>
     <subtitle>Этап 3</subtitle>
     <p>Вы, вероятно, не захотите, чтобы ваш администратор ресурсов назывался <code><strong>/dev/whatever</strong></code> (букв. — «<code>/dev/абы_что</code>» — <emphasis>прим. ред</emphasis>.), так что вам придется выбрать для него соответствующее имя. Отметим, что привязка атрибутной записи (параметр <emphasis>attr</emphasis>) к регистрируемому префиксу осуществляется вызовом <emphasis>resmgr_attach()</emphasis> — если бы нам было надо, чтобы наш администратор обрабатывал несколько устройств, нам пришлось бы вызывать <emphasis>resmgr_attach()</emphasis> несколько раз, каждый раз с новой атрибутной записью, чтобы на этапе выполнения можно было отличить зарегистрированные префиксы друг от друга.</p>
    </section>
    <section>
     <title>
      <p>Простой пример функции <emphasis>io_read()</emphasis></p>
     </title>
     <p>Чтобы проиллюстрировать, как ваш администратор ресурса мог бы возвращать клиенту данные, рассмотрим простейший администратор ресурса, который всегда возвращает одну и ту же строковую константу «<code><strong>Здравствуй, мир!\n</strong></code>». Даже в таком простом случае необходимо учесть ряд моментов, как-то:</p>
     <p>• согласование размера клиентской области данных с количеством данных, подлежащих возврату;</p>
     <p>• обработка EOF;</p>
     <p>• поддерживание контекстной информации (индекс <emphasis>lseek()</emphasis>);</p>
     <p>• обновление POSIX-информации <emphasis>stat()</emphasis>.</p>
     <subtitle>Учет размеров областей данных</subtitle>
     <p>В нашем случае администратор ресурсов возвращает фиксированную строку длиной в 17 байт, то есть размер доступных данных точно известен и постоянен. Эти аналогично случаю с дисковым файлом, доступным только для чтения и содержащим рассматриваемую строку. Единственное реальное отличие состоит в том, что этот «файл» обеспечивается в нашей программе строкой:</p>
     <p><code>char *data_string = "Здравствуй, мир!\n";</code></p>
     <p>С другой стороны, клиент может запросить чтение любого объема данных — один байт, 17 байт или более. Это должно отразиться на характеристиках вашей реализации <emphasis>io_read()</emphasis> ее умением согласовывать размер запрашиваемых клиентом данных с размером данных, имеющихся в наличии.</p>
     <subtitle>Обработка EOF</subtitle>
     <p>Особым случаем согласования размеров областей данных является обработка EOF для строки фиксированной длины. Как только клиент считал заключительный символ «<code><strong>\n</strong></code>», дальнейшие его попытки считать данные должны возвращать EOF.</p>
     <subtitle>Поддерживание контекстной информации</subtitle>
     <p>И «учет размеров областей данных», и «обработка EOF» требуют, чтобы в OCB, передаваемом вашей функции <emphasis>io_read()</emphasis>, поддерживалась контекстная информация — в частности, поле <emphasis>offset</emphasis>.</p>
     <subtitle>Обновление информации POSIX</subtitle>
     <p>И еще одно заключительное соображение: при чтении данных из ресурса должна обновляться POSIX-переменная времени доступа <emphasis>atime</emphasis> («access time» — «время доступа»). Это делается для того, чтобы клиентская функция <emphasis>stat()</emphasis> могла обнаружить, что к устройству кто-то обращался.</p>
     <subtitle>Собственно код</subtitle>
     <p>Ниже приведена программа, в которой учтены все вышеперечисленные моменты. Ниже мы ее последовательно проанализируем.</p>
     <p><code>/*</code></p>
     <p><code> * io.read1.c</code></p>
     <p><code>*/</code></p>
     <empty-line/>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;errno.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <empty-line/>
     <p><code>// наша строка с данными</code></p>
     <p><code>char* data_string = "Здравствуй, мир!\n";</code></p>
     <empty-line/>
     <p><code>int io_read(resmgr_context_t* ctp, io_read_t* msg,</code></p>
     <p><code> iofunc_ocb_t* ocb) {</code></p>
     <p><code> int sts;</code></p>
     <p><code> int nbytes;</code></p>
     <p><code> int nleft;</code></p>
     <p><code> int off;</code></p>
     <p><code> int xtype;</code></p>
     <p><code> struct _xtype_offset* xoffset;</code></p>
     <empty-line/>
     <p><code> // 1) Проверить, открыто ли устройство на чтение</code></p>
     <p><code> if ((sts ==</code></p>
     <p><code>  iofunc_read_verify(ctp, msg, ocb, NULL)) != EOK) {</code></p>
     <p><code>  return sts;</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 2) проверить и обработать переопределение XTYPE</code></p>
     <p><code> xtype = msg-&gt;i.xtype &amp; _IO_XTYPE_MASK;</code></p>
     <p><code> if (xtype == _IO_XTYPE_OFFSET) {</code></p>
     <p><code>  xoffset = (struct _xtype_offset*)(msg-&gt;i + 1);</code></p>
     <p><code>  off = xoffset-&gt;offset;</code></p>
     <p><code> } else if (xtype = _IO_XTYPE_NONE) {</code></p>
     <p><code>  off = ocb-&gt;offset;</code></p>
     <p><code> } else { // Неизвестный тип; игнорировать</code></p>
     <p><code>  return ENOSYS;</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 3) Сколько байт осталось?</code></p>
     <p><code> nleft = ocb-&gt;attr-&gt;nbytes – off;</code></p>
     <empty-line/>
     <p><code> // 4) Сколько байт мы можем отдать клиенту?</code></p>
     <p><code> nbytes = min(nleft, msg-&gt;i.nbytes);</code></p>
     <empty-line/>
     <p><code> // 5) Если возвращаем данные, отдать их клиенту</code></p>
     <p><code> if (nbytes) {</code></p>
     <p><code>  MsgReply(ctp-&gt;rcvid, nbytes, data_string+off, nbytes);</code></p>
     <p><code>  // 6) Установить значение "atime" для POSIX stat()</code></p>
     <p><code>  ocb-&gt;attr-&gt;flags |=</code></p>
     <p><code>   IOFUNC_ATTR_ATIME | IOFUNC_ATTR_DIRTY_TIME;</code></p>
     <p><code>  // 7) Если индекс lseek() не равен _IO_XTYPE_OFFSET,</code></p>
     <p><code>  // увеличить его на число считанных байт</code></p>
     <p><code>  if (xtype == _IO_XTYPE_NONE) {</code></p>
     <p><code>   ocb-&gt;offset += nbytes;</code></p>
     <p><code>  }</code></p>
     <p><code> } else {</code></p>
     <p><code>  // 8) Не возвращаем данные, просто разблокировать клиента</code></p>
     <p><code>  MsgReply(ctp-&gt;rcvid, EOK, null, 0);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 9) Сказать библиотеке, что мы уже ответили сами</code></p>
     <p><code> return _RESMGR_NOREPLY;</code></p>
     <p><code>}</code></p>
     <subtitle>Этап 1</subtitle>
     <p>Здесь мы убедились, что клиентский вызов <emphasis>open()</emphasis> действительно запросил открытие устройства на чтение. Если бы клиент открыл устройство только на запись, а затем попытался выполнить чтение, это следовало бы расценивать как ошибку. В этом случае вспомогательная функция <emphasis>iofunc_read_verify()</emphasis> возвратила бы нам (затем мы — библиотеке, а библиотека — клиенту) EBADF, а не EOK.</p>
     <subtitle>Этап 2</subtitle>
     <p>Здесь мы проверили, указал ли клиент индивидуальное для данного сообщения переопределение типа (<emphasis>xtype-override</emphasis>) (например, потому что если мы открыли устройство в неблокирующем режиме, то это указало бы, что для данного конкретного запроса мы хотим задать блокирующее поведение). Отметим, что блокирующий аспект переопределенияа типа может быть отражён в последнем параметре функции <emphasis>iofunc_read_verify()</emphasis>, однако, поскольку мы приводим здесь упрощенный пример, мы передаем NULL, указывая этим, что этот вопрос нас не волнует.</p>
     <p>Более важно, однако, посмотреть, как обрабатываются конкретные модификаторы xtype. Очень интересен, например, модификатор _IO_XTYPE_OFFSET, который, если присутствует, указывает на то, что принятое от клиента сообщение содержит смещение, и что операция чтения не должна изменять «текущую позицию файла» для данного файлового дескриптора (так делает, например, функция <emphasis>pread()</emphasis>). Если модификатор _IO_XTYPE_OFFSET не указан, то операция чтения может смело модифицировать «текущую позицию файла». Мы используем переменную <emphasis>хtype</emphasis> для сохранения xtype, содержавшегося в принятом сообщении, и переменную <emphasis>off</emphasis> для представления текущего смещения, которое мы должны будем использовать при обработке. Далее, на этапе 7, вы увидите еще кое-какие действия по обработке модификатора _IO_XTYPE_OFFSET.</p>
     <p>Если присутствует иное переопределение xtype, чем _IO_XTYPE_OFFSET (и это не пустая команда _IO_XTYPE_NONE), мы отказываемся обрабатывать запрос и возвращаем ENOSYS. Это просто означает, что мы не знаем, как обрабатывать такую ситуацию, и поэтому возвращаем клиенту признак ошибки.</p>
     <subtitle>Этапы 3 и 4</subtitle>
     <p>Чтобы вычислить, сколько байт мы можем реально возвратить клиенту, мы выполняем этапы 3 и 4, в которых выясняется, сколько байт доступно у устройства (разность между полным объемом устройства, полученным из <code><strong>ocb-&gt;attr-&gt;nbytes</strong></code>, и текущим смещением в устройстве). Узнав, сколько байт осталось, мы выбираем наименьшее значение между размером этого остатка и количеством байт, которые клиент хочет прочитать. Например, у нас может остаться семь байт, а клиент захочет прочитать только два. В этом случае мы возвратим клиенту только два байта. И наоборот, если клиент захочет прочитать 4096 байт, а у нас осталось только семь, мы сможем возвратить ему только семь байт.</p>
     <subtitle>Этап 5</subtitle>
     <p>Теперь, вычислив, сколько байт мы намерены возвратить клиенту, нам нужно сделать ряд вещей в зависимости от того, возвращаем мы данные или нет. Если да, то мы просто отвечаем клиенту с данными сразу после проверки на этапе 5. Обратите внимание, что для возврата данных с корректного смещения мы используем <code><strong>data_string + off</strong></code> (смещение <emphasis>off</emphasis> вычисляется в зависимости от наличия переопределения типа). Отметьте также второй параметр функции <emphasis>MsgReply()</emphasis> — в документации он упоминается как «<emphasis>status</emphasis>» («код завершения»), но в этом случае мы используем его для возврата числа байт. Мы делаем так, потому что реализация клиентской функции <emphasis>read()</emphasis> знает, что значение, возвращаемое ее функцией <emphasis>MsgSendv()</emphasis> (а это, кстати, как раз и есть параметр <emphasis>status</emphasis> функции <emphasis>MsgReply()</emphasis>) представляет собой число реально прочитанных байт — это общеизвестное соглашение.</p>
     <subtitle>Этап 6</subtitle>
     <p>Поскольку мы возвращаем данные от устройства, мы знаем, что к устройству производился доступ. Мы устанавливаем биты IOFUNC_ATTR_ATIME и IOFUNC_ATTR_DIRTY_TIME в поле <emphasis>flags</emphasis> атрибутной записи. Это служит напоминанием для функции <emphasis>io_stat()</emphasis> о том, что время доступа стало недействительным, и перед выполнением ответа его следует скорректировать по системным часам. Если бы нам очень хотелось, мы могли бы записать текущее время в поле <emphasis>atime</emphasis> атрибутной записи и сбросить флаг IOFUNC_ATTR_DIRTY_TIME; однако, это было бы не очень-то эффективно, поскольку мы предполагаем получить от клиента значительно большее запросов типа <emphasis>read()</emphasis>, чем запросов типа <emphasis>stat()</emphasis>. Впрочем, ваши условия могут диктовать иначе.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Так какое же время видит клиент, когда он вызывает-таки функцию <emphasis>stat()</emphasis>? Функция <emphasis>iofunc_stat_default()</emphasis>, предоставляемая библиотекой администратора ресурсов, посмотрит на поле <emphasis>flags</emphasis> атрибутной записи, чтобы проверить, являются времена доступа (поля <emphasis>atime</emphasis>, <emphasis>ctime</emphasis> и <emphasis>mtime</emphasis>) корректными или нет. Если нет (как это было бы после вызова <emphasis>io_read()</emphasis> с возвратом данных), <emphasis>iofunc_stat_default()</emphasis> устанавливает нужные из них в значение текущего времени.</p>
     </cite>
     <subtitle>Этап 7</subtitle>
     <p>Теперь мы увеличиваем смещение <emphasis>lseek()</emphasis> на число возвращенных клиенту байт, но делаем это только в том случае, если не обрабатываем модификатор _IO_XTYPE_OFFSET. Это гарантирует, что в случае отсутствия флага _IO_XTYPE_OFFSET, если клиент вызовет функцию <emphasis>lseek()</emphasis> для определения текущей позиции, или (более важный случай) если клиент вызовет <emphasis>read()</emphasis> для чтения еще нескольких байт, смещение в ресурсе будет корректным. Если _IO_XTYPE_OFFSET установлен, мы оставляем содержащееся в <emphasis>ocb</emphasis> смещение в покое.</p>
     <subtitle>Этап 8</subtitle>
     <p>Сопоставьте этот этап с этапом 6. Здесь мы только разблокируем клиента и не выполняем больше никаких действий. Также обратите внимание, что функции <emphasis>MsgReply()</emphasis> не передается никакой области данных, потому что в этом случае данные мы не возвращаем.</p>
     <subtitle>Этап 9</subtitle>
     <p>И наконец, на этапе 9 мы выполняем действия, не зависящие от того, возвращаем мы данные клиенту или нет. Поскольку мы уже сами разблокировали клиента при помощи <emphasis>MsgReply()</emphasis>, мы, конечно же, не хотим, чтобы это попыталась сделать еще и библиотека администратора ресурсов. Поэтому мы сообщаем ей, что мы уже сделали это сами, возвратом _RESMGR_NOREPLY.</p>
     <subtitle>Эффективное применение других функций обмена сообщениями</subtitle>
     <p>Как вы помните из главы «Обмен сообщениями», мы упоминали еще несколько функций обмена сообщениями, а именно — функции <emphasis>MsgWrite()</emphasis>, <emphasis>MsgWritev()</emphasis> и <emphasis>MsgReplyv()</emphasis>. Повод, в связи с которым я снова упоминаю здесь эти функции, состоит в том, что ваша функция <emphasis>io_read()</emphasis> может быть превосходным местом для их применения. В простом примере, показанном выше, мы возвращали непрерывный массив данных из постоянного места в памяти. В реальной же жизни вам может понадобиться возвратить, скажем, множество фрагментов данных из различных выделенных вами буферов. Классическим примером такого случая является циклический буфер, который часто применяется, например, в драйверах последовательных устройств. Часть данных может быть размещена в конце буфера, другая часть — в начале. В этом случае для возврата обеих частей данных вам понадобилось бы передать <emphasis>MsgReplyv()</emphasis> двухэлементный вектор ввода/вывода (IOV), где первый элемент содержал бы адрес (и длину) «нижней» части данных, а второй — адрес (и длину) «верхней» части. Или же, если вы ожидаете прибытия данных частями, вы могли бы вместо этого использовать функции <emphasis>MsgWrite()</emphasis> или <emphasis>MsgWritev()</emphasis> для записи данных в адресное пространство клиента по мере их поступления, а затем выдать заключительный вызов <emphasis>MsgReply()</emphasis> или <emphasis>MsgReplyv()</emphasis>, чтобы разблокировать клиента. Как мы уже показали выше, функция <emphasis>MsgReply()</emphasis> может и не передавать никаких данных— вы можете использовать ее просто для того, чтобы разблокировать клиента.</p>
    </section>
    <section>
     <title>
      <p>Простой пример функции <emphasis>io_write()</emphasis></p>
     </title>
     <p>Это был простой пример функции io_read(); давайте теперь перейдем к функции <emphasis>io_write()</emphasis>. Основной камень преткновения, связанный с <emphasis>io_write()</emphasis>, — получить доступ к данным. Поскольку библиотека администратора ресурсов считывает лишь незначительную часть сообщения от клиента, переданные клиентом данные (они идут сразу после заголовка _IO_WRITE) могут быть приняты функцией <emphasis>io_write()</emphasis> только частично. Простой пример — представьте себе клиента, записывающего один мегабайт. Библиотекой администратора ресурсов будут считаны только заголовок сообщения и несколько байт данных. Остальная часть мегабайта остается по-прежнему доступной на клиентской стороне — администратор ресурсов при желании может к ней обращаться.</p>
     <p>Реально рассмотрения заслуживают только два случая:</p>
     <p>• все содержимое сообщения клиентской функции <emphasis>write()</emphasis> было считано библиотекой администратора ресурсов полностью; или</p>
     <p>• этого не произошло.</p>
     <p>Судьбоносное решение, однако, состоит в ответе на следующий вопрос: «Какие проблемы сопряжены с попыткой сохранить полученную с первым сообщением часть данных?» Ответ такой: овчинка не стоит выделки. Тому есть ряд причин:</p>
     <p>• обмен сообщениями (операции копирования на уровне ядра) выполняется очень быстро;</p>
     <p>• проверка, получены ли данные целиком или частично, влечет определенные накладные расходы;</p>
     <p>• дополнительные накладные расходы связаны с попыткой «сохранения» первой, уже прибывшей, части данных, в свете того факта, что ожидаются дополнительные.</p>
     <p>По-моему, первые два пункта говорят сами за себя. Третий же пункт заслуживает пояснения. Давайте предположим, что клиент переслал большую порцию данных, и мы <emphasis>приняли-таки</emphasis> решение о том, что было бы неплохо попробовать сохранить ту часть данных, которая уже получена. К сожалению, эта часть оказалось очень небольшой. Это означает, что вместо одного непрерывного массива байт у нас теперь будет большой кусок и маленький «довесок». Иными словами, только ради этого маленького кусочка нам придется значительно усложнять работу с данными, что может неприятно сказаться на эффективности кода. Это потенциальная головная боль, не делайте так!</p>
     <p>Реальным ответом на поставленный вопрос будет просто заново считать данные в заранее подготовленные буферы. В нашем простом примере функции <emphasis>io_write()</emphasis> я буду просто каждый раз выделять буфер при помощи <emphasis>malloc()</emphasis>, считывать в него данные, а затем освобождать его функцией <emphasis>free()</emphasis>. Разумеется, есть и куда более эффективные способы выделения буферов и управления ими!</p>
     <p>Еще один тонкий момент в этом примере функции <emphasis>io_write()</emphasis> — это обработка модификатора _IO_XTYPE_OFFSET (и связанных с этим данных; здесь она выполняется несколько иначе, чем в примере <emphasis>io_read())</emphasis>.</p>
     <p><code>/*</code></p>
     <p><code> * io_write1.c</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;stdlib.h&gt;</code></p>
     <p><code>#include &lt;errno.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <empty-line/>
     <p><code>void process_data(int offset, void *buffer, int nbytes) {</code></p>
     <p><code> // Сделать что-нибудь с данными</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>int io_write(resmgr_context_t *ctp, io_write_t *msg,</code></p>
     <p><code> iofunc_ocb_t *ocb) {</code></p>
     <p><code> int sts;</code></p>
     <p><code> int nbytes;</code></p>
     <p><code> int off;</code></p>
     <p><code> int start_data_offset;</code></p>
     <p><code> int xtype;</code></p>
     <p><code> char *buffer;</code></p>
     <p><code> struct _xtype_offset *xoffset;</code></p>
     <empty-line/>
     <p><code> // Проверить, открыто ли устройство на запись</code></p>
     <p><code> if ((sts =</code></p>
     <p><code>  iofunc_write_verify(ctp, msg, ocb, NULL)) != EOK) {</code></p>
     <p><code>  return (sts);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 1) Проверить и обработать переопределение</code></p>
     <p><code> XTYPE xtype = msg-&gt;i.xtype &amp; _IO_XTYPE_MASK;</code></p>
     <p><code> if (xtype == _IO_XTYPE_OFFSET) {</code></p>
     <p><code>  xoffset = (struct _xtype_offset*)(&amp;msg-&gt;i + 1);</code></p>
     <p><code>  start_data_offset = sizeof(msg-&gt;i) + sizeof(*xoffset);</code></p>
     <p><code>  off = xoffset-&gt;offset;</code></p>
     <p><code> } else if (xtype == _IO_XTYPE_NONE) {</code></p>
     <p><code>  off = ocb-&gt;offset;</code></p>
     <p><code>  start_data_offset = sizeof(msg-&gt;i);</code></p>
     <p><code> } else {</code></p>
     <p><code>  // Неизвестный тип; игнорировать</code></p>
     <p><code>  return (ENOSYS);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 2) Выделить достаточно большой буфер для данных</code></p>
     <p><code> nbytes = msg-&gt;i.nbytes;</code></p>
     <p><code> if ((buffer = malloc(nbytes)) == NULL) {</code></p>
     <p><code>  return (ENOMEM);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 3) Считать данные от клиента (возможно, повторно)</code></p>
     <p><code> if (resmgr_msgread(ctp, buffer, nbytes,</code></p>
     <p><code>  start_data_offset) == -1) {</code></p>
     <p><code>  free(buffer);</code></p>
     <p><code>  return (errno);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 4) Сделать что-нибудь с данными</code></p>
     <p><code> process_data(off, buffer, nbytes);</code></p>
     <empty-line/>
     <p><code> // 5) Освободить память буфера</code></p>
     <p><code> free(buffer);</code></p>
     <empty-line/>
     <p><code> // 6) Установить, сколько байт должна возвращать</code></p>
     <p><code> // клиентская функция «write»</code></p>
     <p><code> _IO_SET_WRITE_NBYTES(ctp, nbytes);</code></p>
     <empty-line/>
     <p><code> // 7) Если данные записаны, обновить структуры</code></p>
     <p><code> // данных POSIX и смещение OCB</code></p>
     <p><code> if (nbytes) {</code></p>
     <p><code>  ocb-&gt;attr-&gt;flags |=</code></p>
     <p><code>   IOFUNC_ATTR_MTIME | IОFUNC_ATTR_DIRTY_TIME;</code></p>
     <p><code>  if (xtype == _IO_XTYPE_NONE) {</code></p>
     <p><code>   ocb-&gt;offset += nbytes;</code></p>
     <p><code>  }</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 8) Пусть библиотека сама ответит, что все в порядке</code></p>
     <p><code> return (EOK);</code></p>
     <p><code>}</code></p>
     <p>Как вы видите, некоторые начальные действия идентичны таковым из примера функции <emphasis>io_read()</emphasis> — функция <emphasis>iofunc_write_verify()</emphasis> аналогична функции <emphasis>iofunc_read_verify()</emphasis>, и проверка переопределения xtype выполняется точно также.</p>
     <subtitle>Этап 1</subtitle>
     <p>Здесь мы выполняем обработку переопределения xtype, в значительной степени аналогичную примеру с <emphasis>io_read()</emphasis> — за исключением того, что смещение не сохраняется в поле структуры входящего сообщения. Причина этого состоит в том, что обычной практикой для определения начального адреса поступающих от клиента данных является использование размера структуры входящего сообщения. Мы предпринимаем дополнительные усилия, чтобы удостовериться, что смещение начала данных (<emphasis>doffset</emphasis>) в коде обработки xtype является корректным.</p>
     <subtitle>Этап 2</subtitle>
     <p>Здесь мы выделяем буфер, достаточный для размещения в нем данных. Число байт, которые клиент собирается записать, представлено нам в поле nbytes объединения <emphasis>msg</emphasis>, оно заполняется автоматически Си-библиотекой клиента в коде функции <emphasis>write()</emphasis>. Отметим, что у нас недостаточно памяти для обработки запроса <emphasis>malloc()</emphasis>, мы возвращаем клиенту ENOMEM, чтобы он знал, почему его запрос потерпел неудачу.</p>
     <subtitle>Этап 3</subtitle>
     <p>Здесь мы применяем вспомогательную функцию <emphasis>resmgr_msgread()</emphasis> для считывания всего объема данных от клиента непосредственно в только что выделенный для этого буфер. В большинстве случаев здесь вполне сошла бы функция <emphasis>MsgRead()</emphasis>, но в случаях, когда сообщение является частью составного сообщения, функция <emphasis>resmgr_msgread()</emphasis> выполни для нас еще и соответствующие «магические» действия (о том, почему это надо, см. раздел «Составные сообщения»). Параметры функции <emphasis>resmgr_msgread()</emphasis> довольно очевидны: мы передаем ей указатель на внутренний контекстный блок (<emphasis>ctp</emphasis>), буфер, в который мы хотим поместить данные (<emphasis>buffer</emphasis>), и число байт, которые мы хотим считать (поле <emphasis>nbytes</emphasis> объединения <emphasis>msg</emphasis>). Последний параметр — это смещение, которое мы вычислили ранее, на этапе 1. Смещение реально позволяет пропустить заголовок, помещенный туда функцией <emphasis>write()</emphasis> клиентской Си-библиотеки, и сразу перейти к данным. Здесь возникает два интересных момента:</p>
     <p>• мы могли бы взять произвольное смещение, чтобы считывать любые фрагменты данных в любом порядке, как нам заблагорассудится;</p>
     <p>• мы могли бы использовать функцию <emphasis>resmgr_msgreadv()</emphasis> (обратите внимание на символ «V» в названии) для считывания данных от клиента в вектор ввода/вывода, возможно, описывающий несколько разных буферов, подобно тому, как мы делали с буферами кэша при обсуждении файловых систем в главе «Обмен сообщениями».</p>
     <subtitle>Этап 4</subtitle>
     <p>Здесь вы можете делать с данными все, что вашей душе угодно — я, например, вызвал некую условную функцию <emphasis>process_data</emphasis>() и передал ей буфер и его размер.</p>
     <subtitle>Этап 5</subtitle>
     <p>Критически важный этап! Упустить его из виду очень просто, но это неизбежно приведет к «утечкам памяти». Обратите также внимание, как мы позаботились об освобождении памяти в случае неудачи на этапе 3.</p>
     <subtitle>Этап 6</subtitle>
     <p>Здесь мы используем макрос <emphasis>_IO_SET_WRITE_NBYTES()</emphasis> для сохранения числа записанных байт, которое затем будет передано назад клиенту в качестве значения, возвращаемого функцией <emphasis>write()</emphasis>. Важно отметить, что вы должны возвратить фактическое число байт! От этого зависит судьба клиента.</p>
     <subtitle>Этап 7</subtitle>
     <p>Пришло время навести лоск для функций <emphasis>stat()</emphasis>, <emphasis>lseek()</emphasis> и последующих <emphasis>write()</emphasis>, аналогично тому, как мы это делали в нашей <emphasis>io_read()</emphasis> (и опять мы изменяем смещение в ocb только в том случае, если это не сообщение типа _IO_XTYPE_OFFSET). Однако, поскольку мы выполняем запись в устройство, мы используем при этом константу IOFUNC_ATTR_MTIME вместо константы IOFUNC_ATTR_ATIME. Флаг MTIME означает «время модификации» (modification time) — функция <emphasis>write()</emphasis> определенно его изменяет.</p>
     <subtitle>Этап 8</subtitle>
     <p>Последний этап прост: мы возвращаем константу EOK, которая сообщает библиотеке администратора ресурсов, что она должна ответить клиенту. Здесь наша работа закончена. Библиотека администратора ресурсов использует в ответе данные о числе записанных байт, которые мы сохранили с помощью макроса <emphasis>IO_SET_WRITE_NBYTES()</emphasis>, и разблокирует клиента. Клиентская функция <emphasis>write()</emphasis> возвратит число байт, записанное нашим устройством.</p>
    </section>
    <section>
     <title>
      <p>Простой пример функции <emphasis>io_devctl()</emphasis></p>
     </title>
     <p>Клиентский вызов <emphasis>devctl()</emphasis> формально определен так:</p>
     <p><code>#include &lt;sys/types.h&gt;</code></p>
     <p><code>#include &lt;unistd.h&gt;</code></p>
     <p><code>#include &lt;devctl.h&gt;</code></p>
     <empty-line/>
     <p><code>int devctl(int <emphasis>fd</emphasis>, int <emphasis>dcmd</emphasis>, void *<emphasis>dev_data_ptr</emphasis>,</code></p>
     <p><code> size_t <emphasis>nbytes</emphasis>, int *<emphasis>dev_info_ptr</emphasis>);</code></p>
     <p>Прежде чем рассматривать эту функцию с позиций администратора ресурсов, надо сначала понять, что это за зверь. Функция <emphasis>devctl()</emphasis> применяется для «нестандартных» и «управляющих» операций. Например, вы можете записывать данные в звуковую плату (реальные оцифрованные звуковые фрагменты, которые звуковая плата должна будет конвертировать в аналоговый аудиосигнал) и принять решение об изменении числа каналов от одного (моно) до двух (стерео) или об изменении частоты дискретизации данных от стандарта CD (44.1 кГц) к стандарту DAT (48 кГц). Такие вещи было бы правильно делать при помощи функции <emphasis>devctl()</emphasis>. При написании администратора ресурсов вы можете решить, что вам вообще не нужны никакие <emphasis>devctl()</emphasis>, и что всю необходимую функциональность можно свести к стандартным функциям <emphasis>read()</emphasis> и <emphasis>write()</emphasis>. С другой стороны, вы можете захотеть использовать как вызовы devctl() наряду с вызовами <emphasis>read()</emphasis> и <emphasis>write()</emphasis>, так и только <emphasis>devctl()</emphasis> — это будет зависеть от вашего устройства.</p>
     <p>Функция <emphasis>devctl()</emphasis> принимает 5 аргументов:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>fd</emphasis></td>
       <td align="left" valign="top">Дескриптор файла администратора ресурсов, которому вы посылаете команду <emphasis>devctl()</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>dcmd</emphasis></td>
       <td align="left" valign="top">Собственно команда — комбинация из двух разрядов направления обмена данными и 30 разрядов команды (см. ниже).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>dev_data_ptr</emphasis></td>
       <td align="left" valign="top">Указатель на область данных, которые передаются, принимаются или и то, и другое.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>nbytes</emphasis></td>
       <td align="left" valign="top">Размер области данных, на которую указывает <emphasis>dev_data_ptr</emphasis>.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>dev_info_ptr</emphasis></td>
       <td align="left" valign="top">Переменная для дополнительной информации, установку которой может выполнить администратор ресурса.</td>
      </tr>
     </table>
     <p>Двумя старшими разрядами команды <emphasis>dcmd</emphasis> кодируется направление обмена данными, если он вообще имеет место. Подробности см. выше в описании функций ввода/вывода (параграф «<emphasis>io_devctl()</emphasis>»).</p>
     <p>Когда администратор ресурсов принимает сообщение _IO_DEVCTL, оно обрабатывается вашей функцией <emphasis>io_devctl()</emphasis>. Ниже представлен очень простой пример, который предполагается использовать для настройки каналов и частоты дискретизации для аудиоустройства, как упоминалось выше.</p>
     <p><code>/*</code></p>
     <p><code> * io_devctl1.c</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;stdlib.h&gt;</code></p>
     <p><code>#include &lt;unistd.h&gt;</code></p>
     <p><code>#include &lt;errno.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <empty-line/>
     <p><code>#define DCMD_AUDIO_SET_CHANNEL_MONO    1</code></p>
     <p><code>#define DCMD_AUDIO_SET_CHANNEL_STEREO  2</code></p>
     <p><code>#define DCMD_AUDIO_SET_SAMPLE_RATE_CD  3</code></p>
     <p><code>#define DCMD_AUDIO_SET_SAMPLE_RATE_DAT 4</code></p>
     <empty-line/>
     <p><code>int io_devctl(resmgr_context_t *ctp, io_devctl_t *msg,</code></p>
     <p><code> iofunc_ocb_t *ocb) {</code></p>
     <p><code> int sts;</code></p>
     <empty-line/>
     <p><code> // 1) Проверить, не является ли это обычным</code></p>
     <p><code> // POSIX-совместимым devctl()</code></p>
     <p><code> if ((sts =</code></p>
     <p><code>  iofunc_devctl_default(ctp, msg, ocb)) !=</code></p>
     <p><code>   _RESMGR_DEFAULT) {</code></p>
     <p><code>  return (sts);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 2) Узнать, что за команда, и отработать ее</code></p>
     <p><code> switch (msg-&gt;i.dcmd) {</code></p>
     <p><code> case DCMD_AUDIO_SET_CHANNEL_MONO:</code></p>
     <p><code>  audio_set_nchannels(1);</code></p>
     <p><code>  break;</code></p>
     <p><code> case DCMD_AUDIO_SET_CHANNEL_STEREO:</code></p>
     <p><code>  audio_set_nchannels(2);</code></p>
     <p><code>  break;</code></p>
     <p><code> case DCMD_AUDIO_SET_SAMPLE_RATE_CD:</code></p>
     <p><code>  audio_set_samplerate(44100);</code></p>
     <p><code>  break;</code></p>
     <p><code> case DCMD_AUDIO_SET_SAMPLE_RATE_DAT:</code></p>
     <p><code>  audio_set_samplerate(48000);</code></p>
     <p><code>  break;</code></p>
     <p><code> // 3) Если мы не знаем такой команды, отвергнуть ее</code></p>
     <p><code> default:</code></p>
     <p><code>  return (ENOSYS);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 4) Сказать клиенту, что все отработано</code></p>
     <p><code> memset(imsg-&gt;о, 0, sizeof(msg-&gt;о));</code></p>
     <p><code> SETIOV(ctp-&gt;iov, &amp;msg-&gt;o, sizeof(msg-&gt;o));</code></p>
     <p><code> return (_RESMGR_NPARTS(1));</code></p>
     <p><code>}</code></p>
     <subtitle>Этап 1</subtitle>
     <p>На первом этапе мы снова видим применение вспомогательной функции, на этот раз — функции <emphasis>iofunc_devctl_default()</emphasis>, которая используется для выполнения всей обработки по умолчанию для <emphasis>devctl()</emphasis>. Если вы не поставляете свою версию <emphasis>io_devctl()</emphasis>, а только инициализируете таблицы функций ввода/вывода и установления соединения при помощи <emphasis>iofunc_func_init()</emphasis>, будет вызвана именно <emphasis>iofunc_devctl_default()</emphasis>. Мы включаем ее в нашу версию <emphasis>io_devctl()</emphasis>, потому что мы хотим, чтобы она обработала для нас все стандартные POSIX-варианты вызова <emphasis>devctl()</emphasis>. Затем мы проверяем возвращаемое значение; если это не _RESMGR_DEFAULT, значит, функция <emphasis>iofunc_devctl_default()</emphasis> «обработала» запрос, и нам остается только возвратить это значение, выдав его за «наше».</p>
     <p>Если возвращенное значение <emphasis>является</emphasis> константой _RESMGR_DEFAULT, это говорит нам, что вспомогательная функция <emphasis>не</emphasis> обработала запрос, и что мы должны выяснить, является ли он одним из «наших».</p>
     <subtitle>Этап 2</subtitle>
     <p>Эта проверка выполняется на этапе 2 при помощи инструкции <code><strong>switch</strong></code>/<code><strong>case</strong></code>. Мы просто проверяем значение <emphasis>dcmd</emphasis>, которое клиентский код указал во втором параметре функции <emphasis>devctl()</emphasis>, на предмет совпадения с какой-нибудь из «наших» команд. Обратите внимание, что для выполнения фактической «работы» для клиента мы вызываем фиктивные функции <emphasis>audio_set_nchannels()</emphasis> и <emphasis>audio_set_samplerate()</emphasis>. Здесь важно отметить, что мы преднамеренно избегаем обсуждения области данных функции <emphasis>devctl()</emphasis>. Вы можете подумать: «А что если я хочу установить частоту дискретизации в некое значение <emphasis>n</emphasis>? Как это сделать?» На этот вопрос мы ответим в следующем примере <emphasis>io_devctl()</emphasis>, который представлен ниже.</p>
     <subtitle>Этап 3</subtitle>
     <p>Этот этап — дань концепции защитного программирования. Мы возвращаем код ошибки ENOSYS, чтобы сообщить клиенту, что мы не распознали его запрос.</p>
     <subtitle>Этап 4</subtitle>
     <p>Наконец, мы обнуляем возвратную структуру и устанавливаем на нее одноэлементный вектор ввода/вывода. Затем мы возвращаем библиотеке администратора ресурсов единицу (1) через макрос <emphasis>_RESMGR_NPARTS()</emphasis>, сообщая ей тем самым, что мы возвращаем одноэлементный вектор ввода/вывода. Это и будет возвращено клиенту. Как вариант, мы могли бы применить макрос <emphasis>_RESMGR_PTR()</emphasis>:</p>
     <p><code>// Вместо этого</code></p>
     <p><code>// 4) Сказать клиенту, что это сработало</code></p>
     <p><code>memset(imsg-&gt;о, 0, sizeof(msg-&gt;о));</code></p>
     <p><code>SETIOV(&amp;ctp-&gt;iov, &amp;msg-&gt;о, sizeof(msg-&gt;o));</code></p>
     <p><code>return (_RESMGR_NPARTS(1));</code></p>
     <empty-line/>
     <p><code>//Мы могли бы сделать так:</code></p>
     <p><code>// 4) Сказать клиенту, что это сработало</code></p>
     <p><code>memset(imsg-&gt;о, 0, sizeof(msg-&gt;о));</code></p>
     <p><code>return (_RESMGR_PTR(ctp, imsg-&gt;o, sizeof(msg-&gt;o)));</code></p>
     <p>Причиной тому, что мы здесь обнулили возвращаемую структуру (вспомните, в примерах <emphasis>io_read()</emphasis> и <emphasis>io_write()</emphasis> мы этого не делали) является то, что в данном случае возвращаемая структура имеет реальное содержимое! (В случае с <emphasis>io_read()</emphasis> мы возвращали только собственно данные и число считанных байт — никакой «возвращаемой структуры» не было; в случае же с <emphasis>io_write()</emphasis> единственным возвращаемым значением было число записанных байт.)</p>
    </section>
    <section>
     <title>
      <p>Пример функции <emphasis>io_devctl()</emphasis>, имеющей дело с данными</p>
     </title>
     <p>В предыдущем примере <emphasis>io_devctl()</emphasis> мы подняли вопрос о том, как устанавливать произвольные значения частоты дискретизации, Очевидно, создание большого количества констант DCMD_AUDIO_SET_SAMPLE_RATE_* было бы не самым оптимальным решением — у нас бы просто не хватило разрядности поля <emphasis>dcmd</emphasis>.</p>
     <p>С клиентской стороны мы будем использовать указатель на частоту дискретизации, <emphasis>dev_data_ptr</emphasis>, которую мы просто передадим как целое, поэтому поле <emphasis>nbytes</emphasis> будет содержать число байт в целом числе (для 32-разрядных машин это 4). Будем предполагать, что для этих целей определена константа DCMD_AUDIO_SET_SAMPLE_RATE.</p>
     <p>Также неплохо было бы уметь считывать текущее значение частоты дискретизации. Для этого мы также будем использовать переменные <emphasis>dev_data_ptr</emphasis> и <emphasis>nbytes</emphasis>, как описано выше, но в обратном направлении — администратор ресурсов запишет <emphasis>nbytes</emphasis> данных в ячейку памяти, на которую указывает <emphasis>dev_data_ptr</emphasis>, вместо чтения данных из этой ячейки. Предположим, что для этого определена константа DCMD_AUDIO_GET_SAMPLE_RATE.</p>
     <p>Давайте посмотрим, что в таком случае происходит в функции <emphasis>io_devctl()</emphasis> администратора ресурсов (того, что обсуждалось в предыдущем примере, мы здесь касаться не будем):</p>
     <p><code>/*</code></p>
     <p><code> * io_devctl2.с</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;stdlib.h&gt;</code></p>
     <p><code>#include &lt;unistd.h&gt;</code></p>
     <p><code>#include &lt;errno.h&gt;</code></p>
     <p><code>#include &lt;devctl.h&gt;</code></p>
     <p><code>#include &lt;sys/neutrino.h&gt;</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <empty-line/>
     <p><code>#define DCMD_AUDIO_SET_SAMPLE_RATE 1</code></p>
     <p><code>#define DCMD_AUDIO_GET_SAMPLE_RATE 2</code></p>
     <empty-line/>
     <p><code>int io_devctl(resmgr_context_t *ctp, io_devctl_t *msg,</code></p>
     <p><code> iofunc_ocb_t *ocb) {</code></p>
     <p><code> int  sts;</code></p>
     <p><code> void *data;</code></p>
     <p><code> int  nbytes;</code></p>
     <p><code> if ((sts =</code></p>
     <p><code>  iofunc_devctl_default(ctp, msg, ocb)) !=</code></p>
     <p><code>   _RESMGR_DEFAULT) {</code></p>
     <p><code>  return (sts);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 1) Установить указатель на область данных сообщения</code></p>
     <p><code> data = _DEVCTL_DATA(msg);</code></p>
     <empty-line/>
     <p><code> // 2) Установить число возвращаемых байт в 0</code></p>
     <p><code> nbytes = 0;</code></p>
     <p><code> // Проверить все команды; покажем только те, которые нам</code></p>
     <p><code> // здесь интересны</code></p>
     <p><code> switch (msg-&gt;i.dcmd) {</code></p>
     <p><code>  ...</code></p>
     <empty-line/>
     <p><code> // 3) Обработать команду SET</code></p>
     <p><code> case DCMD_AUDIO_SET_SAMPLE_RATE:</code></p>
     <p><code>  audio_set_samplerate(*(int*)data);</code></p>
     <p><code>  break;</code></p>
     <p><code> // 4) Обработать команду GET</code></p>
     <p><code> case DCMD_AUDIO_GET_SAMPLE_RATE:</code></p>
     <p><code>  *(int*)data = audio_get_samplerate();</code></p>
     <p><code>  nbytes = sizeof(int);</code></p>
     <p><code>  break;</code></p>
     <p><code>  ...</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // 5) Возвратить данные (если есть) клиенту</code></p>
     <p><code> memset(&amp;msg-&gt;о, 0, sizeof(msg-&gt;о));</code></p>
     <p><code> msg-&gt;о.nbytes = nbytes;</code></p>
     <p><code> SETIOV(ctp-&gt;iov, &amp;msg-&gt;o, sizeof(msg-&gt;o) + nbytes);</code></p>
     <p><code> return (_RESMGR_NPARTS(1));</code></p>
     <p><code>}</code></p>
     <subtitle>Этап 1</subtitle>
     <p>В «шапке» мы декларировали указатель типа <code><strong>void*</strong></code> по имени <emphasis>data</emphasis> («данные»), которые мы будем использовать в качестве универсального указателя на область данных. Если вы обратитесь к приведенному выше описанию <emphasis>io_devctl()</emphasis>, то вы увидите, что структура данных состоит из объединения заголовков входной и выходной структур, за которым неявно следует область данных. На этапе 1 указатель на эту область данных возвращается макросом <emphasis>_DEVCTL_DATA()</emphasis>.</p>
     <subtitle>Этап 2</subtitle>
     <p>Здесь мы должны указать, сколько байт мы собираемся возвратить клиенту. Я для удобства обнулил переменную <emphasis>nbytes перед</emphasis> выполнением каких-либо действий — теперь мне не придется принудительно обнулять ее в каждой ветви <code><strong>switch</strong></code>/<code><strong>case</strong></code>.</p>
     <subtitle>Этап 3</subtitle>
     <p>Пришло время для команды «set» («установить»). Мы вызываем фиктивную функцию <emphasis>audio_set_samplerate()</emphasis> и передаем ей значение частоты дискретизации, полученное разыменованием указателя <emphasis>data</emphasis> (который мы коварно выставили указателем на целое число... нет, никакого коварства, обычный для Си прием приведения типов). Это ключевой механизм, потому что это и есть наш способ «интерпретации» области данных (клиентского указателя <emphasis>dev_data_ptr</emphasis>) в соответствии с командой. В более сложном случае вы, наверное, выполнили бы приведение его типа к какой-нибудь структуре побольше вместо простого целого числа. Очевидно, что описания этой структуры на стороне как клиента, так и администратора ресурсов, должны быть идентичными, поэтому лучшим местом для описания такой структуры является заголовочный файл, в котором хранятся ваши командные константы DCMD_*</p>
     <subtitle>Этап 4</subtitle>
     <p>Обработка команды «get» («получить») на этапе 4 во многом аналогична (по части приведения типов), кроме того, что на этот раз мы записываем данные в структуру вместо считывания из нее. Заметьте, что мы также присваиваем переменной <emphasis>nbytes</emphasis> число байт, которые мы хотим возвратить клиенту. В случае более сложного доступа к данным вы должны были бы возвратить размер области данных (т.е. если бы эта область была бы структурой, вам нужно было бы возвратить ее размер).</p>
     <subtitle>Этап 5</subtitle>
     <p>Наконец, для возврата данных клиенту мы должны вспомнить, что клиент ожидает не только возвращаемые данные (если таковые имеются), но и заголовочную структуру, за которой идут эти данные. Поэтому на этом этапе мы обнуляем заголовочную структуру и устанавливаем число байт (поле <emphasis>nbytes</emphasis>) в число байт, которые мы намерены возвратить (вспомните, мы обнуляли это значение ранее). Затем мы создаем одноэлементный вектор ввода/ вывода с указателем на заголовок и расширяем размер заголовка на число возвращаемых байт. В конце мы просто сообщаем библиотеке администратора ресурсов, что мы возвращаем клиенту одноэлементный вектор ввода/вывода.</p>
     <subtitle>Важное замечание</subtitle>
     <p>Вспомните рассуждения про следующую за заголовком область данных из примера <emphasis>io_write()</emphasis>, приведенного выше. Мы утверждали, что байты, расположенные сразу после заголовка, <emphasis>могут как быть полноценными, так и нет</emphasis> (то есть возможны случаи, когда область данных со стороны клиента была считана лишь частично) — в зависимости от того, сколько данных считала библиотека администратора ресурсов. Затем мы говорили о том, что было бы неэффективно пытаться «сэкономить» лишнюю операцию обмена сообщениями и «повторно использовать» область данных. Однако, в случае с <emphasis>devctl()</emphasis> все обстоит несколько иначе, особенно если количество передаваемых данных достаточно невелико (как было и в наших примерах). Здесь у нас есть неплохой шанс того, что данные от клиента были-таки считаны в область данных целиком, и тогда повторное их считывание будет напрасной тратой сил. Узнать, сколько у вас доступно пространства, очень просто: поле <emphasis>size</emphasis> («размер») структуры <emphasis>ctp</emphasis> содержит число байт, доступных для вас, начиная с параметра <emphasis>msg</emphasis>. Размер доступной области данных, расположенной за буфером сообщений, вычисляется как разность между размером буфера сообщений и полем size структуры <emphasis>ctp</emphasis>:</p>
     <p><code>data_area_size = ctp-&gt;size - sizeof(*msg);</code></p>
     <p>Отметим, что этот размер будет действителен также и в случае <emphasis>возврата</emphasis> данных клиенту (как при команде DCMD_AUDIO_GET_SAMPLE_RATE).</p>
     <p>Для всего, что превосходит по размеру выделенную область, вам придется получать данные от клиента так же, как мы это делали в примере с <emphasis>io_write()</emphasis> (см. выше), а также выделить буфер для возврата данных клиенту.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Дополнительно</p>
    </title>
    <section>
     <p>Теперь, после того как мы овладели «основами» построения администраторов ресурсов, пришло время рассмотреть более сложные вопросы. К ним относятся:</p>
     <p>• расширение OCB;</p>
     <p>• расширение атрибутной записи;</p>
     <p>• блокирование в пределах администратора ресурсов;</p>
     <p>• возврат элементов каталога.</p>
    </section>
    <section>
     <title>
      <p>Расширение OCB</p>
     </title>
     <p>В ряде случаев у вас может возникнуть необходимость расширения OCB. Процедура эта является относительно безболезненной. Обычно OCB расширяют дополнительными флагами, характеризующими каждый конкретный <emphasis>open()</emphasis>. Один такой флаг можно было бы использовать с обработчиком <emphasis>io_unblock()</emphasis> для кэширования значения флага ядра _NTO_MI_UNBLOCK_REQ (подробнее см. параграф «Применение флага _NTO_MI_UNBLOCK_REQ» в главе «Обмен сообщениями»).</p>
     <p>Для расширения блока OCB вам нужно будет обеспечить две дополнительных функции: одну для выделения OCB, и одну — для его освобождения. Затем вы должны будете привязать эти две функции к записи точки монтирования. (Да-да, совершенно верно — вам понадобится запись точки монтирования, даже если только для этого.) И наконец, вы должны будете определить ваш собственный тип OCB, чтобы все прототипы в программе были корректны.</p>
     <p>Давайте рассмотрим сначала описание типа OCB, а затем уже поглядим, как переопределяются функции:</p>
     <p><code>#define IOFUNC_OCB_T struct my_ocb</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <p>Это сообщает включаемому файлу <code><strong>&lt;sys/iofunc.h&gt;</strong></code>, что именованная константа IOFUNC_OCB_T теперь указывает на вашу новую усовершенствованную структуру OCB.</p>
     <cite>
      <p><emphasis><image l:href="#img_1.png"/>Очень важно</emphasis> иметь в виду, что ваш «расширенный» OCB <emphasis>должен</emphasis> содержать «стандартный» OCB в качестве своего первого элемента! Это так, потому что вспомогательная библиотека POSIX везде передает указатель на то, что она считает стандартным OCB — о вашем расширенном OCB ей ничего не известно, так что первый элемент данных, расположенный по этому указателю, должен соответствовать стандартному OCB.</p>
     </cite>
     <p>Вот наш расширенный OCB:</p>
     <p><code>typedef struct my_ocb {</code></p>
     <p><code> iofunc_ocb_t <emphasis>normal_ocb</emphasis>;</code></p>
     <p><code> int          <emphasis>my_extra_flags</emphasis>;</code></p>
     <p><code> ...</code></p>
     <p><code>} my_ocb_t;</code></p>
     <p>А вот код, иллюстрирующий, как переопределяются функции выделения и освобождения OCB в записи точки монтирования:</p>
     <p><code>// Декларации</code></p>
     <p><code>iofunc_mount_t mount;</code></p>
     <p><code>iofunc_funcs_t mount_funcs;</code></p>
     <empty-line/>
     <p><code>// Задать в записи точки монтирования</code></p>
     <p><code>// наши функции выделения/освобождения</code></p>
     <p><code>// _IOFUNC_NFUNCS взята из .h-файла</code></p>
     <p><code>mount_funcs.nfuncs = _IOFUNC_NFUNCS;</code></p>
     <empty-line/>
     <p><code>// Новая функция выделения OCB</code></p>
     <p><code>mount_funcs.ocb_calloc = my_ocb_calloc;</code></p>
     <empty-line/>
     <p><code>// Новая функция освобождения OCB</code></p>
     <p><code>mount_funcs.ocb_free = my_ocb_free;</code></p>
     <empty-line/>
     <p><code>// Настроить запись точки монтирования</code></p>
     <p><code>memset(&amp;mount, 0, sizeof(mount));</code></p>
     <p>После этого остается только привязать запись точки монтирования к атрибутной записи:</p>
     <p><code>...</code></p>
     <p><code>attr.mount = &amp;mount;</code></p>
     <p>Функции <emphasis>my_ocb_calloc()</emphasis> и <emphasis>my_ocb_free()</emphasis> отвечают за выделение обнуленного расширенного OCB и освобождения OCB, соответственно. Вот их прототипы:</p>
     <p><code>IOFUNC_OCB_T* my_ocb_calloc(resmgr_context_t *<emphasis>ctp</emphasis>,</code></p>
     <p><code> IOFUNC_ATTR_T *<emphasis>attr</emphasis>);</code></p>
     <empty-line/>
     <p><code>void my_ocb_free(IOFUNC_OCB_T *<emphasis>ocb</emphasis>);</code></p>
     <p>Это означает, что функции <emphasis>my_ocb_calloc()</emphasis> передаются одновременно и внутренний контекст администратора ресурсов, и атрибутная запись. Функция отвечает за возврат обнуленного OCB. Функция <emphasis>my_ocb_free()</emphasis> получает OCB и отвечает за освобождение выделенной под него памяти.</p>
     <p>Для этих двух функций имеются два интересных применения (которые ничем не связаны с выполнением расширения блока OCB):</p>
     <p>• контроль распределения/освобождения блока OCB;</p>
     <p>• обеспечение более эффективного распределения/ освобождения</p>
     <subtitle>Контроль за OCB</subtitle>
     <p>В этом случае вы можете просто «подключиться» к функциям распределения/освобождения и контролировать использование OCB (например, вам может быть необходимо ограничить суммарное количество OCB). Это может оказаться полезным, если вы не перехватываете функцию <emphasis>io_open()</emphasis>, но создание (и, возможно, удаление) OCB все-таки хотите контролировать.</p>
     <subtitle>Более эффективное распределение</subtitle>
     <p>Другое применение для переопределения встроенных библиотечных функций распределения/освобождения OCB может заключаться в том, что вы можете захотеть хранить OCB в свободном списке вместо использования библиотечных <emphasis>calloc()</emphasis> и <emphasis>free()</emphasis>. Если вы распределяете и освобождаете OCB с большой частотой, это может оказаться более эффективно.</p>
    </section>
    <section>
     <title>
      <p>Расширение атрибутной записи</p>
     </title>
     <p>Вы можете захотеть расширить атрибутную запись в случаях, когда вам необходимо хранить дополнительную информацию об устройствах. Поскольку атрибутные записи создаются «по каждому устройству», это означает, что любая дополнительная информация, которую вы сохраните там, будет доступна для всех OCB, относящихся к этому устройству (поскольку OCB содержит указатель на атрибутную запись). В расширенных атрибутных записях часто хранятся такие параметры как скорость передачи данных по последовательному каналу, и т.п.</p>
     <p>Расширять атрибутную запись намного проще, чем OCB, потому что атрибутные записи в любом случае распределяются и освобождаются вашим кодом.</p>
     <p>Вам нужно будет выполнить тот же трюк с переопределением атрибутной записи в заголовочных файлах, как мы это делали ранее при расширении OCB:</p>
     <p><code>#define IOFUNC_ATTR_T struct my_attr</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <p>Затем вы фактически определяете содержимое ваших расширенных атрибутных записей. Отметьте, что расширенная атрибутная запись <emphasis>должна</emphasis> включать в себя стандартную атрибутную запись <emphasis>первым</emphasis> элементом — аналогично случаю с расширением OCB (и по тем же самым причинам).</p>
    </section>
    <section>
     <title>
      <p>Блокирование в пределах администратора ресурсов</p>
     </title>
     <p>До настоящего момента мы избегали разговоров о возможности блокирования в пределах администратора ресурсов. Мы предполагали, что у нас есть функция-обработчик (например, <emphasis>io_read()</emphasis>), и что данные будут доступны немедленно. А что если нам придется блокироваться в ожидании данных? Например, выполнение <emphasis>read()</emphasis> применительно к последовательному порту может потребовать блокирования до приема символа. Очевидно, что мы не можем предсказать, сколько может продолжаться такое ожидание.</p>
     <p>Блокирование в пределах администратора ресурсов базируется на тех же самых принципах, которые мы обсуждали в главе «Обмен сообщениями» — в конце концов, администратор ресурса фактически является сервером, который обрабатывает рад четко определенных сообщений. Когда прибывает сообщение, соответствующее клиентскому запросу <emphasis>read()</emphasis>, оно прибывает вместе с идентификатором отправителя (receive ID), и клиент блокируется. Если у администратора ресурсов есть данные, он просто возвращает их клиенту, как мы уже видели в различных приведенных ранее примерах. Однако, если данные недоступны, администратор ресурсов должен будет удерживать этого клиента в заблокированном состоянии (конечно, если клиент для этой операции определил блокирующий режим), чтобы иметь возможность продолжить обработку других сообщений. Реально это означает, что поток администратора ресурсов, который принял сообщение от клиента, <emphasis>не должен блокироваться</emphasis> в ожидании данных — в противном случае это может закончиться для администратора ресурсов огромным числом заблокированных потоков, каждый из которых ожидал бы данные от некоего устройства.</p>
     <p>Правильным решением для этой проблемы является сохранение идентификатора отправителя полученного сообщения в какой-нибудь очереди и возврат из вашего обработчика константы _RESMGR._NOREPLY. Это укажет библиотеке администратора ресурсов, что обработка сообщения закончена, но клиента пока разблокировать не надо.</p>
     <p>Несколько позже, по готовности данных, вы сможете выбрать идентификатор отправителя нужного клиента и сконструировать ответное сообщение с данными. После этого можно отвечать клиенту.</p>
     <p>Вы могли бы также расширить эту концепцию добавлением тайм-аутов, как мы это делали в главе «Часы, таймеры и периодические уведомления» (параграф «Поддерживаемые сервером тайм-ауты»). Если говорить вкратце, там по истечении некоторого интервала времени клиентский запрос считался «просроченным», и сервер отвечал по сохраненному по идентификатору отправителя неким сообщением об ошибке.</p>
    </section>
    <section>
     <title>
      <p>Возврат элементов каталога</p>
     </title>
     <p>В приведенном ранее примере функции <emphasis>io_read()</emphasis> мы уже видели, как происходит возврат данных. Как было упомянуто в описании функции <emphasis>io_read()</emphasis> (в разделе «Алфавитный список функций установления соединения и ввода-вывода»), <emphasis>io_read()</emphasis> можно возвращать и элементы каталога тоже. Поскольку это может понадобиться далеко не всем, я решил рассмотреть этот вопрос здесь отдельно.</p>
     <p>Прежде всего давайте посмотрим, <emphasis>почему и когда</emphasis> вашей <emphasis>io_read()</emphasis> могло бы понадобиться возвращать элементы каталога, а не «сырые» данные.</p>
     <p>Если вы дискретно объявляете элементы в пространстве имени путей, и эти элементы <emphasis>не</emphasis> помечены флагом _RESMGR_FLAG_DIR, тогда вам не придется возвращать элементы каталога из функции <emphasis>io_read()</emphasis>. Если рассматривать это как «файловую систему», то ваш объект будет «файлом». Если же, с другой стороны, вы <emphasis>указываете</emphasis> _RESMGR_FLAG_DIR, то будет создан объект типа «каталог». Никто, кроме вас, не знает ничего о содержимом этого каталога, поэтому вы должны будете предоставить эти данные. Это и есть ответ на вопрос, почему функции <emphasis>io_read()</emphasis> может понадобиться возвращать элементы каталогов.</p>
     <subtitle>Вообще говоря…</subtitle>
     <p>Вообще говоря, возврат элементов каталога — это почти то же самое, что и возврат «сырых» данных, за исключением того, что:</p>
     <p>• вы должны возвратить <emphasis>целое</emphasis> число структур типа <code><strong>struct dirent</strong></code>;</p>
     <p>• эти структуры <code><strong>struct dirent</strong></code> должны быть заполнены.</p>
     <p>Первый пункт означает, что вы не можете возвратить, например, семь с половиной структур <code><strong>struct dirent</strong></code>. Если восемь структур не вписываются в выделенное пространство, то вы должны будете возвратить только семь элементов.</p>
     <p>Второй пункт достаточно очевиден. Он упомянут здесь только потому, что заполнение структуры <code><strong>struct dirent</strong></code> может быть несколько «хитрее», чем «сырой» подход к данным в случае с «обычной» <emphasis>io_read()</emphasis>.</p>
     <subtitle>Структура <code>struct dirent</code> и ее друзья</subtitle>
     <p>Давайте взглянем на структуру <code><strong>struct dirent</strong></code>, поскольку это именно то, что возвращает функция <emphasis>io_read()</emphasis> в случае чтения каталога. Мы также вкратце рассмотрим клиентские вызовы, имеющие дело с элементами каталогов, поскольку у них со структурой <code><strong>struct dirent</strong></code> существует ряд интересных взаимоотношений.</p>
     <p>Чтобы работать с каталогами, клиент использует функции <emphasis>closedir()</emphasis>, <emphasis>opendir()</emphasis>, <emphasis>readdir()</emphasis>, <emphasis>rewinddir()</emphasis>, <emphasis>seekdir()</emphasis> и <emphasis>telldir()</emphasis>.</p>
     <p>Обратите внимание на сходство с «нормальными» функций для файлов (и совпадение применяемых типов сообщений):</p>
     <table>
      <tr align="left">
       <th align="left" valign="top">Функция для работы с каталогами</th>
       <th align="left" valign="top">Функция для работы с файлами</th>
       <th align="left" valign="top">Сообщение</th>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>closedir()</emphasis></td>
       <td align="left" valign="top"><emphasis>close()</emphasis></td>
       <td align="left" valign="top">IO_CLOSE_DUP</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>opendir()</emphasis></td>
       <td align="left" valign="top"><emphasis>open()</emphasis></td>
       <td align="left" valign="top">_IO_CONNECT</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>readdir()</emphasis></td>
       <td align="left" valign="top"><emphasis>read()</emphasis></td>
       <td align="left" valign="top">_IO_READ</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>rewinddir()</emphasis></td>
       <td align="left" valign="top"><emphasis>lseek()</emphasis></td>
       <td align="left" valign="top">_IO_LSEEK</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>seekdir()</emphasis></td>
       <td align="left" valign="top"><emphasis>lseek()</emphasis></td>
       <td align="left" valign="top">_IO_LSEEK</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>telldir()</emphasis></td>
       <td align="left" valign="top"><emphasis>tell()</emphasis></td>
       <td align="left" valign="top">_IO_LSEEK</td>
      </tr>
     </table>
     <empty-line/>
     <p>Если мы на мгновение вообразим, что функции <emphasis>opendir()</emphasis> и <emphasis>closedir()</emphasis> будут обработаны для нас автоматически, мы сможем сконцентрироваться только на сообщениях типа _IO_READ и _IO_LSEEK и на соответствующих им функциях.</p>
     <subtitle>Смещения</subtitle>
     <p>Сообщение _IO_LSEEK и соответствующая ему функция применяются для «поиска» (или «перемещения») в пределах файла. С каталогом происходит та же история. Вы можете переместиться к «первому» элементу каталога (как явно задав смещение функции <emphasis>seekdir()</emphasis>, так и вызвав функцию <emphasis>rewinddir()</emphasis>) переместиться к любому произвольному элементу (используя функцию <emphasis>seekdir()</emphasis>), или же узнать свою текущую позицию в списке элементов каталога (вызвав функцию <emphasis>telldir()</emphasis>).</p>
     <p>Однако, «хитрость» при работе с каталогами состоит в том, что <emphasis>смещения задаете вы сами, и управление ими тоже всецело лежит на вас</emphasis>. Это означает, что вы можете назначать смещения элементов в каталоге равными как «0», «2» и так далее, так и «0», «64», «128» и так далее. Единственно, что здесь необходимо предусмотреть — чтобы обработчики <emphasis>io_lseek()</emphasis> и <emphasis>io_read()</emphasis> одинаково трактовали эти смещения.</p>
     <p>В приведенном ниже примере мы предположим, что используется простой подход с номерами «0», «1», «2», и т.д. (Вы могли бы использовать «0», «64», «128», и т.д., если бы эти числа соответствовали, например, неким смещениям на носителе. Выбор за вами!)</p>
     <subtitle>Содержимое</subtitle>
     <p>Ну вот, остается «просто» заполнить <code><strong>struct dirent</strong></code> «содержимым» нашего каталога. Структура <code><strong>struct dirent</strong></code> выглядит так (взято из <code><strong>&lt;dirent.h&gt;</strong></code>):</p>
     <p><code>struct dirent {</code></p>
     <p><code> ino_t    <emphasis>d_ino</emphasis>;</code></p>
     <p><code> off_t    <emphasis>d_offset</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>d_reclen</emphasis>;</code></p>
     <p><code> uint16_t <emphasis>d_namelen</emphasis>;</code></p>
     <p><code> char     <emphasis>d_name</emphasis>[1];</code></p>
     <p><code>};</code></p>
     <p>Коротко о ее полях:</p>
     <table>
      <tr align="left">
       <td align="left" valign="top"><emphasis>d_ino</emphasis></td>
       <td align="left" valign="top">«Индексный дескриптор» («inode») — уникальный для точки монтирования порядковый номер, который не может быть нулевым (нуль указывал бы на то, что элемент, соответствующий данному индексному дескриптору, является свободным/пустым).</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>d_offset</emphasis></td>
       <td align="left" valign="top">Смещение в каталоге, о котором мы только что говорили. В нашем примере это будут обычные числа типа «0», «1», «2», и т.д.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>d_reclen</emphasis></td>
       <td align="left" valign="top">Размер структуры <code><strong>struct dirent</strong></code> целиком, включая любые добавляемые в нее расширения. Заполнители для выравнивания при вычислении размера учитываются.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>d_namelen</emphasis></td>
       <td align="left" valign="top">Число символов в поле <emphasis>d_name</emphasis>, не включая признак конца строки NULL.</td>
      </tr>
      <tr align="left">
       <td align="left" valign="top"><emphasis>d_name</emphasis></td>
       <td align="left" valign="top">Имя элемента каталога, которое должно завершаться признаком конца строки — NULL.</td>
      </tr>
     </table>
     <p>При возврате структур типа <code><strong>struct dirent</strong></code> код возврата, который передается клиенту, представляет собой число возвращенных байт.</p>
     <subtitle>Пример</subtitle>
     <p>Давайте для примера создадим администратора каталогового ресурса <code><strong>/dev/atoz</strong></code>. Этот администратор зарегистрирует «файлы» с именами от <code><strong>/dev/atoz/a</strong></code> до <code><strong>/dev/atoz/z</strong></code>, чтобы команда cat, примененная к любому из них, возвращала соответствующие их именам заглавные буквы. Чтобы понять, как это должно работать, вот пример командно-строковой сессии:</p>
     <p><code># cd /dev</code></p>
     <empty-line/>
     <p><code># ls</code></p>
     <p><code>atoz  null  ptyp2 socket ttyp0 ttyp3</code></p>
     <p><code>enet0 ptyp0 ptyp3 text   ttyp1 zero</code></p>
     <p><code>mem   ptyp1 shmem tty    ttyp2</code></p>
     <empty-line/>
     <p><code># ls -ld atoz</code></p>
     <p><code>dr-xr-xr-x 1 root 0 26 Sep 05 07:59 atoz</code></p>
     <empty-line/>
     <p><code># cd atoz</code></p>
     <p><code># ls</code></p>
     <p><code>a  e  i  m  q  u  y</code></p>
     <p><code>b  f  j  n  r  v  z</code></p>
     <p><code>c  g  k  o  s  w</code></p>
     <p><code>d  h  l  p  t  x</code></p>
     <empty-line/>
     <p><code># ls -l e</code></p>
     <p><code>-r--r--r-x 1 root 0 1 Sep 05 07:59 e</code></p>
     <empty-line/>
     <p><code># cat m</code></p>
     <p><code>M# cat q</code></p>
     <p><code>Q#</code></p>
     <p>Приведенный пример показывает, что в каталоге <code><strong>/dev</strong></code> есть каталог <code><strong>atoz</strong></code>, и что к нему можно применить команду <code><strong>ls</strong></code> и выполнить в него <code><strong>cd</strong></code>. Данный каталог <code><strong>/dev/atoz</strong></code> имеет размер «26» — мы так задали в нашей программе. Сменив текущий каталог на <code><strong>atoz</strong></code> и выполнив еще одну <code><strong>ls</strong></code>, получаем его содержимое — файлы с именами от <code><strong>а</strong></code> до <code><strong>z</strong></code>. Выполнив <code><strong>ls</strong></code> для отдельного файла — в данном случае для файла <code><strong>e</strong></code> — мы видим, что файл доступен по чтению всем (часть «<code><strong>-r--r--r--</strong></code>») и имеет размер, равный 1 байту. Наконец, выполнение нескольких <code><strong>cat</strong></code> показывает, что файлы действительно имеют заявленное содержимое. (Отметим, что поскольку файлы содержат только один байт и не содержат символа новой строки, после вывода символа строка не переводится, и приглашение командного интерпретатора оказывается на той же самой строке, что и вывод <code><strong>cat</strong></code>.)</p>
     <p>Теперь, когда мы знаем нужные характеристики, давайте посмотрим на код. Он разбит на следующие функции:</p>
     <p><emphasis>main()</emphasis> и декларации</p>
     <p>Основная функция; здесь мы все инициализируем и запускаем наш администратор ресурса.</p>
     <p><emphasis>my_open()</emphasis></p>
     <p>Обработчик сообщения _IO_CONNECT.</p>
     <p><emphasis>my_read()</emphasis></p>
     <p>Обработчик сообщения _IO_READ.</p>
     <p><emphasis>my_read_dir()</emphasis> и <emphasis>my_read_file()</emphasis></p>
     <p>Выполняют фактическую работу функции <emphasis>my_read()</emphasis>.</p>
     <p><emphasis>dirent_size()</emphasis> и <emphasis>dirent_fill()</emphasis></p>
     <p>Сервисные функции для работы со структурой <code>struct diren</code>t.</p>
     <p>Заметьте, что при том, что разбит на короткие секции, перемежаемые текстовыми пояснениями, архив с полным исходным текстом в виде единого файла можно взять на веб-сайте компании PARSE Software Devices (<code><strong>http://www.parse.com/</strong></code>), он называется <code><strong>atoz.c.</strong></code></p>
     <subtitle><emphasis>main()</emphasis> и декларации</subtitle>
     <p>Первый приведенный раздел программы представляет собой функцию <emphasis>main()</emphasis> и ряд деклараций. Для удобства объявлен макрос <emphasis>ALIGN()</emphasis>, который используется функциями <emphasis>dirent_size()</emphasis> и <emphasis>dirent_fill()</emphasis> для выравнивания.</p>
     <p>Массив <emphasis>atoz_attrs</emphasis> содержит атрибутные записи, используемые в этом примере для «файлов». Мы объявляем массив из NUM_ENTS элементов, потому что у нас есть NUM_ENTS (то есть 26) файлов — от «<code><strong>а</strong></code>» до «<code><strong>z</strong></code>». Атрибутная запись, применяемая непосредственно для каталога (то есть для <code><strong>/dev/atoz</strong></code>, объявлена в теле функции <emphasis>main()</emphasis> и называется просто <emphasis>attr</emphasis>. Обратите внимание на различное содержимое у этих двух типов атрибутных записей:</p>
     <p>Файловая атрибутная запись:</p>
     <p> Маркируется как обычный файл (константа S_IFREG) с режимом доступа 0444 (это означает, что доступ по чтению имеет каждый, но доступа по записи нет ни у кого). Размер равен «1» — файл содержит только один байт, а именно прописную букву, соответствующую своему имени. Индексные дескрипторы (inodes) этих файлов пронумерованы от «1» до «26» включительно (было бы удобнее взять числа от «0» до «25», но цифра «0» зарезервирована).</p>
     <p>Каталоговая атрибутная запись:</p>
     <p>Маркируется как файл типа «каталог» (константа S_IFDIR) с режимом доступа 0555 (это означает, что доступ по чтению и поиску имеет каждый, но доступа по записи нет ни у кого). Размер определен как «26» — это просто число, взятое по количеству элементов в каталоге. Индексный дескриптор выбран равным «27» — это число заведомо не используется больше ни в одной атрибутной записи.</p>
     <p>Обратите внимание, что мы переопределили только поле <emphasis>open</emphasis> структуры <emphasis>connect_func</emphasis> и поле <emphasis>read</emphasis> структуры <emphasis>io_func</emphasis>. Для всех остальных полей сохранены POSIX-значения по умолчанию.</p>
     <p>Наконец, обратите внимание, как мы создали имя <code><strong>/dev/atoz</strong></code>, используя <emphasis>resmgr_attach()</emphasis>. Наиболее важным здесь является то, что мы применили флаг _RESMGR_FLAG_DIR, который сообщает администратору процессов, что он может разрешать запросы <emphasis>на эту точку монтирования и ниже</emphasis>.</p>
     <p><code>/*</code></p>
     <p><code> * atoz.с</code></p>
     <p><code> *</code> </p>
     <p><code> * /dev/atoz с использованием библиотеки администратора ресурсов</code></p>
     <p><code>*/</code></p>
     <p><code>#include &lt;stdio.h&gt;</code></p>
     <p><code>#include &lt;stddef.h&gt;</code></p>
     <p><code>#include &lt;stdlib.h&gt;</code></p>
     <p><code>#include &lt;errno.h&gt;</code></p>
     <p><code>#include &lt;dirent.h&gt;</code></p>
     <p><code>#include &lt;limits.h&gt;</code></p>
     <p><code>#include &lt;sys/iofunc.h&gt;</code></p>
     <p><code>#include &lt;sys/dispatch.h&gt;</code></p>
     <empty-line/>
     <p><code>#define ALIGN(x) (((x) +3) &amp; ~3)</code></p>
     <p><code>#define NUM_ENTS 26</code></p>
     <empty-line/>
     <p><code>static iofunc_attr_t atoz_attrs[NUM_ENTS];</code></p>
     <empty-line/>
     <p><code>int main (int argc, char **argv) {</code></p>
     <p><code> dispatch_t *dpp;</code></p>
     <p><code> resmgr_attr_t resmgr_attr;</code></p>
     <p><code> resmgr_context_t *ctp;</code></p>
     <p><code> resmgr_connect_funcs_t connect_func;</code></p>
     <p><code> resmgr_io_funcs_t io_func;</code></p>
     <p><code> iofunc_attr_t attr;</code></p>
     <p><code> int i;</code></p>
     <empty-line/>
     <p><code> // Создать структуру диспетчеризации</code></p>
     <p><code> if ((dpp = dispatch_create()) == NULL) {</code></p>
     <p><code>  perror("Ошибка dispatch_create\n");</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // Инициализировать структуры данных</code></p>
     <p><code> memset(&amp;resmgr_attr, 0, sizeof(resmgr_attr));</code></p>
     <p><code> resmgr_attr.nparts_max = 1;</code></p>
     <p><code> resmgr_attr.msg_max_size = 2048;</code></p>
     <empty-line/>
     <p><code> // Назначить обработчики по умолчанию</code></p>
     <p><code> iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &amp;connect_func,</code></p>
     <p><code>  _RESMGR_IO_NFUNCS, &amp;io_func);</code></p>
     <empty-line/>
     <p><code> // Создать и инициализировать атрибутную запись для</code></p>
     <p><code> // каталога...</code></p>
     <p><code> iofunc_attr_init(&amp;attr, S_IFDIR | 0555, 0, 0);</code></p>
     <p><code> attr.inode = NUM_ENTS + 1; // 1-26 зарезервированы для</code></p>
     <p><code>                            // файлов от «a» до «z»</code></p>
     <p><code> attr.nbytes = NUM_ENTS; // 26 элементов в каталоге</code></p>
     <empty-line/>
     <p><code> // ...и для имен от «a» до «z»</code></p>
     <p><code> for (i = 0; i &lt; NUM_ENTS; i++) {</code></p>
     <p><code>  iofunc_attr_init(&amp;atoz_attrs[i], S_IFREG | 0444, 0, 0);</code></p>
     <p><code>  atoz_attrs[i].inode = i + 1;</code></p>
     <p><code>  atoz_attrs[i].nbytes = 1;</code></p>
     <p><code> }</code></p>
     <p><code> // Добавить наши функции; нам интересны только io_open</code></p>
     <p><code> // и io_read</code></p>
     <p><code> connect_func.open = my_open;</code></p>
     <p><code> io_func.read = my_read;</code></p>
     <empty-line/>
     <p><code> // Зарегистрировать префикс</code></p>
     <p><code> if (resmgr_attach(dpp, &amp;resmgr_attr, "/dev/atoz",</code></p>
     <p><code>  _FTYPE_ANY, _RESMGR_FLAG_DIR, &amp;connect_func,</code></p>
     <p><code>  &amp;io_func, &amp;attr) == -1) {</code></p>
     <p><code>  perror("Ошибка resmgr_attach\n");</code></p>
     <p><code>  exit(EXIT_FAILURE);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // Выделить контекст</code></p>
     <p><code> ctp = resmgr_context_alloc(dpp);</code></p>
     <empty-line/>
     <p><code> // Ждать сообщений в вечном цикле</code></p>
     <p><code> while (1) {</code></p>
     <p><code>  if ((ctp = resmgr_block(ctp)) == NULL) {</code></p>
     <p><code>   perror("Ошибка resmgr_block\n");</code></p>
     <p><code>   exit(EXIT_FAILURE);</code></p>
     <p><code>  }</code></p>
     <p><code>  resmgr_handler(ctp);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>my_open()</emphasis></subtitle>
     <p>При том, что функция <emphasis>my_open()</emphasis> очень невелика, в ней есть ряд критических мест. Обратите внимание, как мы принимаем решение о том, был ли ресурс открыт как «файл» или как «каталог», на основе только длины имени пути. Мы можем себе это позволить, потому что знаем, что других каталогов, кроме основного, в этом администраторе ресурсов нет. Если вы захотите расположить ниже вашей точки монтирования дополнительные каталоги, вам придется применить более сложный механизм анализа поля <emphasis>path</emphasis> структуры <emphasis>msg</emphasis>. В нашем простом примере, если в имени пути ничего нет, то мы знаем, что это каталог. Также обратите внимание на чрезвычайно упрощенную проверку корректности имени пути: мы просто проверяем, что у нас действительно только один символ, и что он лежит в диапазоне от «a» до «z» включительно. Опять же, в случае более сложного администратора ресурсов вам пришлось бы выполнять синтаксический анализа имени, следующего за зарегистрированной точкой монтирования.</p>
     <p>Теперь о наиболее важной особенности. Обратите внимание, как мы использовали для выполнения всей нашей работы функции POSIX-уровня по умолчанию! Функция <emphasis>iofunc_open_default()</emphasis> обычно размещается в таблице функций установления соединения в той же самой ячейке, которую сейчас занимает наша функция <emphasis>my_open()</emphasis>. Это означает, что они принимают одинаковый набор аргументов!</p>
     <p>Все, что мы должны сделать — это решить, <emphasis>какую</emphasis> атрибутную запись мы хотим связать с OCB, создаваемым функцией по умолчанию: либо каталоговую (в этом случае мы передаем <emphasis>attr</emphasis>), либо одну из 26 имеющихся у нас файловых (тогда мы передаем соответствующий элемент <emphasis>atoz_attrs</emphasis>). Это ключевой момент, поскольку обработчик, который вы помещаете в слот <emphasis>open</emphasis> в таблице функций установления соединения, действует как «швейцар» по отношению ко всем последующим запросам к вашему администратору ресурса.</p>
     <p><code>static int my_open(resmgr_context_t *ctp, io_open_t *msg,</code></p>
     <p><code> iofunc_attr_t *attr, void *extra) {</code></p>
     <p><code> if (msg-&gt;connect.path[0] == 0) {</code></p>
     <p><code>  // Каталог (/dev/atoz)</code></p>
     <p><code>  return (iofunc_open_default(ctp, msg, attr, extra));</code></p>
     <p><code> } else if (msg-&gt;connect.path[1] == 0 &amp;&amp;</code></p>
     <p><code>  (msg-&gt;connect.path[0] &gt;= 'a' &amp;&amp;</code></p>
     <p><code>  msg-&gt;connect.path[0] &lt;= 'z')) { // Файл (/dev/atoz/[a-z])</code></p>
     <p><code>  return</code></p>
     <p><code>   (iofunc_open_default(ctp, msg, atoz_attrs +</code></p>
     <p><code>    msg-&gt;connect.path[0] - 'a', extra));</code></p>
     <p><code> } else {</code></p>
     <p><code>  return (ENOENT);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>my_read()</emphasis></subtitle>
     <p>В функции <emphasis>my_read()</emphasis>, чтобы решить, какие операции надо выполнить, мы анализируем поле mode атрибутной записи. Если макрос <emphasis>S_ISDIR()</emphasis> говорит, что это каталог, мы вызываем функцию <emphasis>my_read_dir()</emphasis>; если макрос <emphasis>S_ISREG()</emphasis> говорит, что это файл, мы вызываем функцию <emphasis>my_read_file()</emphasis>. (Отметим, что если мы не можем разобрать, что это такое, мы возвращаем клиенту EBADF, указывая ему этим, что что-то здесь не так.)</p>
     <p>Приведенный ниже код ничего не знает о наших специальных устройствах, да ему, собственно, и дела до них нет — он просто принимает решение на основе стандартных общеизвестных данных.</p>
     <p><code>static int my_read(resmgr_context_t *ctp, io_read_t *msg,</code></p>
     <p><code> iofunc_ocb_t *ocb) {</code></p>
     <p><code> int sts;</code></p>
     <empty-line/>
     <p><code> // Использовать вспомогательную функцию для проверки</code></p>
     <p><code> // корректности</code></p>
     <p><code> if ((sts =</code></p>
     <p><code>  iofunc_read_verify(ctp, msg, ocb, NULL)) != EOK) {</code></p>
     <p><code>   return (sts);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // Решить, надо ли читать «файл» или «каталог»</code></p>
     <p><code> if (S_ISDIR(ocb-&gt;attr-&gt;mode)) {</code></p>
     <p><code>  return (my_read_dir(ctp, msg, ocb));</code></p>
     <p><code> } else if (S_ISREG(ocb-&gt;attr-&gt;mode)) {</code></p>
     <p><code>  return (my_read_file(ctp, msg, ocb));</code></p>
     <p><code> } else {</code></p>
     <p><code>  return (EBADF);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>my_read_dir()</emphasis></subtitle>
     <p>Вот тут-то все веселье и начинается. С точки зрения высокого уровня, мы выделяем буфер (он называется <emphasis>reply_msg</emphasis>), в котором собираемся разместить результат операции. Затем мы используем <emphasis>dp</emphasis>, чтобы «прогуляться» по буферу, заполняя его по ходу дела элементами <code><strong>struct dirent</strong></code>. Вспомогательная подпрограмма <emphasis>dirent_size()</emphasis> применяется, чтобы определить, есть ли у нас место в буфере для еще одного элемента. Вспомогательная подпрограмма <emphasis>dirent_fill()</emphasis> кладет элемент в буфер. (Отметим, что эти подпрограммы не входят в библиотеку администратора ресурсов; мы обсудим их ниже.)</p>
     <p>На первый взгляд этот код может показаться неэффективным; мы используем функцию <emphasis>sprintf()</emphasis> для создания двухбайтового имени файла (символ имени файла и признак конца строки NULL) в буфере длиной _POSIX_PATH_MAX (то есть 256) байт. Это делается для того, чтобы сделать код по возможности универсальным.</p>
     <p>Наконец, обратите внимание, что мы используем поле <emphasis>offset</emphasis> в OCB для указания, для какого конкретного файла мы в данный момент генерируем структуру <code><strong>struct dirent</strong></code>. Это означает, что мы также должны корректировать поле <emphasis>offset</emphasis> всякий раз, когда возвращаем данные.</p>
     <p>Возврат данных клиенту осуществляется «обычным» способом при помощи функции <emphasis>MsgReply()</emphasis>. Заметьте, что поле состояния (status) функции <emphasis>MsgReply()</emphasis> используется для указания числа отправленных клиенту байт.</p>
     <p><code>static int my_read_dir(resmgr_context_t *ctp,</code></p>
     <p><code> io_read_t *msg, iofunc_ocb_t *ocb) {</code></p>
     <p><code> int nbytes;</code></p>
     <p><code> int nleft;</code></p>
     <p><code> struct dirent *dp;</code></p>
     <p><code> char *reply_msg;</code></p>
     <p><code> char fname[_POSIX_PATH_MAX];</code></p>
     <p><code> // Выделить буфер для ответа</code></p>
     <p><code> reply_msg = calloc(1, msg-&gt;i.nbytes);</code></p>
     <p><code> if (reply_msg == NULL) {</code></p>
     <p><code>  return (ENOMEM);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // Назначить выходной буфер</code></p>
     <p><code> dp = (struct dirent *)reply_msg;</code></p>
     <empty-line/>
     <p><code> // Осталось «nleft» байт</code></p>
     <p><code> nleft = msg-&gt;i.nbytes;</code></p>
     <p><code> while (ocb-&gt;offset &lt; NUM_ENTS) {</code></p>
     <p><code>  // Создать имя файла</code></p>
     <p><code>  sprintf(fname, "%с", ocb-&gt;offset + "a");</code></p>
     <p><code>  // Проверить, насколько велик результат</code></p>
     <p><code>  nbytes = dirent_size(fname);</code></p>
     <p><code>  // Есть место?</code></p>
     <p><code>  if (nleft - nbytes &gt;= 0) {</code></p>
     <p><code>   // Заполнить элемент каталога и увеличить указатель</code></p>
     <p><code>   dp =</code></p>
     <p><code>    dirent_fill(dp, ocb-&gt;offset + 1, ocb-&gt;offset, fname);</code></p>
     <p><code>   // Увеличить смещение OCB</code></p>
     <p><code>   ocb-&gt;offset++;</code></p>
     <p><code>   // Учесть, сколько байт мы использовали</code></p>
     <p><code>   nleft -= nbytes;</code></p>
     <p><code>  } else {</code></p>
     <p><code>   // Места больше нет, остановиться</code></p>
     <p><code>   break;</code></p>
     <p><code>  }</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // Возвращаемся обратно к клиенту</code></p>
     <p><code> MsgReply(ctp-&gt;rcvid, (char*)dp - reply_msg, reply_msg,</code></p>
     <p><code>  (char*)dp — reply_msg);</code></p>
     <empty-line/>
     <p><code> // Освободить буфер</code></p>
     <p><code> free(reply_msg);</code></p>
     <p><code> // Сказать библиотеке, что мы уже ответили сами</code></p>
     <p><code> return (_RESMGR_NOREPLY);</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>my_read_file()</emphasis></subtitle>
     <p>В функции <emphasis>my_read_file()</emphasis> мы видим код, почти аналогичный простому примеру функции чтения, который приведен выше в данном разделе. Единственная странная вещь, которую мы здесь делаем — поскольку мы знаем, что возвращается всегда только один байт данных, значит, если параметр <emphasis>nbytes</emphasis> не равен нулю, то он должен быть равен единице (и ничему другому). Таким образом, мы можем создавать данные, подлежащие возврату, непосредственным заполнением символьной переменной <emphasis>string</emphasis>. Обратите внимание, как мы используем поле <emphasis>inode</emphasis> атрибутной записи для определения, какие данные возвращать. Это обычный прием для администраторов, обслуживающих несколько ресурсов. Дополнительным трюком было бы расширить атрибутную запись (мы говорили об этом в разделе «Расширение атрибутной записи») и хранить непосредственно в ней либо сами данные, либо указатель на них.</p>
     <p><code>static int my_read_file(resmgr_context_t *ctp,</code></p>
     <p><code> io_read_t *msg, iofunc_ocb_t *ocb) {</code></p>
     <p><code> int nbytes;</code></p>
     <p><code> int nleft;</code></p>
     <p><code> char string;</code></p>
     <empty-line/>
     <p><code> // Тут нет никаких xtype...</code></p>
     <p><code> if ((msg-&gt;i.xtype &amp; _IO_XTYPE_MASK) != _IO_XTYPE_NONE) (</code></p>
     <p><code>  return (ENOSYS);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // Выяснить, сколько байт осталось...</code></p>
     <p><code> nleft = ocb-&gt;attr-&gt;nbytes — ocb-&gt;offset;</code></p>
     <empty-line/>
     <p><code> // ...и сколько мы можем возвратить клиенту</code></p>
     <p><code> nbytes = min(nleft, msg-&gt;i.nbytes);</code></p>
     <p><code> if (nbytes) {</code></p>
     <p><code>  // Создать ответную строку</code></p>
     <p><code>  string = ocb-&gt;attr-&gt;inode - 1 + "A";</code></p>
     <p><code>  // Возвратить ее клиенту</code></p>
     <p><code>  MsgReply(ctp-&gt;rcvid, nbytes, &amp;string + ocb-&gt;offset,</code></p>
     <p><code>   nbytes);</code></p>
     <p><code>  // Обновить флаги и смещение</code></p>
     <p><code>  ocb-&gt;attr-&gt;flags |=</code></p>
     <p><code>   IOFUNC_ATTR_ATIME | IOFUNC_ATTR_DIRTY_TIME;</code></p>
     <p><code>  ocb-&gt;offset += nbytes;</code></p>
     <p><code> } else {</code></p>
     <p><code>  // Возвращать нечего, индицировать конец файла</code></p>
     <p><code>  MsgReply(ctp-&gt;rcvid, EOK, NULL, 0);</code></p>
     <p><code> }</code></p>
     <empty-line/>
     <p><code> // Уже ответили сами</code></p>
     <p><code> return (_RESMGR_NOREPLY);</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>dirent_size()</emphasis></subtitle>
     <p>Вспомогательная подпрограмма <emphasis>dirent_size()</emphasis> просто вычисляет число байт, необходимое для структуры <code><strong>struct dirent</strong></code>, с учетом ограничений по выравниванию. Опять же, для нашего простого администратора ресурсов здесь имеет место небольшое искусственное упрощение, потому что мы точно знаем, каков размер каждого элемента каталога — все имена файлов имеют длину ровно один байт. Однако, как бы там ни было, это все равно полезная служебная подпрограмма.</p>
     <p><code>int dirent_size(char *fname) {</code></p>
     <p><code>return (ALIGN(sizeof(struct dirent) - 4 + strlen(fname)));</code></p>
     <p><code>}</code></p>
     <subtitle><emphasis>dirent_fill()</emphasis></subtitle>
     <p>И, наконец, вспомогательная подпрограмма <emphasis>dirent_fill()</emphasis> применяется для помещения передаваемых ей значений (а именно — полей <emphasis>inode</emphasis>, <emphasis>offset</emphasis> и <emphasis>fname</emphasis>) в также передаваемый ей элемент каталога. В порядке щедрости она также возвращает указатель на адрес (с учетом выравнивания), с которого должен начинаться следующий элемент каталога.</p>
     <p><code>struct dirent* dirent_fill(struct dirent *dp, int inode,</code></p>
     <p><code> int offset, char *fname) {</code></p>
     <p><code> dp-&gt;d_ino = inode;</code></p>
     <p><code> dp-&gt;d_offset = offset;</code></p>
     <p><code> strcpy(dp-&gt;d_name, fname);</code></p>
     <p><code> dp-&gt;d_namelen = strlen(dp-&gt;d_name);</code></p>
     <p><code> dp-&gt;d_reclen =</code></p>
     <p><code> ALIGN(sizeof(struct dirent) - 4 + dp-&gt;d_namelen);</code></p>
     <p><code> return ((struct dirent*)((char*)dp + dp-&gt;d_reclen));</code></p>
     <p><code>}</code></p>
    </section>
   </section>
   <section>
    <title>
     <p>Резюме</p>
    </title>
    <p>Написание администратора ресурсов — наиболее сложная задача из описанных в этой книге.</p>
    <p>Администратор ресурсов — это сервер, который воспринимает ряд четко определенных сообщений. Эти сообщения можно подразделить на две категории:</p>
    <p>Сообщения установления соединения</p>
    <p>Относятся к операциям с именами путей, с их помощью можно устанавливать контекст для дальнейшей работы.</p>
    <p>Сообщения ввода/вывода</p>
    <p>Всегда следуют <emphasis>за</emphasis> сообщениями установления соединения и указывают, какую реальную работу нужно сделать для клиента (например, <emphasis>stat()</emphasis>).</p>
    <p>Действия администратора ресурса управляются функциями пула потоков (мы обсуждали их в главе «Процессы и потоки») и функциями интерфейса диспетчеризации.</p>
    <p>QSSL в библиотеке администратора ресурсов предоставляет ряд вспомогательных POSIX-функций, которые выполняют львиную долю работы по обслуживанию клиентских сообщений установления соединения и ввода/вывода, поступающих в администратор ресурсов.</p>
    <p>Существует ряд структур данных, относящихся к клиентам и объявляемым администраторами ресурсов устройствам, которые необходимо принимать во внимание:</p>
    <p>OCB (блок открытого контекста)</p>
    <p>Выделяется при каждом «открытии» ресурса; содержат контекст для клиента (например, текущее смещение <emphasis>lseek()</emphasis>).</p>
    <p>Атрибутная запись</p>
    <p>Выделяется для каждого устройства; содержит информацию об устройстве (например, размер устройства, режимы доступа, и т.д.).</p>
    <p>Запись точки монтирования</p>
    <p>Распределяется на базисе по каждому администратору ресурса и содержит полную информацию о характеристиках администратора ресурса.</p>
    <p>Клиент общается с администраторами ресурсов посредством обмена сообщениями, разрешив имя пути (с помощью функции <emphasis>open()</emphasis> и других вызовов) в соответствующий нужному администратору дескриптор узла, идентификатор процесса, идентификатор канала и обработчик (handle).</p>
    <p>И наконец, вы наделяете администратор ресурса необходимой вам функциональностью, переопределяя некоторые из вызовов в таблицах функций установления соединения и функций ввода/вывода.</p>
   </section>
  </section>
  <section>
   <title>
    <p>Приложение А</p>
    <p>Переход с QNX4 на QNX/Neutrino</p>
   </title>
   <section>
    <title>
     <p>Переход с QNX4 на QNX/Neutrino</p>
    </title>
    <section>
     <p>В данном приложении мы рассмотрим предыдущую операционную систему разработки QSSL, QNX4, и сравним ее с QNX/Neutrino. Данное приложение будет вам интересно главным образом в том случае, если вы уже являетесь пользователем QNX4 и хотите узнать.</p>
     <p>• что такого замечательного в QNX/Neutrino?</p>
     <p>• какие сложности связаны с переносом программного обеспечения в QNX/Neutrino?</p>
     <p>а также если вы разрабатываете (или портируете) программное обеспечение для обеих операционных систем.</p>
    </section>
    <section>
     <title>
      <p>Сходства</p>
     </title>
     <p>Давайте начнем со общих черт этих двух операционных систем:</p>
     <p>• архитектура на основе обмена сообщениями;</p>
     <p>• распределенный обмен сообщениями в сети;</p>
     <p>• реальное время;</p>
     <p>• микроядерная архитектура;</p>
     <p>• защита памяти на уровне процессов;</p>
     <p>• POSIX-совместимость;</p>
     <p>• относительно простая модель «драйвера устройства»;</p>
     <p>• встраиваемость.</p>
     <p>Заметьте, что хоть часть вышеперечисленных базовых свойств действительно подобны в этих двух ОС, в целом QNX/Neutrino обеспечивает более расширенную поддержку. Например, поддержки POSIX в QNX/Neutrino больше, чем в QNX4, — просто потому что многие из стандартов этой серии на момент выхода QNX4 были еще только в стадии разработки. И хотя в процессе разработки QNX/Neutrino в состоянии разработки находилась гораздо меньшая их часть, постоянно появляются новые. Эта бесконечная игра в догонялки.</p>
    </section>
    <section>
     <title>
      <p>Улучшения</p>
     </title>
     <p>Теперь, когда вы выяснили, что общего между этими двумя ОС, давайте посмотрим, каковы преимущества QNX/Neutrino перед QNX4:</p>
     <p>• большее число поддерживаемых стандартов POSIX;</p>
     <p>• лучше встраивается;</p>
     <p>• ядро лучше конфигурируется для других аппаратных платформ;</p>
     <p>• поддержка многопоточности;</p>
     <p>• более простая модель драйвера устройства;</p>
     <p>• переносимая архитектура (в настоящее поддерживаются, кроме x86, процессоры MIPS и PPC) (а теперь уже и ARM, StrongARM и SuperH-4 — <emphasis>прим. ред</emphasis>.),</p>
     <p>• поддерживает SMP;</p>
     <p>• лучше документирована.</p>
     <p>При том, что некоторые из этих усовершенствований «вне сравнения», поскольку аналогов для них в QNX4 нет, а значит, нет и проблем совместимости (например, многопоточность POSIX не поддерживалась в QNX4), некоторые из проблем потребовали внесения в ОС кардинальных изменений. Сначала я вкратце упомяну, какие классы изменений были необходимы, а затем мы подробно рассмотрим возникшие в связи с этим проблемы совместимости и предложения о том, как переносить программы в QNX/Neutrino (или заставить программу работать в обеих ОС).</p>
     <subtitle>Встраиваемость</subtitle>
     <p>В QNX/Neutrino коренным образом пересмотрена стратегия встраивания. В первоначальном варианте QNX4 встраиваемость обеспечивалась только частично. Затем появилась QNX/Neutrino, которая с самого начала разрабатывалась как встраиваемая. В качестве премии, QNX4 подверглась некоторой модернизации на основе опыта, полученного с QNX/Neutrino, и теперь является в гораздо более значительной степени встраиваемой, чем ранее. Как бы там ни было, по части встраиваемости QNX/Neutrino и QNX4 отличаются, как день и ночь. QNX4 не содержит никакой реальной поддержки таких вещей как:</p>
     <p>• исходящие вызовы ядра (kernel callouts) (прерывание, таймер);</p>
     <p>• настройка стартового кода;</p>
     <p>• образная файловая система.</p>
     <p>а вот в QNX/Neutrino все это есть. Подробное описание методик встраивания QNX/Neutrino приведено в книге «Построение встраиваемых систем», входящей в комплект документации.</p>
     <subtitle>Поддержка многопоточности</subtitle>
     <p>В QNX4 есть функция, называемая <emphasis>tfork()</emphasis>, которая позволяет вам организовать «многопоточность», создавая процесс с сегментами кода и данных, отображенными в то же адресное пространство, что и у родительского. Создание процесса и потом приведение его к «потокоподобному» виду дает иллюзию создания потока. И хотя в системе обновлений QSSL содержится библиотека поддержки потоков для QNX4, само ядро потоки непосредственно не поддерживает.</p>
     <p>В QNX/Neutrino для организации многопоточности применяется POSIX-модель «рthread». Это означает, что там вы увидите (и уже видели в данной книге) знакомые вызовы функций типа <emphasis>pthread_create()</emphasis>, <emphasis>pthread_mutex_lock()</emphasis>, и т.п.</p>
     <subtitle>Обмен сообщениями</subtitle>
     <p>При том что воздействие потоков на обмен сообщениями может показаться минимальным, использование потоков привело к фундаментальному изменению реализации механизма обмена сообщениями (не самой <emphasis>концепции</emphasis> SEND/RECEIVE/REPLY, а именно ее <emphasis>реализации</emphasis>).</p>
     <p>В QNX4 сообщения адресовались идентификатору процесса. Чтобы отправить сообщение, нужно было просто найти идентификатор процесса-адресата и выполнить <emphasis>Send()</emphasis>. Чтобы сервер мог принять сообщение в QNX4, он должен был вызвать <emphasis>Receive()</emphasis>. Это блокировало его до прибытия сообщения. Затем сервер отвечал с помощью функции <emphasis>Reply()</emphasis>.</p>
     <p>В QNX/Neutrino обмен сообщениями устроен так же, только при этом используются другие имена функций. Что изменилось, так это сам механизм. Теперь, прежде чем вызывать стандартные функции обмена сообщениями, клиент должен сперва создать соединение с сервером. А сервер, прежде чем он сможет выполнять стандартные функции обмена сообщениями, должен сначала создать канал.</p>
     <p>Заметьте, что существовавшая в QNX4 функция <emphasis>Creceive()</emphasis>, которая выполняла неблокирующий вызов <emphasis>Receive()</emphasis>, в QNX/Neutrino отсутствует. Мы вообще не одобряем такие «опрашивающие» функции, особенно когда можно запустить поток; впрочем, если вы настаиваете на выполнении неблокирующего вызова <emphasis>MsgReceive()</emphasis>, посмотрите главу «Часы, таймеры и периодические уведомления», раздел «Тайм-ауты ядра». Вот соответствующий пример кода в качестве краткого пояснения:</p>
     <p><code>TimerTimeout(CLOCK_REALTIME, _NTO_TIMEOUT_RECEIVE,</code></p>
     <p><code> NULL, NULL, NULL);</code></p>
     <p><code>rcvid = MsgReceive(...</code></p>
     <subtitle>Импульсы и события</subtitle>
     <p>В QNX4 было нечто по имени «прокси». Прокси лучше всего описывается как «законсервированное» (т.е. неизменяемое) сообщение, которое можно отослать владельцу этой (почему-то исторически сложилось, что в русском языке термин «прокси» женского рода; возможно, из-за распространенного сленгового произношения «прокся» #:о) — <emphasis>прим. ред</emphasis>.) прокси от имени процесса или ядра (например, по срабатыванию таймера или из обработчика прерывания). Прокси является неблокирующей для отправителя и принимается точно также, как и любое другое сообщение. Распознать сообщение прокси (то есть отличить его от обычного сообщения) можно было либо проанализировав его содержимое (не самый надежный способ, поскольку процесс тоже может передать что-нибудь с подобным содержимым), либо проверив идентификатор процесса, от которого оно было получено. Если идентификатор процесса совпадает с идентификатором прокси — значит, это прокси, потому что идентификаторы процессов и прокси берутся из того же самого пула номеров и не пересекаются.</p>
     <p>(На самом деле, документация по QNX4 вносит в понятие «прокси» страшную путаницу, которая требует пояснения. Если бы прокси являлась именно <emphasis>сообщением</emphasis>, как это записано в определении прокси, обороты типа «получить сообщение от прокси» были бы лишены смысла. Само по себе слово «proxy» переводится как «промежуточный агент». Если внимательно прочитать главу «IPC via proxies» книги «QNX4 System Architecture», становится ясно, что прокси — это не само сообщение, а специализированный «<emphasis>квазипроцесс</emphasis>», не обладающий ресурсами, но имеющий идентификатор (который, разумеется, по понятным причинам не перекрывается с другими идентификаторами процессов) и способный к обмену специализированными сообщениями. Применяя к прокси с определенным идентификатором функцию <emphasis>Trigger()</emphasis>, процесс в QNX4 фактически делает этому квазипроцессу специализированный <emphasis>Send()</emphasis>, в ответ на который мгновенно следует <emphasis>Reply()</emphasis>, поэтому отправитель (вызвавший <emphasis>Trigger()</emphasis>) и не блокируется. Затем прокси отправляет своему процессу-владельцу свое предопределенное сообщение; если владелец прокси в этот момент не является RECEIVE-блокированным, сообщение становится в очередь. Таким образом, правильнее было бы говорить «переключить прокси» вместо «отправить прокси» (о вызове <emphasis>Trigger()</emphasis>) и «получить <emphasis>сообщение</emphasis> прокси» вместо «получить прокси» — <emphasis>прим. ред</emphasis>.).</p>
     <p>QNX/Neutrino расширяет концепцию прокси введением «импульсов». Импульсы по-прежнему являются неблокирующими сообщениями, и их по-прежнему можно переслать либо от потока к потоку, либо от служебной функции ядра (например, от того же таймера или обработчика прерывания) к потоку. Различие состоит в том, что в то время как прокси имели фиксированное содержимое, импульсы имеют фиксированную длину, но содержимое их может быть изменено отправителем в любой момент. Например, обработчик прерываний (ISR) может сохранить в импульсе какие-либо данные и затем отправить этот импульс потоку.</p>
     <p>В QNX4 некоторые сервисы были наделены способностью послать как сигнал, так и прокси, в то время как другие сервисы были наделены способностью послать либо только одно, либо только другое. Мало того, обычно это выполнялось несколькими различными способами. Например, чтобы доставить сигнал, вы должны были применить функцию <emphasis>kill()</emphasis>. Чтобы доставить прокси или сигнал по срабатыванию таймера, вы должны были использовать отрицательный номер сигнала (для указания на то, что это прокси) или положительный номер сигнала (для указания на то, что это сигнал). И, наконец, обработчик прерываний мог доставлять <emphasis>только</emphasis> прокси.</p>
     <p>В QNX/Neutrino все это было абстрагировано в расширение POSIX-структуры <code><strong>struct sigevent</strong></code>. Все, что использует и возвращает <code><strong>struct sigevent</strong></code>, может использовать как сигнал, так и импульс.</p>
     <p>На самом деле, функциональность <code><strong>struct sigevent</strong></code> была расширена вплоть до возможности создания потока! Мы говорили об этом в Главе «<emphasis>Часы, таймеры и периодические уведомления</emphasis>» в разделе «<emphasis>Уведомление созданием потока</emphasis>».</p>
     <subtitle>Модель драйвера устройства</subtitle>
     <p>В самых ранних версиях QNX (семейство QNX2) программирование драйверов устройств было чем-то из области черной магии. В QNX4 это тоже поначалу была загадочная вещь, но затем, в конце концов, появилось несколько примеров программ. В QNX/Neutrino этому вопросу посвящены книги и учебные курсы. И, как оказалось, модели драйверов в QNX/ Neutrino и QNX4 на архитектурном уровне достаточно сходны. В то время как в QNX4 была кромешная путаница вокруг того, что такое «функции установления соединения» и что такое «функции ввода-вывода», в QNX/Neutrino все четко разделено. Также под QNX4 вы (разработчик драйвера устройства) должны были выполнять большую часть работы (программирование основного цикла обработки сообщений, сопоставление контекста каждому сообщению ввода-вывода, и т.д.) самостоятельно. В QNX/Neutrino все это в значительной степени упрощено введением библиотеки администратора ресурсов.</p>
     <subtitle>Поддержка MIPS и PPC</subtitle>
     <p>Одно из самых существенных отличий QNX/Neutrino от QNX4 в отношении встраиваемости состоит в том, что QNX/Neutrino поддерживает процессоры MIPS и PPC (Power PC). QNX4 изначально была «дома» на IBM PC с их BIOS и «очень» стандартным набором аппаратных средств, QNX/Neutrino же одинаково по-домашнему чувствует себя на разных платформах с BIOS (или монитором ПЗУ) или без нее, а также на нестандартной аппаратуре, комплектация которой выбирается изготовителем (и часто без учета требований ОС). Это означает, что ядро QNX/Neutrino должно было обеспечивать поддержку исходящих вызовов (callouts), чтобы вы могли, например, задать свой тип контроллера прерываний и работать на этих аппаратных средствах без необходимости приобретения лицензии на исходный текст операционной системы.</p>
     <p>Другая группа изменений, которые вы заметите при переносе приложений из QNX4 в QNX/Neutrino, особенно на другие платформы, касается того, что процессоры MIPS и PPC уж больно вычурны по части выравнивания. Вы не можете обращаться к N-байтовому объекту иначе чем по адресу, кратному N. При работе на x86 (с выключенным флагом выравнивания) вы бы волей-неволей обратились к памяти. Изменив вашу программу так, чтобы все структуры были правильно выровнены (для не-x86 процессоров), вы также заметите, что ваша программа после этого на x86 станет выполняться быстрее, потому что x86 быстрее обращается к выровненным данным.</p>
     <p>Другое, что так часто не дает людям жить — это порядок следования байт, прямой (big-endian) или обратный (little-endian). (Кому интересна этимология этих терминов, загляните в английский оригинал «Путешествий Гулливера» Джонатана Свифта :-) — <emphasis>прим. ред</emphasis>.). У процессора x86 возможен только один порядок следования байт, и он обратный. Процессоры MIPS и PPC допускают оба порядка следования байт, т. е. могут работать как с прямым, так и с обратным. Кроме того, процессоры MIPS и PPC являются процессорами типа RISC (с сокращенным набором команд), что означает, что некоторые операции типа <code><strong>|=</strong></code> в Си (установка бита) могут и не быть атомарными. Это иногда приводит к жутким последствиям! Список вспомогательных функций, которые обеспечивают атомарность выполняемой операции, приведен в файле <code><strong>&lt;atomic.h&gt;</strong></code>.</p>
     <subtitle>Поддержка SMP</subtitle>
     <p>Существующие версии QNX4 работают только на однопроцессорных системах, в то время как QNX/Neutrino уже на момент публикации первого издания этой книги обеспечивала поддержку SMP по меньшей мере на архитектуре x86. SMP дает значительные преимущества, особенно в многопоточной ОС, но это одновременно и гораздо более серьезный пистолет для простреливания ноги (кто любопытен, поищите в Интернет по ключевой фразе «shoot yourself in the foot» — <emphasis>прим. ред</emphasis>.).</p>
     <p>Например, в однопроцессорной машине обработчик прерывания (ISR) может вытеснить поток, но <emphasis>наоборот никогда не бывает</emphasis>. В однопроцессорной машине бывает полезно представить себе, что потоки якобы выполняются одновременно, хотя в действительности это не так.</p>
     <p>В блоке SMP поток и обработчик прерывания могут работать одновременно, да и несколько потоков тоже могут работать одновременно. Так что SMP — это не только превосходная рабочая станция, но и неплохое средство тестирования программного обеспечения — если вы сделали какие бы то ни было «плохие» предположения о защите в многопоточной среде, в SMP-системе они однажды обязательно выплывут.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Для иллюстрации того, насколько это верно, один пример. Одна из ошибок в ранней внутренней версии поддержки SMP проявлялась в «окне» длиной в один машинный цикл! То, что для однопроцессорной машины было запрограммировано как атомарная операция «чтение/модификация/запись», в SMP-блоке стало допускать вмешательство в ход операции второго процессора, выполняющего «сравнение/обмен».</p>
     </cite>
    </section>
   </section>
   <section>
    <title>
     <p>Философия переноса программ</p>
    </title>
    <section>
     <p>Давайте теперь взглянем на все «сверху». Здесь мы рассмотрим:</p>
     <p>• обмен сообщениями и систему «клиент/сервер»;</p>
     <p>• обработчики прерываний (ISR).</p>
    </section>
    <section>
     <title>
      <p>Анализ обмена сообщениями</p>
     </title>
     <p>В QNX4 клиент мог найти сервер двумя способами:</p>
     <p>• используя глобальное пространство имен;</p>
     <p>• выполнение <emphasis>open()</emphasis> в отношении администратора ввода/вывода.</p>
     <subtitle>«Клиент/сервер» с использованием глобального пространства имен</subtitle>
     <p>Если взаимоотношения «клиент/сервер, которые вы переносите, базируются на глобальном пространстве имен, тогда клиент использует:</p>
     <p><emphasis>qnx_name_locate()</emphasis></p>
     <p>а сервер регистрирует свое имя при помощи:</p>
     <p><emphasis>qnx_name_attach()</emphasis></p>
     <p>В этом случае у вас есть два выбора. Вы можете либо попробовать сохранить вариант с глобальными именами, либо модифицировать клиента и сервер так, чтобы они работали подобно стандартному администратору ресурсов.</p>
     <p>Я рекомендую вам последний вариант, поскольку именно этот вариант характерен для QNX/Neutrino — сводить все к администраторам ресурсов, а не пытаться навесить кусок администратора ресурсов на службу глобальных имен.</p>
     <p>Модификация будет достаточно проста. Скорее всего, клиентская сторона вызывает функцию, либо возвращающую идентификатор серверного процесса, либо создающую виртуальный канал («VC» — Virtual Circuit) от клиентского узла к удаленному узлу сервера. В обоих случаях как идентификатор процесса, так и идентификатор виртуального канала к удаленному процессу определяются при помощи <emphasis>qnx_name_locate()</emphasis>. «Магическим амулетом», связывающим клиента с сервером, здесь является специальная разновидность идентификатора процесса (мы считаем идентификатор виртуального канала идентификатором процесса, поскольку он берется из того же пула номеров и со всех точек зрения выглядит как идентификатор процесса).</p>
     <p>Преодолеть основное различие можно было бы, возвращая вместо идентификатора процесса идентификатор соединения. Поскольку клиент в QNX4, вероятно, не анализирует идентификаторы процессов (да и зачем? Так, просто число), вы могли бы «обмануть» его, применив к «глобальному имени» функцию <emphasis>open()</emphasis>. В этом случае, однако, глобальное имя должно было бы быть точкой монтирования, зарегистрированной администратором ресурса в качестве своего «идентификатора». Вот, например, типовой пример клиента QNX4, взятый из моей серверной библиотеки CLID:</p>
     <p><code>/*</code></p>
     <p><code> * CLID_Attach(ServerName)</code></p>
     <p><code> *</code></p>
     <p><code> * Эта подпрограмма отвечает за установление соединения</code></p>
     <p><code> * с сервером CLID.</code></p>
     <p><code> *</code></p>
     <p><code> * Возвращает PID сервера CLID или идентификатор</code></p>
     <p><code> * виртуального канала к нему.</code></p>
     <p><code>*/</code></p>
     <empty-line/>
     <p><code>// Сюда запишется имя - для других библиотечных вызовов</code></p>
     <p><code>static char CLID_serverName(MAX_CLID_SERVER_NAME + 1);</code></p>
     <empty-line/>
     <p><code>// Сюда запишется идентификатор сервера</code></p>
     <p><code>CLID static int clid_pid = -1;</code></p>
     <empty-line/>
     <p><code>int CLID_Attach(char *serverName) {</code></p>
     <p><code> if (ServerName == NULL) {</code></p>
     <p><code>  sprintf(CLID_serverName, "/PARSE/CLID");</code></p>
     <p><code> } else {</code></p>
     <p><code>  strcpy(CLID_serverName, serverName);</code></p>
     <p><code> }</code></p>
     <p><code> clid_pid = qnx_name_locate(0, CLID_serverName,</code></p>
     <p><code>  sizeof(CLID_ServerIPC), NULL);</code></p>
     <p><code> if (clid_pid != -1) {</code></p>
     <p><code>  CLID_IPC(CLID_MsgAttach); // Послать сообщение ATTACH</code></p>
     <p><code>  return (clid_pid);</code></p>
     <p><code> }</code></p>
     <p><code> return (-1);</code></p>
     <p><code>}</code></p>
     <p>Вы могли бы изменить это на следующее:</p>
     <p><code>/*</code></p>
     <p><code> * CLID_Attach(serverName), версия для QNX/Neutrino</code></p>
     <p><code>*/</code></p>
     <p><code>int CLID_Attach(char *serverName) {</code></p>
     <p><code> if (ServerName == NULL) {</code></p>
     <p><code>  sprintf(CLID_serverName, "/PARSE/CLID");</code></p>
     <p><code> } else {</code></p>
     <p><code>  strcpy(CLID_serverName, serverName);</code></p>
     <p><code> }</code></p>
     <p><code> return (clid_pid = open(CLID_serverName, O_RDWR));</code></p>
     <p><code>}</code></p>
     <p>И клиент ничего бы не заметил.</p>
     <cite>
      <p><image l:href="#img_1.png"/>Два замечания по реализации. В качестве зарегистрированного префикса администратора ресурса я просто оставил имя по умолчанию («<code><strong>/PARSE/CLID</strong></code>»). Вероятно, лучше было бы взять имя «<code><strong>/dev/clid</strong></code>», но насколько вы хотите следовать POSIX — это ваше личное дело. В любом случае, это незначительное изменение, и оно мало связано с тем, что здесь обсуждается.</p>
      <p>Второе замечание касается того, что я по-прежнему назвал дескриптор файла <emphasis>clid_pid</emphasis>, хотя реально ему бы теперь следовало называться <emphasis>clid_fd</emphasis>. Это, опять же, вопрос стиля и касается только того, сколько различий вы хотите иметь между версиями кода для QNX4 и QNX/Neutrino.</p>
      <p>В любом случае, того чтобы данная программа была переносима в обе ОС, вам придется выделить код соединения с сервером в отдельную функцию — как я это сделал выше с функцией <emphasis>CLID_Attach()</emphasis>.</p>
     </cite>
     <p>В какой-то момент клиент должен будет выполнить собственно операцию отправки сообщения. Здесь все становится несколько сложнее. Поскольку отношения клиент/сервер <emphasis>не</emphasis> основаны на отношениях с администраторами ввода/вывода, клиент обычно создает «нестандартные» сообщения. Снова пример из CLID-библиотеки («незащищенный» клиентский вызов здесь — <emphasis>CLID_AddSingleNPANXX()</emphasis>, я также включил функции <emphasis>checkAttach()</emphasis> и <emphasis>CLID_IPC()</emphasis> для того, чтобы продемонстрировать фактическую передачу сообщений и логику проверок):</p>
     <p><code>/*</code></p>
     <p><code> * CLID_AddSingleNPANXX(npa, nxx)</code></p>
     <p><code>*/</code></p>
     <empty-line/>
     <p><code>int CLID_AddSingleNPANXX(int npa, int nxx) {</code></p>
     <p><code> checkAttach();</code></p>
     <p><code> CLID_IPCData.npa = npa;</code></p>
     <p><code> CLID_IPCData.nxx = nxx;</code></p>
     <p><code> CLID_IPC(CLID_MsgAddSingleNPANXX);</code></p>
     <p><code> return (CLID_IPCData.returnValue);</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>/*</code></p>
     <p><code> * CLID_IPC(номер_сообщения_IPC)</code></p>
     <p><code> *</code></p>
     <p><code> * Эта подпрограмма вызывает сервер с глобальным буфером</code></p>
     <p><code> * CLID_IPCData и заносит в него номер сообщения,</code></p>
     <p><code> * переданный ей в качестве аргумента.</code></p>
     <p><code> *</code></p>
     <p><code> * Если сервера нет, эта подпрограмма установит</code></p>
     <p><code> * поле returnValue в CLID_NoServer. Остальные</code></p>
     <p><code> * поля остаются как есть.</code></p>
     <p><code>*/</code></p>
     <p><code>void CLID_IPC(int IPCMessage) {</code></p>
     <p><code> if (clid_pid == -1) {</code></p>
     <p><code>  CLID_IPCData.returnValue = CLID_NoServer;</code></p>
     <p><code>  return;</code></p>
     <p><code> }</code></p>
     <p><code> CLID_IPCData.serverFunction = IPCMessage;</code></p>
     <p><code> CLID_IPCData.type = 0x8001;</code></p>
     <p><code> CLID_IPCData.subtype = 0;</code></p>
     <p><code> if (Send(clid_pid, &amp;CLID_IPCData, &amp;CLID_IPCData,</code></p>
     <p><code>  sizeof(CLID_IPCData), sizeof(CLID_IPCData))) {</code></p>
     <p><code>  CLID_IPCData.returnValue = CLID_IPCError;</code></p>
     <p><code>  return;</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <empty-line/>
     <p><code>void checkAttach() {</code></p>
     <p><code> if (clid_pid == -1) {</code></p>
     <p><code>  CLID_Attach(NULL);</code></p>
     <p><code> }</code></p>
     <p><code>}</code></p>
     <p>Как вы видите, функция <emphasis>checkAttach()</emphasis> применяется для проверки существования соединения с сервером CLID. Если бы соединения не было, это было бы подобно запросу <emphasis>read()</emphasis> по несуществующему дескриптору файла. В моем варианте программы функция <emphasis>checkAttach()</emphasis> создает соединение автоматически. Это как если бы функция <emphasis>read()</emphasis> определила, что дескриптор файла некорректен, и сама создала бы корректный. Еще один вопрос стиля.</p>
     <p>Обмен специализированными сообщениями происходит в функции <emphasis>CLID_IPC()</emphasis>. Она берет значение глобальной переменной <emphasis>CLID_IPCData</emphasis> и пробует переслать его серверу, используя функцию QNX4 <emphasis>Send()</emphasis>.</p>
     <p>Специализированные сообщения могут быть обработаны о из двух способов. Можно:</p>
     <p>1. транслировать их в стандартные вызовы функций POSIX основанные на файловых дескрипторах;</p>
     <p>2. инкапсулировать их в сообщение типа <emphasis>devctl()</emphasis>, либо в специализированное сообщение, используя тип _IO_MSG.</p>
     <p>В обоих случаях вы перестраиваете клиента на обмен сообщениями стандартными для администраторов ресурсов средствами. Как? У вас нет файлового дескриптора? Есть только идентификатор соединения? Или наоборот? Ну, это как раз не проблема. В QNX/Neutrino <emphasis>дескриптор файла</emphasis> в действительности является <emphasis>идентификатором соединения</emphasis>!</p>
     <subtitle>Трансляция сообщений в стандартные вызовы POSIX на основе файловых дескрипторов</subtitle>
     <p>В случае CLID-сервера это не вариант. Не существует стандартного POSIX-вызова на основе файлового дескриптора, который мог бы «добавить к администратору ресурса CLID пару NPA/NXX». Однако, существует стандартный механизм <emphasis>devctl()</emphasis>, так что если ваши отношения клиент/сервер требуют такой формы, смотрите ниже.</p>
     <p>Прежде чем броситься реализовывать этот подход (трансляцию в стандартные сообщения на основе файловых дескрипторов), давайте остановимся и подумаем, где это может оказаться полезным. В аудиодрайвере QNX4 вы могли бы использовать нестандартные сообщения для передачи аудиоданных администратору и от него. При ближайшем рассмотрении здесь, для задачи блочной передачи данных, вероятно, наиболее бы подошли функции <emphasis>read()</emphasis> и <emphasis>write()</emphasis>. Установку частоты оцифровки, с другой стороны, можно было бы гораздо удачнее реализовать с применением функции <emphasis>devctl()</emphasis>.</p>
     <p>Хорошо, но ведь не каждое взаимодействие клиент/сервер сводится к блочной передаче данных (тот же сервер CLID — тому пример).</p>
     <subtitle>Трансляция сообщений в вызовы <emphasis>devctl()</emphasis> или _IO_MSG</subtitle>
     <p>Итак, возник вопрос — как выполнять операции управления? Самый простой способ состоит в применении POSIX-вызова <emphasis>devctl()</emphasis>. Наш пример из библиотеки CLID примет вид:</p>
     <p><code>/*</code></p>
     <p><code> * CLID_AddSingleNPANXX(npa, nxx)</code></p>
     <p><code>*/</code></p>
     <p><code>int CLID_AddSingleNPANXX(int npa, int nxx) {</code></p>
     <p><code> struct clid_addnpanxx_t msg;</code></p>
     <p><code> checkAttach(); // Оставить или нет — дело вкуса</code></p>
     <p><code> msg.npa = npa;</code></p>
     <p><code> msg.nxx = nxx;</code></p>
     <p><code> return</code></p>
     <p><code>  (devctl(clid_pid, DCMD_CLID_ADD_NPANXX,</code></p>
     <p><code>  &amp;msg, sizeof(msg), NULL));</code></p>
     <p><code>}</code></p>
     <p>Как видите, операция относительно безболезненная. (Для тех, кто не любит <emphasis>devctl()</emphasis> за то, что приходится отправлять в обе стороны блоки данных одного и того же размера, см. ниже обсуждение сообщений _IO_MSG.). Опять же, если вы пишете программу, которая должна работать в обеих операционных системах, вам следует выделить функцию обмена сообщениями в отдельный библиотечный модуль и предоставить несколько вариантов реализации, в зависимости от применяемой операционной системы.</p>
     <p>Реально мы убили двух зайцев:</p>
     <p>1. отказались от глобальной переменной и стали собирать сообщения на основе стековой переменной — это делает нашу программу безопасной в многопоточной среде (thread- safe);</p>
     <p>2. передали структуру данных нужного размера вместо структуры данных максимального размера, как мы это делали в предыдущем примере с QNX4.</p>
     <p>Заметьте, что нам пришлось определить константу DCMD_CLID_ADD_NPANXX — в принципе, мы могли бы для этих же целей применить константу CLID_MsgAddSingleNPANXX (сделав соответствующее изменение в заголовочном файле), но я просто хотел подчеркнуть тот факт, что эти две константы не являются одинаковыми.</p>
     <p>Второй убитый заяц заключался в том, что мы передали «структуру данных нужного размера». На самом деле, мы тут немножко приврали. Обратите внимание на то, что функция <emphasis>devctl()</emphasis> имеет только один параметр размера (четвертый, который мы установили в <code><strong>sizeof(msg)</strong></code>). Как <emphasis>на самом деле</emphasis> происходит пересылка данных? Второй параметр функции <emphasis>devctl()</emphasis> содержит команду для устройства (поэтому и «DCMD»). Двумя старшими битами команды кодируется направление, которое может быть одним из четырех:</p>
     <p>1. «00» — передачи данных нет;</p>
     <p>2. «01» — передача от драйвера клиенту;</p>
     <p>3. «10» — передача от клиента драйверу;</p>
     <p>4. «11» — двунаправленная передача.</p>
     <p>Если вы не передаете данные (то есть достаточно просто команды) или передаете их в одном направлении, то применение функции <emphasis>devctl()</emphasis> — прекрасный выбор. Интересен тот вариант, когда вы передаете данные в обоих направлениях. Интересен он тем, что, поскольку у функции <emphasis>devctl()</emphasis> только один параметр размера, обе пересылки данных (как драйверу, так и от драйвера) передадут весь буфер данных целиком! Это хорошо в том частном случае, когда размеры буферов «ввода» и «вывода» одинаковы, но представьте себе, что буфер принимаемых драйвером данных имеет размер в несколько байт, а буфер передаваемых данных гораздо больше. Поскольку у нас есть только один параметр размера, мы вынуждены будем каждый раз передавать драйверу полный буфер данных, хотя требовалось передать всего несколько байт!</p>
     <p>Эта проблема может быть решена применением «своих собственных» сообщений на основе общего механизма управляющих последовательностей, поддерживаемого в сообщениях типа _IO_MSG.</p>
     <p>Сообщение типа _IO_MSG было предусмотрено для того, чтобы дать вам возможность вводить свои собственные типы сообщений, не конфликтуя при этом со «стандартными» типами сообщений администраторов ресурсов, поскольку для администраторов ресурсов сам тип сообщения _IO_MSG уже является «стандартным».</p>
     <p>Первое, что вы должны сделать при использовании сообщений типа _IO_MSG — это определить ваши «специальные» сообщения. В этом примере мы определим два таких типа и последуем стандартной модели сообщений администратора ресурсов: один тип будет сообщением ввода, другой — вывода.</p>
     <p><code>typedef struct {</code></p>
     <p><code> int data_rate;</code></p>
     <p><code> int more_stuff;</code></p>
     <p><code>} my_input_xyz_t;</code></p>
     <empty-line/>
     <p><code>typedef struct {</code></p>
     <p><code> int old_data_rate;</code></p>
     <p><code> int new_data_rate;</code></p>
     <p><code> int more_stuff;</code></p>
     <p><code>} my_output_xyz_t;</code></p>
     <empty-line/>
     <p><code>typedef union {</code></p>
     <p><code> my_input_xyz_t  i;</code></p>
     <p><code> my_output_xyz_t o;</code></p>
     <p><code>} my_message_xyz_t;</code></p>
     <p>Здесь мы определили новый тип — объединение (union) из сообщений ввода и вывода — и назвали этот тип <code><strong>my_message_xyz_t</strong></code>. Закономерность в имени идентификатора заключается в том, что это сообщение относится к службе «<code><strong>xyz</strong></code>» какова бы она ни была. Сообщение ввода имеет тип <code><strong>my_input_xyz_t</strong></code>, а сообщение вывода — <code><strong>my_output_xyz_t</strong></code>. Отметьте, что и «ввод», и «вывод» определяются с позиции администратора ресурса: «ввод» — это данные, поступающие <emphasis>в</emphasis> администратор ресурса, а «вывод» — это данные, поступающие <emphasis>из</emphasis> него (обратно клиенту).</p>
     <p>Нам надо придумать какой-то вызов API для клиента — мы, конечно, можем принудить клиента «вручную» заполнять структуры <code><strong>my_input_xyz_t</strong></code> и <code><strong>my_output_xyz_t</strong></code>, но я не рекомендовал бы так делать по той причине, что API призван «отвязать» реализацию передаваемого сообщения от функциональности. Давайте предположим, что API клиента у нас такой:</p>
     <p><code>int adjust_xyz(int *<emphasis>data_rate</emphasis>, int *<emphasis>оdata_rate</emphasis>,</code></p>
     <p><code> int *<emphasis>more_stuff</emphasis>);</code></p>
     <p>Теперь мы имеем хорошо документированную функцию <emphasis>adjust_xyz()</emphasis>, которая выполняет нечто полезное для клиента. Заметьте, что для передачи данных мы использовали указатели на целые числа — это просто пример реализации. Вот текст функции <emphasis>adjust_xyz()</emphasis>:</p>
     <p><code>int adjust_xyz(int *dr, int *odr, int *ms) {</code></p>
     <p><code> my_message_xyz_t msg;</code></p>
     <p><code> int sts;</code></p>
     <p><code> msg.i.data_rate = *dr;</code></p>
     <p><code> msg.i.more_stuff = *ms;</code></p>
     <p><code> sts =</code></p>
     <p><code>  io_msg(global_fd, COMMAND_XYZ, &amp;msg, sizeof(msg.i),</code></p>
     <p><code>   sizeof(msg.o));</code></p>
     <p><code> if (sts == EOK) {</code></p>
     <p><code>  *odr = msg.o.old_data_rate;</code></p>
     <p><code>  *ms = msg.o.more_stuff;</code></p>
     <p><code> }</code></p>
     <p><code> return (sts);</code></p>
     <p><code>}</code></p>
     <p>Это пример применения функции <emphasis>io_msg()</emphasis> (ее мы скоро опишем — это не стандартный библиотечный вызов!). Функция <emphasis>io_msg()</emphasis> колдует над сборкой сообщения _IO_MSG. Чтобы уйти от проблемы функции <emphasis>devctl()</emphasis> с наличием только одного параметра размера, мы дали функции <emphasis>io_msg()</emphasis> два таких параметра: один — для ввода (<code><strong>sizeof(msg.i)</strong></code>), другой — для вывода (<code><strong>sizeof(msg.о)</strong></code>). Заметьте, что мы обновляем значения <emphasis>*odr</emphasis> и <emphasis>*ms только в том случае</emphasis>, когда функция <emphasis>io_msg()</emphasis> возвращает EOK. Это обычный прием, и здесь он полезен потому, что передаваемые аргументы не изменятся, если команда не завершится успешно. (Это предохраняет клиентскую программу от необходимости держать копию передаваемых данных на случай несрабатывания функции.)</p>
     <p>Последнее, что я сделал в функции <emphasis>adjust_xyz()</emphasis>, — это зависимость от переменной <emphasis>global_fd</emphasis>, содержащей дескриптор файла администратора ресурса. Есть, опять же, множество способов обработки этого:</p>
     <p>• Скрыть дескриптор файла внутри функции <emphasis>io_msg()</emphasis> (это было бы полезно, если бы вы пожелали избавиться от необходимости передавать дескриптор файла с каждым вызовом; это хорошо в случаях, когда вы собираетесь обмениваться сообщениями только с одним администратором ресурса, и не подходит в качестве универсального решения).</p>
     <p>• Передавать от клиента дескриптор файла каждому вызову функции библиотеки API (полезно, если клиент хочет разговаривать с администратором ресурса еще и другими способами, например, стандартными POSIX-вызовами на основе файловых дескрипторов типа <emphasis>read()</emphasis>, или если клиент должен уметь общаться с несколькими администраторами ресурсов).</p>
     <p>Вот текст функции <emphasis>io_msg()</emphasis>:</p>
     <p><code>int io_msg(int fd, int cmd, void *msg, int isize,</code></p>
     <p><code> int osize) {</code></p>
     <p><code> io_msg_t io_message;</code></p>
     <p><code> iov_t rx_iov[2];</code></p>
     <p><code> iov_t tx_iov[2];</code></p>
     <p><code> int sts;</code></p>
     <p><code> // set up the transmit IOV</code></p>
     <p><code> SETIOV(tx_iov + 0, &amp;io_msg.o, sizeof(io_msg.o));</code></p>
     <p><code> SETIOV(tx_iov + 1, msg, osize);</code></p>
     <p><code> // set up the receive IOV</code></p>
     <p><code> SETIOV(rx_iov + 0, &amp;io_msg.i, sizeof(io_msg.i));</code></p>
     <p><code> SETIOV(r.x_iov + 1, msg, isize);</code></p>
     <p><code> // set up the _IO_MSG itself</code></p>
     <p><code> memset(&amp;io_message, 0, sizeof(io_message));</code></p>
     <p><code> io_message.type = _IO_MSG;</code></p>
     <p><code> io_message.mgrid = cmd;</code></p>
     <p><code> return (MsgSendv(fd, tx_iov, 2, rx_iov, 2));</code></p>
     <p><code>}</code></p>
     <p>Отметьте несколько вещей.</p>
     <p>В функции <emphasis>io_msg()</emphasis> для «инкапсуляции» специального сообщения (передаваемого в параметре «<emphasis>msg</emphasis>») в структуру <emphasis>io_message</emphasis> использован двухкомпонентный вектор ввода-вывода IOV.</p>
     <p>Структура <emphasis>io_message</emphasis> была предварительно обнулена, и в ней был задан тип сообщения (_IO_MSG), а также инициализировано поле <emphasis>cmd</emphasis> (это будет использовано администратором ресурса для определения типа посылаемого сообщения).</p>
     <p>В качестве кода завершения функции <emphasis>io_msg()</emphasis> использовался непосредственно код завершения <emphasis>MsgSendv()</emphasis>.</p>
     <p>Единственная «забавная» вещь, которую мы тут сделали, касается поля <emphasis>mgrid</emphasis>. QSSL резервирует для данного поля диапазон значений со специальным поддиапазоном для «неофициальных» драйверов. Этот поддиапазон ограничен значениями от _IOMGR_PRIVATE_BASE до IOMGR_PRIVATE_MAX соответственно. Если вы разрабатываете глубоко встраиваемую систему и хотите быть уверены, что ваш администратор ресурса не получит никаких неподходящих сообщений, то смело можете использовать значения из этого специального диапазона. С другой стороны, если вы разрабатываете в большей степени «настольную» или «обычную» систему, вы можете захотеть точно проконтролировать, будут ли вашему администратору ресурса приходит несоответствующие сообщения или нет. В этом случае вам нужно будет обратиться в QSSL за значением <emphasis>mgrid</emphasis>, зарезервированным специально для вас — никто, кроме вас, не должен использовать это номер. Посмотрите файл <code><strong>&lt;sys/iomgr.h&gt;</strong></code>, там представлены используемые в настоящее время диапазоны. В нашем вышепредставленном мы могли предположить, что COMMAND_XYZ базирована на _IOMGR_PRIVATE_BASE:</p>
     <p><code>#define COMMAND_XYZ (_IOMGR_PRIVATE_BASE + 0x0007)</code></p>
     <p>или QSSL назначила нам специальный поддиапазон:</p>
     <p><code>#define COMMAND_XYZ ( IOMGR_ACME_CORP + 0x0007)</code></p>
     <subtitle>«Клиент/сервер» с использованием администратора ввода/вывода</subtitle>
     <p>А что если клиент, которого вы переносите, использует администратор ввода/вывода? Как адаптировать его для QNX/ Neutrino? Ответ прост: мы уже это сделали. Установив интерфейс на основе файловых дескрипторов, мы уже используем администратор ресурса. В QNX/Neutrino вам почти <emphasis>никогда</emphasis> не придется использовать интерфейс «сырых» сообщений. Почему?</p>
     <p>1. Вам пришлось бы самим беспокоиться о сообщении _IO_CONNECT, поступившим с клиентским вызовом <emphasis>open()</emphasis>, или искать способ поиска администратор ресурса, альтернативный использованию <emphasis>open()</emphasis>.</p>
     <p>2. Вам пришлось бы искать способ сопоставить клиенту конкретный контекстный блок в администраторе ресурса. Это, конечно, не ракетная техника, но поработать придется.</p>
     <p>3. Вам придется инкапсулировать все ваши сообщения вручную вместо использования стандартных POSIX-функций, которые бы сделали эту работу за вас.</p>
     <p>4. Ваш администратор ресурса не будет работать с приложениями на основе <emphasis>stdin</emphasis>/<emphasis>stdout</emphasis>. В примере с аудиодрайвером вы не смогли бы просто так выполнить <code><strong>mp3_decode spud.mp3 &gt;/dev/audio</strong></code>, потому что <emphasis>open()</emphasis>, скорее всего, не сработала бы (а если бы и сработала, то не сработала бы <emphasis>write()</emphasis>, и так далее).</p>
     <subtitle>Прокси</subtitle>
     <p>В QNX4 единственным способом передачи неблокирующего сообщения было создание прокси — это делалось с помощью функции <emphasis>qnx_proxy_attach()</emphasis>. Эта функция возвращает идентификатор прокси (proxy ID), (он выбирается из того же самого пространства номеров, что и идентификаторы процессов), к которому вы затем можете применить функцию <emphasis>Trigger()</emphasis> или возвратить его из функции обработки прерывания (см. ниже).</p>
     <p>В QNX/Neutrino вы бы вместо этого настроили структуру <code><strong>struct sigevent</strong></code> на генерацию «импульса», а потом либо использовали бы функцию <emphasis>MsgDeliverEvent()</emphasis> для доставки события, либо привязали бы событие к таймеру или обработчику прерывания.</p>
     <p>Обычный прием распознавания прокси-сообщений QNX4 (полученных с помощью <emphasis>Receive()</emphasis> или <emphasis>Creceive()</emphasis>) — сравнить идентификатор процесса, возвращенный функцией приема сообщения, с ожидаемым идентификатором прокси. Если совпадают — значит, это прокси. Как вариант, идентификатор процесса можно было игнорировать и обрабатывать сообщение как «стандартное». К сожалению, это несколько усложняет проблему переноса программ.</p>
     <subtitle>Анализ прокси по идентификаторам</subtitle>
     <p>Если вы сравниваете полученный от функции приема идентификатор процесса со списком ожидаемых идентификаторов прокси, обычно вы игнорируете содержимое прокси. В конце концов, коль скоро содержимое прокси нельзя изменить после ее создания, какой прок с анализа сообщения, о котором вы уже знаете, что это одна из ваших прокси? Вы можете возразить, что это для удобства — помещаем в прокси нужные сообщения, а затем обрабатываем все сообщения одним стандартным декодером. Если это ваш случай, см. ниже «Анализ прокси по содержимому».</p>
     <p>Поэтому, в QNX4 ваш код выглядел бы примерно так:</p>
     <p><code>pid = Receive(0, &amp;msg, sizeof(msg));</code></p>
     <p><code>if (pid == proxyPidTimer) {</code></p>
     <p><code> // Сработал наш таймер, сделать что-нибудь</code></p>
     <p><code>} else if (pid == proxyPidISR) {</code></p>
     <p><code> // Сработал наш ISR, сделать что-нибудь</code></p>
     <p><code>} else {</code></p>
     <p><code> // Не наша прокси — возможно, обычное</code></p>
     <p><code> // клиентское сообщение. Сделать что-нибудь.</code></p>
     <p><code>}</code></p>
     <p>В QNX/Neutrino он заменился бы на следующий:</p>
     <p><code>rcvid = MsgReceive(chid, &amp;msg, sizeof(msg), NULL);</code></p>
     <p><code>if (rcvid == 0) { // 0 значит, что это импульс</code></p>
     <p><code> switch (msg.pulse.code) {</code></p>
     <p><code> case MyCodeTimer:</code></p>
     <p><code>  // Сработал наш таймер, сделать что-нибудь</code></p>
     <p><code>  break;</code></p>
     <p><code> case MyCodeISR:</code></p>
     <p><code>  // Сработал наш ISR, сделать что-нибудь</code></p>
     <p><code>  break;</code></p>
     <p><code> default:</code></p>
     <p><code>  // Неизвестный код импульса</code></p>
     <p><code>  break;</code></p>
     <p><code> }</code></p>
     <p><code>} else {</code></p>
     <p><code> // rcvid - не нуль, значит, это обычное</code></p>
     <p><code> // клиентское сообщение. Сделать что-нибудь.</code></p>
     <p><code>}</code></p>
     <p>Отметим, что это пример для случая, когда вы обрабатываете сообщения самостоятельно. Но поскольку мы рекомендуем использовать библиотеку администратора ресурсов, на самом деле ваша программа выглядела бы примерно так:</p>
     <p><code>int main(int argc, char **argv) {</code></p>
     <p><code> ...</code></p>
     <p><code> // Выполнить обычные инициализации</code></p>
     <p><code> pulse_attach(dpp, 0, MyCodeTimer, my_timer_pulse_handler,</code></p>
     <p><code>  NULL);</code></p>
     <p><code> pulse_attach(dpp, 0, MyCodeISR, my_isr_pulse_handler,</code></p>
     <p><code>  NULL);</code></p>
     <p><code> ...</code></p>
     <p><code>}</code></p>
     <p>На этот раз мы предписываем библиотеке администратора ресурсов ввести две проверки из предыдущего примера в основной цикл приема сообщений и вызывать две наши функции обработки (<emphasis>my_timer_pulse_handler()</emphasis> и <emphasis>my_isr_pulse_handler()</emphasis>) всякий раз, когда обнаруживаются нужные коды. Гораздо проще.</p>
     <subtitle>Анализ прокси по содержимому</subtitle>
     <p>Если вы анализируете содержимое прокси (фактически игнорируя, что это прокси, и обрабатывая их как сообщения), то вы автоматически имеете дело с тем, что в QNX4 на прокси ответить нельзя. В QNX/Neutrino ответить на импульс тоже нельзя. Это означает, что у вас уже есть код, который либо анализирует идентификатор, возвращаемый функцией приема, и определяет, что это прокси, и отвечать не надо, либо смотрит на содержимое сообщения и по нему определяет, надо отвечать на это сообщение или нет.</p>
     <p>К сожалению в QNX/Neutrino произвольные данные в импульс не запихнешь. Импульс имеет четко определенную структуру, и обойти это нельзя. Умным решением здесь было бы «имитировать» сообщение от прокси при помощи импульса и таблицы. Таблица содержала бы сообщения, которые раньше передавались посредством прокси. Получив импульс, вы использовали бы поле <emphasis>value</emphasis> в качестве индекса к этой таблице, выбрали бы из таблицы соответствующее сообщение и «притворились», что получено именно оно.</p>
    </section>
    <section>
     <title>
      <p>Обработчики прерываний</p>
     </title>
     <p>Обработчики прерываний в QNX4 могли либо возвратить идентификатор прокси (указывая этим, что надо переключить прокси и таким образом уведомить ее владельца о прерывании), либо возвратить нуль (что означало бы, что в дальнейшем ничего делать не требуется). В QNX/Neutrino механизм почти идентичен — за исключением того, что вместо возвращения идентификатора прокси вы возвращаете указатель на <code><strong>struct sigevent</strong></code>. Генерируемое событие может быть либо импульсом (ближайший аналог прокси), либо сигналом, либо созданием потока — как выберете, так и будет.</p>
     <p>Также в QNX4 вы <emphasis>обязаны были</emphasis> иметь обработчик прерывания — даже в том случае, если он должен был только возвратить идентификатор прокси. В QNX/Neutrino вы можете привязать <code><strong>struct sigevent</strong></code> к вектору прерывания, используя <emphasis>InterruptAttachEvent()</emphasis>, и это событие будет генерироваться при каждой активизации данного вектора.</p>
    </section>
   </section>
   <section>
    <title>
     <p>Резюме</p>
    </title>
    <p>Перенос приложений из QNX4 в QNX/Neutrino или поддержка программы, функционирующей в обеих операционных системах, — это возможно, если придерживаться следующих правил:</p>
    <p>• абстрагировать, абстрагировать и еще раз абстрагировать;</p>
    <p>• развязывать, развязывать и еще раз развязывать.</p>
    <p>Ключ в том, чтобы отказаться от использования конкретного «дескриптора», который «связывал» бы клиента с сервером, и не привязываться к конкретному механизму обнаружения сервера. Если вы абстрагируете операции обнаружения сервера и установления соединения в отдельный набор функций, то вы сможете производить условную компиляцию для любой платформы, на которую вы пожелаете перенести ваш код.</p>
    <p>Аналогичные рассуждения применимы и к транспортировке сообщений — всегда абстрагируйте клиентский API от «понимания» того, как сообщение доставляются от клиента к серверу, в некоторый универсальный API, который уже бы базировался на конкретном транспортном API. Затем этот конкретный транспортный API можно будет условно откомпилировать для любой платформы.</p>
    <p>Перенос <emphasis>сервера</emphasis> из QNX4 в QNX/Neutrino является более трудной задачей вследствие того, что QNX4-серверы были сделаны «вручную» и никогда не следовали четкой структуре подобно тому, как это принято в библиотеке администраторов ресурсов в QNX/Neutrino. Впрочем, в общем случае, если вы переносите что-то сильно аппаратно-зависимое (например, аудиодрайвер или блок-ориентированный дисковый драйвер), основная часть портируемого кода никак не связна с операционной системой, и ой как связана с собственно аппаратурой. Подход, который я обычно использовал для таких случаев, состоит в том, чтобы запрограммировать каркас «драйвера» и обеспечить набор четко определенных аппаратно-зависимых функций. Каркас будет отличаться для разных операционных систем, но аппаратно-зависимые функции получатся на удивление переносимыми.</p>
   </section>
  </section>
  <section>
   <title>
    <p>Приложение Б</p>
    <p>Скорая помощь</p>
   </title>
   <section>
    <title>
     <p>Обращайтесь за помощью к профессионалам</p>
    </title>
    <section>
     <p>Независимо от того, насколько вы хороший программист, иногда возникают моменты, когда вы:</p>
     <p>• сталкиваетесь с проблемой, которую не можете решить;</p>
     <p>• выявили ошибку и хотите сообщить о ней и/или найти обходной путь;</p>
     <p>• сталкиваетесь с необходимостью консультации по реализации проекта.</p>
     <p>В этой главе мы рассмотрим ресурсы, необходимые при разрешении подобных проблем.</p>
    </section>
    <section>
     <title>
      <p>Итак, у вас проблема...</p>
     </title>
     <p>Мы обсудим первые две проблемы вместе, потому что подчас бывает трудно определить, с которой из них имеешь дело.</p>
     <p>Предположим, что что-то вдруг перестало работать или работает не так, как ожидается. Что делать?</p>
    </section>
    <section>
     <title>
      <p>RTFM</p>
     </title>
     <p>Читайте документацию! (Оригинальный перевод аббревиатуры RTFM широко известен, поэтому приводить его здесь не будем; любопытные могут затянуть в словарь — например, <code><strong>http://www.instantweb.com/foldoc/</strong></code> — прим. ред.) Несмотря на то, что это может показаться элементарным и очевидным первым шагом, число людей, которые этого не делают, просто поразительно!</p>
     <p>Все нижеперечисленные справочные руководства по QNX/Neutrino доступны он-лайн:</p>
     <p>• <emphasis>«Building Embedded Systems» («Построение встраиваемых систем»)</emphasis>;</p>
     <p>• <emphasis>«Development Tools» («Средства разработки»)</emphasis>;</p>
     <p>• <emphasis>«Library Reference» («Справочник по библиотекам»)</emphasis>;</p>
     <p>• <emphasis>«System Architecture» («Системная архитектура»)</emphasis>;</p>
     <p>• <emphasis>«Technotes» («Технические замечания»)</emphasis>;</p>
     <p>• <emphasis>«Utilities Reference» («Справочник по утилитам»)</emphasis>.</p>
     <subtitle>«Building Embedded Systems» («Построение встраиваемых систем»)</subtitle>
     <p>В этой книге содержится вся информация, необходимая для встраивания QNX/Neutrino — то есть чтобы создать систему на основе QNX/Neutrino и заставить ее работать. В ней есть главы по среде разработки программных продуктов (как компилировать, компоновать и отлаживать программы для QNX/Neutrino), построению образа ОС (как создать образ, как встроить этот образ глубоко встраиваемую систему, как заставить его работать на нужной платформе), а также кое-какие общие рекомендации по проектированию.</p>
     <subtitle>«Development Tools» («Средства разработки»)</subtitle>
     <p>Это справочник по различным средствам разработки, о которых упоминается в книге «<emphasis>Построение встраиваемых систем</emphasis>» — там есть документация по GNU-компилятору и сопутствующим инструментальным средствам, описания средств формирования образов, и т.д. Это прекрасный источник информации для тех случаев, когда инструмент ведет себя не так, как вы от него ожидаете, или когда нужно понять, как работает тот или иной инструмент.</p>
     <subtitle>«Library Reference» («Справочник по библиотекам»)</subtitle>
     <p>Это руководство по вызовам Си-библиотеки QNX/Neutrino — от А до Z. Применительно к вызовам функций это — истина в высшей инстанции. Я часто ссылался на это издание в предыдущих главах (например, за более подробной информацией о конкретной функции — скажем, о ее редко используемых аргументах).</p>
     <subtitle>«System Architecture» («Системная архитектура»)</subtitle>
     <p>«Высокоуровневый» документ по системной архитектуре QNX/Neutrino, описывающий ее «сверху» и в то же время приводящий достаточно подробное описание реализации, из которого вы сможете понять, что из себя представляют компоненты системы, и как они связаны между собой.</p>
     <subtitle>«Technotes» («Технические замечания»)</subtitle>
     <p>Серия книг «<emphasis>Технические замечания</emphasis>» содержит описание специфических особенностей QNX/Neutrino и может меняться от версии к версии. Посмотрите онлайновую версию этих документов, чтобы узнать, что характерно для имеющейся у вас версии QNX/Neutrino. Например, для релиза QNX/Neutrino 2.00 BETA 5 в серии «Технические замечания» были рассмотрены следующие темы:</p>
     <p>• <emphasis>Переход с Neutrino 1.X на версию 2.0</emphasis>;</p>
     <p>• <emphasis>Интерфейс диспетчеризации в QNX/Neutrino 2.0</emphasis>.</p>
     <subtitle>«Utilities Reference» («Справочник по утилитам»)</subtitle>
     <p>Этом алфавитный справочник по доступным командно-строковым утилитам. Он описывает все утилиты, не относящиеся к средствам разработки, то есть те, которые не охвачены справочником «<emphasis>Средства разработки</emphasis>» — <code><strong>grep</strong></code>, <code><strong>саt</strong></code>, <code><strong>echo</strong></code>, и т.д.</p>
    </section>
    <section>
     <title>
      <p>Свяжитесь с технической поддержкой</p>
     </title>
     <p>Когда вы определитесь, что дело не в вашем недопонимании вызова функции или утилиты и не в опечатке, добро пожаловать за помощью в королевство технической поддержки QSSL. Бояться нечего — большинство заказчиков очень довольны уровнем технической поддержки, которую они получают от QSSL.</p>
     <p>Обращаться за помощью в группу технической поддержки QSSL можно двояко: либо по телефону, либо через телеконференции QUICS (еще раз отмечу: эта информация устарела, на настоящий момент QUICS упразднена в пользу QDN (<code><strong>http://qdn.qnx.com</strong></code>, <code><strong>nntp://inn.qnx.com</strong></code>) — <emphasis>прим. ред</emphasis>.).</p>
     <p>Прежде чем мы решим, каким способом лучше воспользоваться, хотелось бы упомянуть ряд подготовительных действий, выполнив которые, вы сможете свести время получения ответа на ваш вопрос к минимуму.</p>
     <subtitle>Опишите проблему</subtitle>
     <p>Как правило, заказчики пытаются решить проблему самостоятельно, пробуя различные варианты, которые им приходят на ум. Это великолепно. Только, к сожалению, это часто приводит к тому, что заказчик приходит в уныние и вывешивает в телеконференцию сообщение типа:</p>
     <p><code>«Я тут запустил TCP/IP, пытаюсь соединиться</code></p>
     <p><code>с Windows-машиной, а оно не работает.</code></p>
     <empty-line/>
     <p><code>В чем дело?!?»</code></p>
     <p>Первый ответ службы технической поддержки будет выглядеть примерно так (бьюсь об заклад, у них даже есть специальный шаблон для этого):</p>
     <p><code>Что значит «не работает»? Вы имеете в виду TCP/IP на</code></p>
     <p><code>стороне QNX или на стороне Windows? Какой модуль TCP/IP</code></p>
     <p><code>не работает? Что вы пытаетесь сделать? Какие у вас</code></p>
     <p><code>версии QNX и TCP/IP? Какая у вас версия Windows и какой</code></p>
     <p><code>там TCP/IP?</code></p>
     <p>Мораль сей басни такова: если у вас проблема, то вам, вероятно, надо ее решить быстро. Если вам нужен ответ на вопрос быстро, постарайтесь привести максимум информации о возникшей у вас проблеме в первом же запросе, чтобы специалисты технической поддержки QSSL сразу же смогли попробовать воспроизвести вашу проблемную ситуацию.</p>
     <p>Существует ряд вещей, которые служба технической поддержки спрашивает всегда. К ним относятся:</p>
     <p>• точное описание сбоя;</p>
     <p>• версии программных продуктов;</p>
     <p>• конфигурация системы;</p>
     <p>• аппаратная платформа (x86, РРС, и т.д.)</p>
     <subtitle>Точная информация</subtitle>
     <p>Чтобы предоставить эту информацию, опишите, что вы ожидали, и что произошло на самом деле. Вышеприведенный пример выглядел бы гораздо лучше в таком варианте:</p>
     <p>Я пытаюсь подсоединиться по telnet из QNX/Neutrino 2.00A к Windows-машине и сразу же после приглашения login получаю сообщение «Connection closed by foreign host».</p>
     <subtitle>Версии</subtitle>
     <p>Следующее, что следует указать, — это информация о версиях различных модулей, которые вы используете. Ее можно взять от команд <code><strong>ls</strong></code> и <code><strong>cksum</strong></code>. В случае с нашим примером хорошо было бы бы сообщить службе техподдержки, какая у вас версия команды <code><strong>telnet</strong></code>, стека TCP/IP, и т.д.</p>
     <p><code># cd /usr/nto/x86</code></p>
     <p><code># ls -l bin/telnet dll/devn-ne2000.so dll/npi-ttcpip.so</code></p>
     <p><code>-rwxr-xr-x 1 root root  71588 May 19 11:45 bin/telnet*</code></p>
     <p><code>-rwxr-xr-x 1 root root 680928 May 19 11:05 dll/devn-no2000.so*</code></p>
     <p><code>-rwxr-xr-x 1 root root  83765 Jun 02 06:39 dll/npi-ttcpip.so*</code></p>
     <p><code># cksum bin/telnet dll/devn-ne2000.so dll/npi-ttcpip.so</code></p>
     <p><code>1574162245  71588 bin/telnet</code></p>
     <p><code>3564123752 680928 dll/devn-ne2000.so</code></p>
     <p><code>2582029317  83765 dll/npi-ttcpip.so</code></p>
     <p>Это даст сотрудникам техотдела примерную картину дат, размеров и контрольных сумм ряда объектов, которые, возможно, связаны с проблемой. Отметьте строку «<code><strong>cd /usr/nto/x86</strong></code>» — она говорит, что я использую средства разработки в версии для x86.</p>
     <p>Если у вас есть подозрение, что проблема может быть связана с особенностями платформы, вам, безусловно, следует указать ее название, торговую марку (brand) и соответствующие применяемые в ней чипсеты.</p>
     <p>Также служба технической поддержки, как правило, требует описания конфигурации вашей системы — особенно когда есть подозрение, что проблема связана с недостатком памяти, лицензированием, и т.п. Вы должны попытаться дать полную информацию относительно объема установленной памяти, количестве выполняющихся процессов, приблизительной степени загрузки, и т.д:</p>
     <p>Чем более подробную информацию вы предоставите, тем быстрее вам смогут помочь.</p>
     <subtitle>Если у вас бета...</subtitle>
     <p>Если вы используете бета-версию программного обеспечения (то есть вы подписаны на бета-программу QSSL), вся вышеперечисленная информация критична, потому ваши версии программного обеспечения, скорее всего, будут отличаться от официально выпущенных. Однако, имейте в виду, что в общем случае поддержка бета-версий по телефону в QSSL не предусмотрена, так что здесь единственный способ получить информацию — написать в телеконференцию или, если разработчик запрашивает прямой контакт с вами, поговорить с разработчиком напрямую.</p>
     <p>В любом случае одним из лучших решений являются телеконференции, потому что они дают возможность и другим участникам видеть, какие есть проблемы, и как с ними быть (то есть если найдена ошибка, то какие есть обходные пути). Как бы там ни было, вышеупомянутая информация критична для определения того, какие именно программные продукты из бета-релиза вы применяете.</p>
     <p>Также, если вы обмениваетесь информацией с разработчиком, то имейте в виду, что у него, как правило, есть еще тысяча дел, и он вряд ли сможет ответить на ваш запрос сразу. Дружеский «ping» спустя несколько дней молчания делу не повредит, а вот повторное требование ответа через 15 минут после запроса определенно не прибавит вам новых друзей.</p>
     <p>Что часто проявляется при работе с бета-версиями, так это то, что люди забывают ставить обновления. Механизм же бета-тестирования работает так, что пропуск обновления запросто может заставить вашу систему вести себя странновато. Некоторые новые драйверы или администраторы ресурсов могут вести себя со своими клиентами совершенно иначе, чем в предыдущих версиях.</p>
     <p>В этом случае вы должны быть уверены (потому что вас обязательно спросят!), что все обновления установлены, и в нужном порядке.</p>
     <subtitle>Воспроизведите проблему</subtitle>
     <p>Один из первых вопросов персонала технической поддержки обычно звучит так: «А оно происходит исключительно с бухты-барахты, или вы можете это повторить намеренно?»</p>
     <p>Это не праздное любопытство. Если проблема проявляется редко, она столь же серьезна, как и проблема, которая проявляется регулярно. Главное — понять, с какой стороны подойти.</p>
     <p>Обычно в случае редко проявляющейся проблемы персонал технической поддержки рекомендует сконфигурировать операционную систему и другие компоненты так, чтобы когда проблема проявится вновь, остался какой-нибудь отчет о состоянии системы или был вызван отладчик — чтобы можно было потом разобраться, что произошло.</p>
     <p>Если проблемная ситуация легко воспроизводится, то технический персонал захочет воспроизвести ее на месте, чтобы продемонстрировать разработчику на «живой» системе. «Смотри-ка! Все умирает, стоит мне сде…»</p>
     <subtitle>Сузьте круг поиска</subtitle>
     <p>Даже если проблема воспроизводима, персонал технической поддержки, скорее всего, не будет в восторге от перспективы переворошить 6000 строк вашей Си-кода в поисках затаившейся в его недрах ошибки.</p>
     <p>В большинстве случаев, которые мне доводилось видеть, местоположение ошибки можно было сузить примерно до 20–30 строк <emphasis>максимум</emphasis>. Большие файлы могут быть действительно полезны для анализа в тех случаях, когда вы подозреваете, что ошибка связана с размерами объектов, а не с библиотеками или ядром. Например, у некоторых утилит может быть задан размер массива по умолчанию, и попытка увеличить его размер ведет к проблеме. В этом случае персонал технической поддержки может запросить у вас <code><strong>tar</strong></code>-архив, в котором содержится <emphasis>все</emphasis>. К счастью, создавать tar-архивы несложно. Например, если каталог вашего проекта называется <code><strong>/src/projects/xyzzy</strong></code>, и техперсонал хочет посмотреть все, что там находится, вы можете сделать следующее:</p>
     <p><code># cd /src/projects</code></p>
     <p><code># tar cvf xyzzy.tar xyzzy</code></p>
     <p>Это «высосет» все содержимое каталога <code><strong>xyzzy</strong></code> (включая все подкаталоги!) в файл с именем <code><strong>xyzzy.tar</strong></code>. Если результирующий <code><strong>tar</strong></code>-файл получится большим, вы сможете сэкономить время на его загрузку и место на диске, если сожмете его с помощью утилиты <code><strong>gzip</strong></code>:</p>
     <p><code># gzip -9v xyzzy.tar</code></p>
     <p><code>xyzzy.tar: 60.2% — replaced with xyzzy.tar.gz</code></p>
     <p>Вы сможете затем переслать полученный файл <code><strong>xyzzy.tar.gz</strong></code> персоналу техподдержки (лучше, конечно, по FTP, чем электронным письмом :-).</p>
     <subtitle>Как сообщить об ошибке</subtitle>
     <p>Если вы убеждены в том, что нашли ошибку (например, утилита «падает» по SIGSEGV), вы можете не выставлять ее на всеобщее обсуждение в телеконференцию, а напрямую сообщить об ошибке в QSSL при помощи команды <code><strong>bug</strong></code> на QUICS. (Этот способ также устарел. Теперь для сообщения об ошибках используется специальная веб-форма на QDN — <code><strong>http://qdn.qnx.com/report/problem_report.html</strong></code> — <emphasis>прим. ред</emphasis>.)</p>
    </section>
   </section>
   <section>
    <title>
     <p>Другие источники информации</p>
    </title>
    <section>
     <p>Кроме QUICS, существует ряд других информационных ресурсов, куда можно обращаться за помощью, информацией или услугами. Ниже приведен далеко не полный список, но по крайней мере у вас будет с чего начать.</p>
     <p><code><strong>www.qnx.com</strong></code></p>
     <p>Веб-сайт компании QSSL, особенно его раздел техподдержки (<code><strong>qdn.qnx.com</strong></code> — <emphasis>прим. ред</emphasis>.), наполнен полезной информацией и файлами.</p>
     <p><code><strong>comp.os.qnx</strong></code></p>
     <p>Это обычная телеконференция USENET. Она была создана для пользователей QNX, чтобы они могли вместе обсуждать проблемы, особенности, решения, и т.п.</p>
     <p>Персонал QSSL не всегда следит за этой конференцией (хотя некоторые там все-таки прячутся). Вообще это конференция для тех, у кого есть вопросы, но нет продуктов QNX. В эту конференцию периодически вывешивается сборник ЧаВо (Часто задаваемые Вопросы, или Frequently Asked Questions — FAQ — <emphasis>прим. ред</emphasis>.), из которого можно узнать много полезного о продуктах.</p>
     <p>Если вам нужен <emphasis>гарантированный</emphasis> ответ, то лучшим выбором была и остается QUICS.</p>
    </section>
    <section>
     <title>
      <p>Каталог «третьих» фирм — продукты и консалтинг</p>
     </title>
     <p>Компания QSSL издает Каталог «третьих» фирм (Third Party Directory), в котором перечислены все сторонние компании и их продукты для QNX (например, многопортовые платы последовательного интерфейса, аппаратные средства стандарта X.25, программное обеспечение), а также представлен список организаций и лиц, оказывающих консалтинговые услуги при разработке проектов.</p>
     <p>Данный каталог доступен от QSSL, для получения копии обращайтесь к своему торговому представителю.</p>
     <subtitle>QNXnews</subtitle>
     <p>Компания QSSL издает ежеквартальный журнал, называемый QNXnews. Он содержит статьи о заказчиках компании QSSL, примеры применения ими QNX в своих продуктах и решениях, а также объявления от QSSL и «третьих» фирм о новых продуктах и услугах.</p>
     <p>Подписка на этот журнал также доступна в QSSL. Обращайтесь к вашему торговому представителю для получения дополнительной информации</p>
     <subtitle>Обучение</subtitle>
     <p>И, наконец, некоторые компании предлагают курсы обучения по программным продуктам QNX — за подробностями обращайтесь в отдел обучения QSSL</p>
    </section>
   </section>
  </section>
  <section>
   <title>
    <p>Глоссарий</p>
   </title>
   <subtitle>Neutrino</subtitle>
   <p>Цитата из веб-странички сайта Обсерватории по изучению нейтрино, расположенной в Садбери (Sudbury Neutrino Observatory) (см. <code><strong>http://www.sno.phy.gueensu.ca</strong></code>):</p>
   <p>«Нейтрино — это мельчайшие, возможно невесомые, нейтральные элементарные частицы, которые взаимодействуют с веществом через слабые ядерные силы взаимодействия. Незначительность этих сил взаимодействия наделяет нейтрино свойством свободного прохождения через вещество — говорят, что материя является почти прозрачной для нейтрино. Солнце и все другие звезды в результате процессов синтеза и распада в пределах ядра генерируют мощные потоки нейтрино. Поскольку нейтрино взаимодействуют редко, они проникают через Солнце и Землю (и вас) беспрепятственно. Другими источниками нейтрино являются взрывающиеся звезды (сверхновые), реликтовые нейтрино (от рождения вселенной) и атомные электростанции (при работе которых много энергии топлива уносится потоком нейтрино). Например, Солнце генерирует более чем две сотни триллионов триллионов триллионов нейтрино каждую секунду, а взрыв сверхновой может выдать поток нейтрино в тысячи раз больше, чем наше Солнце, которое произведет его в течение всех 10 миллиардов лет своей жизни. Поток в миллиарды нейтрино каждую секунду пронизывает Ваше тело и всего только одна или две частицы из частиц с наибольшей энергией рассеиваются в Вас за всю Вашу жизнь.»</p>
   <subtitle>PDP-8</subtitle>
   <p>Старинный компьютер, создан в период между 1965 и 1970 гг. компанией Digital Equipment Corporation (DEC; в настоящее время — Compaq) и имел самую продвинутую (в оригинале было «coolest» — <emphasis>прим. ред</emphasis>.) по тем временам переднюю панель. Также это был первый компьютер, который мне довелось программировать. Если у вас есть такой (особенно PDP-8/1) или его фрагменты, инструкции и т.д., известите меня об этом по электронной почте (адрес: <code><strong>rk@parse.com</strong></code>) — я их коллекционирую! К сожалению, эта замечательная 12-разрядная машина не работает под Neutrino. :(</p>
   <subtitle>pthreads</subtitle>
   <p>Собирательное имя для набора вызовов функций <emphasis>pthread_*()</emphasis>. Подавляющее большинство этих вызовов определены стандартом POSIX (Portable Operating System Interface for Unix) и используется при работе с потоками.</p>
   <subtitle>QNX Software Systems Limited</subtitle>
   <p>Компания-разработчик операционных систем QNX2, QNX4 и QNX/Neutrino (QNX6). </p>
   <subtitle>QSSL</subtitle>
   <p>Аббревиатура от QNX Software Systems Limited. </p>
   <subtitle>абсолютный таймер (absolute timer)</subtitle>
   <p>Таймер, истекающий в фиксированный момент времени, например, 20 января 2005 года в 9 часов 42 минуты утра по восточному поясному времени (EDT — Eastern Daylight Time). Сравните с определением «относительный таймер».</p>
   <subtitle>администратор ресурса (resource manager)</subtitle>
   <p>Сокращенно также «resmgr». Это серверный процесс, предоставляющий произвольному клиенту ряд строго определенных сервисов на основе файловых дескрипторов. Администратор ресурса поддерживает некоторый ограниченный набор сообщений, которые соответствуют стандартным клиентским библиотечным функциям Си, таким как <emphasis>open()</emphasis>, <emphasis>write()</emphasis>, <emphasis>lseek()</emphasis>, <emphasis>devctl()</emphasis> и т.д.</p>
   <subtitle>асинхронный (asynchronous)</subtitle>
   <p>Понятие, используемое для того, чтобы указать на то, что данная операция не синхронизирована по отношению к другой операции. Например, временные метки, которые генерируются микросхемой системного таймера, как говорят, «асинхронны» в отношении потока, запрашивающего задержку на некоторое время, поскольку запрос на задержку никак не синхронизирован с возникающим прерыванием отсчета таймера. Сравните с определением «синхронный».</p>
   <subtitle>атомарная операция (atomic operation)</subtitle>
   <p>Операция, которая является «неделимой», то есть такая, которая не может быть прервана любой другой операцией. Атомарные операции критично важны, особенно в подпрограммах обработки прерываний и многопоточных программах, поскольку последовательность событий типа «проверить и установить», которая осуществляется в одном потоке, должна быть гарантирована от прерывания другим потоком. Любую последовательность можно сделать атомарной с точки зрения защищённости от вмешательства других потоков, применяя мутексы или — в обработчиках прерываний — функции <emphasis>InterruptLock()</emphasis> и <emphasis>InterruptUnlock()</emphasis>. См. также заголовочный файл <code><strong>&lt;atomic.h&gt;</strong></code>.</p>
   <subtitle>атрибутная запись (attribute structure)</subtitle>
   <p>Структура, используемая в пределах администратора ресурса и содержащая информацию, относящуюся к устройству, которое администратор ресурса декларирует в пространстве имен путей, Если администратор ресурса декларирует несколько устройств (например, администратор последовательного порта может объявить <code><strong>/dev/ser1</strong></code> и <code><strong>/dev/ser2</strong></code>), он будет поддерживать соответствующее число атрибутных записей. Сравните с определением OCB.</p>
   <subtitle>барьер (barrier)</subtitle>
   <p>Объект синхронизации на уровне потоков, которому соответствует некое значение счетчика. Потоки, запрашивающие блокировку по барьеру (функция <emphasis>barrier_wait()</emphasis>), блокируются до тех пор, пока число потоков, запросивших блокировку, не станет равным указанному значению; как только это произойдет, все эти потоки будут разблокированы. Сопоставьте это с работой семафора.</p>
   <subtitle>блок управления открытым контекстом (open context block, сокр. OCB</subtitle>
   <p>Структура данных, используемая администратором ресурсов и содержащая информацию по каждому клиентскому запросу типа <emphasis>open()</emphasis>. Если клиент открыл несколько файлов, то для каждого дескриптора файла, который этот клиент имеет у соответствующих администраторов ресурсов, будет существовать соответствующий блок управления открытым контекстом (OCB). Сравните с атрибутной записью.</p>
   <subtitle>блокирование (blocking)</subtitle>
   <p>Средство синхронизации потоков по отношению к другим потокам или событиям. В заблокированном состоянии (которых имеется порядка дюжины) поток не расходует процессорное время — он находится в ожидании в списке, поддерживаемом в пределах ядра. Когда происходит ожидаемое потоком событие, поток разблокируется и снова становится способным использовать процессор.</p>
   <subtitle>вектор ввода/вывода (I/O Vector, сокр. IOV)</subtitle>
   <p>Структура, в которой каждый элемент содержит указатель и длину. Обычно применяются не одиночные векторы ввода/ вывода, а массивы векторов — такой массив структур из указателей и длин определяет список фрагментов сообщения для операции фрагментации/дефрагментации (scatter/gather), позволяющей выполнять обмен сообщениями намного эффективнее (в противном случае, чтобы сформировать один непрерывный буфер, данные пришлось бы копировать по отдельности).</p>
   <subtitle>взаимная блокировка (deadlock)</subtitle>
   <p>Аварийная ситуация, которая возникает, когда два потока взаимно заблокированы, ожидая друг от друга ответ. Это состояние можно легко воспроизвести: просто заставьте два потока отправить друг другу сообщение — с этого момента оба потока перейдут в состояние ожидания ответа. Поскольку оба потока блокированы, они не имеют возможности ответить, следовательно, наблюдается тупиковая ситуация. Для исключения взаимной блокировки клиенты и серверы должны придерживаться иерархического принципа обмена.</p>
   <subtitle>виртуальная память (virtual memory)</subtitle>
   <p>Система виртуальной памяти — это система, в которой виртуальное адресное пространство может, но не обязательно, быть преобразовано на основе взаимно-однозначного соответствия с физическим адресным пространством. Типовым примером этого (на момент написания книги в QNX/Neutrino это не поддерживается) является система «со страничной организацией памяти», где в случае недостатка ОЗУ некоторые фрагменты адресного пространства процессов могут быть выгружены на диск. Что QNX/Neutrino действительно поддерживает, так это динамическое распределение стековых страниц.</p>
   <subtitle>виртуальный адрес (virtual address)</subtitle>
   <p>Адрес, которому не обязательно соответствует физический адрес. В QNX/Neutrino все потоки работают в режиме виртуальной адресации, когда виртуальные адреса транслируются в физические при помощи диспетчера памяти. Сравните с понятиями «физический адрес» и «виртуальная память».</p>
   <subtitle>выравнивание (alignment)</subtitle>
   <p>Выравнивание — характеристика операции, при которой доступ к N-байтовому элементу данных должен выполняться только по адресу, кратному N. Например, чтобы обратиться к 4-байтовому целому числу, адрес этого целого числа должен быть кратным 4 байтам (например, 0x2304B008, а не 0x2304B009). В архитектуре некоторых процессоров (CPU) при попытке невыровненного доступа генерируется ошибка выравнивания (alignment fault). В архитектуре других процессоров (например, x86) невыровненный доступ осуществляется медленнее, чем выровненный доступ.</p>
   <subtitle>диспетчер памяти (Memory Management Unit, сокр. MMU)</subtitle>
   <p>Аппаратный блок (обычно интегрированный с центральным процессором), который обеспечивает трансляцию виртуальных адресов в физические и может использоваться для реализации системы виртуальной памяти. В QNX/Neutrino главным преимуществом применения диспетчер памяти является возможность обнаружить момент, когда поток обращается к виртуальному адресу, который не отображен в адресное пространство соответствующего процесса.</p>
   <subtitle>диспетчеризация FIFO (FIFO scheduling)</subtitle>
   <p>При диспетчеризации FIFO (First In — First Out) поток будет использовать процессор до тех пор, пока поток с более высоким приоритетом не перейдет в состояние готовности, или пока поток добровольно не освободит процессор. Если не существует потоков с более высокими приоритетами, и поток добровольно не освобождает процессор, он будет выполняться вечно. Сопоставьте с карусельной диспетчеризацией.</p>
   <subtitle>идентификатор отправителя (receive ID)</subtitle>
   <p>Когда сервер принимает сообщение от клиента, функции сервера <emphasis>MsgReceive()</emphasis> или <emphasis>MsgReceivev()</emphasis> возвращают идентификатор отправителя (часто сокращенно в программах называемый «<emphasis>rcvid</emphasis>»). Этот идентификатор <emphasis>rcvid</emphasis> затем используется по отношению к заблокированному клиенту как дескриптор, позволяя серверу отправить клиенту ответ с данными, там самым разблокировав его. После использования <emphasis>rcvid</emphasis> для ответа клиенту, он перестает иметь значение для всех вызовов функций, кроме функции <emphasis>MsgDeliverEvent()</emphasis>.</p>
   <subtitle>идентификатор соединения (connection ID, сокр. CID)</subtitle>
   <p>Дескриптор, возвращаемый функцией <emphasis>ConnectAttach()</emphasis> (на клиентской стороне) и используемый для всех операций обмена данными между клиентом и сервером. Идентификатор соединения аналогичен дескриптору файла в терминах стандартной библиотеки языка Си — иными словами, когда функция <emphasis>open()</emphasis> в QNX/Neutrino возвращает дескриптор файла, реально возвращается идентификатор соединения.</p>
   <subtitle>иерархический принцип обмена (send hierarchy)</subtitle>
   <p>Принятая в QNX/Neutrino концепция, в силу которой отправляемые сообщения передаются в одном направлении, а ответы на сообщения — в другом. Основной целью реализации иерархического принципа обмена является необходимость исключения состояния взаимной блокировки потоков. Иерархический принцип реализуется назначением клиентам и серверам «уровней иерархии» и обеспечения того, чтобы сообщения передавались только на более высокий уровень иерархии. Это исключает ситуации взаимной блокировки, когда два потока посылают сообщения друг другу, потому что такая ситуация нарушила бы принцип — поток не должен отправлять сообщения другому, если тот находится на нижнем уровне иерархии.</p>
   <subtitle>импульс (pulse)</subtitle>
   <p>Неблокирующее сообщение, получаемое аналогично обычному сообщению. Это сообщение является неблокирующим для отправителя; получатель же может ожидать его применением стандартных функций обмена сообщениями (<emphasis>MsgReceive()</emphasis> и <emphasis>MsgReceivev()</emphasis>) или же, если необходимо ждать именно импульса, то при помощи функции <emphasis>MsgReceivePulse()</emphasis>. В то время как большинство сообщений обычно посылаются от клиента к серверу, импульсы обычно пересылаются в противоположном направлении, чтобы не нарушать иерархический принцип обмена (это вызвало бы взаимную блокировку). Сравните с сигналом.</p>
   <subtitle>исходящий вызов ядра (kernel callout)</subtitle>
   <p>Операционная система QNX/Neutrino может быть настроена для функционирования на различных аппаратных средствах без необходимости в лицензии на исходный код. Для этого необходимо предусмотреть в начальном загрузчике возможность обработки исходящих вызовов ядра. Механизм исходящих вызовов ядра позволяет разработчику добавлять в систему свой код, «знающий» о специфике оборудования — например, как опрашивать контроллер прерывания о том, какое прерывание произошло, или о том, как настроить таймер на регулярную генерацию прерываний, и т.п. Это очень подробно изложено в книге «Building Embedded Systems» («Построение встраиваемых систем»). </p>
   <subtitle>канал (channel)</subtitle>
   <p>Абстрактный объект, через который сервер принимает сообщения. Это тот же самый объект, к которому клиент подключается, чтобы отправить сообщение серверу. При создании канала с помощью <emphasis>ChannelCreate()</emphasis> возвращается идентификатор канала («channel ID», сокращенно «chid») — тот самый идентификатор канала, который назначается администратором ресурса каждой объявляемой им точке монтирования.</p>
   <subtitle>карусельная диспетчеризация (round robin (RR) scheduling)</subtitle>
   <p>При карусельной диспетчеризации поток использует процессор до тех пор, пока либо не будет готов к работе поток с более высоким приоритетом, либо пока этот поток добровольно не освободит процессор, либо пока не истечет выделенный данному потоку квант времени. Если потоков с более высоким приоритетом нет, поток добровольно не освобождает процессор, и не существует других потоков с тем же самым приоритетом, поток будет выполняться вечно. Если удовлетворены все вышеизложенные условия, за исключением того, что становится готов к работе другой поток с таким же самым приоритетом, то предыдущий поток освободит процессор после того, как истечет выделенный ему квант времени — таким образом, другой поток будет иметь шанс на обслуживание. Сравните с диспетчеризацией FIFO.</p>
   <subtitle>клиент (client)</subtitle>
   <p>Архитектура обмена сообщениями в QNX/Neutrino имеет клиент- серверную структуру. Клиент является тем, кто запрашивает услугу у определенного сервера. Обычно клиент запрашивает услуги, используя функции, ориентированные на работу со стандартными файловыми дескрипторами (например, <emphasis>lseek()</emphasis>). Эти функции являются синхронными в том отношении, что вызов, сделанный клиентом, не возвращает ответ до тех пор, пока не будет завершена обработка запроса сервером. Любой поток может являться одновременно и клиентом, и сервером.</p>
   <subtitle>мутекс (mutex, от mutual exclusion — «взаимное исключение»)</subtitle>
   <p>Объект, применяемый для упорядочения последовательности доступа потоков к ресурсу — так, чтобы к ресурсу, определяемому мутексом, в конкретный момент времени имел доступ только один поток. Например, используя мутекс всякий раз при обращении к некоторой переменной, вы гарантируете, что только один поток в данный момент времени имеет к ней доступ, там самым предотвращая гонки. См. также «атомарная операция».</p>
   <subtitle>обмен сообщениями (message-passing)</subtitle>
   <p>Операционная система QNX/Neutrino имеет в своей основе модель обмена сообщениями, в которой все сервисы предоставляются синхронно, передачей сообщения от клиента к серверу и обратно. Клиент посылает сообщение серверу и блокируется. Сервер принимает сообщение от клиента, выполняет обработку запроса и затем отвечает на сообщение клиента, разблокируя его.</p>
   <subtitle>обработчик прерывания (interrupt service routine)</subtitle>
   <p>Подпрограмма, которой ядро передает управление (в привилегированном режиме) в результате аппаратного прерывания. Эта подпрограмма не имеет права выполнять системные вызовы и должна обеспечить возврат управления как можно скорее, поскольку ее приоритет реально выше, чем у любого потока в системе. Обработчики прерываний в QNX/ Neutrino могут возвращать структуру типа <code><strong>struct sigevent</strong></code>, которая указывает, какое событие, если нужно, следует сгенерировать.</p>
   <subtitle>ответить на сообщение (reply to а message)</subtitle>
   <p>Сервер отвечает на сообщение клиента, чтобы доставить клиенту результаты обработки его запроса.</p>
   <subtitle>относительный таймер (relative timer)</subtitle>
   <p>Таймер с моментом истечения, определяемым как смещение от текущего момента времени, например, «через 5 минут». Сравните с абсолютным таймером.</p>
   <subtitle>передать сообщение (send a message)</subtitle>
   <p>Поток может передавать сообщения другому потоку. Для передачи сообщения применяется семейство функций <emphasis>MsgSend*()</emphasis>; передающий сообщение поток блокируется до тех пор, пока принимающий поток не ответит на это сообщение (см. «обмен сообщениями»). Поток, который передает сообщение, считается клиентом.</p>
   <subtitle>периодический таймер (periodic timer, repetitive timer)</subtitle>
   <p>Абсолютный или относительный таймер, который после истечения времени отсчета автоматически перезагружается, и продолжает делать так до явной отмены. Полезен для приема регулярных уведомлений.</p>
   <subtitle>поток управления, «нить» (thread)</subtitle>
   <p>Одиночный диспетчеризуемый поток управления. Реализация потоков обеспечивается непосредственно ядром Neutrino и соответствуют вызовам функций POSIX <emphasis>pthread*()</emphasis>. Поток может быть синхронизирован с другими потоками (если таковые имеются) путем применения различных примитивов синхронизации, таких как мутексы, условные переменные, семафоры и т.д. Потоки подвергаются диспетчеризации по типу FIFO или RR (карусельного типа).</p>
   <subtitle>принять сообщение (receive a message)</subtitle>
   <p>Поток может принять сообщение, вызвав функцию <emphasis>MsgReceive()</emphasis> или <emphasis>MsgReceivev()</emphasis>. Если сообщений нет, поток заблокируется в ожидании сообщения (см. «обмен сообщениями»). Поток, который принимает сообщение, считается сервером.</p>
   <subtitle>процесс (process)</subtitle>
   <p>Недиспетчеризируемый объект, занимающий память и вмещающий в себя один или более потоков.</p>
   <subtitle>разблокировать (unblock)</subtitle>
   <p>Ранее заблокированный поток будет разблокирован, когда условие, на основе которого он был заблокирован, будет удовлетворено. Например, поток может быть заблокирован в ожидании сообщения. Как только ему будет послано сообщение, он будет разблокирован.</p>
   <subtitle>семафор (semaphore)</subtitle>
   <p>Примитив синхронизации потоков, с которым ассоциируется счетчик. Потоки могут вызывать функцию <emphasis>sem_wait()</emphasis>, и не будут при этом блокироваться, если в момент запроса счетчик имел ненулевое значение. Вызывая функцию <emphasis>sem_wait()</emphasis>, поток уменьшает значение счетчика. Если поток вызывает <emphasis>sem_wait()</emphasis> в тот момент, когда счетчик равен нулю, поток блокируется, пока некоторый другой поток не вызывает функцию <emphasis>sem_post()</emphasis>, увеличивая тем самым значение счетчика. Сравните с барьером.</p>
   <subtitle>сервер, серверный процесс (server)</subtitle>
   <p>Сервер представляет собой обычный процесс, работающий в кольце пользователя и предоставляющий клиентам определенные сервисы (обычно на основе файловых дескрипторов). Серверы обычно являются администраторами ресурсов, и существует обширная специализированная библиотека функций, созданная компанией QSSL и содержащая готовые реализации многих функциональных возможностей администраторов ресурсов. Работа сервера состоит в том, чтобы принимать сообщения от клиентов, обрабатывать запросы и затем отвечать на сообщения, тем самым разблокируя клиентов. Любой поток может одновременно являться и клиентом, и сервером.</p>
   <subtitle>сигнал (signal)</subtitle>
   <p>Механизм, относящийся к ранним UNIX-системам, который применялся для посылки асинхронного уведомления о событиях от одного потока к другому. Сигналы не блокируют отправителя. Получатель сигнала может решить сам, обрабатывать ли сигнал синхронным способом, путем его явного ожидания. Сравните с импульсом.</p>
   <subtitle>синхронный (synchronous)</subtitle>
   <p>Это понятие применяется для указания на то, что данная операция синхронизирована по отношению к другой операции. Например, в процессе обмена сообщениями, когда сервер выполняет <emphasis>MsgReply()</emphasis> для ответа клиенту, говорят, что деблокирование клиента синхронно по отношению к операции ответа. Сравните с определением «асинхронный».</p>
   <subtitle>соединение (connection)</subtitle>
   <p>Понятие, говорящее о подключении клиента к каналу. Соединение устанавливается либо непосредственно клиентом (вызовом функции <emphasis>ConnectAttach()</emphasis>), либо «третьей» стороной от имени клиента (в случае вызова библиотечной функции <emphasis>open()</emphasis>). В любом случае, возвращаемый идентификатор соединения пригоден для использования в качестве дескриптора для всех операций обмена данными между клиентом и сервером.</p>
   <subtitle>условная переменная (condition variable)</subtitle>
   <p>Объект синхронизации, применяемый для множества потоков и характеризуемый как «точка встречи», в которой несколько потоков могут быть заблокированы в ожидании некоего сигнала (не путать с понятием сигнала в UNIX!). При поступлении сигнала один или более потоков разблокируются.</p>
   <subtitle>физический адрес (physical address)</subtitle>
   <p>Адрес, который выставляется ЦП на шину, связанную с подсистемой памяти. Поскольку QNX/Neutrino работает в режиме виртуальной адресации, это означает, что диспетчер памяти должен транслировать виртуальные адреса, которые используются потоками, в физические адреса, пригодные для использования подсистемой памяти. Сравните с виртуальной адресацией и виртуальной памятью.</p>
   <subtitle>фрагментация/дефрагментация сообщений (scatter/gather)</subtitle>
   <p>Применяется для определения операций обмена сообщениями, в которой множество различных фрагментов собираются ядром (на стороне либо клиента, либо сервера) (дефрагментация), после чего сообщение разбивается на фрагменты (их может быть и другое число, нежели было при дефрагментации) с другой стороны (фрагментация). Эта операция чрезвычайно полезна, когда, например, необходимо добавить к данным клиента заголовок прежде, чем их отправят серверу. Клиенту в таком случае следует определить вектор ввода/вывода (IOV), который должен содержать указатель на заголовок и его длину в виде первого элемента, а также указатель на данные и их длину в качестве второго элемента. Тогда ядро «дефрагментирует» эти данные и перешлет их серверу как один непрерывный объект. Действия на стороне сервера будут аналогичными.</p>
  </section>
 </body>
 <binary id="img_1.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAABcAAAAMAQMAAACz9bS7AAAACXBIWXMAAA7HAAAOyQGmEQL4
AAAABlBMVEUAAAD///+l2Z/dAAAAOElEQVR4nGP4//8fwz/+fwwfP7Ax9M/8xrCfgY9hX/E/
hn3M/xjmPf/H8AQo90H+H8Mf+38MQLUAV0EZfteq9wEAAAAASUVORK5CYII=</binary>
 <binary id="img_2.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAHUAAAB1AQMAAABKyjz/AAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAACc0lEQVR4nJ2VPW7jMBCFFWyR0kfQNVwtr7UdscgBfARd
I50XSOFSR1ACFSptQ4UkSCJ33pshqZS7BOLw48/w8ZEcVfFbmav/40dVHzlUVXXkz1P8Ux/4
RYa8FN5OGOMzs7rVmd846TXzKXby+5F4c/EODYmlMmmz8leMK6bVxrcY96j/ybJ6APfKQeZF
KHgqb6hfEVB5xlgIkA7wEwwB0ZF7VCEgNuQbqhTQki+5fyDXeX4PDj9TfIkM9s7Wjwt4843p
E0HCM8YGtd0LL4i1F35irUWPyQn30DIq18IDBg/KjfANwTrlVrjFYge+QEyrPCh3af4k3GDf
Fn8UdvBl+cYeVsKBZa7E7Q1K6RDYc7DjFnbySCuxxW2uxPSBVsICMOtoE4sCGf6M3EP4BXbq
/z2x1/6JvKh3gx4R2Wt8CDgLc37D41duqK/0t9TP+eRBx07kJ9da7IjPlcJoR/wO3tT/a2IJ
1toRk2WxdMTKTZJn3CX5uX+0G3au1mj+D9q/Mn6wK/AOver/EpN+3hRuQdn894f9QYBL3Kl/
Df3bdSOrNpOdhsz+WxmNg/F6PJ/IDQae91V543mHfBn3xPccb+Z9mpQH3idZfVVuE+vjgF7c
xzoJcMpNEiC/vd5/CsCoQbmz5clDkc/3kXgytqfA9obvsYyv+V7zdvg+k0HB3u9mduDVgIPZ
Kapm5o/EneYHnoP6PZZ8Q0F9yUcUcCv5igIuJZ9RQF3yHRtcyYcolv+yoIfl07TgV8m3LCnf
pgVPxnMOn/M5yuxLvrdwxh/kt8wPKAqnzPx0sNG+Nz/k73cs/FnHx+uB8b3yB5bvmWn41+/h
Xz5QXEhoYA5pAAAAAElFTkSuQmCC</binary>
 <binary id="img_3.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAANMAAABkAQMAAAAmBpdXAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAADw0lEQVR4nHWWvW7bMBDHaWTI6LGjs/UZOukR+ggN0KFj
hy4dCshAhqBAC48d3UfIGzhFBo9+BCswCg0dbFcoJNUU2bv/3dFUkGqwTP7I+xaPLv73qZz+
WY+mb85sO3fLM9gc3CSxcHButkjMO1fsE3Oz4F4mdn8R3Vtj/iLGw4vnbbkr6OdyNL8rlM2x
yqaP/HOwfRc86l4re4A9H4SFGY+CCb1kG+NEWFdg7qugMGtjT04Ku1PT1bviFH2MjwUYBaim
yUbYIQ4xlLGTfbcy30FtfORdRfTvwaaRFURfgq1514pnibGZnmffgU15F1myZMbrA++5BZvx
NLnxmVnHM+zGFi4UbH8b409mrIuW8ki11vHE9hJD/EhB/I3QwRvSfyiJ7XiKHfxbaqR71u95
HyoFDpb6h3aVpJgYioGVDiZgAHtDjM3APBxhKacY2FFiM2NwhI1qEYtF5UJh+sBYyh4qvhFD
GFv+WUhYKAi8dF05CfHeAsB7V7DoF7GYnrXaU0IrMYSMkwL7mQUZHo2VyljDIMM/lYOFAQuO
kkhxLx6UDcZ62IxhVzk2iXNyZrUMjcE9Bkcw2OCvhe2NNYjRoGxzZp2wXmwxJjK9pMiLD6ES
JraQc8zIf45e+C5syJnUZwxXwsT3oGwpr6v88DC2kXSOmQgz543tc6YOOmXy8SnrJY3G+pxp
kuauiGcnCvlenrBTzjSBxtqcaQKN7XMm9ZnYNmdSn4mtcrYSMcbKnJXyUiaxVqb1Sb7jpcUt
Ba25jXqI97ZvE1PIjDX5vkbFaB7qnGkNGhPXw7OsztkTmaIvyCKz5Sq3M6T6hNPXoxqMo/q8
zmtQmdWnMhFCDIeb1CfVvDQ+0wqjpD69fn9WX8Ikt4nVOZMza6gcNEi9NOrMCQ72ehbIXKtv
qcFT5XAki4NtSiR229ljSjPWjM6zvZ5n8rQjth2x2s5IYxIgkWJnK57ViG3ovD6P+N8SPZZ7
7sLOcn4ggZi/5G4bV9YDCmMUWP8QXtG/UnsHDnv4T2nxTfjEK7XnoI0jCjtm7Bf6EQfbs2BE
tuHrBdLArDFmeRLWW//rFlHz601mYz21m6rr6HywpUYvLvSigSCQVcQ+cgPSHt7UKcwLvupM
tIezg805dWu+kdywFGbkYNOm73OXAshMR8fRS+8anQnDo/lshOlI8yG3IF55vhP5QlM1tV/c
pXDJOJTKHmwl2COPfiiSyxhbBNaR0DA15qcqSu6Dk0wkrmtQLnfM++kXl1CsZnFeJtY5l10H
6Y4JBVW2Pj1zkV/9A1CKqaeXnBUwAAAAAElFTkSuQmCC</binary>
 <binary id="img_4.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAANkAAADRAQMAAAB7I4VYAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAAC/0lEQVR4nO3Yz2vTUBwA8Ki4bjCIeFLsluNOgrjLxmTR
oyD6D0wcqOtlanGHyS4tetDDYIIXD6PxtJ1EoQe9tWWHKHZ9/4BbqzsEoVtXA2tK2nzNj261
7ft+i5mzEfagJOFDvrz3fd/kvUYAvMWFI8Kac1DsX0EQxHr6QKwW3DJEVl9GUDPEUjURg7p9
XgUVYi1YVUtm4s77S9GRaPTE01AbaiX9RT65OroaeTgyFpFbscL0uJpcTa5EZuYnKx6Wt5qY
uGbjm+j0/GTIw0JmPyzTlSnnzuijysSIh8WNJiamkh9GV+ZmImfmPcxU94fCzHgm+THyLhLV
4hWvt+lyIwkSM7Vm8jzM1Zrpa1ojQw3c5WI26yJ/VhpD4WNZJ7CRvsCgUBCmJRTthmKsB709
BJKzQs5njqqETI3AdJ0KW6VQo1ClhpKmkhAP2KxQ2F8gqo8szbPC9Ol/3VsP05xedUXTtFEt
yB2x3fl06pYhmHXet0xLcZGtObjFvzNNhtXtg4hgzr1TU/4cyaGUTRdjXATfuYUevFKp0iSx
F709xm7IAOooygApCQ0rWzJDsOpsx/gtLuiga2AiyMBYQjEFVgwNi0AP8RvAGor2rvUKhlZM
sdOLoAGaDksIFqCswisEl8GSIYGg/xqyfN/pHw3fGLxH8HCvVBKPoLce+lquLMlZBY3O2A7W
RAoZgeDiT2RxZfD6qqgjKMFLQdzlo/2s4Au6/QcB35t4Q2H8fQLcclBFMGBbgf+xNP2hQqFE
oEUhHTZwGLhZOS7NLgVmiQS6b00USz1AqkPkUEAO2KxQ2LU0ZQTdQ4pCfvtb6HyT2AvtX5mt
6Kw8e4Agq0nKDjxjD7LD4fCFjTY0pHwOTvXdzw4OnjvfjjpTcqHxx7cXt9fZ56/tCMrm3NjC
0Nvt4nB/uA0rkN80x54MXfxR/HR9uQMVGxduzG6vf+kMW1Jyd8cvzxYHF/sHOpDld/omnt8r
2kMZ4Ayl5Jy431TrrbhmnJS0A+Skr4Lh95v53y/pKfsF/UgEuUoE5oAAAAAASUVORK5C
YII=</binary>
 <binary id="img_5.png" content-type="image/png">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
sMoAAAAASUVORK5CYII=</binary>
 <binary id="img_6.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAMEAAACoAQMAAABjdxhNAAAACXBIWXMAAA7FAAAOxAFed92+
AAAABlBMVEUAAAD///+l2Z/dAAAFA0lEQVR4nO3W708bZRwA8JaS3ghsTdwLNaDdCwLvpLYv
OhG57eUSU/8BNFUXQOMY2sXKIFybaoiJ0sW+2NDCaeJ7wb4ouFGuIbEsjN3bJSsUrFmzuNnW
vrhraO/xnl/X54CG6PbGzIa2d3zu+d73vvc836sFNHmFLP9SbE2FR5s5/e0A6gS4db4hEW66
y3nq5Payfb/kGnFZVgyZe25qI/fj1vbSzn6lZ6Q3sETFWu0f+lLObG3/sNOpDLznbcgb4bOt
bcVfT2637bQrZz5cDH5BZWCl/2JV/v2xLn8oPaNLga+pvPaqe3hD7L4jvbizqXSPJoKGWMOc
/YTz8w7LnH6e0qVkcLtRAx5U9c/aEdU5Tv5JRf+XZ1GOmjtPKiErKxJviMrtOhtSs1gNkciK
xiIJuzwVDoC9hnDoD4mmh1J5KjV9a52IKmDFourvEpEyjBGlskfGQSkA8oEkQ+JDkaFUqDhJ
hLr+JQISHoqGLjODxbhkKDUB7uWRaGgbfkJRUYHKSEhReSwoUei6kIUsYqkAEkGjR1EpMIKP
grlDkWnsGj0KfjEiQimaRGSEHFUxiQxzE03CG2c1pGqSIjMGCy4IPGvdOKp2nAB61oNSY0Vo
JjSyhqROZJ8VAUqVlXOYHpikigSnIGEJYdmDIiMR41gWsKT1XDTZNa6l17kallQ5D+QoH0Zj
PD1jb387ohKR0qtcqxNH8/aNWVLCX0TuZlc9U/g8bwV/mramhAdUFlZb6oCOmXb+ItwlsrOw
OkjFPfzRhRg324gWuCLjaK7x3Xcz0ZCRQThixxkICqoqrls1tZ5PyyK/f1jQ05dWFEuN1Loh
5vtD78Ix95Rn545J1lgxzR2R3sVmUjEJmqPmGU/OKjNSMEkOinllkdgiFPNqJMIzIh5Yp4dW
fQbt6Cke6hR5Wqo67TS0u1RoQeq0O9GOVGUEd7QqkRq9bCiwUaL4qL+h2KS/Zcj24Z5YJklj
QRGcWGCzQ2kYHRamWyf/hT0bC4wA+zWaEabuD49JU5HwkwCL5sB7SFSrxWkICIESTwVY8CMJ
Swnv0ZnHCAg5mwl+PTsC/rNSZCXHisyKzEoGyDlD8Jojq+16hnO0Zc+inghm1jlH+71xAUlC
cQ9fmPNgScbcw+/HvFhsyat29fQ3PBJvrH9YPe3iGbmGxRbTd56/hsdMJD/5tBq7jjOYTFyd
rsSSCLQJT6/9g+8SaIw2kfDo53GjaNqUTc/tHs5Nq1s5rv3GZziaNikw1ZkQGlfaXIzGjCtq
EsDKUa+nIWq5AAaFNSSl0fJv3Mudy+dn9FZS8vul1o7OZYuN08V/buOrjvZFq20EAGXE91L8
4f2kJxEIWZRhnxh/vJlUfg5AOcXPF1/wVlqgXH5HnP/zhFdpofIIyiSRR5texR0g0W7fTyZu
omhvzse3NhPe14O6+P3peFvnoifSq4t/Lx3val8ORvTWo1ws57NdncuBmah+PQ8rhXpKuIUn
V8FYX7oU2cXWXJ5uRdGvaQB/IOtCdxRWIlBksoMmvjwID4kKK0gGVJDPzzqxuMaz6YjdUUXi
8vvSUqsDRdN/uwzN37wiY/H0+O4sCDKR6FAoJUQrSLx9Piml4TFFb3QovDblKCDp7fPlUjU6
5hU92pQsYxnzZW98TMTVnb0da42KSM74JX+aZuAal/KZWUcYyffl3UBh4bJCrldrVEcxHvcH
pdBUnvQuMPI3/YFaRN9TDmkAAAAASUVORK5CYII=</binary>
 <binary id="img_7.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAYUAAABYAQMAAAAgOnlOAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAACR0lEQVR4nO3Yv4sTQRQH8AkXuYhC2hQnCXdqLYhoc+wf
4N8glhYWIhYxCO5118gJ2liI+w/YWQkHCUTQQu8sDw7clZzOCUIiK2xCdufr5s1Esptf+/Qi
FnmQbCbkk/fezO7kh9gSoiKEOBD5nPooVhCJVQBtUISP9bFvQRUsetgV3CgIByaUDRkf9DiK
b1fho5M/A28T8E7ZeI5e/OzxFDEIC0Fi7C5E+Cci3iVEe05VGBP8HHwR4G+rmjS786tyWSKY
II7n9jFbZMmhq1JFEp+kknUSxWEffS22SQSvsdvWOUJJ4n2gWiTULRJ7Vdx1SVwm4W9iV3ok
elp8k0aEPokj3wj1gES/hmdtI3QfLaluWyR0Hz98XHFI6D7CHg6SIn7n6/ZgHGnRC3ChPioi
HN5JiM8SN+zRHAFejopuDYc3E318kFjToj/sY0eL2rCPZlmL8K2ZK6zReqh7JL5WsWO5v8eD
uZJGmPV4UY33Cxqvkti3sWfT7K6QkA6kowXqMGuu9JrTneW+gqvFPgnq12Ofux7rTIz+MEc5
JeqpHE5KjF9Rs0REIp0j3cd4jlmixRbZcszqI9techKC38ciq3KM4H86bzUhRPw1QBTD+C6f
QYAZ3aVYiqWYFh2+uCbe5Ap4JHLZXv8TnZJomOt3sjkau85L8aawnmkPsU1V9xffuf9fruBS
jEVYZosiV0Tt0LZ44ntp45LNqupsc+MiT5xurPNE9KVR4VUVeU8rT3hVFUrnt3ni3MPh/wAZ
hflBsVgxNf6J+AVSDjK6waKS9wAAAABJRU5ErkJggg==</binary>
 <binary id="img_8.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAVkAAACJAQMAAAC8ddkDAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAACuUlEQVR4nO3Xz2vTYBgH8JQeGmFY2XWHDoYM8bqD6CQ7
+l/0vKMUvAxNbyKIIj24YbUHr3oqgkIhrZdNNpaDB52wJthDDoKrFkxD3uTxzZsf79ssafOO
DrrR99CENx/efpO8T95EgOytKow97JbAFaQ4tgGaK3hrxrAQtiKD9ajX6wtaARA5VsI/KoON
cCx8uCED9B+SThT9RxpWwn0D3Dm+0Ng8A/6TiEsUI5nG0OUItyPsFikeSnTkw1FsE4wMigcN
ilUao2sg6BHsMJlNJrPG4M/i5q/M+Kh50x6LVZq5+7r5NsAmgzWKfzP4uLli49TpJ6gzMTof
gxhplw4Y3Gtt2ny3u8eDBzzYZHBjEjaSMdcTaY6njNm50aCYawEirV0CeAkM9mYBquB5nMOL
D3bLgnCtXYDRNmHRnOM5nnF8VPa3yxKuATkBt3EFLZG9flgvQXWdwoh058i+/2DEtezKXsGi
05hUtxVg4MHe8/lkEraDMb0YalbsRljjGZkLc8XgOsFpx+DCEGE45xje25eVgq030Doh+JNm
P8P4APf2GexWFONOgJ0WbPu4plpPMe7+vCr7JxCMvKqYIUbr8F0leFe13nl4b2HNZUaG58q/
pQC7j+BHmWBDc957+HjnAxrFf2t05P1yMLJzneBXNSuGv4W4Bb0SwV9VdN/HLwYsXlWGu/Rq
GD7e0dyKhzvbazqLKxTbh2BIBK+rPu6hBzpzNdwtxTG47qAVYI75bPJN/pnAXJnPjLXZiAFJ
mHx/ZK1uh/NRIIVYD9/oxi9AebKPV6sN8S6EOJeEN0Icbxdj7Z7jS4UlHlzkwHt59utYSXXD
0UrpC+Oa/ymUP6/MfFdjRu7g5cIdHlxPmFfp+LF4IGXF1cV7X+Jjp+PbwpPMuH5LXMgco74o
itljFK7cyIwhnmF6OKFV/wMo7PYSa1w5+QAAAABJRU5ErkJggg==</binary>
 <binary id="img_9.png" content-type="image/png">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</binary>
 <binary id="img_10.png" content-type="image/png">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
35z/TvAAAAAASUVORK5CYII=</binary>
 <binary id="img_11.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAQcAAAChAQMAAADwV+ijAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAAEXElEQVR4nK3Yz2obRxgA8FEM2UAdu8cenO6hL1Dsg0lb
PJdcAoU8QaEGh1wMhbjg2i5atYa6UKgOOTiH4n0UO+ggQoj1BEE2gurgg+XqoFW9mu1882f3
2/mz61Yd8Oyu9NuZ2dmZ+UYmmZG6xjUjxgdnJKgRJCJRpUjCLFmuFBf877BSwLeXlQJqSKIK
kVKd+UQi8rBC3Ii8XSGGKHeLnsjHFSIW+bRCUJGnfsGUiPwiwtAldPF+oZsYzyF0R/S8YlIr
hsbxvwhd+qRWjL2iXytidZzOIU7rRe+2RtCFQzEXZl6xsReK+/3iq+n9pU9+aGyt+MVsdevt
2trXf1SIL3Z/O14PH1WI/e8/Ol5f8deyMV19/u54/ekHv/jsl8UHn+9u+2uhR+efNvcy9o1f
ZEwc/f2hxVxvrlbEdxa3tWKekXz3+TLPrNTf+Od+/fqhRdsrdF++qhWPveJCffijV6gwmTYj
j2D3qDgmeMktiyAUxxu8oJZrCeRjDnCHGEIG/Q7u9nIt3YE4tvHrL4mZioQhHoYlcSuDMV9j
Uo+YyNjDmQpFlriWUR3iPnWLvtwZwBYldotT/maiLIVyem7BK08CQHgcYiFmNSENOIydQqwJ
IxLCYeoUeC6mTlGaR5FLXCNRdAgWMRaxS1Aseg6B3gWuEonSrqPoECRKO5fi0ZEo7X6KEpEo
7aCKViHRK4n8yZCIyyK2BS2LviXK3VE0qxDl7igerRBTQ9xaYmyImSWGhmATJFJCre7Ishfy
eYQ4I8TqDpg6LRizIPhfi5rdwUVKiPw2S3h4Wza7I8saI5pdUCHELt7sDi7OMph/IDr8cmR2
R5a9CURjQMA8TQ8skYRwJwixgWeBLSKRcSF/BNhiBBmjXMi1y/zpA2smpJCLAbrGSd7T5kKe
3VhCrr5dLuRZYolQ5GMu5Flpjy7bKMvmguIPiqRuSRjRL8QS6sCILj42hHoNrEl0E9uGUKOS
bRD9yswxpsftBtEdYXaqHrcnZGx8olM3F7q7zW7XtZ4Tfa85X9q1IlbHP4kuzRyG9YL+CxHX
iL9yYU6YWpHPQRByJtjijTj5G8SCW7TEScKXDraztEIdAnbtfSkWFhedIt48XFbiyXuXaMXb
a99J8fLhFTUHOxe/djYPlNjfunKVcfLTt18qsfPkzNmO19urz1Q7lj52tuPnzVbARQzXk1OH
6PTH6bBS8DQbij6Vqeq93FVMvaLvFmh8/B/j1PWbFtJpLupmVDHnzJldCN/c17X/Tly7eHx9
4l2DdJnFKhV7hX4foSH0StfMV0tqCLW+pU294lqRUNWe5qu2ta6r2qdcyCZasUEVCrGhh6tF
SZY9yCOQHaNCkXfzKGbHOblcQhRL0DVO8h6IhOIpWGgJ0bIU4q340npY9XRJHtVHFpCx/FII
WK8ObSHuPBJixH8nLDtERzQPRBpkI+oQ/E64GfZBahdvNTVgDbVTGpn/71SpBZ/L/RhzgiyF
hZb9A2//pK8Ek4aCAAAAAElFTkSuQmCC</binary>
 <binary id="img_12.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAPsAAACcAQMAAACDTU8HAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAAFOklEQVR4nK3YTYgjRRQA4MpEJpdZswv+LMxA4lz04EFB
WESkDx4VBA9encO6c3NhvXnYjuPgOKsY8DqHXkU9LLhHrx0cIQ6z2BcPLsNs9zpgC3tIQmS6
s93psurV60pVpToRsSCh0/m6uqq66r1KCNULaemfI6J/HpLaYtCjkbMIFE1Km4tA6lJ6tgg8
5M1YBPrslbsLANy/tQA4/K1bDUTt/WqQwvtZNRjB+6QanCn1WEGstESCnPDyxqv8UwDnChe+
gFKPSCqOoCZPXOXMQC0i2KScv/ni2JN3oxMJivLSRaDA1gXLwKAK5EpvF4KkCkwRjKvAZBnI
EGTLwKQKjO1gaAWDWIJzBIUEUzZk8fEM1PbJ/tWbtHCSUQpTloFW/Mvq5sqLl+AWP3fa+y8x
0EyiLfYVgEb801c7O088xUGy+84Ld1YYqCfhdRzRojb49dJn33QuAuiQ7Vs11gaShM8d7m60
v9+gOdl+QMidzWdEN6/zRrLp87x39/7fRwdf3mPHDTr19XHgJ727p7uPTrprAMyB4ifDD05f
eXTyxVu8tvmRLMh2cO3+taODb++xY3Woiyu/P8tBLYkeHq5uvPbjGutRTNMgeg/Bzm9XOGjE
GQ2p6GYzpo+/XglLcHHEQYvNpBCH2mGgs9mnWUQyBp4esatyN5HPImcT9/HKu3sCUFGtmIvy
aVL6eRgYYKwDmFwSqPMhswG1hnEVmPxvINaAEh9yK1B6UYK+rYaK+KC3oQR7GlBjlCOOD9jr
D+yQHuUQvE7VMJjPQNoSzbmJzXJoGWlDcab3tvjCdU2AGaK3jc3xTbAnQCSC+QRrVAAugqFI
B2McJQXUBfhLJII+Tq0ZKBCcikTQxVikAMzXPpxiWW9qgByBC93JHRHZVSDSDjt9SEVGcnWQ
CjAVqZm/fBPAFaxpqSP6HOpgKEAMI8KNGIg5wM/2xGNNjIESzyKE9jbwbhbgU1mmBghwGGQp
bDXkVCmuBWg53beATAWhBcQqgC7XdDBQQWIBoQr4/Xp1HTgq4AMR6aBcd6LwgYgaGtCGAQZi
qIOJDnwGmhrIdMCanOog1sEAl+oMBDpgA5HrwNNBBtNbBb4OpnMLx9VBESigx/bKxjDwrWWH
ICjqbHFOTdBIG3wgOGA73dQ1hoElqMjlq54DFjYgHRngE9j/csD7sDcwAP20BR1lABp7Fppg
6EJLGYD2j3wTQDjqcwCPUd9M8wIBbcwBHInYohZ4NhMOxC65ZQIPKuZALMyuCaDKwmUAqL7b
pnKKOgw4szYpBZ+NFxGkYwPgFA1YOtBOyIIXxBKYj3sGJnaAc5ilpLHWallwDmfLANuilOvB
0YGHd/4XYKCfKYuPd45IYAdYYbFVBVwJQr3ZWGSntoi3GLT/MygPbhMf9rkmyMvfnbcJy3GQ
ImITiHTc4SApbOBDmOoAzi+/bAE3srXLrgCrJ99ZwGr2/gmCP9dt4Mb6hRKcH/9gAR+9Oavh
6IKthuOrnbING8TWhvUn2w4AVh5YAO86Bz6CgQWwadORz0IHyrOwg3weVDzN9rIZ1ZZzMlwG
PA3I2F45qx2saUuuLH8ZcHSAFeZydZsJZQ5UL//JMoD/FVSHoIooh+BMxsnqMIhxvjqQ4gAY
qbv8G6Mrg7mRupVYjV95BlCivajcMYA4kfN8MVIuUEpXtISBVGmTUiDDjCAturT8i0YtY6F4
5m1RsR3WSyruwwGvrDkH+P5DpGYa4W7EKE1IzxywzUxv/nt+7mPcgbCfMhYwlBsM9ma5A7/O
nf/jTqsCdmL/AELAzsNg+FlkAAAAAElFTkSuQmCC</binary>
 <binary id="img_13.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAREAAACNAQMAAACqidxBAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAAE10lEQVR4nNWYTYgcRRTHax1w9rAwgh4iHtpgznElB5GA
zZKDGxFyzEEXDewhBBJY/LgoVLFZspfAhixIiGArmJOEGDwoCdJD9rCKhzl4WXW1N4Qwuc0u
Q5iedHVV6uNVV093TXcfctB36emuX//fq/de1cwU4mWj/sQtQTxG1SaZ+/aNxxtl0dgXjOP5
hHmC8WuYHkEU1zB9guIahA8JOqhjDgh6UMeMCerXMZSgXgMmasAEdQxrxoSiZsr8whg89gTj
c/Kc+Nj+vshcFBIIzQSKAW9FBh7/F5nw2fladzPbeWbTzfyoLqFmvpg5177klZi30bnWCQ8Y
fH7x6vWgxODls3+++2HGvPzXHQ9Lb9lii3ngX5jdXfIgnmPnr+1cETodztodWLcxDu4uX9vd
MTrrZ+78c8nDrM3ZE5+L5hU9Hnvi8fUP7pl4+CAcBxxzxGk898KJtZdEW8TtgPdD2uMZ4yeh
iAbhlC5/efLYp4lgZkQOcRpljDLRaB0aP95ZOr6yRWUD8nx+LMOePLy3dHflqxITQr0Y8tnH
c6snNz+R8ZjFAAzDnI84Zojz9tz8wvpnW4JpBeytnC96+ObR18W8WpxHqclhJ2DzN26dNjr0
9uXnN4ROR4xljB+wn9dee8fopN9eFssecy9XLVEL+sPaq6cyncM3Z4WOHkyymtL5G99pnUDc
vPHLN2+a/mGWOfTT7fdFjyHZzzqGEiOSLC+a4XnG9g9cXEzYgGmiM2jAjBowyTNinL7Exof5
frYfmpi/XlAXzzD5PTOCa7ZnSkbYQTaicyjvZKPJe8tYw9bfeAqjajp0MHuWSbNBw5BJZgjp
SR06XawftoBheIIJcgxrGTd+mSH6mdg5OM0SUPBlEtg2bnplHbO4O8bNoMQwYJh8Xb00msqk
mZuk7OuMdqVG+jZBrlooD6oObCozsG6wZcgEE9lww2lMaKcdTfOFs1FNOhhmxbRHh698OZMp
Ovm2oFN0lD60F8vreJaxcUL86jOFZaWZIHOoQ2folmZyOmFeJ5JMRzNBxuhoKTDSsWZIjtGj
MC81AeJpxsYDXnyeJcIyFBhYnIG+yBx0fcMIdcXAL8WeTZBYUiH4AgYGzY9KEdceFtPTTKAZ
cDK0CRLMQDKeTMVBIVhI0J6cntYZKMZM2thATSUBZqQYOonwkQozBSZRzLjAiJ8MiVTXTKri
HBYY+Jfh61rolhkVGU9PT6/T1MQ4YbFmsGaGZq4OHQbrXWc2LDD6pRQYXQRcZJQlwARa1Gkj
YHwt6rSBZnQREjcTaYbKr49SesTE5LuhZlLedUrEHpcz0b74rpNJIqvD8e76nF9mxn8/WtmG
eHjYvfqeg4lf+e3IFsyLR93Vhy6dncULf5j8DP5dve9iHi0e3TR5Hv26uuBifp898qKpV9K9
WPonI+M5dPajlqm7KMMwLDPSjpv+odOZzyf7cIr5EE8FwsBXuZzWqMlPBTM2ea5ghqZeFUzf
1L2C6Zn+qbDI9GGl0VodH/bwqngCYPwKpgf7fFDB9CfWu9uGE/uG28bgq7jL5Y0CU9wt8yZ6
g9T2WNjkHOD/yIwJqjsikkztWcqQoO065kGDs53tBmdEGwTFVatUGPMIojPyT8Lp/fwuRU/Z
zwQTxK9UH7PNyHO2KttHYpw8BQ0RAw6B9P1cAAAAAElFTkSuQmCC</binary>
 <binary id="img_14.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAANIAAACfAQMAAAC2kRWzAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAAEMElEQVR4nM3XYWgbVRwA8Jem7gZmbSpjrKUjLSvpF5n0
bg5jR9KVzVmsne6bE2SyYuiX0jpIGWxcz8x2BWGuJ2hIaz4Nv4qCww1c0my7itEU/WJdQxIy
c4KhCYuYO3q957u7ZOZd7mKtncv7cHfcj/fu/d/7v3d3AJqWMICboTrGGNwvarYRMLANzYqL
ESZ1LOKfaWbd3hh3wd/fkou1ly2wSmWHA0HyLIWMdbHkeu7KYsUSt7Ijiyw5QbpPLj/sayN3
5eYq9nXCj8zfNDHhJsJ8mwXssu3uLJsrwSILuJH1houve0g5Nlyx9sUgel6AHO1z996ZPxEk
x3ND5TYfggP92WNR/8xTSj/fe9VPtuS+DWgmXETnOIQyrQTGqUcoGhk+LpKsGVZKmtWbB1QK
ACutdYyotiLW1mbc3OATNolXzklz40NQHk059CbcZQ5086EQHH/FU1PvLmkbyuwNsI636Jp6
J47YTv2+73Mu5K55ntjss5367avPuNajtdbj22PJfTR9L+5K4paBonNqz8Ff7cyH3P6afkK5
KnZOZ1KV8XXGTF9vJ2zLc1QAFskDaXQlTKHDVdzq5Kfd3KDaXnlsHo1QA5j0GKyR4ttho7dZ
T73coTErqHtfSs2wPKD1+akZodzBczC1X7MOA8uo65FXVxAH9et9C8bXWhFn3MS8uUlxxcS4
YoJDZ62qhYxMdvAV42QP3k/Zo9STVJNonan1JI9igi4+qcpEnQlqD2XVCnhfMlrvZZozMC1q
iTYfT6GebXuO/idDOWit5GCq9h1OmBm2xi7S0Mjg38d/b420Zz2OfbeR7Anv1wIAhwBktLvT
t/X5afqeTtWxDMSKfj2Ymvqhk6XN7c+EeZuFK+eeHvAYm+gaAq+FjNsUXW+AAYexbXQPDdpN
6hXZcx0Rk36axPdfv5Oj6N8g62rFraTGDIPI/mDjuC3ZL/98ZmX2G/v73W1cErPSyr7mTwb3
3nAvPT82yYfwMUPWPmifvbl0+O1JHn8/lH60X+84s/Lx/OkjY6PZ53CDfd+v/bJyg3tmJj2+
TuisZBpfPSvYLQ/AwHTLWavwJpzZ+jdmKQ8jEjgMdgNrGHQBD/48o/LP/1yNYkWr7lbVmub1
liqfN5BROkuWz2IY5Ck/kb5li0apKWIVCl3pyO3sd7O+rGMzDOLUF97l+ZcjAfIdb0D60jn8
QbAnevDdO+2XFHvRuxZevi9SL3mvbVLOkQX2+n2KjCSOKnaTWEuFV5mmk8Q14fyhkU9/YoLW
psudip2neteWjqN/e6KXUWyBY4IeZ0/nQhj8YCG9sfnjAZZ0eudEyjkc43r6Kd+zSps58AKf
Zm0WhnTycxLVlU7fG7XM+sZWUV9yohpNn7LfodDypdK6PHlVzidRDA80u1SOOI/STL5Qjl2X
D8hCj8bMdBpK256/vwDbaoG5YlQJNwAAAABJRU5ErkJggg==</binary>
 <binary id="img_15.png" content-type="image/png">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
AElFTkSuQmCC</binary>
 <binary id="img_16.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAg8AAACOAQMAAACMmHm1AAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAAFaklEQVR4nO3YbUwbZRwA8Ct0wBLGzWgMHwilsRNNfKlj
jJBt3DRzhpiYOPZh2Qdb4jY1ltGR+JZIrgKCS6Y4lBkT442EzS8uk2xjMRqvwGJBwToXiRmG
EjH2g1DaNa6tHPf4PHe99rlyV653EefW/wdyfe7P7/7Py12fKwGMBk9oz10C/iWDRK9ALIMZ
/UQFOefj99idI7oJ7k3y9yBvrvf6dRP8TjIU5EvrvRkDkktHTpALQVBS79FfBfCBKyZQUmEf
00+oxPoTvMk4UWiYUGRvXyJOJMPqA4B+pIowmS7nSMSkA+avhWjYSZgJ97N7k6qZyo1QCvaW
IJRi/Qk2T+SJO5jwwGeKSSC80oPLAps5IhUCEUZHlDLBQEWBiOdGgIRA+DOapFyBiKLDO5AI
KBAxIwSnSnC04Y4kvtJKbKVVqliOnDgGhjKIr5WIV9FfJSL6xzmrzZPK/xAR/MHpi/404auE
+2bUERnBsvADBb6BRGjKaiZS+R6B8B4exIjr1z+/IBHpsRg6c/HFfcPXnoNjcXbRajal8kdE
ovntXoyoeOCCMCMyon9gePGeUz89A2fkseNWWxfM56k00bS7n8SIeFVTmggmiSvW7kWzfbQS
rYsfrbaTMJ+rpGH+PIhBonz6UqtA8GJHup5OE52FiFgBUwM3F/dPf3cUETE4V2CeSUyKRMSf
7LZA1KPhvHfqT4mIgXoKERyYOnPzt4PbZypTq3OOWV6hIBEEYRInNgJxlaaroETC4k9NqhCR
BGGJrlDi84WU2OM7PAWmDCLI08UAJm0uZuQErMKyHKeEdRQuo0C6ivSWWeoIJFAV0oUQsUs4
gmPxPSMQkUkaIzamCGlptYsdodIEX9BdC9fAEhP/ICAQsXGqF/xAi+uCfwNl8xaMEIdzBWDE
Uy3zreiSXJ1IgHHbYKN5YNNgD+otbJqkQzRGdLLSPVLhgxcTibFegN9TE2e3NdYOdNkuJ5u6
vpg+sOfBVfcIAI1H+j8TiCdaOh/NJB5/5S1zlVTF0PSNGddLCsTLrqs+YTgPhGpJGTF1v+3J
Lee2nP4oRYzOuJrFGUnIiIeuVkuTel5GTB55d1OZdcOg1JFLp/pmXIdFIl6HE0d/rlZ4dgLx
qUVhTe/ZO8tdDwP+hTJIFGOEK7StRxsh3uCAv890Xk6IzWqErEn8theGk8M7kgMhhjgWGr5H
1InVk6qvCmPE+lThN0wk91og20YJZN8opUN9uyYR9FpEbnFbEYxRgqMNExEQYKXvq+wRBBSL
fixcRcyDWUv0+c5PUgirRkTViAj41R9u7i61SE0Mfh7/hzgoohL2kSISyAKNxexSuHm2iJaa
ZL+/BfBc+m42vt1XQGUSgC4iw42zRYoXlvWKAzVs3B4qoIEs0KT6LH97R9I/kETx83h6BNQw
cechJQJFbDZFJPDTeG4QbCUT7mNdFmUCixWVY7UQiLDM5bFCl7USGYERma+L8YwJVSPQ44vY
vOFjeNiqs4ocI0/8Hwi0qmm4rAK6iSAiagIgjG5vVhfh3dWzcNpaWe52DHu/pXQR4+3Vc7ZD
DfvaHH3v960tKBG/tNfNVTU3NLU5PN4OfQSsIuxsabjW5vB6PtVHjLX33HBad5a0OSY8OqsI
tgfAa1GuFDgmRvfqI+CDxQIXhx+EaQ2ACpF8ZzJC5BZ5Ik+sH8FmSae1EYEsF9RYRa7x7xBZ
djZRpUYFIqGQloygRoJTJ/xKjQoEvl3LCEYjAd4R3uVO7lh9htJKwL5U3fX6l2zyA83biVJn
aeaL4RpETpEn1peg//sqPB1mt3mti2QnRsdId/duY0Qh4+7eb4joIIrsHmMdQe9HmwsMEZri
FiH+AecCVP2Z3GPXAAAAAElFTkSuQmCC</binary>
 <binary id="img_17.png" content-type="image/png">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</binary>
 <binary id="img_18.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAALEAAABDAQMAAADpvXaUAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAACW0lEQVR4nIWUPW4TQRTHJ15kN4gUqZBQ9gYU0CSN9wJI
3ABRpSSAKIgEOyOlcElLgeQL0HCBOFEKiwYXHCCGRGxpWyvZs8p4Hu9jPPY6Nqzl+Y9/8z7m
jWefgo2PUTjYROY7LCpZcKV2cXRgM5SxUqlwm3oyuQbooFyAbwn/hUYIugAVCrpOhKOVz8Br
oK/DWE4T9xT1E8wpZwYWR58SdwT6cEsygBLYCjkFhcl8RFLghwU5W1h3RVKiKQvyPs3cB8qC
vqkIcrbwwp0XId7l6g9SWeYfXhslFvBZ5ECOLUOueXYu4JtIapST2YXIT5GuUZXMipoMIi9r
vG9UWeODsBx5VeOlUcEzpO/+m1dGBU8nZWTr3Nf4fAt3RnVXeVhd4WL5P+6N6m3kSmXwAO/X
LHCA084lDIn7ZuvsXiHcQYLP85c58SnsT48X/FC/zV8ds/0feFGmkbd1+0h4s/lsyZNG3jga
cpwf+f1ZL3K0f1dIHP91yZ/m7/PXI+Efb4rID/eePH7zW/azM92N/CTpNE44fq1eiOew7XwW
fMs5bzv/O/xqld/9fx2s8mo7H9V4yLa8b9U633hvZ+v3vC9SrL8XneBm1Fxm11JFEnnYyHce
7V5IH9/TL8IfhjLwfZdS9qWKRxJOL/qAz9ht3tZSJHLestM9ktsz9raxz1hZnkmfKWNfmsgb
PZBl7lfU6eBSlntSB/c36ow4c7wP6p3YTrh/Ykhcoz6EZ2L1on9S4rFmwRx+2W/HcjpD9oRT
SsbctSz1c0gMjePMpqHPn8tlHPLe8SrrwOuPI2fzF505L/tbSa2qAAAAAElFTkSuQmCC
</binary>
 <binary id="img_19.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAALgAAACTAQMAAAAHlkFxAAAACXBIWXMAAA7FAAAOxQFHbOz/
AAAABlBMVEUAAAD///+l2Z/dAAADpklEQVR4nKXXv2rbQBgA8Gud4sXBa5fUlNB3aCDJDX2W
kgfI0EIKF+jgpeCha0FLoY/QoYNTMngxqC9QbJOkWgKyq8EyPul6f77v050sEUI1SMpPd9+d
znffKUzRMR9U94rRnWSs0ec8HzT5R6U6DV7qwosGz4Vugu/6CurU/dqcRrtu6WbXBxgs9NI2
me+4FNXZ99yr5fvGa8X3zF2iuifuEtd9EjyuPG7xKGjmYedBdx/0Urir/E/Hv8s2F49zVfMC
nYeu36fsN7rs9faT8a5v7uRdxht8Ku/+iuY4eUP8zRd5IJv8d1E0eTEtpPbX9fctbvdkr/qd
3FU+t5dyx2m6M+57Sf5yEvgecPleBnF66AKmuvPshUCHGeY8Pa089Tzy5kPmOY2vxCnHoDbH
8YCfmkGpiNyFtL4N5jknz2i9GI/Ik2AdzchjWljmkpLPaAHF8Mz5OFjXa9+9PLAlF/g2djAD
d29j80yBXqogLwXu8pvNYyW6LTChU+g3VEkFvlQ4ZHYtGbd9NzkY0p7wy6s+1Kncvoi6sqke
Xt/zJWP9JldXOBlrTsdDfiVavKX8vMUXLb56pF/iYnPHJXdePvnV9XjOOs6XPOgpsxW0/9S/
dxVIDpTsWx8qO/hwmN/g2rjdFK/Jb9wz5n68qqumtibmqNroTG0dgrkJQhudm3cj7RMqZgvY
80S7m0sR+AoaZ1ASN7QMnjLIH7hxuetGu/DKYT2pmFSBu3ZKwSDD4wYF/eJs4/eP8mrEIAA0
g3mozWOGHeRBuDZPWAp346BbCcMBiALPap6Rz7ClmkeNvq35Gv7a1DwhH7d6tzMboaAX2g8P
484JeTko9FQ0fsFHRz9UYjqcpkpOe7cjveGM1Tn/dvJZ/RloXySquJe3ifVu5/vxxH1Nzs+U
/CDfrq2fie5xouZP9/rFZVeVvf1Xzt+JNycplNfz/f7gq/Xy/PTiKHXx56kq7opPLv4F23RS
lZgXT0x/iulEO46D63+q4z+773ue1sahbdwe63HgGXna6FuaV1HgWc2r+YMlxoGvybk943dQ
gusCJjjO27TFY1x3MnisRkwGBfHDN8L1ix0B5wzuautXu7vDL/MRNMNgYOCzBK6S8g/uXJQ3
IMIAfAWtQX6jD/McokE+zOF9INGZ/GaLUp50FU0+tKGrvGoyr94TmbMh+dBF1T4XJlnjYTab
hc3bed88wsOk9iHuF94/RmXX1ja+ZFUYs00y4f8/RYFwH2k8/gG2AUlkq9LqNwAAAABJRU5E
rkJggg==</binary>
 <binary id="img_20.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAPAAAAChAQMAAADDSaCDAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAAFJElEQVR4nIWYP4/cRBTAJ0pxTdAVKaCIMEgoZRpaWAtR
oEjhKyAQSDSREJ/ABymuXOWo4MTNh7gy0e5GFFtF5k+FCNpDVyxU3pWLtTmvh3n/xmN7ZrFO
ts+/9cx7b957856VMZW6ZWKHMmZhqvQAPsa/GG7sqz/FcWXP2zi+tucqjpf23GZRPIVLGsVI
dAy3iPMoxmmXMdzg5TqGa7yUh3Edw/Te/jBuYniNl77ZNkodwkqpI74rxnhzZFpFDxTbK/Xw
iUUVva5WI9wgIf9TeoQ3OO5VFsEvaIzEx4mjLQuNF3WBmhW6m5oH+gvx7CO45B3e8LUi/ElS
/pscd1icGkdRs3t3Pz1PihM158encJqzOBb/+vg8LXTFdiIhNf9OzV79/MGTFERTtzvJwJbX
iB/98tU5YnOlEokIUGeLin3x2zuM7X8px1PJoiu9++PrZ3PRe3GLnBKcC1x8ZNQTkCBtWMgR
nv79NsQ7qDENLGhik0lyGxVfjjHG1EKZFWnG3tIKbtA4ChXfdphNRmrbNV3R/dBTtzzWnEZi
7MJAYjGlXw2jZM0S8m8YuxBkv6aY06MInRrv/6mEv4tvTZcdnpeC14JZ/4Kfcm4RLObJeUxO
XJKZRP+5wzhbwlgUnLOAnDTTAV7hueKU2/CYTkGxpcKokMhweMeS2Hyeeele9L9xGOZ1m4Uo
uEdlAJvbpkqGuKXb1OKr9EQk6zJzhvMArih6+niOUmrIb95Go+VmhSbQyvQOD4N98xjWuHhD
nHo3Oo5xZKtcBINc6xF27g5a7eK4JFxGMFi0iGOw3mqEXbBogw4VwxZBgtiOccoYwqweYxgX
5t2HMa5pQf40wDXrBHrvwrjkmyKMa54F8H6MUTlrn10YS94A0Zoxxnxp1dtH8JwUbw9hWp7o
4Dh9CLfyj4YIGWFXRuTBt2/kn3UQuxy2CxrVReE6iLU3dwCnnuRj3NUg6UEMNwO8N/0wi7ki
yxFzRUNOFcBiFtyjx1gSI/haIIjqOLZZMpM424WxrPdNCJ84bwEb7Ib4m87XTCAzfdiZLRvj
99ad0bWVHvAic/hu0WWInLJiI2UlveG20zVhm+6daIm3nVrFV7hZUOFnj1p7rmgwn8MbpzJe
3q/kNW1UUhLovN8H2L0EakLeLloYOe1j2ENZmS3gTkvcx6Ac4C12CWJ7uMl6G3QCeO7hXl9i
9+Fa9mbBZGLUpsQ6qOhw3SstcpRx18Ne46ExNG46XErVU7J2FntNxtovikrSsO3htcM56ea1
CblXkLWiYeqwlmqvkfK213klrhiUdfZXG8q5Fd1lvJBYxfDw9qGUsRlLBEtb87PGq3LZA1HD
V58t76V47zBPCuVRWd8/e6TREA5rJ47FD87urKjKndPjiRgjsXjz4NvXcqqRZ7iAx9+LNtZ5
bn6//8PjFVXYE1C8/VJcGV6p3njryZ2E6vPJhSnbN3da8DXoWyyfk/HV5Pm7Dy+TtcPo0wUs
Kyywmr1/9vFl2jU9EBWEsbOY/Xhm+xLtsCsbuS/58+nxZZo63LVeEB/q4uXTh5eTrMNL/3fq
4p/vPn82OXJDUh9mpKPi1zos3wGw+hzjlifH6jPQaB57F2lTsw6/6KYeNR7G9aF4H2iRqdM8
6mHjHZAGNyRgqD23G2vDn2CGjQceV0oljIOfBhavmx6OflgYNB5D/D8fNWj5pzFMoaej2MuK
IQxSucZjjMEhNxHKH5Jin1sMNR6ncWzdQvwniBe89BHsNx4B7O+Cw+M/LsSfrwAgIK8AAAAA
SUVORK5CYII=</binary>
 <binary id="img_21.png" content-type="image/png">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
rkJggg==</binary>
 <binary id="img_22.png" content-type="image/png">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
DhgAAAAASUVORK5CYII=</binary>
 <binary id="img_23.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAaEAAACXAQMAAACGMnaFAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAAE1klEQVR4nM3Z32/bRBwAcG+tuqFNOG9EWsFFmvZc8VRW
2A2pz/AfoAqq9YkJxEMfmHrJihpNQsoYT5ROfuEPQOKBoaIlU6SViiA/8IQqYVeW6ieUmFRb
rLk+fOdfd3Uc3zciwEm1nXM+vcv9PlshE4SaMm3lxWdcrgKMdUKOUHg57GNKyKfl6un7mybB
v7aI+fjO8Uc9oqLgs3J1d+4tzcIPt+Yuz8/3bm5duPFe4JWr5fU3wxzqe8drP8/3nLpzR5VR
eGPZtJC+N0dV794rcuqNjaUwre43H6wd7N5ab8xsazK/6+nCyyoh9o/n17rVjzv2V4ZKJNSI
IFHyI8P/SQ0VRXkdrKxQKRiq9sO/AEGVQQ+jVTuXhTSYhcpXZiZQFi5OjOWwNUo1SKCBFS2i
fbAahicXrKgYgpUTnnwEVaMrskzp9KxDFUrvAlSA6dmRUm2UKJ+dBzJKUc6Jyhshzqr+DBmq
sfKk1Vb49yRWUd78cuVr4WGo8Soqk7Gqz46NSDljVTNTUQs/AiqWt6jF1pS4fhEhp5RGH5tm
i9jk1IwUYl+P/i8begRFp082GDgNHYVV6Bmc6sepq0yZ0Qc9uFzt4xpZvlhZWr3ZOT/3WgW/
MLimbceqw5SeqJUvwgEk2Fz4ffHaekfXul+igcOVYTNWrqD8P7sObpNN9/C+6tTNxy/9Jiot
VvT3pcr0V7vHqE2uf/LL/WvrX5s/HQyQx6m0kGlx1JSoWIkRzFYWw4gtpV5ZXK0+qc3OIM/I
VDZGaLwi2tLb9Poh2SXerm3cRoLKxqOmoPLh1Oylyk5jHarQs86t5VpBZ7ZTlY2YLlMnd9eu
FClLTVQzjfOYcndWDvZIW0nDOWVBeTW+ZkonWcGzThWqwc7KxWWWA5P7SXxaDWHmC3CU1tXD
vaIcsuhHYQI4i0S0DE92rl76fnxpIGFI16l6dn3jSlFacX0Zz/npozm+vlLlOrYQl7Xe0Srq
X/6H/CjrSCpS57Niy6p2k4tzub48RrVIW+PiPGHcGKNq/O2hrBIWRX46HhapuM0Lw2WAy0bR
WAnTQIAkx3lx8kCSc4o4UWnJ/AVSek2JWnPJrCdOwKGKek7JvCzeNpI1gJ0HY1VUU0WLNqb0
MwsLJ1nbNPOATyuv2A1tvOoJkYNQ0Z9UVMmJEnv6oGx9GPevvKLiqETpOUV7QQOkvLI1drGy
sIXhyk8WcMUK5RWxUP7r5WpMGKl8yZ0UUIUVWVkCqz6dMM82uFIVhX9fBelHBaD6SYog9bw1
iXIeHW1XEVj1bz840KGqZ737+SWmIGXoWH98e9gCqx+638HVX4vvbFcxVPlhD3jBLmAtajJl
TqTiMF2FJlFnRoDhRKp0ZPsPlP4PKHciNZiqys/LcOVMVYkLJkNSiSui6arcqnKKSlzgoqkq
cReAp6qEQcqXfY6d2xNJKWEIkHiDEAedux5IK37lLq/4ruJIK775GtKK38fo0op/DqBJK26b
ECBpxTUpX+L9V5IA5pOVfjOC0isXoLJqtgFqn7+SVlmFNQEqqzANoNIKo4/ypVX63J9y+XdS
Wnx2QSrZDnZAKilEFaTi4mCbSXkVFwfbCgDe0F1gR/aAHqCiB/oqUA1ReOgjoAroTrdOgIpu
da3kbYW8IkqyPwGpk1kzVn8DMfNvkgp5pgsAAAAASUVORK5CYII=</binary>
 <binary id="img_24.png" content-type="image/png">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</binary>
 <binary id="img_25.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAaMAAABlAQMAAADanR6qAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAAEyUlEQVR4nMWYf2gbVRzAb+26toq7bJWtavGSuiGVTZo2
VcaYV1sqGx1k+J/gtKljP8AKIzAL01zGstI5dQ1O3daxAymC/9oirP3jMrO1KIWAFMEfNAmp
i4HWpARySe9yz3uXS/KSu9yPiOz7x733fXmfe+993/d9vy+HgTokgj12KmmW4gE4BO4DgQU5
qOYBKTWTSYFCKI6qpNauU4fuHA+NP3Jv8oE4tUwdpiMffAjw5TRKZaPvf0QglA/Hd7Wc8VnX
ba7sVD/h227DE02u7H7veyRKJY6NodQNHOxqO9/k21hvzX4xRH/2pZVMzG1wfS+9UEkdtZEI
9XACH7r8dtPnIsW3ueiHftv1xLdjOcfZ8wxCbZ2zuVBqphkMHT2+37d+pXWrrZ+ese/DE/Yu
bse5LpTinjrQhVKLOBhrd12ZemR3jYff2Ptprx/fmHFd3HmqYl1ARbKiuVgAKMDvBTHAAXCf
4qFakxJgJ76kkSAOy5i4b4IGBagUoTZ8hSgp5u96qMDzeB0UANVjsYaocJWuPABGzlf1W0yc
ypApqrhLl8xQ+S25EjRDcUUDxsxQJftVmP//imyMXMZNUa/JJWeGEs+KUozP0ATFIBTiHSbG
StdF5Y1S8QqtxOtQYbFrDtUMUbBreZ/KI+uvq3ySywvTpLhKtbzhmlR1mAkbosJVemlhuuua
RuqlGetSWaQuGKYqhDZAKaLndNGBtSi6Shc8RQfWoihFC8xrrA5V+0VmKE7aweSSJqVMJbmk
OL3k15oUXd3AbiahA7PmZshGk3BhcZPUX9IEOC1KmVfZgFTMalEqF8tCMNC0IaNsyhcduDZF
abzMjDUMxo0qEaTxeQ2KV2+GQtVFaYwFb4jmqVRdVIRUaSxtvKlTWcp7qlS4BlVsF1QppgZV
bFenqBqULOxoBNuUR+fjoBlI7gkhGJxvzafhnMQfGqU0RhcidmjtiRFsDdZmBYI/A9xiLZ/z
UIAlOSEuUu4CtdkLwmIopKdJINj5/pXOESy5aFnyYG92dJzu7m2fcHzsbfFgo87A2QHi6rwb
Wo1/YN3d/msGWyEwYnG7Lb/s6BnBQrcDU3edJ2i+4dSOwYbvFu5d8xwbCP02+Qt5aT4Nd4gf
Wm8YPJFZ9dO3yZW5g/zJvs4IFvJGvKtOy52O3S+fbtwzeXjhmmd4JPTHN0HCL1NPH3il0ZJZ
tdNB4sEPB0FrjyeC/fPswM3V17uJ/A3fhYE9VxdmP/EM9ztvdf5O+793S1SftSf4TObnPrrh
3dScjR+kxF1O/WgJ3I3NE/mvui+2LP155PLkO8OjT968MPrqvQnMCe3jsC5Y3soEHS/+JI7l
YgRIASG3RkomjcJHHrqA8JwY9diSN0RBOpeGgVB8RBlB8iiheLTlnYNdCTkHF6gYYNPF7Col
I8k3kBuSMXks/+gZnY4V95wSRelQFWsvUim9SQmoIlOpbXoUwBALmLAGVU2xjD6EXuwLVFLn
K4g6pZIYFcITCiqt3lOborgaXbUoIXtEn1LaMFcPJcjfgcxRPPIRwjgFeMq7E2yLAh7DQbZR
POtiFBZEz5P9rJDMFDPMMrQXg9IYkYqWrFRgHYWiWfpRQY0bWRci8n6Zg/5bBAjXQ8XIusYy
KXVS/wLlb3UoFf+DSAAAAABJRU5ErkJggg==</binary>
 <binary id="img_26.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAMcAAACpAQMAAAClNbuvAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAACo0lEQVR4nO3WsWtaQRgA8FcjWqjkbW0KRR2KWU0TjNQW
3bpI6B/QIS1BMwSRV0lTSvsCQjLa4pCkBty6tlvGZ6EdHi2mY4ZWXkG0QkATBzWed32+e4nv
zrtCHaqF3KLez/u+7757JwqIM6AwikAnV5o8Ad3taXcwbJ2shrEgn8O167FKBUsPSeuqmicE
oaYhNunqNULKjUTku03Pc1dSu4RorbXoj5IAmndiNZWS1cjHkgCnHA7XDFFbuboa+VwSkLMo
e0LEVirxJ0t6NL0+GXUJqc7GIhxxxh8bwh6jncL/KZuCMWTLlDny+ONAAH7bZgi6EDjtsQpE
qJHEAqKHlJz6ZSzHtLQWsaBjkRIkmVKkBSZN2aejwaop9+uUdPRn3qj6JS1NU4BLpHaqmXkA
udO+BC3deSVbBHdHwZKk+3Z2LhGuCA/Pj0UYSMsQLWh71DzsJ0BI6eI8TqTXtmyJBsJt2ZAw
UhDcsNTWQT0jGhTFXx6i6kbwmWyIz17wEDttzAW8sh4NSuUtmZBmLhDq54FP7bQcLWCRbvso
+TYXMvqWLMdI6ewYoiCHPUFK2z+P12Su18lTABsHLwa9tgrZ0SHhr/mT6M9Ogbg/QI7gcxq6
WYB7s2DrwHgZlvMvXMql/GOBJ12OAO2EJ0WetIs/OXKqqRypaHsc0Zau8MQt8Pbz6TlPtM7E
noJ4xhHQ/71ny731qQxb9gIPZpnS+7IQj7Plqz++zRbVH2NHA7n5GDsanFmMOye110jk3gXO
mtdp161gosCQd9kV9W3iiCHvs6labmWfIZl0qpZdZsnWzVRtZ22XIR+yjqiXWcGbdMbtTVSY
+0GoLk/sKfy1KGOvYJJF4YpnBOHnGSXa+CU/9gpGqY0vythrG/rfeyECV9ANes6U34ohEEyH
ojXhAAAAAElFTkSuQmCC</binary>
 <binary id="img_27.png" content-type="image/png">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
VXkAAAAASUVORK5CYII=</binary>
 <binary id="img_28.png" content-type="image/png">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
Dav8b0lz3s6JQX5lAAAAAElFTkSuQmCC</binary>
 <binary id="img_29.png" content-type="image/png">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</binary>
 <binary id="img_30.png" content-type="image/png">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
ypKu3v8BDlPhhx0knAcAAAAASUVORK5CYII=</binary>
 <binary id="img_31.png" content-type="image/png">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</binary>
 <binary id="img_32.png" content-type="image/png">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
NOIAAAAASUVORK5CYII=</binary>
 <binary id="img_33.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAL0AAACdAQMAAADbtetFAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAADiUlEQVR4nJ2XT2sUMRTApxT0ou7Bi2JlvkahaL6SR3tK
pQc9CD14EarO5/DitvQwiqV7Flq2ZcVBWLq7jtgMm07My0umk+TFFsPOTshvkpc/7728lym6
iEz/bWU0EKvPBiQ4VWqVBNuGxaDNlZKMAILrd06ABbx3CDCB94QAJbwXBDCjCAIYuTIGrZlp
y2OATSwCdpA8Ag1WwoV0YBSBGitlClQRqHAR9c3B6BrQ3ByMEYTbK7ICQbi914BBArQpoFJD
/SfgBPiTa5AT4McjDUoCTPVGyYYAh080CIQYcLwGsypicHIIoIrB2IA6AGPsIYIlmhM8vmtO
MI/A0WMDupVscQtmjdESZ1XzbBWA/q6qDXBCVtQeR4W7UEZFWxSid03/RGbtFVaBGj/Xz0tf
27E6MY/IrEj4GoXsmF6d4ZidYvAHkiT3bRC6GStumQZcdR/Xyp1x7tv5lXaPtGdgrgn74dRK
DYxY0c0N1zUJvE/trHQR+Cvplt9oMHcThfKUm5GVCH3iEUMgwYuynhf9lWOtfa797gB3FL/s
g33jky24x02DWtdgzmTn28UtDbYsaLOMdwCq73TlLdwQp7dVV2BvCgf6BcCBfl6FADYN5vg5
BLDrn1LgQwq8ToH9FFD/Ans3BosUMD04DX6v3afB9y+7NJh9/UZPNw12TxJggwL6oM433xMH
pcF07SF9tNM7GzEA9anUUqkHBLgAa1gPgBwwrESgr7veUCynQdWzD6+MONqWEIHuMuvnwAb7
2t46M60BzHPh7KNxDqASaFEvLLjou4y+DY46p6UBWKyz2qF1S+B90I3YZXFbBX+F0konG12f
gIDFeJWJk43VBQCz1IWTjRM5UL6/Gppt4jBRz8PZoEa3si7EMf/WBPb4Si/EgX7WaYts0AOF
8qMZz1MXHqi7XfJirP5tcKk8YK+/KohZDFjOzEaUPtCNzcx8Xvhg1AEeA3iim1MPALrQRHdt
4XoQtzPM6pK6z0G49GX3QBCYiCzHoWQdAoYVWaVAkQIsBTgBlgeciJY02HzDSNCefRyS8VX7
s6GBbOqCBJdNNU6AckyGnLIpxmT0KpvtggTtcntIA9B1KqbWOsvpYBtKHJ4n4/YrMwhAMjew
Sy4ikMw/khkLNhE5TjIrSudRyczrDN7hwpVzfVR2BzpoY0cfwLTIDFKHkXTOmc5SRZbRea3q
xT4+kLOoXYm/u/7n9L8nDakAAAAASUVORK5CYII=</binary>
 <binary id="img_34.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAMAAAAC1AQMAAAAz/WI2AAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAAExUlEQVR4nLWXT4gbVRjApwY3Lk2bggcpTZ0FYXvxsAZx
lS2dc0Hco5eFFkFXQdjqHrRtdFqRxF4M0kMJ7DI3vRb2YGHb3ZQIcaHunL3sZE0kl2KyHdjM
mNl5vj/fm8xMvhe3oG83yWR+ee/7M9/3ve9pRDG05weBCvRn5u8bDQQMfm5vWBYC3MHBpl35
6fx2o/yqkViq/9lm787Uh6/feSt3L0a0YO7Nzd568c9P1z+378bB4N7hwq+Pip2P12/kziTB
YMGmMz6oFXcSM/pn5t/Wa8VTs7enpl+JA+/a/APjjy/O/1j5vvC+GQPRVcqWY4C05RNBSzNQ
EGpzWRR4BtlHAb3rmRio0NV0DFj0VUVAyFTqIoCHwwECfPbpIcBln0FaLQ2WDxFg8wtjHAhN
rXEgbtnjQCzSVIG0hZrUxz0+gATxUWBgwP8XkE65CcCdBMwJM9J+FyCDgYG4+C+BgYCjbylY
TAP6hPwyBW1kxvCSScIvETvcrEmClBAOBq9RkAp47l3XNShoI+AmAwECMgwkhXAwrDAZkZBg
m4Mjagf1B+VSyC2W9jxK/JAHA8RDkOlnecCJr37krr5JHqciUUR8g6d9BFhQCyF5PjcR7TxD
+YLVZOLwe9zO5ghU5Ruf1h4lp0VACM+HgyidhSQfhNFpGugidGPvfIHA1KCMgGstWDM0NJAG
7qDr6GJlDcoIONA/ArMsDapbBRxYAkdYUPpgAUK+gzJkQ7EMDACtEhkB6ssW3Cf9d4RNXV6Q
syQqyK2zdHoeAH2WegTO0VCmSrscBLHS7wpj3bHdwBX6+xhg+o+DrpCBgoDlEgqySX0iwOIE
B+R/Bj1C/j6pmPFsWgEOv1KBggJ0cgqX9P5SOLGLAvo8OrsKcEhlDNMgfJnWnEfIM2fh4x4h
gAXccCjjKja8i6Dc2IySiYMnkAZ2GqxBQaly4I16ODOWUSyo83A/kPWA5SAJTpQyAFwoESFL
Z1agZOI0YdMNeJfRGKWaBfnrc8AmCyFMpQOxJAMGkbs2SxlPLKnBMm0pW3zlhYz/5EDKFqVP
J7LIeFI2p7xYxvoS4Y59/h/1JQaRtcnTSTbWl+gk6gI0UcLBOewdWpA+s0GThceWsmEk+hIz
AUYlPLHlRKCdar74/hEOuUaQZQ/zTKwAq/znQrZXyF/MuQBu8voqtPB/I4Vnq2LzYjP8qKV5
QpbcCwAyTCWw28udXhpWAZxjAGR7hdPvDi2x1zIZgbTbXw9yrhMHhpQRdBJA2u0/fbGTdUad
DNOMjyFZPNQloCNcJokx6jL0NAAtx/uSSWDvmoN3S2sfreDgwcqy3L2TYOvrZbwj2yp1cXDp
Ogao27dWVeBqTwVW8JbTogGKdq8O/VP2u8/RIUO5GW+2lV24uqEXwBoHtgrE+pIk4Ipi5w/l
UUZ5+In3JQnAKwB2wFIfyZSHOOWxj5wI8jioK46WsuoiABnamGUS3KrX9663ELDm7F5ZSD8+
Bh46e1c2HjeaTqtRPbmdWGp3sfnL71N3Zy6Xp60YuF2v283dWrE4W9jYiYOa07Cfvle7MXeh
8M1LekK4Y/fojDdml+Y+ic9Yqzv1Xqf2wg8zO/cvx8EWA/tny42r7fKpOIiuUjF6DJC2XAX+
ASvzvj3EGdgxAAAAAElFTkSuQmCC</binary>
 <binary id="img_35.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAMAAAACyAQMAAAAu+FKOAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAAEpElEQVR4nK3XzWsbRxQA8DUOUUuDdPChlBo2J+XmOLiQ
uJG9hx56yKH/QCFpDymFlpRgVAKut4kPulkHH0ocp3vMrQQMdUvSSsJQNbRhC726WglRb0iD
JbMQadnVTudT2pl9Y5PSwbaMftLMvN2ZN28NpGmGFmId9K+de2jtAeCf6+46DgR+d9erFMy1
Bxuli1JX7U933Znilc82i3d37fTgZz/fcRdXnj17VPZ20oAqhzt/YHi+dHN2Pg1+5XDBe7Ty
/OMfy79JXQXF8wvmnfKV65vFJxfTMPzi/EPrl8Hb31U2Zt+UxhCtpsRxMqiRHwttw4ZhKi6A
MLRQ9roTaGMDoYlQYkFg4t8qAPTTPgB0OQQAhOQ1BIB+OLOKDN59YmfBpf+o88Xg0H/MLLC3
nCywTtwM8GHVCI8BHoEaOgMbgpCBek1OAAuCgMFIB+rl/S8wwH+n8Q3JALkWgQ7QcWC/CoSn
MJgARHkMPgTLGEIAgryNkthWAMcwyNk4cjMLfkDABb5B4SgLL7+08f2IFcCzCU7b5MUSb1pj
SOitbYoPGxaHMKbAB4lzwwJfV6MRvfI8kj5CDWUlskEqdNtLa5dFggONrQk0+bfY7jbl/UEH
odmjOtlRVRHAEet4vAcd0V8XUZV3bcAR9yfvczII7TNWM4PF+0xsJZe4YpKWyD5dET8PyxH5
il/ZOEyBlOEu8cFcg121cU6sc2iKLGpz6N9iXfg075roNhKwiAcqcEimLo0zdXsG3zeyBtXc
3p9mG5hB+uwI6G0SkGrkyuGOQxB8DYQ6iN+HoIenD47RYy+j/wHcY77xs2/D8LhhwTB/vwaP
sTLy4MhXQx0EOvB14EKA7+DqbQ+4g38itBbVstCfIcu6xpZPurUXydKws0AWHJ1DBlbZq6vC
i1gD2+n9kW5f8Q1hUaiP127M80FCITGuidUe8F0b27wuaXDw+U2n+xx1JjvK4fkgRMoetHmS
6Sp1CQmCpqUmLT8sJPZ5hHjSqo7rkq4Ymx4kianUJXSqFToXOfvQuXXoj5SvmOKwcpO6hM6X
HfaJYZhKXcKPqfoUUuoSB02alHctCSaZWjpZDHEG+Eg+i8anQVepWSgkAZ2qx8KYzZXOBOzE
IXAkxo7+QqWDTyYQij6jp8nsSwL4o8k0AZ6Tw+18KyiwczB5jwAfe1g6823kcCBpjY+Nop/i
fCABjzu6PzocOKz8oCCqiWg/PGg67DwnMD7ooxfTBwUvVUwMkNQm4OrA0oENQOtofALLsN32
gDIKww+HH8DwbnIDhuW4B1dkazibgzXcWqCBZb8HF5DL9RtwyVnD6xAEB989sN71kK4Qxm2Q
gZOK7V4GtHU7BycL7C0rC9qnCe3zh/aJRfuMQxNUF4B0XSKB9slLqksk6NASEIChidZBwDUm
/ASZyu0KQE0P6/WGB8LU1/XWrQ4AJad1dcEVaSEFj73W1QeN9eI3H+3lzVq6q/q+V3167/r3
xbvFJ2kgg7v7W+V35pZyElz2PO/XD7duXphbOv2alYJFDO7vW+ULc2/NS98o1b2627lc3Dy7
d+r1NOBi8G8UWFXvH3/jDQlEUxbvvwDBgoWSeXugAAAAAElFTkSuQmCC</binary>
 <binary id="img_36.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAX4AAADEAQMAAACiMtgYAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAAHuklEQVR4nO3af0wbVRwA8CutZRhH2WYMU1i3xCxbMuPi
kgXdoJuamCVDjf9NYxwJAxdhIk6k2nFVDNVErUPjhrB1/5j9Z4wmMIRQtv2xEYGSkegWRgFH
iiZqi0Xbeq/3fO/du+uPu+u9080fiZe0vfbuw/u+H/fet0c5yL59jx42junUUU7eTIASCAlh
AiFlb8UsiLEBP3qIUxyo4thAUqkz12IG2JlBStkT2YBgFuSU4EWBNaHHWs6hfBzLVJKclXlb
KgFpc5Bj6zKgKA8UcxYEgsrfDUnH0HNc+QyQZ155P6kCvNckIKfkATEHBMyCYJruu1hBUgZR
VhC7+G753kMrc7ogr9K/t83UX2k8tBJgAWEEYm0zi+FGmzYQ1SUEELCNNtrudDKC4NLOmfoL
jYc+eo61Dkv9M7ayxvo7NSutVQJ6GSZvWOuAXtK6QArJxQ6kElzevJCgAgBEHS/EL8rn75ZA
H35OYHCUAKngENlDl2Myfl6OiIAN/PCbTeX7LP2XXWFt8PJsiD8z6RABIEfs/PAn+6frSvrH
ghKw73p8wZcNrLNdJWe6asUpz25cIY4f/mr19Pa9XW/TEq53lzZ/KAOALl7r7APNux6YEMOe
PnIxI3Df9MbKrm0UzHU/umeIgmSalFDUtMs+XlPOX8chFaGQtk1vKuk/HKAllD36hAxi1bgO
p2qeHxqZGLlyjIBSfrjr8NXazv4XaB2uH+dqv6RgqQc3q/et5t6PxmumPBUY1PAwQJuNgjW3
1/l4CsZwx3HjPw1N/MgnedKsYj6AgpAIUhCdwGB3ztDIukTDSk/nAlEXHM2bl8ASAdAEmDUJ
lisLAxs3AtsXEs721G/FUsdVG4FhsbUu8nhr8jteGhrpwsDDnQOvNnftfyX53et+NjACX34p
4jiIgIMBTGLQ3h5xNLQ/zARGMfB4ItFG91MSiBWu9ChA4LX6yItt7rVMIIRAtH0h4mhJrW0n
IF44pDmQ1XF+NhAIW/CsM0VTFhrSCidW4QX1HAGZddoJ8tfpOB4aeQs7zAE9pCQIZ8ghMX6K
Fr4VZjb8WYLGKP2JOflQBuhsKhCrNAnIaDUDYFrv1H8KgKzsqxDIvI89qHXaD/m5d6aExHot
4C0AerQAGmack+wtMgO6SUC0GtRBBYoV8IsO+BXOcxbURjSkDEhoNWtWsn5UAg4DcI+8J9Zz
uTFqg6zcm5YQUIBmT5sGy9JLmoDca1oC+sn6hqNKovszr2QCMM5L5RcPSNPMXUpZp1ULitHs
rQIgZRIkWcCCv9gRlIGbAYzZNztdFAjHWEBrW4crKzkxDuk3scIUmIiI++U6LJ1iAWHhR6cp
cLo1JIcUZQELVrvSrGw97YP4+c8NjT8JgCsXWIxA0skMcMel4qlJNKRSaQEC7Gv46tLxqt7H
2q671CCO89Z46ur28fOLn5dNA3x1vs9Xb77c/fQzDWMSEMvPj1TUUZDAmfG65PqP9/csDTRc
Am+gD9/kq3e+M3Cwc3MnBi4odu/rv3trkJaQhOnL1tTX3sudUd+RELC4927jeHdbyYm6Tvsq
1+kVDEZrz25fTUGsnUO5d+rrs6MXJ/1H/MDKPQIx2HdiT2/DAVcYA3DhWd/2Wh8NCc2tyR2p
kxuv9C6G6n0Af/Ww8277tQ/WWo9MSyD97cSJHbUDFPTgVkpWtXzTuxx66QSpdA1/bIMwGMRf
iAhIltV+WbWalhDvIT1N5rq5TE/LfS2BytmgZ5YCsj5QENABwlx2TxtMAhjkDg2DaebWg0UJ
CKEAzzZV2ggQ45138DmtpAuaMFhO3hh6noIbPYWBB/f0W6Iw9L78pckAAAz6RKFTBtExIxCE
oEwULkbYwCgGOKQvGswAb4fgK5XrcK0gILl3GooCvlGQPVoLAnlj6QcCRjmY+oADU5l1etmL
Mhdxag2EWwhwRectb2sn60YLu1NKEl1zmRDYssq/AOY1s8oCIGYWALMh/Q3AbHavU8KlNbj3
WjRyb21QIFnXBn4lj1Ql69o9HVD28kH6JgE/tEJhjwXl4Gwga7izglWfcVZ8XXhUIDGoAbJu
462YBMAUWIbcbm2gf01X5AMQl0Ax/tdBQw5wotfoaVUJBvOSGoDCoE8VEhw0B9I3G6jqYLTs
/gMgfcsBWDLZ0+ZBxCDXUIHFwh2nBvOawKIPFgbxLeBc0MGv1wc3BnHeCoEjkQEbeDfO7h9p
vOpSg6VBnBmjhTWeAUW8e/N499O99R9qADxak1YIz6VP+UuiEsC593sou7+3Wwug6XALAl80
9/k2ITDPWRwYrB6oC9jKtQC+d78OwpHDJ3e0vAjpzX53W+3Ak731BxQQkAEZSzFU4Za64/aD
DrnSr5VOf979WNO0q29RCwgItB76dGedQ+6Hjg6IsnsUSJ/23IpLaP60a5UClL5WlxB/UAJC
Mwx38OpJQAV+cUP5NiM+zxSQWs0IxFI3GeSv04Yh2fRaSQ+AvwzgoFlQuNLeWw4CeQmWeMws
qDSYW2XQdccbJ48f3ARFi5R7W4rwTLT+Gl12UepWJP3egebemR8/WPKSdTKDqZN1ZRNvIyB1
P2kH8SEaC2eDIv31Qh4AFSbvGQOdjEwX6KVwNxGYv+/976vD/630HwXAahZwFu0T9UD2FKMN
/gD/0Zo10Ip0iwAAAABJRU5ErkJggg==</binary>
 <binary id="img_37.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAW4AAACvAQMAAADnr6ZpAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAAEqklEQVR4nO3Yf2waVRwAcLZQ4w86mF2UOipN7Gz8y4aa
/tgo59LFjKyuiy5mxmiYFjYz6Gg2x2paL0DCYsx02kSCtNRE4/4y1tbYYVlpJcuiUNukf7h6
/Cjr1h+2jhFUIL3j5DcHPLi7TExn+CbH5R6fO77v3bv33sHA6QTK+I/5Nnq8lR7vvVCJthyn
zquka2MvUuc/vLNa5aHFG2nwvWfXAo7c0hC/ENcyWQ15VQ14CvD+jwbZoUAed0q84LtqxX35
heGh+g4aHK2uf4NGn8GQE3R4GHn6MA0emKstUFVgBOfqA0X4Sp6/jAO4V8PkitYvSX5k5/IV
EEdHu+yT61XdBivoN/N4WCPdEK1zlGpqHH1Cuvb96rxsOJf7gHxDILc3rc535CXDBnKvRs7l
VM5JbDk8AgN5tAiQM2bTPbJ8Q/k8M59jAL74vuDU6Gey1wep8Tuud29f2afcT7ETxHhL45uv
Aq4O5EOCW0o9MJkID8rnat0O2ZRkP4CjP1nzuQ03wT4oqyEjLT0BndxutZvOnGPtHrG99WtP
+jtPtH1zOLqzx2s8+ZoVcTfKuux1C+LLSgIPxQkxmeFmh2F7NWx0N76g+O1Zdc3nZ/MSJXKu
auqqpg3e5/7iFcXP36h5tfKifOD0rPlh3nu9pg+apDe+Gjp8oihHObUO7cmOPpbI3vT2wsh0
t+pgMR5pYfh1G3fQHbDnXHjTHIyoplJfJcYpYRaHcXd0BMt0SDR6nOIhjmRI9yCUzT1RHgby
8ADLJdjMuXrB8IX1T04wAlSHpTivo8HHWEaBGKLMR46rL/BgqhwPxp+yLcITUeb3yFPrM4od
mCbXlpRfLykHNeS/x1NB4BF6/G7uGqM4D/GpcIjAiSu8CJFlqprmKD9riYSScaiUPNqQJeQ4
fp/w2TSnFTG+zYVjI+mSh24++onhgQYGs5/xElGyt0xV752Xts9EStrfqT186aIQHycNIqc3
ztAclmiOkVRH4E0bJY4mecCYxX/hZxjmTexjs8d5OMFt5sT7RpJ/OJvh/pk0R1VJvmleaoAz
vNLXmpqKcK8ziMaKozMfprIs85O8mXB1h696IJ2MO2CEEhxXWWSzCX6znsBnfNXWDPfHT1XH
+YFEMhO/j2fxnRnu9FfF9rFJXiU8muDa1VFC7g5fxUSG//Vy7FxFdDvTtssX59c3rhAbkr1H
m+bTfx6L5R5r9/P9j/EBd9XDX0l3ZGzpbyec5H34Fpm1y7zMy7zMy/w+5kXWvmwApxBl/r/n
oH+vi/BFKPYZjG7wSvbLM5DfbjhYoVdOn5pc5rTLuaA0s/jMgLhGL3MdEl1bMHTMP0XKB9vN
j0uGd4uuYWo+p5mM/3Gp3XJEMswVfY1peUfGyfhi8yGLq3vhmGmPyyBGSK8euvuMxSNZEptk
NTY5l5RjIQXuwfHT8QPyhiSPLcwjfbR4iAPT4WE9hPs8Vn90SFyi0pAe4S0Ht6lL0y60CSnw
yT7pReTLzr1GOUKFC9EDF5HvOre3ShAmOUd70aMK5NtOSChHnqPAA9guBTLWNeFEP64j5yFO
26dr8+M9GgeqrqCQe7oYvwouB3M/PV4o6PJ/AIhYcQdQ0rOqAAAAAElFTkSuQmCC</binary>
 <binary id="img_38.png" content-type="image/png">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
QmCC</binary>
 <binary id="img_39.png" content-type="image/png">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
RP3+/S/u0oicFBj9FgAAAABJRU5ErkJggg==</binary>
 <binary id="img_40.png" content-type="image/png">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
</binary>
 <binary id="img_41.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAASUAAADIAQMAAACUBGXcAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAADAElEQVR4nO3ZP2gTURgA8Jc/Ni1GIroELFyHEkFUNEvd
DkW3dhDs5tAuWqFIXPxDxaMiOJqpcSgEHNRJCiktXYxgwYqGU1wUkdihTcXSXojmck18z7sk
TZrL3fs+2jME22/IHS8/vve9d5eX4x1hiCgQZ5Xa0ELJZnh4SmJxvT3ANhpVxqyqkftnqqmu
XaSKbaqA6hVRPyYhldEVFUE15RHuwmrG3SXB6tnoCk4xWD1F5Zq6jqlrbv2ICM5XNfaUtUKu
Jh6Myru2qg07dYd1iyxL10SjRampokl1jBydSoXn42Ul2qnwtdiE5p6PKx63SCwiWFan9n+I
Lrq9AstZGUI6yyryY2JmINzLGuuqV79QVqGu0MzFcK9gtGSZjRrzBSPKUv5KrW4rpTKpyCxi
m8oKNSnr2FM7URl7VQpglCZjetTWzkRgVeg/3QOr0oHECwlWqcRJWGmpRKiq7MJQCweH4LqK
3w9rDMzFcp8YQlU+2vfOoQSl+lFq1UF1FqVczs1EwWenlC1rNE5RFVV9DqWuosYYQKm4c7mo
7Uw0qJgNalQPUcqHUSyNUqgxllBjZLiZELgqXu1R5KllbwDR4+pShsFXaLlUVW94Ku+v9Ehl
nlIfPEL0qCpypfo+bl3Z9Yqa5c8EZoxq+a8Pe6+2evX93PJc/7nKQKq+Rgd5KlAgRKCE9Dxu
vzGilA+jKE7l2L2q4jw9UtUfFBE9Hn+XhO/ovhM/YUVvjSJy6eqtCD9j5r3dsGL0t3EGql8Y
VQkHVoA0KtfX2bk4NYrj5preR8jKff/gOW6uaW/HSGfCfx5QFzqGjj0hl/gqlnh5W5p8P7jM
VWnjmyRjf9rvl1ZXnF/HXo9gLkqaI7hdJW2eLdba2nn/q1133HblLv8rHRZEk2JmJetKM6v1
JjXsDY8loR7l/skovGstX/7yUT8AV8hQJfA6ygPfohqshicPKea6mu8JKX3zhnmMrX/X5KRq
uo47yOVCqXEH1WsHlWmMAkrZvKU0hUCfS1nFxfjKIpDqL5FDRLv03GfaAAAAAElFTkSuQmCC
</binary>
 <binary id="img_42.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAARoAAAB2AQMAAAAtPM5sAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAACq0lEQVR4nO3YMWgTURgA4BcavGItoViXNhAIWNwMOlhw
OANOomgnF7Fd3KI4iFUckhZNMwiKEYk4HDjURaSD1KV4KRkKOqiIg2i5SIakUwrBXo7c3fPd
u2uae3n33qs2cem/HHl8vP9/d7k/PwGQH0XQO1QEXgz4kOJDmYEZEGlAIFcYqP2p0SOk7SMh
BHuGNmPoohJIJVADbWbLnAesF5LD70mkkTvlD1xLk+lI1Pq6fCut8FB++SYXNV6cOipzUWF2
WOXV5MU++r/oZ0QArdc7TCUAWbXJ1ZXs+JuSs1AOQhuPz69kP77+jpHXVqUMiKyhixmKeuhz
9OFC7tjcZqgJaHEGNCWErtyfH4zP4Z3SXelsB61v5C8sLf2IP3dWfkEaar2zaonkg+zQ4qqz
ogcgaFga7I5OZCAEeagpQYuKKh3InFYowttpwUNlWQ1EtrSNYnSDke6hFq2eNhpMuKf7FKCc
mqRUKQ0hOl2MgU5eX8PIlhlo1kE2G9m3U88wYqWzt8KT0ElnTDPSmb+hiy6ykOUi5uncQM+O
dTocqHDy5dxB2+tbrHRfDo+MXC3kFp+g+xSI3oaPTI2+BGAI7RRY09Ozo8lLCB1EKLCmb6Ho
1OVcJj7GOp1Whfh3jlk4fr1UyHt2bjjfTJVu/K9UTACZmsJHthaQb9fpmjGBnci5YO+Q2zbD
p8shNJgCpx1QUKa7sdJmFa8l4v6kox7ZoiK4g3D8K+rXuEZFWn+RUE17h1QRpIggMl1ZRs2S
nFjJdFWEDN7sW03Mj98j58wuNHHoQ9fsS6bTX6VqkFeTfjxVM9Ocb6Y+cePcXd7NrCbqY3d4
p4NGHVrq37wIAajPO+0+nSkJIDsA+dLZETryn04IPRJBigjSfKjdWP3/F/ijdGIGSHj6ZSBK
FP8ApydF2h2/IoMAAAAASUVORK5CYII=</binary>
 <binary id="img_43.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAQQAAACuAQMAAADqNuF1AAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAADGUlEQVR4nO3YT2jTYBQA8FhLW3uwnW5ug2FVRAdednG6
HgxzggcPoyfByzyIeFCZIBs7SGRI8VZBEKdIp4d1eBEcbOClHQOnDKxSUYStqQjW4VirVdOQ
Np+JbbokTd4r/ccUv0OyLL+9976kvH39KGI0zlBblR/TFCGs+t51qjC2mIpw8cz/F3+dSEhH
DhTrDCGLsDjujiGis/c0nCXVNzqIiM6Rk4gYGH6PiORYDBaFUYsI1iFG4wSLiWUXJlZejSVJ
FhRfesZ/7PC+YKTrT8bi65T3apdliZauE3Th90uR/ee2dYVLYvLZLrfl5jHKeEhiderp23fe
Q/LfJtpZXZbInzqG/a1u7wU5wUflhqARgiAnXJOvk8oNTQxWKM1xY2hisGW3y2KYiASDiRQs
pDpSV2weOEb6Wi8Li++XDiOCg4VUx3q/VarjNVTpm+00LMgDCcCCMKggoGBRMXP+xCnpnf8y
F9OOtu5bvvF9oHD4fRYLIGw+58MW5yOg0qjcmolYy2w3ieBR0ZwYtWQp60BuQ8FwqT75IkFF
2LIYtL6Osq6tCPO+3twYTRdZe5VCNVt+rkqhypJv14l56RhQi5y+jmXpeAR86vEbe0Oa9Ufa
oxezF58kNbMN6gV3ORhVCz6qF3NrgaBmLqxe+M+GPJq5lNURzYTsmjfH6ERhQDGqEPwQJvJ1
yIJ/CvEYIl4H+sSyqNBl2SkyPxeU/iMt1ObdVM6lEQZdSvfElKHqdLyxUHXL3BAmxEE0hstM
ZOmiMK1DCBaEeV/P3F0MwCLbsXAQFrl7K6Ow4O9gIjP7EsnCTQ4glZLPQwT7L8aigtQowoWz
aL7qm7RZnRO3jwJrmOnWNsfu/j3QSsna4nxMdQMxwlGG8DM0sB5T2tumXwf9I0Ig/IfVb0Qk
eZeRqGClxDMkld/oQdxwI1YX4YpFLd+z6aaIelSqiNqeGLZjRJMIkb9MQ28u/ryDg3eM4hEK
2cuJTxxAdowqEPflfSmw0oWeGCTCxXNjP0H1yFJdHaIdrbQeIoBWqhGGMVi1MNy1Jlphofx2
klkqid+64ehLR4oj8wAAAABJRU5ErkJggg==</binary>
 <binary id="img_44.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAUYAAADZAQMAAABmXguRAAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAADg0lEQVR4nO3ZX0hTYRQA8G9N2kOijjAfSuZTPloEapBs
UuFDokSvmVeQ+VLamwTRFIkeS30wQREK8iXQtwjyT+zBHmYrfCiR2cxwmHO7csFNdv1Od/db
enfd7ndWd7CHHdBdrj/POd+95+7zD4GT4YSfyueo8jF4fFImAFQHKTkKC1c6REIagC89R4cH
RZlX6cmDzEefhSZlW/JrGHkYU17jOPnSfiDiZCvx4qrvR9zXkXIr0oKW2Jy9W8g+Y2XCKkpC
ChX6hBTlSSk6/lHOZ5exH2myzEB+bxSi4sN7gSH13GkDuTpa2zfqvjs+rp4jGaOEybGZnpEn
rq8WMbNiocjQ2pc64YXg+sg2PyuA0njcARJ9BPsK8O8AiCSVc7p1tLfjqVOVNsjUZ5Ct6GyN
a6S7Y3lYPefIKH/9XfvGrNAZ8IIu9BIS6rafIf5LZokTMmsUpfky7sHKoBMrl9DS3xjcw8nQ
JUFA9lnvbvMwaRBJKdX3IWRycEP1EWTO0EV3G65PPxGCOAmLPcirBP46QEr24Bb8fBalKuVC
lXHdboiWRtXl537lRQJ4181/4jR94iX+yhuEZkvLpTo1smk5ZWLQZ7qMoHPmIA+RkvrQkmAl
nEdLo0i78qIVK+M8ScUHTFKJJ901TiYTnKmj0ekFJq/wZCjEJNg4TzEVgqw6lBtcT1U+rvGw
6jZe9c43KXmHJ9unmZRXskP1btJgLeuTcqXYN8VkC3pC/KbMUvrTYUXPPFqCDStlJ7o6WsIz
81dE0SsCQK8dmTNMaANWgjlSs8eVGvcpaqXhfde8v4Rz2o/Mv/K5SIPIV/UBtJxgh2G+vAab
UZy8sDHoRfa5OzGOy+nbnZjDycndyTlc9bnlyfe4nE0Dg0N8mZxPz8CUF5cTPkwBUs6jJQue
lNBy5ujwPke+slns9nPNlsrbHClFbK7m/opTXXbLW478PXbDokhSWcGTr2u7rvZUXR6uvsmr
vqDcI4AYf0W6dxuzpc/4J3+dNCVnDhNSlAZStigfHu59lzP+9bM0syxByuNDSfv9uUj9hJiR
syiLMiXj5aCe50upHdT/BiLk5/4Y7GFk4hMRVlDVE9sB3zcZJXeW15fMltuB9RWcDFcvruFk
pGljDQp/QgpSimi56MTKIF560H3iJWBlHC0P9NIK8i31iG6mS31odkOrsVR+dzwDdLacElKl
kX8Ao0QZ6cfYMIoAAAAASUVORK5CYII=</binary>
 <binary id="img_45.png" content-type="image/png">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</binary>
 <binary id="img_46.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAANcAAABaAQMAAAARnVHwAAAACXBIWXMAAA7FAAAOxgFsQb88
AAAABlBMVEUAAAD///+l2Z/dAAADSUlEQVR4nH3Wz0sUURwA8CcrrmG51SEIjamjiBBJsLXl
QCfLTl07hXjosIq4YOHqs4S6CAtCVCK7f0EQQRASuy4WYgfHqxq6i9LqQXd1oGbccV7zfsyb
76yzvsPMd/zsvJ/feU9EeNHwIakpNhLB76FCXfsweqATkgu0+VEr39h+EwdZdrSyllieCbYR
K5/YextYZ3bEvpbYW6pjKx8Ty+/rtFfIJ2RfjhVomZJOrJzlvoGgbZWcB7e2BcVQoJWBhQiZ
AgaLpRJSwcFWoY0p0HaRJiI2ypRnNsLEaOBxil4WPXvNR8KuKr0cSjOwd7NYbElbFK/TyTZ4
I6owS3G7j2ltrCjCtmVvI/IhJUyRposhODduvHVeqRgCISVuOvGK6laic9OAaT0qD0xuOWB6
D4ZmY2BWuxswOyHQrorAnnBscgyacQ6YgcI+C7lRDyJF4stKmg28pJEzvf6vqwRMgdMCbQU5
A7BVaGXPaLoq0DTPTOKfM+/hD6Lz7Ovolmcl2DwtGTf4y0wPtCOWsCa0NK5vWVUabdq3ENnP
a/FPk00Xp1+gXK3d/7r28Hl8ZqPrCzMLWvL2cu+z8dm2gfnT9vJdvmMgPrvR9Q3Fam1s4Wdn
/MKbtukoimHwMdOCd9go1RMD3S0OklYM7YBet4hj0f3HnWmfuYHz3n7zjSwJtlvxjr10HYvF
uzrm/LbYTubKFcdyqcH4d2gq+dX7tO/BZccyteNTyXr/o5b0KjmuY2PpVczNrLGWvuYnV3DA
+uXI+r383NIlZ/1oOlahORVhtmhHZ+YL/Xu9PDNrDeTuWXltEbHxnTL+HeFA2zzj+2PfbSVS
zwok3ABNDjaLiIWUEDQ5gc4+4ey5U0HG9h5CfsDO7PhNSwHz72ekDBZp1x2t6e7zctuyIu4s
STPcIYbkQmzLc8Vgv2aP/v2algJqqKAwbUvsKBo84+whURm/ZYLOP7EtqtJOUrKv/DyysGdN
dr9bt8p/Ic3snqhawzxW6KXoWfVf0kDX+YKw/yqWPNNL49Xu8zwfivxlz7TxalScxvT0dObC
s0xSj7VysyPsXc/UZP+dRpFjr4gdAlYNTYSjw8IqCCnAzE0MpmaSzoM3dt/exJuV8xlg/wEl
UZl2IgoEnAAAAABJRU5ErkJggg==</binary>
 <binary id="img_47.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAANoAAABdAQMAAAD5Zur4AAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAABlBMVEUAAAD///+l2Z/dAAADi0lEQVR4nI3WTUgUURwA8FeGFpUr3QJpsoMSRCVECVY7
YRdhy+gkdNgkaSushECTslESNi8qdLFDDB7qFkgdulRObLQVtoMeOkQ5LkZza1f3sLPsOK/3
NW/fm52N3mHmzfz2vXnv/d/HAlg7GYDel6xsbZzpMbSa+Px25o4NoQuhWY0nE1Y2tevz7K3O
hjDUF58cmnzc2j5cjacT+sr4wKOm/XMhJY8krJgysGNP/2S0Gg8kMurM2e5XhyfbqnE+sZo1
tn2ebd2bJM+6iHoOQq/Ay4wBRUTUPa/om9Pg1gloWdLQQLiqVVBOqDuuUgNd3J20hAY4w3J5
fFkTEUShxx7WSKMEpOF8SK4kMF6Uo6OQmxcn/SJ5hWOKVbBOy9DyDN0oQ1zU1UjW9nGNt9km
TcGp4KPO0cFvSSoxZBWxdtg04zKshAPCnD/FPI2iLWDZ9OdflKIuoGv6TwpBT/gkConfLZ2g
C0PRROiBTtG8rZqA+cYGESGPk40wBd9LmBdx2h8vlkp+poBQ8cMQxJIB8NApIpYFxHVOi1gQ
ENeSDkXXADgvjm3lAaEN5ahImBbbgFNOZxnPAKbYeoLNQ5fgr7JZXFEJSiOfi/Vf/KYenWip
V4EexMy5/uUL8aG20eNhuPvyl6b4UMPosAoWYCDamfVrX5a7rrZebwNgQQvgz/y97xt2X/JK
RAXHg2gVNNpbTQXHUj/gPhH9bmJs/9R9Q0KL51Qwn9le9y4cAXjZe7CrJp7ovbYziCN2ObOG
8bXyIB5Ar2lpbqIFf7NqEEwUjqcDb8cx4pZLw4dKGm9unQpHVHJ55uazFxitEOz4mJ3qw1gV
bITQxXPFU0EuiPj9Zo5i7Qnm0alZc/atw3/MWwdKR5RQzSZaSFDeMKS1gtdfVERxCaJibm1M
w3xERL7O8cpe1eqkU4CPJd4THBAZl1ATEI5BCXlwc3Tv+6AJmPebZ1I0FQFtH3W2pa5WijqK
znKKfzrUcUzBwH6LDj2/YvTFtN8wfiIZ9Et4PAJ7PCkLOu46zfgn6/RFSToik0l6XgfPFSkF
TySSFtidbaLTFSx/9XhsFHblWFh0+eRJsvIci1OlfFOW7vbkoHU0ARtHNkCMYp79guPv8yOF
wZhFmxuldVewHqFKkfxPiMhYvNqj04cxGlWOduZ+mpd0It4WKGDxjxcZzDJEW0VExELgtIMi
ljZhVQr/V/Nf+BebiBwJTXmwRAAAAABJRU5ErkJggg==</binary>
 <binary id="img_50.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAaoAAABaAQMAAADTpIAYAAAACXBIWXMAAA7EAAAOxgGnHWyZ
AAAABlBMVEUAAAD///+l2Z/dAAADgUlEQVR4nO3Xf0gTURwA8LfSnL86UwijYiY68Z9StKlk
zR8V9gsi+iOCMps5Fm4rCmvNcpa5/tCO/kgDq0X0R4HRhCw1c4siq1VHCqboXFC4P/pjxsjb
2t293t3avHTZGuU/7cv2dvfe98N7997dPQZgKGEGf489+emMCpZZQ+vN2pgFUqoEK7RimTq6
frk0IJtda708NNKbTm57JN6nVkgOowT7bFaoOmU7uOR59j2IaaGTYx39I8ZIMrYrtRirrolB
rB86uHQGQtIkLiDMgKGqdg0UJQ6ktdjTsitXNaCkMeOQtf0Qqei6vUNdXaNANe+bLMpKgXBB
8v7J0kWJD1oRu1ZZklgukal65Ln4Xfw4SrramDCWUpMXV3CnQqUit6CawfZXyk2kWjy62Vn2
rWMqygwo0YGtI+WSUlWP/jjWjB9hLxWHblT2eYdlQOW59rKKSPKA2FLiLHJ1TyWZAf1Zsffd
FslgZYs+orH57loTyjJxjAsXV3ZXZyrTH8rElj3OMkXLUj2akg/rRkdlRy1HL+Ytzh7LbpJy
bEb0RmYq0/KEq5NLJj+u1LNTgqYG1ZNsbh28FmAVuaHKIQ1RuwdNtds0vdw/2Hp0SOf9JEj2
w5DsoZd5TAHvEjomdSO4tDP/bAMAqcAIGoSts3ICsa03dgNBRH6KbrHx9oXc7fWS5UGxIlCS
sCQhH5xfoLuZIgFAmBQUo9ps7K8aQgO6mc4YIBkMCybCLMzCLMzCDE5vcfPCPPPKnKGx2fvM
v2RzxX/K5ncBwizM/i/mAmyIArKo0BjbIg3MvC9vIiCrQ98gGT9YZvq7zPFnjMYasyjG5iHY
Hok6XxPNSzuN/myaoDlOpnd88jG3/FaLiyI8L5gTV4699jPebkpoIAX7Sr+8jJYSz3zMI+/X
uVwOz7CrWFhh9rMvoukxaQw6vK/NOejG8Od1fma/STGIWdxagVmAceuHMUKRDvhCI7qP97aR
Rk2UY8LHnHKyqhaxsW1PtPEvfL0xr/i9LevEH7c5cnLIaebJyJ1i2bAmWrbjrdTHLDjv2ohO
O8sy5DwW35TVO44YVRxbYYP+3sr9jNFia9esf0qcXIi9mfBPCZpqw7jNu8XzmIjHRF/xwglY
+5R+w/jXjUufyfjBsAPbwAKapn3sR/yW8Sp4bBJEQVpv+CWTBmYznzc+m+N5mzuo6/yzUPfu
71KKjCUNm0QaAAAAAElFTkSuQmCC</binary>
 <binary id="img_51.png" content-type="image/png">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</binary>
 <binary id="img_52.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAATgAAAAgAQMAAAC8Q/ZrAAAACXBIWXMAAA7EAAAOxwG+Bl3Y
AAAABlBMVEUAAAD///+l2Z/dAAABoElEQVR4nNWUQUsCQRTHpzTXwGavHqI91rFDhxa07RAi
EfQBOuQh9CJdpUsadDQ/Qbgdgj5DsJjRYpexPkEmtLoXSaNIpZ15zY5YRGRbl+jPMO8N/OY9
3pvhIfAm5JXLIaGJCELYdebcbRENdTQwk0jvaR30rcaQ/nWuh3e3/5+5/u/iObICGlwTuSQD
nLGlKSYD06Aoc+6mCx1zyE3LLBDO7eOQxALBOg5WJOafkf0S53LnUDYThdCOmzdiOxZJr7Ws
WcdqmbaRTDnJSCyZ4txdHmpXpBYTnGpTaqXTKj1xqLVyaWwrDkSauwrnHougk9pGTNShdmm/
MRVSQaP0yS8ZWKEQ7QYEVwK9XFteFVzU5txB/tTlGgu2UdikoD43N9/ircQHdaw7VrVKWm2N
22PbuMo43flmMuNyeSCEbBHBYYXhcDyOscxwcNkfTPN6x/Gg3j2om4n7ylufKeWbArpxcese
DyVF9LlsuyfW/sF7vJx54zo+4Euo99E+AMpCLvv5H/Qkb9zIvH/EiXmQ9ch5mAfA+Ou47vyo
Gz6v8+oVjeXABpImdu8AAAAASUVORK5CYII=</binary>
 <binary id="img_53.png" content-type="image/png">iVBORw0KGgoAAAANSUhEUgAAAKwAAAAdAQMAAAANaRVLAAAACXBIWXMAAA7EAAAOxQGMMD9a
AAAABlBMVEUAAAD///+l2Z/dAAABHElEQVR4nGNo4bT/DwT/Mg4Uf7//HwL+XWBoYMAKQMIS
Jir8QEXHHwDZAgwTHJgPMMz/Dwfv4awDDPLYhXGoRhI+T5JqZOH9QNeDWB/uIQlfAKr+YWH/
4IfDA4f69///8PH/kJGpsAep/vd5/s/vaemZINVzjv85c89y/gWQ8Pf7v78nPnwMEt77/M+Z
PzvvX2CwBwt/ePjwIVj4+x8Zm5nnD0CF96U+fwNyN1C17B7J8xeAngea/Xt5WnoOSPWe53/O
3uicDwqTDxb2HxQSEtjqz///B3SJC2ODPEg1xN1gz/8Dhe/91/8vIIcgcpjgEn7A//8AP0T4
B/N/IMJU3U7QEMqFL6ClGg4GFgVwYsEBgFp+IRkEAi9lPwAA3onkb/8gtFoAAAAASUVORK5C
YII=</binary>
 <binary id="img_54.png" content-type="image/png">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</binary>
</FictionBook>
