Реклама:

Системы Windows ХР и UNIX позволяют разделить работу на несколько процессов, выполняющихся параллельно и взаимодействующих друг с другом, как в примере с производителем и потребителем, который мы обсуждали ранее. В этом подразделе мы поговорим о том, как происходит управление процессами в обеих системах. Обе системы поддерживают параллелизм в пределах одного процесса с использованием программных потоков, и об этом мы тоже расскажем.

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

В любой момент процесс в UNIX может создать субпроцесс, являющийся его точной копией. Для этого выполняется системный вызов fork. Исходный процесс называется родительским, а новый - дочерним. Два процесса, полученные в результате вызова fork, абсолютно идентичны и даже разделяют одни и те же файловые дескрипторы. Однако каждый из этих двух процессов выполняет свою работу независимо от другого.

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

Эти два процесса работают параллельно (с системным вызовом exec или без него), но иногда родительский процесс должен по каким-либо причинам ждать, чтобы дочерний процесс завершил свою работу, и только после этого продолжает выполнение тех или иных действий. В этом случае родительский процесс выполняет системный вызов wait или waitpid, в результате чего он временно приостанавливается и ждет, пока дочерний процесс не выполнит системный вызов exit.

Процессы могут выполнять вызов fork сколь угодно часто, в результате чего получается целое дерево процессов. Посмотрите на рис. 6.29. Здесь процесс А выполнил вызов fork дважды и породил два новых процесса, В и С. Затем процесс В тоже выполнил вызов fork дважды, а процесс С - один раз. Таким образом, получилось дерево из шести процессов.

Рис. 6.29. Дерево процессов в системе UNIX

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

Примеры управления процессами

собой вид буфера, в который один процесс записывает поток данных, а другой процесс извлекает оттуда эти данные. Байты всегда возвращаются из канала в том порядке, в котором они были записаны. Случайный доступ невозможен. Каналы не сохраняют границ между фрагментами данных, поэтому если один процесс записал в канал 4 фрагмента по 128 байт, а другой процесс считывает данные по 512 байт, то второй процесс получит все данные сразу без указания на то, что они были записаны за несколько приемов.

В System V и Solaris применяется другой метод взаимодействия процессов. Здесь используются так называемые очереди сообщений. Процесс может создать новую очередь сообщений или открыть уже существующую с помощью вызова msgget. Для отправки сообщений используется вызов msgsnd, а для получения - msgrecv. Сообщения, отправленные таким способом, отличаются от данных, помещаемых в канал. Во-первых, границы сообщений сохраняются, в то же время канал просто передает поток данных. Во-вторых, сообщения имеют приоритеты, поэтому срочные сообщения идут перед всеми остальными. В-третьих, сообщения типизированы, и вызов msgrecv позволяет определять их тип, если это необходимо.

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

Еще одна особенность System V и Solaris - наличие семафоров. Принципы их работы мы уже описывали, когда говорили о производителе и потребителе.

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

В каких случаях могут понадобиться программные потоки? Рассмотрим веб-сервер. Такой сервер может хранить в основной памяти кэш часто используемых веб-страниц. Если нужная страница находится в кэш-памяти, то она выдается немедленно. Если нет, то она вызывается с диска. К сожалению, на это требуется довольно длительное время (обычно 20 миллисекунд), на это время процесс блокируется и не может обслуживать вновь поступающие запросы, даже если запрашиваемые веб-страницы находятся в кэш-памяти.

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

Стандарт системы UNIX на программные потоки называется pthreads и определяется в POSIX (Р1003.1С). Он описывает вызовы для управления программными потоками и их синхронизации. В стандарте ничего не сказано о том, должно ли управлять программными потоками ядро, или они должны функционировать только в пользовательском пространстве. Наиболее распространенные функции для работы с программными потоками приведены в табл. 6.9.

Таблица 6.9. Основные функции для работы с программными потоками, определенные в стандарте РОЭ1Х

Функция

Описание

pthread_create

Создание нового программного потока в адресном пространстве

 

вызывающей процедуры

pthread_exit

Завершение программного потока

pthreadjoin

Ожидание завершения программного потока

pthread_mutex_init

Создание нового мьютекса

pthread_mutex_destroy

Удаление мьютекса

pth read_m utexj oc k

Блокирование мьютекса

pthread_mutex_unlock

Разблокирование мьютекса

pthread_cond_init

Создание условной переменной

pthread_cond_destroy

Удаление условной переменной

pthread_cond_wait

Ждет условную переменную

pthread_cond_signal

Разблокирование одного из программных потоков, ожидающего

 

условной переменной

Давайте рассмотрим эти вызовы. Первый вызов, рт,пгеас1_сгеа1е, создает новый программный поток. После выполнения этой процедуры в адресном пространстве появляется на один программный поток больше. Программный поток, который выполнил свою работу, вызывает функцию рт,пгеас1_ех11;. Если потоку нужно подождать, пока другой поток окончит работу, он вызывает функцию р!г1геас1оо1П. Если этот другой программный поток уже закончил свою работу, вызов р1;г1геас1_зо"т немедленно завершается. В противном случае он блокируется.

Программные потоки можно синхронизировать с помощью специальных объектов, которые называются мьютексами (пигЬехез). Обычно мьютекс управляет каким-либо ресурсом (например, буфером, разделенным между двумя программными потоками). Для того чтобы в конкретный момент времени только один поток мог получать доступ к общему ресурсу, потоки должны блокировать мьютекс перед использованием ресурса и разблокировать его после завершения работы с ним. Так можно избежать состояния гонок, поскольку этому протоколу подчиняются все программные потоки. Мьютексы похожи на бинарные семафоры (то есть семафоры, которые могут принимать только два значения: 0 или 1).

Мьютексы можно создавать и разрушать вызовами pthreadmutexinit и pthreadmutexdestroy соответственно. Мьютекс может находиться в одном из двух состояний: заблокированном и незаблокированном. Если программному потоку нужно заблокировать незаблокированный мьютекс, он выполняет вызов pthread_ mutexlock, а затем продолжает работу. Однако если программный поток попытается заблокировать уже заблокированный мьютекс, поток приостанавливается. Когда поток, который в данный момент использует общий ресурс, завершит работу с этим ресурсом, он должен разблокировать соответствующий мьютекс вызовом pthread_mutex_u.nl оск.

Мьютексы предназначены для кратковременной блокировки (например, для защиты общей переменной), но не предназначены для длительной синхронизации (например, для ожидания, пока освободится накопитель на магнитной ленте). Для длительной синхронизации существуют условные переменные (condition variables). Эти переменные создаются и удаляются вызовами pthreadcondinit и pthreadconddestroy соответственно.

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

Управление процессами в Windows ХР

Windows ХР поддерживает несколько процессов, которые могут взаимодействовать и синхронизироваться. Каждый процесс содержит по крайней мере один программный поток, который, в свою очередь, содержит по крайней мере один легковесный поток, или волокно (fiber). Процессы, программные потоки и волокна в совокупности представляют собой инструменты поддержания параллелизма в одно- и многопроцессорных системах.

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

+ указатель на имя исполняемого файла;

♦ сама командная строка (без синтаксического разбора);

+ указатель на дескриптор безопасности данного процесса;

+ указатель на дескриптор безопасности исходного программного потока;

+ бит, который сообщает, наследует ли новый процесс описатели родительского процесса;

+ различные флаги (например, ошибки, приоритета, отладки, консолей);

+ указатель на строки описания конфигурации;

+ указатель на имя рабочего каталога нового процесса;

+ указатель на структуру, которая описывает исходное окно на экране;

+ указатель на структуру, которая возвращает 18 значений вызывающей процедуре.

В Windows ХР нет никакой иерархии родительских дочерних процессов. Все процессы создаются равными. Однако поскольку одним из 18 параметров, возвращаемых исходному процессу, является описатель нового процесса (который дает возможность контролировать новый процесс), здесь существует внутренняя иерархия в том смысле, что определенные процессы содержат описатели других процессов. Эти описатели нельзя просто непосредственно передавать другим процессам, но процесс может сделать определенный описатель доступным для другого процесса, а затем передать ему этот описатель, так что внутренняя иерархия процессов не может сохраняться долго.

Каждый процесс в Windows ХР создается с одним программным потоком, но позднее этот процесс может создать еще несколько таких потоков. Создать программный поток проще, чем процесс, поскольку вызов CreateThread имеет всего 6 параметров вместо 10: дескриптор безопасности, размер стека, начальный адрес, пользовательский параметр, начальное состояние потока (готов к работе или блокирован) и идентификатор потока. Поскольку созданием потоков занимается ядро, оно располагает информацией обо всех программных потоках (иными словами, их реализация не ограничивается пользовательским пространством, как в некоторых других системах).

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

Создавать программные потоки в Windows ХР довольно расточительно, поскольку для этого требуется войти в ядро, а затем выйти из него. Чтобы избежать этого, в Windows ХР предусмотрены волокна, которые похожи на программные потоки, но выполняются и синхронизируются в пользовательском пространстве программой, которая их создает. Каждый программный поток может иметь несколько волокон, точно так же как процесс может иметь несколько программных потоков, только в данном случае, когда волокно блокируется, оно встает в очередь заблокированных волокон и выбирает другое волокно для работы в своем и программном потоке. Ядро не знает об этом переходе, поскольку поток продолжает выполняться, даже если сначала в нем работало одно волокно, а затем другое.

Ядро управляет процессами и программными потоками, но не управляет волокнами. Волокна могут пригодиться, например, в том случае, когда программы, которые имеют собственные программные потоки, переносятся в Windows ХР.

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

Почтовые слоты (mailslot) - это атрибут исключительно Windows ХР (в UNIX их нет). Они во многом похожи на каналы, хотя не во всем. Во-первых, они односторонние, а каналы - двухсторонние. Их можно использовать в сети, но они не гарантируют доставку. Наконец, они поддерживают широковещательную рассылку сообщений нескольким получателям, а не только одному.

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

Удаленные вызовы процедур позволяют процессу А дать процессу В команду совершить вызов процедуры в адресном пространстве В от имени А и возвратить результат процессу А. Здесь существуют различные ограничения на параметры вызова. Например, передача указателя другому процессу не имеет никакого смысла.

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

Windows ХР предоставляет множество механизмов синхронизации (семафоры, мьютексы, критические секции, события). Все эти механизмы работают не с процессами, а с программными потоками, поэтому когда поток блокируется на семафоре, это никак не влияет на другие программные потоки этого процесса - они просто продолжают работать.

Семафор создается с помощью функции CreateSemaphore API, которая может установить его на определенное значение и определить его максимальное значение. Семафоры являются объектами ядра, поэтому они имеют дескрипторы безопасности и описатели. Описатель семафора можно продублировать с помощью функции DuplicateHandle и передать другому процессу, поэтому один семафор может обеспечить синхронизацию нескольких процессов. Поддерживаются также функции up и down, хотя они имеют другие названия: ReleaseSemaphore (для up) и WaitForSingleObject (для down). Можно определить для функции WaitForSingleObject предельное время простоя, и тогда вызывающий программный поток в конце концов может быть разблокирован, даже если семафор сохраняет значение 0 (однако таймеры способствуют возникновению условий гонок).

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

Третий механизм синхронизации основан на критических секциях. Критические секции похожи на мьютексы, если не считать их локальности по отношению к адресному пространству исходного программного потока. Поскольку критические секции не являются объектами ядра, у них нет описателей и дескрипторов безопасности, поэтому их нельзя передавать другим процессам. Блокирование и разблокирование осуществляется с помощью функций EnterCriticalSection и LeaveCriticalSection соответственно. Так как эти функции API выполняются исключительно в пользовательском пространстве, они работают гораздо быстрее, чем мьютексы.

Последний механизм связан с использованием объектов ядра, которые называются событиями. Если программному потоку нужно дождаться того или иного события, он вызывает WaitForSingleObject. С помощью функции SetEvent можно разблокировать один ожидающий программный поток, а с помощью функции PulseEvent - все ожидающие. Существует несколько видов событий, которые имеют по несколько параметров.

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

Примеры виртуального ввода-вывода || Оглавление || Краткое содержание главы6