Лабораторная работа № 1

108
- 1 - СОДЕРЖАНИЕ 1. Цель работы 2. Лабораторное задание 3. Краткие сведения из теории v Программирование для Windows Ø Процессы § Функция CreateProcess · Параметры IpszApplicationName и IpszCommandLine · Параметры lpsaProcess, lpsaThread и finheritHandles · Параметр fdwCreate · Параметр lpvEnvironment · Параметр lpszCurDir · Параметр lpsiStartInfo · Параметр lppiProcInfo § Завершение процесса · Функция ExitProcess · Функция TerminateProcess · Если все потоки процесса «уходят» · Что происходит при завершении процесса § Дочерние процессы · Запуск обособленных дочерних процессов Ø Потоки · В каких случаях потоки создаются · В каких случаях потоки не создаются · Ваша первая функция потока · Стек потока · Структура CONTEXT § Функция CreateThread · Параметр Ipsa

Upload: api-3722765

Post on 13-Nov-2014

492 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Лабораторная работа № 1

- 1 -

СОДЕРЖАНИЕ

1. Цель работы

2. Лабораторное задание

3. Краткие сведения из теории

v Программирование для Windows

Ø Процессы

§ Функция CreateProcess

· Параметры IpszApplicationName и IpszCommandLine

· Параметры lpsaProcess, lpsaThread и finheritHandles

· Параметр fdwCreate

· Параметр lpvEnvironment

· Параметр lpszCurDir

· Параметр lpsiStartInfo

· Параметр lppiProcInfo

§ Завершение процесса

· Функция ExitProcess

· Функция TerminateProcess

· Если все потоки процесса «уходят»

· Что происходит при завершении процесса

§ Дочерние процессы

· Запуск обособленных дочерних процессов

Ø Потоки

· В каких случаях потоки создаются

· В каких случаях потоки не создаются

· Ваша первая функция потока

· Стек потока

· Структура CONTEXT

§ Функция CreateThread

· Параметр Ipsa

Page 2: Лабораторная работа № 1

- 2 -

· Параметр cbStack

· Параметры IpStartAddr и IpvThreadParm

· Параметр fdwCreate

· Параметр iplDThread

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

· Функция ExitThread

· Функция TerminateThread

· Если завершается процесс

· Что происходит при завершении потока

§ Как узнать о себе

§ Распределение процессорного времени между потоками

· Присвоение уровней приоритета в Win32

· Классы приоритета процессов

· Изменение класса приоритета процесса

· Установка относительного приоритета потока

· Динамическое изменение уровней приоритетов потоков

· Задержка и возобновление потоков

v Программирование для Unix

Ø Программы, процессы и потоки

Ø Процессы

§ Системный вызов exec

· Системный вызов execl

· Другие пять вызовов семейства exec

¨ execv

¨ exeсlp

¨ execvp

¨ execle

¨ execve

§ Системный вызов fork

Page 3: Лабораторная работа № 1

- 3 -

§ Завершение процесса и системные вызовы exit

· _exit

· _Exit

· exit

§ Системные вызовы wait, waitpid и waitid

· Системный вызов waitpid

· Системный вызов wait

· Системный вызов waitid

§ Получение идентификатора процесса

· getpid

· getppid

§ Получение и изменение приоритета

Ø Потоки

§ Создание потока

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

§ Принудительное завершение потока

· Системный вызов pthread_cancel

· Системный вызов pthread_testcancel

4. Дополнительная информация

v Рекомендуемая литература

v Рекомендуемые Интернет ресурсы

Ø Литература

Ø Полезные ссылки

Ø Программное обеспечение

Page 4: Лабораторная работа № 1

- 4 -

1. ЦЕЛЬ РАБОТЫ

· создание процессов, потоков;

· ожидание их завершения, получение результата работы.

2. ЛАБОРАТОРНОЕ ЗАДАНИЕ

1. Процессы. Процесс А инициализирует массив случайными значениями и

записывает их в файл, а затем запускает процесс Б в командной строке

передается имя файла с данными. После этого ожидает завершения процесса Б

и выводит на экран результат возврата процесс Б. Процесс Б открывает файл,

переданный ему в командной строке, находит в нем максимальный элемент, и

возвращает его в качестве результата.

2. Потоки. Поток А инициализирует массив случайными значениями, а затем

поток Б. После этого ожидает завершения потока Б и выводит на экран

результат возврата потока Б. Потока Б находит в массиве максимальный

элемент, и возвращает его в качестве результата.

Page 5: Лабораторная работа № 1

- 5 -

3. КРАТКИЕ СВЕДЕНИЯ ИЗ ТЕОРИИ

ПРОГРАММИРОВАНИЕ ДЛЯ WINDOWS

· ПРОЦЕССЫ

Процесс обычно определяют как экземпляр выполняемой программы. В

Win32 процессу отводится 4 Гб адресного пространства. Win32-процессы — в

отличие от своих аналогов в MS-DOS и 16-разрядной Windows — инертны.

Иными словами, Win32-процесс ничего не исполняет — просто владеет

четырехгигабайтовым адресным пространством, содержащим код и данные

ЕХЕ-файла приложения. В это же пространство загружаются код и данные

DLL-библиотек, если того требует ЕХЕ-файл. Кроме адресного пространства,

процессу принадлежат такие ресурсы, как файлы, динамически выделяемые

области памяти и потоки. Ресурсы, создаваемые при жизни процесса,

обязательно уничтожаются при его завершении.

Как было сказано, процессы инертны. Чтобы процесс что-нибудь

выполнил, в нем нужно создать поток. Именно потоки отвечают за

исполнение кода, содержащегося в адресном пространстве процесса. В

принципе, один процесс может владеть несколькими потоками, и тогда они

«одновременно» исполняют код в адресном пространстве процесса. Для этого

каждый поток должен располагать собственным набором регистров

процессора и собственным стеком, а каждый процесс — минимум одним

потоком. Если бы у процесса не было ни одного потока, ему нечего было бы

делать «на этом свете», и система автоматически уничтожила бы его вместе с

выделенным ему адресным пространством.

Чтобы все эти потоки работали, операционная система отводит каждому

из них определенное процессорное время. Выделяя потокам отрезки времени

(называемые квантами) по принципу карусели, она создает тем самым

иллюзию одновременного выполнения потоков (см. рис. 3-1).

Page 6: Лабораторная работа № 1

- 6 -

Рис. 3-1. “Операционная система выделяет потокам кванты времени

по принципу карусели”

При создании Win32-процесса первый (точнее, первичный) поток

создается системой автоматически. Далее этот поток может породить другие

потоки, те в свою очередь — новые и т. д.

Windows NT способна в полной мере использовать возможности машин с

несколькими процессорами. Windows NT способна закрепить каждый поток за

отдельным процессором, и тогда два потока исполняются действительно

одновременно. Ядро Windows NT полностью поддерживает распределение

процессорного времени между потоками и управление ими на таких системах.

Page 7: Лабораторная работа № 1

- 7 -

Ø Функция 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);

Когда поток в приложении вызывает CreateProcess, система создает

объект ядра «процесс» с начальным значением счетчика числа его

пользователей, равным 1. Этот объект — не сам процесс, а компактная

структура данных, через которую операционная система управляет процессом.

(Объект ядра «процесс» следует рассматривать как структуру данных со

статистической информацией о процессе.) Затем система создает для нового

процесса виртуальное адресное пространство размером 4 Гб и загружает в

него код и данные, как для исполняемого файла, так и для любых DLL (если

таковые требуются).

Далее система формирует объект ядра «поток» (со счетчиком, равным 1)

для первичного потока нового процесса. Как и в первом случае, объект ядра

«поток» — это компактная структура данных, через которую система

управляет потоком. Первичный поток начнет с исполнения стартового кода из

стандартной библиотеки С, который — как всегда — вызовет функцию

WinMain в Вашей программе (или main — если приложение относится к кон-

Page 8: Лабораторная работа № 1

- 8 -

сольному типу). Если системе удастся создать новый процесс и его первичный

поток, CreateProcess вернет TRUE.

Если Вам знакомы две функции 16-разрядной Windows, предназначенные

для создания процесса, — WinExec и LoadModule, — то, сравнив число их

параметров с тем, что есть в новой функции CreateProcess, Вы сразу поймете,

насколько шире ее возможности в контроле за созданием процесса. В 32-

разрядной Windows функции WinExec и LoadModule оставлены

исключительно для совместимости с 16-разрядной Windows и реализованы

через внутренние вызовы CreateProcess. Однако для старых функций не

предусмотрено версий, способных работать с Unicode, — так что в них можно

передавать только ANSI-строки.

Параметры IpszApplicationName и IpszCommandLine

Эти параметры определяют имя исполняемого файла, которым будет

пользоваться новый процесс, и командную строку, передаваемую этому

процессу. Начнем с IpszCommandLine.

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

CreateProcess при создании нового процесса. При анализе строки

IpszCommandLine функция извлекает первый компонент, полагая, что это имя

исполняемого файла, который Вы хотите запустить. Если в имени файла не

указано расширение, она считает его ЕХЕ. Далее функция приступает к

поиску заданного файла и делает это в следующем порядке:

1. Каталог, содержащий ЕХЕ-файл вызывающего процесса.

2. Текущий каталог вызывающего процесса.

3. Системный каталог Windows.

4. Основной каталог Windows.

5. Каталоги, перечисленные в переменной окружения PATH.

Конечно, если в имени файла указан полный путь доступа, система сразу

обращается туда и не просматривает упомянутые каталоги. Найдя нужный

Page 9: Лабораторная работа № 1

- 9 -

исполняемый файл, она создает новый процесс и проецирует код и данные

исполняемого файла на адресное пространство этого процесса. Затем

обращается к процедурам стартового кода из стандартной библиотеки С. Тот в

свою очередь, как уже говорилось, анализирует командную строку процесса и

передает WinMain адрес первого (за именем исполняемого файла) аргумента

как IpszCmdLine.

Все, о чем было сказано выше, произойдет, только если параметр

IpszApplicationName — NULL (что и бывает в 99% случаев). Вместо NULL

можно передать адрес строки с именем исполняемого файла, который надо

запустить. Однако тогда придется указать не только его имя, но и расширение,

поскольку в этом случае имя не дополняется расширением ЕХЕ автома-

тически. CreateProcess предполагает, что файл находится в текущем каталоге

(если полный путь не задан). Если в текущем каталоге файла нет, функция не

станет искать его в других каталогах, и на этом все закончится.

Но даже при указанном в IpszApplicationName имени файла CreateProcess

все равно передает новому процессу содержимое параметра IpszCommandLine

как командную строку. Допустим, Вы вызвали CreateProcess так:

CreateProcess(

"С:\\WINNT\\SYSTEM32\\NOTEPAD.EXE",

"WORDPAD README.TXT");

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

«WORDPAD README.TXT». Странно, да? Но так уж она работает, эта

CreateProcess. Упомянутая возможность, которую обеспечивает параметр

IpszApplicationName, на самом деле введена в CreateProcess для поддержки

подсистемы POSIX операционной системы Windows NT.

Page 10: Лабораторная работа № 1

- 10 -

Параметры 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, система

передает процессу С все наследуемые описатели. В этом случаи наследуется и

Page 11: Лабораторная работа № 1

- 11 -

описатель объекта ядра «процесс» процесса В. А вот описатель объекта «пер-

вичный поток» процесса В не наследуется ни при каком значении

finheritHandles. Кроме того, если процесс A вызывает CreateProcess, передавая

через параметр finheritHandles значение FALSE, процесс С не наследует

никаких описателей, используемых в данный момент процессом А.

Рис. 3-2. “Пример, иллюстрирующий наследование описателей

объектов ядра”INHERIT.C

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstExePrev,

LPSTR lpszCmdLine, int nCmdShow)

{

STARTUPINFO si;

SECURITY_ATTRIBUTES saProcess, saThread;

PROCESS_INFORMATION piProcessB, piProcessC;

// готовим структуру STARTUPINFO для порождаемых

// процессов ZeroMemory(&si, sizeof(si));

si.cb = sizeof(si);

// готовимся к созданию процесса В из процесса А;

// описатель, идентифицирующий новый объект "процесс",

// должен быть наследуемым

saProcess.nLength = sizeof(saProcess);

saProcess.lpSecurityDescriptor = NULL;

saProcess.bInheritHandle = TRUE;

// описатель, идентифицирующий новый объект "поток",

// НЕ должен быть наследуемым

saThread.nLength = sizeof(saThread);

saThread.IpSecurityDescriptor = NULL;

saThread.bInheritHandle = FALSE;

Page 12: Лабораторная работа № 1

- 12 -

// порождаем процесс В

CreateProcess(NULL, "ProcessB", &saProcess, &saThread, FALSE, 0, NULL,

NULL, &si, &piProcessB);

// Структура pi содержит два описателя, относящиеся к процессу А;

// hProcess, который идентифицирует объект "процесс" процесса В

// и является наследуемым, и hThread, который идентифицирует объект

// "первичный поток" процесса В и НЕ является наследуемым.

// Готовимся создать процесс С из процесса А.

// Так как в IpsaProcess и IpsaThread передаются NULL, описатели

// объектов "процесс" и "первичный поток" процесса С считаются

// ненаследуемыми по умолчанию.

// Если процесс А создаст еще один процесс, тот НЕ унаследует

// описатели объектов "процесс" и "первичный поток" процесса С.

// Поскольку в параметре flnheritHandles передается TRUE,

// процесс С унаследует описатель, идентифицирующий объект

// "процесс" процесса В, но НЕ описатель, идентифицирующий объект

// "первичный поток" того же процесса.

CreateProcess(NULL, "ProcessC", NULL, NULL. TRUE, 0. NULL,

NULL, &si, &piProcessC);

return(0);

}

Параметр fdwCreate

Параметр fdwCreate определяет флаги, влияющие на то, как именно

создается новый процесс. Несколько флагов комбинируются булевым

оператором OR.

Флаг DEBUG_PROCESS позволяет родительскому процессу проводить

отладку дочернего, а также всех процессов, которые последним могут быть

порождены. Если этот флаг установлен, система уведомляет родительский

Page 13: Лабораторная работа № 1

- 13 -

процесс (он теперь получает статус отладчика) о возникновении

определенных событий в любом из дочерних процессов (а они получают

статус отлаживаемых).

Флаг DEBUG_ONLY_THIS_PROCESS аналогичен флагу

DEBUG_PROCESS с тем исключением, что заставляет систему уведомлять

родительский процесс о возникновении специфических событий только в

одном дочернем процессе — его прямом потомке. И тогда, если дочерний

процесс создаст ряд дополнительных, отладчик уже не уведомляется о

событиях, «происходящих» в них.

Флаг CREATE_SUSPENDED позволяет создать процесс и в то же время

приостановить его первичный поток. Этим флагом обычно пользуются

отладчики. Получив команду загрузить отлаживаемую программу, отладчик

должен сообщить системе, чтобы та инициализировала новый процесс и его

первичный поток, но исполнение этого потока пока задержала. Благодаря

этому флагу пользователь, проводя отладку приложения, может, расставив по

всей программе точки прерывания, разрешить перехват определенных

событий, а затем дать отладчику команду приступить к исполнению

первичного потока.

Флаг DETACHED_PROCESS блокирует доступ процессу,

активизированному консольной программой, к созданному родительским

процессом окну и сообщает, что вывод следует перенаправить в новое окно.

Если процесс этого типа создается другим процессом, то — по умолчанию —

новый будет использовать окно родительского процесса. Так вот, этот флаг

заставляет новый процесс перенаправлять свой вывоз в новое консольное

окно.

Флаг CREATE_NEW_CONSOLE приводит к созданию нового

консольного окна для нового процесса. Имейте в виду: одновременная

установка флагов CREATE_NEW_CONSOLE и DETACHED_PROCESS

недопустима.

Page 14: Лабораторная работа № 1

- 14 -

Флаг 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-

Page 15: Лабораторная работа № 1

- 15 -

разрядные 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-сим волов.

При создании процесса можно задать и класс его приоритета. Однако это

необязательно и даже, как правило, не рекомендуется; система присваивает

новому процессу класс приоритета по умолчанию. Вот какие классы

приоритета существуют:

Класс приоритета Флаговый идентификатор

Idle (простаивающим)

Normal (обычный)

High (высокий)

Realtime (реального времени)

IDLE__PRIORITY_CLASS

NORMAL_PRIORITY_CLASS

HIGH_PRIORITY_CLASS

REALTIME_PRIORITY_CLASS

Классы приоритета влияют на распределение процессорного времени

между процессами и их потоками.

Параметр lpvEnvironment

Этот параметр указывает на блок памяти, хранящий строки переменных

окружения, которыми будет пользоваться новый процесс. Обычно вместо

lpvEnvironment передается NULL, в результате чего дочерний процесс

Page 16: Лабораторная работа № 1

- 16 -

наследует строки переменных окружения от родительского процесса. В

качестве альтернативы можно вызвать функцию GetEnvironmentStrings:

LPVOID GetEnvironmentStrings(VOID);

Она позволяет узнать адрес блока памяти со строками переменных

окружения, используемых вызывающим процессом. Полученный адрес можно

занести в параметр lpvEnvironment функции CreateProcess. (Именно это и

делает CreateProcess, если Вы передаете ей NULL вместо IpvEnvironment)

Параметр 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;

Page 17: Лабораторная работа № 1

- 17 -

DWORD dwFlags;

WORD wShowWindow;

WORD cbReserved2;

LPBYTE lpReserved2:

HANDLE hStdInput;

HANDLE hStdOutput;

HANDLE hStdError:

} STARTUPINFO, *LPSTARTUPINFO;

Элементы структуры STARTUPINFO используются Win32-функциями

при создании нового процесса. Надо сказать, большинство приложений

порождают процессы с атрибутами по умолчанию. Но и в этом случае надо

как минимум инициализировать все элементы структуры STARTUPINFO

нулевыми значениями, а в элемент cb — занести размер этой структуры:

STARTUPINFO si;

ZeroMemory(&si, sizeof(si));

si.cb = sizeof(si);

CreateProcess(..., &si, . . . ) ;

К сожалению, разработчики приложений об этом часто забывают. И,

кстати, если Вы хотите изменить какие-то элементы структуры, делайте это

перед вызовом CreateProcess.

Все элементы этой структуры мы подробно рассмотрим в таблице на рис.

3-3. Но заметьте: некоторые элементы имеют смысл, только если дочернее

приложение создает перекрываемое (overlapped) окно, а другие — если это

приложение осуществляет ввод/вывод на консоль.

Page 18: Лабораторная работа № 1

- 18 -

Рис. 3-3. “Элементы структуры STARTUPINFO”

ЭлементОкно или

консольОписание

cb то и другое

Содержит количество байтов, занимаемых структурой

STARTUPINFO. Служит для контроля версии — на тот

случай, если Microsoft расширит эту структуру в будущем

Win32. Программа должна инициализировать cb как

sizeof(STARTUPINFO).

IpReserved то и другое Зарезервирован. Инициализируйте как NULL.

IpDesktop то и другое

Идентифицирует имя рабочего стола, на котором

запускается приложение. Если указанный рабочий стол

существует, новый процесс сразу же связывается с ним. В

ином случае система сначала создаст рабочий стол с

атрибутами по умолчанию, присваивает ему имя,

указанное в данном элементе структуры, и связывает его

с новым процессом. Если IpDesktop равен NULL (что

чаще всего и бывает), процесс связывается с текущим

рабочим столом.

IpTitle консольОпределяет заголовок консольного окна. Если IpTitle —

NULL, в заголовок выводится имя исполняемого файла.

dwX

dwYто и другое

Указывают х- и у-координаты (в пикселях) области

экрана, в которой размещается окно приложения. Эти

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

создает свое первое перекрываемое окно с

идентификатором CW_USEDEFAULT в параметре х

функции CreateWindow. В приложениях, создающих

консольные окна, данные элементы определяют верхний

левый угол консольного окна.

dwXSize

dwYSizeто и другое

Определяют ширину и высоту (в пикселах) окна

приложения. Эти значения используются, только если

дочерний процесс создает свое первое перекрываемое

окно с идентификатором CW_USEDEFAULT в параметре

nWidth функции CreateWindow. В приложениях,

создающих консольные окна, данные элементы

Page 19: Лабораторная работа № 1

- 19 -

определяют ширину и высоту консольного окна.

dwXCountChars

dwYCountChars

консоль Определяют ширину и высоту (в символах) консольных

окон дочернего процесса.

dwFillAttribute консольЗадает цвет текста и фона в консольных окнах дочернего

процесса.

dwFlags то и другое См. ниже и следующую таблицу.

wShowWindow окно

Определяет, как именно должно выглядеть первое

перекрываемое окно дочернего процесса, если

приложение при первом вызове функции ShowWindow

передает в параметре nCmdShow идентификатор

SW_SHOWDEFAULT. В этот элемент можно записать

любой из идентификаторов типа SW_*, обычно

допустимых при вызове ShowWindow.

cbReserved2 то и другое Зарезервирован. Инициализируйте как 0.

lpReserved2 то и другое Зарезервирован. Инициализируйте как NULL.

hStdInput

hStdOutput

IiStdError

консоль

Определяют описатели буферов для консольного

ввода/вывода. По умолчанию hStdInput идентифицирует

буфер клавиатуры, a hStdOutput и hStdError — буфер

консольного окна.

Теперь обсудим элемент dwFlags. Он содержит набор флагов, позво-

ляющих управлять созданием дочернего процесса. Большая часть флагов

просто сообщает функции CreateProcess, содержат ли прочие элементы

структуры STARTUPINFO полезную информацию или некоторые из них

можно игнорировать. В таблице приведен список допустимых флагов и

описано их назначение:

Page 20: Лабораторная работа № 1

- 20 -

Флаг Описание

STARTF_USESIZE

STARTF_USESHOWWINDOW

STARTF_USEPOSITION

STARTF_USECOUNTCHARS

STARTF_USEFILLATTRIBUTE

STARTF_USESTDHANDLE

Заставляет использовать элементы dwXSize и dwYSize.

Заставляет использовать элемент wShowWindow.

Заставляет использовать элементы dwX и dwY.

Заставляет использовать элементы dwXCountChars и

dwYCountChars.

Заставляет использовать элемент dwFillAttribute.

Заставляет использовать элементы hStdInput,

hStdOutput и hStdError.

Два дополнительных флага — STARTF_FORCEONFEEDBACK и

STARTF_FORCE-OFFFEEDBACK — позволяют контролировать форму

курсора мыши в момент запуска нового процесса. Поскольку Windows 95 и

Windows NT поддерживают истинную вытесняющую многозадачность, можно

запустить одно приложение и, пока оно инициализируется, поработать с

другой программой. Для визуальной обратной связи с пользователем функция

CreateProcess временно изменяет форму системного курсора мыши: .

Курсор такой формы подсказывает: можно либо подождать чего-то, что

вот-вот случится, либо продолжить работу в системе. В самых первых бета-

версиях Windows NT такого курсора не было — CreateProcess вообще не

меняла форму курсора. Это несколько сбивало с толку. Поэтому теперь в

CreateProcess предусмотрена возможность управления формой курсора при

запуске процесса. Если же Вы укажете флаг STARTF_FORCEOFFFEEDBACK,

CreateProcess не станет трансформировать курсор в «песочные часы» —

останется стандартная стрелка.

Флаг STARTF_FORCEONFEEDBACK, напротив, заставляет

CreateProcess отслеживать инициализацию нового процесса и в зависимости

от результата проверки изменять форму курсора. Когда CreateProcess

вызывается с этим флагом, курсор преобразуется в «песочные часы». Если

Page 21: Лабораторная работа № 1

- 21 -

спустя 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. Выполнение программы — экранной заставки обычно завершается,

когда пользователь начинает работать с каким-нибудь

Page 22: Лабораторная работа № 1

- 22 -

приложением. Ведь последнее, скорее всего, имеет нормальный

приоритет, а это могло бы привести к повторному вытеснению

потоков в программе — экранной заставке, и в результате ее не

удалось бы завершить.

В заключение раздела — несколько слов об элементе wShowWindow

структуры STARTUPINFO. Этот элемент инициализируется значением,

которое Вы передаете в WinMain через ее последний параметр — nCmdShow.

Он позволяет указать, в каком виде должно появиться основное окно Вашего

приложения. В качестве значения используется один из идентификаторов,

обычно передаваемых в ShowWindow (чаще всего SW_SHOWNORMAL или

SW_SHOWMINNOACTIVE, но иногда и SW_SHOWDEFAULT).

После запуска программы двойным щелчком из Explorer ее функция

WinMain вызывается с SWSHOWNORMAL в параметре nCmdShow. Если же

программа запускается двойным щелчком при нажатой клавише Shift, в этом

параметре передается идентификатор SW_SHOWMINNOACTIVE. Благодаря

этому пользователь может легко выбрать, в каком окне запустить программу

— нормальном или свернутом.

Наконец, чтобы получить копию структуры STARTUPINFO,

инициализированной родительским процессом, приложение может вызвать:

VOID GetStartupInfo (LPSTARTUPINFO lpStartupInfo) ;

Анализируя эту структуру, дочерний процесс способен изменять свое

поведение в зависимости от значений ее элементов. Но учтите: хотя в

документации на Win32 об этом четко не сказано, перед вызовом

GetStartupInfo нужно инициализировать элемент сb структуры

STARTUPINFO:

STARTUPINFO si;

si.cb = sizeof(si);

GetStartupInfo(&si);

Page 23: Лабораторная работа № 1

- 23 -

Параметр lppiProcInfo

Параметр ippiProcInfo указывает на структуру

PROCESS_INFORMATION, которую Вы должны предварительно создать; ее

элементы инициализируются самой функцией CreateProcess. Структура

представляет собой следующее:

typedef struct _PROCESS_INFORMATION{

HANDLE hProcess;

HANDLE hThread;

DWORD dwProcessId;

DWORD dwThreadld;

} PROCESS_INFORMATION;

Cоздание нового процесса влечет за собой и создание объектов ядра

«процесс» и «поток». В момент создания система присваивает счетчику

каждого объекта начальное значение — 1. Далее CreateProcess (перед самым

возвратом управления) открывает объекты «процесс» и «поток» и заносит их

описатели, специфичные для данного процесса, в элементы hProcess и hThread

структуры PROCESS_INFORMATION. Когда CreateProcess открывает эти

объекты, счетчики каждого из них увеличиваются до 2.

Это означает, что, перед тем как система сможет высвободить из памяти

объект «процесс», процесс должен быть завершен (счетчик уменьшается до 1),

а родительский процесс — вызвать функцию CloseHandle (и тем самым

обнулить счетчик). То же самое относится и к объекту «поток»: поток должен

быть завершен, а родительский процесс должен закрыть описатель объекта

«поток».

Не забывайте закрывать упомянутые описатели. Пропуск этой операции

одна из самых частых ошибок; она приводит к утечке системной памяти

вплоть до завершения процесса, вызвавшего CreateProcess.

Page 24: Лабораторная работа № 1

- 24 -

Созданному процессу присваивается уникальный идентификатор; ни у

каких процессов, выполняемых в системе, не может быть одинаковых

идентификаторов. То же касается и потоков. Завершая свою работу,

CreateProcess заносит значения идентификаторов в элементы dwProcessId и

dwThreadId структуры PROCESS_INFORMATION. Используя их, роди-

тельский процесс может обращаться к дочернему.

Подчеркнем еще один чрезвычайно важный момент: система способна

повторно использовать идентификаторы процессов и потоков. Например, при

создании процесса система формирует объект «процесс», присваивая ему

идентификатор со значением, допустим, 0x22222222. Создавая новый объект

«процесс», система уже не присвоит ему данный идентификатор. Но после

выгрузки из памяти первого объекта следующему создаваемому объекту

«процесс» может быть присвоен тот же идентификатор — 0x22222222.

Эту особенность нужно учитывать при написании кода, избегая ссылок на

неверный объект «процесс» (или «поток»). Действительно, затребовать и

сохранить идентификатор процесса несложно, но задумайтесь, что получится,

если в следующий момент этот процесс будет завершен, а новый получит тот

же идентификатор: сохраненный ранее идентификатор уже связан совсем с

другим процессом.

Чтобы избавиться от подобных неприятностей, доступ к объекту

«процесс» нужно блокировать. Иначе говоря, Вы должны обязательно

увеличивать значение счетчика, связанного с объектом «процесс», — ведь

система никогда не выгрузит из памяти объект, счетчик которого отличен от

нуля. Впрочем, в большинстве случаев счетчик увеличивается и без Вашего

участия, как, например, после вызова CreateProcess.

Зная, что значение счетчика выше нуля, можно свободно оперировать

идентификатором процесса. А когда необходимость в нем отпадет, вызовите

CloseHandle — чтобы уменьшить счетчик объекта «процесс». Затем

удостоверьтесь, что этот идентификатор больше нигде не используется.

Page 25: Лабораторная работа № 1

- 25 -

Ø Завершение процесса

Процесс можно завершить тремя способами:

1. Один из потоков процесса вызывает функцию ExitProcess (самый

распространенный способ).

2. Поток другого процесса вызывает функцию TerminateProcess (этого

надо избегать).

3. Все потоки процесса «умирают» по своей «воле» (большая

редкость).

Функция ExitProcess

Процесс завершается, когда один из его потоков вызывает ExitProcess:

VOID ExitProcess(UINT fuExitCode);

Эта функция завершает процесс и заносит в параметр fuExitCode код

завершения процесса. Возвращаемого значения у ExitProcess нет, так как

результат ее действия — завершение процесса. Если за вызовом этой функции

в программе присутствует какой-нибудь код, он никогда не исполняется.

Вызов ExitProcess — самый распространенный способ завершения

процесса, поскольку она (функция) вызывается автоматически в тот момент,

когда WinMain в Вашей программе возвращает управление стартовому коду из

стандартной библиотеки С. Стартовый код обращается к ExitProcess,

передавая ей значение, возвращенное WinMain. При завершении процесса

прекращается выполнение и всех его потоков.

Кстати, в документации на Win32 утверждается, что процесс не

завершается до тех пор, пока не завершится выполнение всех его потоков.

Это, конечно, верно, но тут есть одна тонкость. Стартовый код из стандартной

библиотеки С обеспечивает завершение процесса, вызывая ExitProcess после

того, как первичный поток Вашего приложения возвращается из функции

WinMain. Однако, вызвав из WinMain функцию ExitThread (вместо того, чтобы

Page 26: Лабораторная работа № 1

- 26 -

вызвать ExitProcess или просто вернуть управление), Вы завершите

первичный поток, но не сам процесс — если в нем еще выполняется какой-то

другой поток (или потоки).

Функция TerminateProcess

Вызов TerminateProcess тоже завершает процесс:

BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode);

Главное отличие этой функции от ExitProcess в том, что ее может вызвать

любой поток и завершить любой процесс. Параметр hProcess идентифицирует

описатель завершаемого процесса, а в параметр fuExitCode помещается код

завершения процесса.

Пользоваться TerminateProcess вообще-то не рекомендуется; к ней можно

прибегнуть лишь в том случае, когда иным способом процесс завершить не

удается. При нормальном ходе вещей система уведомляет о завершении

процесса все связанные с ним DLL-модули. Однако этого не происходит, если

Вы обращаетесь к TerminateProcess, — а значит, не исключено некорректное

завершение процесса. Например, Ваша программа может задействовать DLL-

модуль, который при отключении от процесса сбрасывает данные из какого-то

буфера в дисковый файл. В обычных условиях отключение DLL происходит

при его выгрузке с помощью функции FreeLibrary. Ну а поскольку при об-

ращении к TerminateProcess DLL-модуль об отключении не уведомляется, он

не сможет выполнить своей задачи. Так что система сообщает DLL-модулям о

завершении процесса только при нормальном его прекращении или при

вызове ExitProcess.

Несмотря на все сказанное, надо заметить, что при любом способе

завершения процесса система гарантирует освобождение занятой процессом

памяти и объектов User или GDI, а также закрытие всех открытых файлов и

уменьшение счетчиков объектов ядра.

Page 27: Лабораторная работа № 1

- 27 -

Если все потоки процесса «уходят»

В такой ситуации (а она может возникнуть, если все потоки вызвали

ExitThread или их закрыли вызовом TerminateThread) операционная система

больше не считает нужным «содержать» адресное пространство данного

процесса. Обнаружив, что в процессе не исполняется ни один поток, она

немедленно завершает его. При этом код завершения процесса приравнивается

коду завершения последнего потока.

Что происходит при завершении процесса

А происходит вот что:

1. Выполнение всех потоков в процессе прекращается.

2. Все объекты User и GDI, созданные процессом, уничтожаются, а

объекты ядра закрываются.

3. Объект ядра «процесс» переходит в свободное, или незанятое

(signaled), состояние. Прочие потоки в системе могут приостановить

свое выполнение вплоть до завершения данного процесса.

4. Код завершения процесса меняется со значения STILL_ACTIVE на

код, переданный в ExitProcess или TerminateProcess.

5. Счетчик объекта ядра «процесс» уменьшается на 1.

Связанный с завершаемым процессом объект ядра «процесс» не

высвобождается, пока не будут закрыты все ссылки на него. Кроме того,

закрытие процесса не приводит к автоматическому завершению порожденных

им процессов.

По завершении процесса его код и выделенные ему ресурсы удаляются из

памяти. Однако закрытая память, выделенная системой для объекта ядра

«процесс», не освобождается, пока счетчик числа его пользователей не

достигнет нуля. А это произойдет, только если все прочие процессы,

создавшие или открывшие описатели для «ныне покойного» процесса,

уведомят систему (вызовом CloseHandle) о том, что ссылки на этот процесс им

больше не нужны.

Page 28: Лабораторная работа № 1

- 28 -

Описатели завершенного процессса уже мало на что пригодны. Разве что

родительский процесс, вызвав GetExitCodeProcess, может проверить, завершен

ли процесс, идентифицируемый параметром hProcess, и, если да, определить

код завершения:

BOOL GetExitCodeProcess(HANDLE hProcess, LPDWORD IpdwExitCode);

Код завершения возвращается как DWORD, на которое указывает

IpdwExitCode. Если на момент вызова GetExitCodeProcess процесс еще не

завершился, в DWORD заносится идентификатор STILL_ACTIVE

(определенный как 0x103). Если функция выполнена успешно, она возвращает

TRUE.

Page 29: Лабораторная работа № 1

- 29 -

Ø Дочерние процессы

При разработке приложения часто бывает нужно, чтобы какую-то

операцию выполнял внешний блок кода. Поэтому — хочешь, не хочешь —

приходится постоянно вызывать функции или подпрограммы. Но вызов

функции приводит к приостановке выполнения основного кода Вашей

программы до возврата из вызванной функции.

Альтернативный способ — передать выполнение какой-то операции

другому потоку в пределах данного процесса (поток, разумеется, нужно

сначала создать). Это позволит основному коду программы продолжить

работу, в то время как дополнительный поток будет выполнять другую

операцию. Прием весьма удобный, но, когда основному потоку потребуется

узнать результаты работы другого потока, Вам не избежать проблем,

связанных с синхронизацией.

Есть еще один прием: Ваш процесс порождает дочерний и возлагает на

него выполнение части операций. Будем считать, что эти операции очень

сложны. Допустим, для их реализации Вы решили просто создать новый поток

внутри того же процесса. Вы написали тот или иной код, проверили его и

получили некорректный результат — может, ошиблись в алгоритме или

запутались в ссылках и случайно перезаписали какие-нибудь важные данные в

адресном пространстве своего процесса. Так вот, один из способов защитить

адресное пространство основного процесса от подобных ошибок как раз и

состоит в том, чтобы передать часть работы отдельному процессу. Далее

можно или подождать, пока он завершится, или продолжить работу

параллельно с ним.

К сожалению, дочернему процессу, по-видимому, придется оперировать с

данными, содержащимися в адресном пространстве родительского процесса.

Было бы неплохо, чтобы он работал в собственном адресном пространстве, а в

«Вашем» — просто считывал нужные ему данные; тогда он не сможет что-то

испортить в адресном пространстве родительского процесса. В Win32

предусмотрено несколько способов обмена данными между процессами: DDE

Page 30: Лабораторная работа № 1

- 30 -

(Dynamic Data Exchange — динамический обмен данными), OLE (Object

Linking and Embedding — связывание и внедрение объектов), каналы (pipes),

почтовые ящики (mailslots) и т. д. А один из самых удобных способов,

обеспечивающих совместный доступ к данным, — использование файлов,

проецируемых в память (memory-mapped files).

Если Вы хотите создать новый процесс, заставить его выполнить какие-

либо операции и дождаться их результатов, напишите примерно такой код:

PROCESS_INFORMATION pi;

DWORD dwExitCode;

BOOL fSuccess = CreateProcess(…, &pi);

if(fSuccess){

// закрывайте описатель потока, как только необходимость в нем отпадает!

CloseHandle(pi.hihread);

WaitForSingleObject(pi.hProcess, INFINITE);

// процесс завершился

GetExitCodeProcess(pi.hProcess, &dwExitCode);

// закрывайте описатель процесса, как только необходимость в нем отпадает!

CloseHandle(pi.hProcess);

}

В этом фрагменте кода мы создали новый процесс и, если это прошло

успешно, вызвали WaitForSingleObject:

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeOut);

Функция задерживает выполнение кода пока объект, определяемый

параметром hObject, не перейдет в свободное (незанятое) состояние. (Объекты

«процесс» переходят в такое состояние при завершении процесса.) Итак,

Page 31: Лабораторная работа № 1

- 31 -

вызов WaitForSingleObject приостанавливает выполнение потока

родительского процесса до завершения порожденного им процесса. Когда

WaitForSingleObject вернет управление, Вы узнаете код завершения дочернего

процесса через функцию GetExitCodeProcess.

Обращение к CloseHandle в приведенном выше фрагменте кода

заставляет систему уменьшить значения счетчиков объектов «поток» и

«процесс» до нуля и тем самым освободить память, занимаемую этими

объектами.

Вы, наверное, заметили, что в этом фрагменте я закрыл описатель объекта

ядра «первичный поток» (принадлежащий дочернему процессу) сразу после

возврата из CreateProcess. Это не приводит к завершению первичного потока

дочернего процесса — просто уменьшает счетчик, связанный с упомянутым

объектом. А вот почему это делается — и, кстати, даже рекомендуется делать

— именно так, станет ясно из простого примера. Допустим, первичный поток

дочернего процесса порождает еще один поток, а сам после этого завершается.

В этот момент система может высвободить объект «первичный поток»

дочернего процесса из памяти, если у родительского процесса нет описателя

данного объекта. Но если родительский процесс располагает таким

описателем, система не сможет удалить этот объект из памяти до тех пор, пока

и родительский процесс не закроет его описатель.

Запуск обособленных дочерних процессов

Что ни говори, но чаще приложение все-таки создает другие процессы как

обособленные (detached processes). Это значит, что после создания и запуска

нового процесса родительскому процессу нет нужды с ним «общаться» или

ждать, пока тот закончит работу. Именно так и действует Explorer: запускает

для пользователя новые процессы, а дальше его уже «не волнует», что там с

ними происходит.

Чтобы обрубить все пуповины, связывающие Explorer с дочерним

процессом, ему нужно (вызовом CloseHandle) закрыть свои описатели,

Page 32: Лабораторная работа № 1

- 32 -

связанные с новым процессом и его первичным потоком. Приведенный ниже

фрагмент кода подскажет Вам, как, создав процесс, сделать его

обособленным:

PROCESS_INFORMATION pi;

BOOL fSuccess = CreateProcess(.... &pi):

if (fSuccess) {

CloseHandle(pi.hThread);

CloseHandle(pi.hProcess);

}

Page 33: Лабораторная работа № 1

- 33 -

· ПОТОКИ

В каких случаях потоки создаются

Поток (thread) определяет последовательность исполнения кода в

процессе. При инициализации процесса система всегда создает первичный

поток. Начинаясь со стартового кода из стандартной библиотеки С (который в

свою очередь вызывает функцию WinMain из Вашей программы), он «живет»

до того момента, когда WinMain возвращает управление стартовому коду и тот

вызывает функцию ExitProcess. Большинство приложений обходится един-

ственным, первичным потоком. Однако процессы могут создавать

дополнительные потоки, что позволяет добиваться минимального простоя

процессора и работать эффективнее.

Например, в электронных таблицах нужно пересчитывать данные при

изменении пользователем содержимого ячеек. Пересчет сложной таблицы

может занять несколько секунд, но тщательно продуманное приложение не

должно тратить время на эту операцию после каждого изменения. Вместо

этого следует выделить функциональный блок повторных расчетов в

отдельный поток с более низким (чем у первичного) приоритетом. Таким

образом, пока пользователь набирает данные, исполняется первичный поток,

т. е. система не выделяет процессорного времени потоку, отвечающему за

пересчет. А возникнет пауза — даже крошечная, — система приостановит

выполнение первичного потока, ожидающего ввода данных, и отдаст

процессорное время другому потоку (в нашем случае — блоку повторных

расчетов). При возобновлении ввода данных первичный поток, имеющий

более высокий приоритет, вытеснит поток, занимающийся пересчетом.

Создание дополнительного потока делает программу «отзывчивой» на

действия пользователя — да и реализация такого алгоритма довольно проста.

С той же целью можно создать дополнительный поток и в текстовом

процессоре для фоновой разбивки документа на страницы в паузах между

вводом текста. Например, в 16-разрядной Windows текстовому процессору

Microsoft Word приходится моделировать многопоточность, но в версии для

Page 34: Лабораторная работа № 1

- 34 -

Win32 он просто порождает вспомогательный поток для разбивки документа

на страницы. Первичный поток отвечает за обработку информации, вводимой

пользователем, а фоновый — за определение концов страниц.

Полезно создать отдельный поток и для обработки заданий на печать.

Тогда пользователь, отправив документ на печать, продолжил бы работу с

программой.

Еще пример. При выполнении длительной операции программы обычно

открывают диалоговое окно, позволяющее эту операцию отменять. Скажем,

при копировании файлов Explorer выводит на экран диалоговое окно, где,

кроме индикатора прогресса операции, содержится и кнопка Cancel (Отмена).

Щелкнув ее, Вы отмените копирование файла (или файлов).

В 16-разрядной Windows для этого приходилось из цикла File Copy

периодически вызывать функцию PeekMessage, но делать это можно было

только в паузах между чтением и записью. При считывании большого блока

данных реакция программы на «нажатие» кнопки Cancel была слишком

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

Из-за такого запаздывания пользователь, полагая, что система почему-то не

«поняла» его, мог несколько раз щелкнуть.

Теперь представьте, что код, отвечающий за копирование файлов,

выделен в свой поток. Вам больше не надо расставлять по всему коду вызовы

PeekMessage — поток, обеспечивающий работу пользовательского

интерфейса, действует независимо. А значит, щелчок кнопки Cancel даст

немедленный результат.

На основе принципа многопоточности можно также создавать

программы, моделирующие события реальной жизни. Рассмотрим модель

супермаркета. Каждый покупатель представлен в ней отдельным потоком, так

что теоретически все они независимы друг от друга и входят, покупают и

выходят тогда и как им угодно.

Хотя подобную модель, в общем-то, можно реализовать подобным

образом, здесь нас подстерегает ряд потенциальных проблем. Во-первых, в

Page 35: Лабораторная работа № 1

- 35 -

идеале надо бы выполнять каждый поток (соответствующий одному

покупателю) на отдельном процессоре. Но сами понимаете, это совершенно

нереально, поэтому при моделировании нужно учитывать время, затрачива-

емое системой на вытеснение первого потока и активизацию второго.

Например, если в модели всего 2 потока, а у компьютера 8 процессоров,

система сможет закрепить каждый поток за отдельным процессором. Если же

в модели 1000 потоков, системе придется постоянно «коммутировать» их

между 8 процессорами. Так что при распределении большого количества

потоков между несколькими процессорами станет заметным время, требуемое

на переключение потоков. Если моделируется продолжительный процесс, этот

эффект проявляется относительно слабо. Но при моделировании

быстротечных процессов перераспределение потоков может занять едва ли не

львиную долю времени выполнения всей программы.

Во-вторых, операционной системе самой нужны потоки, исполняемые

«вместе» с потоками, принадлежащими программам. Значит, надо учитывать и

время, затрачиваемое на переключение этих дополнительных потоков; оно

почти наверняка повлияет на общие результаты.

И, в-третьих, моделировать имеет смысл, только если Вы периодически

фиксируете какие-то параметры процесса в его развитии. Скажем, в модели

супермаркета каждый покупатель, входя в магазин, регистрируется в списке, а

внесение элемента в список тоже занимает время (отнимая его у собственно

моделируемого процесса). Вспомните, что, согласно принципу

неопределенности Гейзенберга, чем точнее определяется один квант, тем

больше ошибка при измерении другого. Это в полной мере относится и к

нашим рассуждениям.

В каких случаях потоки не создаются

Ох как часто программисты, впервые получая доступ к среде,

поддерживающей многопоточность, впадают чуть ли не в исступление: «Если

бы я раньше мог работать с потоками, насколько проще было бы писать

Page 36: Лабораторная работа № 1

- 36 -

программы!» И по какой-то, непонятной мне причине они начинают дробить

свои программы на куски, которые можно было бы исполнять как отдельные

потоки. Но...

Потоки — вещь невероятно полезная, когда ими пользуются с умом. Увы,

решая старые проблемы, можно создать себе новые. Допустим, Вы

разрабатываете текстовый процессор и хотите выделить функциональный

блок, отвечающий за распечатку, в отдельный поток. Идея вроде неплоха:

пользователь, отправив документ на распечатку, может сразу вернуться к

редактированию. Но задумайтесь вот над чем: значит, информация в

документе может быть изменена при распечатке документа? Как видите,

теперь перед Вами совершенно новая проблема, с которой прежде

сталкиваться не приходилось. Тут-то и подумаешь, а стоит ли выделять печать

в отдельный поток, зачем искать лишних приключений? А что если при

распечатке разрешить редактирование любых документов, кроме того, что в

данный момент печатается, — иными словами, запретим изменение

печатаемого документа. Или так: скопируем документ во временный файл и

отправим на печать именно его, а пользователь пусть редактирует оригинал в

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

удалим — вот и все.

Еще одно «узкое» место, где неправильное применение потоков может

привести к появлению проблем, — разработка пользовательского интерфейса

в приложении. В большинстве программ все компоненты пользовательского

интерфейса (окна) формируются в одном потоке. Например, если Вы создаете

диалоговое окно, какой смысл формировать список одним потоком, а кнопку

— другим?

Рассмотрим эту проблему подробнее и предположим, что в программе

имеется элемент управления — список, сортирующий данные всякий раз, как

в него что-то добавляют (или удаляют). Сортировка может занять несколько

секунд, и поэтому Вы, допустим, выделили этот элемент управления в

отдельный поток. Тогда пользователь вроде бы может работать с другими

Page 37: Лабораторная работа № 1

- 37 -

элементами управления, пока поток, принадлежащий списку, занят

сортировкой.

Но эта идея — не из лучших. Во-первых, каждый поток, создающий то

или иное окно, должен содержать в себе цикл GetMessage. Во-вторых, если

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

столкнетесь с проблемами синхронизации потоков. Их в принципе можно

решить, закрепив за списком специальный поток, который только и делает,

что сортирует элементы в фоновом режиме.

Закрепление объектов пользовательского интерфейса за отдельными

потоками редко приносит хоть какую-то пользу. Каждый процесс в системе

управляет своим интерфейсом с помощью отдельного потока. Скажем, у

приложения Calculator свой поток, который создает и манипулирует всеми

окнами этой программы, а у приложения Paint — свой, с аналогичными

функциями. Такая схема обладает наибольшей отказоустойчивостью. Если

поток калькулятора войдет в бесконечный цикл, это не скажется на потоке

Paint. Разительное отличие от 16-разрядной Windows, не так ли? В ней, если

виснет одно приложение, виснет вся система. А системы, построенные на

основе Win32, позволяют переключиться из зависшего приложения Calculator

и перейти в тот же Paint.

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

потоков, — Explorer. Если Вы используете одно окно Explorer и поток для

этого окна входит в бесконечный цикл, оно становится «недееспособным», но

другие окна Explorer остаются «на плаву». И это его свойство очень важно,

потому что пользователи терпеть не могут, когда оболочка операционной

системы перестает реагировать на их команды.

Еще одно применение многопоточности в компонентах GUI —

приложения с многодокументным интерфейсом (multiple document interface,

MD1), где каждое дочернее MDI-окно поддерживается отдельным потоком.

Если один из таких потоков входит в бесконечный цикл или начинает

выполнять длительную операцию, пользователь может переключиться в

Page 38: Лабораторная работа № 1

- 38 -

другое дочернее MDI-окно И поработать с ним, пока предыдущее выполняет

поставленную задачу. Это настолько удобно, что в Win32 даже есть

специальная функция, дающая результат, аналогичный тому, как если бы Вы

создали дочернее MDI-OKHO, передав сообщение WM_MDICREATE окну

MDIClient:

HWND CreateMDIWindow(LPTSTR IpszClassName, LPTSTR IpszWindowName,

DWORD dwStyle, int x, int y, int nWidth, int nHeight,

HWND hwndParent, HINSTANCE hinst,

LONG IParam);

Отличие лишь в том, что CreateMDIWindow позволяет создавать дочернее

MDI-OKHO вместе со своим потоком.

Многопоточность следует использовать разумно. Не создавайте

несколько потоков только потому, что это возможно. Многие полезные и

мощные программы по-прежнему строятся на основе одного первичного

потока, принадлежащего процессу.

Ваша первая функция потока

Потоки начинают выполнение с некоей, определенной Вами функции. У

нее должен быть такой прототип:

DWORD WINAPI YourThreadFunc(LPVOID lpvThreadParm);

Как и WinMain, эта функция операционной системой не вызывается.

Вместо этого система обращается к внутренней функции, содержащейся в

KERNEL32.DLL (а не в стандартной библиотеке С). Назовем эту функцию

StartOfThread— как она называется на самом деле, значения не имеет. Вот ее

синтаксис:

Page 39: Лабораторная работа № 1

- 39 -

void StartOfThread(LPTHREAD_START_ROUTINE IpStartAddr,

LPVOID lpvThreadParm)

{

__try

{

DWORD dwThreadExitCode = IpStartAddr(lpvThreadPann);

ExitThread(dwThreadExitCode); )

}

__except(UnhandledExceptionFilter (Get Exceptlonlnformation()))

{

ExitProcess(GetExceptionCode());

}

}

К чему приводит вызов StartOfThread?

1. Ваша функция потока помещается во фрейм структурной обработки

исключений (SEH-frame — далее для краткости SEH-фрейм),

благодаря чему любое исключение — произойди оно в момент

выполнения Вашего потока — получит хоть какую-то обработку,

предлагаемую по умолчанию.

2. Система обращается к Вашей функции, передавая ей 32-битный

параметр lpvThreadParm, который Вы ранее передали CreateThread,

3. Когда Ваша функция вернет управление, StartOfThread вызовет

ExitThread, передав ей значение, возвращенное Вашей функцией.

Счетчик числа пользователей объекта ядра «поток» уменьшится, и

выполнение потока прекратится.

4. Если Ваш поток вызовет необрабатываемое им исключение, его

обработает SEH-фрейм, созданный «вокруг» StartOfThread. Обычно

в результате этого появляется окно с каким-нибудь сообщением, и,

когда негодующий пользователь щелкнет в нем кнопку OK,

Page 40: Лабораторная работа № 1

- 40 -

StartOfThread вызовет ExitProcess и завершит весь процесс, а не

только тот поток, в котором произошло исключение.

Исполнение первичного потока процесса на самом деле начинается с

функции StartOfThread. Потом она передает управление стартовому коду из

стандартной библиотеки С, который и вызывает WinMain из Вашего

приложения. Но стартовый код никогда не возвращается в StartOfThread, так

как «под занавес» он явным образом вызывает ExitProcess.

Рассмотрим атрибуты, «присуждаемые» новому потоку.

Стек потока

Каждому потоку выделяется собственный стек в адресном пространстве

процесса. При использовании статических и глобальных переменных не

исключена возможность одновременного обращения к ним из нескольких

потоков, что может повредить значения переменных. С другой стороны,

локальные и автоматические переменные создаются в стеке потока, а значит,

они в гораздо меньшей степени подвержены «вредному влиянию» другого

потока. Поэтому всегда старайтесь при написании функций применять

локальные или автоматические переменные и избегать статических и

глобальных.

Истинный размер стека, принадлежащего потоку, и то, как операционная

система и компилятор управляют стеком, — темы чрезвычайно сложные.

Структура CONTEXT

У каждого потока собственный набор регистров процессора, называемый

контекстом потока. Эта структура с именем CONTEXT отражает состояние

регистров процессора на момент последнего исполнения потока. Она —

единственная структура данных в Win32, зависимая от типа процессора. В

справочном файле по Win32 ее содержимое вообще не показано. Если Вас

интересует, из каких элементов она состоит, загляните в файл WINNT.H —

там Вы найдете несколько ее определений: для х86, для MIPS, для Alpha и для

Page 41: Лабораторная работа № 1

- 41 -

PowerPC. Компилятор сам выбирает нужный вариант структуры в

зависимости от типа процессора, для которого предназначен Ваш ЕХЕ- или

DLL-модуль.

Когда потоку выделяется процессорное время, система инициализирует

регистры процессора содержимым контекста и, разумеется, регистр —

указатель команд идентифицирует адрес следующей машинной команды,

необходимой для выполнения потока. Кроме того, в контекст включается

указатель стека, который определяет адрес стека, принадлежащего потоку.

Page 42: Лабораторная работа № 1

- 42 -

Ø Функция CreateThread

Мы уже говорили, как при вызове CreateProcess появляется на свет

первичный поток процесса. Если же Вы хотите, чтобы первичный поток

породил дополнительные потоки, нужно воспользоваться другой функцией —

CreateThread:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES Ipsa; DWORD cbStack;

LPTHREAD_START__ROUTINE IpStartAddr; LPVOID IpvThreadParm;

DWORD fdwCreate; LPDWORD IpIDThread);

При каждом вызове этой функции система:

1. Создает объект ядра «поток» для идентификации и управления

«новорожденным» потоком. В нем хранится большая часть

системной информации, необходимой для управления потоком.

Описатель этого объекта — значение, возвращаемое функцией

CreateThread.

2. Инициализирует код завершения потока (регистрируемый в объекте

ядра «поток») идентификатором STILL_ACTIVE и присваивает

счетчику простоя потока (thread's suspend count) единицу.

Последний тоже запоминается в объекте ядра «поток».

3. Создает для нового потока структуру CONTEXT.

4. Формирует стек потока, для чего резервирует в адресном

пространстве регион, передает ему 2 страницы физической памяти и

присваивает им атрибут защиты PAGE_READWRITE, а второй

странице (если считать снизу вверх) дополнительно устанавливает

атрибут PAGE_GUARD.

5. Значения IpStartAddr и IpvThreadParm помещаются в самый верх

стека — так, чтобы представить их параметрами, передаваемыми в

StartOfThread.

Page 43: Лабораторная работа № 1

- 43 -

6. Инициализирует регистры — указатель стека и указатель команд в

структуре CONTEXT потока, так чтобы первый указывал на

верхнюю границу стека, а второй — на внутреннюю функцию

StartOfThread.

Теперь подробно рассмотрим все ее параметры.

Параметр Ipsa

Параметр Ipsa является указателем на структуру

SECURITY_ATTRIBUTES. Если Вы хотите, чтобы объекту «поток» были

присвоены атрибуты защиты по умолчанию, передайте в этом параметре

NULL. А чтобы дочерние процессы смогли наследовать описатель данного

объекта «поток», определите структуру SECURITY_ATTRIBUTES и

инициализируйте ее элемент blnheritHandle значением TRUE.

Параметр cbStack

Этот параметр определяет, какую часть адресного пространства поток

сможет использовать под свой стек. Каждому потоку выделяется отдельный

стек. CreateProcess, запуская приложение, вызывает функцию CreateThread, и

та инициализирует первичный поток процесса. При этом CreateProcess

заносит в параметр cbStack значение, хранящееся в самом исполняемом файле.

Управлять этим значением позволяет ключ /STACK компоновщика:

/STACK:[reserve] [. commit]

Аргумент reserve определяет объем адресного пространства, который

система должна зарезервировать под стек потока (по умолчанию 1 Мб).

Аргумент commit задает объем физической памяти, изначально передаваемой

зарезервированному региону стека (по умолчанию 1 страницу). По мере

исполнения кода в потоке Вам, весьма вероятно, понадобится отвести под стек

больше одной страницы памяти. При переполнении стека возникнет

Page 44: Лабораторная работа № 1

- 44 -

исключение. Перехватив это исключение, система передаст

зарезервированному пространству еще одну страницу (или столько, сколько

указано в аргументе commit). Такой механизм позволяет динамически

увеличивать размер стека лишь по мере необходимости.

Обращаясь к CreateThread, можно обнулить значение параметра cbStack.

В этом случае функция создает стек для нового потока, используя аргумент

commit, внедренный компоновщиком в ЕХЕ-файл. Объем резервируемого

пространства всегда равен 1 Мб. Это ограничение позволяет прекращать

деятельность функций с бесконечной рекурсией.

Допустим, Вы пишете функцию, которая рекурсивно вызывает сама себя.

Предположим также, что в ней «сидит жучок», приводящий к бесконечной

рекурсии. Всякий раз, когда функция вызывает сама себя, в стеке создается

новый стековый фрейм. И если бы система не ограничивала максимальный

размер стека, рекурсивная функция так и вызывала бы сама себя до

бесконечности, а стек поглотил бы все адресное пространство. Задавая же

определенный предел, система, во-первых, предотвращает разрастание стека

до гигантских объемов и, во-вторых, позволяет Вам гораздо быстрее

убедиться в наличии ошибок в своей программе.

Параметры IpStartAddr и IpvThreadParm

Параметр IpStartAddr определяет адрес функции потока, с которой

должен будет начать работу создаваемый поток. Вполне допустимо и даже

полезно создавать несколько потоков, у которых в качестве входной точки

используется адрес одной и той же стартовой функции. Например, можно

создать MDI-приложение, в котором все дочерние окна ведут себя совершенно

одинаково, но каждое оперирует в своем потоке. Ваша функция потока

должна иметь следующий прототип:

Page 45: Лабораторная работа № 1

- 45 -

DWORD WINAPI ThreadFunc(LPVOID IpvThreadParm)

{

DWORD dwResult = 0;

return(dwResult);

}

Параметр IpvThreadParm функции потока идентичен параметру

IpvThreadParm, первоначально передаваемому Вами в CreateThread.

Последняя лишь передает этот параметр по эстафете той функции, с которой

начинается выполнение создаваемого потока. Таким образом, данный

параметр позволяет передавать функции потока какое-либо инициализиру-

ющее значение. Оно может представлять собой или просто 32-битное

значение, или 32-битный указатель на структуру данных с дополнительной

информацией.

Параметр fdwCreate

Этот параметр определяет дополнительные флаги, управляющие

созданием потока. Он принимает одно из двух значений: 0 (исполнение потока

начинается немедленно) или CREATE_SUSPENDED. В последнем случае

система создает поток, затем его стек, инициализирует элементы

соответствующей структуры CONTEXT и, приготовившись к исполнению

первой команды из функции потока, «придерживает» поток до последующих

указаний.

Сразу после возврата из CreateThread и пока еще исполняется вызвавший

ее поток, исполняется и новый поток — если только при его создании Вы не

указали флаг CREATE_SUSPENDED. А поскольку новый поток выполняется

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

проблем. Рассмотрим код:

Page 46: Лабораторная работа № 1

- 46 -

DWORD WINAPI FirstThread(LPVOID IpvThreadParm)

{

int x = 0;

DWORD dwResult = 0.dwThreadld;

HANDLE hThread;

hThread = CreateThread(NULL, 0, SecondThread, (LPVOID)&x,

0, &dwThreadId); CIoseHandle(hThread);

return(dwResult);

}

DWORD WINAPI SecondThread(LPVOID IpvThreadParm)

{

DWORD dwResult = 0;

// выполняем здесь какую-нибудь длительную операцию

((int *)IpvThreadParm) = 5;

return(dwResult);

}

He исключено, что в приведенном коде FirstThread закончит свою работу

до того, как SecondThread присвоит значение 5 переменной х из FirstThread.

Если так и будет, SecondThread не узнает, что FirstThread больше не

существует, и попытается изменить содержимое какого-то участка памяти с

недействительным теперь адресом. Это неизбежно вызовет нарушение

доступа: ведь стек первого потока был уничтожен по завершении FirstThread.

Что же делать? Можно объявить х статической переменной, и компилятор

отведет память для хранения переменной х не в стеке, а в разделе данных

приложения (application's data section). Но тогда функция станет

нереентерабельной. Иначе говоря, в этом случае Вы не смогли бы создать два

Page 47: Лабораторная работа № 1

- 47 -

потока, выполняющих одну и ту же функцию, так как оба потока совместно

использовали бы статическую переменную.

Другое решение проблемы (и его более сложные варианты) состоит в

применении синхронизирующих объектов.

Параметр iplDThread

Последний параметр функции CreateThread — это адрес переменной типа

DWORD, в которой функция вернет идентификатор, приписанный системой

новому потоку.

В Windows 95 передача NULL вместо этого параметра даст ошибку. В

Windows NT до версии 4 этот параметр также не мог быть NULL — иначе

система попыталась бы записать идентификатор потока по адресу 0x00000000,

что привело бы к нарушению доступа. Однако Windows NT 4 разрешает

передавать NULL в параметре IplDThread, если Вас не интересует

идентификатор потока.

Такое несоответствие между Windows 95 и Windows NT может создать

массу проблем для разработчиков приложений. Допустим, Вы пишете и

тестируете программу в Windows NT 4 и при этом пользуетесь тем, что она

допускает указывать NULL в параметре IplDThread функции CreateThread.

Отлично. Но вот Вы запускаете приложение в Windows 95, и оно, естественно,

не работает. Вывод один: Вы обязаны тщательно тестировать свое

приложение как в Windows 95, так и в Windows NT — причем в разных их

версиях.

Page 48: Лабораторная работа № 1

- 48 -

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

Как и процесс, поток можно завершить тремя способами:

1. Поток самоуничтожается вызовом ExitThread (самый

распространенный способ).

2. Один из потоков данного или стороннего процесса вызывает

TerminateThread (этого надо избегать).

3. Завершается процесс, содержащий данный поток.

Функция ExitThread

Поток завершается, когда вызывается ExitThread:

VOID ExitThread(UINT fuExitCode);

У этой функции нет возвращаемого значения, ведь после ее вызова поток

перестает существовать. В параметр fuExitCode она помещает код завершения

потока.

Этот способ завершения потока применяют чаще потому, что после

передачи управления от функции потока внутрисистемной функции

StartOjThread вызывается именно ExitThread, которой передается значение,

возвращенное функцией потока.

Функция TerminateThread

Вызов этой функции также завершает поток:

BOOL TerminateThread(HANDLE hThread. DACFD dwExitCode);

Функция завершает поток, идентифицируемый параметром hThread, и

помещает код завершения в dwExitCode. Ее используют лишь в крайнем

случае, когда управление потоком потеряно и он ни на что не реагирует.

Page 49: Лабораторная работа № 1

- 49 -

«Гибель» потока при вызове ExitThread приводит к разрушению его стека.

Но если он завершен функцией TerminateThread, система не уничтожает стек,

пока не завершится и процесс, которому принадлежал этот поток. Так сделано

потому, что другие потоки могут использовать указатели, ссылающиеся на

данные в стеке завершенного потока. Если бы они обратились к

несуществующему стеку, произошло бы нарушение доступа.

Когда поток прекращается, система уведомляет об этом все DLL-модули,

подключенные к процессу — владельцу завершенного потока. Но при вызове

TerminateThread такого не происходит, и процесс может быть завершен

некорректно. Например, какой-то DLL-модуль при отключении от потока

должен был бы сбросить все данные в дисковый файл. Не получив

уведомления об отключении — а именно так и будет после вызова Termina-

teThread, — он не выполнит свою задачу.

Если завершается процесс

Функции ExitProcess и TerminateProcess тоже завершают потоки.

Единственное отличие в том, что они прекращают выполнение всех потоков,

принадлежавших завершенному процессу.

Что происходит при завершении потока

А происходит вот что:

1. Освобождаются все описатели объектов User, принадлежавших

потоку. В Win32 большинство объектов принадлежат процессу,

содержащему поток, из которого они были созданы. Однако поток

может владеть двумя объектами User: окнами и ловушками (hooks).

Когда поток, создавший такие объекты, завершается, система

уничтожает их автоматически. Прочие объекты разрушаются,

только когда завершается владевший ими процесс.

2. Объект ядра «поток» переводится в свободное состояние.

Page 50: Лабораторная работа № 1

- 50 -

3. Код завершения потока меняется со STILL_ACTIVE на код,

переданный в функцию ExitThread или TerminateThread.

4. Если данный поток — последний активный поток в процессе,

завершается и сам процесс.

5. Счетчик числа пользователей объекта ядра «поток» уменьшается на

1.

При завершении потока сопоставленный с ним объект ядра «поток» не

освобождается, пока не будут закрыты все внешние ссылки на этот объект.

Когда поток завершился, толку от его описателя другим потокам в

системе в общем немного. Единственное, что они могут сделать, — вызвать

GetExitCodeThread и проверить, завершен ли поток, идентифицируемый

описателем hThread, и, если да, определить его код завершения:

BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpdwExitCode);

Код завершения возвращается в переменный типа DWORD, на которую

указывает lpdwExitCode. Если поток не завершен на момент вызова

GetExitCodeThread, функция записывает в эту переменную идентификатор

STILL_ACTIVE (0x103). При успешном вызове функция возвращает TRUE.

Page 51: Лабораторная работа № 1

- 51 -

Ø Как узнать о себе

Некоторые Win32-функции требуют в качестве параметра передавать

описатель какого-либо процесса. Поток может получить описатель

своего процесса, вызвав функцию GetCurrentProcess:

HANDLE GetCurrentProcess(VOID);

Она возвращает псевдоописатель процесса и, не создавая нового

описателя, не увеличивает счетчик числа пользователей объекта «процесс».

Если вызвать CloseHandle и передать ей этот псевдоописатель, она

проигнорирует вызов и просто вернет управление.

Псевдоописатели можно использовать для вызова функций, которым

нужен описатель процесса. Например, показанная ниже строка меняет класс

приоритета вызывающего процесса на HIGH_PRIORITY_CLASS:

SetPriontyClass(GetCurrentProcess().HIGH_PRIORITY_CLASS);

В Win32 API включено также несколько функций, требующих

идентификатор процесса. Поток может запросить идентификатор у своего

процесса, вызвав GetCurrentProcessId:

DWORD GetCurrentProcessId(VOID);

Она возвращает уникальный общесистемный идентификатор текущего

процесса.

При обращении к CreateThread вызывающему потоку возвращается

описатель только что созданного потока, но новый поток знать не знает о том,

каков его собственный описатель. Получить его он может с помощью:

HANDLE GetCurrentThread(VOID);

Page 52: Лабораторная работа № 1

- 52 -

Как и GetCurrentProcess, функция GetCurrentThread возвращает

псевдоописатель, имеющий смысл только при использовании его в контексте

текущего потока. Счетчик объекта «поток» при этом не увеличивается, и

вызов CloseHandle с передачей ей этого псевдоописателя ничего не дает.

Поток может запросить свой идентификатор вызовом:

DWORD GetCurrentThreadld(VOID);

Иногда нужно выяснить «настоящий», а не псевдоописатель потока. Под

«настоящим» я подразумеваю описатель, который однозначно

идентифицирует уникальный поток. Вдумайтесь в такой фрагмент:

DWORD WINAPI ParentThread(LPVOID lpvThreadParm)

{

DWORD IDThread;

HANDLE hThreadParent = GetCurrentThread();

CreateThread(NULL, 0, ChildThread, (LPVOID)hThreadParent, 0, SIDThread);

// далее следует какой-то код...

}

DWORD WINAPI ChildThread(LPVOID lpvThreadParm)

{

HANDLE hThreadParent = (HANDLE) lpvThreadParm;

SetThreadPrionty(hThreadParent, THREAD_PRI0RITY_N0RMAL);

// далее следует какой-то код...

}

Вы заметили, что здесь не все ладно? Идея была в том, чтобы

родительский поток передавал дочернему свой описатель. Но он передает

псевдо, а не «настоящий» описатель. Начиная выполнение, дочерний поток

передает этот псевдоописатель функции SetThreadPriority, а она вследствие

этого меняет приоритет дочернего — а вовсе не родительского! — потока.

Page 53: Лабораторная работа № 1

- 53 -

Происходит так потому, что псевдоописатель является описателем того

потока, что вызывает эту функцию.

Чтобы исправить приведенный выше фрагмент кода, превратим

псевдоописатель в «настоящий» через функцию DuplicateHandle:

BOOL DuplicateHandle(

HANDLE hSourceProcess,

HANDLE hSource,

HANDLE hTargetProcess,

LPHANDLE lphTarget,

DWORD fdwAccess,

BOOL flnherit,

DWORD fdwOptions);

Обычно она используется для создания нового «процессо-зависимого»

описателя из описателя объекта ядра, значение которого увязано с другим

процессом. А мы воспользуемся DuplicateHandle не совсем по назначению и

скорректируем с ее помощью приведенный выше фрагмент кода так:

DWORD WINAPI ParentThread(LPVOID lpvThreadParm)

{

DWORD IDThread;

HANDLE hThreadParent;

DuplicateHandle(

GetCurrentProcess(), // описатель процесса, к которому

// относится псевдоописатель потока;

GetCurrentThread(), // псевдоописатель родительского потока;

GetCurrentProcess(), // описатель процесса, к которому

// относится новый, "настоящий"

// описатель потока;

&hThreadParent, // даст новый, "настоящий" описатель,

// идентифицирующий родительский поток;

0, // игнорируется из-за

Page 54: Лабораторная работа № 1

- 54 -

// DUPLICATE_SAME_ACCESS;

FALSE, // новый описатель потока ненаследуемый;

// новому описателю потока присваиваются

DUPLICATE_SAME_ACCESS); // те же атрибуты защиты.

// что и псевдоописателю

CreateThread(NULL, 0, ChildThread, (LPVOID) hThreadParent, 0 &IDThread);

// далее следует какой-то код...

}

DWORD WINAPI ChildThread(LPVOID lpvThreadParm)

{

HANDLE hThreadParent = (HANDLE) lpvThreadParm;

SetThreadPrionty(hThreadParent, THREAD_PRIORITY_NORMAL);

CloseHandle(hThreadParent);

// далее следует какой-то код . . .

}

Теперь родительский поток преобразует свой «двусмысленный»

псевдоописатель в «настоящий», однозначно определяющий родительский

поток, и передаст его в CreateThread. Когда дочерний поток начнет

выполнение, его параметр lpvThreadParm будет содержать «настоящий»

описатель потока. В итоге вызов какой-либо функции с этим описателем уже

повлияет не на дочерний, а на родительский поток.

Поскольку DuplicateHandle увеличивает счетчик указанного объекта ядра,

то, закончив работу с продублированным описателем объекта, очень важно не

забыть уменьшить счетчик; для этого описатель потока-приемника (в данном

случае родительского потока) передается функции CloseHandle. Сразу после

обращения к SetThreadPriority дочерний поток вызывает CloseHandle, тем

самым, уменьшая на 1 счетчик объекта «родительский поток». В предыдущем

фрагменте кода предположили, что дочерний поток не вызывает других

функций с передачей этого описателя. Ну а если ему нужно вызвать какие-то

Page 55: Лабораторная работа № 1

- 55 -

функции с передачей описателя родительского потока, то, естественно, к

CloseHandle следует обращаться только после того, как необходимость в этом

описателе у дочернего потока отпадет. Надо заметить, что DuplicateHandle

позволяет также преобразовать псевдоописатель процесса в «настоящий». Вот

как это сделать:

HANDLE hProcess;

DuplicateHandle(

GetCurrerttProcess(), // описатель процесса, к которому

// относится псевдоописатель;

GetCurrentProcess(), // псевдоописатель процесса;

GetCurrentProcess(), // описатель процесса, к которому

// относится новый, "настоящий"

// описатель;

&hProcess; // даст новый, "настоящий" описатель.

// идентифицирующий процесс;

0, // игнорируется из-за

// DUPLICATE_SAME_ACCESS;

FALSE, // новый описатель ненаследуемый;

// новому описателю процесса присваиваются

DUPLICATE_SAME_ACCESS); // те же атрибуты защиты,

// что и псевдоописателю

Page 56: Лабораторная работа № 1

- 56 -

Ø Распределение процессорного времени между потоками

Операционная система с вытесняющей многозадачностью должна

использовать тот или иной алгоритм, позволяющий ей распределять

процессорное время между потоками. Рассмотрим алгоритм, применяемый в

Windows NT и Windows 95.

Система выделяет процессорное время всем активным потокам, исходя из

их уровней приоритета, которые варьируются от 0 (низший) до 31 (высший).

Нулевой уровень присваивается в системе особому потоку обнуления страниц

(zero page thread), обнуляющему свободные страницы при отсутствии других

потоков, требующих внимания со стороны системы. Ни один поток, кроме

него, не может иметь нулевой уровень приоритета.

Когда система подключает процессор к потоку, он обрабатывает потоки с

одинаковым приоритетом как равноправные. Иначе говоря, на процессор

подается первый поток с приоритетом 31, а по истечении его кванта времени

система переключает процессор на выполнение следующего потока с тем же

приоритетом. Как только все потоки с приоритетом 31 получат по кванту

времени, система вновь подаст на процессор первый поток с приоритетом 31.

Заметьте: если в Вашей системе за каждым процессором закреплен хотя бы

один поток с приоритетом 31, остальные потоки с более низким приоритетом

никогда не получат доступ к процессору и поэтому не будут выполняться.

Такая ситуация называется перегрузкой (starvation). Она наблюдается, когда

некоторые потоки так интенсивно используют процессорное время, что

остальные практически не работают.

При отсутствии потоков с приоритетом 31 система переходит к потокам с

приоритетом 30; если отпала необходимость в выполнении и этих, к

процессору подключаются потоки с приоритетом 29 и т. д.

На первый взгляд, в системе, организованной таким образом, у потоков с

низким приоритетом (вроде потока обнуления страниц) нет ни единого шанса

на исполнение. Но вот ведь в чем штука: зачастую потоки как раз и не нужно

выполнять. Например, если первичный поток Вашего процесса вызывает

Page 57: Лабораторная работа № 1

- 57 -

GetMessage, а система «видит», что никаких сообщений пока нет, она

приостанавливает его выполнение, отнимает остаток неиспользованного

времени и тут же подключает к процессору другой, ожидающий поток. И пока

в системе не появится сообщений для потока Вашего процесса, он будет

простаивать — система не станет тратить на него время. Но вот в очереди

этого потока появляется сообщение, и система сразу же подключает его к

процессору (если только в этот момент не выполняется поток с более высоким

приоритетом).

А теперь еще один момент. Допустим, процессор исполняет поток с

приоритетом 5, и тут система обнаруживает, что поток с более высоким

приоритетом готов к выполнению. Что будет? Система остановит поток с

более низким приоритетом — даже если не истек отведенный ему квант

процессорного времени — и подключит к процессору поток с более высоким

приоритетом (и, между прочим, выдаст ему полный квант времени). Так что

потоки с более высоким приоритетом всегда вытесняют потоки с более

низким приоритетом независимо от того, исполняются последние или нет.

Присвоение уровней приоритета в Win32

Уровни приоритета созданным потокам присваивает сама система, и

делается это в два этапа. На первом — процессу присваивается определенный

класс приоритета, который говорит системе, какой приоритет нужен данному

процессу по сравнению с другими выполняемыми процессами. А на втором —

потокам, принадлежащим этому процессу, присваиваются относительные

уровни приоритета. В следующих разделах мы и обсудим оба этапа более

подробно.

Классы приоритета процессов

Win32 поддерживает 4 класса приоритетов: idle (простаивающий), normal

(обычный), high (высокий) и realtime (реального времени). Класс приоритета

присваивается процессу объединением (операцией OR) одного из флагов

Page 58: Лабораторная работа № 1

- 58 -

функции CreateProcess с другими флагами fdwCreate. Вот какие уровни

приоритета связаны с каждым классом приоритета:

Класс Флаг в CreateProcess Уровень

Idle IDLE_PRIORITY_CLASS 4

Normal NORMAL_PRIORITY_CLASS 8

High H1GH_PRI0RITY_CLASS 13

Realtime REALTIME PRIORITY CLASS 24

Как видите, любой поток, созданный в процессе с обычным классом

приоритета, получает уровень приоритета 8.

Вызывая CreateProcess, большинство приложений должны либо

использовать флаг NORMAL_PRIORITY_CLASS, либо вообще не указывать

класс приоритета. В последнем случае система присваивает процессу класс

normal — если только родительский процесс не имеет класса idle (тогда

дочерний процесс тоже получает класс приоритета idle).

Когда пользователь работает с каким-то процессом, тот считается

активным (foreground), а остальные процессы — фоновыми (background).

Программы, запускаемые пользователем, в основном относятся к

приложениям с обычным классом приоритета. И тут надо заметить, что

процессы с обычным классом приоритета ведут себя несколько иначе, чем

процессы остальных классов.

Приоритет idle идеален для приложений, занимающихся мониторингом

системы. Допустим, Вы написали программу, которая периодически сообщает

объем свободной оперативной памяти. Но Вы ведь не хотите, чтобы она

мешала работе остальных приложений? Значит, установите класс приоритета

этого процесса как IDLE_PRIORITY_CLASS.

Еще один пример программы, использующей приоритет idle, — экранная

заставка. Большую часть времени она просто отслеживает деятельность

пользователя. Если в течение определенного времени никаких действий не

Page 59: Лабораторная работа № 1

- 59 -

было, заставка активизируется. Мониторинг незачем вести при очень высоком

приоритете — достаточно и низкого, т. е. idle.

Класс приоритета high следует использовать только при крайней

необходимости. Может, Вы этого и не знаете, но Explorer выполняется с

высоким приоритетом. Большую часть времени его потоки простаивают,

готовые пробудиться, как только пользователь нажмет какую-нибудь клавишу

или щелкнет кнопку мыши. Пока потоки Explorer простаивают, система не

выделяет им процессорного времени, что позволяет выполнять потоки с

низким приоритетом. Но вот пользователь нажал, скажем, Ctrl+Esc, и система

пробуждает поток Explorer. (Комбинация клавиш Ctrl+Esc попутно открывает

меню Start.) Если в данный момент исполняются потоки с низким

приоритетом, они немедленно вытесняются, и начинает работать поток

Explorer. Microsoft разработала Explorer именно так потому, что любой

пользователь — независимо от текущей ситуации в системе — ожидает

мгновенной реакции оболочки на свои команды. В сущности, окна Explorer

можно открывать, даже когда все потоки с низким приоритетом зависают в

бесконечных циклах. Обладая более высоким приоритетом, потоки Explorer

вытесняют поток, исполняющий бесконечный цикл, и дают возможность

закрыть зависший процесс.

Надо отметить высокую степень продуманности Explorer. Основную

часть времени он просто «спит», не требуя процессорного времени. Будь это

не так, вся система работала бы гораздо медленнее, а многие приложения

просто не отзывались бы на действия пользователя.

И, наконец, флагом четвертого по счету класса приоритета —

REALTIME_PRIORI-TY_CLASS — почти никогда не стоит пользоваться. На

самом деле в ранних бета-версиях Win32 API даже не предусматривалось

присвоения этого приоритета приложениям, хотя операционная система

поддерживала эту возможность. Realtime — чрезвычайно высокий приоритет,

и поскольку большинство потоков в системе (включая те, что управляют

самой системой) имеют более низкий приоритет, процесс с таким классом

Page 60: Лабораторная работа № 1

- 60 -

окажет на них сильное влияние. Так, системные потоки, контролирующие

мышь и клавиатуру, фоновый сброс данных на диск и перехват Alt+Ctrl+Del,

— все они оперируют при более низком классе приоритета. Если пользователь

переместит мышь, поток, реагирующий на движение мыши, будет вытеснен

потоком с приоритетом realtime. А это повлияет на характер перемещения

курсора мыши: он станет двигаться не плавно, а рывками. Может случиться и

кое-что похуже — вплоть до потери данных.

Класс приоритета realtime используют только в программе, напрямую

обращающейся к оборудованию, или если приложение выполняет

быстротечную операцию, которую нельзя прерывать ни при каких

обстоятельствах.

Процесс с классом приоритета realtime нельзя запустить, если

пользователь, зарегистрировавшийся в системе, не имеет привилегии Increase

Scheduling Priority. По умолчанию такой привилегией обладает администратор

или пользователь с соответствующими полномочиями. Но она может быть

предоставлена и другим

пользователям (или группам) с помощью User Manager.

Изменение класса приоритета процесса

Может показаться странным, что, создавая дочерний процесс,

родительский сам устанавливает ему класс приоритета. За примером далеко

ходить не надо — возьмем все тот же Explorer. При запуске из него какого-

нибудь приложения новый процесс создается с обычным приоритетом. Но

Explorer ведь не знает, что делает этот процесс и как часто его потокам надо

выделять процессорное время. Поэтому в системе предусмотрена возможность

изменения класса приоритета самим выполняемым процессом — вызовом

SetPriorityClass:

BOOL SetPriorityClass(HANDLE hProcess, DWORD fdwPriority);

Page 61: Лабораторная работа № 1

- 61 -

Эта функция меняет класс приоритета процесса, определяемого

описателем hProcess, в соответствии со значением параметра fdwPriority. Этот

параметр принимает одно из значений: IDLE_PRIORITY_CLASS,

NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS или

REALTIME_PRIORITY_CLASS. При успешном выполнении функция

возвращает TRUE; в ином случае — FALSE. Поскольку SetPriorityClass

принимает описатель процесса, Вы можете изменить приоритет любого

процесса, выполняемого в системе, — если его описатель известен и у Вас

есть соответствующие права доступа.

Парная ей функция GetPriorityClass позволяет узнать класс приоритета

любого процесса:

DWORD GetPriorityClass(HANDLE hProcess);

Она возвращает, как Вы догадываетесь, один из перечисленных выше

флагов.

При запуске из оболочки командного процессора начальный приоритет

программы тоже обычный. Однако, запуская ее командой START, можно

указать ключ, определяющий начальный приоритет программы. Например,

следующая команда, введенная в оболочке командного процессора, заставит

систему запустить приложение Calculator и присвоить ему низкий приоритет:

C:\>START /LOW CALC.EXE

Команда START допускает также ключи /NORMAL, /HIGH и

/REALTIME, позволяющие начать выполнение программы соответственно с

обычным (как и по умолчанию), высоким приоритетом и приоритетом

реального времени. Разумеется, после запуска программа может вызвать

SetPriorityClass и установить себе другой класс приоритета.

Page 62: Лабораторная работа № 1

- 62 -

В Windows 95 команда START не поддерживает ключи /LOW, /NORMAL,

/HIGH и /REALTIME. Из оболочки командного процессора Windows 95

процессы всегда запускаются с обычным классом приоритета.

Установка относительного приоритета потока

Когда поток только создается, уровень его приоритета соответствует

классу приоритета процесса. Скажем, первичный поток процесса с классом

HIGH_PRIORITY_CLASS получает начальное значение уровня приоритета 13.

Но уровень приоритета отдельного потока можно и повысить, и понизить. В

то же время приоритет потока всегда относителен классу приоритета его

процесса.

Относительный приоритет потока в пределах одного процесса можно

изменить функцией SetThreadPriority:

BOOL SetThreadPriority(HANDLE hThread, int nPriority);

Первый ее параметр, hThread, — это описатель потока, приоритет

которого Вы изменяете, a nPriority принимает одно из значений, приведенных

в таблице:

Идентификатор Описание

THREAD PRIORITY LOWEST

THREAD PRIORITY BELOW NORMAL

THREAD PRIORITY NORMAL

THREAD PRIORITY ABOVE NORMAL

THREAD PRIORITY HIGHEST

Приоритет потока должен быть на 2

единицы ниже класса приоритета процесса.

Приоритет потока должен быть на I

единицу ниже класса приоритета процесса.

Приоритет потока должен соответствовать

классу приоритета потока.

Приоритет потока должен быть на 1

единицу выше класса приоритета процесса.

Приоритет потока должен быть на 2

единицы выше класса приоритета процесса.

Page 63: Лабораторная работа № 1

- 63 -

В момент создания потока начальное значение его относительного

приоритета равно 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. В таблице показано, как система

определяет базовый уровень приоритета потока, комбинируя класс приоритета

процесса с относительным приоритетом потока.

Page 64: Лабораторная работа № 1

- 64 -

Класс приоритета процесса

Относительный приоритет потока Idle Normal High Realtime

Time critical (критичный по времени) 15 15 15 31

Highest (высший) 6 10 15 26

Above normal (выше обычного) 5 9 14 25

Normal (обычный) 4 8 13 24

Below normal (ниже обычного) 3 7 12 23

Lowest (низший) 2 6 11 22

Idle (простаивающий) 1 1 1 16

Таблица показывает приоритеты потоков для выполняемых в фоновом

режиме процессов с обычным классом приоритета. В Windows NT эти

приоритеты не изменяются, когда процесс становится активным, — из-

меняются только кванты времени, выделяемые потокам. В то же время

Windows 95, когда процесс становится активным, увеличивает приоритет

потоков lowest, below normal, normal и above normal на 1 единицу, но не

меняет приоритет потоков idle и time critical.

Функция GetThreadPriority, парная SetThreadPriority, позволяет узнать

относительный приоритет потока:

int GetThreadPrionty(HANDLE hThread);

Она возвращает один из перечисленных выше идентификаторов, а при

ошибке — THREAD_PRIORITY_ERROR_RETURN.

Изменение класса приоритета процесса не сказывается на относительных

приоритетах его потоков. Обратите внимание и на то, что повторные вызовы

SetThreadPriority не дают кумулятивного эффекта. Например, если поток

Page 65: Лабораторная работа № 1

- 65 -

создается в процессе с высоким классом приоритета и исполняются

следующие две строки кода:

SetThreadPrionty(hThread, THREAD_PRI0RITY_LOWEST);

SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST);

то поток получает уровень приоритета 11, а не 9.

Ни одна Win32-функция не возвращает уровень приоритета потока, т. е. в

Win32 нет функций, которые принимали бы описатель потока и сообщали, что

уровень приоритета данного потока равен, например, 8. Такая ситуация

создана преднамеренно. Вспомните, что Microsoft может в любой момент

изменить алгоритм распределения процессорного времени. Поэтому при

разработке приложений не стоит опираться на какие-то нюансы этого

алгоритма. Используйте классы приоритетов процессов и относительные

приоритеты потоков, и все будет о'кэй.

Динамическое изменение уровней приоритетов потоков

Уровень приоритета, получаемый комбинацией относительного

приоритета потока и класса приоритета процесса, которому принадлежит

данный поток, называют базовым уровнем приоритета потока. Иногда

система изменяет уровень приоритета потока. Обычно это происходит в ответ

на некоторые события, связанные с вводом/выводом (например, оконные

сообщения или чтение диска). Скажем, поток с относительным приоритетом

normal, выполняемый в процессе с высоким классом приоритета, имеет

базовый приоритет 13.

Если пользователь нажимает какую-нибудь клавишу, система помещает в

очередь потока сообщение WM_KEYDOWN. А поскольку в очереди потока

появилось сообщение, поток подключается к процессору и обрабатывает это

сообщение. При этом система временно поднимает уровень его приоритета с

13 до 15 (действительное значение может отличаться в ту или другую

Page 66: Лабораторная работа № 1

- 66 -

сторону). Этот новый уровень приоритета потока называется динамическим

приоритетом потока. Процессор исполняет поток в течение отведенного

отрезка времени, а по его истечении система снижает приоритет потока на 1,

до уровня 14. Далее потоку вновь выделяется квант процессорного времени,

по окончании которого система опять снижает уровень приоритета потока на

1. И теперь динамический приоритет потока снова соответствует базовому

уровню его приоритета. При этом динамический приоритет никогда не

опускается ниже базового уровня приоритета.

Microsoft всегда старается тонко настраивать динамическое изменение

приоритета потоков в операционной системе, чтобы та максимально быстро

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

приоритетов реального времени (от 16 до 31) системой никогда не меняются.

Описанный механизм действует лишь по отношению к потокам с уровнями

приоритетов, попадающими в динамический диапазон. И, кроме того, система

не допускает динамического повышения приоритета потока до уровней реаль-

ного времени (более 15).

В Windows NT 4 появились две новые функции, позволяющие отключать

автоматическое изменение приоритетов потоков:

BOOL SetProcessPriorityBoost(HANDLE hProcess, BOOL DisablePriorityBoost);

BOOL SetThreadPriorityBoost(HANDLE hThread, BOOL DisablePriorityBoost);

SetProcessPriorityBoost заставляет систему включить или отключить

изменение приоритетов всех потоков в указанном процессе, a

SetThreadPriorityBoost действует применительно к отдельным потокам. Эти

функции имеют свои аналоги, позволяющие определить, разрешено или

запрещено изменение приоритетов:

BOOL Ge1ProcessPriontyBoost(HANDLE hProcess, PBOOL pDisablePnontyBoost);

BOOL GetThreadPnontyBoost(HANDLE hThread, PBOOL pDisablePriorityBoost);

Page 67: Лабораторная работа № 1

- 67 -

Каждой из этих двух функций Вы передаете описатель нужного процесса

или потока и адрес переменной типа BOOL, в которой и возвращается

результат.

В Windows 95 все четыре описанные функции не предусмотрены. В ней

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

Задержка и возобновление потоков

Cоздаваемый поток можно «попридержать», передав в CreateProcess или

CreateThread флаг CREATE_SUSPENDED. В этом случае система создает

объект ядра, идентифицирующий поток, формирует стек потока и

инициализирует контекст (т. е. элементы структуры CONTEXT). Однако

счетчику простоев объекта «поток» присваивается начальное значение 1, а это

значит, что система не собирается подключать поток к процессору для

выполнения. Чтобы такой поток начал работать, нужно из другого потока

вызвать ResumeThread и передать ей описатель, возвращенный функцией

CreateThread. Впрочем, описатель потока можно взять и из структуры, на

которую указывает параметр IppiProdnfo, передаваемый в CreateProcess.

DWORD Resume Thread (HANDLE hThread);

Если вызов ResumeThread прошел успешно, она возвращает предыдущее

значение счетчика простоев данного потока; в ином случае — OxFFFFFFFF. ,

Выполнение отдельного потока можно задерживать несколько раз. Если

поток/задержан 3 раза, то и возобновлен он должен быть тоже трижды —

только тогда система выделит ему процессорное время. Выполнение потока

можно приостановить не только при его создании с флагом

CREATE_SUSPENDED, но и вызовом SuspendThread:

DWORD SuspendThread(HANDLE hThread);

Page 68: Лабораторная работа № 1

- 68 -

Любой поток может вызвать эту функцию и приостановить выполнение

другого потока. Хоть об этом нигде и не говорится, приостановить свое

выполнение поток способен сам, а возобновить себя без посторонней помощи

— нет. Как и ResumeThread, SuspendThread возвращает предыдущее значение

счетчика простоев данного потока. Поток допустимо задерживать не более

чем MAXIMUM_SUSPEND_COUNT раз (в файле WINNT.H это значение

определено как 127).

Page 69: Лабораторная работа № 1

- 69 -

ПРОГРАММИРОВАНИЕ ДЛЯ UNIX

· ПРОГРАММЫ, ПРОЦЕССЫ И ПОТОКИ

Программа – это набор команд и данных, который хранится в обычном

файле на диске. В индексном узле этот файл отмечен как исполняемый, а

содержимое файла организовано согласно определенным ядром правилам

(случай, когда ядру не безразлично содержимое файла).

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

заблагорассудится: если содержимое файла соответствует правилам и файл

отмечен как исполняемый, программу можно выполнить. На практике обычно

имеет место следующее: сначала исходный код программы, написанный на

каком-то языке программирования (скажем, С или C++), сохраняется в

обычном файле, который часто называют текстовым файлом, потому что он

содержит строки текста. Затем создается другой обычный файл, называемый

объектным файлом — он содержит машинный код, полученный в результате

преобразования исходной программы. Для выполнения этого преобразования

используется компилятор или ассемблер (которые сами являются

программами). Если в объектном файле имеются все нужные функции, он

отмечается, как исполняемый и может быть выполнен как есть. В противном

случае разработчик должен с помощью компоновщика (иногда называемого в

мире UNIX «загрузчиком») связать объектный файл с другими объектными

файлами, которые могут быть организованы в библиотеки. Если компоновщик

сможет обнаружить все, что ему нужно, он создаст исполняемый файл.

Чтобы запустить программу, ядро сначала должно создать новый процесс

— среду, и которой выполняется программа. Процесс состоит из трех

сегментов: сегмента команд, сегмента пользовательских данных и сегмента

системных данных. Программа используется для инициализации сегментов

команд и пользовательских данных. После этой инициализации процесс

начинает все сильнее отличаться от выполняемой в нем программы — чтобы

подтвердить это, достаточно вспомнить, что программисты изменяют данные

и, в редких случаях, сами команды. Кроме того, процесс может получать в

Page 70: Лабораторная работа № 1

- 70 -

свое распоряжение дополнительную память, открывать файлы и приобретать

другие ресурсы, отсутствующие в программе.

Пока процесс выполняется, ядро следит за его потоками — отдельными

путями выполнения команд, которые потенциально могут осуществлять

чтение и запись одних и тех же данных процесса (однако каждый поток имеет

свой стек). Приступив к написанию программы, вы имеете в своем

распоряжении только один поток, пока не создадите другой с помощью

специального системного вызова. Таким образом, начинающие программисты

могут считать, что процесс является однопоточным.

Используя в качестве источника одну программу, можно создать и

инициализировать несколько одновременно выполняемых процессов, однако

между ними не будет никакого функционального отношения. Ядро может

сэкономить память, сделав сегмент команд этих процессов общим, но сами

процессы не могут узнать об этом. В то же время потоки одного процесса

связаны сильным функциональным отношением.

Системные данные процесса включают такие атрибуты, как текущий

каталог, дескрипторы открытых файлов, использованное время процессора и

т. д. Процесс не может читать или изменять свои системные данные

непосредственно, так как они находятся вне его адресного пространства.

Вместо этого для чтения и изменения атрибутов используются различные

системные вызовы.

Выполняемый процесс может поручить ядру создать другой процесс и

стать родителем нового дочернего процесса. Дочерний процесс наследует

большинство атрибутов системных данных родителя. Например, если

родительскому процессу соответствуют какие-то открытые файлы, для

дочернего процесса эти файлы также будут открыты. Преемственность такого

рода — фундаментальный принцип UNIX. Это отличается от создания

потоком нового потока. Потоки одного процесса в большинстве отношений

равны и ничего не наследуют друг от друга. Все потоки могут обращаться ко

всем данным и ресурсам — не к копиям.

Page 71: Лабораторная работа № 1

- 71 -

· ПРОЦЕССЫ

Ø Системный вызов exec

Невозможно понять системные вызовы exec и fork без четкого понимания

различий между процессом и программой. В двух словах напомню суть

различий: процесс — это среда исполнения, которая включает в себя сегмент

исполняемого кода, сегменты пользовательских и системных данных, а также

набор дополнительных ресурсов, полученных во время исполнения.

Программа — это файл, который содержит исполняемый код, сегменты с

данными для инициализации и с данными пользователя.

Системный вызов exec повторно инициализирует процесс, подменяя его

указанной программой — программа меняется, а процесс остается.

Системный вызов fork наоборот запускает новый процесс, который является

точной копией существующего, простым копированием сегментов с

исполняемым кодом и данными. Вновь созданный процесс продолжает работу

с того же места, что и изначальный, таким образом, оба процесса продолжают

исполнять один и тот же код.

По отдельности друг от друга exec и fork используются крайне редко. Мы

будем использовать их вместе друг с другом, и Вы убедитесь, насколько

мощной может быть эта пара.

Системный вызов exec — единственный способ запуска программ в

UNIX. Мало того, что командная оболочка использует exec для запуска наших

программ, но и сама она запускается именно таким образом. А системный

вызов fork — единственный способ запустить новый процесс.

На самом деле, системного вызова с именем exec не существует. Под этим

именем подразумевается целое семейство из шести системных вызовов, имена

которых в общем виде можно записать как ехесАВ. А — это один из

символов, «1» или «v», они определяют, как входные аргументы передаются

вызову — в виде списка (от анг. list) или в виде массива (от англ. vector). В

(может отсутствовать) — это либо «р» указывающий, что поиск файла

программы должен выполняться с помощью переменной PATH, либо «е» —

Page 72: Лабораторная работа № 1

- 72 -

такому вызову передается специфичная среда окружения (как это ни странно,

но нет системного вызова exec, который совмещал бы в себе характерные

особенности «е» и «р»). Таким образом, мы получаем шесть различных

системных вызовов: execl, execv, exeelp, execvp, execle и execve.

Системный вызов execl

execl – запускает программу; входные аргументы передаются в виде

списка:

#include <unistd.h>

int execl(

const char *path, /* полный путь к программе */

const char *arg0, /* первый аргумент (аrg[0] - имя файла) */

const char *arg1, /* второй аргумент (если необходимо) */

…, /* остальные аргументы (если необходимы) *

NULL /* пустой указатель, завершающий список */

);

/* В случае ошибки возвращает -1 (код ошибки - в переменной еrrnо) */

Аргумент path должен содержать полное имя исполняемого файла с

программой для действующего идентификатора пользователя (например, с

правами 755). Исполняемый код процесса будет «затерт» исполняемым кодом

новой программы, сегмент данных будет затерт сегментом данных новой

программы, а стек будет переинициализирован. Исполнение новой программы

начнется с самого начала (т. е. будет вызвана функция main()).

В случае успеха возврат из execl не предусмотрен, потому что точка

возврата будет утеряна. В случае ошибки execl вернет -1, не имеет смысла

проверять это значение, поскольку никакого другого вы не получите.

Наиболее частые причины, приводящие к невозможности запустить новую

программу, это либо отсутствие файла с программой, либо отсутствие права

на ее запуск.

Page 73: Лабораторная работа № 1

- 73 -

Все остальные аргументы вызова собираются в массив указателей на

строки, а последним всегда должен стоять пустой указатель, который

определяет конец списка входных аргументов. Первый аргумент, в

соответствии с соглашениями — это имя файла программы (только само имя

файла, без пути к нему). Новая программа получает доступ к этому списку

через уже знакомые нам аргументы функции main() – argc и argv. Среда

окружения также передается новой программе и доступна ей через указатель

environ или посредством функции getenv.

Поскольку процесс продолжает существовать и сегмент с системными

данными практически не изменяется, почти псе атрибуты процесса также

остаются неизменными, включая идентификатор процесса, идентификатор

процесса-предка, идентификатор группы процесса, идентификатор сессии,

управляющий терминал, реальные индефикаторы пользователя и группы,

текущий и корневой каталоги, приоритет, статистические характеристики

времени исполнения в пространстве пользователя и в пространстве ядра и, как

правило, дескрипторы открытых файлов. Гораздо проще перечислить

основные атрибуты, которые изменяются:

1. Если процесс назначал свои обработчики сигналов, то все они

сбрасываются в исходное состояние, поскольку функции

обработчики после запуска новой программы станут недоступны.

2. Если в новой программе установлены, биты set-user-ID или set-

group-ID, то действующие идентификаторы пользователя и группы

переустанавливаются в соответствии с идентификаторами

владельца и группы файла программы. Нет никакого способа

вернуть прежние действующие идентификаторы, если они

отличаются от реальных.

3. Регистрация всех функций, которая была выполнена с помощью

atexit(), отменяется, поскольку код этих функций будет затерт.

4. Сегменты общей памяти отсоединяются, поскольку точки

соединения будут утеряны.

Page 74: Лабораторная работа № 1

- 74 -

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

}

В результате работы этой функции мы получим:

ленивую собаку.

А что же произошло с лисицей? Получается, что она не такая уж и

шустрая. Стандартная библиотека ввода-вывода, в состав которой входит

функция printf, буферизует вывод и по завершении процесса все

незаполненные буферы выталкиваются. Но перед вызовом execl процесс не

завершался и буфер вывода, находившийся и сегменте данных пользователя,

оказался затертым еще до того, как он мог бы быть вытолкнут.

Эта проблема легко решается либо отказом от буферизации вывода:

setbuf(stdout, NULL);

Page 75: Лабораторная работа № 1

- 75 -

либо принудительным выталкиванием буфера непосредственно перед вызовом execl:

fflush(stdout);

Как правило, exec используется в паре с системным вызовом fork, но

иногда он может использоваться отдельно. Например, иногда большую

программу можно разбить на стадии и с помощью exec переходить от одной

стадии к другой. Но поскольку при запуске новой программы происходит

потеря сегмента данных, стадии исполнения должны быть достаточно

независимы друг от друга, хотя данные могут быть переданы через аргументы,

переменные среды окружения или через файлы. Такое применение exec хотя и

достаточно редкое, но известное. Гораздо более типичный случай

использования exec — выполнение некоторого объема подготовительной

работы перед вызовом основной программы. Например, команда nohup,

которая сначала подавляет сигналы прерывания и завершения, а затем с

помощью execvp запускает команду пользователя. Другой пример — команда

nice, которая задает приоритет запускаемой программы.

Другие пять вызовов семейства exec

Другие пять вызовов семейства exec предоставляют три дополнительные

возможности, недоступные в execl:

1. Передать аргументы в виде массива, а не в виде списка. Это

совершенно необходимо, когда число аргументов в момент

написания программы (наполнен командного интерпретатора)

заранее неизвестно.

2. Поиск файла программы с использованием значения переменной

окружения PATH, как это делает командная оболочка.

3. Передавать указатель на сформированную «вручную» среду

окружения вместо неявной передачи указателя environ*.

Page 76: Лабораторная работа № 1

- 76 -

Ниже приводится краткое описание остальных системных вызовов

семейства exec:

1. execv — запускает программу; аргументы передаются в виде

массива:

#include <unistd.h>

int execv(

const char *path, /* полный путь к файлу а программой */

char *const argv[ ] /* массив аргументов */

);

/* В случае ошибки возвращает -1 (код ошибки - в переменной errno) */

2. exeсlp — запускает программу; аргументы передаются в виде

списка, поиск файла ведется с использованием переменной PATH:

#include <unistd.h>

int execlp(

const char *file, /* имя файла с программой */

const char *arg0, /* первый аргумент (имя файла) */

const char *arg1, /* второй аргумент (если необходим) */

… /* остальные аргументы (если необходимы) */

NULL /* пустой указатель, завершавший список аргументов */

);

/* В случае ошибки возвращает -1 (код ошибки − в переменной еrrno) */

Page 77: Лабораторная работа № 1

- 77 -

3. execvp — запускает программу; аргументы передаются в виде

массива, поиск файла ведется с использованием переменной PATH:

#include <unistd.h>

int execvp(

const char *file, /* имя файла с программой */

char *const argv[ ] /* массив аргументов */

);

/* В случае ошибки возвращает -1 (код ошибки − в переменной errno) */

4. execle – запускает программу аргументы; передаются в виде списка,

так же передается среда окружения:

#include <unistd.h>

int execle(

const char *path, /* полный путь к файлу с программой */

const char *arg0, /* первый аргумент (имя файла) */

const char *arg1, /* второй аргумент (если необходим) */

… /* остальные аргументы (если необходимы) */

NULL /* пустой указатель, завершающий список аргументов */

char *const envv[ ] /* массив сформированной среды окружения */

);

/* В случае ошибки возвращает -1 (код ошибки - в переменной errno) */

Page 78: Лабораторная работа № 1

- 78 -

5. execve − запускает программу; аргументы передаются в виде

массива, так же передается среда окружения:

#include <unistd.h>

int execve(

const char *path, /* полный путь к файлу с программой */

char *const argv[ ] /* массив аргументов */

char *const envv[ ] /* массив сформированной среды окружения */

);

/* В случае ошибки возвращает -1 (код ошибки - в переменной errno) */

Обратите внимание: используемый здесь аргумент argv абсолютно

идентичен аргументу argv, передаваемому функции main. He забывайте:

последним в этом массиве всегда должен стоять NULL.

Page 79: Лабораторная работа № 1

- 79 -

Ø Системный вызов fork

Вызов fork до определенной степени является противоположностью

вызову exec. Он запускает новый процесс, но не новую программу, где новый

процесс – это точная копия старого со всеми его данными.

fork – создает новый процесс:

#include <unistd.h>

pid_t fork(void);

/* Возвращает идентификатор дочернего процесса или 0 в случае успеха и -1

в случае ошибки (код ошибки - в переменной еrrno) */

По завершении fork оба процесса (потомок и предок) получают от него

возвращаемое значение. В зависимости от него реакция потомка и предка

может сильно отличаться. Обычно процесс-потомок сразу же вызывает exec

для запуска новой программы, а предок может либо подождать завершения

процесса-потомка, либо сняться своими делами.

Процесс-потомок получает от вызова fork значение 0, родительский

процесс – идентификатор процесса-потомка. Возвращаемое значение -1

свидетельствуй об ошибке, но поскольку fork не имеет входных аргументов,

то ошибочная ситуаций никак не связана с вызывающим процессом.

Единственно возможная ошибка исчерпание системных ресурсов, то есть либо

нехватка места в файле подкачки, либо в системе исполняется слишком много

процессов. В случае неудачи процесс-предок может подождать некоторое

время (обратившись, например, к системному вызову sleep) и повторить

попытку, но это немного не то, что обычно делают командные

интерпретаторы. Как правило, они незамедлительно выводят сообщение об

ошибке и переходят в ожидание новых команд от пользователя.

Программы, запущенные вызовом exec получают от запустившей их

программы большинство атрибутов в неизменном состоянии, поскольку

сегмент системны данных не изменяется. То же самое происходит и при

создании нового процесса вызовом fork: процесс-потомок наследует атрибуты

Page 80: Лабораторная работа № 1

- 80 -

от родителя как результат копирования сегмента системных данных предка.

Такого рода наследование позволяет пользователю установить атрибуты в

командной оболочке, например, текущий каталог, действующий

идентификатор пользователя и приоритет, которые затем будут автоматически

передаваться всем запускаемым командам. Можно рассматривать эти

атрибуты как «черты, характерные для близких родственников». Для

наследования недоступно лишь несколько атрибутов:

1. Идентификаторы процессов у потомка и предка будут отличаться,

это совершенно очевидно.

2. Если в рамках родительского процесса было запущено несколько

потоков, потомок унаследует только один, тот который вызвал fork.

3. Потомок получает от родителя дубликаты открытых дескрипторов.

Они соответствуют одним и тем же файлам. Они совместно

используют одни и те же записи в системной таблице файлов, а

значит и текущие позиции в файлах у них будут одни и те же. Если

потомок изменит ее с помощью lseek, то следующая операция

ввода-вывода предка будет производиться с новой текущей

позиции. Однако сами файловые дескрипторы у предка и потомка

разные. Если потомок закроет свой дескриптор, то

соответствующий дескриптор предка по-прежнему останется

открытым.

4. Процесс-потомок сбрасывает накапливаемые значения времени

исполнения в пространстве пользователя и ядра в ноль, потому что

был порожден новый.

Простой пример, демонстрирующий работу системного вызова fork:

Page 81: Лабораторная работа № 1

- 81 -

void forktest(void)

{

int pid;

printf(("Начало теста \n");

pid = fork();

printf("Возвращаемое значение %d\n", pid);

}

Pезультат:

$ forktest

Начало теста

Возвращаемое значение 98657

Возвращаемое значение 0

$

В данной ситуации предок первым вывел свое сообщение, но это

совершенно необязательно, Если для вас очередность исполнения имеет

значение, процессы можно синхронизировать. Сделать это можно с помощью

каналов или, что немного сложнее, с помощью сигналов.

Перезапустим пример forktest, перенаправив вывод в файл:

$ forktest > tmp

$ cat tmp

Начало теста

Возвращаемое значение 56807

Начало теста

Возвращаемое значение

$

На этот раз мы получили сообщение «Начало теста» дважды! Можете

объяснить, почему?

Page 82: Лабораторная работа № 1

- 82 -

Это произошло из-за буферизации вывода − потомок, вместе со всем

остальным, унаследовал от предка и частично заполненный выходной буфер.

Когда потомок завершил свою работу, его буфер был вытолкнут, то же самое

произошло и с предком. В предыдущем испытании printf не буферизовала

свой вывод, поскольку «знала», что устройством стандартного вывода

является терминал, который предполагает более интерактивный pежим

работы.

Вызов fork и ехeс прекрасно дополняют друг друга. Дочерний процесс,

созданный вызовом fork, сам по себе не представляет особой ценности, так как

является точной копией родителя. Мы получим гораздо больше выгоды, если

потомок заменит себя новой программой — то есть как раз то, что делает

вызов exec.

Page 83: Лабораторная работа № 1

- 83 -

Ø Завершение процесса и системные вызовы 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. Делается это потому, что код завершения процесса,

унаследованный потомком (занимающийся сборкой мусора, освобождением

ресурсов и пр.), обычно должен выполняться единожды. Но это справедливо

Page 84: Лабораторная работа № 1

- 84 -

не для всех случаев, поэтому в каждой конкретной ситуации вы должны еще

раз все оценить и принять решение какой из вариантов подходит вам больше.

Если для контроля над ошибками вы собираетесь использовать макросы

«ес», не забывайте, что автоматическое отображение сообщений выполняется

в функции, зарегистрированной с помощью atexit, которая не будет вызвана

при завершении процесса вызовом _exit.

Ниже приводится краткое описание семейства функции exit. Обратите

внимание, объявления функций находятся в двух различных заголовочных

файлах:

1. _exit − завершает процесс без обращения к коду сборки мусора:

#include <unistd.h>

void _exit(

int status /* код завершения */

);

/* В программу управление уже не возвращается */

2. _Exit − завершает процесс без обращения к коду сборки мусора

#include <stdlib.h>

void _Exit(

int status /* код завершения */

);

/* В программу управление уже не возвращается */

3. exit − завершает процесс с обращением к коду сборки мусора

#include <stdlib.h>

void exit(

int status /* код завершения */

);

/* В программу управление уже не возвращается */

Page 85: Лабораторная работа № 1

- 85 -

_exit и родственные ему системные вызовы завершают работу процесса,

обратившегося к нему, с кодом завершения, равным младшему байту

аргумента status. Эти вызовы имеют ряд побочных эффектов, наиболее

важные из которых перечислены ниже, полный список вы найдете в

[SUS2002]. Фактически, эти побочные эффекты имеют место при любом

варианте завершения процесса (исключая, разве что, крах системы).

1. Все открытые дескрипторы закрываются.

2. Если процесс был управляющим процессом (лидер сеанса), сеанс

теряет свой управляющий терминал. Кроме того, каждому процессу

в сеансе передается сигнал SIGHUP, который приводит к

завершению процессов.

3. Родительский процесс извещается о завершении процесса-потомка

через один из системных вызовов семейства wait.

4. Это не оказывает непосредственного влияния на дочерние

процессы, за исключением того, что их новым предком становится

специальный процесс в системе, чей идентификатор (обычно − 1)

потомок может получить, обратившись к системному вызову

getppid.

Родительский процесс получает код завершения дочернего процесса

через один из системных вызовов wait. Код завершения − это число в

диапазоне от о до 255. В соответствии с соглашениями, значение 0

соответствует коду успешного завершения, а ненулевое значение какой-либо

ошибке, код которой определяется самим завершившим работу приложением.

После того как процесс завершится, он прекращает свое исполнение, но

остается в системе до тех пор, пока код завершения не будет передан

процессу-предку, если он, конечно же, заинтересован в этом. Если потомок не

смог сообщить предку о своем завершении, он переходит в состояние

«зомби».

Page 86: Лабораторная работа № 1

- 86 -

Ø Системные вызовы wait, waitpid и waitid

Системные вызовы wait, waitpid и waitid ожидают, пока дочерний процесс

не изменит свое состояние (приостановка, возобновление или завершение) и

возвращают его вызывающей программе.

Системный вызов waitpid

waitpid — ожидает изменения состояния дочернего процесса:

#include <sys/wait.h>

pid_t waitpid(

pid_t pid, /* идентификатор процесса или группы процессов */

int *statusp, /* указатель на статус или NULL */

int options /* флаги */

);

/* В случае успеха возвращает идентификатор процесса или 0, в случае

ошибки -1 (код ошибки в переменной errno) */

Аргумент pid может принимать следующие значения:

> 0Ожидает изменение состояния дочернего процесса с указанным

идентификатором

-1 Ожидать изменение состояния любого дочернего процесса

0Ожидать изменение состояния любого дочернего процесса принадлежащего к

той же группе процессов, что и вызывающий

< -1Ожидать изменение состояния любого дочернего процесса, принадлежащего к

группе процессов с идентификатором -pid

Как правило, командная оболочка запускает каждый конвейер из команд

под своим групповым идентификатором и в ожидании завершения его работы

будет передавать в waitpid отрицательное значение группового

Page 87: Лабораторная работа № 1

- 87 -

идентификатора, благодаря чему системный вызов будет возвращать

управление по завершении каждой из команд.

На выходе из waitpid вызывающий процесс получает идентификатор

процесса-потомка в виде возвращаемого значения, чей идентификатор совпал

с аргументом pid. Ноль возвращается только в том случае, когда был

установлен флаг WNOHANG (будет описан ниже).

Допускается ожидать изменения состояния только прямых потомков,

порожденных системным вызовом fork. Ожидание процессов-«внуков» не

допускается, даже если их родители (прямые потомки вызывающего процесса)

к моменту вызова уже завершили свою работу. Как объяснялось в

предыдущем разделе, «осиротевшие» процессы передаются под опекунство

специального системного процесса, а не их «бабушкам-дедушкам».

Как правило, процессы-предки заинтересованы в получении информации

о состоянии своих потомков — в противном случае процессы-потомки по

завершении превращаются в «зомби» и пребывают в таком виде, пока не

завершит работу родительский процесс. Тогда системный процесс, ставший

«приемным родителем», сможет обратиться к вызову wait и удалить

«зомбированный» процесс. Учитывая, что многие процессы могут

исполняться в системе довольно длительное время (до нескольких месяцев),

такое «невнимание» к дочерним процессам может привести к заполнению

системных таблиц ненужным мусором. Если ожидание потомков невозможно,

по тем или иным причинам, процесс может использовать сигналы для

предотвращения «зомбироваиия» — подробнее об этом будет рассказано в

следующем разделе.

Системный вызов waitpid может вернуть состояние изменившего его

дочернего процесса только один раз (такой дочерний процесс называется

ожидаемый). Или другими только один раз словами: ожидаемый потомок

перестает быть ожидаемым, если отчет об изменении его состояния уже

получен. Это означает следующее: если в одной точке программы было

получено состояние потомка и вдруг обнаружилось, что это не тот потомок,

Page 88: Лабораторная работа № 1

- 88 -

которого ожидали, то нет никакого способа вернуть результат обратно в

систему, чтобы другой waitpid мог получить его (Хотя waitpid может сделать

это сам).

Если к моменту вызова waitpid имеется ожидаемый дочерний процесс,

соответствующий заданному аргументу pid, управление в вызывающую

программу возвращается немедленно. Если потомок найден, но еще не

изменил снос состояние, системный вызов waitpid блокируется до появления

подходящего ожидаемого потомка. Если потомок не был найден, вызывающей

программе возвращается значение -1 и код ошибки ECHILD. Это может

произойти потому, что аргумент pid задан неправильно пни процесс потомок

перестал быть ожидаемым, т. е. его состояние уже было получено.

Если в качестве аргумента statusp передан непустой указатель, то по

заданному адресу записывается код состояния потомка. Он представляет

собой комбинацию аргумента системного вызова _exit или exit (если речь идет

о завершении потомка) и числа, описывающего причину завершения или

приостановки.

Последний аргумент options может содержать один или более флагов,

объединенных операцией ИЛИ:

WCONTINUED Сообщать о возобновлении работы потомка

WNOHANGНе ожидать потомка, если он еще не изменил свое состояние,

возвращать 0 вместо идентификатора процесса.

WUNTRACED Сообщать о приостановке в работе потомка

Несколько примеров, использующих системный вызов waitpid:

1. /* Ожидать завершения потомка pid и получить код завершения */

waitpid(pid, &status, 0)

Page 89: Лабораторная работа № 1

- 89 -

2. /* Ожидать завершения любого из потомков, без получения кода

завершения */

pid = waitpid(-1, NULL, 0)

3. /* Получить от любого из потомков по групповому идентификатору

pgid сообщение о завершении или о приостановке и получить код

состояния. Не ожидать, если потомок еще не изменил состояние */

pid = waitpid(-pgid, &status, WNOHANG | WUNTRACED)

Системный вызов wait

Второй представитель группы системных вызовов wait представляет

собой упрощенный вариант waitpid и эквивалентен вызову последнего с

аргументом pid, равным -1, и с аргументом options равным нулю.

wait − ожидает завершения дочернего процесса:

#include <sys/wait.h>

pid_t wait(

int *statusp, /* указатель на статус или NULL */

);

/* Возвращает идентификатор процесса или -1 в случае ошибки (код ошибки − в

переменной errno) */

Системный вызов wait довольно редко используется в больших

приложениях, поскольку ожидает завершения любого потомка. Дело в том,

что когда некоторая функция, являющаяся частью большого приложения,

создает дочерний процесс и пытается его подождать, она может случайно

«дождаться» завершения совершенно другого потомка, внося беспорядок и

сумятицу в ход исполнения всего приложения в целом. Для таких случаев

waitpid подходит гораздо лучше, так как он позволяет ожидать конкретный

Page 90: Лабораторная работа № 1

- 90 -

процесс или, по крайней мере, члена группы процессов. Никогда не

используйте wait при написании библиотечных функций, которые создают

дочерние процессы.

Предположим, что процесс имеет двух потомков и заранее не известно,

какой из них завершит работу первым. Если нам нужно только дождаться

завершения обоих, можно сначала подождать завершения любого из

потомков, а потом вызвать waitpid для ожидании завершения другого. Или

родительский процесс может выполнять какую-либо работу и периодически

вызывать waitpid с флагом WNOHANG для каждого из потомком. Но никогда

не используйте waitpid с аргументом pid, равным -1, если приложением не

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

проектах, где над отдельными частями работают целые группы разработчиков

(например, библиотеки для работы с изображениями или библиотеки для

взаимодействия с базами данных) таких гарантий дать никто не может. Было

бы просто здорово, если бы существовала такая разновидность системного

вызова wait, которому можно было бы передать целый массив

идентификаторов, но, увы, такого вызова нет.

Системный вызов waitid

Третий представитель группы системных вызовов wait − waitid, был

введен в UNIX , спецификацией SUS1. Он имеет одну новую и очень важную

особенность: вы можете получить код состояния процесса и оставить его

ожидаемым, не причиняя никакого вреда, если случайно «дождались» не того

потомка. Кроме того, он более информативен по сравнению с waitpid или wait.

waitid — ожидает изменения состояния дочернего процесса [Х/Ореn]:

Page 91: Лабораторная работа № 1

- 91 -

#include <sys/wait.h>

int waitid(

idtype_t idtype, /* тип идентификатора */

id_t id, /* идентификатор */

siginfo_t *infop, /* возвращаемые сведения */

int options /* флаги */

);

/* Возвращает 0 в случае успеха, -1 в случае ошибки (код ошибки в

переменной errno) */

siginfo_t — структура для waitid:

typedef struct { /* показаны только поля имеющие отношение к wait */

int si_code; /* код */

pid_t si_pid; /* идентификатор процесса-потомка */

uid_t si_uid;/* реальный идентификатор пользователя процесса-потомка */

int si_status; /* код завершения или сигнал */

} siginfo_t;

Первый аргумент idtype может принимать одно из следующих значений:

P_PID

Аргумент id является идентификатором ожидаемого процесса − ожидается

изменение состояния потомка с идентификатором процесса id (аналоги

waitpid с pid>0)

P_PGID

Аргумент id является групповым идентификатором − ожидается изменение

состояния потомка из группы и идентификатором id (аналогично waitpid с

pid<-i)

P_ALLАргумент id игнорируется — ожидается изменение состояния любого потомка

(аналогично waitpid с pid==-1)

Page 92: Лабораторная работа № 1

- 92 -

Системный вызов waitid не имеет прямого эквивалента waitpid с pid==0,

но то же самое можно сделать, если в первом аргументе передать значение

P_PGID, а во втором — групповой идентификатор вызывающего процесса.

Аргумент options может содержать один или более флагов, объединенных

операцией ИЛИ:

WEXITEDСообщать о завершении потомка (всегда подразумевается в waitpid,

который не имеет такого флага)

WSTOPPEDСообщать о приостановке потомка (подобно waitpid c флагом

WUNTRACED)

WCONTINUEDСообщать о возобновлении работы потомком (аналогичный флаг

предусмотрен и в waitpid)

WNOHANGНе ожидать изменения состояния потомка. Если оно еще не изменилось

— возвращать значение 0 (аналогичный флаг предусмотрен и в waitpid)

WNOWAITОставить потомка ожидаемым. Таким образом, отследить изменение

состояния потомка можно будет несколькими вызовами waitpid

Через аргумент infop системный вызов waitpid может вернуть

значительно больше информации, чем те же waitpid или wait. Этот аргумент

является указателем на структуру типа siginfo_t. Выше показаны только те

поля структуры, которые имеют непосредственное отношение к системному

вызову waitpid. По возвращении из вызова, идентификатор процесса-потомка

находится в поле si_pid. Поле si_code содержит информацию о причине

завершения потомка. Наиболее часто встречающиеся коды:

CLD_EXITEDПотомок завершил работу посредством обращения к вызову _exit

или exit

CLD_KILLED Аварийное завершение потомка (по сигналу)

CLD_DUMPED Аварийное завершение потомка, создан файл с дампом памяти

CLD_STOPPED Потомок приостановлен

CLD_CONTINUED Потомок возобновил работу

Page 93: Лабораторная работа № 1

- 93 -

Если код 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, описанные выше.

Page 94: Лабораторная работа № 1

- 94 -

Ø Получение идентификатора процесса

Процесс может получить свой идентификатор и тдентификатор предка с

помощью системных вызовов:

1. getpid – возвращает идентификатор процесса:

#include <unistd.h>

pid_t getpid(void );

/* Возвращает идентификатор процесса */

2. getppid – возвращает идентификатор родительского процесса:

#include <unistd.h>

pid_t getppid(void );

/* Возвращает идентификатор родительского процесса */

Page 95: Лабораторная работа № 1

- 95 -

Ø Получение и изменение приоритета

Каждому процессу назначается значение параметра 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) */

Системный вызов nice добавляет приращение incr к текущему значению

параметра nice. Результат должен получиться в диапазоне от 0 до 39

включительно. Если он вышел за пределы указанного диапазона, используется

ближайшее допустимое значение. Только суперпользователь может

уменьшить значение параметра nice, повысив тем самым приоритет

обслуживания.

Page 96: Лабораторная работа № 1

- 96 -

Фактически, системный вызов nice возвращает новое значение nice,

уменьшенное на 20, таким образом, возвращаемое значение лежит в диапазоне

от -20 до 19, при условии, что NZER0==20. Однако возвращаемое значение

редко используется в программах. Особенно если учесть, что новое значение

nice, равное 19, неотличимо от признака ошибки (19-20=-1). Эта ошибка

остается неисправленной.

Существуют два более новых системных вызова – getpriority и setpriority,

которые также могут использоваться для управления приоритетом

приложения. За дополнительной информацией обращайтесь к [SUS2002] или к

документации по вашей системе.

Page 97: Лабораторная работа № 1

- 97 -

· ПОТОКИ

Ø Создание потока

В примерах, встречавшихся до сих пор, создаваемые нами процессы (с

помощью вызова fork) имели единственную последовательность исполнения,

которая называется потоком. Процесс двигался вперед от инструкции к

инструкции, пользуясь единственным стеком, изменяя глобальные данные и

используя системные ресурсы, порой в результате обращения к системным

вызовам.

Благодаря наличию в UNIX поддержки потоков POSIX, процесс может

иметь несколько последовательностей исполнения, каждая из которых

исполняет свою собственную последовательность команд и обладает своим

собственным стеком. Все остальное, чем владеет процесс, включая

глобальные данные и ресурсы, например открытые файлы или текущий

каталог, находятся в совместном использовании у потоков. Следующий

пример наглядно показывает, что имелось ввиду:

static long x = 0;

static void *thread_func(void *arg)

{

while (true) {

printf("Поток 2, значение счетчика %ld\n", ++х);

sleep(1);

}

}

ini main(void)

{

pthread_t tid;

ec_rv( pthread_create(&tid, NULL, thread_func, NULL))

while (x < 10) {

Page 98: Лабораторная работа № 1

- 98 -

printf("Поток 1, значение счетчика %ld\n", ++x);

sleep(2);

}

return EXIT_SUCCESS;

EC_CLEANUP_BGN

return EXIT_FAILURE;

EC_CLEAANUP_END

}

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

Вот что получилось у нас:

Поток 2, значение счетчика 1

Поток 1, значение счетчика 2

Поток 2, значение счетчика 3

Поток 1, значение счетчика 4

Поток 2, значение счетчика 5

Поток 2, значение счетчика 6

Поток 1, значение счетчика 7

Поток 2, значение счетчика 8

Поток 2, значение счетчика 9

Поток 1, значение счетчика 10

Поток 2, значение счетчика 11

Поток 2, значение счетчика 12

Как видите, оба потока, один из которых представлен функцией main,

другой функцией thread_func, изменяют одну и ту же глобальную переменную

х.

Первичный поток, который представлен функцией main, запускает

втором поток с помощью системного вызова pthread_create:

pthread_create – создает новый поток:

Page 99: Лабораторная работа № 1

- 99 -

#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

);

/* Возвращает код завершения */

Четвертый аргумент системного вызова pthread_create напрямую

передается функции запуска. Это указатель на тип void и зачастую он

действительно используется как указатель на некоторые данные. Поскольку

потоки находятся в одном и том же адресном пространстве, они оба могут

пользоваться этим указателем. При желании вы можете передать в этом

аргументе простое целое число, но при этом необходимо выполнить

приведение типа передаваемого значения к типу void*. А чтобы полностью

обезопасить себя, вы должны проверить (например, с помощью функции

assert) совместимость типа передаваемого аргумента с типом void*, примерно

таким образом:

assert(sizeof(long) <= sizeof(void *));

Page 100: Лабораторная работа № 1

- 100 -

Атрибут – это не просто набор флагов, это, скорее всего объект типа

pthread_attr_t, который вы должны инициализировать такими системными

вызовами, как ptnread_setscope и pthread_attr_setstacksize. В примерах мы

всегда будем использовать атрибуты ПО умолчанию, то есть второй аргумент

pthread_create всегда будет NULL.

Системные вызовы семейства «pthread» в случае успешного выполнения

возвращают значение 0, а в случае неудачи — код ошибки. Они не изменяют

глобальную переменную errno. На этот случай в нашем распоряжении имеется

специальный макрос ec_rv, который принимает код ошибки и интерпретирует

его так, как будто им был получен из переменной errno.

Page 101: Лабораторная работа № 1

- 101 -

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

Поток может дождаться окончания работы другого потока и получить код

его завершения с помощью pthread_join (аналог системного вызова wait).

pthread_join — ожидает завершения потока:

#include <pthread.h>

int pthread_join(

pthread_t thread_id, /* идентификатор потока */

void **status_ptr /* код завершения (или NULL, если не требуется) */

);

/* Возвращает 0 в случае успеха, в противном случае - код ошибки */

Ниже приводится немного измененный пример работы двух потоков. На

этот раз поток 1 передает потоку 2 значение счетчика, на котором он должен

завершить свою работу и вернуть значение переменной х обратно потоку 1:

static long x = 0;

static void *thread_func(void *arg) {

while (x < (long)arg) {

printf("Поток 2, значение счетчика %ld\n", ++x);

sleep(1);

}

return (void *)x;

}

int main(void)

{

pthread_t tid;

void *status;

assert(sizeof(long) <= sizeof(void *));

Page 102: Лабораторная работа № 1

- 102 -

ec_rv(pthread_create(&tid, NULL, thread_func, (void *)6))

while (x < 10) {

printf("Поток 1, значение счетчика %ld\n", ++x);

sleep(2);

}

ec_rv(pthread_join(tid, &status))

printf("Код завершения Потока 2: %ld\n", (long)status);

return EXIT_SUCCESS;

EC_CLEANUP_BGN

return EXIT_FAILURE;

EC_CLEANUP_END

}

Результат работы примера:

Поток 1, значение счетчика 1

Поток 2, значение счетчика 2

Поток 2, значение счетчика 3

Поток 1, значение счетчика 4

Поток 2, значение счетчика 5

Поток 2, значение счетчика 6

Поток 1, значение счетчика 7

Поток 1, значение счетчика 8

Поток 1, значение счетчика 9

Поток 1, значение счетчика 10

Код завершения Потока 2: 7

Page 103: Лабораторная работа № 1

- 103 -

Ø Принудительное завершение потока

Системный вызов pthread_cancel

Любой поток может принудительно завершить работу другого потока с

помощью системного вызова pthread_cancel.

pthread_cancel — принудительно завершает работу потока:

#include <pthread.h>

int pthread_cancel(

pthread_t thread_id /* идентификатор потока */

);

/* Возвращает 0 в случае успеха, в противном случае - код ошибки */

Как правило, принудительное завершение работы потока может

произойти только в точке завершения, в качестве которой может служить один

из двухсот с лишним системных вызовов и стандартных функций, которые

могут быть заблокированы, например read, waitpid или pthread_wait_condition.

Если поток вызывает функцию, определенную в любом месте программы, то

ее можно рассматривать как потенциальную точку завершения, потому что

вызываемая функция может обращаться к одному из системных вызовов или

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

Назначение точек завершения состоит в том, чтобы дать возможность

выполнить обычный код, не беспокоясь о том, что работа потока может быть

прервана в самый неподходящий момент. Например, вы можете вносить

изменения в связанные списки (возможно под защитой мьютекса), будучи

уверенными, что последовательность необходимых действий будет выполнена

до конца.

Ни один из системных вызовов «pthread», работающий с мьютексами, не

может быть точкой завершения, так же как и функции free, malloc, calloc или

realloc. Если бы такое было возможно, работа с мьютексами стала бы весьма

обременительной, поскольку каждый раз при обращении к pthread_mutex_lock

Page 104: Лабораторная работа № 1

- 104 -

вам пришлось предусматривать обработку ситуации с принудительным

завершением.

С другой стороны, если поток вообще не обращается к функциям,

которые могут исполнять роль точки завершения и не предусмотрено варианта

самостоятельного завершения его работы, то такой поток может «жить» очень

долго. В такой ситуации можно предусмотреть одно или несколько обращений

к системному вызову pthread_testcancel в подходящих для этого местах, чтобы

обозначить возможные точки завершения. Эти вызовы не делают абсолютно

ничего, если не было команды на принудительное завершение.

Системный вызов pthread_testcancel

pthread_testcancel — проверяет наличие команды на принудительно!

Завершение

#include <pthread.h>

void pthread_testcancel(void);

Как правило, принудительное завершение работы потока может

произойти только в точке завершения, и это действительно так, если выбран

тип принудительного завершения PTHREAD_CANCEL_DEFERRED. Это

значение по умолчанию, которое может быть изменено на

PTHREAD_CANCEL_ASYNCHRONOUS при обращении к вызову

pthread_setcanceltype (см. [SUS2002]), в этом случае поток будет завершен

немедленно, со всеми вытекающими отсюда последствиями.

Page 105: Лабораторная работа № 1

- 105 -

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”

Page 106: Лабораторная работа № 1

- 106 -

v РЕКОМЕНДУЕМЫЕ ИНТЕРНЕТ РЕСУРСЫ

Ø Литература

§ Э. Таненбаум – “Современные операционные системы, 2-ое издание”

· http://www.hub.ru/modules.php?name=Downloads&d_op=getit&lid=105

§ Э. Таненбаум, А. Вудхалл – “Операционные системы: разработка и

реализация,

2-ое издание”

· http://c-books.info/books/load.php?ty=os&lng=RU

§ В. Столлингс – “Операционные системы, 4-ое издание”

· http://c-books.info/books/load.php?ty=os&lng=RU

§ Дж. Рихтер – “Windows для профессионалов: cоздание эффективных

Win32-пpилoжeний с учетом специфики 64-разрядной версии Windows”

· http://rapidshare.com/files/1607837/windows_dlja_professionalov.rar

· http://irazin.ru/Downloads/Books/Richter.rar − учебник

· http://irazin.ru/Downloads/BookSamples/Richter.zip − примеры к учебнику

§ А. Робачевский – “Операционная система Unix”

· http://www.hub.ru/modules.php?name=Downloads&d_op=getit&lid=84

Page 107: Лабораторная работа № 1

- 107 -

Ø Полезные ссылки

§ Сборник документации по программированию

· http://doks.gorodok.net/

§ Интернет Университет Информационных технологий

· http://www.intuit.ru/

§ Интернет ресурс посвященный операционной системе Unix

· http://www.opennet.ru/

§ Интерактивная система просмотра системных руководств (man-ов)

· http://www.opennet.ru/man.shtml

Page 108: Лабораторная работа № 1

- 108 -

Ø Программное обеспечение

§ Программа для просмотра файлов формате PDF

· http://www.foxitsoftware.com/downloads/

§ Программа для просмотра файлов в формате DjVu

· http://hamradio.online.ru/ftp2/DjVuSolo3.1.exe − программа

· http://msilab.net/download/download.php?ad=3230 − русификатор

§ Программа для автоматической загрузки файлов с файлового сервера

rapidshare

· http://www.rapget.com/download.html