Гл а в а 3 УПРАВЛЕНИЕ ПОТОКАМИ
СПОМОЩЬЮ ФУНКЦИЙ WinAPI
Вглаве рассмотрены основы многопоточного программирования с помощью функций WinAPI. Дается общее понятие объекта ядра ОС Windows, процесса и потока. Обсуждаются особенности реализации проблемы взаимного исключения потоков одного процесса, т. е. находящихся в общем адресном пространстве (синхронизация в пользователь-
ском режиме), и общий случай синхронизации потоков, относящихся к разным процессам, с помощью объектов ядра. Рассмотрен один из простых механизмов организации связи между потоками разных процессов, например, для обмена данными.
3.1. Объекты ядра
Основные понятия
Операционная система Windows позволяет оперировать несколькими типами объектов ядра, такими как процессы, потоки, проекции файлов, события, семафоры, мьютексы, каналы и т. д.
Каждый объект ядра – это блок памяти, выделенный ядром и доступный только ядру. Этот блок представляет собой структуру данных, в элементах которой содержится информация об объекте. Некоторые элементы (имя объекта, дескриптор защиты, счетчик числа пользователей и др.) присутствуют во всех объектах, но большая их часть специфична для объектов конкретного типа. Например, у объекта «процесс» есть идентификатор процесса, базовый приоритет и код завершения, а у объекта «файл» – смещение в байтах, режим разделения и режим открытия.
Для создания объекта ядра необходимо в программе вызвать функцию Win32 API. Например, CreateFileMapping() заставляет систему сформировать объект «проекция файла» (file-mapping object).
Поскольку структуры объектов ядра доступны только ядру, приложение не может самостоятельно найти эти структуры в памяти и напрямую модифицировать их содержимое. Такое ограничение введено намеренно, чтобы ни одна программа не нарушила целостность структур объектов ядра. Это же ограничение позволяет вводить, удалять или изменять элементы структур OC, не нарушая работы каких-либо приложений.
Каким же образом приложение может оперировать объектами ядра? Win32 API предусматривает набор функций, обрабатывающих объекты ядра
95
Г л а в а 3. Управление потоками с помощью функций WinAPI
по строго определенным правилам. Доступ к объектам ядра может быть получен только через эти функции.
Функция создания объекта ядра всегда возвращает описатель, идентифицирующий созданный объект. Описатель следует рассматривать как «непрозрачное» 32-битное значение (в программах Си это переменная типа HANDLE), которое может быть использовано любым потоком процесса. Приложение передает описатель объекта ядра Win32-функциям, сообщая системе, какой объект ядра его интересует.
Для бóльшей надежности ОС значения описателей зависят от кон-
кретного процесса. Даже если с помощью какого-либо механизма межпроцессной связи описатель объекта, созданного в процессе «1», передать потоку процесса «2», то любой вызов из процесса «2» со значением полученного описателя даст ошибку.
Объекты ядра принадлежат ядру, а не процессу. Иначе говоря, если процесс вызывает функцию, создающую объект ядра, а затем завершается, объект ядра может быть не разрушен. Это происходит, если созданный одним процессом объект ядра используется другим процессом. Ядро запретит разрушение объекта до тех пор, пока от него не откажутся все пользующиеся объектом процессы.
Одним из элементов данных любого объекта ядра является счетчик его пользователей. В момент создания объекта счетчику присваивается единица. При открытии существующего объекта ядра каждым новым процессом счетчик увеличивается на единицу. При завершении процесса счетчики всех объектов ядра, которые использовал этот процесс, автоматически уменьшаются на единицу. Как только счетчик какого-либо объекта обнуляется, ядро ОС уничтожает этот объект.
Таблица описателей
При инициализации процесса система создает в нем таблицу описателей, используемую только для объектов ядра [8, 34]. Сведения о структуре этой таблицы и управлении ею не задокументированы. На рис. 3.1 представлена примерная структура таблицы описателей.
Индекс |
Указатель на блок памяти |
Маска |
Флаги |
|
объекта ядра |
доступа |
|||
Рис. 3.1. Примерная структура таблицы описателей объектов ядра
При инициализации процесса таблица описателей пуста. При вызове одним из потоков процесса Win32 API функции, создающей объект ядра (например, CreateFileMapping()), ядро выделяет для этого объекта блок памяти и инициализирует его; далее ядро просматривает таблицу описате-
96
3.1. Объекты ядра
лей, принадлежащую данному процессу, и отыскивает свободную запись. Поскольку таблица еще пуста, ядро обнаруживает структуру с индексом 1 и инициализирует ее. Указатель устанавливается на внутренний адрес памяти структуры данных объекта ядра, маска доступа – на доступ без ограничений, и, наконец, определяется последний компонент – флаги.
Все функции, создающие объекты ядра, возвращают описатели (HANDLE), которые привязаны к конкретному процессу и могут быть использованы в его любом потоке. Значение описателя представляет собой индекс в таблице описателей, принадлежащей процессу, следовательно, идентифицирует место, где хранится информация, связанная с объектом ядра.
Независимо от того, как именно создан объект ядра, по окончании работы с ним его нужно закрыть вызовом Win32 API функции
CloseHandle():
BOOL CloseHandle(HANDLE hobj);
Эта функция сначала проверяет таблицу описателей, принадлежащую вызывающему процессу, чтобы убедиться, идентифицирует ли переданный ей описатель объект, к которому этот процесс действительно имеет доступ. Если переданный описатель неверен, функция возвращает FALSE, a функция GetLastError() – код ERROR_INVALID_HANDLE. Если же индекс достоверен, система получает адрес структуры данных объекта ядра и уменьшает в этой структуре счетчик числа пользователей. Как только счетчик обнулится, ядро удалит объект из памяти. Перед самым возвратом управления CloseHandle() удаляет соответствующую запись из таблицы описателей, после этого данный описатель недействителен в вызвавшем закрытие процессе, использовать его нельзя. Запись из таблицы описателей процесса удаляется независимо от того, разрушен объект ядра или нет. После вызова CloseHandle() процесс больше не получит доступ к этому объекту ядра.
Совместное использование объектов ядра несколькими процессами
Существует необходимость в совместном использовании объектов ядра потоками, исполняемыми в разных процессах. Например:
●объекты «проекции файлов» (file-mapping object) позволяют двум процессам, исполняемым на одном компьютере, совместно использовать одни и те же блоки данных;
●«почтовые ящики» (mailslots) и именованные каналы (named pipes) дают возможность обмениваться данными процессам, исполняемым на разных компьютерах в Сети;
97
Гл а в а 3. Управление потоками с помощью функций WinAPI
●мьютексы (mutexes), семафоры (semaphores) и события (events)
позволяют синхронизировать потоки, исполняемые в разных процессах, чтобы одно приложение могло уведомить другое об окончании той или иной операции.
Но описатели объектов ядра имеют смысл только в конкретном процессе, поэтому разделение объектов ядра между несколькими процессами
вWin32 – задача весьма непростая.
Уразработчиков ОС было несколько веских причин сделать описатели «процессозависимыми», основными считаются устойчивость операционной системы к сбоям и защита. Объекты ядра защищены, процесс, прежде чем оперировать с объектом, должен запросить разрешение на доступ к нему у ОС. Процесс-создатель объекта может предотвратить несанкционированный доступ к этому объекту со стороны другого процесса.
Существует три способа разделения объектов ядра.
1. Наследование. Наследование применимо, только если процессы связаны «родственными» отношениями (родительский – дочерний). Например, родительскому процессу доступны один или несколько описателей объектов ядра, и он решает, породив дочерний процесс, передать ему по наследству доступ к своим объектам ядра.
При вызове функции создания объекта в качестве одного из ее пара-
метров передается структура LPSECURITY_ATTRIBUTES, которая определяет среди прочего и наследуемость объекта. Если при создании дочернего процесса в параметрах функции CreateProcess() флаг наследования объектов blnheritHandles выставлен в значение TRUE, то этот дочерний процесс может наследовать объекты. ОС создает дочерний процесс, но не дает ему немедленно начать свою работу. Сформировав в нем, как обычно, новую (пустую) таблицу описателей, ОС считывает таблицу родительского процесса и копирует ее записи в таблицу дочернего, причем в те же позиции. Последний факт чрезвычайно важен, так как означает, что описатели будут идентичны в обоих процессах (родительском и дочернем).
Кроме того, ОС увеличивает значения счетчиков соответствующих объектов ядра, поскольку эти объекты теперь используются обоими процессами.
Сразу после возврата управления функцией CreateProcess() родительский процесс может закрыть свой описатель объекта, и это никак не отразится на способности дочернего процесса манипулировать этим объектом. Чтобы уничтожить какой-то объект ядра, его описатель должны закрыть (вызовом CloseHandle()) оба процесса – родительский и дочерний.
Следует отметить, что наследуются только описатели объектов, существующие на момент создания дочернего процесса. Если родительский
98
3.1. Объекты ядра
процесс создаст после этого новые объекты ядра с наследуемыми описателями, то эти описатели дочернему процессу уже недоступны.
2. Именованные объекты. Второй способ разделения объектов ядра процессами связан с возможностью присвоения имени объекту ядра. Именовать можно многие, но не все виды объектов. Именуются, к примеру, мьютексы, события, семафоры и проекции файлов.
В качестве имени объекта передается строка, завершающаяся нулевым символом, длиной не более 260 символов и не содержащая символов «/». Все именованные объекты разделяют общее для системы пространство имен, поэтомунельзясоздатьдваразнотипныхобъектасодинаковымименем.
Пусть два разных процесса вызовут функцию вида CreateObject(), указав для объектов одного типа одинаковые имена. Для того, кто это сделал первым (например, процесс А), будет отработана обычная процедура создания объекта. Для второго процесса (соответственно, Б) система проверит и убедится, что объект ядра с таким же именем и такого же типа существует. При этом система считает вызов CreateObject() из процесса Б успешным и создает в его таблице описателей новую запись, а затем инициализируетеетак, чтобыонауказываланаужесуществующийобъектядра.
Надо помнить, что вызов процессом Б функции CreateObject() не привел к созданию нового объекта. Процесс Б получил свой описатель, который идентифицирует уже существующий объект. Счетчик объекта, конечно же, увеличился на единицу, и теперь он не разрушится, пока его описатели не закроют оба процесса А и Б. Скорее всего, значения описателей этого объекта в обоих процессах разные, но так и должно быть: каждый процесс будет оперировать с данным объектом ядра, используя свой описатель.
Параметры защиты созданного таким образом объекта будут соответствовать параметрам, переданным процессом А.
Отметим, что если тип объекта, создаваемого процессом Б, не совпадает с уже созданным объектом с тем же именем, то система сгенерирует ошибку, и второй объект не будет создан.
Вместо Create-функций возможно использование одной из Open— функций. Все Open-функции имеют общий прототип:
BOOL OpenHandle(DWORD dwDesiredAccess,
BOOL blnheritHandle,
LPCTSTR IpszName);
Функция OpenHandle() просматривает единое пространство имен объектов ядра, пытаясь найти совпадение. Если объекта ядра с указанным в параметрах функции именем нет или он другого типа, Open-функции воз-
99
Г л а в а 3. Управление потоками с помощью функций WinAPI
вращают NULL. Но если объект ядра с заданным именем существует и если его тип идентичен тому, что указан, система проверяет, разрешен ли к данному объекту доступ запрошенного вида. Если доступ разрешен, таблица описателей в вызывающем процессе обновляется, и счетчик числа пользователей объекта возрастает на единицу. Опишем параметры функции.
Параметр DWORD dwDesiredAccess определяет вид запрашиваемого к объекту доступа. Для каждого объекта ядра имеется свой набор флагов, любая комбинация которых и передается в этом параметре. Доступ может запрашиваться полный или на выполнение над объектом определенных операций.
Параметр BOOL blnheritHandle обозначает свойство наследования. Если значение этого параметра TRUE, то будет получен наследуемый описатель.
Параметр LPCTSTR IpszName определяет имя объекта ядра. Он всегда должен содержать адрес строки с нулевым символом в конце (передавать NULL нельзя).
Главное отличие между вызовом Create— и Ореn-функций в том, что при отсутствии указанного объекта Create-функция создает его, а Ореn— функция просто сообщает об ошибке.
3. Дублирование описателей объектов. Последний механизм совме-
стного использования объектов ядра несколькими процессами – создание дубликата описателя объекта:
BOOL DuplicateHandle (HANDLE hSourceProcessHandle, HANDLE hSourceHandle,
HANDLE hTargetProcessHandle, LPHANDLE IpTargetHandle, DWORD dwDesiredAccess,
BOOL PInheritHandle, DWORD dwOptions);
Эта функция создает копию записи из таблицы описателей процесса
hSourceProcessHandle в таблице процесса hTargetProcessHandle, не обязательно вызвавшего эту функцию. Опишем параметры функции.
Параметр HANDLE hSourceProcessHandle задает специфичный для вызывающего процесса описатель объекта ядра типа «процесс», который является владельцем запрашиваемого объекта.
Параметр HANDLE hSourceHandle задает описатель запрашиваемого объекта ядра любого типа, специфичного для процесса, на который указы-
вает параметр hSourceProcessHandle.
Параметр HANDLE hTargetProcessHandle задает специфичный для вызывающего процесса описатель объекта ядра типа «процесс», в таблицу которого копируется запись (процесс-приемник).
100
Запись от _lunar_ размещена 16.11.2021 в 12:22
Обновил(-а) _lunar_ 10.05.2022 в 10:42
Больший интерес из всех трёх групп объектов представляет категория объект ядра.
Рассмотрим более подробно Kernel object.
Каждый объект ядра — это блок памяти, выделенный ядром ОС и доступный только ядру ОС.
Этот блок памяти представляет собой структуру данных, члены которой хранят информацию об объекте (дескриптор безопасности, счетчик использования и т.д.).
Некоторые члены этих структур одинаковы для всех типов объектов, но большинство относятся к конкретному типу объекта.
Например, объект процесса имеет идентификатор процесса, базовый приоритет и код выхода, тогда как объект файла имеет байтовое смещение, режим sharing mode и режим open mode.
Поскольку структуры данных объекта ядра доступны только ядру ОС, приложение не может найти эти структуры данных в памяти и напрямую изменить их содержимое.
Windows API предлагает набор функций, которые документированными способами взаимодействуют с этими структурами.
Когда используется функция, которая создает объект ядра, она возвращает дескриптор, который идентифицирует объект.
Передавая этот дескриптор различным функциям Windows, система точно знает с каким объектом ядра необходимо взаимодействовать.Объекты ядра принадлежат ядру ОС, а не процессу.
Процесс вызывает функцию, которая создает объект ядра. Если процесс завершается, объект ядра не обязательно уничтожается.
Если другой процесс использует данный объект ядра, ОС знает, что нельзя уничтожать этот объект, пока другой процесс не перестанет его использовать.
Для этого в ОС предусмотрен счетчик использования. Когда объект ядра создается впервые, его счетчик использования устанавливается на 1.
Затем, когда другой процесс получает доступ к существующему объекту ядра, счетчик использования увеличивается на единицу.
Когда процесс завершается, ядро ОС автоматически уменьшает счетчик использования для всех объектов ядра на единицу.
Если счетчик использования объекта становится равным 0, ядро ОС уничтожает объект.Дескриптор безопасности (Security Descriptor).
Объекты ядра могут быть защищены дескриптором безопасности.
Почти все функции, которые создают объекты ядра, имеют указатель на структуру SECURITY_ATTRIBUTES в качестве аргумента.
Именно этим и отличается Kernel object от других категорий (ни у User object, ни у GDI object нет дескрипторов безопасности).
Более подробно что из себя представляет дескриптор безопасности рассмотрим на примере функции CreateProcess
C++ | ||
|
Большинство приложений просто передают NULL для этого аргумента, поэтому объект ядра создается с безопасностью по умолчанию.
Безопасность по умолчанию означает, что любой пользователь группы администраторов и создатель объекта ядра имеют определённый доступ к создаваемому объекту, всем остальным доступ запрещен.
Структура SECURITY_ATTRIBUTES описывается следующим образом
C++ | ||
|
Несмотря на то, что эта структура называется SECURITY_ATTRIBUTES, на самом деле она включает только один член, имеющий какое-либо отношение к безопасности: lpSecurityDescriptor.
Если требуется ограничить доступ к создаваемому объекту ядра, необходимо создать дескриптор безопасности, а затем инициализировать структуру SECURITY_ATTRIBUTES следующим образом:
C++ | ||
|
*PSECURITY_DESCRIPTOR в данном случае определен как LPVOID, не путайте со структурой PISECURITY_DESCRIPTOR
Существуют два способа получить инициализированный Security Descriptor:
— наследовать уже созданный дескриптор безопасности другим процессом
— сформировать специальную строку типа SDDL (Security Descriptor Definition Language) и конвертировать её в указатель с помощью функции ConvertStringSecurityDescriptorToSecurityDescripto r
C++ | ||
|
Рассмотрим следующую SDDL строку
C++ | ||
|
Первое на что необходимо обратить внимание — это на двоеточие, оно служит разделением для групп
Код:
O:owner_sid G:group_sid D:(string_ace1)(string_ace2)... (string_aceN) S:(string_ace1)(string_ace2)... (string_aceN)
Каждый string_ace заключен в круглые скобки и имеет следующую структуру:
Код:
(ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid)
Допустимые типы ace_type
Разрешенные ace_flags
Набор rights (общие права, права доступа к файлам и папкам, особые права реестра и сервисов)
object_guid и inherit_object_guid используются для определения безопасности объектов в Active Directory.
Они не используются при защите файловой системы или реестра. «OA» и «OD» в поле ace_type строки ACE соответствуют ACE, разрешающему и запрещающему объект, соответственно.
В этом случае object_guid содержит GUID объекта, которому разрешено, а inherit_object_guid содержит GUID объекта, от которого он наследует разрешения.
Поле account_sid в структуре ACE обозначает участника безопасности, которому предоставлены или запрещены права доступа, указанные в ACE.
Поле account_sid может содержать SID, который представляет собой длинный структурированный идентификатор, который практически не имеет смысла для человека, или сокращенное обозначение «строка SID» для обычной учетной записи
Список SID строк можно расшифровать на сайте MSDN
Список ACE строк можно расшифровать на сайте MSDN
Таким образом:
O:BA
— владелец (Owner) данного дескриптора безопасности является SDDL_BUILTIN_ADMINISTRATORS (DOMAIN_ALIAS_RID_ADMINS)
G:BA
— группа (Group), в которую входит дескриптор безопасности, также является SDDL_BUILTIN_ADMINISTRATORS (DOMAIN_ALIAS_RID_ADMINS)
Список управления доступом на уровне пользователя DACL (Discretionary Access Control List)
D:(A;;CCDCLCSWRP;;;SY)(A;;CCDCLCSWRP;;;BA)(A;;CCDCLCSWRP;;;IU)(A;;CCDCLCSWRP;;;LS)(A;;0xb;;;AC)
И системный список управления доступом SACL (System Access Control List)
S:(ML;;NX;;;LW)
Следовательно для первой части DACL получается следующая расшифровка (данный DACL относится к группе Directory service object):
A — SDDL_ACCESS_ALLOWED (флаг ACCESS_ALLOWED_ACE_TYPE)
CC — SDDL_CREATE_CHILD (флаг ADS_RIGHT_DS_CREATE_CHILD)
DC — SDDL_DELETE_CHILD (флаг ADS_RIGHT_DS_DELETE_CHILD)
LC — SDDL_LIST_CHILDREN (флаг ADS_RIGHT_ACTRL_DS_LIST)
SW — SDDL_SELF_WRITE (флаг ADS_RIGHT_DS_SELF)
RP — SDDL_READ_PROPERTY (флаг ADS_RIGHT_DS_READ_PROP)
SY — SDDL_LOCAL_SYSTEM (SECURITY_LOCAL_SYSTEM_RID)
и для SACL
ML — SDDL_MANDATORY_LABEL (флаг SYSTEM_MANDATORY_LABEL_ACE_TYPE)
NX — SDDL_NO_EXECUTE_UP (флаг SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP)
LW — Low Mandatory Level (SID S-1-16-4096)
Метки целостности
Моя программа KernelExplorer (её консольная версия) выводит всю возможную информацию об ACL
По мере нахождения новых комбинаций ACE флагов я обновляю код, поэтому возможно заместо текстового представления флагов будет отображаться его цифровое представление.
*В данный момент занят адаптацией информации о ACL для GUI версии.Таблица дескрипторов объекта ядра процесса.
В первой части я вскользь упомянул, что каждый дескриптор имеет записи во внутренней таблице, которые содержат адреса ресурсов и средства идентификации типа ресурса.
Когда процесс инициализируется, система выделяет для него таблицу дескрипторов.
Эта таблица дескрипторов используется только для объектов ядра, User object или GDI object не имеют доступа к ней.
Подробности того, как структурирована и управляется таблица дескрипторов не документированы.
Но при этом, структуру таблицы дескрипторов процесса схематично можно представить следующим образом
Код:
Index Pointer to Kernel Object Memory Block Access Mask Flags 1 0x0000000000000000 0x00000000 0x00000000 2 0x0000000000000000 0x00000000 0x00000000 ... n 0x0000000000000000 0x00000000 0x00000000
Каждая структура содержит указатель на объект ядра, маску доступа и набор флагов.
При первой инициализации процесса его таблица дескрипторов пуста.
Затем, когда поток в процессе вызывает функцию, которая создает объект ядра, ядро ОС выделяет блок памяти для объекта и инициализирует его.
Ядро ОС сканирует таблицу дескрипторов процесса на наличие пустой записи.
Для указателя будет установлен адрес внутренней памяти структуры данных объекта ядра.
Для маски доступа будет установлен гарантированный доступ (granted access).
Набор флагов соответственно устанавливается из переданного Security Descriptor, а в случае NULL — устанавливается безопасность по умолчанию.
Все функции, создающие объекты ядра, возвращают относительные к процессу дескрипторы.
Это значение дескриптора является индексом в таблице дескрипторов процесса, который определяет, где хранится информация об объекте ядра.
Когда вызывается функция, которая принимает дескриптор объекта ядра в качестве аргумента, происходит передача значения.
Внутри функция просматривает таблицу дескрипторов процесса, чтобы получить адрес объекта ядра, с которым необходимо взаимодействовать.
Пулы памяти.
Выгружаемый и невыгружаемый пулы служат ресурсами памяти, которые ОС использует для хранения своих структур данных.
Невыгружаемый пул (Non-paged pool) — используется операционной системой для хранения данных.
Ядро ОС взаимодействует с невыгружаемым пулом когда выполняются подпрограммы обслуживания прерываний (ISR) и отложенные вызовы процедур (DPC), которые являются функциями, связанными с прерываниями.
Примерами могут служить такие системные прерывания, как переключение режима UserMode в KernelMode — syscall либо sysenter (в Windows XP данное прерывание обозначалось int 2e и в ntdll.dll сохранилось в качестве совместимости с более старым ПО).
Невыгружаемый пул расположен только в физической памяти, а виртуальной памяти невыгружаемого пула назначается физическая память.
Общие системные структуры данных, хранящиеся в невыгружаемом пуле, включают ядро ОС и объекты ядра, которые представляют процессы, потоки, мьютексы, семафоры, события, файловые объекты, IRP и др.
Выгружаемый пул (Paged pool) — буфер памяти операционной системы, куда записываются данные файла подкачки, позволяя переориентировать физическую память.
Когда ОС обращается к памяти выгружаемого пула, которая находится в файле подкачки, происходит системное исключение Page fault, и затем диспетчер памяти считывает данные обратно в физическую память.
Из всех объектов ядра реестр (Registry) является основным программным средством, использующим выгружаемый пул.
Важно понимать, что к физической памяти из пользовательского режима доступа нет.
Также как системные потоки не имеют адресного пространства пользовательского режима.
Физической памятью оперирует только ядро ОС, пользователю для этого отведено виртуальное адресное пространство.
Взаимодействие виртуального адресного пространства с физической памятью осуществляется пулами и кучами (Heap) и только функциями ядра ОС.
Поэтому, выделяя память какой-нибудь VirtualAlloc (или любой другой аналогичной функцией), вы даже близко не находитесь к оперативной памяти, физически установленной в компьютере.
Для саморазвития:
неплохая статья от Джона Р. Миченера — «Понимание прав доступа к файлам и реестру Windows»
Объекты операционной системы. Часть 3: Уровень целостности
PS: если интересно, параллельно я веду тему в разделе Windows 11 по исследованию комплекса безопасности Windows Как работает комплекс мер Безопасность Windows
Introduction
In today’s programming world, multi-threading has become an imperative part of any programming language whether it’s .NET, Java or C++. To write highly responsive and scalable applications, you must avail the power of multi threading programming. While working on .NET Framework, I came across various Framework Class Libraries (FCL) for parallel task processing like Task Parallel Library (TPL), Parallel LINQ (PLINQ), Task Factories, Thread Pool, Asynchronous programming modal, etc., all of which behind the scene use power of Windows threads to achieve parallelism. Understanding the basic structure of Windows thread always help developer in implementing and understanding these advanced features like TPL, PLINQ, etc. in a better way and help in visualizing how multiple threads work in a system together, specially when you are trouble shooting multithreaded applications. In this article, I would like to share some of the basics about Windows thread which may help you in understanding how operating system implements threads.
What Windows Thread Consists Of
Let’s start with looking at the basic components of a thread. There are three basic components of Windows thread:
- Thread Kernel Object
- Stack
- TEB
Windows Thread Components
All of these three components together create Windows thread. I tried to explain all of them one by one below but before looking into these three components, let’s have a brief introduction about Windows kernel and kernel objects as these are the most important part of Windows operating system.
What Is Operating System Kernel
Kernel is the main component of any operating system. It is a bridge between applications and hardware. Kernel provides layer of abstraction through which application can interact with hardware.
Kernel is the part of the operating system that loads first, and it remains in physical memory. The kernel’s primary function is to manage the computer’s hardware and resources and allow other programs to run and use these resources. To know more about kernel, visit this link.
What Are Kernel Objects
Kernel needs to maintain lots of data about numerous resources such as processes, threads, files, etc., for that kernel use “kernel data structures” which are known as kernel objects. Each kernel object is simply a memory block allocated by the kernel and is accessible only to the kernel. This memory block is a data structure whose members maintain information about the object. Some members (security descriptor, usage count, and so on) are same across all object types, but most data members are specific to the type of kernel object. Kernel creates and manipulates several types of kernel objects, such as process objects, thread objects, event objects, file objects, file-mapping objects, I/O completion port objects, job objects, mutex objects, pipe objects, semaphore objects, etc.
Winobj Screenshot
If you are curious to see the list of all the kernel object types, then you can use free WinObj tool from Sysinternals located here.
Thread Kernel Object
First and very basic component of Windows thread is thread kernel object. For every thread in system, operating system create one thread kernel object. Operating systems use these thread kernel objects for managing and executing threads across the system. The kernel object is also where the system keeps all the statistical information about the thread. Below are some of the important properties of thread kernel object.
Thread Context
Each thread kernel object contains set of CPU registers, called the thread’s context. The context reflects state of the CPU registers when the thread last executed. The set of CPU registers for the thread is saved in a CONTEXT structure. The instruction pointer and stack pointer registers are the two most important registers in the threads context. A stack pointer is a register that stores the starting memory address of the stack frame of the current function executing inside the thread. Instruction pointer points to the current instruction that need to be executed by the CPU. Operating system use kernel object context information while performing thread context switching. Context switch is the process of storing and restoring the state (context) of a thread so that execution can be resumed from the same point at a later time.
Below mentioned table displays some of other important information held in thread kernel object about the thread.
Property Name | Description |
CreateTime |
This field contains the time when the Thread was created. |
ThreadsProcess |
This field contains a pointer to the EPROCESS Structure of the Process that owns this Thread. |
StackBase |
This field contains the Base Address of this Thread’s Stack. |
StackLimit |
This field contains the end of the Kernel-Mode Stack of the Thread. |
TEB |
This field contains a pointer to the Thread’s Environment Block. |
State |
This field contains the Thread’s current state. |
Priority |
This field contains the Thread’s current priority. |
ContextSwitches |
This field counts the number of Context Switches that the Thread has gone through (switching Contexts/Threads). |
WaitTime |
This field contains the time until a Wait will expire. |
Queue |
This field contains a Queue for this Thread. |
Preempted |
This field specifies if the Thread will be preempted or not. |
Affinity |
This field contains the Thread’s Kernel Affinity. |
KernelTime |
This field contains the time that the Thread has spent in Kernel Mode. |
UserTime |
This field contains the time that the Thread has spent in User Mode. |
ImpersonationInfo |
This field contains a pointer to a structure used when the Thread is impersonating another one. |
SuspendCount |
This field contains a count on how many times the Thread has been suspended. |
Stack
The second basic component of a thread is stack. Once the thread kernel object has been created, the system allocates memory, which is used for the thread’s stack. Every thread got its own stack which is used for maintaining local variables of functions and for passing arguments to functions executing inside a thread. When a function executes, it may add some of its state data to the top of the stack like arguments and local variables, when the function exits it is responsible for removing that data from the stack. Apart from that, a thread’s stack is used to store the location of function calls in order to allow return
statements to return to the correct location.
Operating system allocates two types of stack for every thread, one is user-mode stack and other is kernel-mode stack.
User-mode stack
The user-mode stack is used for local variables and arguments passed to methods. It also contains the address indicating what the thread should execute next when the current method returns. By default, Windows allocates 1 MB of memory for each thread’s user-mode stack
Kernel-mode stack
The kernel-mode stack is used when application code passes arguments to a kernel function in the operating system. For security reasons, Windows copies any arguments passed from user-mode code to the kernel from the thread’s user-mode stack to the thread’s kernel-mode stack. Once copied, the kernel can verify the arguments’ values, and since the application code can’t access the kernel mode stack, the application can’t modify the arguments’ values after they have been validated and the OS kernel code begins to operate on them. In addition, the kernel calls methods within itself and uses the kernel-mode stack to pass its own arguments, to store a function’s local variables, and to store return addresses. The kernel-mode stack is 12 KB when running on a 32-bit Windows system and 24 KB when running on a 64-bit Windows system.
You can learn more about thread stack at the following links:
- http://www.linfo.org/kernel_space.html
- http://en.wikipedia.org/wiki/Stack-based_memory_allocation
- http://en.wikipedia.org/wiki/Call_stack
Thread Environment Block (TEB)
Another important data structure used by every thread is Thread environment Block (TEB). TEB is a block of memory allocated and initialized in user mode (user mode address space is directly accessible to the application code where else kernel mode address space is not accessible to the application code directly). The TEB consumes 1 page of memory (4 KB on x86 and x64 CPUs).
On of the important information TEB contains is information about exception handling which is used by SEH (Microsoft Structured Exception Handling). The TEB contains the head of the thread’s exception-handling chain. Each try
block that the thread enters inserts a node in the head of this chain.The node is removed from the chain when the thread exit the try block. You can learn more about SEH
here.
In addition, TEB contains the thread-local storage data. In multi-threaded applications, there often arises the need to maintain data that is unique to a thread. The place where this thread specific data get stored called thread-local storage. You can learn more about thread-local storage here.
Below mentioned table displays few important properties of TEB:
Property Name | Description |
ThreadLocalStorage |
This field contains the thread specific data. |
ExceptionList |
This field contains the Exception Handlers List used by SEH |
ExceptionCode |
This field contains the last exception code generated by the Thread. |
LastErrorValue |
This field contains the last DLL Error Value for the Thread. |
CountOwnedCriticalSections |
This field counts the number of Critical Sections (a Synchronization mechanism) that the Thread owns. |
IsImpersonating |
This field is a flag on whether the Thread is doing any impersonation. |
ImpersonationLocale |
This field contains the locale ID that the Thread is impersonating. |
Thread kernel object as thread handle
System keeps all information required for thread execution/ scheduling inside thread kernel object. Apart from that, the operating system stores address of thread stack and thread TEB in thread kernel object as shown in the below figure:
Thread kernel object mapping
Thread kernel object is the only handle through which operating system access all the information about the thread and is use it for thread execution/ scheduling.
Thread State
Each thread exists in a particular execution state at any given time. Operating system stores the state of thread inside thread kernel object field «state». Operating system uses these states that are relevant to performance; these are:
- Running — thread is using CPU
- Blocked — thread is waiting for input
- Ready — thread is ready to run (not Blocked or Running)
- Exited — thread has exited but not been destroyed
Thread State Diagram
Thread Scheduler Queues
Operating system thread scheduler maintains thread kernel objects in different queues based on the state of a thread
- Ready queue — Scheduler maintains list containing threads in Ready state and can be scheduled on CPU. Often list is sorted, generally one queue per CPU.
- Waiting queues — A thread in Blocked state is put in a wait queue. Below are few examples which cause thread block.
- Thread kernel object might have a suspend count greater than 0. This means that the thread is suspended
- Thread is waiting on some lock to get release
- Thread is waiting for reply from E.g., disk, console, network, etc.
- Exited queue — A thread in Exited state is put in this queue
Thread scheduler use doubly linked list data structure for maintaining these queues where in a list head points to a collection of list elements or entries and each item points to the next and previous items in the list.
Thread kernel object doubly link list
Scheduler moves threads across queues on thread state change — E.g., thread moves from a wait queue to ready queue on wake up.
How OS Run Threads
As we already know that thread context structure is maintained inside the thread’s kernel object. This context structure reflects the state of the thread’s CPU registers when the thread was last executing. Every 20 milliseconds or so, operating system thread scheduler looks at all the thread kernel objects currently inside Ready Queue (doubly linked list). Thread scheduler selects one of the thread kernel objects and loads the CPU’s registers with the values that were last saved in the thread’s context. This action is called a context switch. At this point, the thread is executing code and manipulating data in its process’ address space. After another 20 milliseconds or so, scheduler saves the CPU’s registers back into the thread’s context. The scheduler again examines the remaining thread kernel objects in Ready Queue, selects another thread’s kernel object, loads this thread’s context into the CPU’s registers, and continues.
Thread Scheduler Diagram
This operation of loading a thread’s context, letting the thread run, saving the context, and repeating the operation begins when the system boots and continues until the system is shut down.
Processes and Threads
One more thing I would like to share is the relationship between thread and process. Every process requires at least one thread. A process never executes anything, it is simply a container for threads. Threads are always created in the context of some process and live their entire life within that process. What this really means is that the thread executes code and manipulates data within its process’ address space. So if you have two or more threads running in the context of a single process, the threads share a single address space. The threads can execute the same code and manipulate the same data.
Process gives structural information to the in-memory copy of your executable program, such as which memory is currently allocated, which program is running, how much memory it is using, etc. The Process however, does not execute any code on its own. It simply allows the OS (and the user) to know to which executable program a certain Thread belongs to. It also contains all the handles and security rights and privileges that threads create. Therefore, code actually runs in Threads.
For understanding, you can make analogy for processes and threads using a regular, everyday object — a house. A house is really a container, with certain attributes (such as the amount of floor space, the number of bedrooms, and so on). If you look at it that way, the house really doesn’t actively do anything on its own — it’s a passive object. This is effectively what a process is.
The people living in the house are the active objects — they’re the ones using the various rooms, watching TV, cooking, taking showers, and so on. We’ll soon see that’s how threads behave. Just as a house occupies an area of real estate, a process occupies memory. And just as a house’s occupants are free to go into any room they want, a processes’ threads all have common access to that memory.
A process, just like a house, has some well-defined «borders.» A person in a house has a pretty good idea when they’re in the house, and when they’re not. A thread has a very good idea — if it’s accessing memory within the process, it can live. If it steps out of the bounds of the process’s address space, it gets killed. This means that two threads, running in different processes, are effectively isolated from each other.
If you want to learn more about process and thread, please read Processes and Threads.
Summary
Three basic components of thread are:
- Thread Kernel Object is the primary data structure through which OS manages thread.
- Thread stack is used for maintaining local variables of functions and for passing arguments to functions executing inside a thread. Operating system allocates two types of stack for every thread, one is user-mode stack and other is kernel-mode stack.
- Thread Environment Block is a block of memory allocated and initialized in user mode primarily used for exception handling and thread-local storage data.
Thread State
Each thread exists in a particular execution state at any given time which are below:
- Running — thread is using CPU
- Blocked — thread is waiting for input
- Ready — thread is ready to run (not Blocked or Running)
- Exited — thread has exited but not been destroyed
Thread Scheduler Queues
Operating system thread scheduler maintains thread kernel objects in different queues based on the state of a thread:
- Ready queue
- Waiting
- Exited queue
How OS Run Threads
Every 20 milliseconds or so, operating system thread scheduler looks at all the thread kernel objects currently inside Ready Queue. Thread scheduler selects one of the thread kernel objects and loads the CPU’s registers with the values in the thread’s context and execute thread.
Processes and Threads
Every process requires at least one thread. A process never executes anything, it is simply a container for threads. Threads are always created in the context of some process and live their entire life within that process.
References
- CLR via C#, Third Edition (February 10, 2010) By Jeffrey Richter
- Windows via C/C++ Fifth Edition (December 2007) by Jeffrey Richter and Christophe Nasarre
- Introduction to NT Internals — Alex Ionescu’s Blog
- Processes and Threads
Introduction
In today’s programming world, multi-threading has become an imperative part of any programming language whether it’s .NET, Java or C++. To write highly responsive and scalable applications, you must avail the power of multi threading programming. While working on .NET Framework, I came across various Framework Class Libraries (FCL) for parallel task processing like Task Parallel Library (TPL), Parallel LINQ (PLINQ), Task Factories, Thread Pool, Asynchronous programming modal, etc., all of which behind the scene use power of Windows threads to achieve parallelism. Understanding the basic structure of Windows thread always help developer in implementing and understanding these advanced features like TPL, PLINQ, etc. in a better way and help in visualizing how multiple threads work in a system together, specially when you are trouble shooting multithreaded applications. In this article, I would like to share some of the basics about Windows thread which may help you in understanding how operating system implements threads.
What Windows Thread Consists Of
Let’s start with looking at the basic components of a thread. There are three basic components of Windows thread:
- Thread Kernel Object
- Stack
- TEB
Windows Thread Components
All of these three components together create Windows thread. I tried to explain all of them one by one below but before looking into these three components, let’s have a brief introduction about Windows kernel and kernel objects as these are the most important part of Windows operating system.
What Is Operating System Kernel
Kernel is the main component of any operating system. It is a bridge between applications and hardware. Kernel provides layer of abstraction through which application can interact with hardware.
Kernel is the part of the operating system that loads first, and it remains in physical memory. The kernel’s primary function is to manage the computer’s hardware and resources and allow other programs to run and use these resources. To know more about kernel, visit this link.
What Are Kernel Objects
Kernel needs to maintain lots of data about numerous resources such as processes, threads, files, etc., for that kernel use “kernel data structures” which are known as kernel objects. Each kernel object is simply a memory block allocated by the kernel and is accessible only to the kernel. This memory block is a data structure whose members maintain information about the object. Some members (security descriptor, usage count, and so on) are same across all object types, but most data members are specific to the type of kernel object. Kernel creates and manipulates several types of kernel objects, such as process objects, thread objects, event objects, file objects, file-mapping objects, I/O completion port objects, job objects, mutex objects, pipe objects, semaphore objects, etc.
Winobj Screenshot
If you are curious to see the list of all the kernel object types, then you can use free WinObj tool from Sysinternals located here.
Thread Kernel Object
First and very basic component of Windows thread is thread kernel object. For every thread in system, operating system create one thread kernel object. Operating systems use these thread kernel objects for managing and executing threads across the system. The kernel object is also where the system keeps all the statistical information about the thread. Below are some of the important properties of thread kernel object.
Thread Context
Each thread kernel object contains set of CPU registers, called the thread’s context. The context reflects state of the CPU registers when the thread last executed. The set of CPU registers for the thread is saved in a CONTEXT structure. The instruction pointer and stack pointer registers are the two most important registers in the threads context. A stack pointer is a register that stores the starting memory address of the stack frame of the current function executing inside the thread. Instruction pointer points to the current instruction that need to be executed by the CPU. Operating system use kernel object context information while performing thread context switching. Context switch is the process of storing and restoring the state (context) of a thread so that execution can be resumed from the same point at a later time.
Below mentioned table displays some of other important information held in thread kernel object about the thread.
Property Name | Description |
CreateTime |
This field contains the time when the Thread was created. |
ThreadsProcess |
This field contains a pointer to the EPROCESS Structure of the Process that owns this Thread. |
StackBase |
This field contains the Base Address of this Thread’s Stack. |
StackLimit |
This field contains the end of the Kernel-Mode Stack of the Thread. |
TEB |
This field contains a pointer to the Thread’s Environment Block. |
State |
This field contains the Thread’s current state. |
Priority |
This field contains the Thread’s current priority. |
ContextSwitches |
This field counts the number of Context Switches that the Thread has gone through (switching Contexts/Threads). |
WaitTime |
This field contains the time until a Wait will expire. |
Queue |
This field contains a Queue for this Thread. |
Preempted |
This field specifies if the Thread will be preempted or not. |
Affinity |
This field contains the Thread’s Kernel Affinity. |
KernelTime |
This field contains the time that the Thread has spent in Kernel Mode. |
UserTime |
This field contains the time that the Thread has spent in User Mode. |
ImpersonationInfo |
This field contains a pointer to a structure used when the Thread is impersonating another one. |
SuspendCount |
This field contains a count on how many times the Thread has been suspended. |
Stack
The second basic component of a thread is stack. Once the thread kernel object has been created, the system allocates memory, which is used for the thread’s stack. Every thread got its own stack which is used for maintaining local variables of functions and for passing arguments to functions executing inside a thread. When a function executes, it may add some of its state data to the top of the stack like arguments and local variables, when the function exits it is responsible for removing that data from the stack. Apart from that, a thread’s stack is used to store the location of function calls in order to allow return
statements to return to the correct location.
Operating system allocates two types of stack for every thread, one is user-mode stack and other is kernel-mode stack.
User-mode stack
The user-mode stack is used for local variables and arguments passed to methods. It also contains the address indicating what the thread should execute next when the current method returns. By default, Windows allocates 1 MB of memory for each thread’s user-mode stack
Kernel-mode stack
The kernel-mode stack is used when application code passes arguments to a kernel function in the operating system. For security reasons, Windows copies any arguments passed from user-mode code to the kernel from the thread’s user-mode stack to the thread’s kernel-mode stack. Once copied, the kernel can verify the arguments’ values, and since the application code can’t access the kernel mode stack, the application can’t modify the arguments’ values after they have been validated and the OS kernel code begins to operate on them. In addition, the kernel calls methods within itself and uses the kernel-mode stack to pass its own arguments, to store a function’s local variables, and to store return addresses. The kernel-mode stack is 12 KB when running on a 32-bit Windows system and 24 KB when running on a 64-bit Windows system.
You can learn more about thread stack at the following links:
- http://www.linfo.org/kernel_space.html
- http://en.wikipedia.org/wiki/Stack-based_memory_allocation
- http://en.wikipedia.org/wiki/Call_stack
Thread Environment Block (TEB)
Another important data structure used by every thread is Thread environment Block (TEB). TEB is a block of memory allocated and initialized in user mode (user mode address space is directly accessible to the application code where else kernel mode address space is not accessible to the application code directly). The TEB consumes 1 page of memory (4 KB on x86 and x64 CPUs).
On of the important information TEB contains is information about exception handling which is used by SEH (Microsoft Structured Exception Handling). The TEB contains the head of the thread’s exception-handling chain. Each try
block that the thread enters inserts a node in the head of this chain.The node is removed from the chain when the thread exit the try block. You can learn more about SEH
here.
In addition, TEB contains the thread-local storage data. In multi-threaded applications, there often arises the need to maintain data that is unique to a thread. The place where this thread specific data get stored called thread-local storage. You can learn more about thread-local storage here.
Below mentioned table displays few important properties of TEB:
Property Name | Description |
ThreadLocalStorage |
This field contains the thread specific data. |
ExceptionList |
This field contains the Exception Handlers List used by SEH |
ExceptionCode |
This field contains the last exception code generated by the Thread. |
LastErrorValue |
This field contains the last DLL Error Value for the Thread. |
CountOwnedCriticalSections |
This field counts the number of Critical Sections (a Synchronization mechanism) that the Thread owns. |
IsImpersonating |
This field is a flag on whether the Thread is doing any impersonation. |
ImpersonationLocale |
This field contains the locale ID that the Thread is impersonating. |
Thread kernel object as thread handle
System keeps all information required for thread execution/ scheduling inside thread kernel object. Apart from that, the operating system stores address of thread stack and thread TEB in thread kernel object as shown in the below figure:
Thread kernel object mapping
Thread kernel object is the only handle through which operating system access all the information about the thread and is use it for thread execution/ scheduling.
Thread State
Each thread exists in a particular execution state at any given time. Operating system stores the state of thread inside thread kernel object field «state». Operating system uses these states that are relevant to performance; these are:
- Running — thread is using CPU
- Blocked — thread is waiting for input
- Ready — thread is ready to run (not Blocked or Running)
- Exited — thread has exited but not been destroyed
Thread State Diagram
Thread Scheduler Queues
Operating system thread scheduler maintains thread kernel objects in different queues based on the state of a thread
- Ready queue — Scheduler maintains list containing threads in Ready state and can be scheduled on CPU. Often list is sorted, generally one queue per CPU.
- Waiting queues — A thread in Blocked state is put in a wait queue. Below are few examples which cause thread block.
- Thread kernel object might have a suspend count greater than 0. This means that the thread is suspended
- Thread is waiting on some lock to get release
- Thread is waiting for reply from E.g., disk, console, network, etc.
- Exited queue — A thread in Exited state is put in this queue
Thread scheduler use doubly linked list data structure for maintaining these queues where in a list head points to a collection of list elements or entries and each item points to the next and previous items in the list.
Thread kernel object doubly link list
Scheduler moves threads across queues on thread state change — E.g., thread moves from a wait queue to ready queue on wake up.
How OS Run Threads
As we already know that thread context structure is maintained inside the thread’s kernel object. This context structure reflects the state of the thread’s CPU registers when the thread was last executing. Every 20 milliseconds or so, operating system thread scheduler looks at all the thread kernel objects currently inside Ready Queue (doubly linked list). Thread scheduler selects one of the thread kernel objects and loads the CPU’s registers with the values that were last saved in the thread’s context. This action is called a context switch. At this point, the thread is executing code and manipulating data in its process’ address space. After another 20 milliseconds or so, scheduler saves the CPU’s registers back into the thread’s context. The scheduler again examines the remaining thread kernel objects in Ready Queue, selects another thread’s kernel object, loads this thread’s context into the CPU’s registers, and continues.
Thread Scheduler Diagram
This operation of loading a thread’s context, letting the thread run, saving the context, and repeating the operation begins when the system boots and continues until the system is shut down.
Processes and Threads
One more thing I would like to share is the relationship between thread and process. Every process requires at least one thread. A process never executes anything, it is simply a container for threads. Threads are always created in the context of some process and live their entire life within that process. What this really means is that the thread executes code and manipulates data within its process’ address space. So if you have two or more threads running in the context of a single process, the threads share a single address space. The threads can execute the same code and manipulate the same data.
Process gives structural information to the in-memory copy of your executable program, such as which memory is currently allocated, which program is running, how much memory it is using, etc. The Process however, does not execute any code on its own. It simply allows the OS (and the user) to know to which executable program a certain Thread belongs to. It also contains all the handles and security rights and privileges that threads create. Therefore, code actually runs in Threads.
For understanding, you can make analogy for processes and threads using a regular, everyday object — a house. A house is really a container, with certain attributes (such as the amount of floor space, the number of bedrooms, and so on). If you look at it that way, the house really doesn’t actively do anything on its own — it’s a passive object. This is effectively what a process is.
The people living in the house are the active objects — they’re the ones using the various rooms, watching TV, cooking, taking showers, and so on. We’ll soon see that’s how threads behave. Just as a house occupies an area of real estate, a process occupies memory. And just as a house’s occupants are free to go into any room they want, a processes’ threads all have common access to that memory.
A process, just like a house, has some well-defined «borders.» A person in a house has a pretty good idea when they’re in the house, and when they’re not. A thread has a very good idea — if it’s accessing memory within the process, it can live. If it steps out of the bounds of the process’s address space, it gets killed. This means that two threads, running in different processes, are effectively isolated from each other.
If you want to learn more about process and thread, please read Processes and Threads.
Summary
Three basic components of thread are:
- Thread Kernel Object is the primary data structure through which OS manages thread.
- Thread stack is used for maintaining local variables of functions and for passing arguments to functions executing inside a thread. Operating system allocates two types of stack for every thread, one is user-mode stack and other is kernel-mode stack.
- Thread Environment Block is a block of memory allocated and initialized in user mode primarily used for exception handling and thread-local storage data.
Thread State
Each thread exists in a particular execution state at any given time which are below:
- Running — thread is using CPU
- Blocked — thread is waiting for input
- Ready — thread is ready to run (not Blocked or Running)
- Exited — thread has exited but not been destroyed
Thread Scheduler Queues
Operating system thread scheduler maintains thread kernel objects in different queues based on the state of a thread:
- Ready queue
- Waiting
- Exited queue
How OS Run Threads
Every 20 milliseconds or so, operating system thread scheduler looks at all the thread kernel objects currently inside Ready Queue. Thread scheduler selects one of the thread kernel objects and loads the CPU’s registers with the values in the thread’s context and execute thread.
Processes and Threads
Every process requires at least one thread. A process never executes anything, it is simply a container for threads. Threads are always created in the context of some process and live their entire life within that process.
References
- CLR via C#, Third Edition (February 10, 2010) By Jeffrey Richter
- Windows via C/C++ Fifth Edition (December 2007) by Jeffrey Richter and Christophe Nasarre
- Introduction to NT Internals — Alex Ionescu’s Blog
- Processes and Threads
Объекты ядра используются системой и нашими приложениями для управления множеством разных ресурсов: процессами, потоками, файлами и т.д.
Система позволяет создавать и оперировать несколькими типами таких объектов, в том числе: маркерами доступа (access token objects), файлами (file objects), проекциями файлов (file-mapping objects), портами завершения ввода вывода (I/O completion port objects), заданиями (jobs), почтовыми ящиками (mailslot objects), мьютексами (mutex objects), каналами (pipe objects), процессами (thread objects) и ожидаемыми таймерами (waitable timer objects).
Эти объекты создаются Windows-функциями. Например, CreateFileMapping заставляет систему сформировать объект проекцию файла. Каждый объект ядра на самом деле просто блок памяти, выделенный ядром и доступный только ему.
Блок представляет собой структуру данных, в элементах которой содержится информация об объекте. Некоторые элементы (дескриптор защиты, счетчик числа пользователей и др.) присутствуют во всех объектах, но большая их часть специфична для объектов конкретного типа. Например, у объекта процесс есть идентификатор, базовый приоритет и код завершения, а у объекта файл — смещение в байтах, режим разделения и режим открытия.
Приложение не может напрямую обращаться к объектам ядра читать и изменять их содержимое.
Для взаимодействия с объектами ядра у Windows предусмотрен набор функций, обрабатывающих структуры объектов ядра по строго определенным правилам. Когда мы создаем объект ядра, функция возвращает описатель идентифицирующий созданный объект (HANDLE). Все операции с текущим объектом ядра возможны только при указании этого описателя управляющей функции.
Для большей защиты, описатели уникальны только внутри конкретного процесса. Поэтому, передавая по межпроцессорной связи описатель объекта другому процессу, используя описатель в другом процессе, мы получим ошибку. Это ограничение можно обойти, но об этом позже.
Учет пользователей объектов ядра.
Объекты ядра принадлежат ядру, а не процессу. Это говорит о том, что завершая работу с процессом, мы не обязательно разрушаем объект ядра. В большинстве случаев объект разрушается, но если созданный вами объект ядра используется другим процессом, ядро запретит разрушение объекта до тех пор, пока от него не откажется последний пользователь.
В каждом объекте, как уже говорилось, есть счетчик пользователей объектом. В момент создания счетчику присваивается значение 1. Соответственно, при обращении к нему другого процесса, счетчик увеличивается на 1. Когда пользовательский процесс завершается, счетчик уменьшается. Система удаляет объект ядра, когда счетчик обнуляется.
Почти во всех функциях, создающих объекты, есть параметр SECURITY_ATTRIBUTES. Он необходим для защиты объектов ядра от несанкционированного доступа. Чаще всего это свойство используют в серверных приложениях, в клиентских приложениях этот объект можно игнорировать.
Если вы пишете для win98, то там такое свойство отсутствует, но для корректной работы таких приложений в Win2000 и выше, стоит помнить о работе с соответствующими механизмами.
При передаче в таком атрибуте NULL большинство объектов будут созданы со стандартными свойствами защиты, когда к объекту допускаются: создатель и администраторская группа. Остальные игнорируются.
Более подробно о защите объектов ядра в следующий раз.
Ядро ОС — часть ОС, рационально размещаемая в ОЗУ, которая работает в привилегированном режиме и выполняет следующие функции:
— Управление и синхронизация процессов.
— Управление памятью.
— Диспетчеризация.
— Управление файловой системой, прерываниями, IO и т.д.
Доступ к функциям ядра ОС осуществляется через прерывания:
— программные
— аппаратные
Вход в ядро осуществляется по прерываниям. Когда ядро реагирует на данное прерывание, оно запрещает другие (если ядро не реентерабельно). После идентификации поступившего прерывания, ядро передает его обработчику – специальному системному процессу. Запрет прерываний обеспечивает предотвращение повторного входа в нереентерабельное ядро.
Нереентерабельность функций ДОС связана тем, что при повторном вхождении в эти функции в качестве СТЕКА используется одна и та же области памяти, в результате чего происходит зависание системы из-за порчи стека. Для преодоления этого недостатка необходимо перед каждой попыткой использования функций ДОС прерывающим процессом (работа с файловой системой, получение системных даты и времени, потоковый ввод/вывод, запуск процессов и др.) проверять, не используются ли эти функции в данный момент времени прерванным процессом. Об этом сигнализирует специальный критический флаг — внутренняя ячейка памяти ДОС. Если этот флаг установлен (не равен нулю), то ДОС уже находится в своем критическом участке, и повторный вызов функций ДОС запрещен. Если флаг сброшен (равен нулю), то функциями ДОС пользоваться можно.
В ОС реального времени ядро реентерабельное, т.е. допускает повторное вхождение в свой код.
Объекты ядра.
Объект – блок памяти, выделенный ядром для своих целей и доступный только ядру.
Объект ядра содержит имя объекта, класс защиты объекта, счётчик количества пользователей и другую информацию (смещение при открытии файла и т.д.).
Все объекты имеют описатели (Handle). Большинство объектов обладают свойством наследования.
Объекты ядра Windows:
— Процесс, поток.
— Файловый объект – открытый файл.
— Проекция файла на память.
— Событие, Семафор, Мьютекс.
— Почтовый ящик, канал, сокет и т.д.
ОС ведет учет объектов и управляет ими. Пользователь может запросить информацию об объекте.
Объекты можно создавать, уничтожать, открывать и закрывать посредством системных вызовов ОС по просьбе внешней программы. Созданные объекты закрепляются (принадлежат) создавшим их процессам. При порождении дочернего процесса, объекты ядра (как правило) наследуются из родительского, при этом создается копия объекта, принадлежащая уже дочернему процессу. Например, так наследуются стандартные потоки в/в, открытые файлы, каналы, семафоры и др. Номер дескриптора при наследовании сохраняется.
Микроядро отличается от обычного ядра тем, что в нем содержится основные, более общие функции управления ОС, а остальные функции ОС вынесены в отдельные процессы (приложения пользовательского уровня). Благодаря чему разработчик ОС может легко добавлять к ОС новые функции и расширять существующие. Таким образом, обеспечивается более эффективная защита ядра, более высокая производительность (микроядро может быть целиком размещено в кэш процессора). Микроядро изолирует все Машинно-зависимые команды, содержит ограниченный набору услуг с малым количеством системных функций (при переносе ОС на другую аппаратную платформу нужно переработать только микроядро, а все остальное – только перекомпилировать). Следовательно, обеспечивается переносимость, расширяемость и надежность.
Пример – ОС QNX, Win-NT (HAL-аналог микроядра).
Аннотация: В лекции описаны особенности функционирования менеджера объектов — одного из ключевых компонентов ОС Windows. Объекты активно используются для организации доступа к ресурсам, которые нужно защищать, именовать, разделять и т. д. Среди совокупности объектов выделены объекты ядра. Описаны дескрипторы объектов, отвечающие за связь объекта с приложением. Рассмотрены вопросы именования объектов и связь пространства имен объектов с другими пространствами имен. Для управления большим организована специальная централизованная база данных — реестр
Введение
Для работы с важными системными ресурсами ОС Windows создает объекты, управление которыми осуществляет менеджер объектов. Когда приложение открывает файл, создает поток или семафор, оно получает описатель ( handle ) соответствующего объекта (см.
рис.
4.1). Например, после выполнения программного оператора
hSemaphore = CreateSemaphore(NULL, 0, MaxCount, "MySemaphore");
создающего семафор, возвращаемый описатель hSemaphore необходим приложению для последующей работы с этим семафором.
Рис.
4.1.
Создание объекта «семафор» приложением
В данном разделе дается краткое описание того, как функционирует менеджер объектов. С объектами придется сталкиваться на протяжении всего курса. Объекты — абстрактная концепция, которая активно используется в ОС Windows для регулирования доступа к системным ресурсам.
Наличие объектов является несомненным достоинством системы. Во-первых, это единый интерфейс ко всем системным ресурсам и структурам данных, таким, как процессы, потоки, семафоры и т.д. Именование объектов и доступ к ним осуществляются по одной и той же схеме. Использование объектов дает Microsoft возможность обновлять функциональность системы, не затрагивая программного интерфейса приложений. Во-вторых, это очень удобно с точки зрения системной безопасности. Каждый объект имеет список прав доступа, который проверятся каждый раз, когда приложение создает свой описатель объекта. Соответственно, все проверки, связанные с защитой, могут быть выполнены в одном модуле — диспетчере объектов (в соответствии с требованиями безопасности), с гарантией, что ни один процесс не может их обойти. Наконец, легко организовать совместный доступ к объектам, несложно отследить объекты, которые больше не используются, и т.д.
Объекты присутствуют почти во всех компонентах системы, особенно там, где есть данные, которые нужно разделять, защищать, именовать или сделать доступными. Например, посредством объектов реализованы программные и аппаратные прерывания, а также многие другие функции ядра. Некоторые объекты доступны пользовательским приложениям через вызовы Win32. Поэтому иногда ОС Windows называют объектно-ориентированной системой, — так, доступ к ресурсу возможен только через методы соответствующего объекта (инкапсуляция данных). Вместе с тем в данной схеме отсутствуют наследование и полиморфизм, поэтому ОС Windows нельзя считать объектно-ориентированной в строгом смысле этого слова.
Объекты ядра
В рамках данного курса нам придется активно использовать объекты, называемые в руководствах по Win32-программированию объектами ядра (kernel objects). Поддержка объектов ядра осуществляется собственно ядром и исполнительной системой. Помимо объектов ядра имеются также объекты, предназначенные для управления окнами (User), и объекты, предназначенные для управления графикой (GDI). Изучение этих категорий объектов, реализуемых подсистемой поддержки окон и графики и ориентированных на разработку графических интерфейсов пользователя, выходит за пределы данного курса.
К сожалению, понятие «объект ядра» имеет разный смысл у разных авторов (ср., например, это понятие в MSDN или в
[
Рихтер
]
, c одной стороны, и в
[
Руссинович
]
— с другой), поэтому для дальнейшего изложения потребуется уточнение терминологии.
Дело в том, что совокупность объектов образует слоеную структуру. Ядро поддерживает базовые объекты двух видов: объекты диспетчера (события, мьютексы, семафоры, потоки ядра, таймеры и др.) и управляющие (DPC, APC, прерывания, процессы, профили и др.) Более подробно эти внутриядерные объекты описаны в
[
Руссинович
]
.
Над объектами ядра находятся объекты исполнительной системы, каждый из которых инкапсулирует один или более объектов ядра. Объекты исполнительной системы предназначены для управления памятью, процессами и межпроцессным обменом. Они экспортируются в распоряжение пользовательских приложений через Win32 функции. К ним относятся такие объекты, как: процесс, поток, открытый файл, семафор, мьютекс, маркер доступа и ряд других. Полный список можно увидеть в MSDN. Эти объекты и называются объектами ядра в руководствах по программированию.
Внешнее отличие объектов ядра (объектов исполнительной системы) от объектов User и GDI состоит в наличии у первых атрибутов защиты, которые являются одним из параметров, создающих объект ядра функций.
Далее эти объекты ядра (объекты исполнительной системы) будут называться просто объектами.
Объект представляет собой блок памяти в виртуальном адресном пространстве ядра. Этот блок содержит информацию об объекте в виде структуры данных (см. ниже «структура объекта»). Структура содержит как общие, так и специфичные для каждого объекта элементы. Объекты создаются в процессе загрузки и функционирования ОС и теряются при перезагрузке и выключении питания.
Содержимое объектов доступно только ядру, приложение не может модифицировать его непосредственно.
Доступ к объектам можно осуществить только через его функции-методы (инкапсуляция данных), которые инициируются вызовами некоторых библиотечных Win32-функций.
Структура объекта. Методы объекта
Рис.
4.2.
Структура объекта
Как показано на
рис.
4.2, каждый объект имеет заголовок с информацией, общей для всех объектов, а также данные, специфичные для объекта. Например, в поле заголовка имеется список процессов, открывших данный объект, и информация о защите, определяющая, кто и как может использовать объект.
Счетчик ссылок на объект увеличивается на 1 при открытии объекта и уменьшается на 1 при его закрытии. Значение счетчика ссылок, равное нулю, означает, что объект больше не используется и выделенное ему адресное пространство ядра может быть освобождено. Наличие счетчика означает, что даже после завершения процесса, создавшего объект, этот объект может не быть разрушен (если его счетчик не обнулен).
Квота устанавливает ограничения на объемы ресурсов. Несмотря на то, что в ОС Windows реализован код для отслеживания квот, в настоящее время квоты не применяются и существуют достаточно мягкие ограничения. Например, по умолчанию лимит на открытые объекты для процесса — 230. Множество объектов делится на типы, а у каждого из объектов есть атрибуты, неизменные для объектов данного типа. Ссылка на тип объекта также входит в состав заголовка. Поля имя объекта и каталог будут описаны в разделе «именование объектов».
Методы объекта
В состав компонентов объекта типа входит атрибут методы — указатели на внутренние процедуры для выполнения стандартных операций. Методы вызываются диспетчером объектов при создании и уничтожении объекта, открытии и закрытии описателя объекта, изменении параметров защиты. Система позволяет динамически создавать новые типы объектов. В этом случае предполагается регистрация его методов у диспетчера объектов. Например, метод open вызывается всякий раз, когда создается или открывается объект и создается его новый описатель.
Описатели объектов
Создание новых объектов, или открытие по имени уже существующих, приложение может осуществить при помощи Win32-функций, таких, как CreateFile, CreateSemaphore, OpenSemaphore и т.д. Это библиотечные процедуры, за которыми стоят сервисы Windows и методы объектов. В случае успешного выполнения создается 64-битный описатель в таблице описателей процесса в памяти ядра. На эту таблицу есть ссылка из блока управления процессом EPROCESS (см.
«Реализация процессов и потоков»
).
Из 64-х разрядов описателя 29 разрядов используются для ссылки на блок памяти объекта ядра, 3 — для флагов, а оставшиеся 32 — в качестве маски прав доступа. Маска прав доступа формируется на этапе создания или открытия объекта, когда выполняется проверка разрешений. Таким образом, описатель объекта — принадлежность процесса, создавшего этот объект. По умолчанию он не может быть передан другому процессу. Тем не менее, система предоставляет возможность дублирования описателя и передачи его другому процессу специальным образом (см. ниже раздел «Совместное использование объектов» и часть IV «Безопасность«).
Рис.
4.3.
Объекты и их описатели
Win32-функции, создающие объект, возвращают приложению не сам описатель, а индекс в таблице описателей, то есть малое число: типа 1,2 а не 64-разрядное (см.
рис.
4.3). Впоследствии это значение передается одной из функций, которая принимает описатель объекта в качестве аргумента. Одной из таких функций является функция CloseHandle, задача которой — закрыть объект. Во избежание утечки памяти всегда рекомендуется закрывать объект, если в нем отпала надобность. Впрочем, по окончании работы процесса система закрывает все его объекты. Таким образом, структуры объектов ядра доступны только ядру, приложение не может самостоятельно найти эти структуры в памяти и напрямую модифицировать их содержимое.
Именование объектов. Разделяемые ресурсы
Многие объекты в системе имеют имена. Именование объектов удобно для учета объектов и поиска нужного объекта. Кроме того, знание имени объекта может быть использовано процессом для получения к нему доступа (совместное использование ресурсов). Пространство имен объектов по аналогии с пространствами имен реестра и файлов организовано в виде древовидной иерархической системы. В качестве нетерминальной вершины дерева используется объект — «каталог объектов». Каталог включает информацию, необходимую для трансляции имен объектов в указатели на сами объекты. Вследствие необходимости выполнения навигации по каталогам ссылка на объект по имени работает существенно дольше, чем по описателю.
«Увидеть» пространство имен можно только при помощи специальных инструментальных средств, например, с помощью утилиты winobj, входящей в состав MS Platform SDK. Другую версию этой утилиты можно бесплатно получить на сайте http://www.sysinternals.com.
Рис.
4.4.
Окно утилиты winobj