Каких типов потоков нет в windows

Работа операционной системы Windows основана на работе процессов. В этой статье разберём что такое Windows процессы, их свойства, состояния и другое

Работа операционной системы Windows основана на работе процессов. В этой статье разберём что такое Windows процессы, их свойства, состояния и другое.

Процессы

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

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

Windows процессы состоят из следующего:

  • Закрытое виртуальное адресное пространство, то есть выделенная для процесса часть оперативной памяти, которая называется виртуальной.
  • Исполняемая программа выполняя свой код, помещает его в виртуальную память.
  • Список открытых дескрипторов. Процесс может открывать или создавать объекты, например файлы или другие процессы. Эти объекты нумеруются, и их номера называют дескрипторами. Ссылаться на объект по дескриптору быстрее, чем по имени.
  • Контекст безопасности. Сюда входит пользователь процесса, группа, привилегии, сеанс и другое.
  • Идентификатор процесса, то есть его уникальный номер.
  • Программный поток (как минимум один или несколько). Чтобы процесс хоть что-то делал, в нем должен существовать программный поток. Если потока нет, значит что-то пошло не так, возможно процесс не смог корректно завершиться, или стартовать.

У процессов есть еще очень много свойств которые вы можете посмотреть в “Диспетчере задач” или “Process Explorer“.

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

  • Выполняется — обычно все фоновые процессы будут в этом состоянии, а если процесс с окошком, то значит что приложение готово принимать данные от пользователя.
  • Приостановлен — означает что все потоки процесса находятся в приостановленном состоянии. Приложения Windows Apps переходят в это состояние при сворачивании окна для экономии ресурсов.
  • Не отвечает — означает что программный поток не проверял свою очередь сообщений более 5 секунд. Поток может быть занят работой и интенсивно загружать процессор, или может ожидать операции ввода/вывода. При этом окно приложения зависает.

В Windows существуют процессы трёх типов:

  • Приложения. Процессы запущенных приложений. У таких приложений есть окно на рабочем столе, которое вы можете свернуть, развернуть или закрыть.
  • Фоновые процессы. Такие процессы работают в фоне и не имеют окна. Некоторые процессы приложений становятся фоновыми, когда вы сворачиваете их в трей.
  • Процессы Windows. Процессы самой операционной системы, например “Диспетчер печати” или “Проводник”.

Дерево процессов

В Windows процессы знают только своих родителей, а более древних предков не знают.

Например у нас есть такое дерево процессов:

Процесс_1
|- Процесс_2
  |- Процесс_3

Если мы завершим дерево процессов “Процесс_1“, то завершатся все процессы. Потому что “Процесс_1” знает про “Процесс_2“, а “Процесс_2” знает про “Процесс_3“.

Если мы вначале завершим “Процесс_2“, а затем завершаем дерево процессов “Процесс_1“, то завершится только “Процесс_1“, так как между “Процесс_1” и “Процесс_3” не останется связи.

Например, запустите командную строку и выполните команду title parrent чтобы изменить заголовок окна и start cmd чтобы запустить второе окно командной строки:

>title parrent
>start cmd

Измените заголовок второго окна на child и из него запустите программу paint:

>title child
>mspaint

В окне командной строке child введите команду exit, окно закроется а paint продолжит работать:

>exit

После этого на рабочем столе останутся два приложения, командная строка parrent и paint. При этом parrent будет являться как бы дедом для paint.

Запустите “Диспетчер задач”, на вкладке “Процессы” найдите процесс “Обработчик команд Windows”, разверните список и найдите “parrent“. Затем нажмите на нём правой копкой мыши и выберите “Подробно”:

Подробности по процессу parrent

Вы переключитесь на вкладку “Подробно” с выделенным процессом “cmd.exe“. Нажмите правой кнопкой по этому процессу и выберите «Завершить дерево процессов»:

Завершаем дерево процессов в диспетчере задач

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

Потоки

На центральном процессоре обрабатываются не сами процессы, а программные потоки. Каждый поток, это код загруженный программой. Программа может работать в одном потоке или создавать несколько. Если программа работает в несколько потоков, то она может выполняться на разных ядрах процессора. Посмотреть на потоки можно с помощью программы Process Explorer.

Поток содержит:

  • два стека: для режима ядра и для пользовательского режима;
  • локальную памятью потока (TLS, Thread-Local Storage);
  • уникальный идентификатор потока (TID, Thread ID).

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

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

  • svchost.exe — главный процесс для служб Windows.
  • dllhost.exe — отвечает за обработку приложений, использующих динамически подключаемые библиотеки. Также отвечает за COM и .NET. И ещё управляет процессами IIS.
  • lsass.exe — отвечает за авторизацию локальных пользователей, попросту говоря без него вход в систему для локальных пользователей будет невозможен.

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

Потоки выполняются на центральном процессоре, а за их переключение отвечает планировщик ядра. В связи с тем что такое переключение это затратная операция. В Windows придумали два механизма для сокращения таких затрат: волокна (fibers) и планирование пользовательского режима (UMS, User Mode Scheduling).

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

Потоки UMS (User Mode Scheduling), доступные только в 64-разрядных версиях Windows, предоставляют все основные преимущества волокон при минимуме их недостатков. Потоки UMS обладают собственным состоянием ядра, поэтому они «видимы» для ядра, что позволяет нескольким потокам UMS совместно использовать процессор и конкурировать за него. Работает это следующим образом:

  • Когда двум и более потокам UMS требуется выполнить работу в пользовательском режиме, они сами могут периодически уступать управление другому потоку в пользовательском режиме, не обращаясь к планировщику. Ядро при этом думает что продолжает работать один поток.
  • Когда потоку UMS все таки нужно обратиться к ядру, он переключается на специально выделенный поток режима ядра.

Задания

Задания Windows (Job) позволяют объединить несколько процессов в одну группу. Затем можно этой группой управлять:

  • устанавливать лимиты (на память или процессорное время) для группы процессов входящих в задание;
  • останавливать, приостанавливать, запускать такую группу процессов.

Посмотреть на задания можно с помощью Process Explorer.

Диспетчер задач

Чаще всего для получения информации о процессе мы используем «Диспетчер задач». Запустить его можно разными способами:

  • комбинацией клавиш Ctrl+Shift+Esc;
  • щелчком правой кнопкой мыши на панели задач и выборе «Диспетчер задач»;
  • нажатием клавиш Ctrl+Alt+Del и выборе «Диспетчер задач»;
  • запуском исполняемого файла C:Windowssystem32Taskmgr.exe.

При первом запуске диспетчера задач он запускается в кратком режиме, при этом видны только процессы имеющие видимое окно. При нажатие на кнопку «Подробнее» откроется полный режим:

Краткий режим Диспетчера задач

В полном режиме на вкладке «Процессы» виден список процессов и информация по ним. Чтобы получить больше информации можно нажать правой кнопкой мышки на заголовке и добавить столбцы:

Диспетчер задач - Добавление столбцов с информацией

Чтобы получить еще больше информации можно нажать правой кнопкой мышки на процессе и выбрать «Подробно». При этом вы переключитесь на вкладку «Подробности» и этот процесс выделится.

На вкладке «Подробности» можно получить ещё больше информации о процессе. А также здесь также можно добавить колонки с дополнительной информацией, для этого нужно щелкнуть правой кнопкой мыши по заголовку и нажать «Выбрать столбцы»:

Выбор столбцов с информацией о процессах на вкладке «Подробности»

Process Explorer

Установка и подготовка к работе

Более подробную информацию о процессах и потоках можно получить с помощью программы Process Explorer из пакета Sysinternals. Его нужно скачать и запустить.

Некоторые возможности Process Explorer:

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

Запустите Process Explorer:

Process Explorer

Далее нужно настроить сервер символических имен. Если это не сделать, при двойном щелчке на процессе, на вкладке Threads (потоки) вы получите сообщение о том, что символические имена не настроены:

Предупреждение о не настроенных символических именах

Для начала скачиваем установщик «Пакет SDK для Windows 10».

Устанавливать все не нужно, достаточно при установки выбрать “Debugging Tools for Windows“:

Установка SDK для Windows 10

Для настройки символических имен перейдите в меню Options / Configure / Symbols. Введите путь к библиотеке Dbghelp.dll, которая находится внутри установленного «Пакета SDK для Windows 10» по умолчанию:

  • C:Program Files (x86)Windows Kits10Debuggersx64Dbghelp.dll.

И путь к серверу символической информации:

  • srv*C:Symbols*http://msdl.microsoft.com/download/symbols

При этом:

  • C:Symbols — путь к кеширующей локальной папке;
  • http://msdl.microsoft.com/download/symbols — сервер microsoft.

Настройка символический имен в Process Explorer

Некоторые основные настройки Process Explorer:

  • Смена цветового выделения – Options / Configure Colors.
  • Выбор колонок с информацией о процессах – View / Select Columns.
  • Сортировка процессов – нужно щелкнуть на заголовке столбца Process, при первом щелчке сортировка будет в алфавитном порядке, при втором в обратном порядке, при третьем вернется в вид дерева.
  • Просмотр только своих процессов – View / снять галочку Show Processes from All Users.
  • Настройка времени выделения только что запущенных процессов и завершённых – Options / Difference Highlight Duration / введите количество секунд.
  • Чтобы исследователь процесс подробнее можно дважды щелкнуть на нем и посмотреть информацию на различных вкладках.
  • Открыть нижнюю панель для просмотра открытых дескрипторов или библиотек – Vies / Show Lower Panel.

Потоки в Process Explorer

Потоки отдельного процесса можно увидеть в программе Process Explorer. Для этого нужно дважды кликнуть по процессу и в открывшемся окне перейти на вкладку «Threads»:

Process Explorer (потоки процесса)

В колонках видна информация по каждому потоку:

  • TID — идентификатор потока.
  • CPU — загрузка процессора.
  • Cycles Delta — общее количество циклов процессора, которое этот процесс использовал с момента последнего обновления работы Process Explorer. Скорость обновления программы можно настроить, указав например 5 минут.
  • Suspend Count — количество приостановок потока.
  • Service — название службы.
  • Start Address — начальный адрес процедуры, который начинает выполнение нового потока. Выводится в формате:«модуль!функция».

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

Изучение активности потока

  • Идентификатор потока.
  • Время начала работы потока.
  • Состояние потока.
  • Время выполнения в режиме ядра и в пользовательском режиме.
  • Счетчик переключения контекста для центрального процессора.
  • Количество циклов процессора.
  • Базовый приоритет.
  • Динамический приоритет (текущий).
  • Приоритет ввода / вывода.
  • Приоритет памяти.
  • Идеальный процессор (предпочтительный процессор).

Есть также кнопки:

Устройство Windows. Изучение активности потока, изображение №3

  • Stack — посмотреть стек процесса;
  • Module — посмотреть свойства запущенного исполняемого файла;
  • Permission — посмотреть права на поток;
  • Kill — завершить поток;
  • Suspend — приостановить поток.

Задания в Process Explorer

Process Explorer может выделить процессы, управляемые заданиями. Чтобы включить такое выделение откройте меню «Options» и выберите команду «Configure Colors», далее поставьте галочку «Jobs»:

Process Explorer — выделение заданий

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

Process Explorer — вкладка Job

Запустите командную строку и введите команду:

>runas /user:<домен><пользователь> cmd

Таким образом вы запустите еще одну командную строку от имени этого пользователя. Служба Windows, которая выполняет команды runas, создает безымянное задание, чтобы во время выхода из системы завершить процессы из задания.

В новой командной строке запустите блокнот:

>notepad.exe

Далее запускаем Process Explorer и находим такое дерево процессов:

Устройство Windows. Задания, изображение №3

Как видим, процесс cmd и notepad это процессы связанные с каким-то заданием. Если дважды кликнуть по любому из этих процессов и перейти на вкладку Job, то мы увидим следующее:

Process Explorer — вкладка Job

Тут видно что эти два процесса работают в рамках одного задания.

Вернуться к оглавлению

Сводка

Процессы Windows

Имя статьи

Процессы Windows

Описание

Работа операционной системы Windows основана на работе процессов. В этой статье разберём что такое Windows процессы, их свойства, состояния и другое

С точки зрения
программирования каждый процесс win32/64
включает компоненты (рис. 22):

  • один или несколько
    потоков;

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

  • один или более
    сегментов кода, включая код DLL;

Рис. 22. Структура
процесса и его потоков

  • один
    или более сегментов данных, содержащих
    глобальные переменные;

  • строки окружения
    с информацией о переменных окружениях,
    таких как текущий путь поиска файла и
    др.;

  • память кучи
    процесса;

  • ресурсы процесса
    (открытые дескрипторы, файлы, другие
    кучи).

Атрибуты процесса
и потока в Windows
приведены в табл. 2.

Атрибуты процесса
и потока в Windows
Таблица 2

Процесс

Поток

Идентификатор процесса — уникальное
значение, идентифицирующее процесс
в ОС

Идентификатор потока — уникальное
значение, идентифицирующее поток,
когда он вызывает сервис.

Дескриптор защиты – описывает, кто
создал процесс, права доступа и пр.

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

Базовый приоритет – базовый приоритет
для потоков

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

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

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

Родственность процессов по потоку –
множество процессов, где возможно
выполнение потоков.

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

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

Счётчик ввода/вывода — переменные, в
которые заносятся сведения о количестве
и типе операций ввода/вывода, выполненных
потоками процесса.

Статус извещения (оповещения) – этот
флаг, который указывает, следует ли
потоку выполнять асинхронный вызов
процедуры.

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

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

Квоты – максимальное количество
страничной памяти и процессорного
времени доступного процессу

Маркеры режима анонимного воплощения
– это временный признак доступа.

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

Порт завершения – это канал обмена
информацией между процессами, куда
диспетчер процессов отправляет
сообщения при завершении потока.

Статус выхода –
причины завершения процесса

Статус выхода –
причины завершения потока

Все потоки процесса
совместно используют код, глобальные
переменные, строки окружения и ресурсы
процесса [3].
Каждый поток планируется независимо.
Поток включает следующие элементы:

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

  • локальная память
    потока – это массивы указателей, которые
    дают возможность процессу выделить
    память для создания собственного
    уникального окружения данных потока;

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

Некоторые атрибуты
потока подобны атрибутам процессов.
Значениия таких атрибутов потока
извлекаются из значений атрибутов
процесса. Например, в многопроцессорной
системе родственные процессоры по
потоку – это множество процессоров, на
которых может выполняться данный поток.
Оно совпадает с множеством процессоров
родственных по процессу или является
его подмножеством. Информация, содержащаяся
в контексте потока, позволяет ОС
приостанавливать и возобновлять потоки
[9].
Основные функции управления процессами
и потоками показаны в табл.3.

Сервисы процесса
и потока в Windows
Таблица 3

Процесс

Поток

Создание процесса CreateProcess().

Создание потока CreateThread().

Открытие процесса OpenProcess().

Открытие потока OpenThread()

Информация по запросу процесса

Информация по запросу потока

Информация по наладке процесса

Информация по наладке потока

Текущий процесс
GetCurrentProcessID()

Текущий поток GetCurrentThreadID()

Прекращение процесса
ExitProcess()

Завершение потока ExitThread()

Получение контекста

Установка контекста

Приостановка Delay()

Возобновление

Извещение потоков

Проверка извещения потока

Порт регистрации завершения.

Основной функцией
для управления процессом win32 является
функция CreateProcess(). Она создаёт процесс
с одним потоком. Так как процесс требует
наличие кода, то в вызове функции
CreateProcess() необходимо указывать имя
исполняемого файла программы. Функция
имеет 10 параметров и при успешном
выполнении возвращает дескрипторы для
процесса и для первичного потока.
Дополнительные потоки можно создать
функцией CreateThread(). Для Windows
все процессы одинаковы, и она не различает
дочерние и родительские, в отличие от
Unix.

Жизненный цикл
потока в Windows
(рис 23) проходит следующие шесть состояний
[3]:

Готовый к
выполнению: это поток, который может
быть направлен на выполнение, у него
есть все необходимые ресурсы кроме
процессорного времени. Диспетчер
микроядра отслеживает, все готовые
потоки и осуществляет их планирование
в соответствии с их приоритетом.

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

Рис. 23. Жизненный
путь потока в Windows

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

Ожидающее:
поток входит в состояние ожидания, если
он блокирован каким-либо событием
(например, операцией ввода/вывода); он
добровольно ждёт синхронизации; среда
подсистемы предписывает потоку, чтобы
он сам себя остановил. После удовлетворения
условий ожидания, поток переходит в
состояние готовности, если все его
ресурсы доступны.

Переходное:
поток готов к выполнению, но не все
ресурсы необходимые ему для работы
доступны. При доступности всех ресурсов
он переходит в состояние готовности.

Завершающее:
завершение потока может быть инициировано
самим потоком, другим процессом или
может произойти вместе с завершением
родительского процесса.

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

В однозадачной ОС пользователь может запустить одновременно не более одной программы и только после того как она закончит работу можно будет запустить другую. В многозадачной ОС пользователи могут одновременно запускать несколько программ или даже несколько копий одной программы.

В чем разница между программой и процессом? Программа – это статическая последовательность команд, а процесс (process) – это программа и системные ресурсы, необходимые для ее выполнения. Процесс является субъектом владения ресурсами и единицей работы. ОС выделяет каждому процессу порцию системных ресурсов и гарантирует, что программа каждого процесса будет направляться на исполнение в определенном порядке и своевременно.

ОС содержит блок кода, управляющий созданием и удалением процессов, а также отношениями между ними. Этот код называется структурой процессов (process structure) и в Windows NT реализован диспетчером процессов (process manager).

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

В разных ОС процессы реализованы по-разному. Они различаются своим представлением (структурами данных), способами именования и защиты, а также отношениями между собой. Базовые процессы Windows NT имеют ряд характеристик, отличающих их от процессов других ОС:

  • процессы реализованы как объекты, и доступ к ним осуществляется посредством объектных сервисов;
  • в адресном пространстве процесса может исполняться несколько потоков;
  • объект-процесс и объект-поток имеют встроенные возможности синхронизации.

Что такое процесс?

Процесс состоит из:

  • исполняемой программы (код и данные);
  • закрытого адресного пространства (address space), т.е. набора адресов виртуальной памяти, который процесс может использовать;
  • системных ресурсов, выделяемых ОС процессу во время выполнения программы (семафоров, файлов и т.д.);
  • по крайней мере, одного потока управления (thread of execution). Поток – это сущность внутри процесса, которую ядро NT направляет на исполнение. Без него программа процесса не может выполняться.

Адресное пространство

С помощью системы виртуальной памяти (virtual memory) программисты (и создаваемые ими процессы) получают логический образ памяти, который не совпадает с ее физической структурой (см. Рис. 1 Виртуальная и физическая память).

При всяком обращении процесса по виртуальному адресу система виртуальной памяти транслирует этот адрес в физический адрес. Она также предотвращает непосредственный доступ процесса к виртуальной памяти, занятой другими процессами или ОС. Для исполнения кода ОС или доступа к памяти ОС поток должен выполняться в режиме ядра (kernel mode). Большинство процессов – это процессы пользовательского режима (user mode).

Поток пользовательского режима получает доступ к ОС, вызывая некоторый системный сервис. Когда поток вызывает сервис, происходит переключение из пользовательского режима в режим ядра. ОС проверяет аргументы, переданные сервису, после чего исполняет сервис. Перед возвратом управления пользовательской программе ОС переключает поток обратно в пользовательский режим.

Системные ресурсы

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

Маркер доступа (Access Token) присоединяет к процессу ОС. Это объект исполнительной системы, который содержит информацию о правах зарегистрированного в системе пользователя, которого представляет данный процесс. Если процессу требуется получить информацию о своем маркере доступа или изменить некоторые атрибуты маркера, он должен открыть описатель своего объекта-маркера. Подсистема защиты определяет, есть ли у объекта такое право.

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

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

Объект-процесс

Каждый процесс в Windows NT представлен блоком процесса, создаваемым исполнительной системой (EPROCESS). В блоке EPROCESS содержатся атрибуты процесса и указатели на некоторые структуры данных. Так, у каждого процесса есть один или более потоков, представляемых блоками потоков исполнительной системы (ETHREAD). Блок EPROCESS и связанные с ним структуры данных хранятся в системном пространстве. Исключение составляет только блок переменных окружения процесса (process environment block, PEB), он находится в адресном пространстве процесса (см. Рис. 3 Блоки переменных окружения процесса (PEB) и потока (TEB)).

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

Диспетчер процессов определяет атрибуты, хранящиеся в теле объектов-процессов, а также предоставляет системные сервисы для чтения и изменения этих атрибутов. Атрибуты и сервисы для объектов-процессов показаны на Рис. 4 Блоки процесса исполнительной системы (EPROCESS) и ядра (KPROCESS). Объект процесс исполнительной системы включает объект процесс ядра (содержит указатель на объект процесс ядра). Ядро управляет объектом процесс ядра, а исполнительная система управляет объектом исполнительной системы.

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

Рассмотрим основные атрибуты:

  • идентификатор процесса – уникальное значение, идентифицирующее процесс в ОС;
  • базовый приоритет — базовый приоритет потоков процесса;
  • привязка к процессорам (процессорное сродство) – набор процессоров, на которых потоки процесса могут исполняться по умолчанию;
  • размеры квот – максимальный объем резидентной и нерезидентной системной памяти, пространства в файле подкачки и процессорного времени, выделяемый пользовательскому процессу;
  • статус завершения – причина завершения процесса.

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

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

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

Что такое поток?

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

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

Основные составляющие потока в исполнительной системе NT:

  • Уникальный идентификатор, называемый идентификатором клиента
  • Содержимое набора регистров, отражающее состояние процессора
  • Два стека: один используется потоком при работе в пользовательском режиме, а другой — в режиме ядра
  • Собственная область памяти, предназначенная для использования подсистемами, библиотеками периода выполнения и динамически подключаемыми библиотеками (DLL).

Регистры, стек, и собственная область памяти называются контекстом (context) потока. Фактически данные, составляющие контекст потока, определяются типом процессора.

Поток находится в адресном пространстве процесса, используя его для хранения данных во время выполнения. Если в одном процессе существует несколько потоков, то они совместно используют адресное пространство и все ресурсы, включая маркер доступа, базовый приоритет и описатели объектов из таблицы объектов процесса. Ядро NT направляет потоки на исполнение некоторому процессору. Таким образом, каждый процесс NT должен иметь, по меньшей мере, один поток.

Многозадачность и многопроцессорная обработка

ОС вытесняющей многозадачностью должна использовать тот или иной алгоритм, позволяющий ей распределять процессорное время между потоками. Каждые 20 мс Windows просматривает все существующие объекты потоки  и отмечает те из них, которые могут получить процессорное время. Далее она выбирает один из таких объектов и загружает в регистры процессора значение его контекста. Эта операция называется переключением контекста (context switching). Поток выполняет код и манипулирует данными в адресном пространстве своего процесса. Примерно через 20 мс Windows сохранит значения регистров процессора в контексте потока и приостановит его выполнение. Далее система просмотрит остальные объекты потоки, подлежащие выполнению, выберет один из них, загрузит его контекст в регистры процессора, и все повторится. Этот цикл операций – выбор потока, загрузка его контекста, выполнение и сохранение контекста – начинается с момента запуска системы и продолжается до ее выключения (см. Рис. 5 Состояния потоков).

Система планирует выполнение только тех потоков, которые могут получить процессорное время. У некоторых объектов-потоков значение счетчика простоев (suspend count) больше 0, это значит, что соответствующие потоки приостановлены и не получают процессорного времени. Кроме приостановленных, существуют и другие потоки, не участвующие в распределении процессорного времени, — они ожидают каких-либо событий.

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

Каждому потоку присваивается уровень приоритета – от 0 (самый низкий) до 31 (самый высокий). Решая, какому потоку выделить процессорное время, система сначала рассматривает только потоки с приоритетом 31 и подключает их к процессору по принципу карусели. Если поток с приоритетом 31 не исключен из планирования, он получает квант времени, по истечении которого система проверяет, есть ли еще один такой поток. Если есть, то и он получает свой квант процессорного времени.

Пока в системе имеются планируемые потоки с приоритетом 31, ни один поток с более низким приоритетом процессорного времени не получит. Такая ситуация называется голоданием (starvation). Она наблюдается, когда потоки с более высоким приоритетом так интенсивно используют процессор, что остальным ничего не достается.

Кроме того, потоки с более высоким приоритетом вытесняют потоки с более низким приоритетом. Допустим, процессор исполняет поток с приоритетом 5, и тут система обнаруживает, что поток с более высоким приоритетом готов к выполнению. Тогда система остановит поток с более низким приоритетом – даже если не истек отведенный ему квант процессорного времени – подключит к процессору поток с более высоким приоритетом и выдаст ему полный квант времени.

Процессор может выполнять не более одного потока одновременно. Однако многозадачная (multitasking) ОС дает пользователю возможность исполнять несколько программ, причем создается впечатление, что все они исполняются одновременно. Это достигается следующим образом:

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

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

Переключение процессора с исполнения одного потока на исполнение другого потока называется  переключением контекста (context switching). В Windows NT оно осуществляется ядром.

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

Вытесняющая многозадачность (preemptive multitasking) – это разновидность многозадачности, при которой ОС не ждет, пока поток добровольно предоставит процессор другим потокам. Вместо этого ОС прерывает поток, после того как он выполнялся в течение заранее заданного периода времени, так называемого кванта времени (time quantum), или когда готов к выполнению поток с большим приоритетом. Вытеснение предотвращает монополизацию процессора одним потоком и предоставляет другим потокам их долю процессорного времени. Windows NT – это система с вытесняющей многозадачностью. В невытесняющих системах, поток должен был добровольно передавать управление процессором. Плохие программы могут захватить процессор и нарушить работу других приложений или всей системы.

Written on 04 Февраля 2007. Posted in Win32

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

Процессы

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

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

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

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

— Исполняемая программа и данные, проецируемые на виртуальное адресное пространство процесса.

Потоки

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

— Уникальный идентификатор потока;

— Содержимое набора регистров процессора, отражающих состояние процессора;

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

— Закрытую область памяти, называемую локальной памятью потока (thread local storage, TLS) и используемую подсистемами, run-time библиотеками и DLL.

Планирование потоков

Чтобы все потоки работали, операционная система отводит каждому из них определенное процессорное время. Тем самым создается иллюзия одновременного выполнения потоков (разумеется, для многопроцессорных компьютеров возможен истинный параллелизм). В Windows реализована система вытесняющего планирования на основе приоритетов, в которой всегда выполняется поток с наибольшим приоритетом, готовый к выполнению. Выбранный для выполнения поток работает в течение некоторого периода, называемого квантом. Квант определяет, сколько времени будет выполняться поток, пока операционная система не прервет его. По окончании кванта операционная система проверяет, готов ли к выполнению другой поток с таким же (или большим) уровнем приоритета. Если таких потоков не оказалось, текущему потоку выделяется еще один квант. Однако поток может не полностью использовать свой квант. Как только другой поток с более высоким приоритетом готов к выполнению, текущий поток вытесняется, даже если его квант еще не истек.

Квант не измеряется в каких бы то ни было единицах времени, а выражается целым числом. Для каждого потока хранится текущее значение его кванта. Когда потоку выделяется квант процессорного времени, это значит, что его квант устанавливается в начальное значение. Оно зависит от операционной системы. Например, для Win2000 Professional начальное значение кванта равно 6, а для Win2000 Server — 36.

Это значение можно изменить вызвав Control Panel — > System -> Advanced -> Performance options. Значение «Applications» — как для Win2000 Professional; «Background Services» — как для Win2000 Server.
Или напрямую в ключе реестра HKLM Win32PrioritySeparation.

Всякий раз, когда возникает прерывание от таймера, из кванта потока вычитается 3, и так до тех пор, пока он не достигнет нуля. Частота срабатывания таймера зависит от аппаратной платформы. Например, для большинства однопроцессорных x86 систем он составляет 10мс, а на большинстве многопроцессорных x86 систем — 15мс.

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

Планирование в Windows осуществляется на уровне потоков, а не процессов. Это кажется понятным, так как сами процессы не выполняются, а лишь предоставляют ресурсы и контекст для выполнения потоков. Поэтому при планировании потоков, система не обращает внимания на то, какому процессу они принадлежат. Например, если процесс А имеет 10 готовых к выполнению потоков, а процесс Б — два, и все 12 потоков имеют одинаковый приоритет, каждый из потоков получит 1/12 процессорного времени.

Приоритеты

В Windows существует 32 уровня приоритета, от 0 до 31. Они группируются так: 31 — 16 уровни реального времени; 15 — 1 динамические уровни; 0 — системный уровень, зарезервированный для потока обнуления страниц (zero-page thread).

При создании процесса, ему назначается один из шести классов приоритетов:

Real time class (значение 24),

High class (значение 13),

Above normal class (значение 10),

Normal class (значение 8),

Below normal class (значение 6),

и Idle class (значение 4).

В Windows NT/2000/XP можно посмотреть приоритет процесса в Task Manager.

Above normal и Below normal появились начиная с Win2000.

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

Normal: такой же как и у процесса;

Above normal: +1 к приоритету процесса;

Below normal: -1;

Highest: +2;

Lowest: -2;

Time critical: устанавливает базовый приоритет потока для Real time класса в 31, для остальных классов в 15.

Idle: устанавливает базовый приоритет потока для Real time класса в 16, для остальных классов в 1.

В следующей таблице показаны приоритеты процесса, относительный и базовый приоритеты потока.

Приоритет потока

Класс процесса

Класс процесса

   

Idle class

Below normal class

Normal class

Above normal class

High class

Real time class

1

 

Idle

Idle

Idle

Idle

Idle

 

2

 

Lowest

         

3

 

Below …

         

4

Idle class

Normal

Lowest

       

5

 

Above …

Below …

       

6

Below normal class

Highest

Normal

Lowest

     

7

   

Above …

Below …

     

8

Normal class

 

Highest

Normal

Lowest

   

9

     

Above …

Below …

   

10

Above normal class

   

Highest

Normal

   

11

       

Above …

Lowest

 

12

       

Highest

Below …

 

13

High class

       

Normal

 

14

         

Above …

 

15

         

Highest

 

15

 

Time critical

Time critical

Time critical

Time critical

Time critical

 

16

           

Idle

17

             

18

             

19

             

20

             

21

             

22

           

Lowest

23

           

Below …

24

Real time class

         

Normal

25

           

Above …

26

           

Highest

27

             

28

             

29

             

30

             

31

           

Time critical

Привязка к процессорам

Если операционная система выполняется на машине, где установлено более одного процессора, то по умолчанию, поток выполняется на любом доступном процессоре. Однако в некоторых случаях, набор процессоров, на которых поток может работать, может быть ограничен. Это явление называется привязкой к процессорам (processor affinity). Можно изменить привязку к процессорам программно, через Win32-функции планирования.

Память

Каждому процессу в Win32 доступно линейное 4-гигабайтное (2^32 = 4 294 967 296) виртуальное адресное пространство. Обычно верхняя половина этого пространства резервируется за операционной системой, а вторая половина доступна процессу.

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

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

Виртуальная память может вовсе не соответствовать структуре физической памяти. Диспетчер памяти транслирует виртуальные адреса на физические, по которым реально хранятся данные. Поскольку далеко не всякий компьютер в состоянии выделить по 4 Гбайт физической памяти на каждый процесс, используется механизм подкачки (swapping). Когда оперативной памяти не хватает, операционная система перемещает часть содержимого памяти на диск, в файл (swap file или page file), освобождая, таким образом, физическую память для других процессов. Когда поток обращается к странице виртуальной памяти, записанной на диск, диспетчер виртуальной памяти загружает эту информацию с диска обратно в память.

Создание процессов

Создание Win32 процесса осуществляется вызовом одной из таких функций, как CreateProcess, CreateProcessAsUser (для Win NT/2000) и CreateProcessWithLogonW (начиная с Win2000) и происходит в несколько этапов:

— Открывается файл образа (EXE), который будет выполняться в процессе. Если исполняемый файл не является Win32 приложением, то ищется образ поддержки (support image) для запуска этой программы. Например, если исполняется файл с расширением .bat, запускается cmd.exe и т.п.

В WinNT/2000 для отладки программ реализовано следующее. CreateProcess, найдя исполняемый Win32 файл, ищет в SOFTWAREMicrosoftWindows NTCurrentVersionImage File Execution Option раздел с именем и расширением запускаемого файла, затем ищет в нем параметр Debugger, и если строка не пуста, запускает то, что в ней написано вместо данной программы.

— Создается объект Win32 «процесс».

— Создается первичный поток (стек, контекст и объект «поток»).

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

— Начинается выполнение первичного потока.

— В контексте нового процесса и потока инициализируется адресное пространство (например, загружаются требуемые DLL) и начинается выполнение программы.

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

Процесс завершается если:

— Входная функция первичного потока возвратила управление.

— Один из потоков процесса вызвал функцию ExitProcess.

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

Когда процесс завершается, все User- и GDI-объекты, созданные процессом, уничтожаются, объекты ядра закрываются (если их не использует другой процесс), адресное пространство процесса уничтожается.

Пример 1. Программа создает процесс «Калькулятор».


#include <windows.h>
int main(int argc, char* argv[])
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
if( !CreateProcess( NULL, "c:/windows/calc.exe", NULL, NULL, FALSE,
0, NULL, NULL, &si, &pi))

return 0;
// Close process and thread handles.
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
return 0;
}

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

Первичный поток создается автоматически при создании процесса. Остальные потоки создаются функциями CreateThread и CreateRemoteThread (только в Win NT/2000/XP).

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

Поток завершается если

— Функция потока возвращает управление.

— Поток самоуничтожается, вызвав ExitThread.

— Другой поток данного или стороннего процесса вызывает TerminateThread.

— Завершается процесс, содержащий данный поток.

Если вы используете в своем приложении LibCMt C run-time библиотеку, Microsoft настоятельно рекомендует вместо Win32 API функций использовать их аналоги из C run-time: _beginthread, _beginthreadex, _endthread и _endthreadex.

Объекты ядра

Эти объекты используются системой и пользовательскими приложениями для управления множеством самых разных ресурсов: процессами, потоками, файлами и т.д. Windows позволяет создавать и оперировать с несколькими типами таких объектов, в том числе:

Kernel object

Объект ядра

Kernel object

Объект ядра

Access token

Маркер доступа

Module

Подгружаемый модуль (DLL)

Change notification

Уведомление об изменениях на диске

Mutex

Мьютекс

I/O completion ports

Порт завершения ввода-вывода

Pipe

Канал

Event

Событие

Process

Процесс

File

Файл

Semaphore

Семафор

File mapping

Проекция файла

Socket

Сокет

Heap

Куча

Thread

Поток

Job

Задание

Timer

Ожидаемый таймер

Mailslot

Почтовый слот

   

Объект ядра это, по сути, структура, созданная ядром и доступная только ему. В пользовательское приложение передается только описатель (handle) объекта, а управлять объектом ядра можно с помощью функций Win32 API.

Wait функции

Как можно приостановить работу потока? Существует много способов. Вот некоторые из них.

Функция Sleep() приостанавливает работу потока на заданное число миллисекунд. Если в качестве аргумента вы укажите 0 ms, то произойдет следующее. Поток откажется от своего кванта процессорного времени, однако тут же появится в списке потоков готовых к выполнению. Иными словами произойдет намеренное переключение потоков. (Вернее сказать, попытка переключения. Ведь следующим для выполнения потоком вполне может стать тот же самый.)

Функция WaitForSingleObject() приостанавливает выполнение потока до тех пор, пока не произойдет одно из двух событий:

— истечет таймаут ожидания;

— ожидаемый объект перейдет в сигнальное (signaled) состояние.

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

Функции WaitForMultipleObjects передается сразу массив объектов. Можно ожидать срабатывания сразу всех объектов или какого-то одного из них.

Пример 2. Программа создает два одинаковых потока и ожидает их завершения.
Потоки просто выводят текстовое сообщение, которое передано им при инициализации.


#include <windows.h>
#include <process.h>

unsigned __stdcall ThreadFunc( void * arg) // Функция потока
{
char ** str = (char**)arg;
MessageBox(0,str[0],str[1],0);
_endthreadex( 0 );
return 0;
};
int main(int argc, char* argv[])
{
char * InitStr1[2] = {"First thread running!","11111"};// строка для первого потока
char * InitStr2[2] = {"Second thread running!","22222"};// строка для второго потока
unsigned uThreadIDs[2];

HANDLE hThreads[2];
hThreads[0] = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, InitStr1, 0,&uThreadIDs[0]);
hThreads[1] = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, InitStr2, 0,&uThreadIDs[1]);

// Ждем, пока потоки не завершат свою работу
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE ); // Set no time-out
// Закрываем дескрипторы
CloseHandle( hThreads[0] );
CloseHandle( hThreads[1] );
return 0;
}

Синхронизация потоков

Работая параллельно, потоки совместно используют адресное пространство процесса. Также все они имеют доступ к описателям (handles) открытых в процессе объектов. А что делать, если несколько потоков одновременно обращаются к одному ресурсу или необходимо как-то упорядочить работу потоков? Для этого используют объекты синхронизации и соответствующие механизмы.

Мьютексы

Мьютексы (Mutex) это объекты ядра, которые создаются функцией CreateMutex(). Мьютекс бывает в двух состояниях — занятом и свободном. Мьютексом хорошо защищать единичный ресурс от одновременного обращения к нему разными потоками.

Пример 3. Допустим, в программе используется ресурс, например, файл или буфер в памяти. Функция WriteToBuffer() вызывается из разных потоков. Чтобы избежать коллизий при одновременном обращении к буферу из разных потоков, используем мьютекс. Прежде чем обратиться к буферу, ожидаем <освобождения> мютекса.


HANDLE hMutex;

int main()
{
hMutex = CreateMutex( NULL, FALSE, NULL); // Создаем мьютекс в свободном состоянии
...
// Создание потоков, и т.д.
...
}
BOOL WriteToBuffer()
{
DWORD dwWaitResult;
// Ждем освобождения мьютекса перед тем как обратиться к буферу.
dwWaitResult = WaitForSingleObject( hMutex, 5000L); // 5 секунд на таймаут
 
if (dwWaitResult == WAIT_TIMEOUT) // Таймаут. Мьютекс за это время не освободился.
{
return FALSE;
}
else // Мьютекс освободился, и наш поток его занял. Можно работать.
{
Write_to_the_buffer().
...
ReleaseMutex(hMutex); // Освобождаем мьютекс.
}
return TRUE;
}

Семафоры

Семафор (Semaphore) создается функцией CreateSemaphore(). Он очень похож на мьютекс, только в отличие от него у семафора есть счетчик. Семафор открыт если счетчик больше 0 и закрыт, если счетчик равен 0. Семафором обычно «огораживают» наборы равнозначных ресурсов (элементов), например очередь, список и т.п.

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


class CMyQueue
{
HANDLE m_hSemaphore; // Семафор для учета элементов очереди
// Описание других объектов для хранения элементов очереди

public:
CMyQueue()
{
m_hSemaphore = CreateSemaphore(NULL, 0, 1000, NULL); //начальное значение счетчика = 0

//максимальное значение = 1000


// Инициализация других объектов
}
~CMyQueue()
{
CloseHandle( m_hSemaphore);
// Удаление других объектов
}
void AddItem(void * NewItem)
{
// Добавляем элемент в очередь
// Увеличиваем счетчик семафора на 1.
ReleaseSemaphore(m_hSemaphore,1, NULL);
}
void GetItem(void * Item)
{
// Если очередь пуста, то потоки, вызвавшие этот метод,
// будут находиться в ожидании...

WaitForSingleObject(m_hSemaphore,INFINITE);
 
// Удаляем элемент из очереди
}
};

Замечание. В этом примере мы считаем, что сами процедуры добавления элемента в очередь и удаления из очереди безопасны с точки зрения многопоточности. Не будем пока касаться деталей их реализации. Подробнее мы рассмотрим это в примере 9.

События

События (Event), также как и мьютексы имеют два состояния — установленное и сброшенное. События бывают со сбросом вручную и с автосбросом. Когда поток дождался (wait-функция вернула управление) события с автосбросом, такое событие автоматически сбрасывается. В противном случае событие нужно сбрасывать вручную, вызвав функцию ResetEvent(). Допустим, сразу несколько потоков ожидают одного и того же события, и событие сработало. Если это было событие с автосбросом, то оно позволит работать только одному потоку (ведь сразу же после возврата из его wait-функции событие сбросится автоматически!), а остальные потоки останутся ждать. Если же это было событие со сбросом вручную, то все потоки получат управление, а событие так и останется в установленном состоянии, пока какой-нибудь поток не вызовет ResetEvent().

Пример 5. Вот еще один пример многопоточного приложения. Программа имеет два потока; один готовит данные, а второй отсылает их на сервер. Разумно распараллелить их работу. Здесь потоки должны работать по очереди. Сначала первый поток готовит порцию данных. Потом второй поток отправляет ее, а первый тем временем готовит следующую порцию и т.д. Для такой синхронизации понадобится два event’а с автосбросом.


unsigned __stdcall CaptureThreadFunc( void * arg) // Поток, готовящий данные
{
while (bSomeCondition)
{
WaitForSingleObject(m_hEventForCaptureTh,INFINITE); // Ждем своего события
... // Готовим данные
SetEvent(hEventForTransmitTh); // Разрешаем работать второму потоку
}
_endthreadex( 0 );
return 0;
};

unsigned __stdcall TransmitThreadFunc( void * arg) // Поток, отсылающий данные.
{
while (bSomeCondition)
{
WaitForSingleObject(m_hEventForTransmitTh,INFINITE); // Ждем своего события
... // Данные готовы, формируем из них пакет для отправки
SetEvent(hEventForCaptureTh); // Разрешаем работать первому потоку, а сами...
... // отправляем пакет
}
_endthreadex( 0 );
return 0;
};

int main(int argc, char* argv[]) // Основной поток
{
// Создаем два события с автосбросом, со сброшенным начальным состоянием
hEventForCaptureTh = CreateEvent(NULL,FALSE,FALSE,NULL);
hEventForTransmitTh = CreateEvent(NULL,FALSE,FALSE,NULL);

// Создаем потоки
hCaptureTh = (HANDLE)_beginthreadex( NULL, 0, &CaptureThreadFunc, 0, 0,&uTh1);
hTransmitTh = (HANDLE)_beginthreadex( NULL, 0, &TransmitThreadFunc, 0, 0,&uTh2);
// Запускаем первый поток
SetEvent(hEventForCaptureTh);

....
}

Пример 6. Другой пример. Программа непрерывно в цикле производит какие-то вычисления. Нужно иметь возможность приостановить на время ее работу. Допустим, это просмотрщик видео файлов, который в цикле, кадр за кадром отображает информацию на экран. Не будем вдаваться в подробности видео функций. Реализуем функции Pause и Play для программы. Используем событие со сбросом вручную.


// Главная функция потока, которая в цикле отображает кадры
unsigned __stdcall VideoThreadFunc( void * arg)
{
while (bSomeCondition)
{
WaitForSingleObject(m_hPauseEvent,INFINITE); // Если событие сброшено, ждем
... // Отображаем очередной кадр на экран
}
_endthreadex( 0 );
return 0;
};

void Play()
{
SetEvent(m_hPauseEvent);
};

void Pause()
{
ResetEvent(m_hPauseEvent);
};

Функция PulseEvent() устанавливает событие и тут же переводит его обратно в сброшенное состояние; ее вызов равнозначен последовательному вызову SetEvent() и ResetEvent(). Если PulseEvent вызывается для события со сбросом в ручную, то все потоки, ожидающие этот объект, получают управление. При вызове PulseEvent для события с автосбросом пробуждается только один из ждущих потоков. А если ни один из потоков не ждет объект-событие, вызов функции не дает никакого эффекта.

Пример 7. Реализуем функцию NextFrame() для предыдущего примера для промотки файла вручную по кадрам.


void NextFrame()
{
PulseEvent(m_hPauseEvent);
};

Ожидаемые таймеры

Пожалуй, ожидаемые таймеры — самый изощренный объект ядра для синхронизации. Появились они, начиная с Windows 98. Таймеры создаются функцией CreateWaitableTimer и бывают, также как и события, с автосбросом и без него. Затем таймер надо настроить функцией SetWaitableTimer. Таймер переходит в сигнальное состояние, когда истекает его таймаут. Отменить «тиканье» таймера можно функцией CancelWaitableTimer. Примечательно, что можно указать callback функцию при установке таймера. Она будет выполняться, когда срабатывает таймер.

Пример 8. Напишем программу-будильник используя WaitableTimer’ы. Будильник будет срабатыват раз в день в 8 утра и «пикать» 10 раз. Используем для этого два таймера, один из которых с callback-функцией.


#include <process.h>
#include <windows.h>
#include <stdio.h>
#include <conio.h>

#define HOUR (8) // время, когда срабатывает будильник (только часы)
#define RINGS (10) // сколько раз пикать

HANDLE hTerminateEvent ;

// callback функция таймера
VOID CALLBACK TimerAPCProc(LPVOID, DWORD, DWORD)
{
Beep(1000,500); // звоним!
};
 
// функция потока
unsigned __stdcall ThreadFunc(void *)
{
HANDLE hDayTimer = CreateWaitableTimer(NULL,FALSE,NULL);
HANDLE hAlarmTimer = CreateWaitableTimer(NULL,FALSE,NULL);
HANDLE h[2]; // мы будем ждать эти объекты
h[0] = hTerminateEvent; h[1] = hDayTimer;
int iRingCount=0; // число "звонков"
int iFlag;
DWORD dw;

// немного помучаемся со временем,
//т.к. таймер принимает его только в формате FILETIME

LARGE_INTEGER liDueTime, liAllDay;
liDueTime.QuadPart=0;
// сутки в 100-наносекундных интервалах = 10000000 * 60 * 60 * 24 = 0xC92A69C000
liAllDay.QuadPart = 0xC9;
liAllDay.QuadPart=liAllDay.QuadPart << 32;
liAllDay.QuadPart |= 0x2A69C000;
SYSTEMTIME st;
GetLocalTime(&st); // узнаем текущую дату/время
iFlag = st.wHour > HOUR; // если назначенный час еще не наступил,
//то ставим будильник на сегодня, иначе - на завтра

st.wHour = HOUR;
st.wMinute = 0;
st.wSecond =0;
FILETIME ft;
SystemTimeToFileTime( &st, &ft);
if (iFlag)
((LARGE_INTEGER *)&ft)->QuadPart =
((LARGE_INTEGER *)&ft)->QuadPart +liAllDay.QuadPart ;

LocalFileTimeToFileTime(&ft,&ft);
// Устанавливаем таймер,


// он будет срабатывать раз в сутки (24*60*60*1000ms),
// начиная со следующего "часа пик" - HOUR

SetWaitableTimer(hDayTimer, (LARGE_INTEGER *) &ft, 24*60*60000, 0, 0, 0);
do {
dw = WaitForMultipleObjectsEx(2,h,FALSE,INFINITE,TRUE);
if (dw == WAIT_OBJECT_0 +1) // сработал hDayTimer
{
// Устанавливаем таймер, он будет вызывать callback ф-ию раз в секунду,
// начнет с текущего момента

SetWaitableTimer(hAlarmTimer, &liDueTime, 1000, TimerAPCProc, NULL, 0);
iRingCount=0;
}
if (dw == WAIT_IO_COMPLETION) // закончила работать callback ф-ия
{
iRingCount++;
if (iRingCount==RINGS)
CancelWaitableTimer(hAlarmTimer);
}
}while (dw!= WAIT_OBJECT_0); // пока не сработало hTerminateEvent крутимся в цикле

// закрывае handles, выходим


CancelWaitableTimer(hDayTimer);
CancelWaitableTimer(hAlarmTimer);
CloseHandle(hDayTimer);
CloseHandle(hAlarmTimer);
_endthreadex( 0 );
return 0;
};

int main(int argc, char* argv[])
{
// это событие показывае потоку когда надо завершаться
hTerminateEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
unsigned uThreadID;
HANDLE hThread;
// создаем поток
hThread = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, 0, 0,&uThreadID);
puts("Press any key to exit.");
// ждем any key от пользователя для завершения программы
getch();
// выставляем событие
SetEvent(hTerminateEvent);
// ждем завершения потока
WaitForSingleObject(hThread, INFINITE );
// закрываем handle
CloseHandle( hThread );
return 0;
}

Критические секции. Синхронизация в пользовательском режиме

Критическая секция гарантирует вам, что куски кода программы, огороженные ей, не будут выполняться одновременно. Строго говоря, критическая секция не является объектом ядра. Она представляет собой структуру, содержащую несколько флагов и какой-то (не важно) объект ядра. При входе в критическую секцию сначала проверяются флаги, и если выясняется, что она уже занята другим потоком, то выполняется обычная wait-функция. Критическая секция примечательна тем, что для проверки, занята она или нет, программа не переходит в режим ядра (не выполняется wait-функция) а лишь проверяются флаги. Из-за этого считается, что синхронизация с помощью критических секций наиболее быстрая. Такую синхронизацию называют «синхронизация в пользовательском режиме«.

Пример 9. Снова рассмотрим очередь элементов. Один из вариантов ее реализации — двусвязный список. С точки зрения многопоточности, опасными являются операции добавления и удаления элементов из очереди. Существует вероятность, что несколько потоков одновременно начнут перестраивать указатели и связность очереди нарушится. Чтобы этого избежать, используем критическую секцию.


typedef ... ItemData;

// Элемент очереди: данные и два указателя на предыдущий и следующий элементы
typedef struct _ItemStruct
{
ItemData data;
struct _ItemStruct * prev,*next;
} Item;

// Описание класса "Очередь"
class CMyQueue
{
CRITICAL_SECTION m_crisec; // Критическая секция
Item * m_Begin; // Указатель на первый элемент
Item * m_End; // Указатель на последний элемент
int m_Count; // Количество элементов


public:
CMyQueue()
{
// Инициализируем критическую секцию
InitializeCriticalSection(&m_crisec);
// Инициализируем переменные
m_Count = 0;
m_Begin = m_End = NULL;
}

~CMyQueue()
{
// Удаляем все элементы очереди
while(m_Count) GetItem();
// Удаляем критическую секцию
DeleteCriticalSection(&m_crisec);
}

void AddItem(ItemData data)
{
Item * NewItem;
Item * OldFirstItem;
NewItem = new Item(); // New item
NewItem->next = NULL;
NewItem->prev = NULL;
NewItem->data = data;
// ------------------------ Этот кусок не может выполняться параллельно
EnterCriticalSection(&m_crisec); // Заходим в к.с. (ждем входа)
OldFirstItem = m_Begin;
m_Begin = NewItem;
NewItem->next = OldFirstItem;
if (OldFirstItem)
OldFirstItem->prev = NewItem;
else
m_End = NewItem;
m_Count++;
LeaveCriticalSection(&m_crisec); // Выходим из к.с.
// ------------------------ Этот кусок не может выполняться параллельно
}

ItemData GetItem()
{
ItemData data;
// ------------------------ Этот кусок не может выполняться параллельно
EnterCriticalSection(&m_crisec); // Заходим в к.с. (ждем входа)
if (!m_End)
data = NULL;
else
{
data = m_End->data ;
if (m_End->prev )
{
m_End->prev ->next = NULL;
m_End = m_End->prev ;
}
else
{
m_End = NULL;
m_Begin = NULL;
}
m_Count --;
}
LeaveCriticalSection(&m_crisec); // Выходим из к.с.
// ------------------------ Этот кусок не может выполняться параллельно
return data;
};
};

Синхронизация процессов

Описатели объектов ядра зависимы от конкретного процесса (process specific). Проще говоря, handle объекта, полученный в одном процессе, не имеет смысла в другом. Однако существуют способы работы с одними и теми же объектами ядра из разных процессов.

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

Во-вторых, дублирование описателя. Функция DuplicateHandle дублирует описатель объекта одного процесса в другой, т.е. по сути, берет запись в таблице описателей одного процесса и создает ее копию в таблице другого.

И, наконец, именование объекта ядра. При создании объекта ядра для синхронизации (мьютекса, семафора, ожидаемого таймера или события) можно задать его имя. Оно должно быть уникальным в системе. Тогда другой процесс может открыть этот объект ядра, указав в функции Open…(OpenMutex, OpenSemaphore, OpenWaitableTimer, OpenEvent) это имя.

На самом деле, при вызове функции Create… () система сначала проверяет, не существует ли уже объект ядра с таким именем. Если нет, то создается новый объект. Если да, ядро проверяет тип этого объекта и права доступа. Если типы не совпадают или же вызывающий процесс не имеет полных прав на доступ к объекту, вызов Create… функции заканчивается неудачно и возвращается NULL. Если все нормально, то просто создается новый описатель (handle) существующего уже объекта ядра. По коду возврата функции GetLastError() можно понять что произошло: создался новый объект или Create() вернула уже существующий.

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

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


int main(int argc, char* argv[])
{
HANDLE Mutex;
Mutex = CreateMutex(NULL,FALSE,"MyMutex");
if (ERROR_ALREADY_EXISTS == GetLastError()) // Такой мьютекс уже кем-то создан...
{
MessageBox(0,"Приложение уже запущено","Error",0);
CloseHandle( Mutex);
exit(0);
}
...
}

Взаимодействие между процессами

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

Разделяемая память

Как уже говорилось, система виртуальной памяти в Win32 использует файл подкачки — swap file (или файл размещения — page file), имея возможность преобразования страниц оперативной памяти в страницы файла на диске и наоборот. Система может проецировать на оперативную память не только файл размещения, но и любой другой файл. Приложения могут использовать эту возможность. Это может использоваться для обеспечения более быстрого доступа к файлам, а также для совместного использования памяти.

Такие объекты называются проекциями файлов (на оперативную память) (file-mapping object). Для создания проекции файла сначала вызывается функция CreateFileMapping(). Ей передается дескриптор (уже открытого) файла или указывается, что нужно использовать page file операционной системы. Кроме этого, в параметрах ей передается флаг защиты, максимальный размер проекции и имя объекта. Затем вызывается функция MapViewOfFile(). Она отображает представление файла (view of a file) в адресное пространство процесса. По окончании работы вызывается функция UnmapViewOfFile(). Она освобождает память и записывает данные в файл (если это не файл подкачки). Чтобы записать данные на диск немедленно, используется функция FlushViewOfFile(). Проекция файла, как и другие объекты ядра, может использоваться другими процессами через наследование, дублирование дескриптора или по имени.

Пример 11. Вот пример программы, которая создает проекцию в page file и записывает в нее данные.


#include <windows.h>

void main()
{
HANDLE hMapping;
char* lpData;
char* lpBuffer;
...
//Создание или открытие существующей проекции файла
hMapping = CreateFileMapping( (HANDLE)(-1), // используем page file
NULL, PAGE_READWRITE, 0, 0x0100, "MyShare");
if (hMapping == NULL) exit(0);
// Размещаем проекцию hMapping в адресном пространстве нашего процесса;
// lpData получает адрес размещения
lpData = (char*) MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS,0,0,0);
if (lpData == NULL) exit(0);
// Копируем в проекцию данные
memcpy ( lpData , lpBuffer );
...
// Заканчиваем работу. Освобождаем память.
UnmapViewOfFile(lpData);
// Закрываем объект ядра
CloseHandle(hMapping);
};

Прочие механизмы (сокеты, pipe)

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

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

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

Сокет — это абстрактный объект для обозначения одного из концов сетевого соединения, в том числе и через Internet. Сокеты Windows бывают двух типов: сокеты дейтаграмм и сокеты потоков. Интерфейс Windows Sockets (WinSock) основан на BSD-версии сокетов, но в нем имеются также расширения, специфические для Windows.

Сообщения в Windows (оконные сообщения)

Говоря о Windows нельзя не упомянуть о таких понятиях как windows (окна), messages (сообщения), message queue (очередь сообщений) и т.д.

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

Окна управляются сообщениями. Все события, происходящие с окном, сопровождаются посылкой ему сообщений: создание и уничтожение окна, ввод с клавиатуры, перемещение мыши, перерисовка и перемещение окна и т.д. Сообщения окну могут посылаться как самой системой, так и пользовательскими приложениями. Каждому окну приписана функция, называемая оконной процедурой (window procedure), которая и вызывается при обработке сообщения.

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

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

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

Пример 12. Программа находит окно с заголовком “Калькулятор” и закрывает его, посылая сообщение WM_CLOSE.


#include <windows.h>

int main(int argc, char* argv[])
{
HWND hwnd = FindWindow( NULL , "Калькулятор");
if (NULL != hwnd) PostMessage(hwnd, WM_CLOSE, 0, 0 );
return 0;
}

Тема оконных сообщений в Windows весьма обширна и требует для рассмотрения, по крайней мере, отдельной статьи.

Заключение

В этой статье коротко были рассмотрены основные моменты многозадачной работы в Windows: создание и завершение процессов, синхронизация и межпроцессное общение.

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

— Microsoft Platform SDK

— Jeffrey Richter. Programming Applications for Microsoft® Windows. ISBN 1-57231-996-8

— Соломон, Руссинович. Внутреннее устройство MS Windows 2000. ISBN 5-7502-0136-8

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

Image 1

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.

Image 2

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.

Image 3

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.

Image 4

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.

Image 5

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).  

Image 6

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:

Image 7

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

Image 8

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.

Image 9

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.

Image 10

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

Понравилась статья? Поделить с друзьями:
  • Каких процессов не должно быть в диспетчере задач windows 10
  • Каких версий excel для windows не выпускалось
  • Какими файлами представлен реестр в windows
  • Какими способами можно скопировать объект в windows
  • Какими способами можно переименовать папку в windows