|
|||||||||||||||||||||||||||||||||||||
|
Разбиение С++ программ на множество задач
Параллельность в С++-программе достигается путем ее (программы) разложения на несколько процессов или потоков. Несмотря на существование различных вариантов организации логики С++-программы (например, с помощью объектов, функций или обобщенных шаблонов), под параллелизмом все же понимается использование множества процессов и потоков. Прочитав эту главу, вы поймете, что такое процесс и как С++-программы можно разделить на несколько процессов. Определение процесса Процесс (process) — это некоторая часть (единица) работы, создаваемая операционной системой. Важно отметить, что процессы и программы — необязательно эквивалентные понятия. Программа может состоять из нескольких процессов. В некоторых ситуациях процесс может быть не связан с конкретной программой. Процессы - это артефакты операционной системы, а программы — это артефакты разработчика. Такие операционные системы, как UNIX/Linux позволяют управлять сотнями или даже тысячами параллельно загружаемых процессов. Чтобы некоторую часть работы можно было назвать процессом, она должна иметь адресное пространство, назначаемое операционной системой, и идентификатор, или идентификационный номер (id процесса). Процесс должен обладать определенным статусом и иметь свой элемент в таблице процессов. В соответствии со стандартом POSIX он должен содержать один или несколько потоков управления, выполняющихся в рамках его Процесс состоит из множества выполняющихся инструкций, размещенных в адресном пространстве этого процесса. Адресное пространство процесса распределяется между инструкциями, данными, принадлежащими процессу, и стеками, обеспечивающими вызовы функций и хранение локальных переменных. Два вида процессов При выполнении процесса операционная система назначает ему некоторый процессор. Процесс выполняет свои инструкции в течение некоторого периода времени. Затем он выгружается, освобождая процессор для другого процесса. Планировщик операционной системы переключается с кода одного процесса на код другого, предоставляя каждому процессу шанс выполнить свои инструкции. Различают пользовательские процессы и системные. Процессы, которые выполняют системный код, называются системными и применяются к системе в целом. Они занимаются выполнением таких служебных задач, как распределение памяти, обмен страницами между внутренним и вспомогательным запоминающими устройствами, контроль устройств и т.п. Они также выполняют некоторые задачи «по поручению» пользовательских процессов, например, делают запросы на ввод-вывод данных, выделяют память и т.д. Пользовательские процессы выполняют собственный код и иногда обращаются к системным функциям. Выполняя собственный код, пользовательский процесс пребывает в пользовательском режиме (user mode). В пользовательском режиме процесс не может выполнять определенные привилегированные машинные команды. При вызове системных функций (например read(), write () или open ()) пользовательский процесс выполняет инструкции операционной системы. При этом пользовательский процесс «удерживает» процессор до тех пор, пока не будет выполнен системный вызов. Для выполнения системного вызова процессор обращается к ядру операционной системы. В это время о пользовательском процессе говорят, что он пребывает в привилегированном режиме, или режиме ядра (kernel mode), и не может быть выгружен никаким другим пользовательским процессом. Блок управления процессами Процессы имеют характеристики, используемые для идентификации и определения их поведения. Ядро поддерживает необходимые структуры данных и предоставляет системные функции, которые дают возможность пользователю получить доступ к этой информации. Некоторые данные хранятся в блоках управления процессами (process control block—PCB), или БУП. Данные, хранимые в БУП-блоках, описывают процесс с точки зрения потребностей операционной системы. С помощью этой информации операционная система может управлять каждым процессом. Когда операционная система переключается с одного процесса на другой, она сохраняет текущее состояние выполняющегося процесса и его контекст в области сохранения БУП-блока, чт обы надлежащим образом возобновить выполнение этого процесса в следующий раз, когда ему снова будет выделен центральный процессор (ЦП). БУП-блок считывается и обновляется различными модулями операционной системы. Модули «отвечают» за контроль производительности операционной системы, планирование, распределение ресурсов и доступ к механизму обработки прерываний и/или модифицируют БУП-блок. Блок БУП содержит следующую информацию: • текущее состояние и приоритет процесса; • идентификатор процесса, а также идентификаторы родительского и сыновнего процессов; • указатели на выделенные ресурсы; • указатели на область памяти процесса; • указатели на родительский и сыновний процесс; • процессор, занятый процессом; • регистры управления и состояния; • стековые указатели. Среди данных, содержащихся в БУП-блоке, есть такие, которые «отвечают» за управление процессом, т.е. отражают его текущее состояние и приоритет, указывают на БУП-блоки родительского и сыновнего процессов, а также выделенные ресурсы и память. Кроме того, этот блок включает информацию, связанную с планированием, привилегиями процессов, флагами, сообщениями и сигналами, которыми обмениваются процессы (имеется в виду межпроцессное взаимодействие— mterprocess communication, или IPC). С помощью информации, связанной с управлением процессами, операционная система может координировать параллельно выполняемые процессы. Стековые указатели и содержимое регистров пользователя, управления и состояния содержат информацию, связанную с состоянием процессора. При выполнении процесса соответствующая информация размещается в регистрах ЦП. При переключении операционной системы с одного процесса на другой вся информация из этих регистров сохраняется. Когда процесс снова получает ЦП во «временное пользование», ранее сохраненная информация может быть восстановлена. Есть еще один вид информации, который связан с идентификацией процесса. Имеется в виду идентификатор процесса (id), или PID, и идентификатор родительского процесса (PPID). Эти идентификационные номера (которые представлены положительными целочисленными значениями) уникальны для каждого процесса. Анатомия процесса Адресное пространство процесса делится на три логических раздела: текстовый (для кода программы), информационный (для данных программы) и стековый (для стеков программы). Логическая структура процесса показана на рис.3.1. Текстовый раздел (расположенный в нижней части адресного пространства) содержит подлежащие выполнению инструкции, которые называются программным кодом. Раздел данных (расположенный над текстовым разделом) содержит инициализированные глобальные, внешние и статические переменные процесса. Раздел стеков содержит локально создаваемые переменные и параметры, передаваемые функциям. Поскольку процесс может вызывать как системные функции, так и функции, определенные пользователем, в стековом разделе поддерживаются два стека: стек пользователя и стек ядра. При вызове функции создается стековый фрейм функции, который помещается в стек пользователя или стек ядра в зависимости от того , в каком режиме пребывает процесс в данный момент: в пользовательском или привилегированном (режиме ядра). Стековый раздел имеет тенденцию расти в направлении раздела данных. При выходе из функции ее стековый фрейм извлекается из стека. Разделы кода, данных и стеков, а также блок управления процессом образуют часть того, из чего складывается образ процесса (process image).
Адресное пространство процесса виртуально. Применение виртуальной памяти позволяет отделить адреса, используемые в текущем процессе, от адресов, реально доступных во внутренней памяти. Тем самым значительно увеличивается задействованное пространство адресов памяти по сравнению с реально доступными адресами. Разделы виртуального адресного пространства процесса представляют собой смежные блоки памяти. Каждый такой раздел и физическое адресное пространство разделены на участки памяти, именуемые страницами. У каждой страницы есть уникальный номер страничного блока (page frame number). В качестве индекса для входа в таблицы страничных блоков (page frame table) используется номер виртуального страничного блока. Каждый элемент таблицы страничных блоков содержит номер физического страничного блока, что позволяет установить соответствие между виртуальными и физическими страничными блоками. Это соответствие отображено на рис. 3.2. Как видите, виртуальное адресное пространство непрерывно, но устанавливаемое с его помощью соответствие физическим страницам не является упорядоченным. Другими словами, при последовательных виртуальных адресах соответствующие им физические страницы не будут последовательными. Несмотря на то что виртуальное адресное пространство каждого процесса защищено, т.е. приняты меры по предотвращению доступа к нему со стороны другого процесса, текстовый раздел [6] процесса может совместно использоваться несколькими процессами. На рис. 3.2 также показано, как два процесса могут разделять один и тот же программный код. При этом в элементах таблиц страничных блоков обоих процессов хранится один и тот же номер физического страничного блока. Как показано на рис. 3.2, виртуальный страничный блок с номером 0 процесса А соответствует физическому страничному блоку с номером 5, что также справедливо и для виртуального страничного блока с номером 2 процесса В. Рис. 3.2. Соответствие последовательных виртуальных страничных блоков страницам физической памяти (НСБ — номер страничного блока; НВСБ— номер виртуального страничного блока) Чтобы операционная система могла управлять всеми процессами, хранимыми во внутренней памяти, она создает и поддерживает таблицы процессов (process table). В действительности операционная система содержит отдельные таблицы для всех объектов, которыми она управляет. Следует иметь в виду, что операционная система управляет не только процессами, но и всеми ресурсами компьютера, т.е. устройствами ввода-вывода, памятью и файлами. Часть памяти, устройств и файлов управляется от имени пользовательских процессов. Эта информация отмечена в БУП-блоках как ресурсы, выделенные процессу. Таблица процессов должна иметь соответствующую структуру для каждого образа процесса в памяти. Каждая такая структура содержит идентификаторы (id) самого процесса и родительского процесса, идентификаторы реального и эффективного пользователей, идентификатор группы, список подвешенных сигналов, местоположение текстового, информационного и стекового разделов, а также текущее состояние процесса. Если операционной системе нужен доступ к определенному процессу, в таблице процессов разыскивается информация о нем, а затем в памяти размещается его образ (рис. 3.3). Рис. 3.3. Операционная система управляет таблицами. Каждая структура в массиве таблиц процессов представляет процесс в системе Состояния процессов Во время выполнения процесса его состояние изменяется. Под состоянием процесса подразумевается его текущий режим, или статус. В среде UNIX процесс может пребывать в одном из следующих состояний: • выполнения; • работоспособности (готовности); • «зомби»; • ожидания (блокирования); • останова. Состояние процесса меняется при определенных обстоятельствах, создаваемых существованием процесса или операционной системы. Под сменой состояний, или переходом из одного состояния в другое, понимают обстоятельства, которые заставляют процесс изменить свое состояние. На рис. 3.4 отображена диаграмма состояний для среды UNIX. Диаграмма состояний содержит узлы и направленные ребра, соединяющие эти узлы. Каждый узел представляет состояние процесса, а направленные ребра между узлами — переходы из одного состояния в другое. Возможные смены состояний (с их кратким описанием) перечислены в табл. 3.1. На рис. 3.4 и в табл. 3.1 показано, что между состояниями разрешены только определенные переходы. Например, между состояниями готовности и выполнения существует переход (ребро диаграммы), а между состояниями ожидания и выполнения — нет. Это означает, что возможны обстоятельства, заставляющие процесс перейти из состояния готовности в состояние выполнения, но нет обстоятельств, которые могут заставить процесс перейти в состояние выполнения из состояния ожидания. Рис. 3.4. Состояния процессов и переходы между ними в средах UNIX/Linux Когда процесс только создается, он готов к выполнению своих инструкций, но должен ожидать «своего часа» до тех пор, пока не освободится процессор. Каждому процессу единолично разрешается использовать процессор в течение дискретного временного интервала, именуемого квантом времени (time slice). Процессы, ожидающие использования процессора, «занимают» очередь, т.е. помещаются в очереди готовых процессов. Только из таких очередей планировщик выбирает процесс, который будет использовать процессорное время. Процессы, находящиеся в очередях готовых процессов, пребывают в состоянии работоспособности. Когда процессор становится доступным, Таблица 3 .1 Переходы процессов из одного состояние в другое Готовый > выполняющийся (загрузка) Процесс назначается процессору Выполняющийся >готовый (конец кванта времени) Квант времени процесса, который назначен процессору, истек. Процесс возвращается назад в очередь готовых процессов Выполняющийся > готовый (досрочная выгрузка) Процесс выгружается до истечения его кванта времени. (Это возможно в случае, если стал готовым процесс с более высоким приоритетом.) Выгруженный процесс помещается назад в очередь готовых процессов Выполняющийся > ожидающий (блокировка) Процесс отказывается от процессора до истечения его кванта времени. Процессу, возможно, нужно подождать наступления некоторого события, или он вызывает системную функцию, например, делает запрос на ввод-вывод данных. Процесс помещается в очередь ждущих процессов Ожидающий > готовый (разблокировка) Событие, наступления которого ожидал процесс, произошло, или завершилось выполнение системной функции, например, удовлетворен запрос на ввод-вывод данных Выполняющийся > остановленный Процесс отказывается от процессора из-за получения им сигнала останова Остановленный > готовый Процесс получил сигнал продолжать и возвращается назад в очередь готовых процессов Выполняющийся > «Зомби» Процесс прекращен и ожидает, пока родительский процесс не извлечет из таблицы процессов его статус завершения «Зомби» > ВЫХОД Родительский процесс извлекает из таблицы процессов статус завершения и процесс-зомби покидает систему Выполняющийся > ВЫХОД Процесс завершен, но он покидает систему после того как родительский процесс извлечет из таблицы процессов его статус завершения Диспетчер (dispatcher) назначает его работоспособному (готовому) процессу, который занимает его в течение своего кванта времени. По истечении этого кванта времени процесс покидает процессор, независимо от того, выполнил он все свои инструкции или нет. Этот процесс снова помещается в очередь готовых процессов (как в «зал ожидания») ожидать следующего сеанса работы процессора. Тем временем из очереди выбирается новый процесс, которому выделяется его квант процессорного времени. Системные процессы не выгружаются, т.е., «заполучив» процессор, они выполняются до полного завершения. Если квант времени еще не исчерпан, но процесс не в состоянии продолжить выполнение, он может добровольно отказаться от процессорного времени. Причины отказа могут быть разными. Например, процесс может сделать запрос на получение доступа к устройству ввода-вывода, вызвав системную функцию, или ему необходимо подождать освобождения объекта (переменной) синхронизации. Процессы, которые не могут продолжать выполнение из-за необходимости ожидать некоторого события, «засыпают», т.е. переходят в состояние ожидания. Они помещаются в очередь ждущих процессов. После наступления ожидаемого ими события они удаляются из этой очереди и возвращаются в очередь готовых процессов. Текущий процесс, т.е. процесс, занимающий процессорное время, может быть лишен его еще до исчерпания кванта времени, если заявит о своей готовности процесс с более высоким приоритетом (например, системный процесс). Выгруженный досрочно процесс сохраняет статус работоспособного и поэтому снова помещается в очередь готовых процессов. Выполняющийся процесс может получить сигнал остановить выполнение. Состояние останова отличается от состояния ожидания, потому что при этом не был исчерпан квант времени и процесс не делал никакого системного запроса. Процесс мог получить сигнал остановиться либо по причине пребывания в режиме отладки, либо из-за возникновения особой ситуации в системе. Получив сигнал остановиться, процесс переходит из состояния выполнения в состояние останова. Позже процесс может быть «разбужен» или ликвидирован. Выполнив все свои инструкции, процесс покидает систему. В этом случае процесс удаляется из таблицы процессов, его БУП-блок разрушается, и все занимаемые им ресурсы освобождаются и возвращаются в системный пул доступных ресурсов. Процесс, который неспособен продолжать выполнение, но при этом не может выйти из системы, считается «зомбированным». Зомбированный процесс не использует никаких системных ресурсов, но сохраняет свою структуру в таблице процессов. Если в таблице процессов окажется слишком много зомбированных процессов, это негативно отразится на производительности системы и может вызвать ее перезагрузку. Планирование процессов Если готовых к выполнению процессов больше одного, планировщик должен определить, какой из них первым назначить процессору. С этой целью планировщик поддерживает структуры данных, которые позволяют наиболее эффективным образом распределять между процессами процессорное время. Каждый процесс получает класс (тип) приоритета и размещается в соответствующей очереди вместе с другими работоспособными процессами того же приоритетного класса. Поэтому существует несколько приоритетных очередей, которые представляют различные классы приоритетов, используемые системой. Эти приоритетные очереди упорядочиваются и помещаются в массив распределения, именуемый также многоуровневой приоритетной очередью (multilevel priority queue), показанной на рис. 3.5 . Каждый элемент этого массива связан с конкретной приоритетной очередью. Для выполнения процессором планировщик назначает тот процесс, который стоит в головной части непустой очереди, имеющей самый высокий приоритет. Приоритеты могут быть динамическими или статическими. Однажды установленный статический приоритет процесса изменить нельзя, а динамические — можно. Процессы с самым высоким приоритетом могут монополизировать использование процессора. Если же приоритет процессора динамический, то его начальный уровень может быть заменен более высоким значением, в результате чего такой процесс будет переведен в очередь с более высоким приоритетом. Кроме того, процесс, который монополизирует процессор, может получить более низкий приоритет, или же другие процессы могут получить более высокий приоритет, чем процесс-монополист. В средах UNIX/Linux для уровней приоритетов предусмотрен диапазон от -20 до 19. Чем выше значение уровня, тем ниже приоритет процесса. При назначении приоритета пользовательскому процессу следует учитывать, на что именно этот процесс тратит большую часть времени. Одни процессы отличаются повышенной интенсивностью использования процессорного времени (они используют процессор в течение всего кванта процессорного времени). У других же большая часть времени уходит на ожидание выполнения операций ввода-вывода или наступления некоторых иных событий. Если такой процесс готов к использованию процессора, ему следует немедленно предоставить процессор, чтобы он мог сделать следующий запрос к устройствам ввода-вывода. Процессы, которые взаимодействуют между собой, могут требовать довольно высокий приоритет, чтобы рассчитывать на приличное время реакции. Системные процессы имеют более высокий приоритет, чем пользовательские. Рис. 3.5. Многоуровневая приоритетная очередь (массив распределения), каждый элемент которой указывает на очередь готовых процессов с одинаковым уровнем приоритета Стратегия планирования Процессы размещаются в приоритетных очередях в соответствии со стратегией Планирования. В системах UNIX/Linux используются две стратегии планирования: FIFO (сокр. от First In First Out, т.е. первым прибыл, первым обслужен) и RR (сокр. От round-robin, т.е. циклическая). Схема действия стратегии FIFO показана на рис. 3.6, а. При использовании стратегии FIFO процессы назначаются процессору в соответствии со временем поступления в очередь. После истечения кванта времени процесс помещается в начало (головную часть) своей приоритетной очереди. Когда ждущий процесс становится работоспособным (готовым к выполнению), он помещается в конец своей приоритетной очереди. Процесс может вызвать системную функцию и отказаться от процессора в пользу другого процесса с таким же уровнем приоритета. Такой процесс также будет помещен в конец своей приоритетной очереди. Рис.3.6. Схемы действия FIFO- и RR-стратегий планирования При использовании стратегии FIFO процессы назначаются процессору в соответствии со временем поступления в очередь. При использовании стратегии RR процессы назначаются процессору по правилам FIFO-стратегии, но с одним отличием: после истечения кванта времени процесс помещается не в начало, а в конец своей приоритетной очереди В соответствии с циклической стратегией планирования (RR) все процессы счи таются равноправными (см. рис. 3.6, б) . RR-планирование совпадает с FIFO-планированием с одним исключением: после истечения кванта времени процесс помещает ся не в начало, а в конец своей приоритетной очереди, и процессору назначается след ующий (по очереди) процесс. Использование утилиты ps Утилита ps генерирует отчет, который содержит статистические данные о выполнении текущих процессов. Эту информацию можно использовать для контроля за их состоянием. В табл. 3.8 перечислены общие заголовки и описаны выходные данные, генерируемые утилитой ps для сред Solaris/Linux. В любой многопроцессорной среде утилита ps успешно применяется для мониторинга состояния процессов, степени использования ЦП и памяти, приоритетов и времени запуска текущих процессов. Ниже приведены командные опции, которые позволяют управлять информацией, содержащейся в отчете (с их помощью можно уточнить, что именно и какие процессы вас интересуют). В среде Solaris по умолчанию (без командных опций) отображается информация о процессах с тем же идентификатором эффективного пользователя и управляющим терминалом инициатора вызова. В среде Linux по умолчанию отображается информация о процессах, id пользователя которых совпадает с id инициатора запуска. В обеих средах в этом случае отображаемая информация, ограниченная следующими составляющими: PID, TTY, TIME и COMMAND. Перечислим опции, которые позволяют получить информацию о нужных процессах. -t term Список процессов, связанных с терминалом, заданным значением term -e Все текущие процессы -a (Linux) Все процессы с терминалом tty за исключением лидеров сеанса (Solaris) Большинство часто запрашиваемых процессов за исключением лидеров группы и процессов, не связанных с терминалом -d Все текущие процессы за исключением лидеров сеанса T (Linux) Все процессы, связанные с данным терминалом a (Linux) Все процессы, включая процессы остальных пользователей r (Linux) Только выполняющиеся процессы Таблица 3 .2. Общие заголовки, используемые для утилиты ps в средах Solaris/Linux USER, UID Пользовательское имя владельца процесса PID ID процесса PPID ID родительского процесса PGID ID лидирующего процесса в группе SlD ID лидера сеанса %CPU Коэффициент использования времени ЦП (в процентах) процессом в течение последней минуты RSS Объем реального ОЗУ, занимаемый процессом в данный момент (в Кбайт) %MEM Коэффициент использования реального ОЗУ процессом в течение последней минуты SZ Размер виртуальной памяти, занимаемой данными и стеком процесса (в Кбайт или страницах) WCHAN Адрес события, в ожидании которого процесс пребывает в состоянии ожидания COMMAND Имя команды и аргументы CMD TT, TTY Управляющий терминал процесса S, STAT Текущее состояние процесса TIME Общее время ЦП, используемое процессом (HH:MM:SS) STIME, START Время или дата старта процесса NI Фактор уступчивости процесса PRI Приоритет процесса С, CP Коэффициент краткосрочного использования ЦП для вычисления планировщиком значения PRI ADDR Адрес памяти, выделенной процессу LWP ID потока NLWP Количество потоков В следующий список включены командные опции, которые используются для управления отображаемой информацией о процессах: – f полные распечатки – -l в длинном формате – - j в формате задания Приведем пример использования утилиты ps в средах Solaris/Linux: ps -f По этой команде будет отображена полная информация о процессах, которая выводится по умолчанию в каждой среде. На рис. 3.7 показан результат выполнения этой команды в среде Solaris. Командные опции можно использовать тандемом (одна за другой). На рис 3 7 также показан результат совместного использования опций -l и -f в среде Solaris: ps -lf Командная опция l позволяет отобразить дополнительные заголовки: F, S, С, PRI, NI , ADDR и WCHAN. При использовании командной опции P отображается заголовок PSR, означающий номер процессора, которому назначается (или за которым закрепляется) процесс. $ ps -f UID PID PPID C STIME TTY TIME CMD cameron 2214 2212 0 21:03:35 pts/12 0:00 -ksh cameron 2396 2214 2 11:55:49 pts/12 0:01 nedit $ ps -lf F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD 8 S cameron 2214 2212 0 51 20 70e80f00 230 70e80f6c 21:03:35 pts/12 0:00 -ksh 8 S cameron 2396 2214 1 53 24 70d747b8 843 70152aba 11:55:49 pts/12 0:01 nedit Рис. 3.7. Результат выполнения команд ps -f и ps -lf в среде Solaris На рис. 3.8 показан результат выполнения утилиты ps с использованием командных опций Tux в среде Linux. Данные, выводимые с помощью заголовков %CPU, %MEM и STAT, отображаются для процессов. В многопроцессорной среде с помощью этой информации можно узнать, какие процессы являются доминирующими с точки зрения использования времени ЦП и памяти. Заголовок STAT отображает состояние или статус процесса. Ниже приведены символы, обозначающие статус, и дано соответствующее описание. Заголовок STAT позволяет узнать дополнительную информацию о статусе процесса. D (BSD) Ожидание доступа к диску P (BSD) Ожидание доступа к странице X (System V) Ожидание доступа к памяти W (BSD) Процесс выгружен на диск К (AIX) Доступный процесс ядра N (BSD) Приоритет выполнения понижен > (BSD) Приоритет выполнения повышен искусственно < (Linux) Процесс с высоким приоритетом L (Linux) Страницы заблокированы в памяти Эти символы должны предшествовать коду статуса. Например, если перед кодом статуса стоит символ N, значит, процесс выполняется с более низким уровнем приоритета. Если код статуса процесса отображен символами SW<, это означает, что процесс пребывает в ждущем режиме, выгружен и имеет высокий уровень приоритета. Установка и получение приоритета процесса Уровень приоритета процесса можно изменить с помощью функции nice (). Каждый процесс имеет фактор уступчивости (nice value), который используется для вычисления уровня приоритета вызывающего процесса. Процесс наследует приоритет процесса, который его создал. Чтобы понизить приоритет процесса, следует увеличить его фактор уступчивости. Лишь процессы привилегированных пользователей и ядра системы могут увеличивать уровни своих приоритетов. Синопсис #include <unistd.h> int nice(int incr); Чем ниже фактор уступчивости, тем выше уровень приоритета процесса. Параметр incr содержит значение, добавляемое к текущему фактору уступчивости вызывающего процесса. Значение параметра incr может быть отрицательным или положительным, а фактор уступчивости представляет собой неотрицательное число. Положительное значение incr увеличивает фактор уступчивости, а значит, понижает уровень приоритета. Отрицательное значение incr уменьшает фактор уступчивости, тем самым повышая уровень приоритета. Если значение incr изменяет фактор уступчивости выше или ниже соответствующих предельных величин, он будет установлен равным самому высокому или самому низкому пределу соответственно. При успешном выполнении функция nice () возвращает новый фактор уступчивости процесса, в противном случае — число -1, а прежнее значение фактора уступчивости при этом не изменяется. Синопсис #include <sys/resource.h> int getpriority(int which, id_t who); int setpriority(int which, id_t who, int value); _ Функция setpriority() устанавливает фактор уступчивости для заданного процесса, группы процессов или пользователя. Функция getpriority() возвращает приоритет заданного процесса, группы процессов или пользователя. Синтаксис использования функций setpriority() и getpriority() для установки и считывания фактора уступчивости текущего процесса демонстрируется в листинге 3.1. Листинг 3.1. Использование функций setpriority() и getpriority() #include <sys/resource.h> //... id_t pid = 0; int which = PRIO_PROCESS; int value = 10; int nice_value; int ret; nice_value = getpriority(which,pid); if(nice_value < value){ ret = setpriority(which,pid,value); } //.-• В листинге 3.1 возвращается и устанавливается приоритет вызывающего процесса. Если фактор уступчивости вызывающего процесса оказывается меньше 10, он устанавливается равным 10. Процесс задается значениями, хранимыми в параметрах which и who (см. соответствующий синопсис). Параметр which может определять процесс, группу процессов или пользователя и иметь следующие значения. PRIO_PROCESS Означает процесс PRIO_PGRP Означает группу процессов PRIO_USER Означает пользователя В зависимости от значения параметра which параметр who содержит идентификационный номер (id) процесса, группы процессов или эффективного пользователя. В листинге З.1 параметру which присваивается значение PRIO_PROCESS. В листинге З.1 параметр who устанавливается равным 0, означая тем самым текущий процесс. Параметр value для функции setpriority() определяет новое значение фактора уступчивости для заданного процесса, группы процессов или пользователя. Факторы уступчивости в среде Linux должны находиться в диапазоне от -20 до 19 . В листингe 3.1 фактор уступчивости устанавливается равным 10, если текущее его значение оказывается меньше 10 . В отличие от функции nice(), значение, передаваемое функции setpriority(), является фактическим значением фактора уступчивости, а не смещением, которое суммируется с текущим фактором уступчивости. Если процесс имеет несколько потоков, модификация приоритета процесса повлияет на приоритет всех его потоков. При успешном выполнении функции getpriority() возвращается фактор уступчивости заданного процесса, а при успешном выполнении функции setpriority () — значение 0. В случае неудачи обе функции возвращают число -1. Однако число -1 является допустимым значением фактора уступчивости для любого процесса. Чтобы уточнить, не было ли ошибок при выполнении функции getpriority(), имеет смысл протестировать внешнюю переменную errno . Переключение контекста Переключение контекста происходит в момент, когда процессор переключается с одного процесса на другой. При переключении контекста система сохраняет контекст текущего процесса и восстанавливает контекст следующего процесса, выбранного для использования процессора. БУП-блок прерванного процесса при этом обновляется, а также изменяется значение поля состояния процесса (т.е. признак состояния выполнения заменяется признаком другого состояния: готовности, блокирования или «зомби»). Сохраняется и обновляется содержимое регистров процессора, состояние стека, данные об идентификации (и привилегиях) пользователя и процесса, а также о стратегии планирования и учетная информация. Система должна отслеживать статус устройств ввода-вывода процесса и других ресурсов, а также состояние всех структур данных, связанных с управлением памятью. Вы г руженный (прерванный) процесс помещается в соответствующую очередь. Переключение контекста происходит в случаях, когда: • процесс выгружается; • процесс добровольно отказывается от процессора; • процесс делает запрос к устройству ввода-вывода или должен ожидать наступления события; • процесс переходит из пользовательского режима в режим ядра. Когда выгруженный процесс снова выбирается для использования процессора, его контекст восстанавливается, и выполнение продолжается с точки, на которой он был прерван в предыдущем сеансе. Создание процесса Чтобы выполнить любую программу, операционная система должна сначала создать процесс. При создании нового процесса в главной таблице процессов создается новая структура. Создается и инициализируется новый блок БУП, и в его раздел идентификации процесса записывается уникальный идентификационный номер процесса (id) и id родительского процесса. Программный счетчик устанавливается указателем на входную точку программы, а указатели системных стеков устанавливаются таким образом, чтобы определить стековые границы для процесса. Процесс инициализируется любыми требуемыми атрибутами. Если процессу не присвоено значение приоритета, то по умолчанию ему присваивается самое низкое значение. Изначально процесс не обладает никакими ресурсами, если нет явного запроса на ресурсы или если они не были унаследованы от процесса-создателя. Процесс «входит» в состояние выполнения и помещается в очередь готовых к выполнению процессов. Для него выделяется адресное пространство, размер которого определяется по умолчанию на основе типа процесса. Кроме того, размер можно установить по запросу от создателя процесса. Процесс-создатель может передать системе размер адресного пространства в момент создания процесса. Отношения между родительскими и сыновними процессами Процесс, который создает, или порождает, другой процесс, является родительским (parent) процессом по отношению к порожденному, или сыновнему (child) процессу. Процесс init — родитель (или предок) всех пользовательских процессов — первый процесс, видимый системой UNIX после ее загрузки. Процесс init организует систему, при необходимости выполняет другие программы и запускает демон-программы (daemon), т.е. сетевые программы, работающие в фоновом режиме. Идентификатор процесса init (PID) равен 1. Сыновний процесс имеет собственный уникальный идентификатор PID, БУП-блок и отдельную структуру в таблице процессов. Сыновний процесс также может породить новый процесс. Выполняющееся приложение может создать дерево процессов. Например, родительский процесс выполняет поиск накопителя на жестких дисках для заданного HTML-документа. Имя этого HTML-документа записано в глобальной структуре данных, подобной списку, который содержит все запросы на документы. После успешного обнаружения документ удаляется из списка запросов, и его путь (маршрут в сети) записывается в другую глобальную структуру данных, которая содержит пути найденных документов. Чтобы обеспечить Приемлемую реакцию на пользовательские запросы, для процесса предусматривается ограничение в виде пяти необработанных запросов в списке. По достижении этого Предела порождаются два новых процесса. Если порожденный процесс в свою очередь достигнет установленного предела, он создаст еще два новых процесса. Создаваемое таким способом дерево процессов показано на рис. 3.9. Любой процесс может иметь только один родительский, но множество сыновних процессов. Рис. 3.9. Дерево процессов. При определенных условиях процесс порождает два новых потомка Сыновний процесс может быть создан с собственным исполняемым образом или в в иде дубликата родительского процесса. При создании в качестве дубликата предка сыновний процесс наследует множество его атрибутов, включая среду, приоритет, стратегию планирования, ограничения по ресурсам, открытые файлы и разделы общей памяти. Если сыновний процесс перемещает указатель текущей позиции в файле или закрывает файл, то результаты этих действий будут видны родительскому процессу. Если родителю выделяются любые дополнительные ресурсы уже после создания процесса-потомка, то они не будут доступны потомку. В свою очередь, если сыновний процесс использует какие-либо ресурсы, они также будут недоступны для процесса-родителя. Некоторые атрибуты родителя не наследуются потомком. Как упоминалось выше, сыновний процесс не наследует PID родителя и его БУП-блок. Потомок не наследует никаких файловых блокировок, созданных родителем или необработанными сигна лами . Д ля сыновнего процесса используются собственные значения таких временных характеристик, как коэффициент загрузки процессора и время создания. Несмотря на то, что сыновние процессы связаны определенными отношениями с родителями, они все же функционируют как отдельные процессы. Их программные и стековые счетчики действуют раздельно. Поскольку разделы данных копируются, а не используются совместно, процесс-потомок может изменять значения своих переменных, не оказывая влияния на родительскую копию данных. Родительский и сыновний процесс совместно используют раздел программного кода и выполняют инструкции, расположенные непосредственно после вызова системной функции, создавшей сыновний процесс. Они не выполняют эти инструкции на этапе блокировки из-за соперничества за процессор со всеми остальными процессами, загруженными в память. После создания образ сыновнего процесса может быть заменен другим исполняемым образом. Разделы программного кода, данных и стеков, а также его «куча» памяти перезаписывается новым образом процесса. Новый процесс сохраняет свои идентификационные номера (PID и PPID). Атрибуты, сохраняемые новым процессом после замены его исполняемого образа, перечислены в табл. 3.3. В ней также указаны системные функции, которые возвращают эти атрибуты. Переменные среды также сохраняются, если во время замены исполняемого образа процесса не были заданы новые переменные среды. Файлы, которые были открыты до момента замены исполняемого образа, остаются открытыми. Новый процесс будет создавать файлы с теми же файловыми разрешениями. Время ЦП при этом не сбрасывается. Таблица 3.3. Атрибуты, сохраняемые новым процессом после замены его исполняемого образа образом нового процесса Сохраняемые атрибуты Функция
Утилита pstree Утилита pstree в среде Linux отображает дерево процессов (точнее, она отображает выполняющиеся процессы в форме древовидной структуры). Корнем этого дерева является процесс init. pstree [-a] [-c] [-h | -Hpid] [-l] [-n] [-p] [-u] [-G] | -U] [pid | user] pstree -V При вызове этой утилиты можно использовать следующие опции, -а Отобразить аргументы командной строки, -h Выделить текущий процесс и его предков. -H Аналогично опции -h, но выделению подлежит заданный процесс. -n Отсортировать процессы с одинаковым предком по значению PID, а не по имени, -p Отобразить значения PID. На рис. 3.10 показан результат выполнения команды pstree -h в среде Linux. ka:~ # pstree -h init-+-applix |-atd |-axmain |-axnet |-cron |-gpm |-inetd |-9*[kdeinit] |-kdeinit -+-kdeinit | |-kdeinit---bash---gimp---script-fu | '-kdeinit---bash -+-man---sh---sh---less | '-pstree |-kdeinit---cat |-kdm-+-X | '-kdm---kde---ksmserver |-kflushd |-khubd |-klogd |-knotify |-kswapd |-kupdate |-login---bash |-lpd |-mdrecoveryd |-5*[mingetty] |-nscd---nscd---5*[nscd] |-sshd |-syslogd |-usbmgr '-xconsole Ри с . 3.10. Результат выполнения команды pstree -h в среде Linux Использование системной функции fork() Системная функция (или системный вызов) fork () создает новый процесс, который представляет собой дубликат вызывающего процесса, т.е. его родителя. При успешном выполнении функция fork () возвращает родительскому и сыновнему процессам два различных значения. Сыновнему возвращается число 0, а родительскому - значение PID сыновнего процесса. Родительский и сыновний процессы продолжают выполняться с инструкции, непосредственно следующей за функцией fork (). В случае неудачного выполнения (оно выражается в том, что сыновний процесс не был создан) родительскому процессу возвращается число -1. Синопсис #include <unistd.h> pid_t fork(void); _ Неудачный исход функции fork () возможен в случае, если система не обладает ресурсами для создания еще одного процесса. Это происходит при превышении ограничения (если оно существует) на количество сыновних процессов, которое может порождать родитель, или на количество выполняющихся процессов в масштабе всей системы. В этом случае устанавливается переменная errno, которая означает наличие ошибки. Использование семейства системных функций exec Семейство функций exec предназначено для замены образа вызывающего процесса образом нового процесса. При вызове функции fork () создается новый процесс, который является точной копией родительского процесса, а функция exec () заменяет образ «скопированного» процесса образом копии. Образ нового процесса представляет собой обычный выполняемый файл, который немедленно запускается на выполнение. Этот файл можно задать с помощью имени и пути доступа к нему. Функции семейства exec могут передать новому процессу аргументы командной строки, а также установить переменные среды. Если функция выполнилась успешно, она не возвращает никакого значения, поскольку образ процесса, который содержал обращение к функции exec, уже перезаписан. В случае неудачи вызывающему процессу возвращается число -1. Все функции exec () могут иметь неудачный исход при следующих условиях: • разрешения не признаны; разрешение на поиск отвергается для каталога выполняемых файлов; разрешение на выполнение отвергается для выполняемого файла; • файлы не существуют, выполняемый файл не существует; каталог не существует; • файл невозможно выполнить; файл невозможно выполнить, поскольку он открыт для записи другим процессом; файл не является выполняемым; пр облемы с символическими ссылками; при анализе пути к исполняемому файлу символические ссылки образуют циклы; символические ссылки делают путь к исполняемому файлу слишком длинным. Функции семейства exec используются совместно с функцией fork (). Функция fork () создает и инициализирует сыновний процесс «по образу и подобию» родительского. Образ сыновнего процесса затем заменяет образ своего предка посредством вызова функции exec (). Пример использования функций fork() и exec() показан в листинге 3.2. //Лис тинг 3.2. Использование системных функций fork() и exec() RtValue = fork(); if(RtValue == 0){ execl("/path/direct»,«direct»,".»); } В листинге 3.2 демонстрируется вызов функции fork(). Значение, которое она возвращает, сохраняется в переменной RtValue. Если значение RtValue равно 0, значит, это — сыновний процесс, и в нем вызывается функция execl() с параметрами. Первый параметр содержит путь к выполняемому модулю, второй — инструкцию для выполнения, а третий — аргумент. Второй параметр, direct, представляет собой имя утилиты, которая перечисляет все каталоги и подкаталоги из данного каталога. Всего существует шесть версий функций exec, предназначенных для использования различных соглашений о вызовах. Функции execl () Функции execl (), execle () и execlp () передают аргументы командной строки в виде списка. Количество аргументов командной строки должно быть известно во время компиляции. • int execl(const char *path,const char *arg0,.../*,(char * )0 */); Здесь path — путевое имя выполняемой программы. Его можно задать в виде полного составного имени либо относительного составного имени из текущего каталога. Последующие параметры представляют собой список аргументов командной строки, от arg0 до argn. Всего может быть n аргументов. Этот список завершается NULL -указателем. • int execle(const char *path,const char *arg0,.../*,(char *)0 *, char *const envp[]*/); Эта функция аналогична функции execl () с одним отличием: она имеет дополнительный параметр, envp[]. Этот параметр указывает на новую среду для нового процесса, т.е. envp[] — это указатель на строковый массив с завершающим нулевым символом. Каждая его строка, также завершающаяся нулевым символом, имеет следующую форму: name=value Здесь name — имя переменной среды, а value — сохраняемая строка. Значение параметру envp [] можно присвоить следующим образом: char *const envp[] = {«PATH=/opt/kde2:/sbin», «HOME=/home»,NULL}; Здесь PATH и НОМЕ — переменные среды. • int execlp(const char *file,const char *arg0,.../*, (char *)0 */); Здесь file — имя выполняемой программы. Для определения местоположения выполняемых программ используется переменная среды PATH. Остальные параметры представляют собой список аргументов командной строки (см. описание функции execl() ) . Вот примеры применения синтаксиса функций execl () с различными аргументами: char *const args[] = {«direct»,".»,NULL}; char *const envp[] = {«files=50»,NULL}; execl("/path/direct», «direct», ".», NULL) ; execle("/path/direct»,«direct»,".»,NULL,envp); execlp(«direct», «direct», " . ",NULL) ; Здесь в каждом примере вызова execl -функции активизированный процесс выполняет программу direct. Синопсис #include <unistd.h> int execl(const char *path,const char *arg0,.../*,(char *)0 */); int execle(const char *path,const char *arg0,.../*,(char *)0 *,char *const envp[]*/); int execlp(const char *file,const char *arg0,.../*,(char *)0 */); int execv(const char *path,char *const arg[]); int execve(const char *path,char *const arg[],char *const envp[]); int execvp(const char *file,char *const arg[]); Функции execv () Функции execv(), execve() и execvp() передают аргументы командной строки в векторе указателей на строки с завершающим нулевым символом. Количество аргументов командной строки должно быть известно во время компиляции. Элемент argv[0] обычно представляет собой команду. • int execv(const char *path,char *const arg[]); Здесь path — путевое имя выполняемой программы. Его можно задать в виде полного составного имени либо относительного составного имени из текущего каталога. Последующий параметр представляет вектор (с завершающим нулевым символом), содержащий аргументы командной строки, представленные в виде строк с завершающими нулевыми символами. Всего может быть n аргументов. Этот вектор завершается NULL-указателем. Элементу arg[] можно присвоить значение таким образом: char *const arg[] = {«traverse»,".», ">",«1000»,NULL}; Вот пример вызова этой функции: execv(«traverse», arg) ; В этом случае утилита traverse перечислит все файлы в текущем каталоге, размер которых превышает 1000 байт. • int execve(const char *path,char *const arg[],char *const envp[]); Эта функция аналогична функции execv(), с одним отличием: она имеет дополнительный параметр, envp[], который описан выше. • int execvp(const char *file,char *const arg[]); Здесь file — имя выполняемой программы. Последующий параметр представляет собой вектор (с завершающим нулевым символом), содержащий аргументы командной строки, представленные в виде строк с завершающими нулевыми символами. Всего может быть n аргументов. Этот вектор завершается NULL-указателем. Вот примеры применения синтаксиса функций execv () с различными аргументами: char *const arg[] = {«traverse»,".», ">",«1000»,NULL}; char *const envp[] = {«files=50»,NULL}; execv("/path/traverse», arg); execve("/path/traverse», arg, envp); execvp(«traverse», arg); Здесь в каждом примере вызова execv-функции активизированный процесс выполняет программу traverse. Определение ограничений для функций exec () Существуют ограничения на размеры вектора argv[] и массива envp[], передаваемые функциям семейства exec. Для определения максимального размера аргументов командной строки и размера переменных среды при использовании exec-функций (которые принимают параметр envp [ ]) можно использовать функцию sysconf (). Чтобы эта функция возвратила размер, ее параметру name необходимо присвоить значение _SC_ARG_МАХ. Синопсис #include <unistd.h> long sysconf(int name); Еще одним ограничением при использовании функций семейства exec и других Функций, применяемых для создания процессов, является максимальное количество одновременно выполняемых процессов, которое допустимо для одного пользователя. Чтобы функция sysconf() возвратила это число, ее параметру name необходимо присвоить значение _SC_CHILD_MAX. Чтение и установка переменных среды Переменные среды представляют собой строки с завершающими нулевыми символами, в которых хранится такая системная информация, как пути к каталогам, содержащим команды, библиотеки, функции и процедуры, используемые процессом. Их также можно использовать для передачи любых определенных пользователем данных между родительскими и сыновними процессами. Они обеспечивают механизм предоставления процессу специальной информации без необходимости жесткого ее связывания с кодом программы. Переменные среды предопределены системой и совместно используются всеми ее оболочками и процессами. Эти переменные инициализируются файлами запуска. Чаще всего используются следующие системные переменные. $НОМЕ Полное составное имя каталога пользователя. $РАТН Список каталогов для поиска выполняемых файлов при выполнении команд. $MAIL Полное составное имя почтового ящика пользователя. $USER Идентификатор (id) пользователя. $SHELL Полное составное имя командной оболочки зарегистрированного пользователя. $TERM Тип терминала пользователя. Переменные среды могут храниться в файле или в списке, принадлежащем среде. Этот список среды содержит указатели на строки с завершающими нулевыми символами. Когда процесс начинает выполняться, переменная extern char **environ будет указывать на список среды. Строки, составляющие список среды, имеют следующий формат: name=value Процессы, инициализированные с помощью функций execl(), execlp(), execv() и execvp(), наследуют конфигурацию среды родительского процесса. Процессы, инициализированные с помощью функций execve() и execle(), сами устанавливают среду. Существуют функции и утилиты, которые позволяют опросить, добавить или модифицировать переменные среды. Функция getenv() используется для определения факта установки заданной переменной. Интересующая вас переменная задается с помощью параметра name. Если заданная переменная не установлена, функция возвращает значение NULL. В противном случае (если переменная установлена), функция возвращает указатель на строку, содержащую ее значение. Синопсис #include <stdlib.h> char *getenv(const char *name); int setenv(const char *name, const char *value, int overwrite); void unsetenv(const char *name); Рассмотрим пример:
string Path; Path = getenv(«PATH»); Здесь строке Path присваивается значение, содержащееся во встроенной переменной среды РАТН. функция setenv() используется для изменения значения существующей переменной среды или добавления новой переменной в среду вызывающего процесса. Параметр name содержит имя переменной среды, которую надлежит добавить или изменить. Заданной переменной присваивается значение, переданное в параметре value. Если переменная, заданная параметром name, уже существует, ее прежнее значение заменяется значением, заданным параметром value при условии, если параметр overwrite содержит ненулевое значение. Если же значение overwrite равно 0, содержимое заданной переменной среды не модифицируется. Функция setenv () возвращает 0 при успешном выполнении, в противном случае — значение -1. функция unsetenv () удаляет переменную среды, заданную параметром name. 3.6.4. Использование функции system() для порождения процессов Функция system() используется для выполнения команды или запуска программы. Функция system() выполняет функцию fork(), а затем сыновний процесс вызывает функцию exec () с оболочкой, выполняя заданную команду или программу. Синопсис #include <stdlib.h> int system(const char *string); В качестве параметра string можно передать системную команду или имя выполняемого файла. При удачном исходе функция возвращает статус завершения команды или значение, возвращаемое программой (если таковое предусмотрено). Ошибки могут возникнуть на нескольких уровнях, т.е. ошибка может произойти при выполнении функции fork() или exec() либо заданная оболочка может оказаться неподходящей для выполнения команды или программы. Функция system () возвращает значение родительскому процессу. При неудачном исходе функции exec() возвращается число 127, а при обнаружении других ошибок — число -1. Эта функция не влияет на состояние ожидания сыновних процессов. Использование POSIX-функций для порождения процессов Подобно созданию процессов с помощью функций system() и fork-exec, функции posix_spawn() создают новые сыновние процессы из заданных образов процессов. Однако функции posix_spawn() позволяют при этом реализовать более многослойные «рычаги» управления, т.е. они управляют следующими атрибутами сын овних процессов, унаследованных от родительского процесса: • Дескрипторы файлов; • стратегия планирования; • идентификатор группы процессов; • идентификатор пользователя и группы; • маска сигналов. Функции posix_spawn() позволяют управлять тем, будут ли сигналы, проигнорированные родительским процессом, игнорироваться его потомком или устанавливаться для выполнения действий, заданных по умолчанию. Управление дескрипторами файлов позволяет сыновнему процессу получить самостоятельный доступ к потоку данных, независимо открытому родителем. Возможность установить для сыновнего процесса идентификатор группы повлияет на то, как управление сыновней задачей будет связано с управлением родителем. Наконец, стратегию планирования сыновнего процесса можно установить отличной от стратегии планирования его родителя. Синопсис #include <spawn.h> int posix_spawn( pid_t *restrict pid, const char *restrict path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *restrict attrp, char *const argv[restrict], char *const envp[restrict]); int posix_spawnp(pid_t *restrict pid, const char *restrict file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *restrict attrp, char *const argv[restrict], char *const envp[restrict]); Различие между этими двумя функциями состоит в том, что функции posix_spawn () передается параметр path, а функции posix_spawnp () — параметр file. Параметр path в функции posix_spawn() принимает полное или относительное составное имя выполняемого файла, а параметр file в функции posix_spawnp () — только имя выполняемой программы. Если этот параметр содержит символ «косая черта», то содержимое параметра file используется в качестве составного путевого имени. В противном случае путь к выполняемому файлу определяется с помощью переменной среды PATH . Параметр file_actions представляет собой указатель на структуру posix_spawn_file_actions_t: struct posix_spawn_file_actions_t { int _allocated; int _used; struct _spawn_action *actions; int _pad[16] ; } ; Структура posix_spawn_file_actions_t содержит информацию о действиях, выполняемых в новом процессе над дескрипторами файлов. Параметр file_actions используется для преобразования родительского набора дескрипторов открытых файлов в набор дескрипторов файлов для порожденного сыновнего процесса. Эта структура может содержать ряд файловых операций, предназначенных для выполнения в последовательности, в которой они были добавлены в объект действий над файлами. Эти файловые операции выполняются над дескрипторами открытых файлов родительского процесса и позволяют копировать, добавлять, удалять или закрывать дескрипторы заданных файлов от имени сыновнего процесса даже до его создания. Если параметр file_actions содержит нулевой указатель, то дескрипторы файлов, открытые родительским процессом, останутся открытыми для его потомка без каких-либо модификаций. Функции, используемые для добавления действий над файлами в объект типа posix_spawn_file_actions, перечислены в табл. 3.4. Таблица З.4. Функции, используемые для добавления действий над файлами в объект типа posix_spawn_file_actions int posix_spawn_file_actions_addclоse (posix_spawn_file_actions_t *file_actions, int fildes); Добавляет действие close() в объект действий над файлами, заданный параметром file_actions. В результате при порождении нового процесса с помощью этого объекта будет закрыт файловый дескриптор fildes int posix_spawn_file_actions_addopen (posix_spawn_file_actions_t *file_actions, int fildes, const char *restrict path, int oflag, mode_t mode); Добавляет действие open () в объект действий над файлами, заданный параметром file_actions. В результате при порождении нового процесса с помощью этого объекта будет открыт файл, заданный параметром path, с использованием дескриптора fildes int posix_spawn_file_actions_adddup2 (posix_spawn_file_actions_t *file_actions, int fildes, int new fildes); Добавляет действие dup2 () в объект действий над файлами, заданный параметром file_actions. В результате при порождении нового процесса с помощью этого объекта будет создан дубликат файлового дескриптора fildes с использованием файлового дескриптора newfildes int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t *file_actions); Разрушает объект, заданный параметром file_actions, что приводит к деинициализации этого объекта. Затем его можно инициализировать повторно с помощью функции posix_spawn_file_actions_init () int posix_spawn_file_actions_init (posix_spawn_file_actions_t *file_actions); Инициализирует объект, заданный параметром file_actions. После инициализации этот объект не будет содержать действий, предназначенных для выполнения над файлами Параметр attrp указывает на структуру posix_ spawnattr_t: struct posix_spawnattr_t { short int _flags; pid_t _pgrp; sigset__t _sd; sigset_t _ss; struct sched_param _sp; int _policy; int _pad[16] ; }; Эта структура содержит информацию о стратегии планирования, группе процессов, сигналах и флагах для нового процесса. Ниже следует описание отдельных атрибутов этой структуры. _flags Используется для индикации того, какие атрибуты процесса должны быть модифицированы в порожденном процессе. Эти атрибуты организованы поразрядно по принципу включающего ИЛИ: POSIX_SPAWN_RESETIDS POSIX_SPAWN_SETPGROUP POSIX_SPAWN_SETSIGDEF POSIX_SPAWN_SETSIGMASK POSIX_SPAWN_SETSCHEDPARAM POSIX_SPAWN_SETSCHEDULER _pgrp Идентификатор группы процессов, подлежащих объединению с новым процессом. _sd Представляет множество сигналов, подлежащих обработке по умолчанию новым процессом. _ss Представляет маску сигналов, подлежащую использованию новым процессом. _sp Представляет параметр планирования, подлежащий назначению новому процессу. _policy Представляет стратегию планирования, предназначенную для нового процесса. Функции, используемые для установки и считывания отдельных атрибутов, содержащихся в структуре posix_spawnattr_t, перечислены в табл. 3.5. Таблица 3.5. Функции, используемые для установки и считывания отдельных атрибутов структуры posix_spawnattr_t int posix_spawnattr_getflags(const posix_spawnattr_t *restrict attr, short *restrict flags); Возвращает значение атрибута _flags, хранимого в объекте, заданном параметром attr int posix_spawnattr_setflags (posix_spawnattr_t *attr,short flags); Устанавливает значение атрибута _flags, хранимого в объекте, заданном параметром attr, равным значению параметра flags int posix_spawnattr_getpgroup (const posix_spawnattr_t *restrict attr, pid_t *restrict pgroup); Возвращает значение атрибута _pgroup, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре pgroup int posix_spawnattr_setpgroup (posix_spawnattr_t *attr, pid_t pgroup); Устанавливает значение атрибута_pgroup, хранимого в объекте, заданном параметром attr, равным параметру pgroup, если в атрибуте _flags установлен признак POSIX_S PAWN_SETPGROUP int posix_spawnattr_getschedparam (const posix_spawnattr_t *restrict attr, struct sched_param *restrict schedparam) ; Возвращает значение атрибута_sp, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре schedparam int posix_spawnattr_setschedparam (posix_spawnattr_t *attr, const struct sched_param *restrict schedparam) ; Устанавливает значение атрибута_sp, хранимого в объекте, заданном параметром attr, равным параметру schedparam, если в атрибуте _flags установлен признак POSIX_SPAWN_SETSCHEDPARAM int posix_spawnattr_getschedpolicy (const posix_spawnattr_t *restrict attr, int *restrict schedpolicy) ; Возвращает значение атрибута _policy, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре schedpolicy int posix_spawnattr_setschedpolicy (posix_spawnattr_t *attr, int schedpolicy); Устанавливает значение атрибута_policy, хранимого в объекте, заданном параметром attr, равным параметру schedpolicy, если в атрибуте_flags установлен признак POSIX_SPAWN_SETSCHEDULER int posix_spawnattr_getsigdefault (const posix_spawnattr_t *restrict attr, sigset_t *restrict sigdefault); Возвращает значение атрибута_sd, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре sigdefault int posix_spawnattr_setsigdefault (posix_spawnattr_t *attr, const sigset_t *restrictsigdefault); Устанавливает значение атрибута_sd, хранимого в объекте, заданном параметром attr, равным параметру sigdefault, если в атрибуте _flags установлен признак POSIX_SPAWN_SETSIGDEF int p osix_spawnattr_getsigmask (const posix_spawnattr_t *restrict attr, sigset_t *restrict sigmask); Возвращает значение атрибута _ss, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре sigmask int posix_spawnattr_setsigmask (posix_spawnattr_t *restrict attr, const sigset_t *restrict sigmask); Устанавливает значение атрибута_ss, хранимого в объекте, заданном параметром attr, равным параметру sigmask, если в атрибуте _flags установлен признак POSIX_S PAWN_SETSIGMASK int posix_spawnattr_destroy (posix_spawnattr_t *attr); Разрушает объект, заданный параметром attr. Этот объект можно затем снова инициализировать с помощью функции posix_spawnattr_init() int posix_spawnattr_init (posix_spawnattr_t *attr);Инициализирует объект, заданный параметром attr, значениями, действующими по умолчанию для всех атрибутов, содержащихся в этой структуре Пример использования функции posix_spawn () для создания процесса приведен в листинге 3.3. // Листинг 3.3. Порождение процесса с помощью // функции posix_spawn(), которая // вызывает утилиту ps #include <spawn.h> #include <stdio.h> #include <errno.h> #include <iostream> { //... posix_spawnattr_t X; posix_spawn_file_actions_t Y; pid_t Pid; char *const argv[] = {"/bin/ps»,«-lf»,NULL}; char *const envp[] = {«PROCESSES=2»}; posix_spawnattr_init(&X); posix_spawn_file_actions_init(&Y); posix_spawn(&Pid,"/bin/ps»,&Y,&X,argv,envp); perror(«posix_spawn»); cout << «spawned PID: " << Pid << endl; //... return(0); } В листинге 3.3 инициализируются объекты posix_spawnattr_t и posix_spawn_ file_actions_t. Функция posix_spawn() вызывается с такими аргументами: Pid,путь " /bin/ps», Y, X, массив argv (который содержит команду в качестве первого элемента и опцию в качестве второго) и массив envp, содержащий список переменных среды. При успешном выполнении функции posix_spawn() значение, хранимое в параметре Pid, станет идентификатором (PID) порожденного процесса, а функция perror() отобразит следующий результат: posix_spawn: Success Затем будет выведено значение Pid. В данном случае порожденный процесс выполняет следующую команду: /bin/ps -lf При успешном выполнении POSIX-функции возвращают (обычным путем) число 0 и в параметре pid идентификатор (id) сыновнего процесса (для родительского процесса). В случае неудачи сыновний процесс не создается, следовательно, значение pid не имеет смысла, а сама функция возвращает значение ошибки. При использовании spawn-функций ошибки могут возникать на трех уровнях. Во-первых, это возможно, если окажутся недействительными объекты file_actions или attr objects. Если это произойдет после успешного (казалось бы) завершения функции (т.е. после порождения сыновнего процесса), такой сыновний процесс может получить статус завершения со значением 127 . Если ошибка связана с функциями управления атрибутами порожденных процессов, возвращается код ошибки, сгенерированный конкретной функцией (см. табл. 3.4 и 3.5). Если spawn -функция успела успешно завершиться, то сыновний процесс может иметь статус завершения со значением 127 . Ошибки также возникают при попытке породить сыновний процесс. Эти ошибки будут такими же, как при выполнении функций fork () или exec (). В этом случае они (ошибки) займут место значений, возвращаемых spawn -функциями. Если сыновний процесс генерирует ошибку, родительский процесс не получает «дурного известия» автоматически. Для извещения родителя об ошибке сыновнего процесса необходимо использовать другие механизмы, поскольку информация об этом не сохраняется в статусе завершения потомка. С этой целью можно использовать механизм межпроцессного взаимодействия либо специальный флаг, устанавливаемый сыновним процессом и видимый для его родителя. Идентификация родительских и сыновних процессов с помощью функций управления процессами Существуют две функции, которые возвращают значение идентификатора (PID) вызывающего процесса и значение идентификатора (PPID) родительского процесса. Функция getpid () возвращает идентификатор вызывающего процесса, а функция getppid() — идентификатор процесса, который является родительским для вызывающего процесса. Эти функции всегда завершаются успешно, поэтому коды ошибок не определены. Синопсис #include <unistd.h> pid_t getpid(void); pid_t getppid(void); Завершение процесса Когда процесс завершается, его блок БУП разрушается, а используемое им адресное пространство и ресурсы освобождаются. Код завершения помещается в главную таблицу процессов. Как только родительский процесс примет этот код, соответствующая структура таблицы процессов будет удалена. Процесс завершается, если соблюдены следующие требования. • Все инструкции выполнены. • Процесс явным образом передает управление родительскому процессу или вызывает системную функцию, которая завершает процесс. • Сыновние процессы могут завершаться автоматически при завершении родительского процесса. • Родительский процесс посылает сигнал о завершении своих сыновних процессов. Аварийное завершение процесса может произойти в случае, если процесс выполняет недопустимые действия. • Процесс требует больше памяти, чем система может ему предоставить. • Процесс пытается получить доступ к неразрешенным ресурсам. • Процесс пытается выполнить некорректную инструкцию или запрещенные вычисления. Завершение процесса может быть инициировано пользователем, если этот процесс является интерактивным. Родительский процесс несет ответственность за завершение (освобождение) своих потомков. Родительский процесс должен ожидать до тех пор, пока не завершатся все его сыновние процессы. Если родительский процесс выполнит считывание кода завершения сыновнего процесса, процесс-потомок покидает систему нормально. Процесс остается в «зомбированном» состоянии до тех пор, пока его родитель не примет соответствующий сигнал. Если родитель никогда не примет сигнал (поскольку он уже успел сам завершиться и выйти из системы или не ожидал завершения сыновнего процесса), процесс-потомок остается в «зомбированном» состоянии до тех пор, пока процесс init (исходный системный процесс) не примет его код завершения. Большое количество «зомбированных» процессов может негативно отразиться на производительности системы. Функции exit (), kill () и abort () Для самостоятельного завершения процесс может вызвать одну из двух функций: exit() и abort(). Функция exit() обеспечивает нормальное завершение вызывающего процесса. При этом будут закрыты все дескрипторы открытых файлов, связанные с процессом. Функция exit () сбросит на диск все открытые потоки, содержащие еще не переписанные буферизованные данные, после чего открытые потоки будут закрыты. Параметр status принимает статус завершения процесса, возвращаемый ожидающему родительскому процессу, который затем перезапускается. Параметр status может принимать такие значения: 0 , EXIT_FAILURE или EXIT_SUCCESS. Значение 0 говорит об успешном завершении процесса. Ожидающий родительский процесс имеет доступ только к младшим восьми битам значения параметра status. Если родительский процесс не ожидает завершения сыновнего процесса, его (ставшего «зомбированным») «усыновляет» процесс init. Функция abort () вызывает аварийное окончание вызывающего процесса, что по последствиям равноценно результату выполнения функции fclose() для всех открытых потоков. При этом ожидающий родительский процесс получит сигнал о прекращении выполнения сыновнего процесса. Процесс может прибегнуть к преждевременному прекращению только в случае, если он обнаружит ошибку, с которой не сможет справиться программным путем. Синопсис #include <stdlib.h> void exit(int status); void abort(void) ; Функцию kill() можно использовать для принудительного завершения другого процесса. Эта функция отправляет сигнал процессам, заданным параметром pid. Параметр sig — это сигнал, предназначенный для отправки заданному процессу. Возможные сигналы перечислены в заголовке <signal.h>. Для уничтожения процесса параметр sig должен иметь значение SIGKILL. Чтобы иметь право отсылать сигнал процессу, вызывающий процесс должен обладать соответствующими привилегиями, либо его реальный или идентификатор эффективного пользователя должен совпадать с реальным или сохраненным пользовательским идентификатором процесса, который принимает этот сигнал. Вызывающий процесс может иметь разрешение на отправку процессам только определенных (а не любых) сигналов. При успешной отправке сигнала функция возвращает вызывающему процессу значение 0, в противном случае — число -1. Вызывающий процесс может отправить сигнал одному или нескольким процессам при таких условиях. pid > 0 Сигнал будет отослан процессу, идентификатор (PID) которого равен значению параметра pid. pid = 0 Сигнал будет отослан всем процессам, у которых идентификатор группы процессов совпадает с идентификатором вызывающего процесса. pid = -1 Сигнал будет отослан всем процессам, для которых вызывающий процесс имеет разрешение отправлять этот сигнал. pid < -1 Сигнал будет отослан всем процессам, у которых идентификатор группы процессов равен абсолютному значению параметра pid, и для которых вызывающий процесс имеет разрешение отправлять этот сигнал. Синопсис #include <signal.h> int kill(pid_t pid, int sig) Ресурсы процессов При выполнении возложенной на процесс задачи часто приходится записывать Данные в файл, отправлять их на принтер или отображать полученные результаты на э к ране. Процессу могут понадобиться данные, вводимые пользователем с клавиатуры или содержащиеся в файле. Кроме того, процессы в качестве ресурса могут использовать другие процессы, например, подпрограммы. Подпрограммы, файлы, семафоры, мьютексы, клавиатуры и экраны дисплеев — все это примеры ресурсов, которые может затребовать процесс. Под ресурсом понимается все то, что использует процесс в любое заданное время в качестве источника данных, средств обработки, вычислений или отображения информации. Чтобы процесс получил доступ к ресурсу, он должен сначала сделать запрос, обратившись с ним к операционной системе. Если ресурс свободен, операционная система позволит процессу его использовать. После использования ресурса процесс освобождает его, чтобы он стал доступным для других процессов. Если ресурс недоступен, запрос отвергается, и процесс должен подождать его освобождения. Как только ресурс станет доступным, процесс активизируется. Таков базовый подход к распределению ресурсов между процессами. На рис. 3.11 показан граф распределения ресурсов, по которому можно понять, какие процессы удерживают ресурсы, а какие их ожидают. Так, процесс В делает запрос на ресурс 2, который удерживается процессом С. Процесс С делает запрос на ресурс 3, который удерживается процессом D. Рис. 3.11. Граф распределения ресурсов, который показывает, какие процессы удерживают ресурсы, а какие их запрашивают Если удовлетворяется сразу несколько запросов на получение доступа к ресурсу, этот ресурс является совместно используемым, или разделяемым (эта ситуация также отображена на рис. 3.11). Процесс А разделяет ресурс R 1 с процессом D. Разделяемые ресурсы могут допускать параллельный доступ сразу нескольких процессов или разрешать доступ только одному процессу в течение ограниченного промежутка времени, после чего аналогичным доступом сможет воспользоваться другой процесс. Примером такого типа разделяемых ресурсов может служить процессор. Сначала процессор назначается одному процессу в течение короткого интервала времени, а затем процессор «получает» другой процесс. Если удовлетворяется только один запрос на получение доступа к ресурсу, и это происходит после того, как ресурс освободит другой процесс, такой ресурс является неразделяемым, а о процессе говорят, что он имеет монопольный доступ (exclusive access) к ресурсу. В многопроцессорной среде важно знать, какой доступ можно организовать к разделяемому ресурсу: параллельный или последовательный (передавая «эстафету» поочередно от ресурса к ресурсу). Это позволит избежать ловушек, присущих параллелизму. Одни ресурсы могут изменяться или модифицироваться процессами, а другие^ нет. Поведение разделяемых модифицируемых или немодифицируемых ресурсов определяется типом ресурса. § 3.1 • Граф распределения ресурсов , Графы распределения ресурсов — это направленные графы, которые показывают, как распределяются ресурсы в системе. Такой граф состоит из множества вершин V множества ребер E. Множество вершин делится на две категории: P = {P 1 , P 2 ,..., Pn) R = {R 1 , R 2 ,..., Rm} Множество P— это множество всех процессов, а R— это множество всех ресурсов в системе Ребро, направленное от процесса к ресурсу, называется ребром запроса, а ребро, направленное от ресурса к процессу, называется ребром назначения. Направленные ребра обозначаются следующим образом: P i > R j Ребро запроса: процесс Р i запрашивает экземпляр типа ресурса R j R j > P i . Ребро назначения: экземпляр типа ресурса R j выделен процессу P i ; Каждый процесс в графе распределения ресурсов отображается кругом, а каждый ресурс — прямоугольником. Поскольку может быть много экземпляров одного типа ресурса, то каждый из них представляется точкой внутри прямоугольника. Ребро запроса указывает на периметр прямоугольника ресурса, а ребро назначения берет начало из точки и касается периметра круга процесса. Граф распределения ресурсов, показанный на рис. 3.11, отображает следующее. Множества P, R и E P={P a , P b , P c , P d } R={R 1 ,R 2 ,R 3 } E = {R 1 > P a , R 1 > P d , P b > R 2 , R 2 > P c , P c > R 3 , R 3 > P d } Типы ресурсов Существуют три основных типа ресурсов: аппаратные, информационные и программные. Аппаратные ресурсы представляют собой физические устройства, подключенные к компьютеру (например, процессоры, основная память и все устройства ввода-вывода, включая принтеры, жесткий диск, накопитель на магнитной ленте, дисковод с zip-архивом, мониторы, клавиатуры, звуковые, сетевые и графические карты, а также модемы. Все эти устройства могут совместно использовать несколько процессов. Некоторые аппаратные ресурсы прерываются [7], чтобы разрешить доступ к ним различных процессов. Например, прерывания процессора позволяют различным процессам выполняться по очереди. Оперативное запоминающее устройство, или ОЗУ (RAM),- это еще один пример ресурса, разделяемого посредством прерываний. Когда процесс не выполняется, некоторые страничные блоки, которые он занимает, могут быть выгружены во вспомогательное запоминающее устройство, а на их место загружены данные, относящиеся к другому процессу. В любой момент времени весь диапазон памяти может быть занят страничными блоками только одного процесса. Примером разделяемого, но непрерываемого ресурса может служить принтер. При совместном использовании принтера задания, посылаемые на печать каждым процессом, хранятся в очереди. Каждое задание печатается до конца, и только потом начинает выполняться следующее задание. Принтер не прерывается ни одним ждущим заданием, если не отменяется текущее задание. Информационные ресурсы — к ним относятся данные (например, объекты), системные данные (например, переменные среды, файлы и дескрипторы) и такие глобально определенные переменные, как семафоры и мьютексы, — являются разделяемыми ресурсами, которые могут быть модифицированы процессами. Обычные файлы и файлы, связанные с физическими устройствами (например, принтером), могут открываться с учетом ограничивающего типа доступа со стороны процессов. Другими словами, процессы могут обладать правом доступа только для чтения, или только для записи, или для чтения и записи. Сыновний процесс наследует ресурсы родительского процесса и права доступа к ним, существующие на момент создания процесса-потомка. Сыновний процесс может переместить файловый указатель, закрыть, модифицировать или перезаписать содержимое файла, открытого родителем. Доступ к совместно используемым файлам и памяти с разрешением записи должен быть синхронизирован. Для синхронизации доступа к разделяемым ресурсам данных можно использовать такие разделяемые данные, как семафоры и мьютексы. Разделяемые библиотеки могут служить примером программных ресурсов. Разделяемые библиотеки предоставляют общий набор функций для процессов. Процессы могут также совместно использовать приложения, программы и утилиты. В этом случае в памяти находится только одна копия программного кода , например, приложения (приложений). При этом должны существовать отдельные копии данных , по одной для каждого пользователя (процесса). К неизменяемому программному коду (который также именуется реентерабельным, или повторно используемым) могут получать доступ несколько процессов одновременно. POSIX-функции для установки ограничений доступа к ресурсам В библиотеке POSIX определены функции, которые ограничивают возможности процесса по использованию определенных ресурсов. Так, операционная система устанавливает ограничения на возможности процесса по использованию системных ресурсов, а именно: • размер стека процесса; • размер создаваемого файла и файла ядра; • объем времени ЦП, выделенный процессу (размер кванта времени); • объем памяти, используемый процессом; • количество дескрипторов открытых файлов. Операционная система устанавливает жесткие ограничения на использование ресурсов процессом. Процесс может установить или изменить мягкие ограничения ресурсов, но это значение не должно превысить жесткий предел, установленный операционной системой. Процесс может понизить свой жесткий предел, но его значение не должно быть меньше мягкого предела. Операция по понижению процессом своего жесткого предела необратима. Его могут повысить только процессы, обладающие специальными привилегиями. Синопсис #include <sys/resource.h> int setrlimit(int resource, const struct rlimit *rlp); int getrlimit(int resource, struct rlimit *rlp); int getrusage(int who, struct rusage *r_usage); Функция setrlimit() используется для установки ограничений на потребление заданных ресурсов. Эта функция позволяет установить как жесткий, так и мягкий пределы. Параметр resource представляет тип ресурса. Значения типов ресурсов (и их краткое описание) приведено в табл. 3.6. Жесткие и мягкие пределы заданного ресурса представляются параметром rlp, который указывает на структуру rlimit, содержащую два объекта типа rlim_t. struct rlimit { rlim_t rlim_cur; rlim_t rlim_max; } ; Тип rlim_t — это целочисленный тип без знака. Член rlim_cur содержит значение текущего, или мягкого предела, а член rlim_max — значение максимума, или жесткого предела. Членам rlim_cur и rlim_max можно присвоить любые значения, а также символические константы, определенные в заголовке <sys/resource. h>. RLIM_INFINITY Отсутствие ограничения. RLIM_SAVED_MAX Непредставимый хранимый жесткий предел. RLIM_SAVED_CUR Непредставимый хранимый мягкий предел. Как жесткий, так и мягкий пределы можно установить равными значению RLIM_INFINITY, которое подразумевает, что ресурс неограничен. Таблица З.6. Значения параметра resource RLIMIT_CORE Максимальный размер файла ядра в байтах, который может быть создан процессом RLIMIT_CPU Максимальный объем времени ЦП в секундах, которое может быть использовано процессом RLIMIT_DATA Максимальный размер раздела данных процесса в байтах RLIMIT_FSIZE Максимальный размер файла в байтах, который может быть создан процессом RLlMlT_NOFILE Увеличенное на единицу максимальное значение, которое система может назначить вновь созданному дескриптору файла RLlMlT_STACK Максимальный размер стека процесса в байтах RLlMlT_AS Максимальный размер доступной памяти процесса в байтах Функция getrlimit () возвращает значения мягкого и жесткого пределов заданного ресурса в объекте rlp. Обе функции возвращают значение 0 при успешном завершении и число -1 в противном случае. Пример установки процессом мягкого предела для размера файлов в байтах приведен в листинге 3.4. Листинг 3.4. Использование функции setrlimit() для установки мягкого предела для размера файлов #include <sys/resource.h> struct rlimit R_limit; struct rlimit R_limit_values; R_limit.rlim_cur = 2 000; R_limit.rlim_max = RLIM_SAVED_MAX; setrlimit (RLIMIT_FSIZE, &R__1 imit); getrlimit(RLIMIT_FSIZE, &R_limit_values); cout << «мягкий предел для размера файлов: " << R_limit_values.rlim_cur <<endl; В листинге 3.4 мягкий предел для размера файлов устанавливается равным 2000 байт, а жесткий предел — максимально возможному значению. Функции setrlimit () передаются значения RLIMIT_FSIZE и R_limit, а функции getrlimit () — значения RLIMIT_FSIZE и R_limit_values. После их выполнения на экран выводится установленное значение мягкого предела. Функция getrusage () возвращает информацию об использовании ресурсов вызывающим процессом. Она также возвращает информацию о сыновнем процессе, завершения которого ожидает вызывающий процесс. Параметр who может иметь следующие значения: RUSAGE_SELF RUSAGE_CHILDREN Если параметру who передано значение RUSAGE_SELF, то возвращаемая информация будет относиться к вызывающему процессу. Если же параметр who содержит значение RUSAGE_CHILDREN, то возвращаемая информация будет относиться к потомку вызывающего процесса. Если вызывающий процесс не ожидает завершения своего потомка, информация, связанная с ним, отбрасывается (не учитывается). Возвращаемая информация передается через параметр r_usage, который указывает на структуру rusage. Эта структура содержит члены, перечисленные и описанные в табл. 3.7. При успешном выполнении функция возвращает число 0, в противном случае — число -1. Таблица 3.7. Члены структуры rusage Член структуры Описание struct timeval ru_utime Время,потраченное пользователем struct timeval ru_sutime Время,использованное системой long ru_maxrss Максимальный размер, установленный для резидентной программы long ru_maxixrss Размер разделяемой памяти long ru_maxidrss Размер неразделяемой области данных long ru_maxisrss Размер неразделяемой области стеков long ru_minflt Количество запросов на страницы long ru_maj flt Количество ошибок из-за отсутствия страниц long ru_nswap Количество перекачек страниц long ru_inblock Блочные операции по вводу данных long ru_oublock Блочные операции операций по выводу данных long ru_msgsnd Количество отправленных сообщений long ru_msgrcv Количество полученных сообщений long ru_nsignals Количество полученных сигналов long ru_nvcsw Количество преднамеренных переключений контекста long ru_nivcsw Количество принудительных переключений контекста Асинхронные и синхронные процессы Асинхронные процессы выполняются независимо один от другого. Это означает, что процесс А будет выполняться до конца безотносительно к процессу В. Между асинхронными процессами могут быть прямые родственные («родитель-сын») отношения, а могут и не быть. Если процесс А создает процесс В, они оба могут выполняться независимо, но в некоторый момент родитель должен получить статус завершения сыновнего процесса. Если между процессами нет прямых родственных отношений, у них может быть общий родитель. Асинхронные процессы могут выполняться последовательно, параллельно или с перекрытием. Эти сценарии изображены на рис. 3.12. В ситуации 1 до самого конца выполняется процесс А, затем процесс В и процесс С выполняются до самого конца. Это и есть последовательное выполнение процессов. В ситуации 2 процессы выполняются одновременно. Процессы А и В - активные процессы. Во время выполнения процесса А процесс В находится в состоянии ожидания. В течение некоторого интервала времени оба процесса пребывают в ждущем режиме. Затем процесс В «просыпается», причем раньше процесса А, а через некоторое время «просыпается» и процесс А, и теперь оба процесса выполняются одновременно. Эта ситуация показывает, что асинхронные процессы могут выполняться одновременно только в течение определенных интервалов времени. В ситуации 3 выполнение процессов А и В перекрывается. Рис. 3.12. Возможные сценарии асинхронных и синхронных процессов Асинхронные процессы могут совместно использовать такие ресурсы, как файлы или память. Это может потребовать (или не потребовать) синхронизации или взаимодействия при разделении ресурсов. Если процессы выполняются последовательно (ситуация 1), то они не потребуют никакой синхронизации. Например, все три процесса, А, В и С, могут разделять некоторую глобальную переменную. Процесс А (перед тем как завершиться) записывает значение в эту переменную, затем процесс В во время своего выполнения считывает данные, хранимые в этой переменной и (перед тем как завершиться) записывает в нее «свое» значение. Затем во время своего выполнения процесс С считывает данные из этой переменной. Но в ситуациях 2 и 3 процессы могут попытаться одновременно модифицировать эту переменную, поэтому здесь не обойтись без синхронизации доступа к ней. Мы определяем синхронные процессы как процессы с перемежающимся выполнением, когда один процесс приостанавливает свое выполнение до тех пор, пока не з аверш ится другой - Например, процесс А, родительский, при выполнении создает процесс В, сыновний. Процесс А приостанавливает свое выполнение до тех пор, пока не завершится процесс В. После завершения процесса В его выходной код помещается в таблицу процессов. Тем самым процесс А уведомляется о завершении процecca В. Процесс А может продолжить выполнение, а затем завершиться или завершиться немедленно. В этом случае выполнение процессов А и В является синхронизированным. Сценарий синхронного выполнения процессов А и В (для сравнения с асинхронным) также показан на рис. 3.12. Создание синхронных и асинхронных процессов с помощью функций fork (), exec (), system () и posix_spawn() Функции fork (), fork-exec и posix_spawn () позволяют создавать асинхронные процессы. При использовании функции fork() дублируется образ родительского процесса. После создания сыновнего процесса эта функция возвращает родителю (через параметр) идентификатор (PID) процесса-потомка и (обычным путем) число 0, означающее, что создание процесса прошло успешно. При этом родительский процесс не приостанавливается; оба процесса продолжают выполняться независимо от инструкции, следующей непосредственно за вызовом функции fork (). При создании сыновнего процесса посредством fork-exec-комбинации его образ инициализируется с помощью образа нового процесса. Если функция exec () выполнилась успешно (т.е. успешно прошла инициализация), она не возвращает родительскому процессу никакого значения. Функции posix_spawn() создают образы сыновних процессов инициализируют их. Помимо идентификатора (PID), возвращаемого (через параметр) функцией posix_spawn() родительскому процессу, обычным путем возвращается значение, служащее индикатором успешного порождения процесса. После выполнения функции posix_spawn() оба процесса выполняются одновременно. Функция system() позволяет создавать синхронные процессы. При этом создается оболочка, которая выполняет системную команду или запускает выполняемый файл. В этом случае родительский процесс приостанавливается до тех пор, пока не завершится сыновний процесс и функция system () не возвратит значение. Функция wait () Асинхронный процесс, вызвав функцию wait (), может приостановить выполнение до тех пор, пока не завершится сыновний процесс. После завершения сыновнего процесса ожидающий родительский процесс считывает статус завершения своего потомка, чтобы не допустить создания процесса- «зомби». Функция wait () получает статус завершения из таблицы процессов. Параметр status указывает на ту область, которая содержит статус завершения сыновнего процесса. Если родительский процесс имеет не один, а несколько сыновних процессов и некоторые из них уже завершились, функция wait () считывает из таблицы процессов статус завершения только для одного сыновнего процесса. Если информация о статусе окажется доступной еще до вып олнения функции wait (), эта функция завершится немедленно. Если родительский процесс не имеет ни одного потомка, эта функция возвратит код ошибки. Функцию wait () можно использовать также в том случае, когда вызывающий процесс должен ожидать до тех пор, пока не получит сигнал, чтобы затем выполнить определенные действия по его обработке. Синопсис #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); Функция waitpid() аналогична функции wait () за исключением того, что она принимает дополнительные параметры pid и options. Параметр pid задает множество сыновних процессов, для которых считывается статус завершения. Другими словами, значение параметра pid определяет, какие процессы попадают в это множество. pid > 0 Единственный сыновний процесс. pid = 0 Любой сыновний процесс, групповой идентификатор которого совпадает с идентификатором вызывающего процесса. pid < -1 Любые сыновние процессы, групповой идентификатор которых равен абсолютному значению pid. pid = -1 Любые сыновние процессы. Параметр options определяет, как должно происходить ожидание процесса, и может принимать одно из значений следующих констант, определенных в заголовке <sys/wait .h>: WCONTINUED Сообщает статус завершения любого продолженного сыновнего процесса (заданного параметром pid), о статусе которого не было доложено с момента продолжения его выполнения. WUNTRACED Сообщает статус завершения любого остановленного сыновнего процесса (заданного параметром pid), о статусе которого не было доложено с момента его останова. WNOHANG Вызывающий процесс не приостанавливается, если статус завершения заданного сыновнего процесса недоступен. Эти константы могут быть объединены с помощью логической операции ИЛИ и переданы в качестве параметра options (например, WCONTINUED | WUNTRACED). Обе эти функции возвращают идентификатор (PID) сыновнего процесса, для которого получен статус завершения. Если значение, содержащееся в параметре status, равно числу 0, это означает, что сыновний процесс завершился при таких условиях: • процесс вернул значение 0 из функции main (); • процесс вызвал некоторую версию функции exit() с аргументом 0; • процесс был завершен, поскольку завершился последний поток процесса. В табл. 3.8 перечислены макросы, которые позволяют вычислить значение статуса завершения. Таблица З.8. Макросы, которые позволяют вычислить значение статуса завершения WIFEXITED Приводится к ненулевому значению, если статус был возвращен нормально завершенным сыновним процессом WEXITSTATUS Если значение WIFEXITED оказывается ненулевым, то оцениваются младшие 8 бит аргумента status, переданного завершенным сыновним процессом функции _exit () или exit (), либо значения, возвращенного функцией main () WIFSIGNALED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который завершился, поскольку ему был послан сигнал, но этот сигнал не был перехвачен WTERMSIG Если значение WIFSIGNALED оказывается ненулевым, то оценивается номер сигнала, который послужил причиной завершения сыновнего процесса WIFSTOPPED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который в данный момент остановлен WSTOPSIG Если значение WIFSTOPPED оказывается ненулевым, то оценивается номер сигнала, который послужил причиной останова сыновнего процесса WIFCONTINUED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который продолжил выполнение после сигнала останова, принятого от блока управления заданиями Разбиение программы на задачи Рассматривая разбиение программы на несколько задач, вы делаете первый шаг к внесению параллелизма в свою программу. В однопроцессорной среде параллелизм реализуется посредством многозадачности. Это достигается путем переключения процессов. Каждый процесс выполняется в течение некоторого короткого интервала времени, после чего процессор «передается» другому процессу. Это происходит настолько быстро, что создается иллюзия одновременного выполнения процессов. В многопроцессорной среде процессы, принадлежащие одной программе, могут быть назначены одному или различным процессорам. Процессы, назначенные различным процессорам, выполняются параллельно. Различают два уровня параллельной обработки в приложении или системе: уровень процессов и уровень потоков. Параллельная обработка на уровне потоков носит название многопоточности (она рассматривается в следующей главе). Чтобы разумно разделить программу на параллельные задачи, необходимо определить, где «гнездится» параллелизм и где можно воспользоваться преимуществами от его реализации. Иногда в параллелизме нет насущной необходимости. Программа может интерпретироваться с учетом параллелизма, но и при последовательном выполнении действий она прекрасно работает. Безусловно, внесение параллелизма может повысить ее быстродействие и понизить уровень сложности. Одни программы обладают естественным параллелизмом, а другим больше подходит последовательное выполнение действий. Программы также могут иметь двойственную интерпретацию. При декомпозиции программы на функции обычно используется нисходящий принцип, а при разделении на объекты — восходящий. При этом необходимо определить, какие функции или объекты лучше реализовать в виде отдельных программ или подпрограмм, а какие — в виде потоков. Подпрограммы должны выполняться операционной системой как процессы. Отдельные подпрограммы, или процессы, выполняют задачи, порученные проектировщиком ПО. Задачи, на которые будет разделена программа, могут выполняться параллельно, причем здесь можно выделить следующие три способа реализации параллелизма. 1. Выделение в программе одной основной задачи, которая создает некоторое количество подзадач. 2. Разделение программы на множество отдельных выполняемых файлов. 3. Разделение программы на несколько задач разного типа, отвечающих за создание других подзадач только определенного типа. Эти способы реализации параллелизма отображены на рис. 3.13. Например, эти методы реализации параллелизма можно применить к программе визуализации. Под визуализацией будем понимать процесс перехода от представления трехмерного объекта в форме записей базы данных в двухмерную теневую графическую проекцию на поверхность отображения (экран дисплея). Изображение представляется в виде теневых многоугольников, повторяющих форму объекта. Этапы визуализации показаны на рис. 3.14. Визуализацию можно разбить на ряд отдельных задач. 1. Установить структуру данных для сеточных моделей многоугольников. 2. Применить линейные преобразования. 3. Отбраковать многоугольники, относящиеся к невидимой поверхности. 4. Выполнить растеризацию. 5. Применить алгоритм удаления скрытых поверхностей. 6. Затушевать отдельные пиксели. Первая задача состоит в представлении объекта в виде массива многоугольников, в котором каждая вершина многоугольника описывается в трехмерной мировой системе координат. Вторая задача — применить линейные преобразования к сеточной модели многоугольников. Эти преобразования используются для позиционирования объектов на сцене и создания точки обзора или поверхности отображения (области, которая видима наблюдателю с его точки обзора). Третья задача — отбраковать невидимые поверхности объектов на сцене. Это означает удаление линий, принадлежащих тем частям объектов, которые невидимы с точки обзора. Четвертая задача — преобразовать модель вершин в набор координат пикселей. Пятая задача — удалить любые скрытые поверхности. Если сцена содержит взаимодействующие объекты, например, когда одни объекты заслоняют другие, то скрытые (передними объектами) поверхности должны быть удалены. Шестая задача - наложить на поверхности изображения тень. Рис. 3.13. Способы разбиения программы на отдельные задачи Рис. 3.14. Этапы визуализации Решение каждой задачи представляется в виде отдельного выполняемого файла. Первые три задачи (Taskl, Task2 и Task3) выполняются последовательно, а остальные три (Task4, Task5 и Task6)— параллельно. Реализация первого способа создания программы визуализации приведена в листинге 3.5. // Листинг 3.5. Использование способа 1 для создания процессов #include <spawn.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h> #include <errno.h> #include <unistd.h> int main(void) { posix_spawnattr_t Attr; posix_spawn_file_actions_t FileActions; char *const argv4[] = {«Task4»,...,NULL}; char *const argv5[] = {«Task5'\...,NULL}; char *const argv6[] = {«Task6»,...,NULL}; pid_t Pid; int stat; // Выполняем первые три задачи синхронно, system(«Taskl . . . ") ; system(«Task2 . . . ") ; system(«Task3 . . . ") ; //иниииализируем структуры posix_spawnattr_init(&Attr); posix_spawn_file_actions_init(&FileActions); // execute last 3 tasks asynchronously posix_spawn(&Pid,«Task4»,&FileActions,&Attr,argv4,NULL); posix_spawn(&Pid,«Task5»,&FileActions,&Attr,argv5,NULL); posix_spawn(&Pid,«Task6»,&FileActions,&Attr,argv6,NULL); // like a good parent, wait for all your children wait (&stat); wait (&stat); wait (&stat); return(0); } В листинге 3.5 из функции main () с помощью функции system( ) вызываются на выполнение задачи Task1, Task2 и Task3. Каждая из них выполняется синхронно с родительским процессом. Задачи Task4, Task5 и Task6 выполняются асинхронно родительскому процессу благодаря использованию функций posix__spawn( ). Многоточие (... ) используется для обозначения файлов, требуемых задачам. Родительский процесс вызывает три функции wait (), и каждая из них ожидает завершения одной из задач (Task4, Task5 или Task6). Используя второй способ, программу визуализации можно запустить из сценария командной оболочки. Преимущество этого сценария состоит в том, что он позволяет использовать все команды и операторы оболочки. В нашей программе визуализации для управления выполнением задач используются метасимволы & и &&. Task1 ... && Task2 ... && Task3 Task4 . . . & Task5 . . . & Task6 Здесь благодаря использованию метасимвола && задачи Task1, Task2 и Task3 выполняются последовательно при условии успешного выполнения предыдущей задачи. Задачи же Task4, Task5 и Task6 выполняются одновременно, поскольку использован метасимвол &. Приведем некоторые метасимволы, применяемые при разделении команд в средах UNIX/Linux, и способы выполнения этих команд. && Каждая следующая команда будет выполняться только в случае успешного выполнения предыдущей команды. || Каждая следующая команда будет выполняться только в случае неудачного выполнения предыдущей команды. ; Команды должны выполняться последовательно. & Все команды должны выполняться одновременно. При использовании третьего способа задачи делятся по категориям. При декомпозиции программы следует разобраться, можно ли в ней выделить различные категории задач. Например, одни задачи могут «отвечать» за интерфейс пользователя, т.е. его создание, ввод данных, вывод данных и пр. Другим задачам поручаются вычисления, управление данными и пр. Такой подход весьма полезен не только при проектировании программы, но и при ее реализации. В нашей программе визуализации мы можем разделить задачи по следующим категориям: • задачи, которые выполняют линейные преобразованиях преобразования изображения на экране при изменении точки обзора; преобразования сцены; • задачи, которые выполняют растеризацию: вычерчивание линий; заливка участков сплошного фона; растеризация многоугольников; • задачи, которые выполняют удаление поверхностей: удаление скрытых поверхностей; удаление невидимых поверхностей; • задачи, которые выполняют наложение теней: затенение отдельных пикселей; затенение изображения в целом. Разбиение задач по категориям позволяет нашей программе приобрести более общий характер. Процессы при необходимости создают другие процессы, предназначенные для выполнения действий только определенной категории. Например, если нашей программе предстоит визуализировать лишь один объект, а не всю сцену, то нет никакой необходимости порождать процесс, который выполняет удаление скрытых поверхностей; вполне достаточно будет удаления невидимых поверхностей (одного объекта). Если объект не нужно затенять, то нет необходимости порождать задачу, выполняющую наложение тени; обязательным остается лишь линейное преобразование при решении задачи растеризации. Для запуска программы с использованием третьего способа можно использовать родительский процесс или сценарий оболочки. Родительский процесс может определить, какой нужен тип визуализации, и передать соответствующую информацию каждому из специализированных процессов, чтобы они «знали», какие процессы им следует порождать. Эта информация может быть также перенаправлена каждому из специализированных процессов из сценария оболочки. Реализация третьего способа представлена в листинге 3.6. // Листинг 3.6. Использование третьего метода для // создания процессов. Задачи запускаются из // родительского процесса #include <spawn.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h> #include <errno.h> #include <unistd.h> int main(void) { posix_spawnattr_t Attr; posix_spawn_file_actions_t FileActions; pid_t Pid; int stat; //••• system(«Task1 ...»);// Выполняется безотносительно к типу используемой визуализации. //определяем, какой нужен тип визуализации. Это можно // сд елать, получив информацию от пользователя или // выполнив специальный анализ. // затем сообщаем о результате другим задачам с помощью // аргументов. char *const argv4[] = {«TaskType4»,...,NULL}; char *const argv5[] = {«TaskType5»,...,NULL}; char *const argv6[] = {«TaskType6»,...,NULL} system(«TaskType2 . . . "); system(«TaskType3 . . . "); // Инициализируем структуры. posix_spawnattr_init(&Attr) ; posix_spawn_file_actions_init (&FileActions) ; posix_spawn(&Pid, «TaskType4», &FileActions,&Attr,argv4, NULL); posix_spawn(&Pid, «TaskType5», &FileActions,&Attr,argv5, NULL); if(Y){ posix_spawn(&Pid,«TaskType6»,&FileActions,&Attr, argv6,NULL); } // Подобно хорошему родителю, ожидаем возвращения // своих «детей». wait(&stat); wait(&stat); wait(&stat); return(0); } // Все TaskType-задачи должны быть аналогичными. //.. . int main(int argc, char *argv[]){ int Rt; //. . . if(argv[1] == X){ // Инициализируем структуры. posix_spawn(&Pid,«TaskTypeX»,&FileActions,&Attr,..., NULL); else{ // Инициализируем структуры. //.. • posix_spawn(&Pid,«TaskTypeY», &FileActions,&Attr, ...,NULL); } wait(&stat); exit(0); } В листинге 3.6 тип каждой задачи (а следовательно, и тип порождаемого процесса) определяется на основе информации, передаваемой от родительского процесса или сценария оболочки. Линии видимого контура Порождение процессов, как показано в листинге 3.7, возможно с помощью функций, вызываемых из функции main (). // Листинг 3.7. Стержневая ветвь программы, из которой // вызывается функция, порождающая процесс int main(int argc, char *argv[]) { Rt = funcl(X, Y, Z); //.. . } // Определение функции. int funcl(char *M, char *N, char *V) { //.. . char *const args[] = {«TaskX»',M,N,V,NULL}; Pid = fork(); if(Pid == 0) { exec(«TaskX»,args); } if(Pid > 0) { //.. . } wait(&stat); } В листинге 3.7 функция funcl () вызывается с тремя аргументами. Эти аргументы передаются порожденному процессу. Процессы также могут порождаться из методов, принадлежащих объектам. Как показано в листинге 3.8, объекты можно объявить в любом процессе. // Лист инг 3.8. Объявление объекта в процессе //-•• my_pbject MyObject; //-•• // Объявление и определение класса. class my_object { public: //... int spawnProcess(int X); //... }; int my_object::spawnProcess(int X) { //.. . // posix__spawn() или system() //.. . } Как показано в листинге 3.8, объект может создавать любое количество процессов из любого метода. Резюме Параллелизм в С++-программе достигается за счет ее разложения на несколько процессов или несколько потоков. Процесс- это «единица работы», создаваемая операционной системой. Если программа- это артефакт (продукт деятельности) разработчика, то процесс - это артефакт операционной системы. Приложение может состоять из нескольких процессов, которые могут быть не связаны с какой-то конкретной программой. Операционные системы способны управлять сотнями и даже тысячами параллельно загруженных процессов. Некоторые данные и атрибуты процесса хранятся в блоке управления процессами (process control block - PCB), или БУП, используемом операционной системой для идентификации процесса. С помощью этой информации операционная система Управляет процессами. Многозадачность (выполнение одновременно нескольких процессов) реализуется путем переключения контекста. Текущее состояние выполняемого процесса и его контекст сохраняются в БУП-блоке, что позволяет успешно возобновить этот процесс в следующий раз, когда он будет назначен центральному процессору. Занимая процессор, процесс пребывает в состоянии выполнения, а когда он ожидает использования ЦП, - то в состоянии готовности (ожидания). Получить информацию о процессах, выполняющихся в системе, можно с помощью утилиты ps. Процессы, которые создают другие процессы, вступают с ними в «родственные» (отцы- и -дети) отношения. Создатель процесса называется родительским, а созданный процесс — сыновним. Сыновние процессы наследуют от родительских множество атрибутов. «Святая обязанность» родительского процесса — подождать, пока сыновний не покинет систему. Для создания процессов предусмотрены различные системные функции: fork (), fork-exec (), system() и posix_spawn (). Функции fork(), fork-exec() и posix_spawn() создают процессы, которые являются асинхронными, в то время как функция system() создает сыновний процесс, который является синхронным по отношению к родительскому. Асинхронные родительские процессы могут вызвать функцию wait (), после чего «синхронно» ожидать, пока сыновние процессы не завершатся или пока не будут считаны коды завершения для уже завершившихся сыновних процессов. Программу можно разбить на несколько процессов. Эти процессы может породить родительский процесс, либо они могут быть запущены из сценария оболочки как отдельные выполняемые программы. Специализированные процессы могут при необходимости порождать другие процессы, предназначенные для выполнения действий только определенного типа. Порождение процессов может быть осуществлено как из функций, так и из методов. Примечания:6 В оригинале «text segment», что принято переводить как «сегмент кода» 7 В оригинале - «Some hardware resources are preempted to allow different processes access» |
|
|||||||||||||||||||||||||||||||||||