Это и есть еще одна фундаментальная особенность QNX/Neutrino: распределенные операции выполняются в ней абсолютно «непринужденно», поскольку потребности клиентов изначально абстрагированы от служебных функций, обеспечиваемых серверами, благодаря механизму обмена сообщениями.
В традиционном ядре действует «двойной стандарт», когда локальные сервисы реализуются одним способом, а удаленные (сетевые) — совершенно другим.
Обмен сообщениями в QNX/Neutrino элегантно реализован и распределен по сети. И что? Что с этого нам, программистам?
Ну, в первую очередь это означает, что ваши программы унаследуют от ОС те же характеристики — сделать их распределенными будет гораздо проще, чем в других ОС. Но самое полезное, на мой взгляд, преимущество заключается в том, что эта схема обеспечивает модульную структуру программного обеспечения, тем самым значительно упрощая процесс отладки и тестирования.
Вам, вероятно, доводилось участвовать в больших проектах, когда множество людей разрабатывают различные фрагменты целевого программного обеспечения. Разумеется, при этом кто-то опережает график, а кто-то запаздывает.
У таких проектов проблемы чаще всего возникают на двух этапах: на первоначальном, при распределении отдельных частей проекта между конкретными исполнителями, а также на этапе тестирования и интеграции, когда невозможно провести комплексные испытания системы из-за недоступности всех необходимых компонентов.
С использованием принципа обмена сообщениями развязать друг от друга отдельные компоненты проекта становится очень просто, что ведет к значительному упрощению как самого проекта, так и технологии тестирования. Если говорить об этом в терминах существующих парадигм, данный подход очень похож на концепции, применяемые в объектно-ориентированном программировании (ООП).
К чему все это сводится? К тому, что тестирование можно выполнять поэтапно. Вы сможете написать простенькую программку, которая посылает сообщения вашему серверному процессу, а поскольку его входы и выходы являются (или должны быть!) хорошо задокументированными, то вы сможете сразу определить, работает он или нет. Черт возьми, можно даже создать типовые тестовые наборы, включить их в комплект для регрессивного тестирования и выполнять его в автоматическом режиме!
Принципы обмена сообщениями лежат в самой основе философии QNX/Neutrino. Понимание смысла и приемов применения обмена сообщениями будет ключом к наиболее эффективному использованию ОС. Прежде чем углубиться в детали, давайте рассмотрим немного теории.
Обмен сообщениями и многопоточность
При том, что модель «клиент/сервер» проста для понимания и очень широко используется, существуют две вариации на данную тему. Первая — многопоточная реализация (об этом речь в данной главе), вторая — так называемая модель «сервер/субсервер», иногда полезная и в обычных разработках, но в полной мере раскрывающая свои преимущества при проектировании распределенных систем. Сочетание этих двух концепций предоставляет колоссальную мощь, особенно в сетях симметричных мультипроцессорных систем!
Как мы уже обсуждали в главе «Процессы и потоки», QNX/Neutrino позволяет реализовать множество потоков в одном и том же процессе. Какие преимущества это нам даст в сочетании с механизмом обмена сообщениями?
Ответ здесь довольно прост. Мы можем стартовать пул потоков (используя функции семейства thread_pool_*(), о которых мы говорили в разделе «Процессы и потоки»), каждый из которых сможет обрабатывать сообщения от клиентов:
Обслуживание клиентов различными потоками сервера
С этой точки зрения нам абсолютно все равно, которому именно потоку достанется на обработку отправленное клиентом сообщение — главное, чтобы работа была выполнена. Это имеет ряд преимуществ. Способность обслуживать нескольких клиентов отдельными потоками обработки является очень мощной концепцией по сравнению с применением лишь одного потока. Главное преимущество состоит в том, что задача распараллеливания обработки перелагается на ядро, избавляя сам сервер от необходимости реализовывать параллельную обработку самостоятельно.
Когда несколько потоков работают на машине с единственным процессором, это значит, что все эти потоки будут конкурировать друг с другом за процессорное время.
Однако, в SMP-блоке мы можем сделать так, чтобы потоки конкурировали не за один, а за несколько процессоров, используя при этом одну и ту же общую область данных. Это означает, что здесь мы будем ограничены исключительно числом доступных процессоров.
Модель «сервер/субсервер»
Давайте теперь рассмотрим модель «сервер/субсервер», а затем наложим ее на модель многопоточности.
В соответствием с моделью «сервер/субсервер», сервер по- прежнему обеспечивает обслуживание клиентуры, но поскольку обслуживание запросов может занимать слишком много времени, мы должны быть способны поставить запрос на обработку и при этом не потерять способность обрабатывать новые запросы, продолжающие поступать от других клиентов.
Если бы мы попытались реализовать эту задачу с применением традиционной однопоточной модели «клиент/сервер», то после получения одного запроса и начала его обработки мы потеряли бы способность воспринимать другие запросы — нам приходилось бы периодически прекращать обработку, проверять, есть ли еще запросы, помещать таковые (если они есть) в очередь заданий и затем продолжать обработку, уже распыляя внимание на обработку всевозможных заданий, находящихся в очереди. Не очень-то эффективно. Фактически мы здесь дублируем работу ядра путем реализации дополнительного квантования времени между заданиями!
Представьте себе, каково вам было бы делать все это самому. Вы сидите за своим рабочим столом в офисе, и кто-то приносит вам полную папку заданий на выполнение. Вы начинаете их выполнять. Вы по уши заняты работой, и тут в дверном проеме появляется кто-то еще, с еще одним списком не менее первоочередных заданий. Теперь у вас на рабочем столе имеете два списка неотложных дел, и вы продолжаете работать, стараясь выполнить все. Вы тратите несколько минут на работы из одного списка, потом переключаете внимание на другой, и так далее, периодически посматривая на дверной проем, на случай если кто- нибудь принесет еще.
В такой ситуации было бы гораздо разумнее как раз применить модель «сервер/субсервер». В соответствии с этой моделью, у нас есть сервер, который создает ряд других процессов (субсерверов). Каждый из этих субсерверов посылает сообщение серверу, но сервер не отвечает им, пока не получит запрос от клиента. Затем сервер передает запрос клиента одному из субсерверов, отвечая на его сообщение заданием, которое субсервер обязан выполнить. Этот процесс показан на приведенном ниже рисунке. Отметьте для себя направления стрелок — они соответствуют направлениям передачи сообщений!
Модель «сервер/субсервер».
Если бы вы делали что-то подобное, вы бы, скорее всего, наняли дополнительно несколько служащих. Эти служащие все пришли бы к вам (как субсерверы посылают сообщение серверу — отсюда направление стрелок на рисунке) в поисках, чего бы такого сделать. Первоначально у вас могло и не быть для них никакой работы — в таком случае их запросы остались бы без ответа. Но теперь, когда кто-нибудь принесет вам кипу бумаг, вы скажете одному из ваших подчиненных: «Это тебе!» — и подчиненный пойдет заниматься делом. Аналогично, по мере поступления других заданий вы и далее будете делегировать их остальным подчиненным.
Хитрость этой модели заключается в том, что она является управляемой по ответу (reply-driven) — выполнение задания начинается с вашего ответа (reply) субсерверу. Стандартная же модель «клиент/сервер» является управляемой по запросу (send-driven), поскольку работа начинается с передачи сообщения серверу.
Но почему клиенты приходят именно к вам в офис, а не в офисы нанятых вами работников? Почему именно вы распределяете работу? Ответ довольно прост: вы — координатор, ответственный за определенную задачу, и ваша обязанность — гарантировать ее выполнение. Ваши клиенты, заказывающие вам работу, знают Вас, но не знают ни имен, ни местонахождения ваших (возможно, временных) работников.
Как вы, вероятно, и подозревали, концепцию единого многопоточного сервера и модель «сервер/субсервер» можно комбинировать. Главная хитрость при этом будет в определении того, какие части задачи лучше было бы распределить по машинам в сети (обычно это касается компонентов системы, которые не генерируют большого трафика), а какие — по процессорам SMP-архитектур (чаще всего это элементы, требующие наличия разделяемой области данных).