Московский физико-технический институт
Опубликован: 12.12.2007 | Доступ: свободный | Студентов: 5499 / 1834 | Оценка: 4.34 / 4.14 | Длительность: 13:57:00
ISBN: 978-5-94774-827-7
Лекция 5:

Реализация процессов и потоков

< Лекция 4 || Лекция 5: 123 || Лекция 6 >

Прогон программы создания процесса

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

#include <windows.h>
#include <stdio.h>
void main( VOID )
{
    STARTUPINFO StartupInfo;
    PROCESS_INFORMATION ProcInfo;
    TCHAR CommandLine[] = TEXT("sleep");

    ZeroMemory( &StartupInfo, sizeof(StartupInfo) );
    StartupInfo.cb = sizeof(StartupInfo);
    ZeroMemory( &ProcInfo, sizeof(ProcInfo) );

    if( !CreateProcess( NULL, // Не используется имя модуля 
        CommandLine,          // Командная строка
        NULL,                 // Дескриптор процесса не наследуется. 
        NULL,                 // Дескриптор потока не наследуется. 
        FALSE,                // Установка описателей наследования
        0,                    // Нет флагов создания процесса
        NULL,                 // Блок переменных окружения родительского процесса
        NULL,                 // Использовать текущий каталог родительского процесса
        &StartupInfo,         // Указатель на структуру  STARTUPINFO.
        &ProcInfo )           // Указатель на структуру информации о процессе.
      )
    
	printf( "CreateProcess failed." );
  
    // Ждать окончания дочернего процесса
    WaitForSingleObject( ProcInfo.hProcess, INFINITE );

    // Закрыть описатели процесса и потока 
    CloseHandle( ProcInfo.hProcess );
    CloseHandle( ProcInfo.hThread );
}

В приведенной программе имя запускаемого модуля передается через второй параметр функции CreateProcess. В примере в качестве дочерней программы используется простейшая программа sleep, задача которой - выдержать паузу длительностью 10 секунд.

#include <windows.h>
#include <stdio.h>
void main( VOID )
{
printf("Данная программа будет спать 
в течение 10000 мс\n");
Sleep(10000);
}

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

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

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

Реализация потоков

Состояния потоков

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

Состояния потоков в ОС Windows (версии Server 2003)

Рис. 5.3. Состояния потоков в ОС Windows (версии Server 2003)

Обычно в состоянии "Готовности" имеется очередь готовых к выполнению (running) потоков. В данном случае это состояние распадается на три составляющих. Это, собственно, состояние "Готовности (Ready)"; состояние "Готов. Отложен (Deferred Ready)", что означает, что поток выбран для выполнения на конкретном процессоре, но пока не запланирован к выполнению; и, наконец, состояние "Простаивает (Standby)", в котором может находиться только один выбранный к выполнению поток для каждого процессора в системе.

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

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

Прогон программы, иллюстрирующей состояния потоков

Системный монитор (обкладка "Производительность") представляет собой удобное средство наблюдения за состояниями потоков. Предлагается осуществить прогон программы, которая содержит длительные циклы счета и ожидания. Например, программа, вычисляющая 5*107 значений функций sin(x):

#include <windows.h>
#include <stdio.h>
#include <math.h>
 
VOID main( VOID ) { 
  int i,N=50000000;
  double a,b;
  getchar();
  printf("Before circle\n");
  for ( i = 0; i<N; i++) {
	 b=(double)i / (double)N;
	 a=sin(b);	
	}
	printf("After circle\n");
	getchar();
}

Графическое представление предполагает присвоение цифровых значений различным состояниям потока (например, готовность - 1, выполнение - 2, ожидание - 5 и т.п.). Результат работы монитора на однопроцессорной системе для данной программы представлен на рис. 5.4.

Иллюстрация перехода потока из одного состояния в другое

Рис. 5.4. Иллюстрация перехода потока из одного состояния в другое

Горизонтальные участки со значением 5 соответствуют ожиданию нажатия клавиши ввода, а значению 1 (читателю предлагается самостоятельно ответить на вопрос, почему значению 1, а не значению 2) соответствует счетный цикл.

Отдельные характеристики потоков

Идентификаторы потоков, так же как и идентификаторы процессов, кратны четырем, выбираются из того же пространства, что и идентификаторы процессов, и с ними не пересекаются.

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

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

Волокна и задания

Переключение между потоками занимает довольно много времени, поэтому для облегченного псевдопараллелизма в системе поддерживаются волокна (fibers). Наличие волокон позволяет реализовать собственный механизм планирования, не используя встроенный механизм планирования потоков на основе приоритетов. ОС не знает о смене волокон, для управления волокнами нет и настоящих системных вызовов, однако есть вызовы Win32 API ConvertThreadToFiber, CreateFiber, SwitchToFiber и т. д. Подробнее функции, связанные с волокнами, описаны в документации Platform SDK.

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

Внутреннее устройство потоков

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

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

Управляющие структуры данных потока

Рис. 5.5. Управляющие структуры данных потока
< Лекция 4 || Лекция 5: 123 || Лекция 6 >
Ирина Оленина
Ирина Оленина
Николай Сергеев
Николай Сергеев

Здравствуйте! Интересует следующий момент. Как осуществляется контроль доступа по тому или иному адресу с точки зрения обработки процессом кода процесса. Насколько я понял, есть два способа: задание через атрибуты сегмента (чтение, запись, исполнение), либо через атрибуты PDE/PTE (чтение, запись). Но как следует из многочисленных источников, эти механизмы в ОС Windows почти не задействованы. Там ключевую роль играет менеджер памяти, задающий регионы, назначающий им атрибуты (PAGE_READWRITE, PAGE_READONLY, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_NOACCESS, PAGE_GUARD: их гораздо больше, чем можно было бы задать для сегмента памяти) и контролирующий доступ к этим регионам. Непонятно, на каком этапе может включаться в работу этот менеджер памяти? Поскольку процессор может встретить инструкцию: записать такие данные по такому адресу (даже, если этот адрес относится к региону, выделенному менеджером памяти с атрибутом, например, PAGE_READONLY) и ничего не мешает ему это выполнить. Таким образом, менеджер памяти остается в стороне не участвует в процессе...