Давайте сделаем сводку правил диспетчеризации (для одиночного процессора) и отсортируем их в порядке важности:
• только один поток может выполняться в данный момент времени;
• всегда должен выполняться поток с наивысшим авторитетом;
• поток должен работать до тех пор, пока он не блокируется иди не завершается;
• поток, диспетчеризуемый по дисциплине карусельного типа (RR), должен работать в течение выделенного ему кванта времени, после чего ядро обязано его перепланировать (при необходимости).
Для систем с несколькими процессорами, приведенные выше правила остаются такими же, за исключением того, что несколько процессоров могут одновременно выполнять несколько потоков. Порядок, в котором потоки выполняются (то есть последовательность, в которой потоки ставятся на выполнение в многопроцессорной системе), определяется точно так же, как для одиночного процессора — в любой момент времени будет выполняться готовый к выполнению поток с наивысшим приоритетом. Если существует другой готовый к выполнению поток с более высоким приоритетом, и имеется доступный процессор, то этот поток будет выполняться на следующем процессоре, и так далее. Если имеющегося числа потоков недостаточно для того, чтобы загрузить все процессоры по такому принципу, то нет проблем — «неактивные» процессоры будут выполнять «холостой» поток (его приоритет равен нулю, то есть ниже, чем приоритет любого пользовательского потока) Если для того, чтобы обработать всю очередь, недостаточно процессоров, тогда только N потоков с наивысшим приоритетом будут выполняться, где N — число доступных процессоров. Другие потоки будут готовы к выполнению, но в действительности выполняться не будут. Отметим, что вопросы диспетчеризации потоков в симметричной мультипроцессорной системе все еще исследуются, так что возможно, что этот порядок может измениться в будущем.
Схема алгоритма диспетчеризации.
Состояния потоков
Несколько раз небрежно упомянув о «выполнении», «готовности» и «блокировке», давайте теперь формализуем эти состояния потока.
Состояние выполнения (RUNNING) в QNX/Neutrino означает, что поток активно использует ресурсы процессора. В системе SMP будет осуществляться выполнение множества потоков, а в системе с единственным процессором будет осуществляться выполнение одного потока.
Состояние готовности (READY) означает, что этот поток может быть поставлен на выполнение немедленно, но не выполняется, потому что в данный момент времени активен другой поток (с таким же или более высоким приоритетом). Если бы два потока были готовы к выполнению, один из них с приоритетом 10, а другой — с приоритетом 7, то поток с приоритетом 10 был бы переведен в состояние выполнения (RUNNING), а поток с приоритетом 7 — в состояние готовности (READY).
Что называется блокированным состоянием? Проблема здесь состоит в том, что блокированных состояний существует несколько. Реально в QNX/Neutrino имеется более дюжины блокированных состояний.
Почему так много? Потому что ядро отслеживает причину, по которой поток заблокирован.
Мы уже ознакомились с двумя типами блокирующих состояний: когда поток заблокирован в ожидании мутекса, этот поток находится в состоянии блокировки по мутексу (MUTEX). Когда поток заблокирован, ожидая семафор, он находится в состоянии блокировки по семафору (SEM). Эти состояния просто указывают, в очереди на какой ресурс поток заблокирован.
Если по мутексу заблокировано несколько потоков, ядро не уделит им никакого внимания
Почему «при необходимости»? У потока, который только что освободил мутекс, вполне могут быть и другие дела, и он может иметь более высокий приоритет, чем все остальные ожидающие процессор потоки. В этом случае мы следуем второму правилу, которое гласит: «всегда должен выполняться поток с наивысшим приоритетом», что означает, что порядок диспетчеризации не изменяется — поток с наивысшим приоритетом продолжает работать.
Ниже представлен полный список блокированных состояний с краткими пояснениями. Этот список, кстати, есть в заголовочном файле <sys/QNX/Neutrino.h>, только там эти состояния снабжены префиксом «STATE_» (например, состояние READY данной таблицы там будет звучать как STATE_READY).
| Если состояние потока: | To это значит, что: |
|---|---|
| DEAD | Поток «мертв», ядро ожидает освобождения занятых им ресурсов. (В классических UNIX системах это состояние также называют «zombie» — «зомби» — |
| RUNNING | Поток выполняется. |
| READY | Поток не выполняется, но готов к работе (работает один или более потоков с более высокими или равными приоритетами). |
| STOPPED | Поток приостановлен (по сигналу SIGSTOP |
| SEND | Поток ожидает приема своего сообщения сервером. |
| RECEIVE | Поток ожидает сообщение от клиента. |
| REPLY | Поток ожидает от сервера ответ на свое сообщение. |
| STACK | Поток ожидает распределения дополнительного стекового пространства. |
| WAITPAGE | Поток ожидает устранения администратором процессов повреждения на странице. |
| SIGSUSPEND | Поток ожидает сигнал. |
| SIGWAITINFO | Поток ожидает сигнал. |
| NANOSLEEP | Поток «спит» (приостановлен на определенный период времени). |
| MUTEX | Поток ожидает захват мутекса. |
| CONDVAR | Поток ожидает соблюдения условия условной переменной. |
| JOIN | Поток ожидает завершения другого потока. |
| INTR | Поток ожидает прерывание. |
| SEM | Поток ожидает захват семафора. |
Важно помнить о том, что когда поток блокирован, независимо от
Мы рассмотрим блокированные состояния SEND (блокировка по передаче), RECEIVE (блокировка по приему) и REPLY (блокировка по ответу) в главе «Обмен сообщениями». Состояние NANOSLEEP связано с применением функций типа
Процессы и потоки
Вернемся к нашим рассуждениям о потоках и процессах, но на сей раз с точки зрения перспективы их применения в системах реального времени. Затем мы рассмотрим вызовы функций, которые применяются при работе с потоками и процессами.
Мы знаем, что процесс может содержать один или больше потоков. (Процесс с нулевым числом потоков не был бы способен что-либо
Что же делают все эти процессы и потоки? В конечном счете, они формируют систему — собрание потоков и процессов, реализующих определенную цель.
На самом высоком уровне абстракции система состоит из множества процессов. Каждый процесс ответственен за обеспечение служебных функций определенного характера, независимо от того, является ли он элементом файловой системы, драйвером дисплея, модулем сбора данных, модулем управления или чем-либо еще.
В пределах каждого процесса может быть множество потоков. Число потоков варьируется. Один разработчик ПО, используя только единственный поток, может реализовать те же самые функциональные возможности, что и другой, использующий пять потоков. Некоторые задачи сами по себе приводят к многопоточности и дают относительно простые решения, другие в силу своей природы, являются однопоточными, и свести их к многопоточной реализации достаточно трудно.
Проблемы разработки ПО с применением потоков могли легко стать темой отдельной книги. Здесь же мы изложим только основы этой проблемы.
Почему процессы?
Почему же не взять просто один процесс с множеством потоков? В то время как некоторые операционные системы вынуждают вас программировать только в таком варианте, возникает ряд преимуществ при разделении объектов на множество процессов.
К таким преимуществам относятся:
• возможность декомпозиции задачи и модульной организации решения;
• удобство сопровождения;
• надежность.
Концепция разделения задачи на части, т.е., на несколько независимых задач, является очень мощной. И именно такая концепция лежит в основе QNX/Neutrino. Операционная система QNX/Neutrino состоит из множества независимых модулей, каждый из которых наделен некоторой зоной ответственности. Эти модули независимы и реализованы в отдельных процессах. Разработчики из QSSL использовали эту удобную особенность для отдельной разработки модулей, независимых друг от друга. Единственная возможная установить зависимость этих модулей друг от друга — наладить между ними информационную связь с помощью небольшого количества строго определенных интерфейсов.
Это естественно ведет к упрощению сопровождения программных продуктов, благодаря незначительному числу взаимосвязей. Поскольку каждый модуль четко определен, и устранять неисправности в одном таком модуле будет гораздо проще — тем более, что он не связан с другими.
Тем не менее, наиболее важным моментом является надежность. Процесс, точно так же, как и жилой дом, имеет некоторые четкие «границы». Человек, живущий в доме, точно знает, когда он в доме, а когда — нет. Поток наделен в этом смысле пониманием, что если у него есть доступ к памяти в пределах процесса, он может функционировать. Если он переступит границы адресного пространства процесса, он будет уничтожен. Это означает, что два потока, работающие в различных процессах, изолированы один от другого.
Защита памяти.
Это означает, что если в данном процессе имеются есть несколько потоков, и ядру необходимо переключить контекст между ними, это можно сделать очень эффективно, поскольку не нужно изменять адресное пространство, достаточно просто сменить рабочий поток. Если, однако, мы должны переключиться на другой поток в другом процессе, тут уже включается в работу администратор процессов и переключает адресное пространство. Пусть вас не беспокоят возникающие при этом дополнительные издержки — под управлением QNX/Neutrino все это осуществляется очень быстро.
Запуск процесса
Теперь обратим внимание на функции, предназначенные для работы с потоками и процессами. Любой поток может осуществить запуск процесса; единственные налагаемые здесь ограничения вытекают из основных принципов защиты (правила доступа к файлу, ограничения на привилегии и т.д.). По всей вероятности, вам уже доводилось запускать процессы — либо из системного сценария, либо из командного интерпретатора, или из программы от своего имени.
Например, при запуске процесса из командного интерпретатора вы можете ввести командную строку:
$ program1
Это предписывает командному интерпретатору запустить программу program1 и ждать завершения ее работы. Или, вы могли набрать:
$ program2 &
Это предписывает командному интерпретатору запустить программу program2 без ожидания ее завершения. В таком случае говорят, что программа program2 работает в фоновом режиме.
Если вы пожелаете скорректировать приоритет программы до ее запуска, вы можете применить команду nice — точно так же, как в UNIX:
$ nice program3
Это предписывает командному интерпретатору запустить программу program3 с заниженным приоритетом.
Или нет?
Если посмотреть, что происходит в действительности, то мы велели командному интерпретатору выполнить программу, называемую nice, с обычным приоритетом. Команда nice затем занизила свой собственный приоритет (отсюда и имя программы «nice» — «благовоспитанная») и затем запустила программу program3 с этим заниженным приоритетом.
Нас обычно не заботит тот факт, что командный интерпретатор создает процессы — это просто подразумевается. В некоторых прикладных задачах можно положиться на сценарии командного интерпретатора (пакеты команд, записанные в файл), которые сделают эту работу за вас, но в ряде других случаев вы пожелаете создавать процессы самостоятельно.
Например, в большой мультипроцессорной системе вы можете пожелать, чтобы одна главная программа выполнила запуск всех других процессов вашего приложения на основании некоторого конфигурационного файла. Другим примером может служить необходимость запуска процессов по некоторому событию.
Рассмотрим некоторые из функций, которые QNX/Neutrino обеспечивает для запуска других процессов (или подмены одного процесса другим):
•
• семейство функций
• семейство функций
•
•
Какую из этих функций применять, зависит от двух требований: переносимости и функциональности. Как обычно, между этими двумя требованиями возможен компромисс.
Обычно при всех запросах на создание нового процесса происходит следующее. Поток в первоначальном процессе вызывает одну из вышеприведенных функций. В конечном итоге, функция заставит администратор процессов создать адресное пространство для нового процесса. Затем ядро выполнит запуск потока в новом процессе. Этот поток выполнит несколько инструкций и вызовет функцию
Функция
Фактически, для обработки команды, которую вы желаете выполнить, функция
Редактор, который я использую при написании данной книги, использует вызов
system("pwd");
Подходит ли функция
Давайте рассмотрим ряд других функций создания процессов.
Следующие функции создания процессов, которые следует рассмотреть, принадлежат к семействам
Семейство функций
С другой стороны, семейство функций
| Spawn | POSIX | Exec | POSIX |
|---|---|---|---|
| Да | |||
| Нет | Да | ||
| Нет | Да | ||
| Нет | Да | ||
| Нет | Нет | ||
| Да | |||
| Нет | Да | ||
| Нет | Да | ||
| Нет | Да | ||
| Нет | Нет |
Рассмотрим различные варианты функций exec() и spawn(). В таблице, представленной ниже, вы увидите, что некоторые функции из них предусмотрены POSIX, а некоторые — нет. Конечно, для максимальной переносимости, следует использовать только POSIX-совместимые функции.
При том, что названия функций могут показаться малопонятными, в их суффиксах есть логика.
| Суффикс: | Смысл: |
| Список аргументов определяется через список параметров, заданный непосредственно в самом вызове и завершаемый нулевым аргументом NULL. | |
| Указывается окружение. | |
| Если не указано полное имя пути программы, для ее поиска используется переменная окружения PATH. | |
| Список аргументов определяется через указатель на вектор (массив) аргументов. |
Список аргументов здесь — список аргументов командной строки, передаваемых программе.
Заметьте, что в библиотеке языка Си функции
Рассмотрим теперь различные варианты функций
Например, если я хочу вызвать команду ls с аргументами -t, -r, и -l (означает — «сортировать выходные данные по времени в обратном порядке и показывать выходные данные в длинном формате»), я мог бы определить это в программе так:
/* Вызвать ls и продолжить выполнение */
spawnl(P_WAIT, "/bin/ls", "/bin/ls", "-t", "-r", "-l",
NULL);
/* Заменить себя на ls */
execl(P_WAIT, "/bin/ls", "/bin/ls", "-t", "-r", "-l",
NULL);
Или, вариант с применением суффикса v:
char *argv[] = {
"/bin/ls",
"-t",