Учет пользователей объектов ядра 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Учет пользователей объектов ядра



Содержание

1.Объекты ядра_ 3

2. Учет пользователей объектов ядра_ 3

3. Защита объектов ядра_ 3

4. Таблица описателей объектов ядра_ 5

5. Создание объекта ядра_ 5

6. Закрытие объекта ядра_ 6

7. Совместное использование объектов ядра несколькими процессами_ 7

8. Наследование описателя объекта_ 7

9. Изменение флагов описателя_ 8

10. Именованные объекты_ 9

11. Дублирование описателей объектов_ 12

12. Синхронизация с потоков с помощью объектов ядра_ 13

13. Wait-функции_ 15

13.1. Побочные эффекты успешного ожидания_ 17

14. События_ 18

15. Семафоры_ 20

16. Мьютексы_ 21

17. Отказ от объекта-мьютекса_ 23

18. Мьютексы и критические секции_ 23

19. Сводная таблица объектов, используемых для синхронизации потоков_ 24

20. Порядок выполнения работы_ 25

21. Контрольные вопросы_ 25


1.Объекты ядра

Создание, открытие и прочие операции с объектами ядра являются при разработке Windows-приложений, повседневной рутиной. Система позволяет создавать и оперировать с несколькими типами таких объектов, в том числе, маркерами доступа (access token objects), файлами (file objects), проекциями файлов (file-mapping objects), портами завершения ввода-вывода (I/O completion port objects), заданиями (job objects), почтовыми ящиками (mailslot objects), мьютексами (mutex objects), каналами (pipe objects), процессами (process objects), семафорами (semaphore objects), потоками (thread objects) и ожидаемыми таймерами (waitable timer objects).

Каждый объект ядра — на самом деле просто блок памяти, выделенный ядром и доступный только ему. Этот блок представляет собой структуру данных, в элементах которой содержится информация об объекте. Некоторые элементы (дескриптор защиты, счетчик числа пользователей и др.) присутствуют во всех объектах, но большая их часть специфична для объектов конкретного типа. Например, у объекта «процесс» есть идентификатор, базовый приоритет и код завершения, а у объекта «файл» — смещение в байтах, режим разделения и режим открытия.

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

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

Учет пользователей объектов ядра

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

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

Защита объектов ядра

Объекты ядра можно защитить дескриптором защиты (security descriptor), который описывает, кто создал объект и кто имеет права на доступ к нему. Дескрипторы защиты обычно используют при написании серверных приложений; создавая клиентское приложение, можно игнорировать это свойство объектов ядра.

Замечание:
В Windows 98 дескрипторы защиты отсутствуют, так как она не предназначена для выполнения серверных приложений. Тем не менее, при разработке приложений необходимо знать о тонкостях, связанных с защитой, и реализовать соответствующие механизмы, чтобы приложение корректно работало и в Windows 2000.

Почти все функции, создающие объекты ядра, принимают указатель на структуру SECURITY_ATTRIBUTES как аргумент, например:

HANDLE CreateFileMapping(

HANDLE hFile,

PSECURITY_ATTRIBUTES psa,

DWORD flProtect,

DWORD dwMaximumSizeHigh,

DWORD dwMaximumSizeLow, PCTSTR pszName);

 

Большинство приложений вместо этого аргумента передает NULL и создает объект с защитой по умолчанию. Такая защита подразумевает, что создатель объекта и любой член группы администраторов получают к нему полный доступ, а все прочие к объекту не допускаются. Однако можно создать и инициализировать структуру SECURITY_ATTRIBUTES, а затем передать ее адрес. Она выглядит так:

 

typedef struct _SECURITY_ATTRIBUTES {

DWORD nLength;

LPVOID lpSecurityDescriptor;

BOOL bInheritHandle;

} SECURITY_ATTRIBUTES;

Хотя структура называется SECURITY__ATTRIBUTES, лишь один ее элемент имеет отношение к защите — lpSecuntyDescnptor. Если надо ограничить доступ к созданному объекту ядра, создайте дескриптор защиты и инициализируйте структуру SECURITY_ATTRIBUTES следующим образом:

SECURITY_ATTRIBUTES sa;

sa.nLength = sizeof(sa); // используется для выяснения версий
sa.lpSecuntyDescriptor = pSD, // адрес инициализированной SD
sa.bInheritHandle = FALSE; /* используется при наследовании объектов ядра дочерними процессами TRUE – объект ядра наследуется, FALSE – не наследуется*/

HANDLE hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE, 0, 1024, "MyFileMapping");

Желая получить доступ к существующему объекту ядра (вместо того чтобы создавать новый), необходимо указать, какие операции будут проводить над объектом. Например, если надо было бы считывать данные из существующей проекции файла, то необходимо осуществить вызов функции OpenFileMapping таким образом:

HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE, "MyFileMapping");

Приложение передавая FILE_MAPREAD первым параметром в функцию OpenFileMapping, сообщает, что, как только мне предоставят доступ к проекции файла, оно будет считывать из нее данные. Функция OpenFileMapping, прежде чем вернуть действительный описатель, проверяет тип защиты объекта. Если пользователя, как зарегистрировавшегося в системе, допускают к существующему объекту ядра «проекция файла», OpenFileMapping возвращает действительный описатель. Но если пользователю отказывают в доступе, OpenFileMapping возвращает NULL, а вызов GetLastError дает код ошибки 5 (или ERROR_ACCESS_DENIED). В основной массе приложений защиту не используют.

Замечание:
Хотя в большинстве приложений нет нужды беспокоиться о защите, многие функции Windows требуют, чтобы им передавалась информация о нужном уровне защиты. Некоторые приложения, написанные для Windows 98, в Windows 2000 толком не работают из-за того, что при их реализации не было уделено должного внимания защите. Представьте, что при запуске приложение считывает данные из какого-то раздела реестра. Чтобы делать это корректно, оно должно вызывать функцию RegOpenKeyEx, передавая значение KEY_QUERY_VALUE, которое разрешает операцию чтения в указанном разделе. Однако многие приложения для Windows 98 создавались без учета специфики Windows 2000. Поскольку Windows 98 не защищает свой реестр, разработчики часто вызывали RegQpenKeyEx со значением KEY_ALL_ACCESS. Так проще и не надо ломать голову над том, какой уровень доступа требуется на самом деле. Но проблема в том, что раздел реестра может быть доступен для чтения и блокирован для записи. В Windows 2000 вызов RegOpenKeyEx со значением KEY_ALL_ACCESS заканчивается неудачно, и без соответствующего контроля ошибок приложение может повести себя совершенно непредсказуемо. Если бы разработчик хоть немного подумал о защите и поменял значение KEY_ALL_ACCESS на KEY_QUERY_VALUE, его продукт мог бы работать в обеих операционных системах. Пренебрежение флагами, определяющими уровень доступа, — одна из самых крупных ошибок, совершаемых разработчиками. Правильное их использование позволило бы легко перенести многие приложения Windows 98 в Windows 2000.

Кроме объектов ядра программа может использовать объекты других типов — меню, окна, курсоры мыши, кисти и шрифты. Они относятся к объектам User или GDI Новичок в программировании для Windows может запутаться, пытаясь отличить объекты User или GDI от объектов ядра. Выяснить, не принадлежит ли объект ядру, проще всего так проанализировать функцию, создающую объект. Практически у всех функций, создающих объекты ядра, есть параметр, позволяющий указать атрибуты защиты, — как у CreateFileMapping.

В то же время у функций, создающих объекты User или GDI, нет параметра типа PSECURITY_ATTRIBUTES, и пример тому — функция CreateIcon.

Таблица описателей объектов ядра

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

Примерное содержание таблицы.

Таблица 4.1. Таблица описателей объектов ядра

Индекс Указатель на блок памяти объекта ядра Маска доступа (DWORD of Flag Bits) Флаги (DWORD of Flag Bits)
  0xF0000000 0x???????? 0x00000000
  0x00000000 (N/A) (N/A)
  0xF0000010 0x???????? 0x00000001

Создание объекта ядра

Когда процесс инициализируется в первый раз, таблица описателей еще пуста. Но стоит одному из его потоков вызвать функцию, создающую объект ядра (например, CreateFileMapping), как ядро выделяет для этого объекта блок памяти и инициализирует его, далее ядро просматривает таблицу описателей, принадлежащую данному процессу, и отыскивает свободную запись. Поскольку таблица еще пуста, ядро обнаруживает структуру с индексом равным единице и инициализирует ее. Указатель устанавливается на внутренний адрес структуры данных объекта, маска доступа — на доступ без ограничений и, наконец, определяется последний компонент — флаги.

Все функции, создающие объекты ядра, возвращают описатели, которые привязаны к конкретному процессу и могут быть использованы в любом потоке данного процесса. Значение описателя представляет собой индекс в таблице описателей, принадлежащей процессу, и таким образом идентифицирует место, где хранится информация, связанная с объектом ядра. Вот поэтому при отладке своего приложения и просмотре фактического значения описателя объекта ядра можно видеть такие малые величины: 1, 2 и т. д. Но помните, что физическое содержимое описателей не задокументировано и может быть изменено. Кстати, в Windows 2000 это значение определяет, по сути, не индекс, а скорее байтовое смещение нужной записи от начала таблицы описателей.

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

Если передан неверный индекс (описатель), функция завершается с ошибкой и GetLastError возвращает 6 (ERROR_INVALID_HANDLE). Это связано с тем, что на самом деле описатели представляют собой индексы в таблице, их значения привязаны к конкретному процессу и недействительны в других процессах.

Если вызов функции, создающей объект ядра, оказывается неудачен, то обычно возвращается 0 (NULL). Такая ситуация возможна только при острой нехватке памяти или при наличии проблем с защитой. К сожалению, отдельные функции возвращают в таких случаях не 0, а -1 (INVALID_HANDLE_VALUE). Например, если CreateFile не сможет открыть указанный файл, она вернет именно INVALID_HANDLE_VALUE. Надо быть осторожным при проверке значения, возвращаемого функцией, которая создает объект ядра. Так, для CreateMutex проверка на INVALID_HANDLE_VALUE бессмысленна.

Закрытие объекта ядра

Независимо от того, как создан объект ядра, по окончании работы с ним его нужно закрыть вызовом CloseHandle:

BOOL CloseHandle(HANDLE hobj);

Эта функция сначала проверяет таблицу описателей, принадлежащую вызывающему процессу, чтобы убедиться, идентифицирует ли переданный ей индекс (описатель) объект, к которому этот процесс действительно имеет доступ. Если переданный индекс правилен, система получает адрес структуры данных объекта и уменьшает в этой структуре счетчик числа пользователей; как только счетчик обнулится, ядро удалит объект из памяти.

Если же описатель неверен, происходит одно из двух. В нормальном режиме выполнения процесса CloseHandle возвращает FALSE, а GetLastError — код ERROR_INVALID_HANDLE. Но при выполнении процесса в режиме отладки система просто уведомляет отладчик об ошибке.

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

Если не вызвать CloseHadle утечка ресурсов (тех же объектов ядра) вполне вероятна, пока процесс еще исполняется. Однако по завершении процесса операционная система гарантированно освобождает все ресурсы, принадлежавшие этому процессу, и в случае объектов ядра действует так: в момент завершения процесса просматривает его таблицу описателей и закрывает любые открытые описатели.

Изменение флагов описателя

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

BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);

Функция принимает три параметра. Первый (bObject) идентифицирует допустимый описатель. Второй (dwMask) сообщает функции, какой флаг (или флаги) необходимо изменить. На сегодняшний день с каждым описателем связано два флага:

#define HANDLE FLAG_INHERIT 0x00000001
#define HANDLE FLAG_PROTECT_FROM_CLOSE 0x00000002

Чтобы изменить сразу все флаги объекта, нужно объединить их побитовой операцией OR. И, наконец, третий параметр функции SetHandleInformation — dwFlags — указывает, в какое именно состояние следует перевести флаги. Например, чтобы установить флаг наследования для описателя объекта ядра

SetHandleInformation(hobj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

а чтобы сбросить этот флаг:

SetHandleInformation(hobj, HANDLE_FLAG_INHERIT, 0);

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

SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
CloseHandle(hobj); // генерируется исключение

 

Если какой-нибудь поток попытается закрыть защищенный описатель, CloseHandle приведет к исключению. Необходимость в такой защите возникает очень редко. Однако этот флаг весьма полезен, когда процесс порождает дочерний, а тот в свою очередь — еще один процесс. При этом родительский процесс может ожидать, что его «внук» унаследует определенный описатель объекта, переданный дочернему. Но тут вполне возможно, что дочерний процесс, прежде чем породить новый процесс, закрывает нужный описатель. Тогда родительский процесс теряет связь с «внуком», поскольку тот не унаследовал требуемый объект ядра. Защитив описатель от закрытия, можно исправить ситуацию, и «внук» унаследует предназначенный ему объект.

У этого подхода, впрочем, есть один недостаток. Дочерний процесс, вызвав:

SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
CloseHandle(hobj);

может сбросить флаг HANDLE_FLAG_PROTECT_FROM_CLOSE и закрыть затем соответствующий описатель. Родительский процесс ставит на то, что дочерний не исполнит этот код. Но одновременно он ставит и на то, что дочерний процесс породит ему «внука», поэтому в целом ставки не слишком рискованны.

Для полноты картины стоит, пожалуй, упомянуть и функцию GetHandleInformation:

BOOL GetHandleInformation(
HANDLE hObj,
PDWORD pdwFlags);

Эта функция возвращает текущие флаги для заданного описателя в переменной типа DWORD, на которую укапывает pdwFlags. Чтобы проверить, является ли описатель наследуемым, сделайте так:

DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0!= (dwFlags & HANDLE_FLAG_INHERIT));

Именованные объекты

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

HANDLE CreateMutex (
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);

HANDLE CreateEvent (
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);

HANDLE CreateSemaphore (
PSECURITY_ATTRIBUTES psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName);

HANDLE CreateWaitableTimer (
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName);

Последний параметр, pszName, у всех этих функций одинаков. При передачи в нем NULL, создается безымянный (анонимный) объект ядра. В этом случае объект может разделяться между процессами либо через наследование (см. предыдущий раздел), либо с помощью DuplicateHandle (см. следующий раздел). А чтобы разделять объект по имени, необходимо присвоить ему какое-нибудь имя. Тогда вместо NULL в параметре pszName нужно передать адрес строки с именем, завершаемой нулевым символом. Имя может быть длиной до MAX_PATH знаков (это значение определено как 260). К сожалению, Microsoft ничего не сообщает о правилах именования объектов ядра. Например, создавая объект с именем JeffObj, никто не застрахован от того, что в системе еще нет объекта ядра с таким именем. И что хуже, все эти объекты делят единое пространство имен. Из-за этого следующий вызов CreateSemaphore будет всегда возвращать NULL:

HANDLE hMutex = CreateMutex(NULL. FALSE, "JeffObj");
HANDLE hSem = CreateSemaphore(NULL, 1, 1, "JeffObj");
DWORD dwErrorCode = GetLastError();

После выполнения этого фрагмента значение dwErrorCode будет равно 6 (ERROR_INVALID_HANDLE). Полученный код ошибки не слишком вразумителен, но другого не дано.

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

HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, "JeffMutex");

Этот вызов заставляет систему создать новый объект ядра "мъютекс" и присвоить ему имя JeffMutex. Заметьте, что описатель hMutexProcessA в процессе А не является наследуемым, — он и не должен быть таковым при простом именовании объектов.

Спустя какое-то время некий процесс порождает процесс В. Необязательно, чтобы последний был дочерним от процесса А; он может быть порожден Explorer или любым другим приложением. (В этом, кстати, и состоит преимущество механизма именования объектов перед наследованием.) Когда процесс В приступает к работе,
исполняется код:

HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, "JeffMutex");

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

Однако, хотя процесс В успешно вызвал CreateMutex, новый объект-мьютекс он не создал. Вместо этого он получил свой описатель существующего объекта-мьютекса. Счетчик объекта, конечно же, увеличился на 1, и теперь этот объект не разрушится, пока его описатели не закроют оба процесса — А и В. Заметьте, что значения описателей объекта в обоих процессах скорее всего разные, но так и должно быть, каждый процесс будет оперировать с данным объектом ядра, используя свой описатель.

Вызывая CreateMutex, процесс В передает ей атрибуты защиты и второй параметр. Так вот, эти параметры игнорируются, если объект с указанным именем уже существует! Приложение может определить, что оно делает: создает новый объект ядра или просто открывает уже существующий, — вызвав GetLastError сразу же после вызова одной из Create-функций:

HANDLE hMutex = CreateMutex(&sa, FALSE, "JeffObj");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// открыт описатель существующего объекта sa.lpSecurityDescriptor и второй параметр (FALSE) игнорируются
} else {
// создан совершенно новый объект sa.lpSecurityDescriptor и второй параметр (FALSE) используются при создании объекта
}

Есть и другой способ разделения объектов по именам. Вместо вызова Create-функции процесс может обратиться к одной из следующих Open-функций:

HANDLE OpenMutex (
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenEvent (
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenSemaphore (
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName),

HANDLE OpenWaitableTimer (
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

Заметьте: все эти функции имеют один прототип. Последний параметр, pszName, определяет имя объекта ядра. В нем нельзя передать NULL — только адрес строки с нулевым символом в конце. Эти функции просматривают единое пространство имен объектов ядра, пытаясь найти совпадение. Если объекта ядра с указанным именем нет, функции возвращают NULL, a GetLastError — код 2 (ERROR_FILE_NOT_FOUND). Но если объект ядра с заданным именем существует и если его тип идентичен тому, что указан, система проверяет, разрешен ли к данному объекту доступ запрошенного вида (через параметр dwDesiredAccess). Если такой вид доступа разрешен, таблица описателей в вызывающем процессе обновляется, и счетчик числа пользователей объекта возрастает на единицу. Если присвоить параметру bInheritHandle значение TRUE, то будет возвращен наследуемый описатель.

Главное отличие между вызовом Create- и Open-функций в том, что при отсутствии указанного объекта Create-функция создает его, а Open-функция просто уведомляет об ошибке.

Microsoft ничего не сообщает о правилах именования объектов ядра. Но представьте себе, что пользователь запускает две программы от разных компаний и каждая программа пытается создать объект с именем «MyObject». Ничего хорошего из этого не выйдет. Чтобы избежать такой ситуации, можно создавать GUID и использовать его строковое представление как имя объекта.

Именованные объекты часто применяются для того, чтобы не допустить запуска нескольких экземпляров одного приложения. Для этого просто вызывается одна из Create-функций в своей функции main или WinMain и создаете некий именованный объект. Какой именно — не имеет ни малейшего значения. Сразу после Create-функции необходимо вызвать GetLastError. Если она вернет ERROR_ALREADY_EXISTS, значит, один экземпляр приложения уже выполняется и новый его экземпляр можно закрыть. Вот фрагмент кода, иллюстрирующий этот прием:

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow} {
HANDLE h = CreateMutex(NULL, FALSE, "{FA531CC1-0497-11d3-A180-00105A276C3E}");
lf (GetLastError() == ERROR_ALREADY_EXISTS){
// экземпляр этого приложения уже выполняется
return(0),
}

// запущен первый экземпляр данного приложения
// перед выходом закрываем объект
CloseHandle(h),
return(0);

Wait-функции

Wait-функции позволяют потоку в любой момент приостановиться и ждать освобождения какого-либо объекта ядра. Из всего семейства этих функций чаще всего используется WaitForSingleObject:

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds);

Когда поток вызывает эту функцию, первый параметр, hObject, идентифицирует объект ядра, поддерживающий состояния «свободен-занят» (То есть любой объект, упомянутый в списке из предыдущего раздела.) Второй параметр, dwMilliseconds, указывает, сколько времени (в миллисекундах) поток готов ждать освобождения объекта.

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

WaitForSingleObject(hProcess, INFINITE);

В данном случае константа INFINITE, передаваемая во втором параметре, подсказывает системе, что вызывающий поток готов ждать этого события хоть целую вечность. Именно эта константа обычно и передается функции WaitForSingleObject, но здесь можно указать любое значение в миллисекундах. Кстати, константа INFINITE определена как 0xFFFFFFFF (или -1). Разумеется, передача INFINITE не всегда безопасна. Если объект так и не перейдет в свободное состояние, вызывающий поток никогда не проснется; одно утешение, тратить драгоценное процессорное время он при этом не будет.

Вот пример, иллюстрирующий, как вызывать WaitForSingleObject co значением таймаута, отличным от INFINITE

DWORD dw = WaitForSlngleObject(hProcess, 5000);

switch (dw)
{
case WAIT_OBJECT_0:
// процесс завершается
break;

case WAIT_TIMEOUT:
// процесс не завершился в течение 5000 мс
break;

case WAIT_FAILED:
// неправильный вызов функции (неверный описатель?)
break;
}

Данный код сообщает системе, что вызывающий поток не должен получать процессорное время, пока не завершится указанный процесс или не пройдет 5000 мс (в зависимости от того, что случится раньше). Поэтому функция вернет управление либо до истечения 5000 мс, если процесс завершится, либо примерно через 5000 мс, если процесс к тому времени не закончит свою работу. Заметьте, что в параметре dwMilliseconds можно передать 0, тогда WaitForSingleObject немедленно вернет управление.

Возвращаемое значение функции WaitForSingleObject указывает, почему вызывающий поток снова стал планируемым. Если функция возвращает WAITOBTECT_0, объект свободен, а если WAIT_TIMEOUT — заданное время ожидания (таймаут) истекло. При передаче неверного параметра (например, недопустимого описателя) WaitForSingleObject возвращает WAIT_ EAILED. Чтобы выяснить конкретную причину ошибки, вы зовите функцию GetLastError.

Функция WaitForMultipleObjects аналогична WaitForSingleObject c тем исключением, что позволяет ждать освобождения сразу нескольких объектов или какого-то одного из списка объектов:

DWORD WaitForMultipleObjects(DWOHD dwCount, CONST HANDLE* phObjects, BOOL fWaitAll, DWORD dwMilliseconds);

Параметр dwCount определяет количество задействованных объектов ядра. Его значение должно быть в пределах от 1 до MAXIMUM_WAIT_OBJECTS (в заголовочных файлах Windows оно определено как 64). Параметр phObject — это указатель на массив описателей объектов ядра.

WaitForMultipleObjects приостанавливает поток и заставляет его ждать освобождения либо всех заданных объектов ядра, либо одного из них. Параметр fWaitAll как раз и определяет, чего именно Вы хотите от функции. Если он равен TRUE, функция не даст потоку возобновить свою работу, пока не освободятся все объекты.

Параметр dwMilliseconds идентичен одноименному параметру функции WaitForSingleObject. Если указать конкретное время ожидания, то пo его истечении функция в любом случае возвращает управление. И опять же, в этом параметре обычно передают INFINITE (будьте внимательны при написании кода, чтобы не создать ситуацию взаимной блокировки).

Возвращаемое значение функции WaitForMultipleObjects сообщает, почему возобновилось выполнение вызвавшего ее потока. Значения WAIT_FAILED и WAIT_TIMEOUT никаких пояснений не требуют. Если Вы передали TRUE в параметре fWaitAll и все объекты перешли в свободное состояние, функция возвращает значение WAIT_OBJECT_0. Если fWaitAll приравнен FALSE, она возвращает управление, как только освобождается любой из объектов. Вы, по-видимому, захотите выяснить, какой именно объект освободился В этом случае возвращается значение от WAIT_OBJECT_0 до WAIT_OBJECT_0 + dwCount - 1. Иначе говоря, если возвращаемое значение не равно WAIT_TIMEOUT или WAIT_FAILED, необходимо вычесть из него значение WAlT_OBJECT_0, и получится индекс в массиве описателей, на который указывает второй параметр функции WaitForMultipleObjects. Индекс подскажет, какой объект перешел в незанятое состояние. Пример.

HANDLE h[3];
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3,

DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);

switch (dw)
{

case WAIT_FAILED:
// неправильный вызов функции (неверный описатель?)
break;

case WAIT_TIMEOUT:
// ни один из объектов не освободился в течение 5000 мс
break;

case WAIT_OBJECTJ) + 0:
// завершился процесс, идентифицируемый h[0], т.e. описателем (hProcess1)
break;

case WATT_OBJECT_0 + 1:
// завершился процесс, идентифицируемый h[1], т.e. описателем (hProcess2)
break;

case WAIT_OBJECT_0 + 2:
// завершился процесс, идентифицируемый h[2], т.e. описателем (hProcess3)
break;
}

Если передать FALSE в параметре fWaitAll, функция WaitForMultipleObjects сканирует массив описателей (начиная с нулевого элемента), и первый же освободившийся объект прерывает ожидание. Это может привести к нежелательным последствиям. Например, поток ждет завершения трех дочерних процессов; при этом передали функции массив с их описателями. Если завершается процесс, описатель которого находится в нулевом элементе массива, WaitForMultipleObjects возвращает управление. Теперь поток может сделать то, что ему нужно, и вновь вызвать эту функцию, ожидая завершения другого процесса. Если поток передаст те же три описателя, функция немедленно вернет управление, и снова получите значение WAIT_OB JECT_0. Таким образом, пока не удалить описатели тех объектов, об освобождении которых функция уже сообщила, код будет работать некорректно.

События

События - самая примитивная разновидность объектов ядра. Они содержат счетчик числа пользователей (как и все объекты ядра) и две булевы переменные: одна сообщает тип данного объекта-события, другая — его состояние (свободен или занят).

События просто уведомляют об окончании какой-либо операции. Объекты-события бывают двух типов: со сбросом вручную (manual-reset events) и с автосбросом (auto-reset events). Первые позволяют возобновлять выполнение сразу нескольких ждущих потоков, вторые — только одного.

Объекты-события обычно используют в том случае, когда какой-то поток выполняет инициализацию, а затем сигнализирует другому потоку, что тот может продол жить работу. Инициализирующий поток переводит объект "событие» в занятое состояние и приступает к своим операциям. Закончив, он сбрасывает событие в свободное состояние. Тогда другой поток, который ждал перехода события в свободное состояние, пробуждается и вновь становится планируемым.

Объект ядра «событие» создается функцией CreateEvent:

HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa, BOOL fManualReset, BOOL fInitialState, PCTSTR pszName);

Параметр fManualReset (булева переменная) сообщает системе, создать событие со сбросом вручную (TRUE) или с автосбросом (FALSE).

Параметру fInitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE). После того как система создает объект событие, CreateEvent возвращает описатель события, специфичный для конкретного процесса. Потоки из других процессов могут получить доступ к этому объекту: 1) вызовом CreateEvent с тем же параметром pszName; 2) наследованием описателя; 3) применением функции DuplicateHandle; и 4) вызовом OpenEvent с передачей в параметре pszName имени, совпадающего с указанным в аналогичном параметре функции CreateEvent. Вот что представляет собой функция OpenEvent.

HANDLE OpenEvent(DWORD fdwAccess, BOOL fInhent, PCTSTR pszName);

Ненужный объект ядра «событие» следует, как всегда, закрыть вызовом CloseHandle. Создав событие, можно напрямую управлять его состоянием. Чтобы перевести его в свободное состояние, необходим вызов:

BOOL SetEvent(HANDLE hEvent);

А чтобы поменять его на занятое:

BOOL ResetEvent(HANDLE hEvent);

Для событий с автосбросом действует следующее правило. Когда его ожидание потоком успешно завершается, этот объект автоматически сбрасывается в занятое состояние. Отсюда и произошло название таких объектов-событий. Для этого объекта обычно не требуется вызывать ResetEvent, поскольку система сама восстанавливает его состояние. А для событий со сбросом вручную никаких побочных эффектов успешного ожидания не предусмотрено.

Рассмотрим небольшой пример тому, как на практике использовать объекты ядра «событие» для синхронизации потоков. Начнем с такого кода.

// глобальный описатель события со сбросом вручную (в занятом состоянии)

HANDLE g_hEvent;

int WINAPI WinMain()
{

// создаем объект событие со сбросом вручную (в занятом состоянии)
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

// порождаем три новых потока

HANDLE hThread[3];

DWORD dwThreadTD;

hThread[0] = _beginthreadex(NULL, 0, WordCount, NULL, 0, &dwThreadlD);
hThread[1] = _beginthreadex(NULL, 0, SpellCheck, NULL, 0, &dwThreadID);
hTbread[2] = _beginthreadex(NULL, 0, GrarrmarCheck, NULL, 0, &dwThreadID);

OpenFileAndReadContentsIntoMemory();

// разрешаем всем трем потокам обращаться к памяти
SetEvent(g_hEvent),

}

DWORD WINAPI WordCount(PVOID pvParam)
{


// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hEvent, INFINITE);
// обращаемся к блоку памяти
return(0);

}

DWORD WINAPI SpellCheck(PVOID pvParam)
{

 

// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hEvent, INFINITE);

// обращаемся к блоку памяти

return(0};
}

DWORD WINAPI GrammarCheck(PVOID pvParam)
{

// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hEvent, INFINITE);

// обращаемся к блоку памяти

return(0);
}

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

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

Если событие со сбросом вручную заменить на событие с автосбросом, программа будет вести себя совершенно иначе. После вызова первичным потоком функции SetEvent система возобновит выполнение только одного из вторичных потоков. Какого именно — сказать заранее нельзя. Остальные два потока продолжат ждать.

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

Семафоры

Объекты ядра «семафор» используются для учета ресурсов. Как и все объекты ядра, они содержат счетчик числа пользователей, но, кроме того, поддерживают два 32 битных значения со знаком: одно определяет максимальное число ресурсов (контролируемое семафором), другое используется как счетчик текущего числа ресурсов.

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

Изначально, когда запросов от клиентов еще нет, сервер не разрешает выделять процессорное время каким-либо потокам в пуле. Но как только серверу поступает, скажем, три клиентских запроса одновременно, три потока в пуле становятся планируемыми, и система начинает выделять им процессорное время Для слежения за ресурсами и планированием потоков семафор очень удобен. Максимальное число ресурсов задается равным пяти, что соответствует размеру буфера. Счетчик текущего числа ресурсов первоначально получает нулевое значение, так как клиенты еще не выдали ни одного запроса. Этот счетчик увеличивается на единицу в момент приема очередного клиентского запроса и на столько же уменьшается, когда запрос передается на обработку одному из серверных потоков в пуле.



Поделиться:


Последнее изменение этой страницы: 2016-04-26; просмотров: 991; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.226.251.68 (0.112 с.)