6. Администратор процессов


Введение

В ОС QNX Neutrino микроядро вместе с администратором процессов (Process Manager) находится в одном модуле (procnto). Этот модуль требуется для всех систем среды исполнения.

Администратор процессов может создавать множество POSIX-процессов (каждый из которых может содержать множество POSIX-потоков). Перечислим основные функции администратора процессов. Процессы могут использовать функции микроядра напрямую (с помощью вызовов ядра) или посредством администратора процессов (с помощью передачи сообщений модулю procnto). Обратим внимание на то, что пользовательский процесс может передать сообщение ядру посредством вызова MsgSend*().

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

Хотя администратор процессов и микроядро разделяют одно и то же адресное пространство, это не значит, что у них есть какой-то "специальный" или "индивидуальный" интерфейс. Все потоки в системе используют один и тот же общий интерфейс ядра, и все выполняют переключение допуска ввода/вывода (privity switch) при вызове микроядра.
Управление процессами
Первой основной функцией модуля procnto является динамическое создание новых процессов. Впоследствии созданные процессы зависят от других функций этого модуля, связанных с управлением памятью и именами путей.

Управление процессами включает в себя как создание, так и уничтожение процессов, а также управление атрибутами процессов (например, идентификатор процесса, группа процессов, пользовательский идентификатор и т. д.).
Примитивы создания процессов

Существует следующие четыре примитива процессов:

posix_spawn()
Функция posix_spawn() создает дочерний процесс путем прямого указания подлежащего запуску исполняемого модуля. Те, кто знаком с системами UNIX, могут заметить, что этот вызов аналогичен функции fork(), за которой следует функция exec*(). Однако вызов posix_spawn() намного более эффективен, так как он не требует дублирования адресных пространств, как это происходит, когда сначала вызывается функция fork(), а затем дублированное адресное пространство уничтожается посредством вызова exec*().

Одним из главных преимуществ метода совмещенного применения функций fork() и exec*() при создании дочерних процессов является гибкость изменения окружения, которое наследуется дочерним процессом. Это производится в ответвленном дочернем процессе непосредственно перед выполнением функции exec*(). Например, при выполнении следующей простой команды командного интерпретатора стандартный поток вывода будет закрыт и повторно открыт до вызова функции exec*():

ls >file


То же самое может быть выполнено с помощью функции posix_spawn(); она предоставляет возможность управлять следующими классами наследуемого окружения, которые часто корректируются при создании нового дочернего процесса: Существует также вспомогательная функция posix_spawnp(), которая не требует указания полного путевого имени запускаемой программы, а вместо этого ищет программу, используя переменную PATH вызывающего процесса.

Для создания новых дочерних процессов использование функций posix_spawn() является предпочтительным.
spawn()
Функция QNX Neutrino spawn() подобна функции posix_spawn(). Функция spawn() управлять следующими параметрами и атрибутами дочернего процесса: Основными формами функции spawn() являются: Кроме того, существует следующий набор функций, которые реализуются на основе функций spawn()и spawnp(): Когда процесс порождается с помощью этой функции, он наследует следующие атрибуты от родительского процесса: Дочерний процесс имеет несколько отличий от родительского процесса: Если дочерний процесс порожден на удаленном узле, ему не присваивается идентификатор группы процессов и идентификатор сессии (session membership); он помещается в новую сессию и группу процессов.

Дочерний процесс может обращаться к окружению родительского процесса посредством глобальной переменной environ, описанной в заголовочном файле <unistd.h>).

Для получения дополнительной информации см. описание функции spawn() в электронном документе «Library Reference».
fork()
Функция fork() создает новый дочерний процесс посредством совместного использования с вызывающим процессом одного и того же программного кода, а также посредством копирования (дублирования) данных вызывающего процесса для дочернего процесса. Большинство ресурсов процесса наследуется. Тем не менее, некоторые ресурсы не могут быть наследованы, а именно:

Функция fork() обычно используется для двух целей:

При создании нового потока общие данные помещаются в специально создаваемую область разделяемой памяти. До появления стандарта POSIX для потоков это был единственный способ. С появлением стандарта POSIX-потоков функция fork() реализуется созданием потоков внутри одного процесса с помощью функции pthread_create().

При создании процесса для выполнения другой программы за вызовом функции fork() сразу же следует вызов одной из функций exec*(). Это тоже более эффективно выполняется с помощью одного вызова POSIX-функции spawn(), которая совмещает в себе обе эти операции.

Поскольку ОС QNX Neutrino имеет более эффективные средства, чем функция fork(), использование этой функции больше всего подходит в тех случаях, когда необходимо обеспечить переносимость существующего кода, а также для написания переносимого кода, предназначенного для UNIX-систем, не поддерживающих POSIX-совместимых функций pthread_create() и spawn().

Замечание

Функция fork() может вызываться из процесса, который содержит только один поток.
vfork()
Функция vfork() (которая может вызываться только из процесса, содержащего всего один поток) полезна для создания нового контекста с целью вызова одной из функций exec*(). Функция vfork() отличается от функции fork() тем, что для дочернего процесса не создается копия данных вызывающего процесса. Вместо этого дочерний процесс использует память родительского процесса и его основной поток (thread of control) до тех пор, пока не выполнится вызов одной из функций exec*(). Вызывающий процесс приостанавливается на то время, пока дочерний процесс использует его ресурсы.

Дочерний процесс, созданный функцией vfork(), не может вернуться из процедуры, вызвавшей функцию vfork(), поскольку в этом случае возможный выход из родительской функции vfork() может произойти в несуществующую область стека.
exec*()
Семейство функций exec*() замещает текущий процесс новым процессом, загруженным из файла с исполняемым модулем. Поскольку вызывающий процесс замещается, успешный возврат (т. е. с нулевым кодом завершения) не происходит.

Существуют следующие функции типа exec*(): Функции exec*(), как правило, следуют за функцией fork() или vfork(), с тем чтобы загрузить новый дочерний процесс. Однако более эффективным является новый POSIX-совместимый вызов spawn().
Загрузка процессов
Процессы, загружаемые из файловой системы с помощью вызовов exec*() или spawn(), имеют формат ELF (Executable and Linkable Format, формат исполняемых и компонуемых модулей). Если файловая система реализована на блок-ориентированном устройстве, программный код и данные загружаются в основную память.

Если файловая система отображена в памяти (например, в ПЗУ или флеш-памяти), загрузка программного кода в оперативную память не требуется, и этот код может быть исполнен на месте. При таком подходе вся оперативная память может использоваться для данных и стека, а программный код остается в ПЗУ или флеш-памяти. В любом случае, если некоторый процесс загружается несколько раз, его программный код используется совместно всеми его сущностями.
Управление памятью
Некоторые ядра или исполняемые модули реального времени обеспечивают поддержку защиты памяти в среде разработки. Однако редко когда они имеют поддержку защиты памяти в среде исполнения, что, как правило, объясняется слишком большими накладными расходами памяти и производительности. Тем не менее, защита памяти все больше применяется во встраиваемых процессорах, так как ее преимущества значительно перевешивают те небольшие потери, которые связаны с ее обеспечением.

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

При наличии защиты памяти, если один из процессов в многозадачной среде пытается получить доступ к области памяти, для которой данный тип доступа не был назначен явным образом, MMU-оборудование может известить об этом ОС, которая в этом случае может снять данный процесс.

Таким образом обеспечивается "защита" адресных пространств процессов друг от друга, что позволяет предотвратить сбои памяти, используемой одним процессом, из-за ошибок в коде, исполняемом в другом процессе или даже ОС. Такая защита полезна как в процессе разработки системы реального времени, так и в процессе работы данной системы, т. к. позволяет выполнять послеаварийный анализ.

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

С помощью блока управления памятью (MMU) операционная система может завершить процесс сразу после возникновения ошибки доступа к памяти. Таким образом, программист может быстро узнать о возникшей ошибке вместо того, чтобы через какое-то время получить загадочный отказ системы. ОС может сообщить о месте ошибочной инструкции в процессе или запустить символьный отладчик прямо на этой инструкции.
Блоки управления памятью
Говоря в общем, блок управления памятью занимается тем, что делит физическую память на страницы размером 4 Кбайта. Процессор использует набор из множества таблиц страниц, хранящихся в системной памяти. Эти таблицы служат для описания того, как виртуальные адреса (т. е. адреса памяти, используемые приложением) отображаются в адреса, выделяемые процессором для доступа к физической памяти.

Во время выполнения потока в таблицы страниц, управляемые операционной системой, заносятся данные о том, как адреса памяти, используемые потоком, отображаются в физической памяти системы (рис. 6.1).

Рис. 6.1. Отображение виртуальных адресов (на семействе процессоров x86).

Для большого адресного пространства со множеством процессов и потоков количество записей в таблицах страниц, необходимое для описания отображений, может быть весьма большим — больше чем может хранить процессор. Для сохранения высокой производительности процессор выполняет кеширование часто используемых сегментов внешних таблиц страниц в буфере быстрого преобразования адреса (Translation Look-aside Buffer, TLB).

Однако обработка "пропавших" записей в TLB-кеше — одна из причин повышения накладных расходов, связанных с работой блоков управления памятью. Для снижения этих накладных расходов в ОС QNX Neutrino предусмотрено несколько различных механизмов на уровне таблиц страниц.

С таблицами страниц связаны биты, которые определяют атрибуты каждой страницы памяти. Страницы могут иметь различные типы доступа: "только чтение", "чтение/запись" и т. д. Как правило, память выполняемого процесса определяется страницами с доступом "только чтение" для кода и "чтение/запись" для данных и стека.

Когда ОС QNX Neutrino применяет операцию переключения контекстов (т. е. приостанавливает выполнение одного потока и возобновляет выполнение другого потока), она указывает блоку управления памятью применить для возобновленного потока потенциально иной набор таблиц страниц. Если ОС переключает контексты между потоками внутри одного процесса, задействование блока управления памятью не требуется.

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

Проверяя значение указателя команд, записываемое в стек при возникновении прерывания, ОС может определить адрес команды, вызвавшей ошибку обращения к памяти, и выполнить необходимые в этом случае операции.
Защита памяти в режиме исполнения
Защита памяти полезна не только в процессе разработки — она также обеспечивает повышенную степень отказоустойчивости во время работы встраиваемых систем. Во многих встраиваемых системах используются программируемые сторожевые таймеры (watchdog timer), предназначенные для того, чтобы следить за системой и ее программным обеспечением и определять, не "зависла" ли она. Однако такой метод менее точен, чем сигнальные устройства на основе MMU.

Аппаратные сторожевые таймеры обычно реализуются в виде сбрасываемого таймера с одним устойчивым состоянием (retriggerable monostable timer). Этот таймер присоединяется к шине сброса процессора. Если системное программное обеспечение перестает передавать сторожевому таймеру стробирующий сигнал (strobe), то через заданное временное значение таймер истекает и вызывает процедуру перезагрузки процессора. Как правило, в системе предусмотрен специальный программный компонент, который проверяет состояние системы и передает сторожевому таймеру стробирующий сигнал, сообщающий о нормальной работе системы.

Этот метод позволяет восстановить работу системы после ее зависания из-за программного или аппаратного сбоя, однако он приводит к полной перезагрузке системы и, возможно, значительному времени простоя.
Программные сторожевые таймеры
Если в системе с защитой памяти во время работы программного обеспечения происходит случайная ошибка, ОС может перехватить это событие и передать его заданному пользователем потоку вместо создания дампа оперативной памяти. Этот поток может определить оптимальный способ восстановления системы после ошибки, не производя полную перезагрузку системы (как это сделал бы аппаратный сторожевой таймер). Программные сторожевые таймеры имеют следующие функции: Важной особенностью здесь является то, что сохраняется интеллектуальное программируемое управление встраиваемой системы, даже если возник сбой в нескольких процессах и потоках в управляющем программном обеспечении. Аппаратный сторожевой таймер все еще остается полезным для восстановления после зависаний по аппаратным причинам (latch-ups). Однако для сбоев в программном обеспечении существует более эффективное средство.

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

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

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

Дампы оперативной памяти дают программистам информацию, необходимую для решения проблемы.
Контроль качества
За счет разделения встраиваемого программного обеспечения на группу взаимодействующих процессов, имеющих защищенную (изолированную) память и состоящих из одного или нескольких потоков, можно легко рассматривать эти процессы как "компоненты", которые могут повторно использоваться в разных проектах. Посредством явно определенных интерфейсов, имеющих аппаратную поддержку, эти процессы могут быть интегрированы в приложения без риска нарушения общей надежности системы. Кроме того, поскольку точный двоичный образ процесса (не только его исходный код) может быть повторно использован, получается больше возможностей для контроля тех изменений и нестыковок, которые могли появиться после рекомпиляции исходного кода, перекомпоновки программы, применения новых версий инструментов разработки, заголовочных файлов, библиотечных подпрограмм и т. д.

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

Поскольку после выхода продукта, представляющего собой весьма сложную встраиваемую систему, нет возможности что-либо кардинально в нем изменить, то следует изначально разрабатывать системы с высокой отказоустойчивостью и автоматическим восстановлением после сбоев. Для этого необходимо использовать защиту памяти с помощью блоков MMU, интегрированных во встраиваемые системы.
Модель полной защиты памяти
В соответствии с моделью полной защиты памяти (full-protection model), которая используется в ОС QNX Neutrino, весь программный код, находящийся в образе, перемещается в новое виртуальное пространство, при этом включается оборудование, реализующее блок управления памяти, и устанавливаются начальные значения отображения таблиц страниц. Это позволяет запустить модуль procnto корректным образом и в среде со включенным блоком управления памятью. Управление данной средой переключается на администратор процессов, который изменяет таблицы отображения в соответствии с запускаемыми процессами.
Изолированное виртуальное адресное пространство
В модели полной защиты памяти для каждого процесса определяется изолированное виртуальное адресное пространство (private virtual memory), которое может охватывать от 2 до 3,5 Гбайт (в зависимости от типа процессора). Это осуществляется с помощью блока управления памятью. Общие накладные расходы, связанные с переключением процессов и передачей сообщений, возрастают из-за повышения сложности в механизме адресации между двумя абсолютно изолированными адресными пространствами.

Замечание

На процессорах семейства x86, SH-4, ARM и MIPS изолированное адресное пространство начинается с 0, тогда как на процессорах семейства PowerPC адресное пространство от 0 до 1 Гбайт резервируется для системных процессов.



Рис. 6.2. Виртуальная память с полной защитой (на процессорах семейства x86).

Расходы памяти на таблицы страниц могут увеличиваться на значение от 4 до 8 Кбайт в расчете на каждый процесс. Следует обратить внимание на то, что в такой модели управления памятью есть поддержка POSIX-вызова fork().
Изменяемый размер страницы
Менеджер виртуальной памяти может использовать изменяемые размеры страниц, если это поддерживается процессором и если от этого есть некоторая польза. Использование изменяемого размера страниц может повысить производительность по следующим причинам:

Можно сделать размер страницы больше, чем 4 Kбайт. В результате, система будет использовать меньше записей TLB.

Меньше промахов TLB.

Если необходимо отключить поддержку изменяемого размера страниц, необходимо в файле построения задать модулю procnto опцию -m˜v. Опция -mv включает такую поддержку.
Блокирование памяти
QNX Neutrino поддерживает блокирование памяти в соответствии с POSIX, поэтому процесс может избежать потери времени на загрузку страницы памяти путем блокирования памяти так, что страница становится резидентной в памяти (т.е. остается в физической памяти).

Существуют следующие уровни блокирования:

Unlocked


Неблокированная (Unlocked) память может загружаться и выгружаться. Память выделяется при отображении, но элементы таблицы страниц не создаются. При первой попытке доступа быть к этой памяти возникнет ошибка и поток заблокируется в состоянии WAITPAGE до тех пор, пока менеджер памяти инициализирует память и создает соответствующие записи таблицы страниц.

В случае неудачи при инициализации страницы будет сгенерирован сигнал SIGBUS.

Locked

Блокированная (Locked) память не может быть загружаемой и выгружаемой. С целью сопровождения статистики использования и модификации будут по прежнему возникать ошибки при доступе к данной памяти. Страницы, которые были заданы как PROT_WRITE в действительности будут PROT_READ.

Для блокирования и разблокирования части памяти потока используются функции mlock() и munlock(); для блокирования и разблокирования всей памяти потока используются функции mlockall() и munlockall(). Память остается блокированной до тех пор пока процесс не разблокирует ее, завершится или вызовет функцию exec*(). Если процесс вызывает какую-либо из функций fork(), posix_spawn*() или spawn*(), то блокировки памяти в дочернем процессе снимаются.

Один и тот же (или накладывающийся) регион может быть заблокирован более, чем одним процессом; память останется блокированной пока все процессы не разблокируют ее.

Блокировки памяти не суммируются — если поток не единожды блокирует один и тот же регион, то однократное разблокирование снимет все блокировки этого процесса на данном регионе.

Для блокирования всей памяти для всех приложений необходимо задать модулю procnto опцию -ml. При этом все страницы, по меньшей мере, инициализируются.

Superlocked

(Расширение QNX Neutrino) Ошибки доступа к памяти вообще не разрешаются; как только память отображена, она вся должна быть инициализирована и приватизирована, а права доступа должны быть установлены. Суперблокирование (Superlocking) покрывает все адресное пространство потока.

Для суперблокирования памяти необходимо перевести поток в привилегированный режим ввода-вывода с помощью функции ThreadCtl(), установив флаг _NTO_TCTL_IO:

ThreadCtl( _NTO_TCTL_IO, 0 );

Для суперблокирования всей памяти для всех приложений необходимо задать модулю procnto опцию -mL.

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

Дефрагментация физической памяти

Большинство пользователей компьютеров хорошо знакомы с принципом дефрагментации диска, во время которой свободное пространство на диске разбивается на небольшие блоки, беспорядочно-распределенные между используемыми блоками. При этом, самое сложное действие – выделить свободные части физической памяти; по истечении некоторого времени начинается фрагментация физической памяти. В результате, может получиться так, что значительное количество памяти будет свободно, но она будет фрагментирована настолько, что запрос большого объёма непрерывной памяти завершится с ошибкой.

Непрерывная память обычно запрашивается драйверами устройств, если устройство использует DMA. Обычно временное решение – заранее убедиться, что все драйвера устройств инициализированы (до фрагментации памяти), что они хранятся в своей памяти. Это строгое ограничение, особенно для встраиваемых систем, которые могут использовать различные драйверы, в зависимости от действий пользователя; запуск одновременно всех доступных драйверов устройств не возможен.

Алгоритм, используемый QNX Neutrino для выделения физической памяти способствуют значительному уменьшению количества фрагментации. Однако, неважно насколько «разумны» данные алгоритмы, установленный режим работы приложения обеспечит фрагментацию свободной памяти. Рассмотрим полностью дегенеративное приложение, которое обычно резервирует 8 Кб памяти, а затем освобождает половину. Если такое приложение запущено достаточно долго, оно достигает такого состояния, что половина памяти системы свободна, но нет свободных блоков размером больше 4 Кб.

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

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

Для системы памяти QNX Neutrino важны оба вида фрагментации по различным причинам: При дефрагментации свободной памяти, администратор памяти меняет местами свободную и используемую память, таким образом, блоки свободной памяти объединяются в большие блоки, достаточные для удовлетворения запроса непрерывной памяти.

Когда приложение выделяет память, она предоставляется операционной системой квантами, 4Кб блоками памяти, которые выровнены на границу 4 кб (4096 байт). Программа операционной системы MMU позволяет приложению ссылаться на блок физической памяти посредством виртуальных адресов; при этом MMU преобразует виртуальный адрес в физический.

Например, при запросе 16Кб памяти, достаточно выделения четырех 4 Кб квантов. Операционная система игнорирует четыре физических блока приложения и конфигурирует MMU для обеспечения возможности приложения ссылаться на них посредством 16Кб непрерывных виртуальных адресов. Однако, данные блоки могут быть не связаны физически; операционная система может упорядочить конфигурацию MMU (виртуальное физическое распределение), таким образом, не-непрерывные физические адреса доступны через непрерывные виртуальные адреса.

Задача дефрагментации состоит в изменении существующей памяти приложений и распределении используемых различных основных физических страниц. Меняя местами основные физические кванты, операционная система может объединять фрагментированные свободные блоки в непрерывные интервалы. Однако, следует избегать перемещения различных видов памяти, когда преобразование из виртуальной в физическую память не может быть безопасно заменено: Другие случаи, когда память не может быть перемещена см. в подразделе «Автоматическая маркировка памяти, как неперемещаемой», ниже.

Дефрагментация считается законченной, когда приложение распределило части непрерывной памяти. Приложение выполняет данное действие посредством вызова mmap(), с установленными флагами MAP_PHYS | MAP_ANON. В зависимости от того разрешена дефрагментация или запрещена, может быть невозможно установить распределение MAP_PHYS непрерывной памяти: Примечание. Во время дефрагментации поток вызова mmap() блокируется.

Сжатие может занять значительное время (особенно в системах с большим объемом памяти), но это не повлияет на большинство других действий операционной системы.

Если в системе запущено одновременно несколько задач, алгоритм дефрагментации просчитывает, как может измениться распределение памяти, при запуске алгоритма.

По умолчанию, дефрагментация разрешена. Запретить дефрагментацию можно используя опции командной строки procnto -m˜d, и разрешить с помощью опции -md.

Автоматическая маркировка памяти, как неперемещаемой

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

Дополнительно, память, при выполнении mem_offset() – выводе физического адреса из виртуального – должна быть защищена от перемещения алгоритмом сжатия. Однако, операционная система не маркирует всю память, как неперемещаемую, т. к. программа может вызвать mem_offset() «из любопытства» (как, например, программа протоколирования памяти IDE). Во всех других случаях память для перемещений не заблокирована.

С другой стороны, если приложение зависит от результата вызова mem_offset(), и операционная система позже переместила распределение памяти, это может прервать работу приложения. Такое приложение должно заблокировать используемую память (с помощью вызова mlock()), но но поскольку QNX Neutrino не всегда перемещает память в прошлом, то ОС не может считать, что все приложения работают правильно.

Поэтому, procnto поддерживает опции командной строки -ma. При установке данной опции, любые вызовы функции mem_offset() маркируют блоки памяти, как неперемещаемые. Следует заметить, при распределении памяти, как непрерывной или блокировании посредством mlock(), она становиться неперемещаемой, т.о. данная опция не актуальна. Опция полезна только, если доступны параметры дефрагментации.

Данная опция отключена по умолчанию. Однако, если приложение работает недостаточно хорошо, можно разрешить автоматическую маркировку, для корректной работы приложения.

Управление пространством имен

Области ответственности

Ресурсы ввода/вывода не встроены в микроядро, а реализуются специальными процессами — администраторами ресурсов, которые могут динамически запускаться во время работы системы. Администратор procnto позволяет (посредством стандартного программного интерфейса) назначать администраторам ресурсов свои области в пространстве имен путей как некие "области ответственности" (domains of authority), при этом модуль procnto управляет деревом имен путей и следит за процессами, владеющими теми или иными частями пространства имен путей. Назначенное путевое имя иногда называют "префиксом", так как оно предшествует всем нижележащим именам путей. Назначенное путевое имя еще называют точкой монтирования (mountpoint), так как именно в этой точке сервер подсоединяется к путевому имени.

Такой подход к управлению пространством имен путей позволяет сохранить в ОС QNX Neutrino семантику POSIX для реализации доступа к устройствам и файлам и, вместе с тем, при необходимости не использовать соответствующие POSIX-службы в небольших встраиваемых системах.

При начальном запуске модуль procnto помещает в пространстве имен путей следующие префиксы — табл. 6.1.

Таблица 6.1. Префиксы пространства имен

Префикс

Описание

/

Корневой каталог файловой системы

/proc/boot

Некоторые файлы из загрузочного образа, представленного в виде плоской файловой системы3

/proc/

Активные процессы, каждый представлен своим идентификатором (Process ID, PID)

/dev/zero

Устройство, которое всегда возвращает ноль. Используется для выделения страниц, заполненных нулями, с помощью функции mmap()

/dev/mem

Устройство, которое отображает всю физическую память

Разрешение имен путей
Когда какой-либо процесс открывает файл, сначала POSIX-совместимая библиотечная программа open() отправляет путевое имя модулю procnto, который сравнивает его с деревом префиксов для определения того, каким именно администраторам ресурсов должно быть отправлено сообщение open().

Дерево префиксов может содержать идентичные или частично совпадающие области управления, так как один и тот же префикс может быть зарегистрирован множеством серверов. Если эти области идентичны, порядок преобразования может быть уточнен. Если области частично совпадают, используется путевое имя с наибольшей совпадающей частью. Например, зарегистрированы следующие префиксы: Администратор файловой системы регистрирует префикс для смонтированной (mounted) файловой системы QNX4 (например, /). Драйвер блочного устройства регистрирует префикс для специального блочного файла, который отображает весь жесткий диск (т. е. /dev/hd0). Администратор последовательного порта регистрирует два префикса для двух последовательных портов.

Табл. 6.2 демонстрирует, как работает правило "наибольшего совпадения" для разрешения имен путей.

Таблица 6.2. Правило "наибольшего совпадения"

Имя пути

Соответствует

Разрешается в

/dev/ser1

/dev/ser1

devc-ser*

/dev/ser2

/dev/ser2

devc-ser*

/dev/ser

/

fs-qnx4.so

/dev/hd0

/dev/hd0

devb.eide.so

/usr/jhsmith/test

/

fs-qnx4.so

Распределение точек монтирования
В большинстве случаев порядок разрешения имен файлов соответствует порядку, в котором была подмонтирована файловая система в данной точке монтирования (т. е. новые монтированные файлы будут перед или после уже существующих). При монтировании файловой системы может быть установлен порядок разрешения путевых имен.

Например, можно использовать: Также можно использовать опцию -o для монтирования с ключевыми словами:

before

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

after

Монтирование файловой системы с разрешением путевых имен после любых других подмонтированных файловых систем на данной точке монтирования (при использовании других слов – после уже существующего монтирования). При обращении к файлу, система осуществляет поиск в данной файловой системе в последнюю очередь, и только если файл не был обнаружен ранее в других файловых системах.

Если была установлена опция before, файловая система перемещается перед любыми другими монтированными файловыми системы на данной точке монтирования, за исключение файловых систем, которые будут позже монтированы с данной опцией. При установлении опции after, файловая система перемещается за любую подмонтированную ранее файловую систему на данной точке монтирования, за исключением тех файловых систем, которые были подмонтированы ранее с этой опцией. Т.о. порядок поиска для таких файловых систем: для каждого поискового списка, в порядке запроса монтирования. Имя получает первый сервер, запросивший его. Если используется опция after, то файловая система ставится в самый конец очереди на разрешение путевого имени. И запросы к ней будут приходить, если все другие файловые системы в той же точке монтирования (если они есть) не знают ничего о запрашиваемом файле, и before, чтобы файловая система в первую очередь искала имена файлов.

Точка монтирования одного устройства

Пример с тремя серверами:

    1. Сервер А. Файловая система QNX4. Точкой монтирования является /. Содержит файлы bin/true и bin/false.

    2. Сервер B. Файловая система во флеш-памяти. Точка монтирования: /bin. Содержит файлы ls и echo.

    3. Сервер C. Устройство типа "файл", которое генерирует случайные числа. Точка монтирования: /dev/random.

В результате внутренняя таблица точек монтирования, поддерживаемая администратором процессов, выглядит следующим образом — табл. 6.3.

Таблица 6.3. Внутренняя таблица точек монтирования для примера

Точка монтирования

Сервер

/

Сервер А (файловая система QNX4)

/bin

Сервер B (файловая система во флеш-памяти)

/dev/random

Сервер С (устройство)


Естественно, имя каждого сервера в действительности обозначает nd, pid, chid соответствующего серверного канала.

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

int fd;

fd = open("/dev/random", ...);

read(fd, ...);

close(fd);

В этом случае, библиотека Си делает запрос администратору процессов о том, какой сервер потенциально может обработать путь /dev/random. В ответ на запрос администратор процессов возвращает список серверов: Исходя из этой информации, библиотека затем обращается к каждому серверу по очереди и отправляет ему открытое сообщение, содержащее ту часть пути, которую сервер должен подтвердить, в результате чего:

    1. Сервер С получает пустой путь (null path), так как в запросе указан тот же путь, который является точкой монтирования;

    2. Сервер А получает путь dev/random, так как его точкой монтирования было /.

Как только какой-либо сервер подтверждает обработку запроса, к остальным серверам библиотека больше не обращается. Это значит, что запрос к Серверу А будет направлен только в случае, если Сервер С не подтвердит обработку запроса.

С устройством файлового типа этот процесс оказывается довольно простым: как правило, запрос обрабатывается первым доступным сервером. Особый же интерес представляет случай с точками монтирования на основе объединенной файловой системы (unioned filesystem mountpoints).

Точки монтирования на основе объединенной файловой системы
Например, есть два сервера, аналогичных тем, которые были в предыдущем примере. Каждый сервер имеет каталог /bin, с разным наполнением.

После монтирования обоих серверов объединение точек монтирования даст следующую картину: Таким образом, разрешение пути /bin происходит так же, как и в предыдущем случае, но процесс не ограничивается одним идентификатором соединения, так как запрос на обработку пути направляется всем серверам:

DIR *dirp;

dirp = opendir("/bin", ...);

closedir(dirp);


Результатом исполнения этого кода является следующее:

    1. Сервер B получает пустой путь, так как в запросе указан тот же путь, который является точкой монтирования.

    2. Сервер A получает путь "bin", так как его точкой монтирования было "/".

Таким образом, получается набор файловых дескрипторов для серверов, которые обрабатывают путь /bin (в данном случае это два сервера). Действительные имена каталогов считываются по очереди при вызове функции readdir(). Если к какому-либо имени в каталоге производится обращение с помощью функции open(), тогда выполняется обычная процедура разрешения имен путей и запрос направляется только одному серверу.
Польза совмещения точек монтирования
Механизм совмещения точек монтирования очень удобен с точки зрения динамического обновления версий программного обеспечения, обслуживания "на лету" и т. д. Кроме того, этот механизм увеличивает степень интеграции системы, так как имена путей позволяют устанавливать соединения независимо от того, какие службы они предоставляют, что, естественно, дает более унифицированный программный интерфейс.
Символьные префиксы
Ранее были рассмотрены префиксы, которые отображаются в администраторе ресурсов. Еще одной формой префикса является символьный префикс (symbolic prefix), который представляет собой простую символьную подстановку подходящего префикса.

Символьные префиксы создаются POSIX-командой ln (от англ. слова link — связать). Эта команда в сочетании с ключом -s обычно используется для создания жестких или символьных связей в файловой системе. Если в дополнение задать ключ -P, то символьная связь будет создана в префиксном пространстве модуля procnto, которое расположено в оперативной памяти (табл. 6.4).

Таблица 6.4. Команды создания символьной связи

Команда

Описание

ln -s существующий_файл символьная_связь

Создать символьную связь в файловой системе

ln -Ps существующий_файл символьная_связь

Создать символьную связь в дереве префиксов


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

Например, имеется машина, в которой нет локальной файловой системы. Однако на другом узле (назовем его neutron) существует файловая система, к которой необходимо получить доступ по имени пути "/bin". Это достигается с помощью следующего символьного префикса:

ln -Ps /net/neutron/bin /bin

В результате имя пути /bin будет отображено в /net/neutron/bin. Например, имя пути /bin/ls будет преобразовано в следующее:

/net/neutron/bin/ls

Это новое имя пути снова будет сопоставлено с деревом префиксов, но теперь соответствующим префиксом будет /net, указывающий на администратор ресурсов npm-qnet, который выполнит разрешение компонента neutron и переадресует последующие запросы на разрешение на узел neutron. На этом узле остальная часть путевого имени (т. е. /bin/ls) будет разрешена в соответствии с содержимым префиксного пространства на том узле, в результате чего произойдет переход к администратору файловой системы узла neutron, на который будет направлен запрос open(). Таким образом, с помощью всего лишь нескольких символов данный символьный префикс обеспечивает доступ к удаленной файловой системе, как если бы она была локальной.

Видно, что для осуществления такой переадресации нет необходимости иметь локальный процесс, реализующий файловую систему. На рабочей станции, не оснащенной дисками, дерево префиксов может выглядеть, например, следующим образом — рис. 6.3.


Рис. 6.3. Дерево префиксов рабочей станции без дисков

В соответствии с данным деревом префиксов, такие локальные устройства, как, например, /dev/ser1 и /dev/concole, будут перенаправлены (routed) на локальный администратор устройств символьного ввода/вывода, а запросы к другим путевым именам будут перенаправлены на удаленную файловую систему.
Создание специальных имен устройств
Символьные префиксы также можно использовать для создания специальных имен устройств. Например, если модем имеет путевое имя /dev/ser1, то для него можно создать символьный префикс /dev/modem следующим образом:

ln -Ps /dev/ser1 /dev/modem


В результате при любом запросе на открытие /dev/modem произойдет замена на /dev/ser1. Такой способ отображения позволяет легко переключать модем на другой последовательный порт посредством простой замены символьного префикса, и это никак не изменит работу приложений.
Относительные имена путей
Имена путей необязательно должны начинаться с косой черты. В тех случаях, когда в начале пути косая черта отсутствует, путь считается относительным по текущему рабочему каталогу. В ОС QNX Neutrino текущий рабочий каталог сохраняется в виде символьной последовательности. Относительные путевые имена всегда преобразуются в полные сетевые имена путей посредством постановки перед ними символьного обозначения текущего рабочего каталога.

Следует отметить, что в зависимости от того, что предшествует текущему рабочему каталогу — косая черта или сетевой корневой каталог — результат может быть различным.
Команда cd
В некоторых традиционных UNIX-системах, команда cd модифицирует заданное имя пути, если это имя пути содержит символьные ссылки. Таким образом, путевое имя нового рабочего каталога (которое можно отобразить с помощью команды pwd) может отличаться от того, которое было передано команде cd.

Однако в ОС QNX Neutrino команда cd никак не модифицирует имя пути, за исключением случаев сокращения при использовании символа в виде двух точек (..). Например, при выполнении следующей команды:

cd /usr/home/dan/test/../doc

текущим рабочим каталогом станет /usr/home/dan/doc, даже если какие-либо элементы в первоначальном имени пути были символьными ссылками.

Более подробную информацию о символьных ссылках и использовании символа .. можно найти в "Руководстве пользователя" (User's Guide), в разделе "Файловая система QNX4" главы, посвященной работе с файловыми системами.
Пространство имен файловых дескрипторов
После открытия какого-либо ресурса ввода/вывода в действие вступает другое пространство имен. Функция open() возвращает целое число, которое представляет собой файловый дескриптор (File Descriptor, FD), служащий для того, чтобы направить все последующие запросы на ввод/вывод на этот администратор ресурсов.

В отличие от пространства имен путей, пространство имен файловых дескрипторов (file descriptor namespace) является полностью локальным для каждого процесса. Администратор ресурсов использует сочетание SCOID (Server COnnection ID, идентификатор серверного соединения) и FD (File Descriptor/connection ID, идентификатор файлового дескриптора/соединения) для определения управляющей структуры, связанной с предыдущим вызовом open(). Эта структура, называемая блоком управления открытым контекстом (Open Control Block, OCB), содержится внутри администратора ресурсов.

На рис. 6.4 изображено, как администратор ввода/вывода отображает пары SCOID и FD в OCB-блоки.


Рис. 6.4. Преобразование пар SCOID и FD в OCB-блоки

Блоки управления открытым контекстом
Блок управления открытым контекстом (OCB) содержит текущую информацию об открытом ресурсе. Например, файловая система хранит текущую позицию в файле. Каждый вызов open() создает новый OCB. Поэтому, если процесс открывает данный файл дважды, любые вызовы lseek() с использованием одного файлового дескриптора не повлияют на точку поиска другого файлового дескриптора. Это же происходит и с другими процессами, открывающими тот же самый файл.

На рис. 6.5 показаны два процесса, один из которых открывает один и тот же файл дважды, а другой — открывает его один раз. Совместно используемые файловые дескрипторы не применяются.

Рис. 6.5. Два процесса открывают один и тот же файл

Замечание

Файловые дескрипторы являются ресурсом процессов, а не потоков.

Несколько файловых дескрипторов в одном или более процессов могут относиться к одному и тому же OCB. Это достигается двумя способами: Если несколько файловых дескрипторов относятся к одному OCB, то любое изменение состояния этого блока немедленно отображается во всех процессах, в которых имеются файловые дескрипторы, связанные с этим OCB.

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

На рис. 6.6 показаны два процесса, один из которых открывает файл дважды, а затем вызывает функцию dup() для того, чтобы получить третий файловый дескриптор. После этого процесс создает дочерний процесс, который наследует все открытые файлы.

Рис. 6.6. Процесс, который открывает один и тот же файл дважды

Возможно запретить наследование файлового дескриптора при вызове функций spawn() и exec*(). Для этого следует вызвать функцию fcntl() и установить флаг FD_CLOEXEC.

3
Плоская файловая система (flat filesystem) — система организации файлов, при которой они не могут иметь одинаковых имен, если даже находятся в разных каталогах. — Прим. перев.