close

Вход

Забыли?

вход по аккаунту

?

811.Разработка компонентов системного программного обеспечения. Процессы в Linux учеб.-метод

код для вставкиСкачать
Министерство образования и науки Российской Федерации
Сибирский федеральный университет
Разработка компонентов системного
программного обеспечения
Процессы в Linux
Учебно-методическое пособие
Электронное издание
Красноярск
СФУ
2012
1
УДК 004.45(07)
ББК 32.973.2я73
Р170
Составитель: Д.А. Кузьмин
Р170 Разработка компонентов системного программного обеспечения.
Процессы в Linux: учебно-методическое пособие [Электронный ресурс]
/ Д.А. Кузьмин, Ю.В. Удалова. – Электрон. дан. – Красноярск: Сиб.
федер. ун-т, 2012. – 1 диск. – Систем. требования: PC не ниже класса
Pentium I; 128 Mb RAM; Windows 98/XP/7; Microsoft Word 97-2003/2007.
– Загл. с экрана.
Издание посвящено управлению процессами в операционной системе Linux,
оно раскрывает понятие процесса, предоставляет схему управления процессами,
описывает различные механизмы синхронизации и передачи данных между
процессами, а также содержит информацию о работе с процессами и легковесными
процессами (нитями, потоками) в мультиплатформенной библиотеке Qt. Понимание
процессного механизма и умение программировать многопроцессные приложения
необходимы для создания эффективного системного программного обеспечения.
Предназначено для специальностей: прикладная математика и информатика
(010501), компьютерная безопасность (090102), информатика и вычислительная
техника (230100).
УДК 004.45(07)
ББК 32.973.2я73
© Сибирский
федеральный
университет, 2012
Учебное издание
Подготовлено к публикации редакционно-издательским
отделом БИК СФУ
Подписано в свет 18.01.2012 г. Заказ 5885.
Уч.-изд. л. 4,2, 1 Мб.
Тиражируется на машиночитаемых носителях.
Редакционно-издательский отдел
Библиотечно-издательского комплекса
Сибирского федерального университета
660041, г. Красноярск, пр. Свободный, 79
Тел/факс (391) 244-82-31. E-mail rio@sfu-kras.ru
http://rio.sfu-kras.ru
2
Оглавление
Глава 1. Введение в процессы .................................................................................. 4
1.1. Ядро системы .................................................................................................. 4
1.2. Состояния процесса ........................................................................................ 7
1.3. Контекст процесса ........................................................................................ 11
1.4. Идентификатор процесса (PID) ................................................................... 13
1.5. Первичный процесс- процесс init ................................................................ 15
1.6. Каталоги и файлы /proc в ОС Linux ............................................................ 19
1.7. Организация многопользовательского режима ......................................... 23
1.7.1. Схема планирования в UNIX System V Release 4 ............................... 23
1.7.2. Процессы реального времени............................................................... 25
1.7.2.1. Проект KURT ....................................................................................... 26
1.7.2.2. Проект RTLinux ................................................................................... 28
Глава 2. Программирование процессов ................................................................ 29
2.1. Создание процессов ...................................................................................... 29
2.2. Завершение процессов .................................................................................. 30
2.3. Ожидание завершения процесса-потомка .................................................. 31
2.4. Замещение процессов ................................................................................... 35
2.5. Полудуплексные каналы .............................................................................. 36
2.6. Сигнальный механизм .................................................................................. 39
2.6.1. Механизм передачи сигналов ............................................................... 39
2.6.2. Ненадежные сигналы ............................................................................. 40
2.6.3. Надежные сигналы ................................................................................. 42
2.7. Взаимодействие процессов .......................................................................... 45
2.7.1. Разделяемая память ................................................................................ 47
2.7.2. Семафоры ................................................................................................ 48
2.7.3. Очереди сообщений .............................................................................. 52
2.8. Сетевое взаимодействие процессов с использованием программные
гнезд (sockets) ....................................................................................................... 56
Глава 3. Процессы и потоки в библиотеке Qt ...................................................... 62
3.1. Процессы........................................................................................................ 63
3.2. Потоки ............................................................................................................ 67
3.3. Семафоры....................................................................................................... 73
3.4. Условные переменные .................................................................................. 78
Приложение В .......................................................................................................... 84
Описание используемых в ОС Linux сигналов .................................................... 84
Словарь терминов .................................................................................................... 88
3
Глава 1. Введение в процессы
Процесс (или по-другому, задача) - абстракция, описывающая
выполняющуюся программу. Для операционной системы процесс
представляет собой единицу работы, заявку на потребление системных
ресурсов. Подсистема управления процессами планирует выполнение
процессов, то есть распределяет процессорное время между несколькими
одновременно существующими в системе процессами, а также занимается
созданием и уничтожением процессов, обеспечивает процессы необходимыми
системными ресурсами, поддерживает взаимодействие между процессами
Каждый процесс выполняется в собственной виртуальной памяти, и, тем
самым, процессы защищены один от другого, т.е. один процесс не может
бесконтрольно прочитать что-либо из памяти другого процесса или записать в
нее. Однако контролируемые взаимодействия процессов допускаются
системой, в том числе за счет возможности разделения одного сегмента
памяти между виртуальной памятью нескольких процессов[Кузнецов].
1.1. Ядро системы
-
Основу ОС Linux, как и любой UNIX-о подобной системы, составляет
защищенное ядро, в функции которого входит управление ресурсами
компьютера и предоставление базового набора услуг пользователям. Как и в
любой другой многопользовательской операционной системе, ядро
обеспечивает защиту пользователей друг от друга и защиту системных данных
от любого непривилегированного пользователя.
Мобильность ОС: одним из основных достоинств Unix-систем является
ее мобильность - переносимость на различные платформы. Это связано с тем,
что основная часть кода ядра, реализующая набор алгоритмов управления
системой написана на языке Си и для переноса на другую платформу требует
простой перекомпиляции. То же самое относится и ко всем утилитам.
Ядро состоит из двух частей - машинно-зависимой(сравнительно
небольшой), скрывающей особенности архитектуры от основной машиннонезависимой части ядра, реализующей все алгоритмы управления системы.
Машинно-зависимая часть традиционного ядра ОС UNIX написана на
смеси языка Си и языка ассемблера и включает следующие компоненты:
инициализация системы на низком уровне;
первичная обработка внутренних и внешних прерываний;
управление памятью (в той части, которая относится к особенностям
аппаратной поддержки виртуальной памяти);
переключение контекста процессов между режимами пользователя и ядра;
компоненты, связанные с особенностями целевой платформы части драйверов
устройств.
4
-
-
-
-
-
-
Машинно-независимая часть ядра(написана на языке Си) - реализует все
алгоритмы управления системой. Основные функции:
Инициализация системы - функция запуска и раскрутки. Ядро системы
обеспечивает средство раскрутки (системный bootstrap), которое обеспечивает
загрузку полного ядра в память компьютера и запускает ядро.
Управление процессами - функция создания, завершения и отслеживания
существующих процессов и нитей ("процессов", выполняемых на общей
виртуальной памяти). Поскольку ОС UNIX является мультипроцессной
операционной системой, ядро обеспечивает разделение между запущенными
процессами времени процессора (или процессоров в мультипроцессорных
системах) и других ресурсов компьютера, псевдопараллельность.
Управление памятью - функция отображения
виртуальной памяти
процессов
в
физическую
оперативную
память
компьютераю.
Соответствующий компонент ядра обеспечивает разделяемое использование
одних и тех же областей оперативной памяти несколькими процессами с
использованием внешней памяти.
Управление файлами - функция, реализующая абстракцию файловой
системы,- иерархии каталогов и файлов. Реализация виртуальной ФС,
обеспечивающей возможность поддерживать различные типы ФС.
Коммуникационные средства - функция, обеспечивающая возможности
обмена данными между процессами (IPC - Inter-Process Communications),
между процессами, выполняющимися
в разных узлах локальной или
глобальной сети передачи данных, а также между процессами и драйверами
внешних устройств.
Программный интерфейс - функция, обеспечивающая доступ к
возможностям ядра со стороны пользовательских процессов на основе
механизма системных вызовов, оформленных в виде библиотеки функций.
Ядро Unix имеет модульную структуру, что обеспечивает системе
гибкость и позволяет более экономно использовать память. Модули
компилируются отдельно и затем могут быть вставлены в запущенное ядро
или удалены из ядра практически в любое время.
Ядро системы работает в собственном "ядерном" виртуальном
пространстве, к которому не может иметь доступа ни один пользовательский
процесс.
Ядро системы предоставляет возможности (набор системных вызовов)
для порождения новых процессов, отслеживания окончания порожденных
процессов и т.д. Ядро системы - это полностью пассивный набор программ и
данных. Любая программа ядра может начать работать только по инициативе
некоторого пользовательского процесса (при выполнении системного вызова),
либо по причине внутреннего или внешнего прерывания (примером
внутреннего прерывания может быть прерывание из-за отсутствия в основной
памяти требуемой страницы виртуальной памяти пользовательского процесса;
5
примером внешнего прерывания является любое прерывание процессора по
инициативе внешнего устройства). В любом случае считается, что
выполняется ядерная часть обратившегося или прерванного процесса, т.е.
ядро всегда работает в контексте некоторого процесса.
Состав ядра:
•
ДРАЙВЕРЫ УСТРОЙСТВ(модули). И тех, которые есть, и тех, которых нет,
но могут быть, а так же и такие, которые никогда вам не понадобятся.
•
УПРАВЛЯЮЩИЕ ПОДПРОГРАММЫ: части кода, ответственные за
обеспечение работы пользовательских программ - разделение времени и
прочих ресурсов системы.
•
СЛУЖЕБНЫЕ ТАБЛИЦЫ И ДАННЫЕ ЯДРА: таблицы текущих процессов,
открытых файлов, управляющие структуры...
•
СИСТЕМНЫЕ ВЫЗОВЫ. (То, что MS-DOS-е называется "21 прерывание" можно считать некоторой аналогией на системные вызовы.) С точки зрения
программиста это обычная С-функция, только выполняет она системнозависимые действия, например: прочитать данные из файла, установить
сетевое соединение, создать каталог, и т.д. и т.п. Все системные вызовы (а
всего их более 1500 штук ) находятся в теле ядра Unix. Пользовательские
программы, вызывающие функции, являющиеся системными вызовами, на
самом деле содержат только ссылки на соответствующие адреса памяти в
ядре.
Что находится в оперативной памяти:
Собственно само ЯДРО ОПЕРАЦИОННОЙ СИСТЕМЫ
БУФЕРНЫЙ КЭШ. Часть оперативной памяти резервируется под
кэширование чтения и записи на диск. Любая операция чтения с диска
приводит к тому, что прочитанные с блоки помещаются в буферный кэш, а из
него уже передаются запросившим данные программам. Если блок попал в
кэш, то все последующие обращения к нему будут получать образ блока из
КЭШ, причем не зависимо, от того - та же самая программа, обращается к
блоку, или какая-либо другая. Кэшируется так же и запись на диск, опять же,
разделяемая между всеми выполняемыми программами.
• ПРОЦЕССЫ - системные процессы, процессы демоны, пользовательские
процессы
•
•
6
1.2. Состояния процесса
Время жизни процесса можно теоретически разбить на несколько
состояний, описывающих процесс.
Полный набор состояний процесса:
1.
Процесс выполняется в режиме задачи;
2.
Процесс выполняется в режиме ядра;
3.
Процесс не выполняется, но готов к запуску под управлением ядра;
4.
Процесс приостановлен и находится в оперативной памяти;
5.
Процесс готов к запуску, но программа подкачки (нулевой процесс)
должна еще загрузить процесс в оперативную память, прежде чем он будет
запущен под управлением ядра;
6.
Процесс приостановлен и программа подкачки выгрузила его во
внешнюю память, чтобы в оперативной памяти освободить место для других
процессов;
7.
Процесс возвращен из привилегированного режима (режима ядра) в
непривилегированный (режим задачи), ядро резервирует его и переключает
контекст на другой процесс.
8.
Процесс вновь создан и находится в переходном состоянии; процесс
существует, но не готов к выполнению, хотя и не приостановлен. Это
состояние является начальным состоянием всех процессов, кроме нулевого
процесса;
9.
Процесс вызывает системную функцию exit и прекращает
существование, однако, после него осталась запись, содержащая код выхода, и
некоторая хронометрическая статистика, собираемая родительским
процессом(это состояние является последним состоянием процесса).
Для начала рассмотрим два основных состояния, которые проживает
процесс в системе: «выполнение в режиме задачи» и «выполнение в режиме
ядра» и дадим предварительные понятийные определения:
«Выполнение в режиме задачи» - состояние, когда процесс получает
контроль над процессором т.е. состояние выполнения процесса.
«Выполнение в режиме ядра» - выполняющийся процесс прерван в
результате прерывания (аппаратного, программного – любой системный
вызов, таймера) и управление передано системе, которая выполняется «в
контексте текущего процесса» (смотрите 1.3 Контекст процесса).
Рассмотрим детально с помощью модели переходов типичное поведение
процесса представленное на рисунке 1.1.
7
Рисунок 1.1 Диаграмма состояний процесса
Ситуации, которые будут обсуждаться, несколько искусственны, однако
они вполне применимы для иллюстрации различных переходов. Начальным
состоянием модели является создание процесса родительским процессом с
помощью системной функции fork(смотрите главу 2 Программирование
процессов); из этого состояния процесс неминуемо переходит в состояние
готовности к запуску (3 или 5). Предположим, что процесс перешел в
состояние "готовности к запуску в памяти" (3). Планировщик процессов в
конечном счете выберет процесс для выполнения и процесс перейдет в
состояние "выполнения в режиме ядра". После всего этого процесс может
перейти в состояние "выполнения в режиме задачи". По прохождении
определенного периода времени может произойти прерывание работы
процессора по таймеру и процесс снова перейдет в состояние "выполнения в
режиме ядра". Как только программа обработки прерывания закончит работу,
ядру может понадобиться подготовить к запуску другой процесс, поэтому
первый процесс перейдет в состояние "резервирования", уступив дорогу
второму процессу. Состояние "резервирования" в действительности не
отличается от состояния "готовности к запуску в памяти" (пунктирная линия
на рисунке, соединяющая между собой оба состояния, подчеркивает их
эквивалентность), но они выделяются в отдельные состояния, чтобы
подчеркнуть, что процесс, выполняющийся в режиме ядра, может быть
зарезервирован только в том случае, если он собирается вернуться в режим
задачи. Следовательно, ядро может при необходимости подкачивать процесс
8
из состояния "резервирования". При известных условиях планировщик
выберет процесс для исполнения и тот снова вернется в состояние
"выполнения в режиме задачи".
Когда процесс выполняет вызов системной функции, он из состояния
"выполнения в режиме задачи" переходит в состояние "выполнения в режиме
ядра". Предположим, что системной функции требуется ввод-вывод - процесс
вынужден дожидаться завершения ввода-вывода. Он переходит в состояние
"приостанова в памяти", в котором будет находиться до тех пор, пока не
получит извещения об окончании ввода-вывода. Когда ввод-вывод
завершится, произойдет аппаратное прерывание и программа обработки
прерывания возобновит выполнение процесса, в результате чего он перейдет в
состояние "готовности к запуску в памяти". Предположим, что система
выполняет множество процессов, которые одновременно никак не могут
поместиться в оперативной памяти, и программа подкачки (нулевой процесс)
выгружает один процесс, чтобы освободить место для другого процесса,
находящегося в состоянии "готов к запуску, но выгружен". Первый процесс,
выгруженный из оперативной памяти, переходит в то же состояние. Когда
программа подкачки выбирает наиболее подходящий процесс для загрузки в
оперативную память, этот процесс переходит в состояние "готовности к
запуску в памяти". Планировщик выбирает процесс для исполнения и он
переходит в состояние "выполнения в режиме ядра". Когда процесс
завершается, он исполняет системную функцию exit, последовательно
переходя в состояния "выполнения в режиме ядра" и, наконец, в состояние
"прекращения существования". Процесс может управлять некоторыми из
переходов на уровне задачи. Во-первых, один процесс может создать другой
процесс. Тем не менее, в какое из состояний процесс перейдет после создания
(т.е. в состояние "готов к выполнению, находясь в памяти" или в состояние
"готов к выполнению, но выгружен") зависит уже от ядра. Процессу эти
состояния не подконтрольны. Во-вторых, процесс может обратиться к
различным системным функциям, чтобы перейти из состояния "выполнения в
режиме задачи" в состояние "выполнения в режиме ядра", а также перейти в
режим ядра по своей собственной воле. Тем не менее, момент возвращения из
режима ядра от процесса уже не зависит; в результате каких-то событий он
может никогда не вернуться из этого режима и из него перейдет в состояние
"прекращения существования" (Например при получении сигнала). Наконец,
процесс может завершиться с помощью функции exit по своей собственной
воле, но как указывалось ранее, внешние события могут потребовать
завершения процесса без явного обращения к функции exit. Все остальные
переходы относятся к жестко закрепленной части модели, закодированной в
ядре, и являются результатом определенных событий, реагируя на них в
соответствии с правилами. Две принадлежащие ядру структуры данных
описывают процесс: запись в таблице процессов и пространство процесса.
Таблица процессов содержит поля, которые должны быть всегда доступны
9
ядру, а пространство процесса - поля, необходимость в которых возникает
только у выполняющегося процесса. Поэтому ядро выделяет место для
пространства процесса только при создании процесса(в нем нет
необходимости, если записи в таблице процессов не соответствует
конкретный процесс).
Запись в таблице процессов состоит из следующих полей:
Поле состояния, которое идентифицирует состояние процесса.
Поля, используемые ядром при размещении процесса и его пространства
в основной или внешней памяти. Ядро использует информацию этих
полей для переключения контекста на процесс, когда процесс переходит
из состояния "готов к выполнению, находясь в памяти" в состояние
"выполнения в режиме ядра" или из состояния "резервирования" в
состояние "выполнения в режиме задачи". Кроме того, ядро использует
эту информацию при перекачки процессов из и в оперативную память
(между двумя состояниями "в памяти" и двумя состояниями
"выгружен"). Запись в таблице процессов содержит также поле,
описывающее размер процесса и позволяющее ядру планировать
выделение пространства для процесса.
Несколько пользовательских идентификаторов (UID), устанавливающих
различные привилегии процесса. Поля UID, например, описывают
совокупность процессов, могущих обмениваться сигналами (смотрите
описание сигнального механизма в главе 2).
Идентификаторы процесса (PID), указывающие взаимосвязь между
процессами. Значения полей PID задаются при переходе процесса в
состояние "создан" во время выполнения функции fork.
Дескриптор события (устанавливается тогда, когда процесс
приостановлен).
Параметры планирования, позволяющие ядру устанавливать порядок
перехода процессов из состояния "выполнения в режиме ядра" в
состояние "выполнения в режиме задачи".
Поле сигналов, в котором перечисляются сигналы, посланные процессу,
но еще не обработанные (смотрите описание сигнального механизма в
главе 2 Программирование процессов).
Различные таймеры, описывающие время выполнения процесса и
использование ресурсов ядра и позволяющие осуществлять слежение за
выполнением и вычислять приоритет планирования процесса. Одно из
полей является таймером, который устанавливает пользователь и
который необходим для посылки процессу сигнала тревоги (сигнал
SIGALRM – смотрите описание сигнального механизма в главе 2).
Пространство
процесса
содержит
поля,
дополнительно
характеризующие состояния процесса (смотрите раздел 1.3 Контекст
процесса), куда входят:
10
Указатель на таблицу процессов, который идентифицирует запись,
соответствующую процессу.
Пользовательские идентификаторы, устанавливающие различные
привилегии процесса, в частности, права доступа к файлу.
Поля таймеров, хранящие время выполнения процесса (и его потомков)
в режиме задачи и в режиме ядра.
Вектор, описывающий реакцию процесса на сигналы.
Поле операторского терминала, идентифицирующее "регистрационный
терминал", который связан с процессом.
Поле ошибок, в которое записываются ошибки, имевшие место при
выполнении системной функции.
Поле возвращенного значения, хранящее результат выполнения
системной функции.
Параметры ввода-вывода: объем передаваемых данных, адрес источника
(или приемника) данных в пространстве задачи, смещения в файле
(которыми пользуются операции ввода-вывода) и т.д.
Имена текущего каталога и текущего корня, описывающие файловую
систему, в которой выполняется процесс.
Таблица пользовательских дескрипторов файла, которая описывает
файлы, открытые процессом.
Поля
границ,
накладывающие
ограничения
на
размерные
характеристики процесса и на размер файла, в который процесс может
вести запись.
Поле прав доступа, хранящее двоичную маску установок прав доступа к
файлам, которые создаются процессом. Пространство состояний
процесса и переходов между ними рассматривалось в данном разделе на
логическом уровне. Каждое состояние имеет также физические
характеристики, управляемые ядром, в частности, виртуальное адресное
пространство процесса.
1.3. Контекст процесса
Каждому процессу соответствует контекст, в котором он выполняется.
Контекст процесса включает в себя содержимое адресного пространства
задачи, выделенного процессу, а также содержимое относящихся к процессу
аппаратных регистров и структур данных ядра. С формальной точки зрения,
контекст процесса объединяет в себе пользовательский контекст, регистровый
контекст и системный контекст. Пользовательский контекст состоит из
команд и данных процесса, стека задачи и содержимого
совместно
используемого пространства памяти в виртуальных адресах процесса. Те
части виртуального адресного пространства
процесса,
которые
периодически отсутствуют в оперативной памяти вследствие выгрузки или
замещения страниц, также включаются в
пользовательский контекст.
11
Регистровый контекст включает в себя содержимое аппаратных регистров
процессора(состав этого контекста зависит от конкретной архитектуры
вычислительной системы).
Контекст процесса системного уровня в UNIX-системах состоит из
"статической" и "динамических" частей. Для каждого процесса имеется одна
статическая часть контекста системного уровня и переменное число
динамических частей.
Статическая часть контекста процесса системного уровня включает
следующее:
1. Дескриптор процесса, т.е. элемент таблицы описателей существующих в
системе процессов. Дескриптор процесса включает, в частности,
следующую информацию:
• состояние процесса;
• физический адрес в основной или внешней памяти u-области
процесса;
• идентификаторы пользователя, от имени которого запущен
процесс;
• идентификатор процесса;
• прочую информацию, связанную с управлением процессом.
2. U-область (U-area) - индивидуальная для каждого процесса область
пространства ядра, обладающая тем свойством, что хотя u-область
каждого процесса располагается в отдельном месте физической памяти,
u-области всех процессов имеют один и тот же виртуальный адрес в
адресном пространстве ядра. Именно это означает, что какая бы
программа ядра не выполнялась, она всегда выполняется как ядерная
часть некоторого пользовательского процесса, и именно того процесса,
u-область которого является "видимой" для ядра в данный момент
времени. U-область процесса содержит:
• указатель на описатель текущего процесса;
• идентификаторы пользователя;
• счетчик времени, в течение которого процесс реально выполнялся
(т.е. занимал процессор) в режиме пользователя и режиме ядра;
• параметры системного вызова;
• результаты системного вызова;
• таблица дескрипторов открытых файлов;
• предельные размеры адресного пространства процесса;
• предельные размеры файла, в который процесс может писать;
и т.д.
Динамическая часть контекста процесса - это один или несколько
стеков, которые используются процессом при его выполнении в режиме ядра.
Число ядерных стеков процесса соответствует числу уровней прерывания,
поддерживаемых конкретной аппаратурой.
12
На Рисунке 1.2 изображены компоненты контекста процесса. Слева на
рисунке
изображена
статическая
часть контекста. В нее входят:
пользовательский контекст, состоящий из программ процесса (машинных
инструкций), данных, стека и разделяемой памяти (если она имеется), а
также статическая часть системного контекста, состоящая из записи таблицы
процессов, пространства процесса и записей частной таблицы областей
(информации, необходимой для трансляции виртуальных адресов
пользовательского контекста). Справа на рисунке изображена динамическая
часть контекста. Она имеет вид стека и включает в себя несколько элементов,
хранящих регистровый контекст предыдущего уровня и стек ядра для
текущего уровня. Нулевой контекстный уровень представляет собой пустой
уровень, относящийся к пользовательскому контексту; увеличение стека здесь
идет в адресном пространстве задачи, стек ядра недействителен. Стрелка,
соединяющая между собой статическую часть системного контекста и
верхний уровень динамической части контекста, означает то, что в таблице
процессов хранится информация, позволяющая ядру восстанавливать текущий
контекстный уровень процесса.
Рисунок 1.2 Компоненты контекста процесса
1.4. Идентификатор процесса (PID)
Каждый процесс в операционной системе получает уникальный
числовой идентификационный номер PID (Process IDentificatorидентификатор процесса) в диапазоне от 1 до 65535. При создании нового
процесса операционная система пытается присвоить ему свободный номер
больший, чем у процесса, созданного перед ним. Если таких свободных
номеров не оказывается (например, мы достигли максимально возможного
13
номера для процесса), то операционная система выбирает минимальный из
всех свободных номеров. PID является именем процесса, по которому мы
можем адресовать процесс в операционной системе при использовании
различных средств просмотра и управления процессами. PPID (Parent Process
Identifier - идентификатор родительского процесса) определяет
родственные отношения между процессами, которые в значительной степени
определяют его свойства и возможности. Другие параметры, которые
необходимы для работы программы, называют "окружение процесса". Одним
из таких параметров является управляющий терминал - имя терминального
устройства, на которое процесс выводит информацию и с которого
информацию получает. Управляющий терминал имеют далеко не все
процессы. Процессы, не привязанные к какому-то конкретному терминалу
называются "демонами" (daemons). Такие процессы, будучи запущенными
пользователем, не завершают свою работу по окончании сеанса, а продолжают
работать, так как они не связаны никак с текущим сеансом и не могут быть
автоматически завершены. Как правило, с помощью демонов реализуются
серверные службы, так например сервер печати реализован процессомдемоном cupsd, а сервер журналирования – syslogd, web-сервер -httpd.
Процессы в ОС Linux обладают теми же правами, которыми обладает
пользователь, от чьего имени был запущен процесс. На самом деле
операционная система воспринимает работающего в ней пользователя как
набор запущенных от его имени процессов. Ведь и сам сеанс пользователя
открывается в командной оболочке (или оболочке Х) от имени пользователя.
Поэтому когда мы говорим "права доступа пользователя к файлу" то
подразумеваем "права доступа процессов, запущенных от имени пользователя
к файлу".
Реальные и эффективные идентификаторы пользователя и группы
Для определения имени пользователя, запустившего процесс,
операционная система использует реальные идентификаторы пользователя
и группы, назначаемые процессу. Но эти идентификаторы не являются
решающими при определении прав доступа. Для этого у каждого процесса
существует другая группа идентификаторов – эффективные. Как правило,
реальные и эффективные идентификаторы процессов одинаковые, но есть и
исключения. Например, для работы утилиты passwd необходимо использовать
идентификатор суперпользователя(root), так как только суперпользователь
имеет права на запись в файлы паролей. В этом случае эффективные
идентификаторы процесса будут отличаться от реальных.
Как это реализовано:
У каждого файла есть набор специальных прав доступа - биты SUID и
SGID. Эти биты позволяют при запуске программы присвоить ей
эффективные
идентификаторы
владельца
и
группы-владельца
14
соответственно и выполнять процесс с правами доступа другого
пользователя. Так как файл passwd принадлежит пользователю root и у него
установлен бит SUID, то при запуске процесс passwd будет обладать
правами пользователя root.
Устанавливаются биты SGID и SUID командой chmod:
chmod u+s filename - установка бита SUID
chmod g+s filename - установка бита SGID
1.5. Первичный процесс- процесс init
В командной строке наберите следующую команду и вы получите
список всех процессов выполняющихся в системе:
#ps –AfH | more
Команда ps служит для просмотра списка процессов в Unix-системах.
Формат команды следующий: ps [PID] [options] - просмотр списка процессов.
Полное описание команды вы можете узнать - man ps.
Для просмотра иерархии(дерева) процессов можно воспользоваться командой
tree.
Опция A – обеспечит вывод всех процессов, f – полную информацию о
процессе, ключ H покажет иерархию процессов. “| more “или “| less”
обеспечит ‘форматированный’ вывод на экран.
Ниже показан фрагмент результата выполнения команды в ОС Novell Suse
Linux 10.0.
UID PID PPID C STIME TTY TIME CMD
root
1 0 0 18:23 ?
00:00:01 init [5]
root
2 1 0 18:23 ?
00:00:00 [ksoftirqd/0]
root
3 1 0 18:23 ?
00:00:00 [events/0]
root
4 1 0 18:23 ?
00:00:00 [khelper]
root
5 1 0 18:23 ?
00:00:00 [kthread]
root
7 5 0 18:23 ?
00:00:00 \_ [kblockd/0]
root
8 5 0 18:23 ?
00:00:00 \_ [kacpid]
root
120 5 0 18:23 ?
00:00:00 \_ [pdflush]
root
121 5 0 18:23 ?
00:00:00 \_ [pdflush]
root
123 5 0 18:23 ?
00:00:00 \_ [aio/0]
root
329 5 0 18:23 ?
00:00:00 \_ [cqueue/0]
root
330 5 0 18:23 ?
00:00:00 \_ [kseriod]
root
367 5 0 18:23 ?
00:00:00 \_ [kpsmoused]
root 1257 5 0 18:23 ?
00:00:00 \_ [khubd]
root 1652 5 0 18:23 ?
00:00:00 \_ [scsi_eh_0]
root 1653 5 0 18:23 ?
00:00:00 \_ [usb-storage]
root 2519 5 0 18:23 ?
00:00:00 \_ [kauditd]
root 3133 5 0 18:24 ?
00:00:00 \_ [novfs_ST]
root
122 1 0 18:23 ?
00:00:00 [kswapd0]
15
root
788 1 0 18:23 ?
00:00:00 [kjournald]
root
882 1 0 18:23 ?
00:00:00 /sbin/udevd --daemon
root 1518 1 0 18:23 ?
00:00:00 [pccardd]
root 1519 1 0 18:23 ?
00:00:00 [khpsbpkt]
root 1669 1 0 18:23 ?
00:00:00 [knodemgrd_0]
root 1685 1 0 18:23 ?
00:00:00 [shpchpd_event]
root 1859 1 0 18:23 ?
00:00:00 [kjournald]
root 2318 1 0 18:23 ?
00:00:00 /sbin/syslog-ng
root 2321 1 0 18:23 ?
00:00:00 /sbin/klogd -c 1 -x -x
root 2357 1 0 18:23 ?
00:00:00 /sbin/resmgrd
root 2361 1 0 18:23 ?
00:00:00 /sbin/acpid
100
2371 1 0 18:23 ?
00:00:01 /usr/bin/dbus-daemon --system
root 2374 1 0 18:23 ?
00:00:02 /usr/sbin/hald --daemon=yes --retainprivileges
root 2536 2374 0 18:23 ?
00:00:00 \_ hald-addon-acpi
root 2907 2374 0 18:23 ?
00:00:00 \_ hald-addon-storage
root 2916 2374 0 18:23 ?
00:00:00 \_ hald-addon-storage
root 2396 1 0 18:23 ?
00:00:00 /usr/sbin/dhcdbd --system
root 3096 1 0 18:23 ?
00:00:01 /opt/gnome/sbin/gdm
root 3119 3096 0 18:23 ?
00:00:01 \_ /opt/gnome/sbin/gdm
root 3122 3119 1 18:23 tty7 00:00:16 \_ /usr/X11R6/bin/X :0 -audit 0 -br auth /var/lib/gdm/:0.Xauth -nolisten tcp vt7
root 3365 3119 0 18:24 ?
00:00:00 \_ /opt/gnome/bin/gnome-session
root 3519 3365 0 18:24 ?
00:00:00
\_ ssh-agent /bin/bash
/etc/X11/xinit/xinitrc
root 3108 1 0 18:23 ?
00:00:00 startpar -f -- xdm
root 3144 1 0 18:24 ?
00:00:00 /opt/novell/ncl/bin/novfsd
root 3261 1 0 18:24 tty4 00:00:00 /sbin/mingetty tty4
root 3263 1 0 18:24 tty5 00:00:00 /sbin/mingetty tty5
root 3266 1 0 18:24 tty6 00:00:00 /sbin/mingetty tty6
где:
UID – идентификатор пользователя от имени которого запущен процесс,
PID – уникальный идентификатор процесса,
PPID – идентификатор процесса родителя,
TIME – суммарное время выполнения процесса,
TTY – терминал на котором выполняется данный процесс,
CMD – команда.
Как видно из этого фрагмента во главе процессов находится процесс
init, который является первичным процессом в системе. Параметр процесса
init (смотрите фрагмент - init [5]) определяет уровень загрузки системы.
Указание что и в какой последовательности запускать процессу init
находится в файле /etc/inittab
16
Строка initdefault в файле /etc/inittab определяет уровень загрузки
системы по умолчанию.
так init [5] соответствует строка id:5:initdefault:
Уровень запуска определяет, какие действия будут предприняты в
оставшихся предписаниях файла /etc/inittab.
Каждый уровень определяет состояние и поведение системы
0
1, s, S
2
3
5
6
- Полный останов системы;
- single user mode. Однопользовательский режим;
- многопользовательский режим без NFS-сервера;
- многопользовательский режим с NFS-сервером;
- 3 уровень плюс графический сервер
- перезагрузка системы;
Попробуйте выполнить команды /sbin/init 3, /sbin/init 5, /sbin/init 6,
/sbin/init 0, для их выполнения вам понадобятся права администратора.Каждая
из этих команд приведет к переводу состояния системы на соответствующий
уровень выполнения.
Вариант ваших действий:
Выполните команду su
$su
<вам понадобится ввести пароль пользователя root>
#/sbin/init 6
Посмотрите, что произойдет с вашей системой.
Формат inittab:
ИМЯ: Уровни_выполнения: вид_действия: запускаемая команда
ИМЯ: - просто имя строчки - они все должны быть разными
вид_действия:
- sysinit - запустить один раз после начальной загрузки на соответствующем
уровне выполнения;
- wait - запустить один раз и дожидаться, пока не окончится;
- respawn - запустить параллельно, а если окончится, перезапускать снова;
- off - ничего не делать
Существуют ряд действий, которые будут выполняться независимо от
установленного уровня запуска. Эти шаги обозначены в /etc/inittab строками,
sysinit
# System initialization.
si::sysinit:/etc/init.d/rcS
Инициализация, зависимая от уровня запуска. Как правило, /etc/inittab будет
содержать строки типа:
l0:0:wait:/etc/rc.d/rc 0
17
# ...
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6
# Run gettys in standard runlevels
1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6
Цифра в начале показывает, в каком виртуальном терминале будет
работать программа /sbin/mingetty; следующие несколько цифр -- это те
уровни запуска, при которых это случится (например, запуск mingetty при
каждом из уровней 2, 3, 4 и 5).
Терминальные процессы mingetty обеспечивают регистрацию
пользователя на соответствующем терминале(tty1,tty2, … ttyn), пользователь
вводит имя и пароль, на основании этой регистрации проверяется
возможность доступа. Информация о пользователе берется в файле /etc/passwd
там же находится маска прав. Пароли находятся в файле /etc/shadow, который
доступен по чтению только суперпользователю(root).По этой маске прав
процесс регистрации выполняет команду nice которой устанавливает
реальные права пользователя и после выполняет exec запуская вместо себя
shell - символьную bash, sh, csh, ksh или
графическую prefdm для
пользователей загружающихся графическими терминалами.
По окончанию работы пользователя – команда "exit" или при разрыве
связи процесс init тут же выполняет действие "respawn" - перезапускает на
терминал mingetty - и опять ожидает регистрации пользователя на данном
терминале.
$ps -Af | grep mingetty
Процессы mingetty до регистрации пользователя testuser на втором
терминале
UID
root
root
root
root
root
PID PPID C STIME TTY TIME
CMD
3261 1 0 18:24 tty4 00:00:00 /sbin/mingetty tty4
3263 1 0 18:24 tty5 00:00:00 /sbin/mingetty tty5
3266 1 0 18:24 tty6 00:00:00 /sbin/mingetty tty6
4225 1 0 18:41 tty3 00:00:00 /sbin/mingetty tty3
4350 1 0 18:44 tty2 00:00:00 /sbin/mingetty tty2
18
$ps -fu testuser --forest
Эта команда покажет вам все процессы пользователя testuser, ключ –
forest позволяет увидеть иерархию процессов. Обратите внимание – процессу
загружена shell –bash – которая является корневым процессом всех
последующих процессов пользователя (несет в себе всю маску прав
пользователя).
UID
PID PPID C STIME TTY TIME
CMD
testuser 4459 4350 0 18:45 tty2 00:00:00 -bash
testuser 4555 4459 0 18:45 tty2 00:00:00 \_ /usr/bin/mc -P /tmp/mctestuser/mc.pwd.4459
$ps -Af | grep getty
Процессы mingetty после регистрации пользователя testuser на втором
терминале
UID PID PPID C STIME TTY TIME
CMD
root 3261
1 0 18:24 tty4 00:00:00 /sbin/mingetty tty4
root 3263
1 0 18:24 tty5 00:00:00 /sbin/mingetty tty5
root 3266
1 0 18:24 tty6 00:00:00 /sbin/mingetty tty6
root 4225
1 0 18:41 tty3 00:00:00 /sbin/mingetty tty3
1.6. Каталоги и файлы /proc в ОС Linux
В OC Linux вся информация о текущем состоянии системы
отображается в каталоге /proc в формате файловой системы procfs которая
является специальной псевдо-файловая системой, используемой
как
интерфейс к структурам данных ядра.
В каталоге /proc для каждого выполняющегося процесса имеется
подкаталоги с цифровым названием, совпадающим с pid-ом этого процесса.
Каждый из этих подкаталогов содержит псевдо-файлы и каталоги которые
будут описаны ниже. Кроме этого в /proc присутствуют каталоги и файлы,
описывающие состояние и текущую конфигурацию системы, такие как:
•
self Этот файл имеет отношение к пpоцессам имеющим доступ к
файловой системе proc, и идентифициpованным в директориях
названных по id пpоцессов осуществляющих контpоль.
•
kmsg Этот файл используется системным вызовом syslog() для
pегистpации сообщений ядpа. Чтение этого файла может
осуществляться лишь одним пpоцессом имеющим пpивилегию
19
superuser. Этот файл не доступен для чтения пpи pегистpации с
помощью вызова syslog().
•
loadavg Здесь хранятся средние уровни загрузки системы за последние
1, 5 и 15 минут. Этот файл содеpжит числа подобно:
0.13 0.14 0.05
Числа являются pезультатом команд uptime и подобных,
показывающих сpеднее число пpоцессов пытающихся запуститься
в одно и то же вpямя за последнюю минуту, последние пять минут
и последние пятнадцать.
•
meminfo Файл содеpжит обзоp выходной инфоpмации программы free.
Содеpжание его имеет следующий вид:
total:
used:
free:
shared:
buffers:
Mem: 7528448
7344128
184320
2637824
1949696
Swap: 8024064
1474560
6549504
Данные числа представлены в байтах.
•
сpuinfo В этом файле содержатся значения, зависимые от процессора и
архитектуры системы. Для каждой архитектуры значения будут
различные. Имеются только два общих для всех архитектур значения:
cpu
используемый процессор
BogoMIPS
системная константа, которая вычисляется при
инициализации ядра
•
devices Текстовый список основных номеров и групп устройств
Наберите cat /proc/devices и посмотрите результат
•
filesystem Список файловых систем, которые поддерживает ядро
•
interrupts Здесь хранится число обращений к каждому IRQ
•
ioports
Список используемых в данный момент портов
•
net Этот каталог содеpжит файлы, каждый из котоpых пpедставляет
собой псевдофайл, содержащий информацию о сети в Linux. Эти файлы
пpедставляют двоичные стpуктуpы и они визуально нечитабельны,
однако стандаpтный набоp сетевых программ использует их. Файлы
называются следующим обpазом:
•
unix
•
arp
•
route
•
dev
•
raw
•
tcp
•
udp
•
uptime Файл содеpжит вpемя pаботы системы в целом и
идеализиpованное вpемя затpачиваемое системой на один пpоцесс. Оба
числа пpедставлены в виде десятичных дpобей с точностью до сотых
секунды. Точность до двух цифp после запятой не гаpантиpуется на всех
аpхитектуpах, однако на всех подпpогpаммах Linux даются достаточно
20
•
•
•
•
точно используя удобные 100-Гц. Этот файл выглядит следующим
обpазом: 604.33 205.45 В этом случае система функциониpует 604.33
секунды, а вpемя затpачиваемое на идеальный пpцесс pавно 204.45
секунд.
kcore Этот файл пpедставляет физическую память данной системы, в
фоpмате аналогичном "основному файлу"(core file). Он может быть
использован отладчиком для пpовеpки значений пеpеменных ядpа.
Длина файла pавна длине физической памяти плюс 4кб под заголовок.
pci Это список всех pci-устройств, которые нашло ядро во время
инициализации
директория /proc/ide содержит информацию о конфигурации дисков
stat Файл stat отобpажает статистику данной системы в фоpмате ASCII.
Пpимеp:
cpu
5470 0 3764 193792
disk 0 0 0 0
page 11584 937
swap 255 618
intr 239978
ctxt 20932
btime 767808289
Значения стpок:
Каждый из подкаталогов процессов
(пронумерованных и имеющих
собственный каталог) имеет свой набор файлов и подкаталогов /proc/<pid>/….
В подобном подкаталоге присутствует следующий набор файлов:
cmdli Содеpжит полную командную стpоку пpоцесса, если он полностью
не выгpужен или не «убит». В любом из последних двух случаев
ne
файл пуст и чтение его поводит к тому же результату , что и чтение
пустой строки. Этот файл содеpжит в конце нулевой символ.
cwd
Ссылка на текущий каталога данного пpоцесса. Для обнаpужения
cwd пpоцесса 20, сделайте следующее: (cd /proc/20/cwd; pwd)
root
Это ссылка на корневой каталог процесса
enviro Файл содеpжит тpебования пpоцесса. В файле отсутствуют пеpеводы
стpоки: в конце файла и между записями находятся нулевые
n
символы. Для вывода тpебоаний пpоцесса 10 вы должны сделать: cat
/proc/10/environ | tr "\000" "\n"
exe
Это указатель на исполняемый файл, который был запущен.
Вы можете набpать: /proc/10/exe для пеpезапуска пpоцесса 10 с
любыми изменениями.
fd
Этот подкаталог содержит по одному элементу на каждый открытый
21
процессом файл, каждый элемент имеет имя, совпадающее с
номером файлового дескриптора открытого файла
Файл содержащий список распределенных кусков памяти,
используемых процессом. Общедоступные библиотеки pаспpеделены
в памяти таким обpазом, что на каждую из них отводится один
отpезок памяти. Hекотоpые пpоцессы также используют память для
дpугих целей.
Пpимеp:
00000000 - 00013000 r-xs 00000400 03:03 12164
00013000 - 00014000 rwxp 00013400 03:03 12164
00014000 - 0001c000 rwxp 00000000 00:00 0
bffff000 - c0000000 rwxp 00000000 00:00 0
Пеpвое поле записи опpеделяет начало диапазона pаспpеделенного куска
памяти. Втоpое поле опpеделяет конец диапазона отpезка. Тpетье поле
содеpжит флаги:
•
r - читаемый кусок,
•
w - записываемый,
•
x - запускаемый,
•
s - общедоступный,
•
p - частного пользования.
Четвертое поле - смещение от котоpого пpоисходит pаспpеделение. Пятое
поле отобpажает основной номеp: подномеp устpойства pаспpеделяемого
файла(пятое поле показывает число inode pаспpеделяемого файла).
me Этот файл не идентичен устpойству mem, несмотpя на то, что они имет
одинаковый номеp устpойств. Устpойство /dev/mem - физическая
m
память пеpед выполнением пеpеадpесации, здесь mem - память
доступная пpоцессу. В данный момент она не может быть
пеpеpаспpеделена (mmap()), поскольку в ядpе нет функции общего
пеpеpаспpеделения.
maps
root указатель на коpневой каталог пpоцесса. Полезен для пpогpамм
использующих chrroot(), таких как ftpd.
stat Файл содеpжит массу статусной инфоpмации о пpоцессе. Здесь в
поpядке пpедставления в файле описаны поля и их фоpмат чтения
функцией scanf():
Здесь в поpядке пpедставления в файле stat описаны поля и их фоpмат чтения
функцией scanf():
pid (%d) id пpоцесса.
comm (%s) Имя запускаемого файла в кpуглых скобках. Из него видно
использует пpоцесс свопинг или нет.
22
state (%c) один из символов из набоpа "RSDZT", где:
R - запуск
S - приостановлен в ожидании пpеpывания
W - приостановлен с запpещением пpеpывания (в частности для
своппинга)
Z - исключение пpоцесса (зомби)
T - пpиостановка в опpеделенном состоянии
ppid (%d) pid пpоцесса родителя
pgrp (%d) группа процесса
session (%d)
сессия процесса
tty (%d)
Используемый пpоцессом терминал.
tpgid (%d) группа процессов, которые используют эту же терминальную
линию.
flags (%u) Флаги пpоцесса. Каждый флаг имеет набоp битов
и т.п.
Более подробную информацию по описанию полей файла stat смотрите в
Приложении А.
1.7. Организация многопользовательского режима
Исторически UNIX-системы являются системами разделения времени,
т.е. система должна прежде всего "справедливо" разделять ресурсы
процессора(ов) между процессами, относящимися к разным пользователям,
причем таким образом, чтобы время реакции каждого действия
интерактивного пользователя находилось в допустимых пределах. Однако в
последнее время возрастает тенденция к использованию UNIX-систем в
приложениях реального времени, что повлияло и на алгоритмы планирования.
1.7.1. Схема планирования в UNIX System V Release 4
Опишем общую (без технических деталей) схему планирования
разделения ресурсов процессора(ов) между процессами в UNIX System V
Release 4.
Наиболее распространенным алгоритмом планирования в системах
разделения времени является кольцевой режим (round robin). Основной смысл
алгоритма состоит в том, что время процессора делится на кванты
фиксированного размера, а процессы, готовые к выполнению, выстраиваются
в кольцевую очередь. У этой очереди имеются два указателя - начала и конца.
Когда процесс, выполняющийся на процессоре, исчерпывает свой квант
процессорного времени, он снимается с процессора, ставится в конец очереди,
а ресурсы процессора отдаются процессу, находящемуся в начале очереди.
Если выполняющийся на процессоре процесс откладывается (например, по
причине обмена с некоторым внешнем устройством) до того, как он исчерпает
свой квант, то после повторной активизации он становится в конец очереди
(не смог доработать - не вина системы). Это прекрасная схема разделения
23
времени в случае, когда все процессы одновременно помещаются в
оперативной памяти.
Однако ОС UNIX всегда была рассчитана на то, чтобы обслуживать
больше процессов, чем можно одновременно разместить в основной памяти.
Другими словами, часть процессов, потенциально готовых выполняться,
размещалась во внешней памяти (куда образ памяти процесса попадал в
результате свопинга). Поэтому требовалась несколько более гибкая схема
планирования разделения ресурсов процессора(ов). В результате было введено
понятие приоритета. В ОС UNIX значение приоритета определяет, во-первых,
возможность процесса пребывать в основной памяти и на равных
конкурировать за процессор. Во-вторых, от значения приоритета процесса,
вообще говоря, зависит размер временного кванта, который предоставляется
процессу для работы на процессоре при достижении своей очереди. В-третьих,
значение приоритета, влияет на место процесса в общей очереди процессов к
ресурсу процессора(ов).
Схема разделения времени между процессами с приоритетами в общем
случае выглядит следующим образом. Готовые к выполнению процессы
выстраиваются в очередь к процессору в порядке уменьшения своих
приоритетов. Если некоторый процесс отработал свой квант процессорного
времени, но при этом остался готовым к выполнению, то он становится в
очередь к процессору впереди любого процесса с более низким приоритетом,
но вслед за любым процессом, обладающим тем же приоритетом. Если
некоторый процесс активизируется, то он также ставится в очередь вслед за
процессом, обладающим тем же приоритетом. Весь вопрос в том, когда
принимать решение о свопинге процесса, и когда возвращать в оперативную
память процесс, содержимое памяти которого было ранее перемещено во
внешнюю память.
Традиционное решение ОС UNIX состоит в использовании динамически
изменяющихся приоритетов. Каждый процесс при своем образовании
получает некоторый устанавливаемый системой статический приоритет,
который в дальнейшем может быть изменен с помощью системного вызова
nice. Этот статический приоритет является основой начального значения
динамического приоритета процесса, являющегося реальным критерием
планирования. Все процессы с динамическим приоритетом не ниже
порогового участвуют в конкуренции за процессор (по схеме, описанной
выше). Однако каждый раз, когда процесс успешно отрабатывает свой квант
на процессоре, его динамический приоритет уменьшается(величина
уменьшения зависит от статического приоритета процесса). Если значение
динамического приоритета процесса достигает некоторого нижнего предела,
он становится кандидатом на откачку (свопинг) и больше не конкурирует за
процессор.
Процесс, образ памяти которого перемещен во внешнюю память, также
обладает динамическим приоритетом. Этот приоритет не дает процессу право
24
конкурировать за процессор (да это и невозможно, поскольку образ памяти
процесса не находится в основной памяти), но он изменяется, давая в конце
концов процессу возможность вновь вернуться в основную память и принять
участие в конкуренции за процессор. Правила изменения динамического
приоритета для процесса, перемещенного во внешнюю память, в принципе,
очень просты. Чем дольше образ процесса находится во внешней памяти, тем
более высок его динамический приоритет (конкретное значение
динамического приоритета, конечно, зависит от его статического приоритета).
Конечно, раньше или позже значение динамического приоритета такого
процесса перешагнет через некоторый порог, и тогда система принимает
решение о необходимости возврата образа процесса в основную память. После
того, как в результате свопинга будет освобождена достаточная по размерам
область основной памяти, процесс с приоритетом, достигшим критического
значения, будет перемещен в основную память и будет в соответствии со
своим приоритетом конкурировать за процессор.
1.7.2. Процессы реального времени
Как вписываются в эту схему процессы реального времени? Прежде
всего, нужно разобраться, что понимается под концепцией "реального
времени" в ОС UNIX. Известно, что существуют по крайней мере два
понимания термина - " мягкое реальное время (soft realtime)" и " жесткое
реальное время (hard realtime)".
Жесткое реальное время означает, что каждое событие (внутреннее или
внешнее), происходящее в системе (обращение к ядру системы за некоторой
услугой, прерывание от внешнего устройства и т.д.), должно обрабатываться
системой за время, не превосходящее верхнего предела времени, отведенного
для таких действий. Режим жесткого реального времени требует задания
четких временных характеристик процессов, и эти временные характеристики
должны определять поведение планировщика распределения ресурсов
процессора(ов) и основной памяти.
Режим мягкого реального времени, в отличие от этого, предполагает,
что некоторые процессы (процессы реального времени) получают права на
получение ресурсов основной памяти и процессора(ов), существенно
превосходящие права процессов, не относящихся к категории процессов
реального времени. Основная идея состоит в том, чтобы дать возможность
процессу реального времени опередить в конкуренции за вычислительные
ресурсы любой другой процесс, не относящийся к категории процессов
реального времени. Отслеживание проблем конкуренции между различными
процессами реального времени относится к функциям администратора
системы и выходит за пределы этого курса.
В своих самых последних вариантах UNIX-о подобные системы
поддерживают концепцию мягкого реального времени. Это делается
способом, не выходящим за пределы основополагающего принципа
25
разделения времени. Как было отмечено выше, имеется некоторый диапазон
значений статических приоритетов процессов. Некоторый поддиапазон этого
диапазона включает значения статических приоритетов процессов реального
времени. Процессы, обладающие динамическими приоритетами, основанными
на статических приоритетах процессов реального времени, обладают
следующими особенностями:
1. каждому из таких процессов предоставляется неограниченный сверху
квант времени на процессоре. Другими словами, занявший процессор
процесс реального времени не будет с него снят до тех пор, пока сам не
заявит о невозможности продолжения выполнения (например, задав
обмен с внешним устройством);
2. процесс реального времени не может быть перемещен из основной
памяти во внешнюю, если он готов к выполнению, и в оперативной
памяти присутствует хотя бы один процесс, не относящийся к категории
процессов реального времени (т.е. процессы реального времени
перемещаются во внешнюю память последними, причем в порядке
убывания своих динамических приоритетов);
3. любой процесс реального времени, перемещенный во внешнюю память,
но готовый к выполнению, переносится обратно в основную память как
только в ней образуется свободная область соответствующего размера.
(выбор процесса реального времени для возвращения в основную
память производится на основании значений динамических
приоритетов).
Тем самым образом в современных вариантах UNIX-систем
одновременно реализована как возможность разделения времени для
интерактивных процессов, так и возможность мягкого реального времени для
процессов, связанных с реальным управлением поведением объектов в
реальном времени.
В качестве примера можно рассмотрим специальные расширения Linux RTLinux, KURT и UTIME, позволяющие получить устойчивую среду
реального времени. RTLinux представляет собой систему "жесткого"
реального времени, а KURT (KU Real Time Linux) относится к системам
"мягкого" реального времени. Linux-расширение UTIME, входящее в состав
KURT, позволяет добиться увеличения частоты системных часов, что
приводит к более быстрому переключению контекста задач.
1.7.2.1. Проект KURT
Проект KURT (http://hegel.ittc.ukans.edu/projects/kurt) характеризуется
минимальными изменениями ядра Linux и предусматривает два режима
работы - нормальный (normal mode) и режим реального времени (real-time
mode). Процесс, использующий библиотеку API-интерфейсов KURT, в любые
моменты времени может переключаться между этими двумя режимами.
Программный пакет KURT оформлен в виде отдельного системного модуля
26
Linux - RTMod, который становится дополнительным планировщиком
реального времени. Данный планировщик доступен в нескольких вариантах и
может тактироваться от любого системного таймера или от прерываний
стандартного параллельного порта. Так как все процессы работают в общем
пространстве процессов Linux, программист использует в своих программах
стандартные API-интерфейсы Linux и может переключаться из одного режима
в другой по событиям, либо в определенных местах программы. При
переключении в режим реального времени все процессы в системе "засыпают"
до момента освобождения ветви процесса реального времени. Это довольно
удобно при реализации задач с большим объемом вычислений, по своей сути
требующих механизмов реального времени, например в задачах обработки
аудио- и видеоинформации.
Стандартно такты планировщика RTMod задаются от системного таймера время переключения контекста задач реального времени (time slice) равно 10
мс. Используя же KURT совместно с UTIME, можно довести время
переключения контекста задач до 1 мс. Прерывания обрабатываются
стандартным для ОС Linux образом - через механизм драйверов.
API-интерфейс KURT разделяется на две части - прикладную и системную.
Прикладная часть позволяет программисту управлять поведением процессов, а
системный
API-интерфейс
предназначен
для
манипулирования
пользовательскими
процессами
и
программирования
собственных
планировщиков. Прикладная часть API-интерфейса KURT состоит всего из
четырех функций:
• set_rtparams позволяет добавить процесс в ядро с маской SCHED_KURT.
Только процессы, чья политика в планировщике установлена как
SCHED_KURT, смогут работать в режиме реального времени;
• get_num_rtprocs
получает идентификатор "rt_id" процесса из
планировщика реального времени RTMod;
• rt_suspend позволяет приостановить планировщик реального времени;
• get_rt_stats получает статус процесса из таблицы планировщика
процессов реального времени.
Простота использования KURT позволяет с максимальным комфортом
программировать задачи, требующие как функций реального времени, так и
всего многообразия API-интерфейса Unix. Использование "мягкого" реального
времени обычно подходит для реализации мультимедийных задач, а также при
обработке разного рода потоков информации, где критично время получения
результата. Совершенно другой подход применен при реализации в Linux
"жесткого" реального времени.
27
1.7.2.2. Проект RTLinux
RTLinux (http://luz.cs.nmt.edu/~rtlinux) - это дополнение к ядру Linux,
реализующее режим "жесткого" реального времени, которое позволяет
управлять различными чувствительными ко времени реакции системы
процессами. По сути дела, RTLinux - это операционная система, в которой
маленькое ядро реального времени сосуществует со стандартным POSIXядром Linux. Разработчики RTLinux пошли по пути запуска из ядра реального
времени Linux-ядра как задачи с наименьшим приоритетом. В RTLinux все
прерывания обрабатываются ядром реального времени, а в случае отсутствия
обработчика реального времени - передаются Linux-ядру. Фактически Linuxядро является простаивающей (idle) задачей операционной системы реального
времени, запускаемой только в том случае, если никакая задача реального
времени не исполняется. При этом на Linux-задачу накладываются
определенные ограничения, которые прозрачны для программиста. Нельзя
выполнять следующие операции: блокировать аппаратные прерывания и
предохранять себя от вытеснения другой задачей. Ключ к реализации данной
системы - драйвер, эмулирующий систему управления прерываниями, к
которому обращается Linux при попытке блокировать прерывания. В этом
случае драйвер перехватывает запрос, сохраняет его и возвращает управление
Linux-ядру. Все аппаратные прерывания перехватываются ядром
операционной системы реального времени. Когда происходит прерывание,
ядро RTLinux решает, что делать. Если это прерывание имеет отношение к
задаче реального времени, ядро вызывает соответствующий обработчик. В
противном случае, либо если обработчик реального времени говорит, что
хочет разделять это прерывание с Linux, обработчику присваивается
состояние ожидания (pending). Если Linux потребовала разрешить
прерывания, то прерывания, которые находятся в состоянии "pending",
эмулируются. Ядро RTLinux спроектировано таким образом, что ядру
реального времени никогда не приходится ожидать освобождения ресурса,
занятого Linux-процессом (Рисунок 1.3).
У с т р о й с т в а в в о д а /в ы в о д а
Я д р о R T L in u x
Я д р о L in u x
П р о ц е с с ы R T L in u x
F IF O -б у ф е р ы и р а з д е л я е м а я п а м я т ь
П р о ц е с с ы L in u x
Рисунок 1.3 Механизм работы RTLinux
28
•
•
Для обмена данными между операционными системами реального
времени и Linux могут использоваться следующие механизмы:
разделяемые области памяти;
псевдоустройства, предоставляющие возможность обмена данными с
приложениями реального времени.
Ключевой принцип построения RTLinux - как можно больше
использовать Linux и как можно меньше собственно RTLinux. Действительно,
именно Linux заботится об инициализации системы и устройств, а также о
динамическом выделении ресурсов. "На плечи" RTLinux ложится только
планирование задач реального времени и обработка прерываний. Для
простоты запуска в контексте ядра, сохранения модульности и расширяемости
системы процессы реального времени реализованы в виде загружаемых
модулей Linux. Как правило, приложение реального времени с RTLinux
состоит из двух независимых частей: процесса, исполняемого ядром RTLinux,
и обыкновенного Linux-приложения. Как и каждый модуль Linux-ядра,
процесс реального времени должен выполнить процедуры инициализации и
завершения аналогичные модулям Linux: Подобный модульный подход к
написанию приложений присущ многим расширениям реального времени для
универсальных систем, когда задача реального времени работает независимо
от ОС. Разработчики уже приняли схему, по которой задачи, критичные к
времени реакции, программируются с помощью API-интерфейсов,
предусмотренных расширением реального времени, а все служебные и
интерфейсные функции возлагаются на традиционную операционную
систему.
Глава 2. Программирование процессов
2.1. Создание процессов
Системный вызов fork() служит для создания нового процесса в
операционной системе UNIX. Процесс, который инициировал системный
вызов fork(), принято называть родительским процессом (parent process).
Вновь порожденный процесс принято называть процессом потомком (child
process). Процесс потомок является почти полной копией родительского
процесса(Рисунок 2.1). Процесс родитель и процесс потомок разделяют один и
тот же кодовый сегмент. Системный вызов fork() в случае успеха возвращает
родительскому процессу идентификатор потомка, а потомку 0. В ситуации,
когда процесс не может быть создан функция fork() возвращает -1.
29
Рисунок 2.1. Функция fork()
Обратите внимание: процесс может узнать свой собственный
идентификатор и идентификатор родительского процесса, используя
функции getpid() и getppid(). (см. Пример 2.1)
Пример 2.1 Системные вызовы для получения идентификаторов процесса
#include <stream.h>
#include <sys/types.h>
#include <unistd.h>
main ( )
{
cout << "Идентификатор текущего процесса PID: " << getpid() <<” \n”;
cout << " Идентификатор родительского процесса- PPID: " << getppid()<<” \n”;
cout << " Идентификатор группы процесса PGID: " << getpgrp()<<” \n”;
cout << "Идентификатор пользователя –real UID: " << getuid() <<” \n”;
cout << "Реальный идентификатор группы пользователя real -GID:" <<
getgid()<<” \n”;
cout << "Эффектифный идентификатор пользователя - UID: " << geteuid()<<”
\n”;
cout << "Эффектифный идентификатор группы пользователя GID: " <<
getegid()<<” \n”;
exit(0);
}
2.2. Завершение процессов
В системе UNIX процесс завершает свое выполнение, запуская
системную функцию exit. После этого процесс переходит в состояние
"прекращения существования" (см. Рисунок 1.1 Глава 1), освобождает ресурсы
и ликвидирует свой контекст.
30
Синтаксис вызова функции:
exit(status);
где status - значение, возвращаемое функцией родительскому процессу.
Процессы могут вызывать функцию exit как в явном, так и в неявном виде (по
окончании выполнения программы: начальная процедура (startup),
компонуемая со всеми программами на языке Си, вызывает функцию exit на
выходе программы из функции main, являющейся общей точкой входа для
всех программ). С другой стороны, ядро может вызывать функцию exit по
своей инициативе, если процесс не принял посланный ему сигнал (об этом мы
уже говорили выше). В этом случае значение параметра status равно номеру
сигнала.
Система не накладывает никакого ограничения на продолжительность
выполнения процесса, и зачастую процессы существуют в течение довольно
длительного времени. Нулевой процесс (программа подкачки) и процесс 1
(init), к примеру, существуют на протяжении всего времени жизни системы.
Продолжительными процессами являются также getty - процессы,
контролирующие работу терминальной линии, ожидая регистрации
пользователей, и процессы общего
назначения,
выполняемые под
руководством администратора.
Ядро сохраняет в таблице процессов код возврата функции exit
(status), а также суммарное время исполнения процесса и его потомков в
режиме ядра и режиме задачи. Ядро удаляет процесс из дерева процессов, а
его потомков передает процессу 1 (init). Таким образом, процесс init
становится законным родителем всех продолжающих существование
потомков завершающегося процесса. Если кто-либо из потомков прекращает
существование, завершающийся процесс посылает процессу init сигнал
"гибель потомка"(SIGCHLD) для того, чтобы процесс начальной загрузки мог
удалить запись о потомке из таблицы процессов; кроме того, завершающийся
процесс посылает этот сигнал своему родителю. В типичной ситуации
родительский процесс синхронизирует свое выполнение с завершающимся
потомком
с помощью системной функции wait(Рисунок 2.2) или
waitpid(Пример 2.2).
2.3. Ожидание завершения процесса-потомка
Синхронизация процесса-родителя с моментом завершения процессов
потомков осуществляется с помощью системных вызовов wait(Рисунок 2.2) и
waitpid(Пример 2.1). Синтаксис вызова функции wait:
int st;
pid = wait(&st);
где pid - значение кода идентификации (PID) прекратившего свое
существование потомка, st - адрес переменной целого типа, в которую будет
помещено возвращаемое функцией exit значение, в пространстве задачи.
31
Алгоритм выполнения функции wait можно описать примерно так - ядро
ведет поиск потомков процесса, прекративших существование, и в случае их
отсутствия возвращает ошибку. Если потомок, прекративший существование,
обнаружен, ядро передает его код идентификации и значение, возвращаемое
через параметр функции exit, процессу, вызвавшему функцию wait. Таким
образом, через параметр функции exit (status) завершающийся процесс может
передавать различные значения, в закодированном виде содержащие
информацию о причине завершения процесса, однако на практике этот
параметр используется по назначению довольно редко. Ядро передает в
соответствующие поля, принадлежащие
пространству
родительского
процесса, накопленные значения продолжительности исполнения процессапотомка в режиме ядра и в режиме задачи и, наконец, освобождает в таблице
процессов место, которое в ней
занимал
прежде
прекративший
существование процесс. Это место будет предоставлено новому процессу.
Если процесс, выполняющий функцию wait, имеет потомков, продолжающих
существование,
он приостанавливается до получения ожидаемого
сигнала(Рисунок 2.2). Ядро не возобновляет по своей инициативе процесс,
приостановившийся с помощью
функции wait: такой процесс может
возобновиться только в случае получения сигнала. На все сигналы, кроме
сигнала "гибель потомка"(сигнал SIGCHLD – ниже смотрите описание
сигнального механизма), процесс реагирует ранее рассмотренным образом.
Реакция процесса на сигнал "гибель потомка" проявляется по-разному в
зависимости от обстоятельств:
- По умолчанию (то есть если специально не оговорены никакие
другие действия) процесс выходит из состояния останова, в
которое он вошел с помощью функции wait. (Ядро освобождает
место в таблице процессов, занимаемое завершившимся
потомком).
- Если процесс определил реакцию на сигнал "гибель потомка", как
вызов функции, то ядро делает все необходимые установки для
запуска пользовательской функции обработки сигнала, как и в
случае поступления сигнала любого другого типа(смотрите
описание сигнального механизма).
- Если процесс игнорирует сигналы данного типа, ядро
перезапускает функцию wait, освобождает в таблице процессов
место, занимаемое потомками, прекратившими существование, и
исследует оставшихся потомков.
32
Рисунок 2.2. Пример использования функции wait
На рисунке 2.2 показан фрагмент кода и дерево процессов программы
которая будет выполняться в виде 4-х процессов(включая корневой) каждый
из которых выполняет функцию wait.
Обратите внимание:
функция wait не блокирует процесс, если у данного процесса нет
потомков (Рисунок 2.2);
при наличии у процесса нескольких потомков функция wait ждет
первого завершившегося потомка, после чего процесс родитель
продолжает свое выполнение.
В приведенном выше фрагменте кода имеется потенциальная ошибка –
корневому процессу не достаточно одной функции wait, поскольку он имеет
более одного потомка, что может привести к ситуации, когда один потомок
уже завершится, а второй еще продолжает свое выполнение тогда корневой
процесс дождавшийся завершения одного из потомков выполнит функцию
exit, что приведет к завершению всех потомков корневого процесса.
В следующем примере Пример 2.1 корневой процесс порождает процесспотомок и использует функцию waitpid для синхронизации своего
33
выполнения с моментом завершения процесса – потомка и обрабатывает
результат завершения потомка.
Пример 2.2 Вариант использование функции waitpid
#include <stream.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
main()
{
pid_t child_pid, pid;
int status;
if ((child_pid=fork())=0)
{
cout << "Запущен процесс потомок с идентификатором pid = " <<
getpid() << "\n";
exit(getpid()); /* завершение потомка */
}
/* выполняется корневой процесс */
pid = waitpid(child_pid,&status,WUNTRACED);
/* обработка результата завершения процесса потомка */
if (WIFEXITED(status))
cerr << " Потомок с идентификатором pid = " << child_pid << " вернул
код завершения = " << WEXITSTATUS(status) << "\n";
else if (WIFSTOPPED(status))
" Потомок с идентификатором pid = " << child_pid << " остановлен
сигналом " <<WSTOPSIG(status) << "\n";
else if (WIFSIGNALED(status))
" Потомок с идентификатором pid = " << child_pid << " убит
сигналом " <<WTERMSIG(status) << "\n";
else perror("waitpid");
exit(0);
}
Для получения более подробной информации о функциях используйте
универсальную справочную систему man - в командной строке наберите:
$man 2 Имя функции
Например
$man 2 waitpid
34
2.4. Замещение процессов
Замещение процессов осуществляется функциями группы execl(Пример
2.3)
Таблица 2.1 Формат функций замещения процессов
Пример 2.3 - Вызов функций группы exec
main()
{
char *const ps_argv[] = {ps, "-af", 0};
char *const ps_envp[] = {"PATH=/bin:/usr/bin/:/home/user/bin", 0};
//Возможные варианты вызова exec- функций, выполняющие запуск утилиты
// Будем запускать утилиту Linux ps с ключами af ( ps –af )
if (fork() = = 0)
execl("/bin/ps" , "ps", "-af", 0);
if (fork() = = 0)
execlp("ps" , "ps", "-af", 0);
if (fork() = = 0)
execle("/bin/ps" , "ps", "-af", 0, ps_envp);
if (fork() = = 0)
execv("/bin/ps", ps_argv);
if (fork() = = 0)
execvp("ps", ps_argv);
if (fork() = = 0)
execv("/bin/ps", ps_argv, ps_envp);
// запуск ps без ключей
if (fork() = = 0)
execlp("ps" , "ps", 0);
}
35
2.5. Полудуплексные каналы
Канал - это средство связи стандартного вывода одного процесса со
стандартным вводом другого. Каналы - это старейший из инструментов IPC,
существующий приблизительно со времени появления самых ранних версий
оперативной системы UNIX. Они предоставляют метод односторонних
коммуникаций (отсюда термин half-duplex - полудуплексные) между
процессами.
Этот механизм взаимодействия широко используется во всех
реализациях командных интерпретаторов Unix.
Рассмотрим реализацию конвейера команд в Unix
$ps –Af | grep mc
Вертикальная черта “|” между командами обозначает конвейер т.е. выход
одной команды переназначается на вход другой команды – таким образом в
приведенном выше примере результат выполнения ps –Af будет передан
команде grep которая произведет трансформацию входного потока в
соответствии с маской(в данном случае на выходе будут все строки
содержащие подстроку «mc»). Передача данных реализуется с помощью
полудуплексного канала.
Попробуем разобраться в организации такого конвейера на уровне программы
и ядра системы.
Когда процесс создает канал, ядро устанавливает два файловых дескриптора
для пользования этим каналом. Один такой дескриптор используется, чтобы
открыть путь ввода в канал (запись), в то время как другой применяется для
получения данных из канала (чтение).
Если процесс посылает данные через канал (fifo0), он имеет возможность
получить эту информацию из fifo1. Хотя канал первоначально связывает
процесс с самим собой, данные, идущие через канал, проходят через ядро. В
частности, в Linux-е каналы внутренне представлены корректным inodeом(индексным дескриптором). Конечно, этот inode существует в пределах
самого ядра, а не в какой-либо физической файловой системе. Эта
особенность открывает некоторые возможности для организации
ввода/вывода которые мы рассмотрим. Предположим нам необходимо создать
программу создающую одного потомка, который будет обмениваться
данными с родительским процессом. Что для этого нужно – родительский
процесс создает канал в результате чего создаются два файловых дескриптора
36
fifo0 и fifo1, далее порождается потомок. Процесс потомок наследует
открытые файловые дескрипторы от родительского процесса и мы получаем
базу для мультипроцессной коммуникации (между родителем и потомком).
in <----Процесс родитель
out ----->
-----> in
Ядро системы
<----- out
Процесс потомок
И так теперь оба процесса имеют доступ к файловым дескрипторам,
которые основывают канал. На этой стадии должно быть принято решение. В
каком направлении мы хотим передавать данные? Потомок посылает
информацию к родителю или наоборот? Два процесса взаимно
согласовываются и "закрывают" неиспользуемый конец канала. Пусть
потомок выполняет несколько действий и посылает информацию к родителю
обратно через канал, тогда схема взаимодействия будет выглядеть примерно
так:
in
Процесс родитель
out
<----Ядро системы
<-----
in
Процесс потомок
out
Чтобы послать данные в канал, можно воспользоваться системным
вызовом write(), а чтобы получить данные из канала - read().
Что делает интерпретатор, когда встречает конвейер команд – в нашем
примере процесс shell создает полудуплексный канал, затем создает двух
потомков, каждый из которых наследует файловые дескрипторы созданного
канала. Далее первый потомок переназначает свой вывод в канал и замещается
утилитой “ps –Af”, второй потомок переназначает ввод из канала и замещается
утилитой “grep mc”.
Создание канала
Канал создается с помощью системного вызова pipe(). Для него
требуется единственный аргумент, который является массивом из двух целых
(integer), и, в случае успеха, массив будет содержать два новых файловых
дескриптора, которые будут использованы для канала.
Системный вызов: pipe();
PROTOTYPE: int pipe( int fd[2] );
Возвращает:
0 в случае успеха
37
-1 в случае ошибки:
errno = EMFILE (нет свободных дескрипторов)
EMFILE (системная файловая таблица переполнена)
EFAULT (массив fd некорректен)
Замечания: fd[0] устанавливается для чтения, fd[1] - для записи.
Попробуем написать приблизительный фрагмент программы, реализующей
конвейер команд для нашего примера (Пример 2.4).
Пример 2.4 - Вызов функций pipe и dup2
main()
{
int fifo[2],st;
pipe(fifo);
if (fork() = = 0)
{
// Переназначаем стандартный вывод в канал
if (dup2(fifo[1],STDOUT_FILENO)==-1)
perror("dup2"), exit(1);
// и замещаем процесс утилитой ps
execlp("ps" , "ps", "-Af", 0);
}
// создаем второго потомка
if (fork() = = 0)
{
// Переназначаем стандартный ввод из канала
if (dup2(fifo[0],STDIN_FILENO)==-1)
perror("dup2"), exit(1);
// и замещаем процесс утилитой grep
execlp("grep" , "grep", "mc", 0);
}
// дожидаемся завершения обоих потомков
wait(&st);
wait(&st);
//…
//…
//…
}
38
2.6. Сигнальный механизм
Сигналы являются программными прерываниями, которые посылаются
процессу, когда случается некоторое событие. Сигналы могут возникать
синхронно с ошибкой в приложении, например SIGFPE(ошибка вычислений с
плавающей запятой) и SIGSEGV(ошибка адресации), но большинство
сигналов является асинхронными. Сигналы могут посылаться процессу, когда
система обнаруживает программное событие, например, когда пользователь
дает команду прервать или остановить выполнение, или сигнал на завершение
от другого процесса. Сигналы могут прийти непосредственно от ядра ОС,
когда возникает сбой аппаратных средств ЭВМ.
Система определяет набор сигналов, которые могут быть отправлены
процессу. В Linux существует примерно 30 различных сигналов. При этом
каждый сигнал имеет целочисленное значение и приводит к строго
определенным действиям.
Для получения
информации о сигналах можно воспользоваться
следующими командами:
$kill –l
Выдаст вам список доступных в системе сигналов. Однако данная
команда более широко используется для посылки сигнала процессам.
Попробуйте набрать kill -9 PID, PID где PID номер вашего процесса и
посмотрите что произойдет.
Формат команды и описание ключей можно узнать, набрав $man kill
$man 7 signal покажет вам справочную информацию – описание
каждого из используемых сигналов.
2.6.1. Механизм передачи сигналов
Механизм передачи сигналов состоит из следующих частей:
• Установление и обозначение сигналов в форме целочисленных
значений
• Маркер в строке таблицы процессов для прибывших сигналов
• Таблица с адресами функций, которые определяют реакцию на
прибывающие сигналы.
Отдельные сигналы разделяются на три различных класса:
• Системные сигналы (ошибка аппаратуры, системная ошибка и т.д.)
• Сигналы от устройств
• Определенные пользователем сигналы
Как только сигнал приходит, он отмечается записью в таблице
процессов. Если этот сигнал определен для процесса, то по таблице указателей
функций в специальной структуре Task определяется, как нужно реагировать
на этот сигнал. При этом номер сигнала служит индексом таблицы.
Существует три основных варианта реакции на сигналы:
39
вызов собственной функции обработки;
• игнорирование сигнала (не работает для SIGKILL);
• использование предварительно установленной функции обработки
по умолчанию.
Чтобы реагировать на те или другие сигналы, необходимо понимать
концепции обработки сигнала. Процесс должен организовать так называемый
обработчик сигнала. Эти обработчики срабатывают в случае прихода сигнала.
Посылка сигналов от процессов процессам осуществляется функцией
kill(pid, signo), где pid – идентификатор процесса, которому посылается
сигнал, signo – номер сигнала.
В настоящее время
существует 2 класса сигналов: ненадежные
(unreliable) и надежные (reliable). Ненадежные сигналы - это те, для которых
вызванный однажды обработчик сигнала не остается. Такие "сигналывыстрелы" должны перезапускать обработчик внутри самого обработчика,
если есть желание сохранить сигнал действующим. Из-за этого возможна
ситуация гонок, в которой сигнал может прийти до перезапуска обработчика и тогда он будет потерян, или придет вовремя - и тогда сработает в
соответствии с заданным поведением (например, убьет процесс). Такие
сигналы ненадежны, поскольку отлов сигнала и установка обработчика не
являются атомарными операциями.
•
2.6.2. Ненадежные сигналы
Концепция сигналов имеет несколько аспектов, связанных с тем, каким
образом ядро посылает сигнал процессу, каким образом процесс обрабатывает
сигнал и управляет реакцией на него. Посылая сигнал процессу, ядро
устанавливает в единицу разряд в поле сигнала записи таблицы процессов,
соответствующий типу сигнала. Если процесс находится в состоянии
приостанова с приоритетом, допускающим прерывания, ядро возобновит его
выполнение. На этом роль отправителя сигнала (процесса или ядра)
исчерпывается. Процесс может запоминать сигналы различных типов, но не
имеет возможности запоминать количество получаемых сигналов каждого
типа. Например, если процесс получает сигнал о "зависании" или об удалении
процесса из системы, он устанавливает в единицу соответствующие разряды
в
поле сигналов таблицы процессов, но не может сказать, сколько
экземпляров сигнала каждого типа он получил.
Ядро проверяет получение сигнала, когда процесс собирается перейти
из режима ядра в режим задачи, а также когда он переходит в состояние
приостанова или выходит из этого состояния с достаточно низким
приоритетом планирования. Ядро обрабатывает сигналы только тогда, когда
процесс возвращается из режима ядра в режим задачи. Таким образом,
сигнал не оказывает немедленного воздействия на поведение процесса,
исполняемого в режиме ядра. Если процесс исполняется в режиме задачи, а
ядро тем временем обрабатывает прерывание, послужившее поводом для
40
посылки процессу сигнала, ядро распознает и обработает сигнал по выходе из
прерывания. Таким образом, процесс не будет исполняться в режиме задачи,
пока какие-то сигналы остаются необработанными.
Для переопределения реакции на сигнал в механизме ненадежных
сигналов используется функция signal() Пример 2.5.
Описание функции signal()
#include <signal.h>
void(*signal(int signr, void(*sighandler)(int)))(int);
Такой прототип очень сложен для понимания. Следует упростить его,
определив тип для функции обработки.
typedef void signalfunction(int);
После этого прототип функции примет вид:
signalfunction *signal(int signr,signalfunction*sighandler);
signr устанавливает номер сигнала, для которого устанавливается
обработчик.
Переменная sighandler определяет функцию обработки сигнала. В
заголовочном файле <signal.h> определены две константы SIG_DFL и
SIG_IGN. SIG_DFL означает выполнение действий по умолчанию - в
большинстве случаев окончание процесса. Например, определение:
signal(SIGINT, SIG_DFL);
приведет к тому, что при нажатии на комбинацию клавиш CTRL+C во время
выполнения сработает реакция по умолчанию на сигнал SIGINT и программа
завершится.
С другой стороны, можно определить
signal(SIGINT, SIG_IGN);
Если теперь нажать на комбинацию клавиш CTRL+C, ничего не произойдет,
так как сигнал SIGINT игнорируется. Некоторые сигналы (например SIGKILL)
невозможно перехватить или проигнорировать смотри $man 7 signal.
Третьим способом является перехват сигнала SIGINT и передача управления
на адрес собственной функции, которая должна выполнять действия, если
была нажата комбинация клавиш CTRL+C, например:
signal(SIGINT, function);
Пример 2.5 Использование механизма ненадежных сигналов
void sig_handler(int signum)
{
signal(SIGUSR1, sig_handler);
cout << "Процесс с идентификатором PID = " << getpid() << " получил сигнал
" << signum << "\n";
}
main()
{
41
alarm(30);
// переопределяем реакцию на сигнал SIGUSR1, как вызов функции sig_handler
signal(SIGUSR1, sig_handler);
/* создаем потомка, который засыпает на 1 секунду(функция sleep(1)), затем
посылает сигнал SIGUSR1 своему родителю и так в бесконечном цикле
*/
if (fork()==0)
{
while(true)
{
sleep(1);
kill(getppid(),SIGUSR1);
}
}
while(true) pause();
exit(0);
}
Функция pause() – приостанавливает процесс до прихода сигнала, функция
alarm(30) сообщает системе, что через 30 секунд необходимо послать сигнал
SIGALRM процессу, вызвавшему данную функцию. Вы уже должны
догадаться, что произойдет с родительским процессом через 30 сек.
2.6.3. Надежные сигналы
Семантика надежных сигналов оставляет обработчик установленным и
ситуация гонок при перезапуске избегается. В то же время определенные
сигналы могут быть запущены заново, а атомарная операция паузы доступна
через функцию POSIX sigsuspend.
В UNIX стандарта POSIX для управления сигналами есть вызовы sigaction,
sigprocmask, sigpending и sigsuspend все сигналы надежные. Указанные
функции работают с надежными сигналами, но перезапуск системных вызовов
не определен совсем. Если sigaction используется в BSD OC или OC SVR4, то
перезапуск системных вызовов по умолчанию отключен. Но он может
включаться поднятием флага SA_RESTART.
Лучший путь работы с сигналами - это sigaction, которая позволит точно
определить поведение обработчиков сигналов Пример 2.5 и Пример 2.6.
Однако signal до сих пор используется во многих приложениях и имеет
различную семантику в BSD и SVR4.
В Linux определены следующие значения sa_flags структуры sigaction:
• SA_NOCLDSTOP: Не посылайте сигнал гибель потомка(SIGCHLD) во
время остановки процесса-потомка.
• SA_RESTART: Осуществляет перезапуск определенных системных
вызовов во время прерывания обработчиком сигналов.
42
•
•
•
•
•
SA_NOMASK: Обнуление маски сигнала (которое блокирует сигналы во
время работы обработчика сигналов).
SA_ONESHOT: Очищает обработчик сигналов после исполнения.
SA_INTERRUPT: Определен под Linux-ом, но не используется. Под
SunOS системные вызовы автоматически перезапускались, а этот флаг
отменял такое поведение.
SA_STACK: В настоящее время не работает; предназначен для стеков
сигналов
SA_SIGINFO: Получение расширенной информации о приходящих
сигналах .
Функция signal в Linux-е эквивалентна применению sigaction с опциями
SA_ONESHOT и SA_NOMASK, что соответствует классической ненадежной
семантике сигналов.
Пример 2.6 Использование механизма надежных сигналов
void sig_handler(int signum)
{
//..задайте действие..
};
main()
{
int pid, child_pid, i, status;
if ((child_pid =fork()) = = 0)
{
// создаем потомка в котором переопределяем реакцию на сигнал
SIGUSR1
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sig_handler;
sigaction(SIGUSR1,& act,0); // Зададим реакцию на приход сигнала
SIGUSR1
// - вызов функции sig_handler
while (true) pause();
}
sleep(1);
// посылаем потомку сигнал, который у него переопределен
kill(child_pid, SIGUSR1);
// и который не переопределен
kill(child_pid, SIGUSR2);
// смотрим что получилось воспользовавшись следующим кодом - смотри
Пример 2.1
43
pid = waitpid(child_pid,&status,WUNTRACED);
if (WIFSIGNALED(status))
cout << " Потомок с идентификатором pid = " << child_pid << " убит сигналом
" << WTERMSIG(status) << "\n";
exit(0);
}
Пример 2.7 Получение расширенной информации о сигналах в механизме
надежных сигналов
static void sig_handler (int signo, siginfo_t* siginfo,void *)
{
cout << "Процесс " << getpid() << " получил сигнал " << signo << " от процесса
" << siginfo->si_pid << "\n";
}
main()
{
int st;
pid_t pid;
// переопределяем реакцию на сигнал SIGUSR1
struct sigaction act,oact;
// обратите на отличие от примера 2.5
act.sa_sigaction= sig_handler;
sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO | SA_RESTART;
sigaction(SIGUSR1, &act,&oact);
pid=getpid();
cout << "Старт корневого процесса " << getpid() << "\n";
fork();
fork();
if (getpid()= =pid)
{
while(true)
{
if ( wait(&st)<0 ) exit(0);
pause();
cout << "процесс " << getpid() << " приостановлен...\n";
}
}
sleep(1);
kill(pid, SIGUSR1);
cout << "Завершен процесс " << getpid() << "\n";
exit(0);
}
Замечание. Попробуйте убрать бесконечный цикл и сравните результат.
44
2.7. Взаимодействие процессов
Как уже было сказано выше каждый процесс в системе выполняется в
собственной виртуальной памяти, т.е. если не предпринимать дополнительных
усилий, то даже родственные процессы, образованные в результате
выполнения системного вызова fork(), на самом деле полностью изолированы
один от другого (если не считать того, что процесс-потомок наследует от
процесса-родителя сегмент данных и все открытые файлы). Тем самым, в
ранних вариантах ОС UNIX, поддерживались весьма слабые возможности
взаимодействия даже для процессов имеющих общего предка). Слабые
средства поддерживались и для взаимной синхронизации процессов - почти
все ограничивалось возможностью реакции на сигналы, и наиболее
распространенным видом синхронизации являлась реакция процесса-предка
на сигнал о завершении процесса-потомка (SIGCHLD).
По-видимому, применение такого подхода являлось реакцией на
чрезмерно сложные механизмы взаимодействия и синхронизации
параллельных процессов, существовавшие в исторически предшествующей
UNIX ОС Multics(В ОС Multics поддерживалась сегментно-страничная
организация виртуальной памяти, и в общей виртуальной памяти могло
выполняться несколько параллельных процессов, которые, естественно, могли
взаимодействовать через общую память. За счет возможности включения
одного и того же сегмента в разную виртуальную память аналогичная
возможность взаимодействий существовала и для процессов, выполняемых не
в общей виртуальной памяти. Для синхронизации таких взаимодействий
процессов поддерживался общий механизм семафоров, позволяющий, в
частности, организовывать взаимное исключение процессов в критических
участках их выполнения (например, при взаимно-исключающем доступе к
разделяемой памяти). Этот стиль параллельного программирования в
принципе обеспечивает большую гибкость и эффективность, но является
очень трудным для использования. Часто в программах появляются трудно
обнаруживаемые и редко воспроизводимые синхронизационные ошибки;
использование явной синхронизации, не связанной неразрывно с теми
объектами, доступ к которым синхронизуется, делает логику программ трудно
постижимой, а текст программ - трудно читаемым.)
Понятно, что стиль ранних вариантов ОС UNIX стимулировал
существенно более простое программирование. В наиболее простых случаях
процесс-потомок образовывался только для того, чтобы асинхронно с
основным процессом выполнить какое-либо простое действие (например,
запись в файл). В более сложных случаях процессы, связанные иерархией
родства, создавали обрабатывающие "конвейеры" с использованием техники
программных каналов (pipe).
Постепенное распространение системы и сравнительная простота
наращивания ее возможностей привели к тому, что со временем в разных
вариантах UNIX-о подобных ОС в совокупности появился явно избыточный
45
набор системных средств, предназначенных для обеспечения возможности
взаимодействия и синхронизации процессов (эти средства получили название
IPC от Inter-Process Communication Facilities). С появлением UNIX System V
Release 4.0 (и более старшей версии 4.2) все эти средства были узаконены и
вошли в фактический стандарт ОС UNIX современного образца.
Пакет средств IPC, которые появились в UNIX System V Release 3.0. включает
в себя:
• shared memory segments (сегменты разделяемой памяти) - средства,
обеспечивающие возможность разделения общей для процессов памяти;
• семафоры (semaphores) - средства, обеспечивающие возможность
синхронизации процессов при доступе к совместно используемым
ресурсам;
• очереди сообщений(message queues) - средства, обеспечивающие
возможность посылки процессом сообщений другому произвольному
процессу.
• Эти механизмы объединяются в единый пакет, потому что
соответствующие системные вызовы обладают близкими интерфейсами,
а в их реализации используются многие общие подпрограммы. Вот
основные общие свойства всех трех механизмов:
• Для каждого механизма поддерживается общесистемная таблица,
элементы которой описывают всех существующих в данный момент
представителей механизма (конкретные сегменты, семафоры или
очереди сообщений).
• Элемент таблицы содержит некоторый числовой ключ, который
является
выбранным
пользователем
именем
представителя
соответствующего механизма. Другими словами, чтобы два или более
процесса могли использовать некоторый механизм, они должны заранее
договориться об именовании используемого представителя этого
механизма и добиться того, чтобы тот же представитель не
использовался другими процессами.
• Процесс, желающий начать пользоваться одним из механизмов,
обращается к системе с системным вызовом из семейства "get"(shmget,
semget, msgget), прямыми параметрами которого является ключ объекта
и дополнительные флаги, а ответным параметром является числовой
дескриптор, используемый в дальнейших системных вызовах подобно
тому, как используется дескриптор файла при работе с файловой
системой. Допускается использование специального значения ключа с
символическим именем IPC_PRIVATE, обязывающего систему
выделить новый элемент в таблице соответствующего механизма
независимо от наличия или отсутствия в ней элемента, содержащего то
же значение ключа. При указании других значений ключа задание флага
IPC_CREAT приводит к образованию нового элемента таблицы, если в
таблице отсутствует элемент с указанным значением ключа, или
46
нахождению элемента с этим значением ключа. Комбинация флагов
IPC_CREAT и IPC_EXCL приводит к выдаче диагностики об
ошибочной ситуации, если в таблице уже содержится элемент с
указанным значением ключа.
• Защита доступа к ранее созданным элементам таблицы каждого
механизма основывается на тех же принципах, что и защита доступа к
файлам.
Рассмотрим детально механизмы этого семейства.
2.7.1. Разделяемая память
Для работы с разделяемой памятью используются четыре системных вызова:
• shmget создает новый сегмент разделяемой памяти или находит
существующий сегмент с тем же ключом;
• shmat подключает сегмент с указанным дескриптором к виртуальной
памяти обращающегося процесса;
• shmdt отключает от виртуальной памяти ранее подключенный к ней
сегмент с указанным виртуальным адресом начала;
• shmctl служит для управления разнообразными параметрами,
связанными с существующим сегментом.
После того, как сегмент разделяемой памяти подключен к виртуальной
памяти процесса, этот процесс может обращаться к соответствующим
элементам памяти с использованием обычных машинных команд чтения и
записи, не прибегая к использованию дополнительных системных вызовов.
Синтаксис системного вызова shmget выглядит следующим образом:
shmid = shmget(key, size, flag);
Параметр size определяет желаемый размер сегмента в байтах. Далее
работа происходит по общим правилам. Если в таблице разделяемой памяти
находится элемент, содержащий заданный ключ, и права доступа не
противоречат текущим характеристикам обращающегося процесса, то
значением системного вызова является дескриптор существующего сегмента
(и обратившийся процесс так и не узнает реального размера сегмента, хотя
впоследствии его все-таки можно узнать с помощью системного вызова
shmctl). В противном случае создается новый сегмент с размером не меньше
установленного в системе минимального размера сегмента разделяемой
памяти и не больше установленного максимального размера. Создание
сегмента не означает немедленного выделения под него основной памяти. Это
действие откладывается до выполнения первого системного вызова
подключения сегмента к виртуальной памяти некоторого процесса.
Аналогично, при выполнении последнего системного вызова отключения
сегмента от виртуальной памяти соответствующая основная память
освобождается.
Подключение сегмента к виртуальной памяти выполняется путем
обращения к системному вызову shmat:
virtaddr = shmat(id, addr, flags);
47
Здесь id - это ранее полученный дескриптор сегмента, а addr - желаемый
процессом виртуальный адрес, который должен соответствовать началу
сегмента в виртуальной памяти. Значением системного вызова является
реальный виртуальный адрес начала сегмента (его значение не обязательно
совпадает со значением прямого параметра addr). Если значением addr
является нуль, ядро выбирает наиболее удобный виртуальный адрес начала
сегмента. Кроме того, ядро старается обеспечить (но не гарантирует) выбор
такого стартового виртуального адреса сегмента, который обеспечивал бы
отсутствие перекрывающихся виртуальных адресов данного разделяемого
сегмента, сегмента данных и сегмента стека процесса (два последних сегмента
могут расширяться).
Для отключения сегмента от виртуальной памяти используется
системный вызов shmdt:
shmdt(addr); где addr - это виртуальный адрес начала сегмента в
виртуальной памяти, ранее полученный от системного вызова shmat.
Естественно, система гарантирует (на основе использования таблицы
сегментов процесса), что указанный виртуальный адрес действительно
является адресом начала (разделяемого) сегмента в виртуальной памяти
данного процесса.
Системный вызов shmctl:
shmctl(id, cmd, shsstatbuf); содержит прямой параметр cmd,
идентифицирующий требуемое конкретное действие, и предназначен для
выполнения различных функций. Видимо, наиболее важной является функция
уничтожения сегмента разделяемой памяти. Уничтожение сегмента
производится следующим образом. Если к моменту выполнения системного
вызова ни один процесс не подключил сегмент к своей виртуальной памяти,
то
основная
память,
занимаемая
сегментом,
освобождается,
а
соответствующий элемент таблицы разделяемых сегментов объявляется
свободным. В противном случае в элементе таблицы сегментов выставляется
флаг, запрещающий выполнение системного вызова shmget по отношению к
этому сегменту, но процессам, успевшим получить дескриптор сегмента, попрежнему разрешается подключать сегмент к своей виртуальной памяти. При
выполнении последнего системного вызова отключения сегмента от
виртуальной памяти операция уничтожения сегмента завершается.
2.7.2. Семафоры
Механизм семафоров, реализованный в ОС UNIX, является обобщением
классического механизма семафоров общего вида, предложенного более 25
лет тому назад известным голландским специалистом профессором
Дейкстрой. Заметим, что целесообразность введения такого обобщения
достаточно сомнительна. Обычно наоборот использовался облегченный
вариант семафоров Дейкстры - так называемые двоичные семафоры. Мы не
будем здесь углубляться в общую теорию синхронизации на основе
48
семафоров, но заметим, что достаточность в общем случае двоичных
семафоров доказана (известен алгоритм реализации семафоров общего вида на
основе двоичных). Конечно, аналогичные рассуждения можно было бы
применить и к варианту семафоров, примененному в ОС UNIX.
Семафор в ОС UNIX состоит из следующих элементов:
• значение семафора;
• идентификатор процесса, который хронологически последним работал с
семафором;
• число процессов, ожидающих увеличения значения семафора;
• число процессов, ожидающих нулевого значения семафора.
Для работы с семафорами поддерживаются три системных вызова:
• semget для создания и получения доступа к набору семафоров;
• semop для манипулирования значениями семафоров (это именно тот
системный вызов, который позволяет процессам синхронизоваться на
основе использования семафоров);
• semctl для выполнения разнообразных управляющих операций над
набором семафоров.
Системный вызов semget имеет следующий синтаксис:
id = semget(key, count, flag);
где прямые параметры key и flag и возвращаемое значение системного
вызова имеют тот же смысл, что для других системных вызовов семейства
"get", а параметр count задает число семафоров в наборе семафоров,
обладающих одним и тем же ключом. После этого индивидуальный семафор
идентифицируется дескриптором набора семафоров и номером семафора в
этом наборе. Если к моменту выполнения системного вызова semget набор
семафоров с указанным ключом уже существует, то обращающийся процесс
получит соответствующий дескриптор, но так и не узнает о реальном числе
семафоров в группе (хотя позже это все-таки можно узнать с помощью
системного вызова semctl).
Основным системным вызовом для манипулирования семафором
является semop:
oldval = semop(id, oplist, count);
где id - это ранее полученный дескриптор группы семафоров, oplist массив описателей операций над семафорами группы, а count - размер этого
массива. Значение, возвращаемое системным вызовом, является значением
последнего обработанного семафора. Каждый элемент массива oplist имеет
следующую структуру:
• номер семафора в указанном наборе семафоров;
• операция;
• флаги.
Если проверка прав доступа проходит нормально, и указанные в массиве
oplist номера семафоров не выходят за пределы общего размера набора
семафоров, то системный вызов выполняется следующим образом. Для
49
каждого элемента массива oplist значение соответствующего семафора
изменяется в соответствии со значением поля "операция".
• Если значение поля операции положительно, то значение семафора
увеличивается на единицу, а все процессы, ожидающие увеличения
значения семафора, активизируются (пробуждаются в терминологии
UNIX).
• Если значение поля операции равно нулю, то если значение семафора
также равно нулю, выбирается следующий элемент массива oplist. Если
же значение семафора отлично от нуля, то ядро увеличивает на единицу
число процессов, ожидающих нулевого значения семафора, а
обратившийся процесс переводится в состояние ожидания (усыпляется в
терминологии UNIX).
• Наконец, если значение поля операции отрицательно, и его абсолютное
значение меньше или равно значению семафора, то ядро прибавляет это
отрицательное значение к значению семафора. Если в результате
значение семафора стало нулевым, то ядро активизирует (пробуждает)
все процессы, ожидающие нулевого значения этого семафора. Если же
значение семафора меньше абсолютной величины поля операции, то
ядро увеличивает на единицу число процессов, ожидающих увеличения
значения семафора и откладывает (усыпляет) текущий процесс до
наступления этого события.
Основным поводом для введения массовых операций над семафорами
было стремление дать программистам возможность избегать тупиковых
ситуаций в связи с семафорной синхронизацией. Это обеспечивается тем, что
системный вызов semop, каким бы длинным он не был (по причине
потенциально неограниченной длины массива oplist) выполняется как
атомарная операция, т.е. во время выполнения semop ни один другой процесс
не может изменить значение какого-либо семафора.
Наконец, среди флагов-параметров системного вызова semop может
содержаться флаг с символическим именем IPC_NOWAIT, наличие которого
заставляет ядро ОС UNIX не блокировать текущий процесс, а лишь сообщать
в ответных параметрах о возникновении ситуации, приведшей бы к
блокированию процесса при отсутствии флага IPC_NOWAIT. Мы не будем
обсуждать здесь возможности корректного завершения работы с семафорами
при незапланированном завершении процесса; заметим только, что такие
возможности обеспечиваются.
Системный вызов semctl имеет формат
semctl(id, number, cmd, arg);
где id - это дескриптор группы семафоров, number - номер семафора в
группе, cmd - код операции, а arg - указатель на структуру, содержимое
которой интерпретируется по-разному, в зависимости от операции. В
частности, с помощью semctl можно уничтожить индивидуальный семафор в
указанной группе.
50
Напоминание. Для получения детальной информации по всем
системным вызовам и их форматам воспользуйтесь утилитой man.
Например $man 2 semctl
Прмер2.8 Работа с семафором
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stream.h>
#include <signal.h>
#define key = 75
struct sembuf P_op, V_op;
int Sem_Id;
void P()
{
P_op.sem_num=0;
P_op.sem_op=-1;
P_op.sem_flg=SEM_UNDO;
semop(Sem_Id,&P_op,1);
};
void V() {
V_op.sem_num=0;
V_op.sem_op=1;
V_op.sem_flg=SEM_UNDO;
semop(Sem_Id,&V_op,1);
};
main()
{
int i, pid, Sem_Init[0];
pid=getpid();
signal(SIGCHLD, SIG_IGN);
if ( (Sem_Id=semget(key,1,0666 | IPC_CREAT) )!=0)
{ perror(«semget Error»); exit(1); }
Sem_Init[0]=1;
semctl(Sem_Id,0,SETALL,Sem_Init);
for(i=1;i<=3;i++)
fork();
//Захватить семафор
P();
51
cout << “Значение семафора = ” << semctl(Sem_Id,0,GETVAL)
<< “\n”;
cout << “Количество процессов блокированных на семафоре - ” <<
semctl(Sem_Id,0,GETNCNT) << “\n”;
cout << “Количество процессов блокированных на семафоре - ” <<
semctl(Sem_Id,0,GETNCNT) << “\n”;
// выполнение действий с общим ресурсом
...
// Освободить семафор
V();
wait(&st);
/* Удаление очереди сообщений корневым процессом после окончания работы
всех процессов-потомков */
if (pid==getpid())
semctl(Sem_Id, 0, IPC_RMID);
exit(0);
}
2.7.3. Очереди сообщений
Для обеспечения возможности обмена сообщениями между процессами
этот механизм поддерживается следующими системными вызовами:
• msgget для образования новой очереди сообщений или получения
дескриптора существующей очереди;
• msgsnd для посылки сообщения (вернее, для его постановки в
указанную очередь сообщений);
• msgrcv для приема сообщения (вернее, для выборки сообщения из
очереди сообщений);
• msgctl для выполнения ряда управляющих действий.
Системный вызов msgget обладает стандартным для семейства "get"
системных вызовов синтаксисом:
msgqid = msgget(key, flag);
Ядро хранит сообщения в виде связного списка (очереди), а дескриптор
очереди сообщений является индексом в массиве заголовков очередей
сообщений. В дополнение к информации, общей для всех механизмов IPC в
UNIX System V, в заголовке очереди хранятся также:
• указатели на первое и последнее сообщение в данной очереди;
• число сообщений и общее количество байтов данных во всех них вместе
взятых;
• идентификаторы процессов, которые последними послали или приняли
сообщение через данную очередь;
• временные метки последних выполненных операций msgsnd, msgrsv и
msgctl.
52
Как обычно, при выполнении системного вызова msgget ядро ОС UNIX
либо создает новую очередь сообщений, помещая ее заголовок в таблицу
очередей сообщений и возвращая пользователю дескриптор вновь созданной
очереди, либо находит элемент таблицы очередей сообщений, содержащий
указанный ключ, и возвращает соответствующий дескриптор очереди. На
рисунке показаны структуры данных, используемые для организации очередей
сообщений.
Для посылки сообщения используется системный вызов msgsnd:
msgsnd(msgqid, msg, count, flag);
где msg - это указатель на структуру, содержащую определяемый
пользователем целочисленный тип сообщения и символьный массив собственно сообщение; count задает размер сообщения в байтах, а flag
определяет действия ядра при выходе за пределы допустимых размеров
внутренней буферной памяти.
Для того, чтобы ядро успешно поставило указанное сообщение в
указанную очередь сообщений, должны быть выполнены следующие условия:
обращающийся процесс должен иметь соответствующие права по записи в
данную очередь сообщений; длина сообщения не должна превосходить
установленный в системе верхний предел; общая длина сообщений (включая
вновь посылаемое) не должна превосходить установленный предел;
указанный в сообщении тип сообщения должен быть положительным целым
числом. В этом случае обратившийся процесс успешно продолжает свое
выполнение, оставив отправленное сообщение в буфере очереди сообщений.
Тогда ядро активизирует (пробуждает) все процессы, ожидающие
поступления сообщений из данной очереди.
Если же оказывается, что новое сообщение невозможно буферизовать в
ядре по причине превышения верхнего предела суммарной длины сообщений,
находящихся в одной очереди сообщений, то обратившийся процесс
откладывается (усыпляется) до тех пор, пока очередь сообщений не
разгрузится процессами, ожидающими получения сообщений. Чтобы избежать
такого откладывания, обращающийся процесс должен указать в числе
параметров системного вызова msgsnd значение флага с символическим
именем IPC_NOWAIT (как в случае использования семафоров), чтобы ядро
выдало свидетельствующий об ошибке код возврата системного вызова
mgdsng в случае невозможности включить сообщение в указанную очередь.
Для приема сообщения используется системный вызов msgrcv:
count = msgrcv(id, msg, maxcount, type, flag);
Здесь msg - это указатель на структуру данных в адресном пространстве
пользователя, предназначенную для размещения принятого сообщения;
maxcount задает размер области данных (массива байтов) в структуре msg;
значение type специфицирует тип сообщения, которое желательно принять;
значение параметра flag указывает ядру, что следует предпринять, если в
указанной очереди сообщений отсутствует сообщение с указанным типом.
53
Возвращаемое значение системного вызова задает реальное число байтов,
переданных пользователю.
Выполнение системного вызова, как обычно, начинается с проверки
правомочности доступа обращающегося процесса к указанной очереди. Далее,
если значением параметра type является нуль, ядро выбирает первое
сообщение из указанной очереди сообщений и копирует его в заданную
пользовательскую структуру данных. После этого корректируется
информация, содержащаяся в заголовке очереди (число сообщений,
суммарный размер и т.д.). Если какие-либо процессы были отложены по
причине переполнения очереди сообщений, то все они активизируются. В
случае, если значение параметра maxcount оказывается меньше реального
размера сообщения, ядро не удаляет сообщение из очереди и возвращает код
ошибки. Однако, если задан флаг MSG_NOERROR, то выборка сообщения
производится, и в буфер пользователя переписываются первые maxcount
байтов сообщения.
Путем задания соответствующего значения параметра type
пользовательский процесс может потребовать выборки сообщения некоторого
конкретного типа. Если это значение является положительным целым числом,
ядро выбирает из очереди сообщений первое сообщение с таким же типом.
Если же значение параметра type есть отрицательное целое число, то ядро
выбирает из очереди первое сообщение, значение типа которого меньше или
равно абсолютному значению параметра type.
Во всех случаях, если в указанной очереди отсутствуют сообщения,
соответствующие спецификации параметра type, ядро откладывает (усыпляет)
обратившийся процесс до появления в очереди требуемого сообщения.
Однако, если в параметре flag задано значение флага IPC_NOWAIT, то
процесс немедленно оповещается об отсутствии сообщения в очереди путем
возврата кода ошибки.
Системный вызов
msgctl(id, cmd, mstatbuf);
служит для опроса состояния описателя очереди сообщений, изменения
его состояния (например, изменения прав доступа к очереди) и для
уничтожения указанной очереди сообщений (детали мы опускаем).
Рассмотрим небольщой пример демонстрирующий работу с очередями
сообщений
Пример 2.9 Независимые процессы взаимодействующие через очередь
сообщений
Программа server(server.cpp)
создаюшая очередь сообщений.
Программа client(client.cpp)
использующая созданную очередь
сообщений
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/ipc.h>
54
#include <sys/msg.h>
#include <unistd.h>
#include <signal.h>
#include <stream.h>
#include <sys/msg.h>
#include <unistd.h>
#include <stream.h>
#define MSGKEY75
#define MSGKEY
struct msgform
{
long mtype;
char mtext[256];
}msg;
struct msgform {
long mtype;
char
mtext[256];
};
75
main()
{
struct msgform msg;
int msgid, pid, *pint;
msgid = msgget(MSGKEY,0666);
int msgid;
void cleanup(int)
{
msgctl(msgid,IPC_RMID,0);
exit(0);
}
pid = getpid();
pint = (int *) msg.mtext;
*pint = pid;
main()
{
int i,pid,*pint;
extern void cleanup(int);
for (i = 0; i < 20; i++) signal(i,cleanup) ;
msgid =
msgget(MSGKEY,0666|IPC_CREAT);
for (;;)
{
msgrcv(msgid,&msg,256,1,0);
pint = (int *) msg.mtext;
pid = *pint;
cout<<"Получено сообщение от
процесса с идентификаторомPID = "
<< pid << '\n';
msg.mtype = pid;
*pint = getpid();
msgsnd(msgid,&msg,sizeof(int),0);
}
}
55
msg.mtype = 1;
msgsnd(msgid,&msg,sizeof(int),0);
msgrcv(msgid,&msg,256,pid,0);
cout<< "Получен ответ от процесса
с идентификатором PID = "<<
*pint << '\n';
}
В примере 2.9 два независимых приложения взаимодействуют через
очередь сообщений ассоциированную с ключем 75 (MSGKEY=75). Одно из
них – server создает очередь сообщений использя флаг IPC_CREAT
(msgget(MSGKEY,0666|IPC_CREAT);) и в бесконечном цикле ждет
сообщений с типом «1», выводит принятое сообщение и отправляет в ответ
сообщение с типом равным PID-у процесса, пославшего это сообщение.
Приложение client просто присоединяется к уже созданной очереди
сообщений и посылает сообщение с типом «1», содержащее PID корневого
процесса, затем ждет ответного сообщения с типом = PID корневого процесса.
.
Откомпилируем оба приложения:
$g++ server.cpp -o server
$g++ client.cpp -o client
Запускаем в одном терминале server - $./server, в другом несколько раз
client $./client
В третьем терминале набираем $ipcs и получаем в результате
примерно такую информацию:
------ Shared Memory Segments -------key
shmid
owner perms
bytes nattch
0x00000000 65536 root
777
122880 4
------ Semaphore Arrays -------key
semid owner perms nsems
------ Message Queues -------key
msqid
owner
0x0000004b 27754629
student
perms
666
status
used-bytes messages
0
0
Утилита ipcs позволяет отследить в системе состояние очередей
механизма IPC, более подробное описание - man ipcs.
2.8. Сетевое взаимодействие
программные гнезд (sockets)
процессов
с
использованием
Операционная система UNIX с самого начала проектировалась как
сетевая ОС в том смысле, что должна была обеспечивать явную возможность
взаимодействия процессов, выполняющихся на разных компьютерах,
соединенных сетью передачи данных. Главным образом, эта возможность
базировалась на обеспечении файлового интерфейса для устройств (включая
сетевые адаптеры) на основе понятия специального файла. Другими словами,
два или более процессов, располагающихся на разных компьютерах, могли
договориться о способе взаимодействия на основе использования
возможностей соответствующих сетевых драйверов.
56
Эти базовые возможности были в принципе достаточными для создания
сетевых утилит; в частности, на их основе был создан исходный в ОС UNIX
механизм сетевых взаимодействий uucp. Однако организация сетевых
взаимодействий пользовательских процессов была затруднительна главным
образом потому, что при использовании конкретной сетевой аппаратуры и
конкретного сетевого протокола требовалось выполнять множество
системных вызовов ioctl, что делало программы зависимыми от
специфической сетевой среды. Требовался поддерживаемый ядром механизм,
позволяющий скрыть особенности этой среды и позволить единообразно
взаимодействовать процессам, выполняющимся на одном компьютере, в
пределах одной локальной сети или разнесенным на разные компьютеры
территориально распределенной сети. Первое решение этой проблемы было
предложено и реализовано в UNIX BSD 4.1 в 1982 г.
На уровне ядра механизм программных гнезд поддерживается тремя
составляющими: компонентом уровня программных гнезд (независящим от
сетевого протокола и среды передачи данных), компонентом протокольного
уровня (независящим от среды передачи данных) и компонентом уровня
управления сетевым устройством.
По своему духу организация программных гнезд близка к идее потоков,
поскольку основана на разделении функций физического управления
устройством, протокольных функций и функций интерфейса с
пользователями. Однако это менее гибкая схема, поскольку не допускает
изменения конфигурации "на ходу".
Взаимодействие процессов на основе программных гнезд основано на
модели "клиент-сервер". Процесс-сервер "слушает (listens)" свое программное
гнездо, одну из конечных точек двунаправленного пути коммуникаций, а
процесс-клиент пытается общаться с процессом-сервером через другое
программное
гнездо,
являющееся
второй
конечной
точкой
коммуникационного пути и, возможно, располагающееся на другом
компьютере. Ядро поддерживает внутренние соединения и маршрутизацию
данных от клиента к серверу.
Программные гнезда с общими коммуникационными свойствами,
такими как способ именования и протокольный формат адреса, группируются
в домены. Наиболее часто используемыми являются "домен системы UNIX"
(AF_UNIX) для процессов, которые взаимодействуют через программные
гнезда в пределах одного компьютера, и "домен Internet"(AF_INET) для
процессов, которые взаимодействуют в сети в соответствии с семейством
протоколов TCP/IP.
Выделяются два типа программных гнезд - гнезда с виртуальным
соединением (в начальной терминологии stream sockets - SOCK_STREAM) и
датаграммные гнезда (datagram sockets - SOCK_DGRAM). При использовании
программных гнезд с виртуальным соединением обеспечивается передача
данных от клиента к серверу в виде непрерывного потока байтов с гарантией
57
доставки. При этом до начала передачи данных должно быть установлено
соединение, которое поддерживается до конца коммуникационной сессии.
Датаграммные программные гнезда не гарантируют абсолютной надежной,
последовательной доставки сообщений и отсутствия дубликатов пакетов
данных - датаграмм. Но для использования датаграммного режима не
требуется предварительное дорогостоящее установление соединений, и
поэтому этот режим во многих случаях является предпочтительным. Система
по умолчанию сама обеспечивает подходящий протокол для каждой
допустимой комбинации "домен-гнездо". Например, протокол TCP
используется по умолчанию для виртуальных соединений, а протокол UDP для датаграммного способа коммуникаций.
Для работы с программными гнездами поддерживается набор
специальных библиотечных функций (в UNIX BSD это системные вызовы,
однако, как мы отмечали в, в UNIX System V они реализованы на основе
потокового интерфейса TLI). Рассмотрим кратко интерфейсы и семантику
этих функций.
Для создания нового программного гнезда используется функция socket:
sd = socket(domain, type, protocol);
где значение параметра domain определяет домен данного
гнезда(AF_UNIX или AF_INET), параметр type указывает тип создаваемого
программного гнезда (с виртуальным соединением(SOCK_STREAM) или
датаграммное(SOCK_DGRAM)), а значение параметра protocol определяет
желаемый сетевой протокол. Заметим, что если значением параметра protocol
является нуль, то система сама выбирает подходящий протокол для
комбинации значений параметров domain и type, это наиболее
распространенный способ использования функции socket. Возвращаемое
функцией значение является дескриптором программного гнезда и
используется во всех последующих функциях.
Пример:
int sd = socket(AF_UNIX,SOCK_DGRAM,0);
или
int sd = socket (AF_INET, SOCK_STREAM, 0);
Вызов функции close(sd) приводит к закрытию (уничтожению)
указанного программного гнезда.
Для связывания ранее созданного программного гнезда с именем
используется функция bind:
bind(sd, socknm, socknlen);
Здесь sd - дескриптор ранее созданного программного гнезда, socknm адрес структуры, которая содержит имя (идентификатор) гнезда,
соответствующее требованиям домена данного гнезда и используемого
протокола (в частности, для домена системы UNIX имя является именем
объекта в файловой системе, и при создании программного гнезда
58
действительно создается файл), параметр socknlen содержит длину в байтах
структуры socknm (этот параметр необходим, поскольку длина имени может
весьма различаться для разных комбинаций "домен-протокол").
Пример вызова bind для домена AF_UNIX и типа сокета
SOCK_DGRAM:
struct sockaddr_un serv_addr;
int saddrlen;
socklen_t caddrlen;
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sun_family=AF_UNIX;
strcpy(serv_addr.sun_path,"./echo.server");
saddrlen = sizeof(serv_addr.sun_family)+strlen(serv_addr.sun_path);
bind(sockfd,(struct sockaddr *)&serv_addr,saddrlen);
В случае успешного выполнения этого вызова в корневом каталоге
должен появиться файл echo.server.
Пример вызова bind для домена AF_INET и типа сокета
SOCK_STREAM:
struct sockaddr_in serv_addr;
bzero (&serv_addr, sizeof (serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(20500);
if (bind (sd, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) == -1) perror
("Bind error");
С помощью функции connect процесс-клиент запрашивает систему
связаться с существующим программным гнездом (у процесса-сервера):
connect(sd, socknm, socknlen);
Смысл параметров такой же, как у функции bind, однако в качестве
имени указывается имя программного гнезда, которое должно находиться на
другой стороне коммуникационного канала. Для нормального выполнения
функции необходимо, чтобы у гнезда с дескриптором sd и у гнезда с именем
socknm были одинаковые домен и протокол. Если тип гнезда с дескриптором
sd является датаграммным, то функция connect служит только для
информирования системы об адресе назначения пакетов, которые в
дальнейшем будут посылаться с помощью функции send; никакие действия по
установлению соединения в этом случае не производятся.
if ((hp = gethostbyname (“здесь указывается IP адрес сервера”) == 0) {perror
("GetHostByName error"); exit (1);}
bzero (&serv_addr, sizeof (serv_addr));
bcopy (hp->h_addr, &serv_addr.sin_addr, hp->h_length);
serv_addr.sin_family = hp->h_addrtype;
59
serv_addr.sin_port = htons (20500); //указываем номер порта сокета сервера
// создаем сокет клиента
if ((sd = socket (AF_INET, SOCK_STREAM, 0)) == -1) {perror ("Socket call
error"); exit(1);}
// создаем соединение с сервером
if (connect (sd, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) == -1) {
perror ("Connect error"); exit(1); }
Функция listen предназначена для информирования системы о том, что
процесс-сервер планирует установление виртуальных соединений через
указанное гнездо:
listen(sd, qlength);
Здесь sd - это дескриптор существующего программного гнезда, а
значением параметра qlength является максимальная длина очереди запросов
на установление соединения, которые должны буферизоваться системой, пока
их не выберет процесс-сервер.
Для выборки процессом-сервером очередного запроса на установление
соединения с указанным программным гнездом служит функция accept:
nsd = accept(sd, address, addrlen);
Параметр sd задает дескриптор существующего программного гнезда,
для которого ранее была выполнена функция listen; address указывает на
массив данных, в который должна быть помещена информация,
характеризующая имя программного гнезда клиента, со стороны которого
поступает запрос на установление соединения; addrlen - адрес, по которому
находится длина массива address. Если к моменту выполнения функции accept
очередь запросов на установление соединений пуста, то процесс-сервер
откладывается до поступления запроса. Выполнение функции приводит к
установлению виртуального соединения, а ее значением является новый
дескриптор программного гнезда, который должен использоваться при работе
через данное соединение. По адресу addrlen помещается реальный размер
массива данных, которые записаны по адресу address. Процесс-сервер может
продолжать "слушать" следующие запросы на установление соединения,
пользуясь установленным соединением.
Фрагмент программы, принимающий запросы клиента, будет
выглядеть приблизительно так:
struct sockaddr_in clnt_addr;
socklen_t addrlen;
listen (sd, 5);
while(true)
{
bzero (&clnt_addr, sizeof (clnt_addr));
addrlen = sizeof (clnt_addr);
client_sd = accept (sd, (struct sockaddr *)&clnt_addr, &addrlen);
60
/* порождаем серверный процесс, который будет обслуживать запросы
присоединившегося клиента */
if (fork()==0)
{
close(sd);
// обслуживание запроса клиента
…
close(client_sd);
exit(0);
}
}
Для передачи и приема данных через программные гнезда с
установленным виртуальным соединением используются функции send и recv:
count = send(sd, msg, length, flags);
count = recv(sd, buf, length, flags);
В функции send параметр sd задает дескриптор существующего
программного гнезда с установленным соединением; msg указывает на буфер
с данными, которые требуется послать; length задает длину этого буфера.
Наиболее полезным допустимым значением параметра flags является значение
с символическим именем MSG_OOB, задание которого означает потребность
во внеочередной посылке данных. "Внеочередные" сообщения посылаются
помимо нормального для данного соединения потока данных, обгоняя все
непрочитанные сообщения. Потенциальный получатель данных может
получить специальный сигнал и в ходе его обработки немедленно прочитать
внеочередные данные. Возвращаемое значение функции равняется числу
реально посланных байтов и в нормальных ситуациях совпадает со значением
параметра length.
В функции recv параметр sd задает дескриптор существующего
программного гнезда с установленным соединением; buf указывает на буфер,
в который следует поместить принимаемые данные; length задает
максимальную длину этого буфера. Наиболее полезным допустимым
значением параметра flags является значение с символическим именем
MSG_PEEK, задание которого приводит к переписи сообщения в
пользовательский буфер без его удаления из системных буферов.
Возвращаемое значение функции является числом байтов, реально
помещенных в buf.
Заметим, что в случае использования программных гнезд с виртуальным
соединением вместо функций send и recv можно использовать обычные
файловые системные вызовы read и write. Для программных гнезд они
выполняются абсолютно аналогично функциям send и recv. Это позволяет
создавать программы, не зависящие от того, работают ли они с обычными
файлами, программными каналами или программными гнездами.
61
Для посылки и приема сообщений в датаграммном режиме
используются функции sendto и recvfrom:
count = sendto(sd, msg, length, flags, socknm, socknlen);
count = recvfrom(sd, buf, length, flags, socknm, socknlen);
Смысл параметров sd, msg, buf и lenght аналогичен смыслу
одноименных параметров функций send и recv. Параметры socknm и socknlen
функции sendto задают имя программного гнезда, в которое посылается
сообщение, и могут быть опущены, если до этого вызывалась функция
connect. Параметры socknm и socknlen функции recvfrom позволяют серверу
получить имя пославшего сообщение процесса-клиента.
Наконец, для немедленной ликвидации установленного соединения
используется системный вызов shutdown:
shutdown(sd, mode);
Вызов этой функции означает, что нужно немедленно остановить
коммуникации либо со стороны посылающего процесса, либо со стороны
принимающего процесса, либо с обеих сторон (в зависимости от значения
параметра mode). Действия функции shutdown отличаются от действий
функции
close
тем,
что,
во-первых,
выполнение
последней
"притормаживается" до окончания попыток системы доставить уже
отправленные сообщения. Во-вторых, функция shutdown разрывает
соединение, но не ликвидирует дескрипторы ранее соединенных гнезд. Для
окончательной их ликвидации все равно требуется вызов функции close.
Замечание: приведенная в этом пункте информация может несколько
отличаться от требований реально используемой вами системы. В основном
это относится к символическим именам констант.
Глава 3. Процессы и потоки в библиотеке Qt
Процессы в библиотеке Qt используются для запуска дополнительных
программ или команд, и являются аналогом процедуры запуска программы
fork-exec.
Потоки предоставляют возможность проведения параллельных
вычислений в рамках одного приложения. Многопоточное приложение,
запущенное на многопроцессорной системе, получает возможность запуска
различных потоков на различных процессорах, что значительно сокращает
время вычислений. Использование нескольких потоков на одном процессоре
целесообразно, когда необходима обработка большого количества данных,
особенно, если подобная обработка, выполняемая в одном потоке, приводит к
длительному зависанию других функций программы.
62
3.1. Процессы
Порождение процессов в библиотеке Qt основано на использовании
класса QProcess. Каждый процесс создается для запуска определенной
программы. Однако, QProcess не осуществляет эмуляцию командного
интерпретатора или терминала, и не может использоваться для запуска
некоторых программ и команд.
Список функций класса QProcess.
QProcess ( QObject * parent = 0, const char * name = 0 )
Конструктор. Создает объект класса Qprocess с родителем parent и именем
name.
void addArgument ( const QString & arg )
Добавляет аргумент arg к аргументам процесса. Первый аргумент процесса –
запускаемая комманда или программа. Для запуска нужного исполняемого
файла, в качестве первого аргумента можно установить имя файла с указанием
полного пути к нему.
QProcess p ; p.addArgument(“man”) ; p.addArgument(“pipe”) ;
void setArguments ( const QStringList & args )
Устанавливает аргументы процесса из списка строк args.
QStringList arguments ()
Функция возвращает список строк, содержащий аргументы процесса.
QDir workingDirectory ()
Функция возвращает рабочую директорию запущенного процесса. Если
директория не была предварительно определена, рабочая директория
запущенного процесса это та директория, из которой запущена программа,
породившая процесс. Необходимо понимать, что рабочая директория процесса
не есть та директория, в которой находится исполняемый файл, запущенный
процессом.
void setWorkingDirectory ( const QDir & dir )
Функция устанавливает dir как рабочую директорию процесса (но не
программы, запустившей процесс).
bool start ( QStringList * env = 0 )
Функция пытается запустить процесс для выполнения программы, указанной в
первом аргументе процесса. Список строк env содержит определения
переменных окружения запускаемого процесса, в виде параметр=значение.
Если параметр env не указан, запущенный процесс будет обладать тем же
окружением, что и запустившая его программа. Если процесс удалось
запустить, функция возвращает TRUE, иначе возвращает FALSE.
PID processIdentifier ()
Функция возвращает идентификационный номер процесса.
void kill ()
63
Функция завершает процесс.
bool isRunning ()
Функция возвращает TRUE, если процесс выполняется, иначе возвращает
FALSE.
bool normalExit ()
Функция возвращает TRUE, если процесс правильно завершился, иначе
возвращает FALSE. Если процесс не был запущен или выполняется, функция
возвратит FALSE.
bool canReadLineStdout ()
Функция возвращает TRUE, если можно считать строку из стандартного
вывода (stdout) процесса, иначе возвращает FALSE.
bool canReadLineStderr ()
Функция возвращает TRUE, если можно считать строку из стандартного
вывода ошибок(stderr) процесса, иначе возвращает FALSE.
QString readLineStdout ()
Считывает строку из стандартного вывода (stdout) процесса.
QString readLineStderr ()
Считывает строку из стандартного вывода ошибок (stderr) процесса.
QByteArray readStdout ()
Считывает данные из стандартного вывода (stdout) процесса.
QByteArray readStderr ()
Считывает данные из стандартного вывода ошибок (stderr) процесса.
void writeToStdin ( const QString & buf )
Записывает строку buf в стандартный ввод (stdin) процесса.
void writeToStdin ( const QByteArray & buf )
Записывает данные buf в стандартный ввод (stdin) процесса.
void closeStdin ()
Закрывает стандартный ввод процесса.
void setCommunication ( int commFlags )
Функция устанавливает тип связи commFlags с процессом. Где параметр
commFlags может быть равен:
QProcess::Stdin – данные могут быть записаны в стандартный ввод (stdin)
процесса,
QProcess::Stdout – данные могут быть прочитаны из стандартного вывода
(stdout) процесса,
QProcess::Stderr - данные могут быть прочитаны из стандартного вывода
ошибок (stderr) процесса,
QProcess::DupStderr – данные из стандартного вывода ошибок поступают на
стандартный вывод.
По умолчанию, тип связи равен Stdin|Stdout|Stderr.
Сигналы класса QProcess.
void readyReadStdout ()
64
Сигнал возникает при записи данных процессом в стандартный вывод (stdout).
void readyReadStderr ()
Сигнал возникает при записи данных процессом в стандартный вывод ошибок
(stderr).
void wroteToStdin ()
Сигнал формируется при записи данных в стандартный ввод (stdin) процесса.
void processExited ()
Сигнал формируется по завершении работы процесса.
Создадим
приложение,
запускающее
программы,
указанные
пользователем, и выводящее данные, получаемые от запущенных процессов
на форму. Графический интерфейс приложения: окно Form1, кнопка
pushButton1 (запуск процессов), поле ввода текста lineEdit1 (ввод командной
строки для процесса), область ввода текста textEdit1 (вывод данных процесса).
Для окна добавим слот void read().
Рисунок 3.1. Графический интерфейс приложения
Содержание файла form1.ui.h.
#include <qprocess.h>
QProcess *p ;
QString str,str2;
void Form1::pushButton1_clicked()
{
textEdit1->clear() ; //очистить область ввода текста
//если пользователь не задал командную строку для процесса, выйти из
функции
if ( (str = lineEdit1->text()) == "" ) return ;
p = new QProcess( this ); //определить новый процесс
//перенаправить данные процесса из stderr в stdout
p->setCommunication(QProcess::Stdout|QProcess::Stderr|QProcess::DupStderr) ;
//удалить лишние пробелы из командной строки
str = str.simplifyWhiteSpace() ;
65
int i=0 ;
//выделить аргументы командной строки
while ( (str2=str.section(" ",i,i)) != "")
{ i++ ; p->addArgument( str2 ) ; }
if (!p->start()) //запустить процесс
{ textEdit1->append("Процесс не может быть запущен") ; return ; }
else textEdit1->append("Процесс запущен") ;
connect( p, SIGNAL(readyReadStdout()),
this, SLOT(read()) ) ; //соединить слот read() с сигналом
readyReadStdout
}
void Form1::read()
{
//из-за функции setCommunication, слот будет получать данные и из stderr
процесса
while( p->canReadLineStdout() ) textEdit1->append(p->readLineStdout()) ;
}
Тестируя приложение, можно увидеть какие команды могут быть
запущены процессом. Например, команда man или ls будет выполнена, а
команда help или cd – нет. Если вы хотите запустить другое приложение,
породив процесс, указывайте полный путь к исполняемому файлу
приложения. Приложение, запускаемое процессом, может иметь, либо не
иметь графический интерфейс. Выход из программы, запустившей процессы,
не означает завершение порожденных процессов. Если необходимо указать
некоторые данные для приложения процесса, разработанного с помощью
библиотеки Qt, то можно использовать параметры argc, argv функции main,
определяя их значения функцией addArgument.
Создадим консольное приложение, выводящее информацию о
содержимом директории, из которой оно было запущено, или о содержимом
директорий, указанных в командной строке. Для вывода информации,
используется функция qWarning, подающая данные в стандартный вывод
ошибок (stderr).
#include <qapplication.h>
#include <qdir.h>
QDir dir ;
void dir_info()
{
qWarning(dir.absPath()) ;
for (int i=0 ; i<dir.count() ; i++) qWarning(dir[i]) ;
}
int main( int argc, char ** argv )
66
{
/*если исполняемый файл, запущен без дополнительных аргументов,
вывести информацию о
текущей директории*/
if (argc == 1) { dir=QDir::currentDirPath () ; dir_info() ;}
//иначе, просмотреть все указанные директории
else for (int i=1; i<argc; i++)
{ dir = QString(argv[i]) ; dir_info() ;}
return 1;
}
Если запустить это приложение, введя в поле ввода текста такую строку,
как «<исполняемый файл> /bin /usr /home», то область ввода текста будет
содержать имена всех элементов указанных директорий, если командная
строка будет содержать только исполняемый файл приложения, то будет
отображено содержимое директории приложения, вызвавшего процесс.
3.2. Потоки
Потоки предоставляют возможность проведения параллельных
вычислений в рамках одного приложения. Обычное приложение обладает
одним потоком, в котором проходят все вычисления. Добавление потоков в Qt
осуществляется добавлением объектов класса QThread.
Список функций класса QThread.
QThread ( )
Конструктор. Создает новый поток. Созданный поток не начинает вычислений
до вызова функции start().
void start ( Priority priority = InheritPriority )
Функция начинает вычисления потока, запуская виртуальную функцию run().
Если поток уже выполняется, то он будет запущен снова, после того, как
прекратит выполнение. Параметр priority определяет приоритет выполнения
потока, и может принять одно из следующих значений.
QThread::IdlePriority – поток выполняется, когда другие потоки не заняты.
QThread::LowestPriority – очень низкий приоритет выполнения.
QThread::LowPriority - низкий приоритет выполнения.
QThread::NormalPriority - обычный приоритет выполнения.
QThread::HighPriority - высокий приоритет выполнения.
QThread::HighestPriority – очень высокий приоритет выполнения.
QThread::TimeCriticalPriority – поток выполняется так часто, как это возможно.
QThread::InheritPriority – использовать приоритет родительского потока
(устанавливается по умолчанию).
virtual void run ()
Виртуальная функция, запускаемая функцией start(). Должна быть
переопределена программистом. Именно функция run() определяет
вычисления производимые потоком. При завершении функции run()
завершается исполнение потока.
67
bool finished ()
Функция возвращает TRUE, если поток завершил вычисления, иначе
возвращает FALSE.
bool running ()
Функция возвращает TRUE, если поток выполняется, иначе возвращает
FALSE.
void wait( unsigned long time = ULONG_MAX )
Ожидать завершения выполнения потока, если параметр time не определен,
или ожидать time миллисекунд. Поток, в программном коде которого, вызвана
эта функция, во время ожидания блокируется, до тех пор, пока не пройдет
указанное время или не завершится поток, относительно которого была
вызвана функция.
void sleep ( unsigned long secs )
Приостановить исполнение потока на время secs секунд.
void msleep ( unsigned long msecs )
Приостановить исполнение потока на время msecs миллисекунд.
void exit ()
Завершает выполнение потока и “пробуждает” потоки, ожидающие его
завершения.
Для использования нового потока необходимо переопределить функцию
run, команды, описанные в данной функции, и будут выполняться в новом
потоке.
Реализуем приложение, вычисляющее скалярное произведение двух
векторов, используя потоки. Для этого, создадим потоки, вычисляющие
произведение соответственных координат векторов, а также, поток,
ожидающий завершения вычислений всех произведений и находящий
конечный результат (т.е. сумму произведений). Графический интерфейс
приложения состоит из окна Form1 и кнопки pushButton1 (с сигналом clicked),
результат вычислений выводится в терминал.
Содержание файла form1.ui.h.
#include <qthread.h>
int A[6] = { 1,2,2,3,3,1 } ; //первый вектор
int B[6] = { 3,2,0,1,3,2 } ; //второй вектор
int C[6] ; //для записи произведений координат
int flag = 0 ; //счетчик, показывающий, сколько потоков завершило вычисления
//класс потоков, осуществляющих произведение
class MyThread : public QThread
{
public :
int num ; //переменная, показывающая потоку, с какими
координатами работать
68
MyThread() : QThread() { } //конструктор
void run () //функция определяющая вычисления потока
{
//произведение координат
C[num] = A[num]*B[num] ;
flag++ ; //увеличение счетчика
}
};
//класс потока, вычисляющего результат
class MyThreadMonitor : public QThread
{
public :
MyThreadMonitor() : QThread() { } //конструктор
void run () //функция определяющая вычисления потока
{
int i,sum ;
while (flag<6) //ожидать завершения вычислений шести потоков
класса MyThread
{ msleep(50) ; }
for (i=0,sum=0 ; i<6 ; i++) //вычислить сумму произведений
{ sum+=C[i] ; }
//вывести результат в терминал
qWarning("Скалярное произведение = %d",sum) ;
}
};
//объявление потоков
MyThread t[6] ;
MyThreadMonitor tm ;
void Form1::pushButton1_clicked()
{
int i ;
//запуск потоков
for (i=0 ; i<6 ; i++)
{
t[i].num = i ; t[i].start() ;
}
tm.start() ;
}
Переменные классов MyThread и MyThreadMonitor неслучайно
объявлены глобальными. Если бы они были описаны в рамках отдельной
функции, это означало бы наличие возможности уничтожения потока до
завершения его вычислений, а такая ситуация может привести к аварийному
69
завершению всех вычислений приложения, а не только одного потока. В
случае использования локальных переменных потока, следует вызывать
функцию wait, но она заблокирует и графический интерфейс тоже.
Многопоточное приложение проводит параллельные вычисления,
будучи запущенным, на многопроцессорной вычислительной системе. Если
используется один процессор, то такие вычисления правильнее назвать
псевдопараллельными. Однако, использование нескольких потоков на одном
процессоре, тоже может быть полезным, например, для избегания «заморозки»
графического интерфейса во время обработки большого количества данных.
В порожденных потоках не следует производить следующие действия,
т.к. они могут привести к аварийному завершению программы. Создание
объектов классов QWidget (и классов-потомков), QTimer, QSocketNotifier,
QSocket, QServerSocket. Перемещение, обновление, скрытие и т.п. элементов
управления. Использование объектов классов QSocket и QServerSocket. Запуск
или остановка таймера. Вызов функции setEnabled класса QSocketNotifier.
Вызов функции exec. В порожденном потоке можно использовать механизм
сигналов и слотов, но при этом слот должен находиться в контексте того
потока, в котором сформировалось событие. Обратите внимание, что в
приведенном примере, поток выводит результат вычислений с помощью
функции qWarning, если бы вместо нее была использована, например функция
setText, вызванная для некоторого элемента управления, то это привело бы к
возникновению ошибки. Работа с элементами управления может происходить
только в главном потоке приложения. Для вывода информации, вычисляемой
порожденными потоками, в элемент управления можно применить функцию
wait.
void Form1::pushButton1_clicked()
{
for (int i=0 ; i<6 ; i++)
{ t[i].num = i ; t[i].start() ; }
tm.start() ;
tm.wait() ;
lineEdit1->setText(“ … ”) ;
}
Таким образом, главный поток приложения будет ожидать завершения
потока tm, и затем обновит содержимое элемента управления. Но, как
упоминалось выше, вызов wait заблокирует любые вычисления главного
потока. Поэтому, для подобных целей, более удобно использовать механизм
событий.
События представлены классом QEvent, а также дочерними классами,
такими как, QTimerEvent, QMouseEvent, QKeyEvent, QPaintEvent,
QMoveEvent, QResizeEvent, QShowEvent, QHideEvent и т.д. Дополнительную
информацию о событиях вы можете найти в методических указаниях
“Визуальное программирование в Linux”, часть 2, занятия 20-21. Но все
70
вышеперечисленные классы описывают стандартные события, тогда как для
использования в потоках, более разумно использовать события определяемые
программистом. Для этого существует класс QCustomEvent.
Перепишем задачу, исключив поток MyThreadMonitor. Его основная
задача отследить момент завершения шести потоков MyThread. В новом
варианте, для достижения этой цели, будет использовано событие,
испускаемое каждым потоком MyThread, по завершении вычислений.
Функция-обработчик события формируемого любым потоком, должна
принадлежать главному потоку приложения, т.к. только главный поток
способен принимать и обрабатывать события, именно с этим связаны
ограничения, накладываемые на программный код порожденных потоков.
Для реализации собственного события понадобятся следующие
функции.
QCustomEvent ( int type )
Конструктор. Создает объект класса QCustomEvent с идентификационным
номером type. Номера от 1 до 1000 зарезервированы под стандартные события.
int type ()
Функция возвращает идентификационный номер события.
postEvent ( QObject * receiver, QEvent * event )
Функция посылает событие event, объекту receiver. Событие после обработки
удаляется.
customEvent( QCustomEvent *)
Функция-обработчик события класса QCustomEvent. Обработчики событий, в
отличие от обработчиков сигналов, имеют зарезервированные имена.
Указатель содержит информацию о произошедшем событии.
Перед изменением исходного кода, добавим слот void customEvent(
QCustomEvent *) для окна Form1, также, для отображения конечного
результата, добавим поле ввода текста lineEdit1.
Содержание файла form1.ui.h.
#include <qthread.h>
#include <qevent.h>
int A[6] = { 1,2,2,3,3,1 } ; //первый вектор
int B[6] = { 3,2,0,1,3,2 } ; //второй вектор
int C[6] ; //для записи произведений координат
class MyThread : public QThread
{
public :
int num ;
Form1 *receiver ; //указатель на объект получатель события
MyThread() : QThread() { }
void run ()
{
71
C[num] = A[num]*B[num] ;
//формирование пользовательского события с номером 2000 для
объекта receiver
postEvent( receiver, new QCustomEvent(2000));
}
};
MyThread t[6] ;
void Form1::pushButton1_clicked()
{
int i ;
for (i=0 ; i<6 ; i++)
{ t[i].num = i ; t[i].receiver = this ; t[i].start() ; }
}
//обработчик любого пользовательского события
void Form1::customEvent( QCustomEvent *e)
{
static int flag = 0 ;
flag++ ;
if (flag==6)
{
int i,sum ;
for (i=0, sum=0 ; i<6 ; i++) sum+=C[i] ;
lineEdit1->setText(QString("Скалярное произведение = %1").arg(sum)) ;
flag=0 ;
}
}
Каждый поток MyThread формирует пользовательское событие в
завершении своих вычислений. Обработчик пользовательского события
считает общее количество поступивших событий, если оно равно шести,
значит, отработали все шесть потоков, и можно приступать к суммированию
элементов, найденных порожденными потоками.
Обратите
внимание
на
вызов
postEvent(
receiver,
new
QCustomEvent(2000)). Формируемое событие (QCustomEvent) не должно быть
глобальной переменной, т.к. обработанное событие удаляется. Если событие
определено как глобальная переменная, после вызова postEvent, нужно
вызвать оператор событие = new QCustomEvent, но подобный код в
многопоточном приложении не безопасен, из-за возможного одновременного
вызова функции postEvent из разных потоков, не оставляющего возможности
переопределить событие.
Функция-обработчик customEvent принимает любое пользовательское
событие, вне зависимости от его идентификационного номера. Если в
72
программе используется несколько различных пользовательских событий,
обработчик можно описать следующим образом.
postEvent( receiver, new QCustomEvent(2000));
…
postEvent( receiver, new QCustomEvent(4000));
…
void Receiver::customEvent( QCustomEvent *e)
{
if (e->type()==2000) { … }
if (e->type()==4000) { … }
}
3.3. Семафоры
При совместной работе нескольких потоков могут возникать
конфликтные ситуации, связанные с использованием несколькими потоками
одного ресурса. Поэтому на определенные ресурсы необходимо накладывать
ограничения, связанные с количеством потоков, имеющим к ним доступ. Этой
цели служат мьютексы (объекты класса QMutex) и семафоры (объекты класса
QSemaphore).
Список функций класса QMutex.
QMutex ( bool recursive = FALSE )
Конструктор. Создает обычный мьютекс, если recursive = FALSE, или
рекурсивный мьютекс, если recursive = TRUE. По умолчанию recursive =
FALSE. Рекурсивный мьютекс поток может заблокировать несколько раз, и он
не будет открыт, пока функция unlock не будет вызвана столько же раз,
сколько функция lock. Созданный мьютекс является открытым.
void lock ()
Функция закрывает мьютекс. Если мьютекс уже закрыт другим потоком, то
поток, вызвавший функцию lock, останавливается, пока мьютекс не откроется.
void unlock ()
Функция открывает мьютекс. Один и тот же мьютекс может быть закрыт или
открыт из разных потоков.
bool locked ()
Возвращает TRUE, если мьютекс закрыт или FALSE, если мьютекс открыт.
bool tryLock ()
Функция закрывает мьютекс. Если мьютекс уже закрыт другим потоком,
функция возвращает FALSE, но функция выполняется, и вычисления потока
продолжаются. Если мьютекс был открыт, он закрывается и возвращается
значение TRUE. Т.е. функция tryLock позволяет потоку начать вычисления,
несмотря на операции, проводимые другими потоками.
Таким образом, мьютексы хорошо подходят для «запирания» нужных
ресурсов.
73
В последнем примере потоки работают с тремя глобальными массивами
и глобальной переменной flag. Но, если используемые элементы массивов
четко разделены между потоками, то переменная flag используется в каждом
потоке. Необходимо отметить, что одновременное чтение общих данных
несколькими потоками не приводит к возникновению ошибок, тогда как
наличие возможности одновременной записи в общие данные делает
программу ненадежной, даже если она выполняется на одном процессоре.
Также следует избегать ситуаций, когда чтение и запись в общие данные
может произойти одновременно, это может привести к тому, что поток
прочитает устаревшие данные. Чаще всего мьютексы и семафоры блокируют
одновременный доступ к файлу или к глобальной переменной. Перепишем
последний пример, введя защиту глобальной переменной flag, от
одновременного доступа нескольких потоков.
#include <qthread.h>
#include <qmutex.h>
int A[6] = { 1,2,2,3,3,1 } ; //первый вектор
int B[6] = { 3,2,0,1,3,2 } ; //второй вектор
int C[6] ; //для записи произведений координат
int flag = 0 ; //счетчик, показывающий, сколько потоков завершило вычисления
QMutex mutex ; //обычный мьютекс
//класс потоков, осуществляющих произведение
class MyThread : public QThread
{
public :
int num ; //переменная, показывающая потоку, с какими
координатами работать
MyThread() : QThread() { } //конструктор
void run () //функция определяющая вычисления потока
{
//произведение координат
C[num] = A[num]*B[num] ;
mutex.lock() ; //закрытие мьютекса
flag++ ; //увеличение счетчика
mutex.unlock() ; //открытие мьютекса
}
};
//класс потока, вычисляющего результат
class MyThreadMonitor : public QThread
{
public :
MyThreadMonitor() : QThread() { } //конструктор
void run () //функция определяющая вычисления потока
{
int i,sum ;
74
while (flag<6) //ожидать завершения вычислений шести потоков
класса MyThread
{ msleep(50) ; }
for (i=0,sum=0 ; i<6 ; i++) //вычислить сумму произведений
{ sum+=C[i] ; }
//вывести результат в терминал
qWarning("Скалярное произведение = %d",sum) ;
}
};
//объявление потоков
MyThread t[6] ;
MyThreadMonitor tm ;
void Form1::pushButton1_clicked()
{
int i ;
for (i=0 ; i<6 ; i++) //запуск потоков
{ t[i].num = i ; t[i].start() ;}
tm.start() ;
}
Использование подобной простой программной конструкции, позволяет
запретить обращение всех других потоков к переменной flag, пока с ней
работает один поток. Как только один поток вызывает функцию lock первым,
остальные потоки, вызвав эту функцию, прекратят вычисления, пока не будет
выполнена функция unlock, причем данная функция будет вызвана первым
потоком, закрывшим мьютекса. После этого, управление перейдет к
некоторому другому потоку, который закроет мьютекс, а значит, закроет
доступ к переменной для всех оставшихся потоков и т.д. Сам мьютекс тоже
является глобальной переменной, но и мьютексы, и семафоры, созданы для
использования в потоках, и не могут стать причиной ресурсного конфликта.
Для пояснения различий между рекурсивным и обычным мьютексом
внесем следующие изменения в приведенный пример.
QMutex mutex ;
…
void run () {
C[num] = A[num]*B[num] ;
mutex.lock() ; mutex.lock() ;
flag++ ;
mutex.unlock() ; mutex.unlock() ;
}…
Подобные вычисления никогда не завершаться, в силу того, что первый
поток, закрывший мьютекс, повторно вызывает функцию lock, а значит,
останавливает сам себя, т.к. мьютекс уже закрыт. Отсюда, функция unlock не
75
вызывается, мьютекс не открывается, и все порожденные потоки безуспешно
ожидают открытия мьютекса. Что случиться, если вместо обычного мьютекса,
использовать рекурсивный?
QMutex mutex(TRUE) ;
…
void run () {
C[num] = A[num]*B[num] ;
mutex.lock() ; mutex.lock() ;
flag++ ;
mutex.unlock() ; mutex.unlock() ;
}…
Программа с таким программным кодом завершится, т.к. первый поток,
закрывший мьютекс, повторно вызывая функцию lock, не остановит свое
исполнение, а проведет необходимую операцию (flag++), и затем откроет
мьютекс, вызвав дважды функцию unlock. Однако если бы функция unlock
была вызвана один раз, то все порожденные потоки остановились бы.
Если обычный мьютекс может быть или закрыт или открыт, то каждый
семафор обладает приписанным к нему значением.
Функции класса QSemaphore.
QSemaphore ( int maxcount )
Конструктор. Создает семафор со значением maxcount.
int available ()
Функция возвращает количество свободных значений семафора. Для только
что созданного семафора количество свободных значений равно maxcount.
int total ()
Функция возвращает общее количество значений семафора.
bool tryAccess ( int n )
Если количество свободных значений < n, функция возвращает FALSE и не
производит над семафором никаких действий, если количество свободных
значений >= n, функция возвращает TRUE и убирает n свободных значений у
семафора.
Операторы класса QSemaphore.
++ ( int )
Закрывает свободное значение семафора. Если, на момент вызова оператора,
количество свободных значений равно нулю, поток блокируется, до тех пор,
пока количество свободных значений не станет больше нуля.
+= ( int n )
Закрывает n свободных значений семафора. Если, на момент вызова
оператора, количество свободных значений < n, поток блокируется, до тех
пор, пока количество свободных значений не станет >= n.
-- ( int )
Открывает свободное значение семафора.
-= ( int n )
76
Открывает n свободных значений семафора.
Обычно, значение семафора является положительным числом.
Перепишем последний пример, введя защиту глобальной переменной
flag, с помощью семафоров, а не мьютексов. Текст, не подвергшийся
изменению, не переписан.
#include <qthread.h>
#include <qsemaphore.h>
QSemaphore s(1) ; //семафор может обладать одним свободным значением
class MyThread : public QThread
{
public :
MyThread() : QThread() { }
void run () //функция определяющая вычисления потока
{
C[num] = A[num]*B[num] ;
s++ ; //закрытие семафора
flag++ ; //увеличение счетчика
s-- ; //открытие семафора
}
};
Созданный семафор обладает одним свободным значением. Первый
поток, вызвавший оператор s++, закрывает свободное значение семафора,
остальные потоки, дойдя до вызова s++, останавливаются, т.к. количество
свободных значений равно нулю. После вызова s--, свободное значение
открывается, и один из ожидающих потоков, вызывает s++, закрывая семафор,
для оставшихся потоков и т.д.
Перепишем задачу, заменив глобальную переменную flag на семафор.
#include <qthread.h>
#include <qsemaphore.h>
int A[6] = { 1,2,2,3,3,1 } ; //первый вектор
int B[6] = { 3,2,0,1,3,2 } ; //второй вектор
int C[6] ; //для записи произведений координат
QSemaphore s(6) ; //объявление семафора с шестью свободными значениями
class MyThread : public QThread
{
public :
MyThread() : QThread() { }
int num ;
void run () //функция определяющая вычисления потока
{
C[num] = A[num]*B[num] ;
s-- ; //открытие одного свободного значения семафора
77
}
};
class MyThreadMonitor : public QThread
{
public :
MyThreadMonitor() : QThread() { }
void run () //функция определяющая вычисления потока
{
int i,sum ;
s+=6 ; // закрытие шести свободных значений семафора
for (i=0,sum=0 ; i<6 ; i++) //вычислить сумму произведений
{ sum+=C[i] ; }
qWarning("Скалярное произведение = %d",sum);
}
};
MyThread t[6] ;
MyThreadMonitor tm ;
void Form1::pushButton1_clicked()
{ s.tryAccess(6) ; // закрытие шести свободных значений семафора
int i ;
for (i=0 ; i<6 ; i++) //запуск потоков
{ t[i].num = i ; t[i].start() ; }
tm.start() ;
}
Рассмотрим исходный код примера. Задается глобальная переменная s
класса QSemaphore, обладающая шестью свободными значениями. Перед
порождением потоков, все свободные значения закрываются (s.tryAccess(6)).
Каждый из шести потоков, вычисляющих произведение координат, по
окончании вычислений, открывает одно свободное значение семафора. Поток
MyThreadMonitor, вызывает оператор s+=6, ожидая тем самым, открытия
шести свободных значений семафора. А такое состояние наступит, когда
отработают все шесть потоков MyThread.
3.4. Условные переменные
Условные переменные, как и семафоры, осуществляют блокировку
потоков и их пробуждение. Используя условную переменную, можно указать
потоку заснуть и ожидать пробуждения, разбудить один из потоков или все
потоки, ожидающие сигнала от данной условной переменной.
78
Семафоры и мьютексы блокируют и пробуждают потоки, с помощью
внутреннего условия, так мьютекс может находиться в открытом или
закрытом состоянии, а семафор обладает общим и свободным значением.
Условная переменная осуществляет те же действия с помощью вызова
определенных функций, при этом условная переменная не обладает никаким
внутренним условием, с которым необходимо считаться при вызове этих
функций.
Отсутствие внутреннего условия на первый взгляд делает управление
потоками более простым. Но эта особенность условных переменных делает их
ненадежными, так как поток может быть разблокирован, только если он
ожидает сигнала от условной переменной (в том случае, когда не указан
предел времени для блокировки потока). Таким образом, если сигнал о
пробуждении был сформирован до усыпления потока, то этот сигнал
пропадет, поток войдет в состояние сна и далее может из него не выйти.
Так как условные переменные не описывают условий для управления
потоками, эта задача перекладывается на программиста. Очень часто условия
включают в себя общие данные, и эти данные необходимо защищать от
одновременного использования разными потоками. Мьютекс – простое и
эффективное средство защиты, и использование условных переменных в
большинстве случаев предполагает и использование мьютексов, защищающих
общие данные. Мьютексы также необходимы для получения корректного
условия. В силу того, что условие изменяется, поток может получить
устаревшее условие и быть заблокированным (или наоборот, вернуться к
работе) когда это совершенно не нужно. Структура условной переменной не
включает в себя мьютекс, он должен быть описан отдельно. Несколько
условных переменных могут использовать один мьютекс, если все эти
переменные опираются на проверку условий, зависящих от одних и тех же
общих данных.
Для
объявления
условной
переменной
используется
класс
QWaitCondition, рассмотрим функции этого класса.
QWaitCondition()
Конструктор условной переменной.
bool wait ( unsigned long time = ULONG_MAX )
Поток, вызвавший эту функцию, будет заблокирован, пока не произойдет одно
из следующих условий:
1. Другой поток разблокирует его, вызвав одну из необходимых для этого
функций (см. ниже). В этом случае функция возвратит TRUE.
2. Пройдет время, указанное в time. В этом случае функция возвратит
FALSE.
Если параметр time не определен (или равен ULONG_MAX), то поток
блокируется без учета временного предела, в этом случае блокировка потока
может быть снята только вызовом необходимой функции из другого потока.
bool wait ( QMutex * mutex, unsigned long time = ULONG_MAX )
79
Функция почти полностью аналогична предыдущей, за тем исключением, что
перед установкой блокировки мьютекс mutex открывается, а после снятия
блокировки, мьютекс mutex снова закрывается. Предполагается, что мьютекс
должен быть закрыт перед вызовом этой функции.
О потоке, заблокированном какой-либо из функций wait(), говорят, что этот
поток ожидает условную переменную.
void wakeAll()
Функция снимает блокировку со всех потоков, ожидающих условную
переменную.
void wakeOne()
Функция снимает блокировку с одного из потоков, ожидающих условную
переменную.
Воспользуемся условными переменными для контроля длины очереди в
задаче
производитель-потребитель:
имеется
несколько
потоков
производителей, они записывают в очередь некоторые данные, также имеется
поток потребитель, изымающий из очереди данные. С помощью условных
переменных можно наложить на потоки следующие ограничения: поток
потребитель засыпает, если очередь пуста, и просыпается при добавлении
данных в очередь, поток производитель засыпает, если количество элементов
в очереди становится больше шестидесяти, и просыпается, если очередь пуста.
Такая схема позволяет синхронизировать работу всех потоков, здесь не
возникнет случая, когда потоки производители почти непрерывно записывают
данные в очередь, тогда как поток потребитель остается в неактивном
состоянии.
Поток-потребитель:
while(1)
Бесконечный цикл
{
{
mutex->lock() ;
закрыть мьютекс ;
while (length == 0)
пока (длина очереди равна нулю)
cond1-> wait(&mutex) ;
усл.перем-я 1->ожидать(мьютекс) ;
забрать(очередь) ;
изъять элемент из очереди ;
length-- ;
уменьшить длину очереди на единицу ;
mutex->unlock() ;
открыть мьютекс ;
cond2->wakeAll() ;
усл.перем-я 2->разбудить все потоки() ;
...
обработка полученного из очереди
}
элемента ; }
Поток-производитель:
80
while(1)
Бесконечный цикл
{ сформировать элемент для очереди ;
{ ...
mutex->lock() ;
закрыть мьютекс ;
while (length > 60)
пока (длина очереди > 60)
cond2->wait(&mutex) ;
усл.перем-я 2->ожидать(мьютекс) ;
вставить(очередь) ;
добавить элемент в очередь ;
length++ ;
увеличить длину очереди на единицу ;
mutex->unlock() ;
открыть мьютекс ;
cond1->wakeOne() ;
усл.перем-я 1->разбудить один поток() ;
}
}
В описанном примере используется один мьютекс и две условные
переменные (cond1, cond2), все они являются глобальными переменными.
Переменная cond1 введена для блокировки и пробуждения потока
потребителя, cond2 для потоков производителей. Поток потребитель засыпает,
если длина очереди равна нулю, и пробуждает все потоки производители
(wakeAll) после каждого изъятия элемента из очереди. Каждый поток
производитель засыпает, если длина очереди больше шестидесяти, и
пробуждает поток потребитель, после каждого добавления элемента в очередь.
Все потоки пользуются одним мьютексом, это связано с тем, что условия
ожидания всех потоков (length == 0, length > 60) содержат одни и те же общие
данные – глобальную переменную length. Обратите внимание на то, что
мьютекс защищает также процесс изменения очереди и ее длины. Таким
образом, параллельное выполнение потоков возможно только во время
формирования и обработки элемента очереди, добавление и изъятие элементов
из очереди и проверка длины очереди может происходить только
последовательно, сколько бы потоков одновременно ни выполнялось.
81
Приложение А
Структура файла stat файловой системы /proc
Здесь в поpядке пpедставления в файле stat описаны поля и их фоpмат
чтения функцией scanf():
pid (%d) id пpоцесса.
comm (%s) Имя запускаемого файла в кpуглых скобках. Из него видно
использует пpоцесс свопинг или нет.
state (%c) один из символов из набоpа "RSDZT", где:
R - запуск
S - приостановлен в ожидании пpеpывания
W - приостановлен с запpещением пpеpывания (в частности для
своппинга)
Z - исключение пpоцесса (зомби)
T - пpиостановка в опpеделенном состоянии
ppid (%d) pid пpоцесса родителя
pgrp (%d) группа процесса
session (%d)
сессия процесса
tty (%d)
Используемый пpоцессом терминал.
tpgid (%d) группа процессов, которые используют эту же терминальную
линию.
flags (%u) Флаги пpоцесса. Каждый флаг имеет набоp битов
min_flt (%u)
Количество не существенных сбоев pаботы пpоцесса,
котоpые не тpебуют загpузки с диска стpаницы памяти.
cmin_flt (%u)
Количество не существенных сбоев в pаботе пpоцесса и его
сыновей
maj_flt (%u)
Количество существенных сбоев в pаботе пpоцесса,
тpебующих подкачки стpаницы памяти.
сmaj_flt (%u)
Количество существенных сбоев пpоцесса и его сыновей.
utime (%d) Количество тиков, со вpемени pаспpеделения pаботы пpоцесса в
режиме задачи.
stime (%d) Количество тиков, со вpемени pаспpеделения pаботы пpоцесса в
режиме(пpостpанстве) ядpа.
cutime (%d)
Количество тиков, со вpемени pаспpеделения pаботы
пpоцесса и его сыновей в пpостpанстве пользователя.
cstime (%d) Количество тиков, со вpемени pаспpеделения pаботы пpоццесса и
его сыновей в режиме ядpа.
counter (%d)
Текущий максимальный pазмеp в тиках следующего пеpиода
pаботы пpоцесса, в случае его непосpедственной деятельности, количество
тиков до завеpшения деятельности.
priority (%d)
стандаpтное UNIX-е значение плюс пятнадцать. Это число
не может быть отpицательным в ядpе.
timeout (%u)
Вpемя в тиках, следующего пеpеpыва в pаботе пpоцесса.
82
it_real_value (%u)
Пеpиод вpемени в тиках, по истечении которого
процессу передается сигнал SIGALARM (будильник).
start_time (%d)
Вpемя отсчитываемое от момента загpузки системы, по
истечении котоpого начинает pаботу пpоцесс.
vsize (%u) Размеp виpтуальной памяти.
rss (%u)
число страниц реальной памяти, занимаемой процессом –
количество стpаниц используемых пpоцессом, содеpжащихся в pеальной
памяти минус тpи стpаницы занятые под упpавление. Сюда входят стековые
стpаницы и инфоpмфционные. Своп. стpаницы и стpаницы загpузки запpосов
не входят в данное число.
rlim (%u) Пpедел pазмеpа пpоцесса.
start_code (%u) Адрес, выше которого может выполняться текст программы.
end_code (%u) Адрес, ниже которого может выполняться текст программы.
start_stack (%u)Адpес начала стека.
kstk_esp (%u)
Текущее значение указателя на 32-битный стек, получаемый
в стековой стpанице ядpа для пpоцесса.
kstk_eip (%u)
Текущее значение указателя на 32-битную инстpукцию,
получаемую в стековой стpанице ядpа для пpоцесса.
signal (%d) маска сигналов процесса, ожидающих доставки
blocked (%d)
маска блокированных сигналов
sigignore (%d)
маска игнорируемых сигналов.
sigcatch (%d)
маска перехваченных сигналов.
wchan (%u)"Канал" в котоpом пpоцесс находится в состоянии ожидания. Это
адpес системного вызова, котоpый можно посмотpеть в списке имен, если вам
нужно получить стpоковое значение имени.
statm - файл содеpжит специальную статусную инфоpмацию, занимающую
немного больше места, нежели инфоpмация в stat, и используемую достаточно
pедко, чтобы выделить ее в отдельный файл. Для создания каждого поля в
этом файле, файловая система proc должна пpосматpивать каждый из 0x300
составляющих в каталоге страниц и вычислять их текущее состояние.
83
Приложение В
Описание используемых в ОС Linux сигналов
Linux поддерживает следующие сигналы. Некоторые номера сигналов
зависят от используемой архитектуры. Сначала идут сигналы, описанные в
стандарте POSIX.1.
Сигнал
Значение
Действие
Описание
--------------------------------------------------------------SIGHUP
1
A
Обнаружен обрыв связи с
управляющим терминалом
либо завершение
управляющего процесса
Прерывание с клавиатуры
SIGINT
2
A
SIGQUIT
3
C
Выход с клавиатуры
SIGILL
4
C
Несуществующая инструкция
SIGABRT
6
C
Сигнал прерывания,
посланный функцией
abort(3)
SIGFPE
8
C
Ошибка операций с
плавающей запятой
SIGKILL
9
AEF
Kill-сигнал
SIGSEGV
11
C
Обращение к запретной
области памяти
Оборванный канал: запись в
SIGPIPE
13
A
канал, из которого не
читают
SIGALRM
14
A
Сигнал таймера от функции
alarm(2)
SIGTERM
15
A
Сигнал завершения
Первый сигнал,
SIGUSR1
30,10,16
A
определяемый пользователем
SIGUSR2
31,12,17
A
Второй сигнал,
определяемый пользователем
SIGCHLD
20,17,18
B
Потомок остановлен или
прекратил выполнение
SIGCONT
19,18,25
Продолжить выполнение,
если остановлен
SIGSTOP
17,19,23
DEF
Приостановить выполнение
процесса
SIGTSTP
18,20,24
D
Останов введен с терминала
SIGTTIN
21,21,26
D
ввод с терминала у
фонового процесса
SIGTTOU
22,22,27
D
вывод на терминал у
фонового процесса
Следующие сигналы не входят в стандарт POSIX.1, но описаны в
SUSv2.
84
Сигнал
Значение
Действие
Описание
--------------------------------------------------------------SIGBUS
10,7,10
C
Ошибка шины (ошибка
доступа к памяти)
SIGPOLL
A
Ожидаемое событие (Sys
V). Синоним SIGIO
SIGPROF
27,27,29
A
Закончилось время
профилирующего таймера
SIGSYS
12,-,12
C
Неправильный аргумент
процедуры (SVID)
SIGTRAP
5
C
Трассировка/ловушка
SIGURG
16,23,21
B
Неотложное событие в
сокете (4.2 BSD)
Виртуальный будильник
SIGVTALRM
26,26,28
A
(4.2 BSD)
SIGXCPU
24,24,30
C
Лимит процессорного
времени исчерпан (4.2
BSD)
SIGXFSZ
25,25,31
C
Лимит на размер файла
исчерпан (4.2 BSD)
(Для случаев SIGSYS, SIGXCPU, SIGXFSZ, а для некоторых
архитектур -- и SIGBUS, Linux (до настоящего времени -- 2.3.40) по
умолчанию производит действие A (завершение выполнения), тогда как
SUSv2 предписывает делать C (завершить выполнение с записью копии
состояния памяти(дампа памяти – файл core)).)
Сигнал
Значение
Действие
Описание
--------------------------------------------------------------SIGIOT
6
C
IOT-ловушка. Синоним для
SIGABRT
SIGEMT
7,-,7
SIGSTKFLT
-,16,A
Переполнение стека
сопроцессора
SIGIO
23,29,22
A
I/O теперь возможно (4.2
BSD)
SIGCLD
-,-,18
Синоним для SIGCHLD
Авария питающего
SIGPWR
29,30,19
A
напряжения (System V)
SIGINFO
29,-,Синоним для SIGPWR
SIGLOST
-,-,A
Потеря файла блокировки
SIGWINCH
28,28,20
B
Изменение размеров окна
(4.3 BSD, Sun)
SIGUNUSED
-,31,A
Неиспользуемый сигнал
( “-“ является признаком того, что сигнал отсутствует; там, где приведено
три значения, первое -- для архитектур alpha и sparc, второе для архитектур
i386, ppc и sh, последнее для mips. 29-й сигнал -- это SIGINFO / SIGPWR
для alpha, но SIGLOST для sparc.)
85
Буквы в колонке "Действие" имеют следующее значение:
A - действие по умолчанию - прекращение выполнения процесса.
B - действие по умолчанию - игнорировать сигнал.
C - действие по умолчанию - прекращение выполнения процесса и запись
дампа памяти.
D - действие по умолчанию - приостановка выполнения процесса.
E - сигнал не может быть перехвачен.
F - сигнал не может быть проигнорирован.
86
Список литературы
1. Робачевский, А.М. Операционная система Unix: учеб. пособие для вузов /
А.М. Робачевский. – СПб.: БХВ-Санкт-Петербург, 1999. – 528 с.
2. Рочкинд М. Программирование для UNIX. 2-е изд. перераб. и доп. – Пер. с
англ. – М.: Издательско-торговый дом «Русская Редакция»; СПб.: БХВПетербург, 2005.-704с.
3. Стивенс У. UNIX: Взаимодействие процессов. – СПб.: Питер, 2002.
4. Стивенс У. UNIX: разработка сетевых приложений. – СПб: Питер, 2003.
5. Теренс Чан Системное программирование на С++ для UNIX: Пер. с англ. –
К.: Издательская группа BHV, 1999. – 592 с.
6. Кузьмин Д.А. Системное программное обеспечение. Введение в работу с
Unix. Метод. указания для студентов специальностей 210100-210400,
071900. – Красноярск ИПЦ КГТУ , 2002. – 18 с.
7. Кузьмин Д.А., Удалова Ю.В. Системное программное обеспечение.
Визуальное программирование в Linux: практикум. Учебное пособие. –
Красноярск ИПЦ КГТУ , 2005. –167 с.
Информационные ресурсы
1. Сводный информационный ресурс по Linux-о подобным ОС
http://www.linux.org
2. Википедия – свободная энциклопедия. Материалы по Linux
http://ru.wikipedia.org/wiki/Linux
3. Информационный ресурс CIT Forum // http://www.citforum.ru/
4. Журнал Linux Format // http://www.linuxformat.ru/
87
//
//
Словарь терминов
UNIX
—
группа
переносимых,
многозадачных
и
многопользовательских операционных систем. Первая система UNIX была
разработана в конце 1960-х — начале 1970-х годов в подразделении Bell Labs
компании AT&T. С тех пор было создано большое количество различных
UNIX-систем. Юридически лишь некоторые из них имеют полное право
называться «UNIX»; остальные же, хотя и используют сходные концепции и
технологии, объединяются термином «UNIX-подобные».
UNIX System V — одна из версий операционной системы UNIX,
разработанная в AT&T и выпущенная в 1989 г. System V Release 4.0 была
анонсирована в 1 ноября 1989 г. и выпущена в 1990 г. Это был совместный
проект UNIX Systems Laboratories и Sun Microsystems и содержал технологии
из Release 3, 4.3BSD, Xenix, и SunOS:
Multics (Multiplexed Information and Computing Service) была одной из первых
операционных систем с разделением времени исполнения программ (timesharing operating system). Разработка операционной системы Multics была
начата в 1964 году. Изначально в этом проекте были заняты Массачусетский
Технологический Институт (MIT), а также компании General Electric (GE) и
Bell Labs. Multics был задуман компанией General Electric как коммерческий
продукт. В 1969 году Компания Bell Labs вышла из проекта, а в 1970 году
компьютерный бизнес компании General Electric (вместе с Multics), отошел к
компании Honeywell.
GNU/Linux— свободная UNIX-подобная операционная система,
основана на системных программах, разработанных в рамках проекта GNU, и
на ядре Linux.
Qt —мультиплатформенный графический C++
инструментарий
разработки ПО от компании Trolltech (www.trolltech.com)
PID (Process IDentificator- идентификатор процесса) уникальный
числовой идентификационный номер процесса в операционной системе
PPID (Parent Process IDentifier - идентификатор родительского
процесса)
UID – идентификатор пользователя от имени которого запущен процесс
Управляющий терминал - имя терминального устройства, на которое
процесс выводит информацию и с которого информацию получает.
Процессы демоны - процессы, не привязанные к какому-то
конкретному терминалу называются "демонами" (daemons)
root – имя суперпользователя вUnix-о подобных ОС(от имени этого
пользователя запускаются все системные процессы)
Контекст процесса - контекст процесса включает в себя содержимое
адресного пространства задачи, выделенного процессу, а также содержимое
относящихся к процессу аппаратных регистров и структур данных ядра.
Каждому процессу соответствует контекст, в котором он выполняется.
88
Канал - средство связи стандартного вывода одного процесса со
стандартным вводом другого. Бывают 2-х видов – именованные(функция
mkfifo, команды mknod, mkfifo ) и неименованные(функция pipe)
Сигналы - Сигналы являются программными прерываниями, которые
посылаются процессу, когда случается некоторое событие(по механизмам
реализации различают надежные и ненадежные сигналы).
89
Документ
Категория
Без категории
Просмотров
43
Размер файла
1 274 Кб
Теги
компонентов, обеспечение, метод, разработка, Linux, процесс, учеб, системно, 811, программного
1/--страниц
Пожаловаться на содержимое документа