Escolar Documentos
Profissional Documentos
Cultura Documentos
1. Цель работы
2. Лабораторное задание
3. Краткие сведения из теории
v Программирование для Windows
Ø Процессы
§ Функция CreateProcess
· Параметры IpszApplicationName и IpszCommandLine
· Параметры lpsaProcess, lpsaThread и finheritHandles
· Параметр fdwCreate
· Параметр lpvEnvironment
· Параметр lpszCurDir
· Параметр lpsiStartInfo
· Параметр lppiProcInfo
§ Завершение процесса
· Функция ExitProcess
· Функция TerminateProcess
· Если все потоки процесса «уходят»
· Что происходит при завершении процесса
§ Дочерние процессы
· Запуск обособленных дочерних процессов
Ø Потоки
· В каких случаях потоки создаются
· В каких случаях потоки не создаются
· Ваша первая функция потока
· Стек потока
· Структура CONTEXT
§ Функция CreateThread
· Параметр Ipsa
-1-
· Параметр cbStack
· Параметры IpStartAddr и IpvThreadParm
· Параметр fdwCreate
· Параметр iplDThread
§ Завершение потока
· Функция ExitThread
· Функция TerminateThread
· Если завершается процесс
· Что происходит при завершении потока
§ Как узнать о себе
§ Распределение процессорного времени между потоками
· Присвоение уровней приоритета в Win32
· Классы приоритета процессов
· Изменение класса приоритета процесса
· Установка относительного приоритета потока
· Динамическое изменение уровней приоритетов потоков
· Задержка и возобновление потоков
v Программирование для Unix
Ø Программы, процессы и потоки
Ø Процессы
§ Системный вызов exec
· Системный вызов execl
· Другие пять вызовов семейства exec
¨ execv
¨ exeсlp
¨ execvp
¨ execle
¨ execve
§ Системный вызов fork
-2-
§ Завершение процесса и системные вызовы exit
· _exit
· _Exit
· exit
§ Системные вызовы wait, waitpid и waitid
· Системный вызов waitpid
· Системный вызов wait
· Системный вызов waitid
§ Получение идентификатора процесса
· getpid
· getppid
§ Получение и изменение приоритета
Ø Потоки
§ Создание потока
§ Ожидание завершения потока
§ Принудительное завершение потока
· Системный вызов pthread_cancel
· Системный вызов pthread_testcancel
4. Дополнительная информация
v Рекомендуемая литература
v Рекомендуемые Интернет ресурсы
Ø Литература
Ø Полезные ссылки
Ø Программное обеспечение
-3-
1. ЦЕЛЬ РАБОТЫ
· создание процессов, потоков;
· ожидание их завершения, получение результата работы.
2. ЛАБОРАТОРНОЕ ЗАДАНИЕ
1. Процессы. Процесс А инициализирует массив случайными значениями и
записывает их в файл, а затем запускает процесс Б в командной строке
передается имя файла с данными. После этого ожидает завершения процесса Б
и выводит на экран результат возврата процесс Б. Процесс Б открывает файл,
переданный ему в командной строке, находит в нем максимальный элемент, и
возвращает его в качестве результата.
2. Потоки. Поток А инициализирует массив случайными значениями, а затем
поток Б. После этого ожидает завершения потока Б и выводит на экран
результат возврата потока Б. Потока Б находит в массиве максимальный
элемент, и возвращает его в качестве результата.
-4-
3. КРАТКИЕ СВЕДЕНИЯ ИЗ ТЕОРИИ
ПРОГРАММИРОВАНИЕ ДЛЯ WINDOWS
· ПРОЦЕССЫ
Процесс обычно определяют как экземпляр выполняемой программы. В
Win32 процессу отводится 4 Гб адресного пространства. Win32-процессы — в
отличие от своих аналогов в MS-DOS и 16-разрядной Windows — инертны.
Иными словами, Win32-процесс ничего не исполняет — просто владеет
четырехгигабайтовым адресным пространством, содержащим код и данные
ЕХЕ-файла приложения. В это же пространство загружаются код и данные
DLL-библиотек, если того требует ЕХЕ-файл. Кроме адресного пространства,
процессу принадлежат такие ресурсы, как файлы, динамически выделяемые
области памяти и потоки. Ресурсы, создаваемые при жизни процесса,
обязательно уничтожаются при его завершении.
Как было сказано, процессы инертны. Чтобы процесс что-нибудь
выполнил, в нем нужно создать поток. Именно потоки отвечают за
исполнение кода, содержащегося в адресном пространстве процесса. В
принципе, один процесс может владеть несколькими потоками, и тогда они
«одновременно» исполняют код в адресном пространстве процесса. Для этого
каждый поток должен располагать собственным набором регистров
процессора и собственным стеком, а каждый процесс — минимум одним
потоком. Если бы у процесса не было ни одного потока, ему нечего было бы
делать «на этом свете», и система автоматически уничтожила бы его вместе с
выделенным ему адресным пространством.
Чтобы все эти потоки работали, операционная система отводит каждому
из них определенное процессорное время. Выделяя потокам отрезки времени
(называемые квантами) по принципу карусели, она создает тем самым
иллюзию одновременного выполнения потоков (см. рис. 3-1).
-5-
Рис. 3-1. “Операционная система выделяет потокам кванты времени
по принципу карусели”
-6-
Ø Функция CreateProcess
Процесс создается при вызове Вашим приложением функции
CreateProcess:
BOOL CreateProcess(
LPCTSTR lpszApplicationName,
LPCTSTR lpszCommandLine,
LPSECURITY_ATTRIBUTES lpsaProcess,
LPSECURITY_ATTRIBUTES lpsaThread,
BOOL fInheritHandles,
DWORD fdwCreate,
LPVOID lpvEnvironment,
LPTSTR IpszCurDir,
LPSTARTUPINFO IpsiStartInfo,
LPPROCESS.INFORMATION lppiProcInfo);
-7-
сольному типу). Если системе удастся создать новый процесс и его первичный
поток, CreateProcess вернет TRUE.
-8-
исполняемый файл, она создает новый процесс и проецирует код и данные
исполняемого файла на адресное пространство этого процесса. Затем
обращается к процедурам стартового кода из стандартной библиотеки С. Тот в
свою очередь, как уже говорилось, анализирует командную строку процесса и
передает WinMain адрес первого (за именем исполняемого файла) аргумента
как IpszCmdLine.
Все, о чем было сказано выше, произойдет, только если параметр
IpszApplicationName — NULL (что и бывает в 99% случаев). Вместо NULL
можно передать адрес строки с именем исполняемого файла, который надо
запустить. Однако тогда придется указать не только его имя, но и расширение,
поскольку в этом случае имя не дополняется расширением ЕХЕ автома-
тически. CreateProcess предполагает, что файл находится в текущем каталоге
(если полный путь не задан). Если в текущем каталоге файла нет, функция не
станет искать его в других каталогах, и на этом все закончится.
Но даже при указанном в IpszApplicationName имени файла CreateProcess
все равно передает новому процессу содержимое параметра IpszCommandLine
как командную строку. Допустим, Вы вызвали CreateProcess так:
CreateProcess(
"С:\\WINNT\\SYSTEM32\\NOTEPAD.EXE",
"WORDPAD README.TXT");
-9-
Параметры lpsaProcess, lpsaThread и finheritHandles
Чтобы создать новый процесс, система должна сначала создать объекты
ядра «процесс» и «поток» (для первичного потока процесса). Поскольку это
объекты ядра, родительский процесс получает возможность связать с ними
атрибуты защиты. Параметры lpsaProcess и lpsaThread позволяют определить
нужные атрибуты защиты для объектов «процесс» и «поток» соответственно.
В эти параметры можно занести NULL, и система закрепит за данными
объектами дескрипторы защиты по умолчанию. В качестве альтернативы
можно объявить и инициализировать две структуры
SECURITY_ATTRIBUTES; тем самым Вы создадите и присвоите объектам
«процесс» и «поток» их собственные атрибуты защиты.
Структуры SECURITY_ATTRIBUTES для параметров lpsaProcess и
lpsaThread используются и тогда, когда нужно, чтобы какой-либо из этих двух
объектов получил статус наследуемого любым дочерним процессом.
На рис. 3-2 Вы найдете короткую программу, демонстрирующую, как
наследуются описатели объектов ядра. Будем считать, что процесс А
порождает процесс В и заносит в параметр lpsaProcess адрес структуры
SECURITY_ATTRIBUTES, в которой элемент binheritHandle установлен как
TRUE. Одновременно параметр lpsaThread указывает на другую структуру
SECURITY_ATTRIBUTES, в которой значение элемента binheritHandle −
FALSE.
Создавая процесс В, система формирует объекты ядра «процесс» и
«поток», а затем — в структуре, на которую указывает параметр lppiProcInfo,
— возвращает их описатели процессу А, и с этого момента тот может
манипулировать только что созданными объектами «процесс» и «поток».
Теперь предположим, что процесс А собирается вторично вызвать
CreateProcess чтобы породить процесс С. Сначала ему нужно определить,
стоит ли предоставлять процессу С доступ к своим объектам ядра. Для этого
используется параметр finheritHandles. Если он приравнен TRUE, система
передает процессу С все наследуемые описатели. В этом случаи наследуется и
- 10 -
описатель объекта ядра «процесс» процесса В. А вот описатель объекта «пер-
вичный поток» процесса В не наследуется ни при каком значении
finheritHandles. Кроме того, если процесс A вызывает CreateProcess, передавая
через параметр finheritHandles значение FALSE, процесс С не наследует
никаких описателей, используемых в данный момент процессом А.
- 11 -
// порождаем процесс В
CreateProcess(NULL, "ProcessB", &saProcess, &saThread, FALSE, 0, NULL,
NULL, &si, &piProcessB);
return(0);
}
Параметр fdwCreate
Параметр fdwCreate определяет флаги, влияющие на то, как именно
создается новый процесс. Несколько флагов комбинируются булевым
оператором OR.
Флаг DEBUG_PROCESS позволяет родительскому процессу проводить
отладку дочернего, а также всех процессов, которые последним могут быть
порождены. Если этот флаг установлен, система уведомляет родительский
- 12 -
процесс (он теперь получает статус отладчика) о возникновении
определенных событий в любом из дочерних процессов (а они получают
статус отлаживаемых).
Флаг DEBUG_ONLY_THIS_PROCESS аналогичен флагу
DEBUG_PROCESS с тем исключением, что заставляет систему уведомлять
родительский процесс о возникновении специфических событий только в
одном дочернем процессе — его прямом потомке. И тогда, если дочерний
процесс создаст ряд дополнительных, отладчик уже не уведомляется о
событиях, «происходящих» в них.
Флаг CREATE_SUSPENDED позволяет создать процесс и в то же время
приостановить его первичный поток. Этим флагом обычно пользуются
отладчики. Получив команду загрузить отлаживаемую программу, отладчик
должен сообщить системе, чтобы та инициализировала новый процесс и его
первичный поток, но исполнение этого потока пока задержала. Благодаря
этому флагу пользователь, проводя отладку приложения, может, расставив по
всей программе точки прерывания, разрешить перехват определенных
событий, а затем дать отладчику команду приступить к исполнению
первичного потока.
Флаг DETACHED_PROCESS блокирует доступ процессу,
активизированному консольной программой, к созданному родительским
процессом окну и сообщает, что вывод следует перенаправить в новое окно.
Если процесс этого типа создается другим процессом, то — по умолчанию —
новый будет использовать окно родительского процесса. Так вот, этот флаг
заставляет новый процесс перенаправлять свой вывоз в новое консольное
окно.
Флаг CREATE_NEW_CONSOLE приводит к созданию нового
консольного окна для нового процесса. Имейте в виду: одновременная
установка флагов CREATE_NEW_CONSOLE и DETACHED_PROCESS
недопустима.
- 13 -
Флаг CREATE_NO_WINDOW не дает создавать никаких консольных
окон для данного приложения и тем самым позволяет исполнять консольные
программы без пользовательского интерфейса.
Флаг CREATE_NEW_PROCESS_GROUP служит для модификации
списка процессов, уведомляемых о нажатии клавиш Ctrl+C и Ctrl+Break. Если
в системе одновременно исполняется несколько процессов консольного типа,
то при нажатии одной из упомянутых комбинаций клавиш система уведомляет
об этом только процессы, включенные в группу. Указав этот флаг при
создании нового процесса консольного типа. Вы создаете и новую группу.
Таким образом, на нажатие клавиш Ctrl+C и Ctrl+Break реагировать будут
лишь этот процесс и процессы, им порожденные.
Флаг CREATE_DEFAULT_ERROR_MODE сообщает системе, что новый
процесс не должен наследовать режимы обработки ошибок, установленные в
родительском.
Флаг CREATE_SEPARATE_WOW_VDM полезен только при запуске 16-
разрядного Windows-приложения в Windows NT. Если он установлен, система
создает отдельную виртуальную DOS-машину (Virtual DOS machine, VDM) и
запускает 16-разрядное Windows-приложение именно в ней. (По умолчанию
все 16-разрядные Windows-приложения выполняются в одной, общей VDM.)
Выполнение приложения в отдельной VDM дает большое преимущество:
«рухнув», приложение уничтожит лишь эту VDM, а программы, выполняемые
в других VDM, продолжат нормальную работу. Кроме того, 16-разрядные
Windows-приложения, выполняемые в раздельных VDM, имеют и раздельные
очереди ввода. Это значит, что, если одно приложение вдруг «зависнет»»
приложения в других VDM продолжат прием ввода. Единственный недостаток
работы с несколькими VDM в том, что каждая из них требует значительных
объемов физической памяти. Windows 95 выполняет все 16-разрядные
Windows-приложения только в одной VDM, и изменить тут ничего нельзя.
Флаг CREATE_SHARED_WOW_VDM полезен только при запуске 16-
разрядного Windows-приложения в Windows NT. По умолчанию все 16-
- 14 -
разрядные Windows-приложены выполняются в одной VDM, если только не
указан флаг CREATE_SEPARATE_WOW_VDM. Однако стандартное
поведение Windows N'T можно изменить, если присвоить значение «уes»
параметру DefaultSeparateVDM в разделе
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\WOW. (После
модификации этого параметра Windows NT нужно перезагрузить.) Установив
значение «yes», но указав флаг CREATE_SHARED_WOW_VDM, Вы вновь
заставите Windows NT выполнять все 16-разрядные Windows-приложения в
одной VDM.
Флаг CREATE_UNICODE_ENVIRONMENT сообщает системе, что блок
переменных окружения дочернего процесса должен содержать Unicode-
символы. По умолчанию блок формируется на основе ANSI-сим волов.
При создании процесса можно задать и класс его приоритета. Однако это
необязательно и даже, как правило, не рекомендуется; система присваивает
новому процессу класс приоритета по умолчанию. Вот какие классы
приоритета существуют:
Параметр lpvEnvironment
Этот параметр указывает на блок памяти, хранящий строки переменных
окружения, которыми будет пользоваться новый процесс. Обычно вместо
lpvEnvironment передается NULL, в результате чего дочерний процесс
- 15 -
наследует строки переменных окружения от родительского процесса. В
качестве альтернативы можно вызвать функцию GetEnvironmentStrings:
LPVOID GetEnvironmentStrings(VOID);
Параметр lpszCurDir
Он позволяет родительскому процессу установить текущие диск и
каталог для дочернего процесса. Если его значение — NULL, рабочий каталог
нового процесса будет расположен там же, где и у приложения, его
породившего. А если он отличен от NULL, то должен указывать на строку (с
нулевым символом в конце), содержащую нужный диск и каталог. В путь надо
включать и букву диска.
Параметр lpsiStartInfo
Этот параметр указывает на структуру STARTUPINFO:
typedef struct _STARTUPINFO {
DWORD cb;
LPSTR lpReserved;
LPSTR lpDesktop;
LPSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
- 16 -
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2:
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError:
} STARTUPINFO, *LPSTARTUPINFO;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
CreateProcess(..., &si, . . . ) ;
- 17 -
Рис. 3-3. “Элементы структуры STARTUPINFO”
Окно или
Элемент Описание
консоль
Содержит количество байтов, занимаемых структурой
STARTUPINFO. Служит для контроля версии — на тот
cb то и другое случай, если Microsoft расширит эту структуру в будущем
Win32. Программа должна инициализировать cb как
sizeof(STARTUPINFO).
IpReserved то и другое Зарезервирован. Инициализируйте как NULL.
Идентифицирует имя рабочего стола, на котором
запускается приложение. Если указанный рабочий стол
существует, новый процесс сразу же связывается с ним. В
ином случае система сначала создаст рабочий стол с
IpDesktop то и другое атрибутами по умолчанию, присваивает ему имя,
указанное в данном элементе структуры, и связывает его
с новым процессом. Если IpDesktop равен NULL (что
чаще всего и бывает), процесс связывается с текущим
рабочим столом.
Определяет заголовок консольного окна. Если IpTitle —
IpTitle консоль
NULL, в заголовок выводится имя исполняемого файла.
Указывают х- и у-координаты (в пикселях) области
экрана, в которой размещается окно приложения. Эти
координаты используются, только если дочерний процесс
dwX создает свое первое перекрываемое окно с
то и другое
dwY идентификатором CW_USEDEFAULT в параметре х
функции CreateWindow. В приложениях, создающих
консольные окна, данные элементы определяют верхний
левый угол консольного окна.
Определяют ширину и высоту (в пикселах) окна
приложения. Эти значения используются, только если
dwXSize дочерний процесс создает свое первое перекрываемое
то и другое
dwYSize окно с идентификатором CW_USEDEFAULT в параметре
nWidth функции CreateWindow. В приложениях,
создающих консольные окна, данные элементы
- 18 -
определяют ширину и высоту консольного окна.
dwXCountChars консоль Определяют ширину и высоту (в символах) консольных
dwYCountChars окон дочернего процесса.
Задает цвет текста и фона в консольных окнах дочернего
dwFillAttribute консоль
процесса.
- 19 -
Флаг Описание
STARTF_USESIZE Заставляет использовать элементы dwXSize и dwYSize.
STARTF_USESHOWWINDOW Заставляет использовать элемент wShowWindow.
STARTF_USEPOSITION Заставляет использовать элементы dwX и dwY.
STARTF_USECOUNTCHARS Заставляет использовать элементы dwXCountChars и
dwYCountChars.
STARTF_USEFILLATTRIBUTE Заставляет использовать элемент dwFillAttribute.
STARTF_USESTDHANDLE Заставляет использовать элементы hStdInput,
hStdOutput и hStdError.
- 20 -
спустя 2 секунды от нового процесса не поступает GUI-вызова, она восстанав-
ливает исходную форму курсора.
Если же в течение 2 секунд процесс все-таки делает GUI-вызов,
CreateProcess ждет, когда приложение откроет свое окно. Это должно
произойти в течение 5 секунд после GUI-вызова. Если окно не появилось,
CreateProcess восстанавливает курсор, а появилось — сохраняет его в виде
«песочных часов» еще на 5 секунд. Как только приложение вызовет функцию
GetMessage, сообщая тем самым, что оно закончило инициализацию,
CreateProcess немедленно изменит курсор на стандартный и прекратит
мониторинг нового процесса.
И последний флаг — STARTF_SCREENSAVER — подсказывает системе,
что данное приложение — экранная заставка; это заставляет ее
инициализировать программу весьма своеобразно. Когда процесс начнет
исполняться, система разрешит его инициализацию с классом приоритета,
указанным при вызове CreateProcess. А когда процесс обратится к GetMessage
или PeekMessage, система автоматически сменит класс его приоритета на
«простаивающий» (idle).
Если программа — экранная заставка активна и пользователь нажимает
клавишу или двигает мышь, система автоматически возвращает этой
программе исходный класс приоритета (указанный в свое время при вызове
CreateProcess).
Для запуска экранной заставки функцию CreateProcess следует вызывать
с флагом NORMAL_PRIORITY_CLASS, что дает такой эффект:
1. Программа — экранная заставка будет инициализирована перед
тем, как «впадет в спячку». Если бы она выполнялась в таком
состоянии все свое время, ее вытеснили бы процессы с
приоритетами normal и realtime, и заставка никогда не получила бы
шанса на инициализацию.
2. Выполнение программы — экранной заставки обычно завершается,
когда пользователь начинает работать с каким-нибудь
- 21 -
приложением. Ведь последнее, скорее всего, имеет нормальный
приоритет, а это могло бы привести к повторному вытеснению
потоков в программе — экранной заставке, и в результате ее не
удалось бы завершить.
В заключение раздела — несколько слов об элементе wShowWindow
структуры STARTUPINFO. Этот элемент инициализируется значением,
которое Вы передаете в WinMain через ее последний параметр — nCmdShow.
Он позволяет указать, в каком виде должно появиться основное окно Вашего
приложения. В качестве значения используется один из идентификаторов,
обычно передаваемых в ShowWindow (чаще всего SW_SHOWNORMAL или
SW_SHOWMINNOACTIVE, но иногда и SW_SHOWDEFAULT).
После запуска программы двойным щелчком из Explorer ее функция
WinMain вызывается с SWSHOWNORMAL в параметре nCmdShow. Если же
программа запускается двойным щелчком при нажатой клавише Shift, в этом
параметре передается идентификатор SW_SHOWMINNOACTIVE. Благодаря
этому пользователь может легко выбрать, в каком окне запустить программу
— нормальном или свернутом.
Наконец, чтобы получить копию структуры STARTUPINFO,
инициализированной родительским процессом, приложение может вызвать:
STARTUPINFO si;
si.cb = sizeof(si);
GetStartupInfo(&si);
- 22 -
Параметр lppiProcInfo
Параметр ippiProcInfo указывает на структуру
PROCESS_INFORMATION, которую Вы должны предварительно создать; ее
элементы инициализируются самой функцией CreateProcess. Структура
представляет собой следующее:
- 23 -
Созданному процессу присваивается уникальный идентификатор; ни у
каких процессов, выполняемых в системе, не может быть одинаковых
идентификаторов. То же касается и потоков. Завершая свою работу,
CreateProcess заносит значения идентификаторов в элементы dwProcessId и
dwThreadId структуры PROCESS_INFORMATION. Используя их, роди-
тельский процесс может обращаться к дочернему.
Подчеркнем еще один чрезвычайно важный момент: система способна
повторно использовать идентификаторы процессов и потоков. Например, при
создании процесса система формирует объект «процесс», присваивая ему
идентификатор со значением, допустим, 0x22222222. Создавая новый объект
«процесс», система уже не присвоит ему данный идентификатор. Но после
выгрузки из памяти первого объекта следующему создаваемому объекту
«процесс» может быть присвоен тот же идентификатор — 0x22222222.
Эту особенность нужно учитывать при написании кода, избегая ссылок на
неверный объект «процесс» (или «поток»). Действительно, затребовать и
сохранить идентификатор процесса несложно, но задумайтесь, что получится,
если в следующий момент этот процесс будет завершен, а новый получит тот
же идентификатор: сохраненный ранее идентификатор уже связан совсем с
другим процессом.
Чтобы избавиться от подобных неприятностей, доступ к объекту
«процесс» нужно блокировать. Иначе говоря, Вы должны обязательно
увеличивать значение счетчика, связанного с объектом «процесс», — ведь
система никогда не выгрузит из памяти объект, счетчик которого отличен от
нуля. Впрочем, в большинстве случаев счетчик увеличивается и без Вашего
участия, как, например, после вызова CreateProcess.
Зная, что значение счетчика выше нуля, можно свободно оперировать
идентификатором процесса. А когда необходимость в нем отпадет, вызовите
CloseHandle — чтобы уменьшить счетчик объекта «процесс». Затем
удостоверьтесь, что этот идентификатор больше нигде не используется.
- 24 -
Ø Завершение процесса
Процесс можно завершить тремя способами:
1. Один из потоков процесса вызывает функцию ExitProcess (самый
распространенный способ).
2. Поток другого процесса вызывает функцию TerminateProcess (этого
надо избегать).
3. Все потоки процесса «умирают» по своей «воле» (большая
редкость).
Функция ExitProcess
Процесс завершается, когда один из его потоков вызывает ExitProcess:
- 25 -
вызвать ExitProcess или просто вернуть управление), Вы завершите
первичный поток, но не сам процесс — если в нем еще выполняется какой-то
другой поток (или потоки).
Функция TerminateProcess
Вызов TerminateProcess тоже завершает процесс:
- 26 -
Если все потоки процесса «уходят»
В такой ситуации (а она может возникнуть, если все потоки вызвали
ExitThread или их закрыли вызовом TerminateThread) операционная система
больше не считает нужным «содержать» адресное пространство данного
процесса. Обнаружив, что в процессе не исполняется ни один поток, она
немедленно завершает его. При этом код завершения процесса приравнивается
коду завершения последнего потока.
- 27 -
Описатели завершенного процессса уже мало на что пригодны. Разве что
родительский процесс, вызвав GetExitCodeProcess, может проверить, завершен
ли процесс, идентифицируемый параметром hProcess, и, если да, определить
код завершения:
- 28 -
Ø Дочерние процессы
При разработке приложения часто бывает нужно, чтобы какую-то
операцию выполнял внешний блок кода. Поэтому — хочешь, не хочешь —
приходится постоянно вызывать функции или подпрограммы. Но вызов
функции приводит к приостановке выполнения основного кода Вашей
программы до возврата из вызванной функции.
Альтернативный способ — передать выполнение какой-то операции
другому потоку в пределах данного процесса (поток, разумеется, нужно
сначала создать). Это позволит основному коду программы продолжить
работу, в то время как дополнительный поток будет выполнять другую
операцию. Прием весьма удобный, но, когда основному потоку потребуется
узнать результаты работы другого потока, Вам не избежать проблем,
связанных с синхронизацией.
Есть еще один прием: Ваш процесс порождает дочерний и возлагает на
него выполнение части операций. Будем считать, что эти операции очень
сложны. Допустим, для их реализации Вы решили просто создать новый поток
внутри того же процесса. Вы написали тот или иной код, проверили его и
получили некорректный результат — может, ошиблись в алгоритме или
запутались в ссылках и случайно перезаписали какие-нибудь важные данные в
адресном пространстве своего процесса. Так вот, один из способов защитить
адресное пространство основного процесса от подобных ошибок как раз и
состоит в том, чтобы передать часть работы отдельному процессу. Далее
можно или подождать, пока он завершится, или продолжить работу
параллельно с ним.
К сожалению, дочернему процессу, по-видимому, придется оперировать с
данными, содержащимися в адресном пространстве родительского процесса.
Было бы неплохо, чтобы он работал в собственном адресном пространстве, а в
«Вашем» — просто считывал нужные ему данные; тогда он не сможет что-то
испортить в адресном пространстве родительского процесса. В Win32
предусмотрено несколько способов обмена данными между процессами: DDE
- 29 -
(Dynamic Data Exchange — динамический обмен данными), OLE (Object
Linking and Embedding — связывание и внедрение объектов), каналы (pipes),
почтовые ящики (mailslots) и т. д. А один из самых удобных способов,
обеспечивающих совместный доступ к данным, — использование файлов,
проецируемых в память (memory-mapped files).
Если Вы хотите создать новый процесс, заставить его выполнить какие-
либо операции и дождаться их результатов, напишите примерно такой код:
PROCESS_INFORMATION pi;
DWORD dwExitCode;
WaitForSingleObject(pi.hProcess, INFINITE);
// процесс завершился
GetExitCodeProcess(pi.hProcess, &dwExitCode);
- 30 -
вызов WaitForSingleObject приостанавливает выполнение потока
родительского процесса до завершения порожденного им процесса. Когда
WaitForSingleObject вернет управление, Вы узнаете код завершения дочернего
процесса через функцию GetExitCodeProcess.
Обращение к CloseHandle в приведенном выше фрагменте кода
заставляет систему уменьшить значения счетчиков объектов «поток» и
«процесс» до нуля и тем самым освободить память, занимаемую этими
объектами.
Вы, наверное, заметили, что в этом фрагменте я закрыл описатель объекта
ядра «первичный поток» (принадлежащий дочернему процессу) сразу после
возврата из CreateProcess. Это не приводит к завершению первичного потока
дочернего процесса — просто уменьшает счетчик, связанный с упомянутым
объектом. А вот почему это делается — и, кстати, даже рекомендуется делать
— именно так, станет ясно из простого примера. Допустим, первичный поток
дочернего процесса порождает еще один поток, а сам после этого завершается.
В этот момент система может высвободить объект «первичный поток»
дочернего процесса из памяти, если у родительского процесса нет описателя
данного объекта. Но если родительский процесс располагает таким
описателем, система не сможет удалить этот объект из памяти до тех пор, пока
и родительский процесс не закроет его описатель.
- 31 -
связанные с новым процессом и его первичным потоком. Приведенный ниже
фрагмент кода подскажет Вам, как, создав процесс, сделать его
обособленным:
PROCESS_INFORMATION pi;
BOOL fSuccess = CreateProcess(.... &pi):
if (fSuccess) {
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
- 32 -
· ПОТОКИ
В каких случаях потоки создаются
Поток (thread) определяет последовательность исполнения кода в
процессе. При инициализации процесса система всегда создает первичный
поток. Начинаясь со стартового кода из стандартной библиотеки С (который в
свою очередь вызывает функцию WinMain из Вашей программы), он «живет»
до того момента, когда WinMain возвращает управление стартовому коду и тот
вызывает функцию ExitProcess. Большинство приложений обходится един-
ственным, первичным потоком. Однако процессы могут создавать
дополнительные потоки, что позволяет добиваться минимального простоя
процессора и работать эффективнее.
Например, в электронных таблицах нужно пересчитывать данные при
изменении пользователем содержимого ячеек. Пересчет сложной таблицы
может занять несколько секунд, но тщательно продуманное приложение не
должно тратить время на эту операцию после каждого изменения. Вместо
этого следует выделить функциональный блок повторных расчетов в
отдельный поток с более низким (чем у первичного) приоритетом. Таким
образом, пока пользователь набирает данные, исполняется первичный поток,
т. е. система не выделяет процессорного времени потоку, отвечающему за
пересчет. А возникнет пауза — даже крошечная, — система приостановит
выполнение первичного потока, ожидающего ввода данных, и отдаст
процессорное время другому потоку (в нашем случае — блоку повторных
расчетов). При возобновлении ввода данных первичный поток, имеющий
более высокий приоритет, вытеснит поток, занимающийся пересчетом.
Создание дополнительного потока делает программу «отзывчивой» на
действия пользователя — да и реализация такого алгоритма довольно проста.
С той же целью можно создать дополнительный поток и в текстовом
процессоре для фоновой разбивки документа на страницы в паузах между
вводом текста. Например, в 16-разрядной Windows текстовому процессору
Microsoft Word приходится моделировать многопоточность, но в версии для
- 33 -
Win32 он просто порождает вспомогательный поток для разбивки документа
на страницы. Первичный поток отвечает за обработку информации, вводимой
пользователем, а фоновый — за определение концов страниц.
Полезно создать отдельный поток и для обработки заданий на печать.
Тогда пользователь, отправив документ на печать, продолжил бы работу с
программой.
Еще пример. При выполнении длительной операции программы обычно
открывают диалоговое окно, позволяющее эту операцию отменять. Скажем,
при копировании файлов Explorer выводит на экран диалоговое окно, где,
кроме индикатора прогресса операции, содержится и кнопка Cancel (Отмена).
Щелкнув ее, Вы отмените копирование файла (или файлов).
В 16-разрядной Windows для этого приходилось из цикла File Copy
периодически вызывать функцию PeekMessage, но делать это можно было
только в паузах между чтением и записью. При считывании большого блока
данных реакция программы на «нажатие» кнопки Cancel была слишком
запоздалой: если файл считывался с дискеты, могло пройти несколько секунд.
Из-за такого запаздывания пользователь, полагая, что система почему-то не
«поняла» его, мог несколько раз щелкнуть.
Теперь представьте, что код, отвечающий за копирование файлов,
выделен в свой поток. Вам больше не надо расставлять по всему коду вызовы
PeekMessage — поток, обеспечивающий работу пользовательского
интерфейса, действует независимо. А значит, щелчок кнопки Cancel даст
немедленный результат.
На основе принципа многопоточности можно также создавать
программы, моделирующие события реальной жизни. Рассмотрим модель
супермаркета. Каждый покупатель представлен в ней отдельным потоком, так
что теоретически все они независимы друг от друга и входят, покупают и
выходят тогда и как им угодно.
Хотя подобную модель, в общем-то, можно реализовать подобным
образом, здесь нас подстерегает ряд потенциальных проблем. Во-первых, в
- 34 -
идеале надо бы выполнять каждый поток (соответствующий одному
покупателю) на отдельном процессоре. Но сами понимаете, это совершенно
нереально, поэтому при моделировании нужно учитывать время, затрачива-
емое системой на вытеснение первого потока и активизацию второго.
Например, если в модели всего 2 потока, а у компьютера 8 процессоров,
система сможет закрепить каждый поток за отдельным процессором. Если же
в модели 1000 потоков, системе придется постоянно «коммутировать» их
между 8 процессорами. Так что при распределении большого количества
потоков между несколькими процессорами станет заметным время, требуемое
на переключение потоков. Если моделируется продолжительный процесс, этот
эффект проявляется относительно слабо. Но при моделировании
быстротечных процессов перераспределение потоков может занять едва ли не
львиную долю времени выполнения всей программы.
Во-вторых, операционной системе самой нужны потоки, исполняемые
«вместе» с потоками, принадлежащими программам. Значит, надо учитывать и
время, затрачиваемое на переключение этих дополнительных потоков; оно
почти наверняка повлияет на общие результаты.
И, в-третьих, моделировать имеет смысл, только если Вы периодически
фиксируете какие-то параметры процесса в его развитии. Скажем, в модели
супермаркета каждый покупатель, входя в магазин, регистрируется в списке, а
внесение элемента в список тоже занимает время (отнимая его у собственно
моделируемого процесса). Вспомните, что, согласно принципу
неопределенности Гейзенберга, чем точнее определяется один квант, тем
больше ошибка при измерении другого. Это в полной мере относится и к
нашим рассуждениям.
- 35 -
программы!» И по какой-то, непонятной мне причине они начинают дробить
свои программы на куски, которые можно было бы исполнять как отдельные
потоки. Но...
Потоки — вещь невероятно полезная, когда ими пользуются с умом. Увы,
решая старые проблемы, можно создать себе новые. Допустим, Вы
разрабатываете текстовый процессор и хотите выделить функциональный
блок, отвечающий за распечатку, в отдельный поток. Идея вроде неплоха:
пользователь, отправив документ на распечатку, может сразу вернуться к
редактированию. Но задумайтесь вот над чем: значит, информация в
документе может быть изменена при распечатке документа? Как видите,
теперь перед Вами совершенно новая проблема, с которой прежде
сталкиваться не приходилось. Тут-то и подумаешь, а стоит ли выделять печать
в отдельный поток, зачем искать лишних приключений? А что если при
распечатке разрешить редактирование любых документов, кроме того, что в
данный момент печатается, — иными словами, запретим изменение
печатаемого документа. Или так: скопируем документ во временный файл и
отправим на печать именно его, а пользователь пусть редактирует оригинал в
свое удовольствие. Когда распечатка временного файла закончится, мы его
удалим — вот и все.
Еще одно «узкое» место, где неправильное применение потоков может
привести к появлению проблем, — разработка пользовательского интерфейса
в приложении. В большинстве программ все компоненты пользовательского
интерфейса (окна) формируются в одном потоке. Например, если Вы создаете
диалоговое окно, какой смысл формировать список одним потоком, а кнопку
— другим?
Рассмотрим эту проблему подробнее и предположим, что в программе
имеется элемент управления — список, сортирующий данные всякий раз, как
в него что-то добавляют (или удаляют). Сортировка может занять несколько
секунд, и поэтому Вы, допустим, выделили этот элемент управления в
отдельный поток. Тогда пользователь вроде бы может работать с другими
- 36 -
элементами управления, пока поток, принадлежащий списку, занят
сортировкой.
Но эта идея — не из лучших. Во-первых, каждый поток, создающий то
или иное окно, должен содержать в себе цикл GetMessage. Во-вторых, если
поток, принадлежащий списку, будет содержать этот цикл, Вы скорее всего
столкнетесь с проблемами синхронизации потоков. Их в принципе можно
решить, закрепив за списком специальный поток, который только и делает,
что сортирует элементы в фоновом режиме.
Закрепление объектов пользовательского интерфейса за отдельными
потоками редко приносит хоть какую-то пользу. Каждый процесс в системе
управляет своим интерфейсом с помощью отдельного потока. Скажем, у
приложения Calculator свой поток, который создает и манипулирует всеми
окнами этой программы, а у приложения Paint — свой, с аналогичными
функциями. Такая схема обладает наибольшей отказоустойчивостью. Если
поток калькулятора войдет в бесконечный цикл, это не скажется на потоке
Paint. Разительное отличие от 16-разрядной Windows, не так ли? В ней, если
виснет одно приложение, виснет вся система. А системы, построенные на
основе Win32, позволяют переключиться из зависшего приложения Calculator
и перейти в тот же Paint.
Вероятно, лучший пример программы, создающей окна во множестве
потоков, — Explorer. Если Вы используете одно окно Explorer и поток для
этого окна входит в бесконечный цикл, оно становится «недееспособным», но
другие окна Explorer остаются «на плаву». И это его свойство очень важно,
потому что пользователи терпеть не могут, когда оболочка операционной
системы перестает реагировать на их команды.
Еще одно применение многопоточности в компонентах GUI —
приложения с многодокументным интерфейсом (multiple document interface,
MD1), где каждое дочернее MDI-окно поддерживается отдельным потоком.
Если один из таких потоков входит в бесконечный цикл или начинает
выполнять длительную операцию, пользователь может переключиться в
- 37 -
другое дочернее MDI-окно И поработать с ним, пока предыдущее выполняет
поставленную задачу. Это настолько удобно, что в Win32 даже есть
специальная функция, дающая результат, аналогичный тому, как если бы Вы
создали дочернее MDI-OKHO, передав сообщение WM_MDICREATE окну
MDIClient:
- 38 -
void StartOfThread(LPTHREAD_START_ROUTINE IpStartAddr,
LPVOID lpvThreadParm)
{
__try
{
DWORD dwThreadExitCode = IpStartAddr(lpvThreadPann);
ExitThread(dwThreadExitCode); )
}
- 39 -
StartOfThread вызовет ExitProcess и завершит весь процесс, а не
только тот поток, в котором произошло исключение.
Исполнение первичного потока процесса на самом деле начинается с
функции StartOfThread. Потом она передает управление стартовому коду из
стандартной библиотеки С, который и вызывает WinMain из Вашего
приложения. Но стартовый код никогда не возвращается в StartOfThread, так
как «под занавес» он явным образом вызывает ExitProcess.
Рассмотрим атрибуты, «присуждаемые» новому потоку.
Стек потока
Каждому потоку выделяется собственный стек в адресном пространстве
процесса. При использовании статических и глобальных переменных не
исключена возможность одновременного обращения к ним из нескольких
потоков, что может повредить значения переменных. С другой стороны,
локальные и автоматические переменные создаются в стеке потока, а значит,
они в гораздо меньшей степени подвержены «вредному влиянию» другого
потока. Поэтому всегда старайтесь при написании функций применять
локальные или автоматические переменные и избегать статических и
глобальных.
Истинный размер стека, принадлежащего потоку, и то, как операционная
система и компилятор управляют стеком, — темы чрезвычайно сложные.
Структура CONTEXT
У каждого потока собственный набор регистров процессора, называемый
контекстом потока. Эта структура с именем CONTEXT отражает состояние
регистров процессора на момент последнего исполнения потока. Она —
единственная структура данных в Win32, зависимая от типа процессора. В
справочном файле по Win32 ее содержимое вообще не показано. Если Вас
интересует, из каких элементов она состоит, загляните в файл WINNT.H —
там Вы найдете несколько ее определений: для х86, для MIPS, для Alpha и для
- 40 -
PowerPC. Компилятор сам выбирает нужный вариант структуры в
зависимости от типа процессора, для которого предназначен Ваш ЕХЕ- или
DLL-модуль.
Когда потоку выделяется процессорное время, система инициализирует
регистры процессора содержимым контекста и, разумеется, регистр —
указатель команд идентифицирует адрес следующей машинной команды,
необходимой для выполнения потока. Кроме того, в контекст включается
указатель стека, который определяет адрес стека, принадлежащего потоку.
- 41 -
Ø Функция CreateThread
Мы уже говорили, как при вызове CreateProcess появляется на свет
первичный поток процесса. Если же Вы хотите, чтобы первичный поток
породил дополнительные потоки, нужно воспользоваться другой функцией —
CreateThread:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES Ipsa; DWORD cbStack;
LPTHREAD_START__ROUTINE IpStartAddr; LPVOID IpvThreadParm;
DWORD fdwCreate; LPDWORD IpIDThread);
- 42 -
6. Инициализирует регистры — указатель стека и указатель команд в
структуре CONTEXT потока, так чтобы первый указывал на
верхнюю границу стека, а второй — на внутреннюю функцию
StartOfThread.
Теперь подробно рассмотрим все ее параметры.
Параметр Ipsa
Параметр Ipsa является указателем на структуру
SECURITY_ATTRIBUTES. Если Вы хотите, чтобы объекту «поток» были
присвоены атрибуты защиты по умолчанию, передайте в этом параметре
NULL. А чтобы дочерние процессы смогли наследовать описатель данного
объекта «поток», определите структуру SECURITY_ATTRIBUTES и
инициализируйте ее элемент blnheritHandle значением TRUE.
Параметр cbStack
Этот параметр определяет, какую часть адресного пространства поток
сможет использовать под свой стек. Каждому потоку выделяется отдельный
стек. CreateProcess, запуская приложение, вызывает функцию CreateThread, и
та инициализирует первичный поток процесса. При этом CreateProcess
заносит в параметр cbStack значение, хранящееся в самом исполняемом файле.
Управлять этим значением позволяет ключ /STACK компоновщика:
/STACK:[reserve] [. commit]
- 43 -
исключение. Перехватив это исключение, система передаст
зарезервированному пространству еще одну страницу (или столько, сколько
указано в аргументе commit). Такой механизм позволяет динамически
увеличивать размер стека лишь по мере необходимости.
Обращаясь к CreateThread, можно обнулить значение параметра cbStack.
В этом случае функция создает стек для нового потока, используя аргумент
commit, внедренный компоновщиком в ЕХЕ-файл. Объем резервируемого
пространства всегда равен 1 Мб. Это ограничение позволяет прекращать
деятельность функций с бесконечной рекурсией.
Допустим, Вы пишете функцию, которая рекурсивно вызывает сама себя.
Предположим также, что в ней «сидит жучок», приводящий к бесконечной
рекурсии. Всякий раз, когда функция вызывает сама себя, в стеке создается
новый стековый фрейм. И если бы система не ограничивала максимальный
размер стека, рекурсивная функция так и вызывала бы сама себя до
бесконечности, а стек поглотил бы все адресное пространство. Задавая же
определенный предел, система, во-первых, предотвращает разрастание стека
до гигантских объемов и, во-вторых, позволяет Вам гораздо быстрее
убедиться в наличии ошибок в своей программе.
- 44 -
DWORD WINAPI ThreadFunc(LPVOID IpvThreadParm)
{
DWORD dwResult = 0;
return(dwResult);
}
Параметр fdwCreate
Этот параметр определяет дополнительные флаги, управляющие
созданием потока. Он принимает одно из двух значений: 0 (исполнение потока
начинается немедленно) или CREATE_SUSPENDED. В последнем случае
система создает поток, затем его стек, инициализирует элементы
соответствующей структуры CONTEXT и, приготовившись к исполнению
первой команды из функции потока, «придерживает» поток до последующих
указаний.
Сразу после возврата из CreateThread и пока еще исполняется вызвавший
ее поток, исполняется и новый поток — если только при его создании Вы не
указали флаг CREATE_SUSPENDED. А поскольку новый поток выполняется
одновременно с потоком, его породившим, вероятно возникновение ряда
проблем. Рассмотрим код:
- 45 -
DWORD WINAPI FirstThread(LPVOID IpvThreadParm)
{
int x = 0;
DWORD dwResult = 0.dwThreadld;
HANDLE hThread;
- 46 -
потока, выполняющих одну и ту же функцию, так как оба потока совместно
использовали бы статическую переменную.
Другое решение проблемы (и его более сложные варианты) состоит в
применении синхронизирующих объектов.
Параметр iplDThread
Последний параметр функции CreateThread — это адрес переменной типа
DWORD, в которой функция вернет идентификатор, приписанный системой
новому потоку.
В Windows 95 передача NULL вместо этого параметра даст ошибку. В
Windows NT до версии 4 этот параметр также не мог быть NULL — иначе
система попыталась бы записать идентификатор потока по адресу 0x00000000,
что привело бы к нарушению доступа. Однако Windows NT 4 разрешает
передавать NULL в параметре IplDThread, если Вас не интересует
идентификатор потока.
- 47 -
Ø Завершение потока
Как и процесс, поток можно завершить тремя способами:
1. Поток самоуничтожается вызовом ExitThread (самый
распространенный способ).
2. Один из потоков данного или стороннего процесса вызывает
TerminateThread (этого надо избегать).
3. Завершается процесс, содержащий данный поток.
Функция ExitThread
Поток завершается, когда вызывается ExitThread:
Функция TerminateThread
Вызов этой функции также завершает поток:
- 48 -
«Гибель» потока при вызове ExitThread приводит к разрушению его стека.
Но если он завершен функцией TerminateThread, система не уничтожает стек,
пока не завершится и процесс, которому принадлежал этот поток. Так сделано
потому, что другие потоки могут использовать указатели, ссылающиеся на
данные в стеке завершенного потока. Если бы они обратились к
несуществующему стеку, произошло бы нарушение доступа.
Когда поток прекращается, система уведомляет об этом все DLL-модули,
подключенные к процессу — владельцу завершенного потока. Но при вызове
TerminateThread такого не происходит, и процесс может быть завершен
некорректно. Например, какой-то DLL-модуль при отключении от потока
должен был бы сбросить все данные в дисковый файл. Не получив
уведомления об отключении — а именно так и будет после вызова Termina-
teThread, — он не выполнит свою задачу.
- 49 -
3. Код завершения потока меняется со STILL_ACTIVE на код,
переданный в функцию ExitThread или TerminateThread.
4. Если данный поток — последний активный поток в процессе,
завершается и сам процесс.
5. Счетчик числа пользователей объекта ядра «поток» уменьшается на
1.
При завершении потока сопоставленный с ним объект ядра «поток» не
освобождается, пока не будут закрыты все внешние ссылки на этот объект.
Когда поток завершился, толку от его описателя другим потокам в
системе в общем немного. Единственное, что они могут сделать, — вызвать
GetExitCodeThread и проверить, завершен ли поток, идентифицируемый
описателем hThread, и, если да, определить его код завершения:
- 50 -
Ø Как узнать о себе
Некоторые Win32-функции требуют в качестве параметра передавать
описатель какого-либо процесса. Поток может получить описатель
своего процесса, вызвав функцию GetCurrentProcess:
HANDLE GetCurrentProcess(VOID);
SetPriontyClass(GetCurrentProcess().HIGH_PRIORITY_CLASS);
DWORD GetCurrentProcessId(VOID);
HANDLE GetCurrentThread(VOID);
- 51 -
Как и GetCurrentProcess, функция GetCurrentThread возвращает
псевдоописатель, имеющий смысл только при использовании его в контексте
текущего потока. Счетчик объекта «поток» при этом не увеличивается, и
вызов CloseHandle с передачей ей этого псевдоописателя ничего не дает.
Поток может запросить свой идентификатор вызовом:
DWORD GetCurrentThreadld(VOID);
- 52 -
Происходит так потому, что псевдоописатель является описателем того
потока, что вызывает эту функцию.
Чтобы исправить приведенный выше фрагмент кода, превратим
псевдоописатель в «настоящий» через функцию DuplicateHandle:
BOOL DuplicateHandle(
HANDLE hSourceProcess,
HANDLE hSource,
HANDLE hTargetProcess,
LPHANDLE lphTarget,
DWORD fdwAccess,
BOOL flnherit,
DWORD fdwOptions);
- 53 -
// DUPLICATE_SAME_ACCESS;
FALSE, // новый описатель потока ненаследуемый;
// новому описателю потока присваиваются
DUPLICATE_SAME_ACCESS); // те же атрибуты защиты.
// что и псевдоописателю
- 54 -
функции с передачей описателя родительского потока, то, естественно, к
CloseHandle следует обращаться только после того, как необходимость в этом
описателе у дочернего потока отпадет. Надо заметить, что DuplicateHandle
позволяет также преобразовать псевдоописатель процесса в «настоящий». Вот
как это сделать:
HANDLE hProcess;
DuplicateHandle(
GetCurrerttProcess(), // описатель процесса, к которому
// относится псевдоописатель;
GetCurrentProcess(), // псевдоописатель процесса;
GetCurrentProcess(), // описатель процесса, к которому
// относится новый, "настоящий"
// описатель;
&hProcess; // даст новый, "настоящий" описатель.
// идентифицирующий процесс;
0, // игнорируется из-за
// DUPLICATE_SAME_ACCESS;
FALSE, // новый описатель ненаследуемый;
// новому описателю процесса присваиваются
DUPLICATE_SAME_ACCESS); // те же атрибуты защиты,
// что и псевдоописателю
- 55 -
Ø Распределение процессорного времени между потоками
Операционная система с вытесняющей многозадачностью должна
использовать тот или иной алгоритм, позволяющий ей распределять
процессорное время между потоками. Рассмотрим алгоритм, применяемый в
Windows NT и Windows 95.
Система выделяет процессорное время всем активным потокам, исходя из
их уровней приоритета, которые варьируются от 0 (низший) до 31 (высший).
Нулевой уровень присваивается в системе особому потоку обнуления страниц
(zero page thread), обнуляющему свободные страницы при отсутствии других
потоков, требующих внимания со стороны системы. Ни один поток, кроме
него, не может иметь нулевой уровень приоритета.
Когда система подключает процессор к потоку, он обрабатывает потоки с
одинаковым приоритетом как равноправные. Иначе говоря, на процессор
подается первый поток с приоритетом 31, а по истечении его кванта времени
система переключает процессор на выполнение следующего потока с тем же
приоритетом. Как только все потоки с приоритетом 31 получат по кванту
времени, система вновь подаст на процессор первый поток с приоритетом 31.
Заметьте: если в Вашей системе за каждым процессором закреплен хотя бы
один поток с приоритетом 31, остальные потоки с более низким приоритетом
никогда не получат доступ к процессору и поэтому не будут выполняться.
Такая ситуация называется перегрузкой (starvation). Она наблюдается, когда
некоторые потоки так интенсивно используют процессорное время, что
остальные практически не работают.
При отсутствии потоков с приоритетом 31 система переходит к потокам с
приоритетом 30; если отпала необходимость в выполнении и этих, к
процессору подключаются потоки с приоритетом 29 и т. д.
На первый взгляд, в системе, организованной таким образом, у потоков с
низким приоритетом (вроде потока обнуления страниц) нет ни единого шанса
на исполнение. Но вот ведь в чем штука: зачастую потоки как раз и не нужно
выполнять. Например, если первичный поток Вашего процесса вызывает
- 56 -
GetMessage, а система «видит», что никаких сообщений пока нет, она
приостанавливает его выполнение, отнимает остаток неиспользованного
времени и тут же подключает к процессору другой, ожидающий поток. И пока
в системе не появится сообщений для потока Вашего процесса, он будет
простаивать — система не станет тратить на него время. Но вот в очереди
этого потока появляется сообщение, и система сразу же подключает его к
процессору (если только в этот момент не выполняется поток с более высоким
приоритетом).
А теперь еще один момент. Допустим, процессор исполняет поток с
приоритетом 5, и тут система обнаруживает, что поток с более высоким
приоритетом готов к выполнению. Что будет? Система остановит поток с
более низким приоритетом — даже если не истек отведенный ему квант
процессорного времени — и подключит к процессору поток с более высоким
приоритетом (и, между прочим, выдаст ему полный квант времени). Так что
потоки с более высоким приоритетом всегда вытесняют потоки с более
низким приоритетом независимо от того, исполняются последние или нет.
- 57 -
функции CreateProcess с другими флагами fdwCreate. Вот какие уровни
приоритета связаны с каждым классом приоритета:
- 58 -
было, заставка активизируется. Мониторинг незачем вести при очень высоком
приоритете — достаточно и низкого, т. е. idle.
Класс приоритета high следует использовать только при крайней
необходимости. Может, Вы этого и не знаете, но Explorer выполняется с
высоким приоритетом. Большую часть времени его потоки простаивают,
готовые пробудиться, как только пользователь нажмет какую-нибудь клавишу
или щелкнет кнопку мыши. Пока потоки Explorer простаивают, система не
выделяет им процессорного времени, что позволяет выполнять потоки с
низким приоритетом. Но вот пользователь нажал, скажем, Ctrl+Esc, и система
пробуждает поток Explorer. (Комбинация клавиш Ctrl+Esc попутно открывает
меню Start.) Если в данный момент исполняются потоки с низким
приоритетом, они немедленно вытесняются, и начинает работать поток
Explorer. Microsoft разработала Explorer именно так потому, что любой
пользователь — независимо от текущей ситуации в системе — ожидает
мгновенной реакции оболочки на свои команды. В сущности, окна Explorer
можно открывать, даже когда все потоки с низким приоритетом зависают в
бесконечных циклах. Обладая более высоким приоритетом, потоки Explorer
вытесняют поток, исполняющий бесконечный цикл, и дают возможность
закрыть зависший процесс.
Надо отметить высокую степень продуманности Explorer. Основную
часть времени он просто «спит», не требуя процессорного времени. Будь это
не так, вся система работала бы гораздо медленнее, а многие приложения
просто не отзывались бы на действия пользователя.
И, наконец, флагом четвертого по счету класса приоритета —
REALTIME_PRIORI-TY_CLASS — почти никогда не стоит пользоваться. На
самом деле в ранних бета-версиях Win32 API даже не предусматривалось
присвоения этого приоритета приложениям, хотя операционная система
поддерживала эту возможность. Realtime — чрезвычайно высокий приоритет,
и поскольку большинство потоков в системе (включая те, что управляют
самой системой) имеют более низкий приоритет, процесс с таким классом
- 59 -
окажет на них сильное влияние. Так, системные потоки, контролирующие
мышь и клавиатуру, фоновый сброс данных на диск и перехват Alt+Ctrl+Del,
— все они оперируют при более низком классе приоритета. Если пользователь
переместит мышь, поток, реагирующий на движение мыши, будет вытеснен
потоком с приоритетом realtime. А это повлияет на характер перемещения
курсора мыши: он станет двигаться не плавно, а рывками. Может случиться и
кое-что похуже — вплоть до потери данных.
Класс приоритета realtime используют только в программе, напрямую
обращающейся к оборудованию, или если приложение выполняет
быстротечную операцию, которую нельзя прерывать ни при каких
обстоятельствах.
- 60 -
Эта функция меняет класс приоритета процесса, определяемого
описателем hProcess, в соответствии со значением параметра fdwPriority. Этот
параметр принимает одно из значений: IDLE_PRIORITY_CLASS,
NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS или
REALTIME_PRIORITY_CLASS. При успешном выполнении функция
возвращает TRUE; в ином случае — FALSE. Поскольку SetPriorityClass
принимает описатель процесса, Вы можете изменить приоритет любого
процесса, выполняемого в системе, — если его описатель известен и у Вас
есть соответствующие права доступа.
Парная ей функция GetPriorityClass позволяет узнать класс приоритета
любого процесса:
- 61 -
В Windows 95 команда START не поддерживает ключи /LOW, /NORMAL,
/HIGH и /REALTIME. Из оболочки командного процессора Windows 95
процессы всегда запускаются с обычным классом приоритета.
Идентификатор Описание
THREAD PRIORITY LOWEST Приоритет потока должен быть на 2
единицы ниже класса приоритета процесса.
THREAD PRIORITY BELOW NORMAL Приоритет потока должен быть на I
единицу ниже класса приоритета процесса.
THREAD PRIORITY NORMAL Приоритет потока должен соответствовать
классу приоритета потока.
THREAD PRIORITY ABOVE NORMAL Приоритет потока должен быть на 1
единицу выше класса приоритета процесса.
THREAD PRIORITY HIGHEST Приоритет потока должен быть на 2
единицы выше класса приоритета процесса.
- 62 -
В момент создания потока начальное значение его относительного
приоритета равно THREAD_PRIORITY_NORMAL. Правила установки
приоритета для потоков в рамках какого-либо процесса аналогичны правилам
установки приоритета для потоков разных процессов. Устанавливать
приоритет THREAD_PRIORITY_HIGHEST следует, только если это
абсолютно необходимо для корректного выполнения данного потока. Иначе
потоки с более низкими приоритетами будут полностью вытеснены потоками
с более высокими приоритетами.
Кроме упомянутых в таблице флагов, в функцию SetThreadPriority можно
передать еще два (особых) флага: THREAD_PRIORITY_IDLE и
THREAD_PRIORITY_TIM_CRITICAL. Первый устанавливает уровень
приоритета потока равным 1 при классе приоритета данного процесса idle,
normal или high. Если же класс приоритета процесса realtime, уровень приори-
тета потока приравнивается 16. А второй флаг устанавливает уровень
приоритета потока равным 15 при классе приоритета данного процесса idle,
normal или high. Если же класс приоритета процесса realtime, уровень
приоритета потока приравнивается 31. В таблице показано, как система
определяет базовый уровень приоритета потока, комбинируя класс приоритета
процесса с относительным приоритетом потока.
- 63 -
Класс приоритета процесса
Highest (высший) 6 10 15 26
Normal (обычный) 4 8 13 24
Lowest (низший) 2 6 11 22
Idle (простаивающий) 1 1 1 16
- 64 -
создается в процессе с высоким классом приоритета и исполняются
следующие две строки кода:
SetThreadPrionty(hThread, THREAD_PRI0RITY_LOWEST);
SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST);
- 65 -
сторону). Этот новый уровень приоритета потока называется динамическим
приоритетом потока. Процессор исполняет поток в течение отведенного
отрезка времени, а по его истечении система снижает приоритет потока на 1,
до уровня 14. Далее потоку вновь выделяется квант процессорного времени,
по окончании которого система опять снижает уровень приоритета потока на
1. И теперь динамический приоритет потока снова соответствует базовому
уровню его приоритета. При этом динамический приоритет никогда не
опускается ниже базового уровня приоритета.
Microsoft всегда старается тонко настраивать динамическое изменение
приоритета потоков в операционной системе, чтобы та максимально быстро
реагировала на действия конечного пользователя. Кстати, потоки с уровнями
приоритетов реального времени (от 16 до 31) системой никогда не меняются.
Описанный механизм действует лишь по отношению к потокам с уровнями
приоритетов, попадающими в динамический диапазон. И, кроме того, система
не допускает динамического повышения приоритета потока до уровней реаль-
ного времени (более 15).
В Windows NT 4 появились две новые функции, позволяющие отключать
автоматическое изменение приоритетов потоков:
- 66 -
Каждой из этих двух функций Вы передаете описатель нужного процесса
или потока и адрес переменной типа BOOL, в которой и возвращается
результат.
- 67 -
Любой поток может вызвать эту функцию и приостановить выполнение
другого потока. Хоть об этом нигде и не говорится, приостановить свое
выполнение поток способен сам, а возобновить себя без посторонней помощи
— нет. Как и ResumeThread, SuspendThread возвращает предыдущее значение
счетчика простоев данного потока. Поток допустимо задерживать не более
чем MAXIMUM_SUSPEND_COUNT раз (в файле WINNT.H это значение
определено как 127).
- 68 -
ПРОГРАММИРОВАНИЕ ДЛЯ UNIX
· ПРОГРАММЫ, ПРОЦЕССЫ И ПОТОКИ
Программа – это набор команд и данных, который хранится в обычном
файле на диске. В индексном узле этот файл отмечен как исполняемый, а
содержимое файла организовано согласно определенным ядром правилам
(случай, когда ядру не безразлично содержимое файла).
Программисты могут создавать исполняемые файлы так, как им
заблагорассудится: если содержимое файла соответствует правилам и файл
отмечен как исполняемый, программу можно выполнить. На практике обычно
имеет место следующее: сначала исходный код программы, написанный на
каком-то языке программирования (скажем, С или C++), сохраняется в
обычном файле, который часто называют текстовым файлом, потому что он
содержит строки текста. Затем создается другой обычный файл, называемый
объектным файлом — он содержит машинный код, полученный в результате
преобразования исходной программы. Для выполнения этого преобразования
используется компилятор или ассемблер (которые сами являются
программами). Если в объектном файле имеются все нужные функции, он
отмечается, как исполняемый и может быть выполнен как есть. В противном
случае разработчик должен с помощью компоновщика (иногда называемого в
мире UNIX «загрузчиком») связать объектный файл с другими объектными
файлами, которые могут быть организованы в библиотеки. Если компоновщик
сможет обнаружить все, что ему нужно, он создаст исполняемый файл.
Чтобы запустить программу, ядро сначала должно создать новый процесс
— среду, и которой выполняется программа. Процесс состоит из трех
сегментов: сегмента команд, сегмента пользовательских данных и сегмента
системных данных. Программа используется для инициализации сегментов
команд и пользовательских данных. После этой инициализации процесс
начинает все сильнее отличаться от выполняемой в нем программы — чтобы
подтвердить это, достаточно вспомнить, что программисты изменяют данные
и, в редких случаях, сами команды. Кроме того, процесс может получать в
- 69 -
свое распоряжение дополнительную память, открывать файлы и приобретать
другие ресурсы, отсутствующие в программе.
Пока процесс выполняется, ядро следит за его потоками — отдельными
путями выполнения команд, которые потенциально могут осуществлять
чтение и запись одних и тех же данных процесса (однако каждый поток имеет
свой стек). Приступив к написанию программы, вы имеете в своем
распоряжении только один поток, пока не создадите другой с помощью
специального системного вызова. Таким образом, начинающие программисты
могут считать, что процесс является однопоточным.
Используя в качестве источника одну программу, можно создать и
инициализировать несколько одновременно выполняемых процессов, однако
между ними не будет никакого функционального отношения. Ядро может
сэкономить память, сделав сегмент команд этих процессов общим, но сами
процессы не могут узнать об этом. В то же время потоки одного процесса
связаны сильным функциональным отношением.
Системные данные процесса включают такие атрибуты, как текущий
каталог, дескрипторы открытых файлов, использованное время процессора и
т. д. Процесс не может читать или изменять свои системные данные
непосредственно, так как они находятся вне его адресного пространства.
Вместо этого для чтения и изменения атрибутов используются различные
системные вызовы.
Выполняемый процесс может поручить ядру создать другой процесс и
стать родителем нового дочернего процесса. Дочерний процесс наследует
большинство атрибутов системных данных родителя. Например, если
родительскому процессу соответствуют какие-то открытые файлы, для
дочернего процесса эти файлы также будут открыты. Преемственность такого
рода — фундаментальный принцип UNIX. Это отличается от создания
потоком нового потока. Потоки одного процесса в большинстве отношений
равны и ничего не наследуют друг от друга. Все потоки могут обращаться ко
всем данным и ресурсам — не к копиям.
- 70 -
· ПРОЦЕССЫ
Ø Системный вызов exec
Невозможно понять системные вызовы exec и fork без четкого понимания
различий между процессом и программой. В двух словах напомню суть
различий: процесс — это среда исполнения, которая включает в себя сегмент
исполняемого кода, сегменты пользовательских и системных данных, а также
набор дополнительных ресурсов, полученных во время исполнения.
Программа — это файл, который содержит исполняемый код, сегменты с
данными для инициализации и с данными пользователя.
Системный вызов exec повторно инициализирует процесс, подменяя его
указанной программой — программа меняется, а процесс остается.
Системный вызов fork наоборот запускает новый процесс, который является
точной копией существующего, простым копированием сегментов с
исполняемым кодом и данными. Вновь созданный процесс продолжает работу
с того же места, что и изначальный, таким образом, оба процесса продолжают
исполнять один и тот же код.
По отдельности друг от друга exec и fork используются крайне редко. Мы
будем использовать их вместе друг с другом, и Вы убедитесь, насколько
мощной может быть эта пара.
Системный вызов exec — единственный способ запуска программ в
UNIX. Мало того, что командная оболочка использует exec для запуска наших
программ, но и сама она запускается именно таким образом. А системный
вызов fork — единственный способ запустить новый процесс.
На самом деле, системного вызова с именем exec не существует. Под этим
именем подразумевается целое семейство из шести системных вызовов, имена
которых в общем виде можно записать как ехесАВ. А — это один из
символов, «1» или «v», они определяют, как входные аргументы передаются
вызову — в виде списка (от анг. list) или в виде массива (от англ. vector). В
(может отсутствовать) — это либо «р» указывающий, что поиск файла
программы должен выполняться с помощью переменной PATH, либо «е» —
- 71 -
такому вызову передается специфичная среда окружения (как это ни странно,
но нет системного вызова exec, который совмещал бы в себе характерные
особенности «е» и «р»). Таким образом, мы получаем шесть различных
системных вызовов: execl, execv, exeelp, execvp, execle и execve.
#include <unistd.h>
int execl(
const char *path, /* полный путь к программе */
const char *arg0, /* первый аргумент (аrg[0] - имя файла) */
const char *arg1, /* второй аргумент (если необходимо) */
…, /* остальные аргументы (если необходимы) *
NULL /* пустой указатель, завершающий список */
);
/* В случае ошибки возвращает -1 (код ошибки - в переменной еrrnо) */
- 72 -
Все остальные аргументы вызова собираются в массив указателей на
строки, а последним всегда должен стоять пустой указатель, который
определяет конец списка входных аргументов. Первый аргумент, в
соответствии с соглашениями — это имя файла программы (только само имя
файла, без пути к нему). Новая программа получает доступ к этому списку
через уже знакомые нам аргументы функции main() – argc и argv. Среда
окружения также передается новой программе и доступна ей через указатель
environ или посредством функции getenv.
Поскольку процесс продолжает существовать и сегмент с системными
данными практически не изменяется, почти псе атрибуты процесса также
остаются неизменными, включая идентификатор процесса, идентификатор
процесса-предка, идентификатор группы процесса, идентификатор сессии,
управляющий терминал, реальные индефикаторы пользователя и группы,
текущий и корневой каталоги, приоритет, статистические характеристики
времени исполнения в пространстве пользователя и в пространстве ядра и, как
правило, дескрипторы открытых файлов. Гораздо проще перечислить
основные атрибуты, которые изменяются:
1. Если процесс назначал свои обработчики сигналов, то все они
сбрасываются в исходное состояние, поскольку функции
обработчики после запуска новой программы станут недоступны.
2. Если в новой программе установлены, биты set-user-ID или set-
group-ID, то действующие идентификаторы пользователя и группы
переустанавливаются в соответствии с идентификаторами
владельца и группы файла программы. Нет никакого способа
вернуть прежние действующие идентификаторы, если они
отличаются от реальных.
3. Регистрация всех функций, которая была выполнена с помощью
atexit(), отменяется, поскольку код этих функций будет затерт.
4. Сегменты общей памяти отсоединяются, поскольку точки
соединения будут утеряны.
- 73 -
5. Именованные семафоры POSIX закрываются. Семафоры System V
остаются без изменений.
Это не полный список атрибутов, но суть состоит в следующем: если
сохранение атрибута или ресурса не имеет смысла, то он или сбрасываются в
состояние по умолчанию, или закрывается.
Для демонстрации системного вызова execl рассмотрим следующий
пример:
void exectest(void)
{
printf("Шустрая рыжая лисица перепрыгнула через ");
ec_neg1(execl("/bin/echo","echo","ленивую","собаку.",NULL))
return;
EC_CLEANUP_BGN
EC_FLUSH("exectest");
EC_CLEANUP_END
}
ленивую собаку.
setbuf(stdout, NULL);
- 74 -
либо принудительным выталкиванием буфера непосредственно перед вызовом execl:
fflush(stdout);
- 75 -
Ниже приводится краткое описание остальных системных вызовов
семейства exec:
1. execv — запускает программу; аргументы передаются в виде
массива:
#include <unistd.h>
int execv(
const char *path, /* полный путь к файлу а программой */
char *const argv[ ] /* массив аргументов */
);
/* В случае ошибки возвращает -1 (код ошибки - в переменной errno) */
#include <unistd.h>
int execlp(
const char *file, /* имя файла с программой */
const char *arg0, /* первый аргумент (имя файла) */
const char *arg1, /* второй аргумент (если необходим) */
… /* остальные аргументы (если необходимы) */
NULL /* пустой указатель, завершавший список аргументов */
);
/* В случае ошибки возвращает -1 (код ошибки − в переменной еrrno) */
- 76 -
3. execvp — запускает программу; аргументы передаются в виде
массива, поиск файла ведется с использованием переменной PATH:
#include <unistd.h>
int execvp(
const char *file, /* имя файла с программой */
char *const argv[ ] /* массив аргументов */
);
/* В случае ошибки возвращает -1 (код ошибки − в переменной errno) */
#include <unistd.h>
int execle(
const char *path, /* полный путь к файлу с программой */
const char *arg0, /* первый аргумент (имя файла) */
const char *arg1, /* второй аргумент (если необходим) */
… /* остальные аргументы (если необходимы) */
NULL /* пустой указатель, завершающий список аргументов */
char *const envv[ ] /* массив сформированной среды окружения */
);
/* В случае ошибки возвращает -1 (код ошибки - в переменной errno) */
- 77 -
5. execve − запускает программу; аргументы передаются в виде
массива, так же передается среда окружения:
#include <unistd.h>
int execve(
const char *path, /* полный путь к файлу с программой */
char *const argv[ ] /* массив аргументов */
char *const envv[ ] /* массив сформированной среды окружения */
);
/* В случае ошибки возвращает -1 (код ошибки - в переменной errno) */
- 78 -
Ø Системный вызов fork
Вызов fork до определенной степени является противоположностью
вызову exec. Он запускает новый процесс, но не новую программу, где новый
процесс – это точная копия старого со всеми его данными.
fork – создает новый процесс:
#include <unistd.h>
pid_t fork(void);
/* Возвращает идентификатор дочернего процесса или 0 в случае успеха и -1
в случае ошибки (код ошибки - в переменной еrrno) */
- 80 -
void forktest(void)
{
int pid;
Pезультат:
$ forktest
Начало теста
Возвращаемое значение 98657
Возвращаемое значение 0
$
- 81 -
Это произошло из-за буферизации вывода − потомок, вместе со всем
остальным, унаследовал от предка и частично заполненный выходной буфер.
Когда потомок завершил свою работу, его буфер был вытолкнут, то же самое
произошло и с предком. В предыдущем испытании printf не буферизовала
свой вывод, поскольку «знала», что устройством стандартного вывода
является терминал, который предполагает более интерактивный pежим
работы.
Вызов fork и ехeс прекрасно дополняют друг друга. Дочерний процесс,
созданный вызовом fork, сам по себе не представляет особой ценности, так как
является точной копией родителя. Мы получим гораздо больше выгоды, если
потомок заменит себя новой программой — то есть как раз то, что делает
вызов exec.
- 82 -
Ø Завершение процесса и системные вызовы exit
Процесс заканчивает работу в четырех случаях:
1. При обращении к системному вызову exit. Возврат значения из
функции main эквивалентен вызову exit с тем же самым значением.
Выход из функции main без возвращаемого значения равносилен
возврату значения 0.
2. При обращении к _exit или _Exit – двум разновидностям системною
вызова exit t, которые будут описаны чуть ниже.
3. При получении сигнала на завершение.
4. В случае краха системы, причиной которого может стать все что
угодно, начиная от перебоев в сети электропитания и заканчивая
ошибкой в приложении или в операционной системе.
Между тремя разновидностями системного вызова exit существуют
следующие различия:
1. _exit и _Exit ведут себя совершенно идентично, хотя чисто
технически _ехit относится к UNIX, a_Exit — к стандарту языка С.
2. exit (без символа подчеркивания) также определен стандартами
языка С. Он делает все то же самое, что и _exit, но кроме этого
выполняет дополнительные операции по завершению процесса —
вызывает функции, зарегистрированные обращением к atexit и
выталкивает стандартные буферы ввода-вывода, как если бы были
вызваны fflush или fclose. (Выталкивает ли буферы _exit, зависит от
конкретной реализации.)
Поскольку exit выполняет все то же, что и _exit, то все, что будет сказано
об _exit, в равной степени относится и к exit.
Как правило, _exit вызывается вместо exit в процессах-потомках, которые
не смогли вызвать exec. Делается это потому, что код завершения процесса,
унаследованный потомком (занимающийся сборкой мусора, освобождением
ресурсов и пр.), обычно должен выполняться единожды. Но это справедливо
- 83 -
не для всех случаев, поэтому в каждой конкретной ситуации вы должны еще
раз все оценить и принять решение какой из вариантов подходит вам больше.
Если для контроля над ошибками вы собираетесь использовать макросы
«ес», не забывайте, что автоматическое отображение сообщений выполняется
в функции, зарегистрированной с помощью atexit, которая не будет вызвана
при завершении процесса вызовом _exit.
Ниже приводится краткое описание семейства функции exit. Обратите
внимание, объявления функций находятся в двух различных заголовочных
файлах:
1. _exit − завершает процесс без обращения к коду сборки мусора:
#include <unistd.h>
void _exit(
int status /* код завершения */
);
/* В программу управление уже не возвращается */
#include <stdlib.h>
void _Exit(
int status /* код завершения */
);
/* В программу управление уже не возвращается */
#include <stdlib.h>
void exit(
int status /* код завершения */
);
/* В программу управление уже не возвращается */
- 84 -
_exit и родственные ему системные вызовы завершают работу процесса,
обратившегося к нему, с кодом завершения, равным младшему байту
аргумента status. Эти вызовы имеют ряд побочных эффектов, наиболее
важные из которых перечислены ниже, полный список вы найдете в
[SUS2002]. Фактически, эти побочные эффекты имеют место при любом
варианте завершения процесса (исключая, разве что, крах системы).
1. Все открытые дескрипторы закрываются.
2. Если процесс был управляющим процессом (лидер сеанса), сеанс
теряет свой управляющий терминал. Кроме того, каждому процессу
в сеансе передается сигнал SIGHUP, который приводит к
завершению процессов.
3. Родительский процесс извещается о завершении процесса-потомка
через один из системных вызовов семейства wait.
4. Это не оказывает непосредственного влияния на дочерние
процессы, за исключением того, что их новым предком становится
специальный процесс в системе, чей идентификатор (обычно − 1)
потомок может получить, обратившись к системному вызову
getppid.
Родительский процесс получает код завершения дочернего процесса
через один из системных вызовов wait. Код завершения − это число в
диапазоне от о до 255. В соответствии с соглашениями, значение 0
соответствует коду успешного завершения, а ненулевое значение какой-либо
ошибке, код которой определяется самим завершившим работу приложением.
После того как процесс завершится, он прекращает свое исполнение, но
остается в системе до тех пор, пока код завершения не будет передан
процессу-предку, если он, конечно же, заинтересован в этом. Если потомок не
смог сообщить предку о своем завершении, он переходит в состояние
«зомби».
- 85 -
Ø Системные вызовы wait, waitpid и waitid
Системные вызовы wait, waitpid и waitid ожидают, пока дочерний процесс
не изменит свое состояние (приостановка, возобновление или завершение) и
возвращают его вызывающей программе.
#include <sys/wait.h>
pid_t waitpid(
pid_t pid, /* идентификатор процесса или группы процессов */
int *statusp, /* указатель на статус или NULL */
int options /* флаги */
);
/* В случае успеха возвращает идентификатор процесса или 0, в случае
ошибки -1 (код ошибки в переменной errno) */
- 86 -
идентификатора, благодаря чему системный вызов будет возвращать
управление по завершении каждой из команд.
На выходе из waitpid вызывающий процесс получает идентификатор
процесса-потомка в виде возвращаемого значения, чей идентификатор совпал
с аргументом pid. Ноль возвращается только в том случае, когда был
установлен флаг WNOHANG (будет описан ниже).
Допускается ожидать изменения состояния только прямых потомков,
порожденных системным вызовом fork. Ожидание процессов-«внуков» не
допускается, даже если их родители (прямые потомки вызывающего процесса)
к моменту вызова уже завершили свою работу. Как объяснялось в
предыдущем разделе, «осиротевшие» процессы передаются под опекунство
специального системного процесса, а не их «бабушкам-дедушкам».
Как правило, процессы-предки заинтересованы в получении информации
о состоянии своих потомков — в противном случае процессы-потомки по
завершении превращаются в «зомби» и пребывают в таком виде, пока не
завершит работу родительский процесс. Тогда системный процесс, ставший
«приемным родителем», сможет обратиться к вызову wait и удалить
«зомбированный» процесс. Учитывая, что многие процессы могут
исполняться в системе довольно длительное время (до нескольких месяцев),
такое «невнимание» к дочерним процессам может привести к заполнению
системных таблиц ненужным мусором. Если ожидание потомков невозможно,
по тем или иным причинам, процесс может использовать сигналы для
предотвращения «зомбироваиия» — подробнее об этом будет рассказано в
следующем разделе.
Системный вызов waitpid может вернуть состояние изменившего его
дочернего процесса только один раз (такой дочерний процесс называется
ожидаемый). Или другими только один раз словами: ожидаемый потомок
перестает быть ожидаемым, если отчет об изменении его состояния уже
получен. Это означает следующее: если в одной точке программы было
получено состояние потомка и вдруг обнаружилось, что это не тот потомок,
- 87 -
которого ожидали, то нет никакого способа вернуть результат обратно в
систему, чтобы другой waitpid мог получить его (Хотя waitpid может сделать
это сам).
Если к моменту вызова waitpid имеется ожидаемый дочерний процесс,
соответствующий заданному аргументу pid, управление в вызывающую
программу возвращается немедленно. Если потомок найден, но еще не
изменил снос состояние, системный вызов waitpid блокируется до появления
подходящего ожидаемого потомка. Если потомок не был найден, вызывающей
программе возвращается значение -1 и код ошибки ECHILD. Это может
произойти потому, что аргумент pid задан неправильно пни процесс потомок
перестал быть ожидаемым, т. е. его состояние уже было получено.
Если в качестве аргумента statusp передан непустой указатель, то по
заданному адресу записывается код состояния потомка. Он представляет
собой комбинацию аргумента системного вызова _exit или exit (если речь идет
о завершении потомка) и числа, описывающего причину завершения или
приостановки.
Последний аргумент options может содержать один или более флагов,
объединенных операцией ИЛИ:
waitpid(pid, &status, 0)
- 88 -
2. /* Ожидать завершения любого из потомков, без получения кода
завершения */
#include <sys/wait.h>
pid_t wait(
int *statusp, /* указатель на статус или NULL */
);
/* Возвращает идентификатор процесса или -1 в случае ошибки (код ошибки − в
переменной errno) */
- 89 -
процесс или, по крайней мере, члена группы процессов. Никогда не
используйте wait при написании библиотечных функций, которые создают
дочерние процессы.
Предположим, что процесс имеет двух потомков и заранее не известно,
какой из них завершит работу первым. Если нам нужно только дождаться
завершения обоих, можно сначала подождать завершения любого из
потомков, а потом вызвать waitpid для ожидании завершения другого. Или
родительский процесс может выполнять какую-либо работу и периодически
вызывать waitpid с флагом WNOHANG для каждого из потомком. Но никогда
не используйте waitpid с аргументом pid, равным -1, если приложением не
гарантируется отсутствие других потомков. Вообще, в больших и сложных
проектах, где над отдельными частями работают целые группы разработчиков
(например, библиотеки для работы с изображениями или библиотеки для
взаимодействия с базами данных) таких гарантий дать никто не может. Было
бы просто здорово, если бы существовала такая разновидность системного
вызова wait, которому можно было бы передать целый массив
идентификаторов, но, увы, такого вызова нет.
- 90 -
#include <sys/wait.h>
int waitid(
idtype_t idtype, /* тип идентификатора */
id_t id, /* идентификатор */
siginfo_t *infop, /* возвращаемые сведения */
int options /* флаги */
);
/* Возвращает 0 в случае успеха, -1 в случае ошибки (код ошибки в
переменной errno) */
- 91 -
Системный вызов waitid не имеет прямого эквивалента waitpid с pid==0,
но то же самое можно сделать, если в первом аргументе передать значение
P_PGID, а во втором — групповой идентификатор вызывающего процесса.
Аргумент options может содержать один или более флагов, объединенных
операцией ИЛИ:
- 92 -
Если код CLD_EXITED, то поле si_status содержит число, переданное
системному вызову _exit или exit. В любом другом случае изменение
состояния процесса может быть вызвано только сигналом, поэтому поле
si_status будет содержать номер сигнала.
При использовании waitid, проблема, связанная со случайным
получением отчета о завершении работы «не того» потомка, может быть
предотвращена, если построить работу по такому алгоритму:
1. Запустить waitid с аргументами P_ALL и WNOWAIT.
2. Если полученный идентификатор процесса-потомка не
представляет никакого интереса — вернуться к шагу 1.
3. Если получен идентификатор нужного потомка — перезапустить
вызов waitid с данным идентификатором и без флага WNOWAIT
(или просто waitpid)
4. Если есть еще потомки, которых нужно «дождаться» — перейти к
шагу 1.
Единственная проблема в том, что системный вызов waitid не доступен в
системах, не соответствующих спецификации SUS1, включая Linux, FrееВSD
и Darwin. Поэтому в них придется использовать более неуклюжие методы
работы с waitpid, описанные выше.
- 93 -
Ø Получение идентификатора процесса
Процесс может получить свой идентификатор и тдентификатор предка с
помощью системных вызовов:
1. getpid – возвращает идентификатор процесса:
#include <unistd.h>
pid_t getpid(void );
/* Возвращает идентификатор процесса */
#include <unistd.h>
pid_t getppid(void );
/* Возвращает идентификатор родительского процесса */
- 94 -
Ø Получение и изменение приоритета
Каждому процессу назначается значение параметра nice (в переводе с
английского — тактичный, внимательный, дружелюбный), посредством
которого процесс может влиять на уровень своего приоритета. Чем выше
параметр nice, тем более «вежлив» и «тактичен» процесс по отношению к
другим и тем ниже его приоритет. Меньшее значение nice означает более
«грубое» и «беспардонное» поведение процесса и более высокий его
приоритет. Параметр nice представляет собой положительное значение,
обычно — число 20 (довольно странно, но в документации к UNIX это число
называется как NZERO) и является смещением от некоторого числа, которое
зависит от системы. Процесс стартует с параметром nice равным 20 и может
стать как очень «дружелюбным», задав параметр nice равным 39, так и
совершенно «беспардонным», задав параметр nice равным 0.
Для того, чтобы изменить свой приоритет, процесс обращается к
системному вызову nice.
nice — изменяет значение параметра nice:
#include <unistd.h>
int nice(
int incr /* приращение */
);
/* Возвращает новое значение nice - NZER0 или -1 в случае ошибки (код
ошибки - в переменной еrrno) */
- 95 -
Фактически, системный вызов nice возвращает новое значение nice,
уменьшенное на 20, таким образом, возвращаемое значение лежит в диапазоне
от -20 до 19, при условии, что NZER0==20. Однако возвращаемое значение
редко используется в программах. Особенно если учесть, что новое значение
nice, равное 19, неотличимо от признака ошибки (19-20=-1). Эта ошибка
остается неисправленной.
Существуют два более новых системных вызова – getpriority и setpriority,
которые также могут использоваться для управления приоритетом
приложения. За дополнительной информацией обращайтесь к [SUS2002] или к
документации по вашей системе.
- 96 -
· ПОТОКИ
Ø Создание потока
В примерах, встречавшихся до сих пор, создаваемые нами процессы (с
помощью вызова fork) имели единственную последовательность исполнения,
которая называется потоком. Процесс двигался вперед от инструкции к
инструкции, пользуясь единственным стеком, изменяя глобальные данные и
используя системные ресурсы, порой в результате обращения к системным
вызовам.
Благодаря наличию в UNIX поддержки потоков POSIX, процесс может
иметь несколько последовательностей исполнения, каждая из которых
исполняет свою собственную последовательность команд и обладает своим
собственным стеком. Все остальное, чем владеет процесс, включая
глобальные данные и ресурсы, например открытые файлы или текущий
каталог, находятся в совместном использовании у потоков. Следующий
пример наглядно показывает, что имелось ввиду:
static long x = 0;
ini main(void)
{
pthread_t tid;
- 97 -
printf("Поток 1, значение счетчика %ld\n", ++x);
sleep(2);
}
return EXIT_SUCCESS;
EC_CLEANUP_BGN
return EXIT_FAILURE;
EC_CLEAANUP_END
}
- 98 -
#include <pthread.h>
int pthread_create(
pthread_t *thread_id, /* идентификатор нового потока */
const pthread_attr_t **atttr, /* атрибуты (или NULL) */
void *(*start_fcn)(void *), /* функция запуска */
void *arg /* аргумент функции запуска */
);
/* Возвращает 0 в случае успеха, в противном случае – код ошибки */
void *start_fcn(
void *arg
);
/* Возвращает код завершения */
- 99 -
Атрибут – это не просто набор флагов, это, скорее всего объект типа
pthread_attr_t, который вы должны инициализировать такими системными
вызовами, как ptnread_setscope и pthread_attr_setstacksize. В примерах мы
всегда будем использовать атрибуты ПО умолчанию, то есть второй аргумент
pthread_create всегда будет NULL.
Системные вызовы семейства «pthread» в случае успешного выполнения
возвращают значение 0, а в случае неудачи — код ошибки. Они не изменяют
глобальную переменную errno. На этот случай в нашем распоряжении имеется
специальный макрос ec_rv, который принимает код ошибки и интерпретирует
его так, как будто им был получен из переменной errno.
- 100 -
Ø Ожидание завершения потока
Поток может дождаться окончания работы другого потока и получить код
его завершения с помощью pthread_join (аналог системного вызова wait).
pthread_join — ожидает завершения потока:
#include <pthread.h>
int pthread_join(
pthread_t thread_id, /* идентификатор потока */
void **status_ptr /* код завершения (или NULL, если не требуется) */
);
/* Возвращает 0 в случае успеха, в противном случае - код ошибки */
static long x = 0;
int main(void)
{
pthread_t tid;
void *status;
- 101 -
ec_rv(pthread_create(&tid, NULL, thread_func, (void *)6))
ec_rv(pthread_join(tid, &status))
printf("Код завершения Потока 2: %ld\n", (long)status);
return EXIT_SUCCESS;
EC_CLEANUP_BGN
return EXIT_FAILURE;
EC_CLEANUP_END
}
- 102 -
Ø Принудительное завершение потока
Системный вызов pthread_cancel
Любой поток может принудительно завершить работу другого потока с
помощью системного вызова pthread_cancel.
pthread_cancel — принудительно завершает работу потока:
#include <pthread.h>
int pthread_cancel(
pthread_t thread_id /* идентификатор потока */
);
/* Возвращает 0 в случае успеха, в противном случае - код ошибки */
- 103 -
вам пришлось предусматривать обработку ситуации с принудительным
завершением.
С другой стороны, если поток вообще не обращается к функциям,
которые могут исполнять роль точки завершения и не предусмотрено варианта
самостоятельного завершения его работы, то такой поток может «жить» очень
долго. В такой ситуации можно предусмотреть одно или несколько обращений
к системному вызову pthread_testcancel в подходящих для этого местах, чтобы
обозначить возможные точки завершения. Эти вызовы не делают абсолютно
ничего, если не было команды на принудительное завершение.
#include <pthread.h>
void pthread_testcancel(void);
- 104 -
4. ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ
v РЕКОМЕНДУЕМАЯ ЛИТЕРАТУРА
Ø Э. Таненбаум – “Современные операционные системы”
Ø Windows
1. Дж. Рихтер – “Windows для профессионалов: программирование для
Windows 95 и Windows NT 4 на базе Win32 API”
2. Дж. Рихтер – “Windows для профессионалов: cоздание эффективных
Win32-пpилoжeний с учетом специфики 64-разрядной версии Windows”
Ø Unix
1. Марк Дж. Рочкинд – “Программирование для Unix”
2. А. Робачевский – “Операционная система Unix”
- 105 -
v РЕКОМЕНДУЕМЫЕ ИНТЕРНЕТ РЕСУРСЫ
Ø Литература
§ Э. Таненбаум – “Современные операционные системы, 2-ое издание”
· http://www.hub.ru/modules.php?name=Downloads&d_op=getit&lid=105
- 106 -
Ø Полезные ссылки
§ Сборник документации по программированию
· http://doks.gorodok.net/
- 107 -
Ø Программное обеспечение
§ Программа для просмотра файлов формате PDF
· http://www.foxitsoftware.com/downloads/
- 108 -