Добавлен: 29.10.2018
Просмотров: 48009
Скачиваний: 190
11.4. Процессы и потоки в Windows
991
1. Реальный маршрут поиска подлежащей выполнению программы скрыт в библио-
течном коде Win32, а в UNIX им можно управлять более явно.
2. Текущий рабочий каталог — это концепция режима ядра в UNIX, а в Windows —
это строка пользовательского режима. Windows открывает описатель для текущего
каталога для каждого процесса (с тем же назойливым эффектом, что и в UNIX, —
вы не сможете удалить каталог, если только это не сетевой каталог).
3. UNIX разбирает командную строку и передает массив параметров, а Win32 остав-
ляет разбор аргументов программе. Вследствие этого некоторые программы могут
обрабатывать групповые символы (например,
*.txt
и прочие специальные симво-
лы) по-разному.
4. Способность наследования файловых дескрипторов в UNIX — это свойство опи-
сателя. В Windows это свойство и описателя, и параметра создания процесса.
5. Win32 ориентирован на графический интерфейс пользователя, так что новым
процессам напрямую передается информация об их первичном окне (в то время
как в UNIX эта информация передается приложениям графического интерфейса
пользователя как параметры).
6. Исполняемые файлы в Windows не имеют бита SETUID, но процесс может создать
другой процесс, который выполняется как другой пользователь (если он может
получить маркер с учетными данными этого пользователя).
7. Возвращенные из Windows описатели процесса и потока можно в любое время
использовать для модификации нового процесса или потока многими способа-
ми, в том числе изменением виртуальной памяти, внедрением потоков в процесс
и сменой порядка выполнения потоков. UNIX вносит изменения в новый процесс
только между вызовами fork и exec и только ограниченными способами, поскольку
exec отвергает все состояния процесса пользовательского режима.
Некоторые из этих отличий исторические, другие — философские. UNIX был разра-
ботан с ориентацией на командную строку, а Windows — на графический интерфейс
пользователя. Пользователи UNIX более квалифицированные и понимают концеп-
ции типа переменных PATH. А Windows унаследовала от MS-DOS много устаревших
свойств.
Это сравнение также не вполне корректное, так как Win32 — это оболочка поль-
зовательского режима для выполнения собственных процессов NT, подобно тому
как библиотечная функция system является оболочкой для fork и exec в UNIX. Ре-
альные системные вызовы NT для создания процессов и потоков (NtCreateProcess
и NtCreateThread) гораздо проще, чем версии из Win32. Главными параметрами со-
здания процессов в NT являются описатель сегмента (представляющего подлежащий
выполнению файл программы), флаг (указывающий, должен ли новый процесс по
умолчанию наследовать описатели от создателя) и параметры (относящиеся к модели
безопасности). Все подробности настройки строк окружения и создания первоначаль-
ного потока оставлены коду пользовательского режима, который может использовать
описатель нового процесса для того, чтобы напрямую манипулировать его виртуаль-
ным адресным пространством.
Для поддержки подсистемы POSIX создание собственных процессов имеет опцию для
создания нового процесса путем копирования виртуального адресного пространства
другого процесса (вместо отображения объекта сегмента для новой программы). Эта
992
Глава 11. Изучение конкретных примеров: Windows 8
возможность используется только для реализации fork для POSIX (а не для Win32).
Поскольку POSIX с Windows больше не поставляется, дублирование процессов не на-
ходит широкого применения, но иногда инициативные разработчики выдумывают для
него специальное применение, подобное применению fork без exec в UNIX.
Создание потока передает контекст процессора для использования его в новом потоке.
В контекст входят указатель стека и начальный указатель команд, шаблон для ТЕВ,
а также флаг, указывающий, должен поток выполняться немедленно или он должен
быть создан в состоянии приостановки (в ожидании вызова кем-либо NtResumeThread
с его описателем). Создание стека пользовательского режима и помещение в него
параметров argv/argc оставлено для кода пользовательского режима (вызывающего
интерфейс API управления памятью с параметром — описателем процесса).
В версию Windows Vista был добавлен новый собственный API для процессов,
NtCreateUserProcess, который перенес большое количество шагов пользовательского
режима в исполнительный уровень режима ядра и скомбинировал создание процесса
с созданием начального потока. Причиной этих изменений была поддержка использо-
вания процессов в качестве границ безопасности. Обычно все созданные пользователем
процессы считаются одинаково безопасными. Именно пользователь (представленный
маркером) определяет границу безопасности. NtCreateUserProcess позволяет процессам
также предоставлять границы безопасности, но это означает, что создающий процесс
не имеет достаточных прав на описатель нового процесса, чтобы реализовать детали
создания процесса в пользовательском режиме для процессов, находящихся в разных
средах доверия. В основном процесс, используемый в других границах доверия (и на-
зываемый защищенным процессом), предназначен для поддержки форм управления
правами на цифровой контент, защищающих материал, на который распространяются
авторские права, от несанкционированного использования. Разумеется, защищенный
процесс нацелен на противодействие атакам на защищаемый контент, проводимым
только в пользовательском режиме, и не может противодействовать атакам, прово-
димым в режиме ядра.
Межпроцессный обмен
Потоки могут вести обмен самыми разными способами, в том числе при помощи кана-
лов, именованных каналов, почтовых слотов, сокетов, вызовов удаленных процедур,
совместно используемых файлов. Каналы имеют два режима: байтовый и режим со-
общений (выбирается во время создания). В байтовом режиме каналы работают так
же, как и в UNIX. Каналы в режиме сообщений немного похожи на них, но сохраняют
границы сообщений, так что четыре записи по 128 байт будут прочитаны как четыре
сообщения по 128 байт (а не как одно сообщение размером 512 байт, как это может
случиться с каналом в байтовом режиме). Именованные каналы тоже существуют
и имеют такие же два режима работы, как и обычные каналы. Именованные каналы
могут использоваться и по Сети (а обычные — нет).
Почтовые слоты
(mailslots) — это функция операционной системы OS/2, реализован-
ная в Windows для совместимости. В некоторых отношениях они похожи на каналы,
но не во всем. Например, они односторонние (а каналы — двусторонние). Их можно
использовать по Сети, но они не обеспечивают гарантированной доставки. И наконец,
они позволяют посылающему процессу транслировать сообщение множеству полу-
чателей (а не только одному). И почтовые слоты, и именованные каналы реализованы
в Windows как файловые системы, а не как функции исполнительного уровня. Это по-
11.4. Процессы и потоки в Windows
993
зволяет осуществлять к ним доступ по Сети при помощи существующих протоколов
удаленных файловых систем.
Сокеты
(sockets) подобны каналам, за исключением того, что они обычно соединяют
процессы на разных машинах. Например, один процесс пишет в сокет, а другой (на
удаленной машине) читает из него. Сокеты можно также использовать для соединения
процессов на одной и той же машине, но поскольку их использование влечет за собой
большие издержки, чем использование каналов, то их обычно используют только
в сетевом контексте. Сокеты были изначально разработаны для Berkeley UNIX, и эта
реализация стала широкодоступной. Некоторая часть кода и структур данных Berkeley
и до нынешнего дня присутствует в Windows (это признано в «Заметках о версии»,
сопровождающих операционную систему).
RPC
(удаленные вызовы процедур) — это способ для процесса А сделать так, чтобы
процесс В вызвал процедуру в адресном пространстве В от имени А и вернул результат
в А. Параметры имеют различные ограничения. Например, нет смысла передавать ука-
затель на другой процесс, поэтому структуры данных должны быть упакованы и пере-
даны неспецифичным для процесса способом. RPC обычно реализованы как уровень
абстракции над транспортным уровнем. В случае Windows транспортом могут быть
сокеты TCP/IP, именованные каналы или ALPC. ALPC (Advanced Local Procedure
Call — расширенный вызов локальных процедур) — это средство передачи сообще-
ний в исполнительном уровне режима ядра. Оно оптимизировано для обмена между
процессами локального компьютера и не работает по сети. Основная идея — посылать
сообщения, которые генерируют ответы (реализуя таким образом облегченную версию
вызовов удаленных процедур, на основе которой RPC может создать более богатый
набор функциональных возможностей, чем те, которые имеются в ALPC). ALPC ре-
ализован при помощи сочетания копирования параметров и временного выделения
совместно используемой памяти (в зависимости от размера сообщений).
И наконец, процессы могут совместно использовать объекты. Сюда входят и объекты
сегментов, которые можно отобразить на виртуальное адресное пространство разных
процессов (в одно и то же время). Все операции записи, сделанные одним процессом,
появляются в адресных пространствах других процессов. При помощи этого механизма
можно легко реализовать совместно используемый буфер, который применяется при
решении проблем «источник — потребитель».
Синхронизация
Процессы также могут использовать разные типы объектов синхронизации. Точно так
же как Windows предоставляет множество механизмов для межпроцессного обмена,
она предоставляет и множество механизмов синхронизации (включая семафоры,
мьютексы, критические области и события). Все эти механизмы работают с потоками
(а не с процессами), так что когда поток блокируется на семафоре, другие потоки этого
процесса (если они есть) могут продолжать выполнение.
Семафор может создаваться при помощи функции CreateSemaphore интерфейса Win32
API, которая может также инициализировать его заданным значением и задать макси-
мальное значение. Семафоры — это объекты режима ядра, поэтому они имеют дескрип-
торы безопасности и описатели. Описатель для семафора может быть сдублирован
при помощи DuplicateHandle и передан в другой процесс (чтобы по одному и тому же
семафору могли синхронизироваться несколько процессов). Семафору можно дать имя
994
Глава 11. Изучение конкретных примеров: Windows 8
в пространстве имен Win32, он может иметь ACL для своей защиты. Иногда совместное
использование семафора по имени более удобно, чем дублирование описателя.
Имеются вызовы для операций up и down, хотя названия у них несколько странные:
ReleaseSemaphore (это up) и WaitForSingleObject (это down). Можно также задать тайм-
аут для WaitForSingleObject, чтобы вызывающий поток мог быть освобожден, даже
если семафор останется на значении 0 (однако таймеры создают условия гонки).
WaitForSingleObject и WaitForMultipleObjects — это интерфейсы для ожидания объектов
диспетчеризации (обсуждались в разделе 11.3). Несмотря на то что в принципе воз-
можно заключить однообъектную версию этих API в оболочку с более дружественным
для семафоров названием, многие потоки используют многообъектную версию, которая
может вызвать ожидание различных объектов синхронизации и прочих событий (вроде
завершения процессов и потоков, завершения ввода-вывода, поступления сообщений
в сокеты или порты).
Мьютексы — это тоже объекты режима ядра, используемые для синхронизации, но
они проще семафоров, поскольку не имеют счетчиков. По существу это блокировки,
имеющие функции API для блокирования (WaitForSingleObject) и разблокирования
(ReleaseMutex). Подобно описателям семафоров, описатели мьютексов могут дубли-
роваться и передаваться между процессами, чтобы потоки в разных процессах могли
получить доступ к одному и тому же мьютексу.
Третий механизм синхронизации называется критической секцией (critical section).
Он реализует концепцию критических областей. В Windows он похож на мьютекс, за
исключением того, что он является локальным для адресного пространства создаю-
щего потока. Поскольку критические секции не являются объектами режима ядра,
они не имеют явных описателей или дескрипторов безопасности и не могут переда-
ваться между процессами. Блокирование и разблокирование выполняется вызовами
EnterCriticalSection и LeaveCriticalSection соответственно. Поскольку эти функции API
выполняются первоначально в пространстве пользователя и делают вызовы ядра толь-
ко при необходимости в блокировке, то они гораздо быстрее мьютексов. Критические
секции оптимизированы для комбинированного использования спин-блокировок
(на многопроцессорных системах) и синхронизации ядра (при необходимости). Во
многих приложениях большинство критических секций так редко становятся объек-
том соперничества или имеют такое краткое время удерживания, что необходимость
в выделении объекта синхронизации ядра никогда не возникает. Это приводит к очень
существенной экономии памяти ядра.
Еще один рассматриваемый механизм синхронизации использует объекты режима
ядра, которые называются событиями (events). Как мы описывали ранее, существует
два их вида: события уведомления (notification events) и события синхронизации
(synchronization events). Событие может быть в одном из двух состояний: сигнализи-
рованном или несигнализированном. Поток может ждать сигнализации события при
помощи WaitForSingleObject. Если другой поток сигнализирует событие при помощи
SetEvent, то результат зависит от типа события. Для события уведомления будут осво-
бождены все ждущие потоки, а событие останется установленным до тех пор, пока не
будет сброшено вручную при помощи ResetEvent. Для события синхронизации если
ждет один или несколько потоков, то освобождается только один поток и событие
сбрасывается. Альтернативная операция — PulseEvent, которая похожа на SetEvent (за
исключением того, что если никто не ждет, то импульс теряется и событие сбрасывает-
ся). В отличие от нее SetEvent (когда оно происходит в отсутствие ожидающих потоков)
11.4. Процессы и потоки в Windows
995
запоминается — событие остается в сигнализированном состоянии, так что следующий
поток (который вызывает API для ожидания события) фактически ждать не будет.
Количество вызовов Win32 API для работы с процессами, потоками и волокнами
составляет почти 100 штук, причем большое их количество в той или иной форме
работает с IPC.
Недавно в Windows были добавлены два новых примитива синхронизации,
WaitOnAddress и InitOnceExecuteOnce. WaitOnAddress вызывается для ожидания измене-
ния значения по указанному адресу. Приложение после внесения изменения в конкрет-
ное место в памяти должно вызвать либо WakeByAddressSingle, либо WakeByAddressAll,
чтобы инициировать либо первый, либо все потоки, вызывающие WaitOnAddress
относительно этого адреса. Преимуществом этого API над использованием событий
является то, что здесь не нужно выделять для синхронизации явное событие. Вместо
этого система хэширует адрес места, чтобы найти список всех, кто ожидает изменений
по заданному адресу. Функции WaitOnAddress подобны механизму засыпания и про-
буждения (sleep/wakeup), имеющемуся в ядре UNIX. InitOnceExecuteOnce может
использоваться для обеспечения в программе только однократного запуска инициа-
лизирующей подпрограммы. Как ни удивительно, но правильно инициализировать
структуры данных в многопотоковых программах очень непросто. Сводка по рас-
смотренным ранее примитивам синхронизации, а также некоторым другим важным
вызовам дана в табл. 11.12.
Таблица 11.12. Некоторые из вызовов Win32, предназначенные для управления про-
цессами, потоками и волокнами
Функция Win32 API
Описание
CreateProcess
Создать новый процесс
CreateThread
Создать новый поток в существующем процессе
CreateFiber
Создать новое волокно
ExitProcess
Завершить текущий процесс и все его потоки
ExitThread
Завершить этот поток
ExitFiber
Завершить это волокно
SwitchToFiber
Выполнить другое волокно в текущем потоке
SetPriorityClass
Установить класс приоритета для процесса
SetThreadPriority
Установить приоритет для одного потока
CreateSemaphore
Создать новый семафор
CreateMutex
Создать новый мьютекс
OpenSemaphore
Открыть существующий семафор
OpenMutex
Открыть существующий мьютекс
WaitForSingleObject
Блокировать по одному семафору, мьютексу и т. д.
WaitForMultipleObjects
Блокировать по набору объектов, описатели которых заданы
PulseEvent
Установить событие сначала в сигнализированное, а затем в несигна-
лизированное состояние
ReleaseMutex
Освободить мьютекс, чтобы другой поток мог завладеть им