Объекты и их дескрипторы в Windows
Объектом в Windows называется структура данных, которая представляет системный ресурс. Таким ресурсом может быть, например, файл, канал, графический рисунок. Операционные системы Windows предоставляют приложению объекты трех категорий:
□ User (объекты интерфейса пользователя);
□ Graphics Device Interface (объекты интерфейса графических устройств);
□ Kernel (объекты ядра).
Категория User включает объекты, которые используются приложением для интерфейса с пользователем. К таким объектам относятся, например, окна и курсоры. Категория Graphics Device Interface включает объекты, которые используются для вывода информации на графические устройства. К таким объектам относятся, например, кисти и перья. Категория Kernel включает объекты ядра операционной системы Windows. К таким объектам относятся, например, файлы и каналы. При изучении системного программирования подробно рассматриваются только объекты категории Kernel. Объекты двух оставшихся категорий рассматриваются при изучении программирования графических интерфейсов.
Под доступом к объектам понимается возможность приложения выполнять над объектом некоторые функции. Приложение не имеет прямого доступа к объектам, а обращается к ним косвенно. Для этого в операционных системах Windows каждому объекту ставится в соответствие дескриптор (handle).
В Win32 API дескриптор имеет тип handle. Дескриптор объекта представляет собой запись в таблице, которая поддерживается системой и содержит адрес объекта и средства для идентификации типа объекта. Дескрипторы объектов создаются операционной системой и возвращаются функциями Win32 API, которые создают объекты. За редким исключением, эти функции имеют вид createobject, где слово object заменяется именем конкретного объекта. Например, процесс создается при помощи вызова функции createProcess. Как правило, такие функции возвращают дескриптор созданного объекта. Если это значение не равно null (или отрицательному значению), то объект создан успешно.
После завершения работы с объектом его дескриптор нужно закрыть, используя функцию closeHandle, которая имеет следующий прототип:
BOOL CloseHandle(
HANDLE hObject // дескриптор объекта
);
При успешном завершении функция closeHandle возвращает ненулевое значение, в противном случае — false. Функция closeHandle удаляет дескриптор объекта, но сам объект удаляется не всегда. Дело в том, что в Windows на один и тот же объект могут ссылаться несколько дескрипторов, которые создаются другими функциями для доступа к уже созданному ранее объекту. Функция closeHandle уничтожает объект только в том случае, если на него больше не ссылается ни один дескриптор.
Лекция 8. Управление потоками и процессами
Определение потока
Определение потока тесно связано с последовательностью действий процессора во время исполнения программы. Исполняя программу, процессор последовательно выполняет инструкции программы, иногда осуществляя переходы в зависимости от некоторых условий. Такая последовательность выполнения инструкций программы называется потоком управления внутри программы. Отметим, что поток управления зависит от начального состояния переменных, которые используются в программе. В общем случае различные исходные данные порождают различные потоки управления. Поток управления можно представить как нить в программе, на которую нанизаны инструкции, выполняемые микропроцессором. Поэтому часто поток управления также называется нитью (thread). В русскоязычной литературе за потоком управления закрепилось название поток. Для пояснения понятия потока рассмотрим следующую программу, которая выводит минимальное число из двух целых чисел или сообщение о том, что числа равны.
#include <iostream.h>
int main()
{
int a, ' b;
cout « "Input two integers: ";
cin » a » b;
if (a == b)
{
cout « "There is no min." « endl;
return 0;
if (а < Ь)
cout « "min = " « а « endl;
else
cout « "min = " « b « endl;
return 0;
}
Предположим, что перегруженные операторы ввода-вывода не образуют новых потоков. Тогда в зависимости от входных данных эта программа образует один из трех возможных потоков управления. А именно, если выполняется условие (а == Ь), то образуется поток:
cout « "Input two integers: ";
cin » a » b;
if (a == b)
{
cout « "There is no min." « endl;
return 0;
}
Если выполняется условие (а < b), то образуется поток:
cout « "Input two integers: ";
cin ». a » b;
if (a == b)
if (a < b)
cout « "min = " « a « endl;
return 0;
Если же выполняется условие (а > b), то образуется поток
cout « "Input two integers: ";
cin » a » b;
if (a == b)
if (a < b)
cout « "min = " « b « endl;
return 0;
Теперь перейдем к классификации программ в зависимости от количества определяемых ими параллельных потоков управления. Будем говорить, что программа является многопоточной, если в ней может одновременно существовать несколько потоков. Сами потоки в этом случае называются параллельными. Если в программе одновременно может существовать только один поток, то такая программа называется однопотонной. Например, еледующая программа, которая просто вычисляет сумму двух чисел, является
однопоточной:
#include <iostream.h>
int sum(int a, int b)
{
return a + b;
}
int main()
{
int a, b;
int с = 0;
cout « "Input two integers: ";
cin » 'a » b;
с = sum (a, b) ;
cout « "Sum = " « с « endl;
return 0;
}
Теперь предположим, что после вызова функции sum функция main не ждет возвращения значения из функции sum, а продолжает выполняться. В этом случае получим программу, состоящую из двух потоков, один из которых определяется функцией main, а второй — функцией sum. Причем эти потоки независимы, т. к. они не имеют доступа к общим или, другими словами, разделяемым переменным. Правда в этом случае не гарантируется, что поток main выведет сумму чисел а и ь, т. к. инструкция вывода значения суммы может отработать раньше, чем поток sum вычислит эту сумму. Из этих рассуждений видно, что для того чтобы отметить функцию, которая порождает новый поток в программе, должна использоваться специальная нотация. В операционных системах Windows для обозначения того, что функция образует поток, используются специальные спецификаторы функции. Такая функция обычно также называется потоком.
Контекст потока
В общем случае содержимое памяти, к которой поток имеет доступ во время своего исполнения, называется контекстом потока. Определим, каким ограничениям на доступ к памяти должны удовлетворять функции, чтобы их можно было безопасно вызывать в параллельных потоках. Для этого рассмотрим следующую функцию:
int f(int n)
if (n > 0)
—п;
if (n < 0)
++п;
return n;
}
Сколько бы раз эта функция не вызывалась параллельно работающими потоками, она будет корректно изменять значение переменной п, т. к. эта переменная является локальной в функции f. To есть для каждого нового вызова функции f будет создан новый локальный экземпляр переменной п. Такая функция f называется безопасной для потоков. Теперь введем глобальную переменную п и изменим нашу функцию следующим образом:
int n ;
void g()
{
if (n > 0)
—П;
if (n < 0)
++П;
}
В этом случае параллельный вызов функции g несколькими потоками может дать некорректное изменение значения переменной п, т. к. значение этой переменной будет изменяться одновременно несколькими функциями д. В этом случае функция д не является безопасной для потоков. Та же проблема встречается и в случае, когда функция использует статические переменные. Для разбора этого случая рассмотрим функцию
int count()
{
static int n = 0;
++n;
return n;
}
которая возвращает количество своих вызовов. Если эта функция будет вызвана несколькими параллельно исполняемыми потоками, то цельзя точно определить значение переменной п, которое вернет эта функция, т. к. это значение изменяется всеми потоками параллельно.
В общем случае функция называется повторно входилюй или реентерабельной (reentrant или reenterable), если она удовлетворяет следующим требованиям:
□ не использует глобальные переменные, значения которых изменяются
параллельно исполняемыми потоками;
□ не использует статические переменные, определенные внутри функции;
□ не возвращает указатель на статические данные, определенные внутри
функции.
В системном программировании часто также рассматриваются программы в кодах микропроцессора, выполнение которых может прерываться и возобновляться в любой момент времени. Причем одна и та же программа может запускаться прежде, чем завершилось исполнение предыдущего экземпляра этой программы. В этом случае также необходимо, чтобы программный код допускал корректное параллельное выполнение нескольких экземпляров программы. Это условие обеспечивается в том случае, если программа не изменяет свой код во время исполнения. Здесь под кодом подразумеваются как команды, так и данные, принадлежащие программе.
Программа в кодах микропроцессора, которая не изменяет свой код, также называется реентерабельной. В дополнение к реентерабельным функциям определяют также функции, безопасные для вызова параллельно исполняемыми потоками. Функция называется безопасной для потоков, если она обеспечивает блокировку доступа к ресурсам, которые она использует. D этом случае решается задача взаимного исключения доступа к разделяемым ресурсам, используя примитивы синхронизации.
Очевидно, что если функция не является реентерабельной, то она также не является и безопасной для потоков, т. к. в этом случае несколько потоков разделяют общую память, не блокируя доступ к ней. А память, как уже говорилось, также является системным ресурсом.
Состояния потока
Как видно из определения, поток описывает динамическое поведение всей программы или какой-либо функции в программе. Для удобства обозначений предположим, что программа является однопоточной. Тогда поток можно рассматривать как пару:
поток = (процессор, программа).
Программа может исполняться процессором только в том случае, если она готова к исполнению. То есть все системные ресурсы, которые необходимы для исполнения этой программы, свободны для использования. Кроме того, для исполнения программы необходимо, чтобы и сам процессор был свободен и готов к исполнению этой программы. Для более формального описания этих ситуаций вводятся понятия "состояние процессора" и "состояние программы". При этом предполагают, что процессор и программа могут находиться в следующих состояниях.
□ Состояния процессора:
• процессор не выделен для исполнения программы;
• процессор выделен для исполнения программы.
□ Состояния программы:
• программа не готова к исполнению процессором;
• программа готова к исполнению процессором.
Для краткости записи введем для этих состояний следующие названия:
□ Состояния процессора:
• "не выделен";
• "выделен".
□ Состояния программы:
• "не готова";
• "готова".
Тогда мы можем определить состояние потока как пару состояний:
состояние потока = (состояние процессора, состояние программы).
Перечислив различные комбинации состояний процессора и программы, можно описать все возможные состояния потока. Введем для состояний потока следующие названия:
□ поток блокирован = ("не выделен", "не готова");
□ поток готов к выполнению = ("не выделен", "готова");
□ поток выполняется = ("выделен", "готова");
Будем считать, что состояние ("выделен", "не готова") является недостижимым для потока. То есть программе, не готовой к исполнению, процессор не выделяется. Более кратко эти состояния потока будем просто обозначать словами: "блокирован", "готов" и "выполняется". Для полноты картины нужно ввести для потоков еще два состояния: "новый" и "завершен", которые описывают соответственно поток, еще не начавший свою работу, и поток, завершивший свою работу.
В результате мы получили простейшую диаграмму переходов потока из состояния в состояние. Сами переходы потока из состояния в состояние, которые на диаграмме обозначаются дугами, описывают некоторые операции над потоком. Названия этих операций указаны рядом со стрелками. Кратко опишем эти операции.
□ Операция create выполняется потоком, который создает новый поток из функции. Эта операция переводит поток из состояния "новый" в состояние "готов".
□ Операция Exit выполняется самим исполняемым потоком в случае его завершения. Эта операция переводит поток из состояния "выполняется" в состояние "завершен".
Оставшиеся четыре операции выполняются операционной системой.
□ Операция Run запускает готовый поток на выполнение, т. е. выделяет ему процессорное время. Эта операция переводит поток из состояния "готов" в состояние "выполняется". Поток получает процессорное время в том случае, если подошла его очередь к процессору на обслуживание.
□ Операция interrupt задерживает исполнение потока и переводит его из состояния "выполняется" в состояние "готов". Эта операция выполняется над потоком в том случае, если истекло процессорное время, выделенное потоку на исполнение, или исполнение потока прервано по каким-либо другим причинам.
П Операция Block блокирует исполнение потока, т. е. переводит его из состояния "выполняется" в состояние "блокирован". Эта операция выполняется над потоком в том случае, если он ждет наступления некоторого события, например, завершения операции ввода-вывода или освобождения ресурса.
□ Операция unblock разблокирует поток, т. е. переводит его из состояния "блокирован" в состояние "готов". Эта операция выполняется над потоком в том случае, если событие, ожидаемое потоком, наступило.
Разрешим потокам также выполнять операции друг над другом. Для этого введем операции Suspend И Resume.
□ Операция suspend приостанавливает исполнение потока.
□ Операция Resume возобновляет исполнение потока.
Используя эти операции, один поток может соответственно приостановить или возобновить исполнение другого потока независимо от того, в каком состоянии этот последний поток находится. Впрочем, заметим, что поток может приостановить и свое исполнение. Если над потоком выполнена операция suspend, то будем говорить, что поток находится в приостановленном или подвешенном состоянии. Кратко будем говорить, что в этом случае поток "подвешен". Дополним диаграмму состояний потока, этими новыми операциями и состояниями. Получим более полную диаграмму состояний потока, которая показана на рис. 2.2.
Диспетчеризация и планирование потоков
В однопрограммной операционной системе одновременно может выполняться только один поток, которому доступны все ресурсы компьютера. Поэтому блокировка потока может происходить только в случаях ожидания этим потоком события, отмечающего завершение операций ввода-вывода.
Недостатком однопрограммных операционных систем является их низкая производительность, т. к. процессор простаивает, если поток блокирован. В мультипрограммных операционных системах одновременно могут существовать несколько потоков, что повышает производительность компьютера. Однако в этом случае требуется некоторая дисциплина обслуживания этихпотоков, смысл которой заключается в порядке выделения конкурирующим потокам ресурсов компьютера.
Для простоты дальнейшего изложения будем считать, что компьютер имеет только один процессор. Тогда общий подход к обслуживанию потоков в мультипрограммных операционных системах состоит в следующем. Время работы процессора делится на кванты (интервалы), которые выделяются потокам для работы. По истечении кванта времени исполнение потока прерывается и процессор назначается другому потоку. Распределением квантов времени между потоками занимается специальная программа, которая называется менеджер потоков.
Когда менеджер потоков переключает процессор на исполнение другого потока, он должен выполнить следующие действия:
□ сохранить контекст прерываемого потока;
□ восстановить контекст запускаемого потока на момент его прерывания;
□ передать управление запускаемому потоку.
Контекст потока это содержимое памяти, с которой работает поток. Поэтому в каждый момент времени работы потока, его контекст полностью определяется содержимым регистров микропроцессора в этот момент времени. Отсюда следует, что для сохранения контекста потока необходимо сохранить содержимое регистров микропроцессора на момент прерывания потока, а при восстановлении контекста потока необходимо восстановить содержимое этих регистров.
Определение процесса
Процессом или задачей называется исполняемое на компьютере приложение вместе со всеми ресурсами, которые требуются для его исполнения. Все ресурсы, необходимые для исполнения процесса, также называются контекстом процесса. Процессу обязательно принадлежат следующие ресурсы:
□ адресное пространство процесса;
□ потоки, исполняемые в контексте процесса.
Адресное пространство — это виртуальная память, выделенная процессу для запуска программ.
Адресные пространства разных процессов не пересекаются. Более того, процесс не имеет непосредственного доступа в адресное пространство другого процесса. Это позволяет избежать влияния ошибок, произошедших в каком-либо процессе, на исполнение других процессов, что повышает надежность системы в целом. Потоки, исполняемые в контексте процесса, запускаются в одном адресном пространстве, которое принадлежит этому процессу. В принципе, основной причиной, вызвавшей введение в системное программирование понятия потока, и было разделение адресных пространств процессов. Дело в том, что в этом случае взаимодействие между параллельными процессами требует больших затрат на пересылку данных, что заметно замедляет работу приложений. Потоки же выполняются в адресном пространстве одного процесса и, следовательно, могут обращаться к общим адресам памяти, что упрощает их взаимодействие.
Определение потока
Потоком в Windows называется объект ядра, которому операционная система выделяет процессорное время для выполнения приложения. Каждому потоку принадлежат следующие ресурсы:
□ код исполняемой функции;
□ набор регистров процессора;
□ стек для работы приложения;
□ стек для работы операционной системы;
□ маркер доступа, который содержит информацию для системы безопасности.
Все эти ресурсы образуют контекст потока в Windows. Кроме дескриптора каждый поток в Windows также имеет свой идентификатор, который уникален для потоков выполняющихся в системе. Идентификаторы потоков используются служебными программами, которые позволяют пользователям системы отслеживать работу потоков.
В операционных системах Windows различаются потоки двух типов:
□ системные потоки;
□ пользовательские потоки.
Системные потоки выполняют различные сервисы операционной системы и запускаются ядром операционной системы. Пользовательские потоки служат для решения задач пользователя и запускаются приложением. На рис. 3.1 показана диаграмма состояний потока, работающего в среде операционной системе Windows 2000. В работающем приложении различаются потоки двух типов:
□ рабочие потоки (working threads);
□ потоки интерфейса пользователя (user interface threads).
Рис. 3.1. Модель состояний потока в Windows 2000
Рабочие потоки выполняют различные фоновые задачи в приложении. Потоки интерфейса пользователя связаны с окнами и выполняют обработку сообщений, поступающих этим окнам. Каждое приложение имеет, по крайней мере, один поток, который называется первичным (primary) или главным
(main) потоком. В консольных приложениях это поток, который исполняет функцию main. В приложениях с графическим интерфейсом это поток, который исполняет функцию WinMain.
Создание потоков
Создается поток функцией CreateThread, которая имеет следующий прототип:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // атрибуты защиты
DWORD dwStackSize, // размер стека потока в байтах
LPTHREAD_START_ROUTINE IpStartAddress, // адрес функции
LPVOID lpParameter, // адрес параметра
DWORD dwCreationFlags, // флаги создания потока
LPDWORD lpThreadld // идентификатор потока
При успешном завершении функция CreateThread возвращает дескриптор созданного потока и его идентификатор, который является уникальным для всей системы. В противном случае эта функция возвращает значение null. Кратко опишем назначение параметров функции CreateThread.
Параметр lpThreadAttributes устанавливает атрибуты защиты создаваемого потока. До тех пор пока мы не изучим систему безопасности в Windows, мы будем устанавливать значения этого параметра в null при вызове почти всех функций ядра Windows. В данном случае это означает, что операционная система сама установит атрибуты защиты потока, используя настройки по умолчанию.
Параметр dwstacksize определяет размер стека, который выделяется потоку при запуске. Если этот параметр равен нулю, то потоку выделяется стек, размер которого по умолчанию равен 1 Мбайт. Это наименьший размер стека, который может быть выделен потоку. Если величина параметра dwstacksize меньше значения, заданного по умолчанию, то все равно потоку выделяется стек размером в 1 Мбайт. Операционная система Windows округляет размер стека до одной страницы памяти, который обычно равен 4 Кбайт.
Параметр lpstartAddress указывает на исполняемую потоком функцию. Эта функция должна иметь следующий прототип: DWORD WINAPI имя_функции_потока(LPVOID lpParameters);
Видно, что функции потока может быть передан единственный параметр lpParameter, который является указателем на пустой тип. Это ограничение следует из того, что функция потока вызывается операционной системой, а не прикладной программой. Программы операционной системы являются исполняемыми модулями и поэтому они должны вызывать только функции, сигнатура которых заранее определена. Поэтому для потоков определили самый простой список параметров, который содержит только указатель. Так как функции потоков вызываются операционной системой, то они также получили название функции обратного вызова. Параметр dwCreationFiags определяет, в каком состоянии будет создан поток. Если значение этого параметра равно 0, то функция потока начинает выполняться сразу после создания потока. Если же значение этого параметра равно create_suspended, то поток создается в подвешенном состоянии. В дальнейшем этот поток можно запустить вызовом функции ResumeThread.
Параметр lpThreadid является выходным, т. е. его значение устанавливает Windows. Этот параметр должен указывать на переменную, в которую Windows поместит идентификатор потока. Этот идентификатор уникален для всей системы и может в дальнейшем использоваться для ссылок на поток. Идентификатор потока главным образом используется системными функциями и редко функциями приложения. Действителен идентификатор потока только на время существования потока. После завершения потока тот же идентификатор может быть присвоен другому потоку. В операционной системе Windows 98 этот параметр не может быть равен null. В Windows NT и 2000 допускается установить его значение в null — тогда операционная система не возвратит идентификатор потока.
Завершение потоков
Поток завершается вызовом функции ExitThread, которая имеет следующий прототип:
VOID ExitThread(
DWORD dwExitCode // код завершения потока
);
Эта функция может вызываться как явно, так и неявно при возврате значения из функции потока. При выполнении этой функции система посылает динамическим библиотекам, которые загружены процессом, сообщение dll_thread_detach, которое говорит о том, что поток завершает свою работу.
Если поток создается при помощи макрокоманды _beginthreadex, то для завершения потока нужно использовать макрокоманду _endthreadex, единственным параметром которой является код возврата из потока. Эта макрокоманда описана в заголовочном файле process.h. Причина использования в этом случае макрокоманды _endthreadex заключается в том, что она не только выполняет выход из потока, но и освобождает память, которая была распределена макрокомандой _beginthreadex. Если поток создан функцией _beginthreadex, то для выхода из потока функция _endthreadex может вызываться как явно, так и неявно при возврате значения из функции потока.
Один поток может завершить другой поток, вызвав функцию TerminateThread, которая имеет следующий прототип:
BOOL TerminateThread(
HANDLE hThread, // дескриптор потока
DWORD dwExitThread // код завершения потока
);
В случае успешного завершения функция TerminateThread возвращает ненулевое значение, В ПРОТИВНОМ случае — FALSE. Функция TerminateThread завершает поток, но не освобождает все ресурсы, принадлежащие этому потоку. Это происходит потому, что при выполнении этой функции система не посылает динамическим библиотекам, загруженным процессом, сообщение о том, что поток завершает свою работу. В результате динамическая библиотека не освобождает ресурсы, которые были захвачены для работы с этим потоком. Поэтому эта функция должна вызываться только в аварийных ситуациях при зависании потока.
Лекция 9. Синхронизация потоков и процессов
Определение процесса
В Windows под процессом понимается объект ядра, которому принадлежат системные ресурсы, используемые исполняемым приложением. Поэтому можно сказать, что в Windows процессом является исполняемое приложение. Выполнение каждого процесса начинается с первичного потока. Во время своего исполнения процесс может создавать другие потоки. Исполнение процесса заканчивается при завершении работы всех его потоков. Каждый процесс в операционной системе Windows владеет следующими ресурсами:
□ виртуальным адресным пространством;
□ рабочим множеством страниц в реальной памяти;
□ маркером доступа, содержащим информацию для системы безопасности;
□ таблицей для хранения дескрипторов объектов ядра.
Кроме дескриптора, каждый процесс в Windows имеет свой идентификатор, который является уникальным для процессов, выполняющихся в системе. Идентификаторы процессов используются, главным образом, служебными программами, которые позволяют пользователям системы отслеживать работу процессов.
Создание процессов
Новый процесс в Windows создается вызовом функции createProcess, которая имеет следующий прототип:
BOOL CreateProcess(
LPCTSTR lpApplicationName, // имя исполняемого модуля
LPTSTR lpCoramandLine, // командная строка
LPSECURITY_ATTRIBUTES lpProcessAttributes, // защита процесса
LPSECURITY_ATTRIBUTES lpThreadAttributes, // защита потока
BOOL blnheritHandles, // признак наследования дескриптора
DWORD dwCreationFlags, // флаги создания процесса
LPVOID IpEnvironment, // блок новой среды окружения
LPCTSTR lpCurrentDirectory, // текущий каталог
LPSTARTUPINFO IpStartUpInfo, // вид главного окна
LPPROCESS_INFORMATION lpProcessInformation // информация о процессе
);
Функция createProcess возвращает ненулевое значение, если процесс был создан успешно. В противном случае эта функция возвращает значение false. Процесс, который создает новый процесс, называется родительским процессом (parent process) по отношению к создаваемому процессу. Новый же процесс, который создается другим процессом, называется дочерним процессом (child process) по отношению к процессу-родителю.
Завершение процессов
Процесс может завершить свою работу вызовом функции ExitProcess, которая имеет следующий прототип:
VOID ExitProcess(
UINT uExitCode // код возврата из процесса
);
При вызове функции ExitProcess завершаются все потоки процесса с кодом возврата, который является параметром этой функции. При выполнении этой функции система посылает динамическим библиотекам, которые загружены процессом, сообщение dll_process_detach, которое говорит о том, что динамическую библиотеку необходимо отсоединить от процесса.
Лекция 10. Обмен данными между параллельными процессами