Основы организации операционных систем Microsoft Windows

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

Разбить на страницы
Показывать лекцию целиком

Понятие процесса и потока

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

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

Для описания столь сложного динамического объекта ОС поддерживает набор структур, главную из которых принято называть блоком управления процессом (PCB, Process control block). В состав PCB обычно включают:

  • состояние, в котором находится процесс;
  • программный счетчик процесса или, другими словами, адрес команды, которая должна быть выполнена для него следующей;
  • содержимое регистров процессора;
  • данные, необходимые для планирования использования процессора и управления памятью (приоритет процесса, размер и расположение адресного пространства и т. д.);
  • учетные данные (идентификационный номер процесса, какой пользователь инициировал его работу, общее время использования процессора данным процессом и т. д.);
  • информацию об устройствах ввода-вывода, связанных с процессом (например, какие устройства закреплены за процессом; таблица открытых файлов).
  • Блок управления процессом является моделью процесса для операционной системы. Любая операция, производимая операционной системой над процессом, вызывает определенные изменения в PCB. Псевдопараллельное выполнение процессов предполагает периодическую приостановку текущего процесса и его последующее возобновление. Для этого нужно уметь сохранять часть данных из PCB, которые обычно называют контекстом процесса, а операцию по сохранению данных одного процесса и восстановлению данных другого называют переключением контекстов. Переключение контекста не имеет отношения к полезной работе, выполняемой процессами, и время, затраченное на него, сокращает полезное время работы процессора.

    Потоки

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

    (рис 5.1) Процесс с несколькими потоками

    В этом случае процесс можно рассматривать в качестве контейнера ресурсов, а все проблемы, связанные с динамикой исполнения, решаются на уровне потоков. Обычно каждый процесс начинается с одного потока, а остальные (при необходимости) создаются в ходе выполнения. Теперь уже не процесс, а поток характеризуется состоянием, поток является единицей планирования, процессор переключается между потоками, и необходимо сохранять контекст потока (что существенно проще, чем сохранение контекста процесса). Подобно процессам потоки (нити, threads) в системе описываются структурой данных, которую обычно называют блоком управления потоком (thread control block, TCB).

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

    Внутреннее устройство процессов в ОС Windows

    В 32-разрядной версии системы у каждого процесса есть 4-гигабайтное адресное пространство, в котором пользовательский код занимает нижние 2 гигабайта (в серверах 3 Гбайта). В своем адресном пространстве, которое представляет собой набор регионов и описывается специальными структурами данных (см. часть III "система управления памятью"), процесс содержит потоки, учетную информацию и ссылки на ресурсы, которые обобществляются всеми потоками процесса.

    Блок управления процессом (PCB) реализован в виде набора связанных структур, главная из которых называется блоком процесса EPROCESS. Соответственно, каждый поток также представлен набором структур во главе с блоком потока ETHREAD. Эти наборы данных, за исключением блоков переменных окружения процесса и потока (PEB и TEB), существуют в системном адресном пространстве. Упрощенная схема структур данных процесса показана на рис 5.2.

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

    Содержимое блока EPROCESS подробно описано в . Блок KPROCESS (на рис. справа), блок переменных окружения процесса (PEB) и структура данных, поддерживаемая подсистемой Win32 (блок процесса Win32), содержат дополнительные сведения об объекте "процесс".

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

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

    Обычно процесс создается другим процессом вызовом Win32-функции CreateProcess (а также CreateProcessAsUser и CreateProcessWithLogonW ). Создание процесса осуществляется в несколько этапов.

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

    На втором этапе выполняется обращение к системному сервису NtCreateProcess для создания объекта "процесс". Формируются блоки EPROCESS, KPROCESS и блок переменных окружения PEB. Менеджер процессов инициализирует в блоке процесса маркер доступа (копируя аналогичный маркер родительского процесса), идентификатор и другие поля.

    На третьем этапе в уже полностью проинициализированном объекте "процесс" необходимо создать первичный поток. Это, посредством системного сервиса NtCreateThread, делает библиотека kernel32.dll.

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

    Наконец, запускается первичный поток, для чего формируются его начальный контекст и стек, и выполняется запуск стартовой процедуры потока режима ядра KiThreadStartup. После этого стартовый код из библиотеки C/C++ передает управление функции main() запускаемой программы.

    В книге этапы создания процесса описаны более подробно.

    Функция CreateProcess

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

    BOOL CreateProcess( 
      PCTSTR pszApplicationName, 
      PTSTR pszCommandLine, 
      PSECURITY_ATTRIBUTES psaProcess, 
      PSECURITY_ATTRIBUTES psaThread, 
      BOOL bInheritHandles, 
      DWORD fdwCreate, 
      PVOID pvEnvironment,
      PCTSTR pszCurDir, 
      PSTARTUPINFO psiStartInfo, 
      PPROCESS_INFORMATION ppiProcInfo);

    Описание параметров функции можно посмотреть в MSDN.

    Формально ОС Windows не поддерживает какой-либо иерархии процессов, например, отношений "родительский-дочерний". Однако, негласная иерархия, заключающаяся в том, кто чьим дескриптором (описателем) владеет, все же существует. Например, владение дескриптором процесса позволяет влиять на его адресное пространство и функционирование. В данном случае описатель дочернего процесса возвращается создающему процессу в составе параметра ppiProcInfo. Хотя он не может быть напрямую передан другому процессу, тем не менее, имеется возможность передать другому процессу его дубликат. Таким путем при необходимости в группе процессов может быть сформирована нужная иерархия.

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

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

    #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 и лекцию "Планирование потоков").

    (рис 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) Управляющие структуры данных потока

    Изображенные на рис 5.5 структуры, за исключением блоков переменных окружения потока (TEB), существуют в системном адресном пространстве. Помимо этого, параллельная структура для каждого потока, созданного в Win32-процессе, поддерживается процессом Csrss подсистемы Win32. В свою очередь, часть подсистемы Win32, работающая в режиме ядра (Win32k.sys), поддерживает для каждого потока структуру W32THREAD.

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

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

    Создание потока инициируется Win32-функцией CreateThread, которая находится в библиотеке Kernel32.dll. При этом создается объект ядра "поток", хранящий статистическую информацию о создаваемом потоке. В адресном пространстве процесса выделяется память под пользовательский стек потока. Затем инициализируется аппаратный контекст потока (ниже имеется описание соответствующей структуры CONTEXT).

    Вслед за этим создается блок управления потоком вместе с сопутствующими структурами, формируется стек ядра потока и о создании потока уведомляется подсистема Win32. Наконец, вызывающему потоку возвращается описатель создаваемого потока и передается управление, а новому потоку может быть выделено процессорное время.

    Функция CreateThread

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

    HANDLE CreateThread ( 
    PSECURITY_ATTRIBUTES psa, 
    DWORD cbStack,
    PTHREAD_START_ROUTINE  pfnStartAddr, 
    PVOID pvParam, 
    DWORD fdwCreate, 
    PDWORD pdwThreadID);

    Прогон программы создания потока

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

    #include <windows.h>
    #include <stdio.h>
    
    DWORD WINAPI MyThread( LPVOID lpParam ) 
    { 
     printf("Parameter = %d\n", *(DWORD*)lpParam);
     return 0; 
    } 
     
    VOID main( VOID ) 
    { 
      DWORD ThreadId, ThreadParameter = 10; 
      HANDLE hThread; 
      
      hThread = CreateThread( 
        NULL,            // атрибуты безопасности по умолчанию 
        0,               // размер стека по умолчанию  
        MyThread  ,        // указатель на процедуру создаваемого потока
        ThreadParameter,      // аргумент, передаваемый функции потока 
        0,               // флаги создания по умолчанию
        ThreadId);          // возвращаемый идентификатор потока
    
    	 if (hThread == NULL)  printf("CreateThread failed." );    
      
        getchar();
        CloseHandle( hThread );
       }

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

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

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

    Контекст потока, переключение контекстов

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

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

    Контекст отражает состояние регистров процессора на момент последнего исполнения потока и хранится в структуре CONTEXT, определенной в заголовочном файле WinNT.h. Элементы этой структуры соответствуют регистрам процессора, например, для процессоров x86 процессоров в ее состав входят Eax, Ebx, Ecx, Edx и т д.. Win32-функция GetThreadContext позволяет получить текущее состояние контекста, а функция SetThreadContext - задать новое содержимое контекста. Перед этой операцией поток рекомендуется приостановить.

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

    Заключение

    Поток представляет собой набор исполняющихся команд для текущего момента исполнения. С одним или несколькими потоками ассоциирован набор ресурсов, которые объединены в рамках процесса. Для описания процесса в системе поддерживается связанная совокупность структур, главной из которых является структура EPROCESS. В свою очередь, структура ETHREAD и связанные с ней структуры необходимы для реализации потоков. В лекции проанализированы функции CreateProcess и CreateThread и этапы создания процессов и потоков. Важными характеристиками потока являются его контекст и состояние. Наблюдение за состоянием потоков предлагается осуществить при помощи инструментальных средств системы.

    Страницы:

    Понятие процесса и потока

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

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

    Для описания столь сложного динамического объекта ОС поддерживает набор структур, главную из которых принято называть блоком управления процессом (PCB, Process control block). В состав PCB обычно включают:

  • состояние, в котором находится процесс;
  • программный счетчик процесса или, другими словами, адрес команды, которая должна быть выполнена для него следующей;
  • содержимое регистров процессора;
  • данные, необходимые для планирования использования процессора и управления памятью (приоритет процесса, размер и расположение адресного пространства и т. д.);
  • учетные данные (идентификационный номер процесса, какой пользователь инициировал его работу, общее время использования процессора данным процессом и т. д.);
  • информацию об устройствах ввода-вывода, связанных с процессом (например, какие устройства закреплены за процессом; таблица открытых файлов).
  • Блок управления процессом является моделью процесса для операционной системы. Любая операция, производимая операционной системой над процессом, вызывает определенные изменения в PCB. Псевдопараллельное выполнение процессов предполагает периодическую приостановку текущего процесса и его последующее возобновление. Для этого нужно уметь сохранять часть данных из PCB, которые обычно называют контекстом процесса, а операцию по сохранению данных одного процесса и восстановлению данных другого называют переключением контекстов. Переключение контекста не имеет отношения к полезной работе, выполняемой процессами, и время, затраченное на него, сокращает полезное время работы процессора.

    Потоки

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

    (рис 5.1) Процесс с несколькими потоками

    В этом случае процесс можно рассматривать в качестве контейнера ресурсов, а все проблемы, связанные с динамикой исполнения, решаются на уровне потоков. Обычно каждый процесс начинается с одного потока, а остальные (при необходимости) создаются в ходе выполнения. Теперь уже не процесс, а поток характеризуется состоянием, поток является единицей планирования, процессор переключается между потоками, и необходимо сохранять контекст потока (что существенно проще, чем сохранение контекста процесса). Подобно процессам потоки (нити, threads) в системе описываются структурой данных, которую обычно называют блоком управления потоком (thread control block, TCB).

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

    Внутреннее устройство процессов в ОС Windows

    В 32-разрядной версии системы у каждого процесса есть 4-гигабайтное адресное пространство, в котором пользовательский код занимает нижние 2 гигабайта (в серверах 3 Гбайта). В своем адресном пространстве, которое представляет собой набор регионов и описывается специальными структурами данных (см. часть III "система управления памятью"), процесс содержит потоки, учетную информацию и ссылки на ресурсы, которые обобществляются всеми потоками процесса.

    Блок управления процессом (PCB) реализован в виде набора связанных структур, главная из которых называется блоком процесса EPROCESS. Соответственно, каждый поток также представлен набором структур во главе с блоком потока ETHREAD. Эти наборы данных, за исключением блоков переменных окружения процесса и потока (PEB и TEB), существуют в системном адресном пространстве. Упрощенная схема структур данных процесса показана на рис 5.2.

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

    Содержимое блока EPROCESS подробно описано в . Блок KPROCESS (на рис. справа), блок переменных окружения процесса (PEB) и структура данных, поддерживаемая подсистемой Win32 (блок процесса Win32), содержат дополнительные сведения об объекте "процесс".

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

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

    Обычно процесс создается другим процессом вызовом Win32-функции CreateProcess (а также CreateProcessAsUser и CreateProcessWithLogonW ). Создание процесса осуществляется в несколько этапов.

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

    На втором этапе выполняется обращение к системному сервису NtCreateProcess для создания объекта "процесс". Формируются блоки EPROCESS, KPROCESS и блок переменных окружения PEB. Менеджер процессов инициализирует в блоке процесса маркер доступа (копируя аналогичный маркер родительского процесса), идентификатор и другие поля.

    На третьем этапе в уже полностью проинициализированном объекте "процесс" необходимо создать первичный поток. Это, посредством системного сервиса NtCreateThread, делает библиотека kernel32.dll.

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

    Наконец, запускается первичный поток, для чего формируются его начальный контекст и стек, и выполняется запуск стартовой процедуры потока режима ядра KiThreadStartup. После этого стартовый код из библиотеки C/C++ передает управление функции main() запускаемой программы.

    В книге этапы создания процесса описаны более подробно.

    Функция CreateProcess

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

    BOOL CreateProcess( 
      PCTSTR pszApplicationName, 
      PTSTR pszCommandLine, 
      PSECURITY_ATTRIBUTES psaProcess, 
      PSECURITY_ATTRIBUTES psaThread, 
      BOOL bInheritHandles, 
      DWORD fdwCreate, 
      PVOID pvEnvironment,
      PCTSTR pszCurDir, 
      PSTARTUPINFO psiStartInfo, 
      PPROCESS_INFORMATION ppiProcInfo);

    Описание параметров функции можно посмотреть в MSDN.

    Формально ОС Windows не поддерживает какой-либо иерархии процессов, например, отношений "родительский-дочерний". Однако, негласная иерархия, заключающаяся в том, кто чьим дескриптором (описателем) владеет, все же существует. Например, владение дескриптором процесса позволяет влиять на его адресное пространство и функционирование. В данном случае описатель дочернего процесса возвращается создающему процессу в составе параметра ppiProcInfo. Хотя он не может быть напрямую передан другому процессу, тем не менее, имеется возможность передать другому процессу его дубликат. Таким путем при необходимости в группе процессов может быть сформирована нужная иерархия.

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

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

    #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 и лекцию "Планирование потоков").

    (рис 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) Управляющие структуры данных потока

    Изображенные на рис 5.5 структуры, за исключением блоков переменных окружения потока (TEB), существуют в системном адресном пространстве. Помимо этого, параллельная структура для каждого потока, созданного в Win32-процессе, поддерживается процессом Csrss подсистемы Win32. В свою очередь, часть подсистемы Win32, работающая в режиме ядра (Win32k.sys), поддерживает для каждого потока структуру W32THREAD.

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

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

    Создание потока инициируется Win32-функцией CreateThread, которая находится в библиотеке Kernel32.dll. При этом создается объект ядра "поток", хранящий статистическую информацию о создаваемом потоке. В адресном пространстве процесса выделяется память под пользовательский стек потока. Затем инициализируется аппаратный контекст потока (ниже имеется описание соответствующей структуры CONTEXT).

    Вслед за этим создается блок управления потоком вместе с сопутствующими структурами, формируется стек ядра потока и о создании потока уведомляется подсистема Win32. Наконец, вызывающему потоку возвращается описатель создаваемого потока и передается управление, а новому потоку может быть выделено процессорное время.

    Функция CreateThread

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

    HANDLE CreateThread ( 
    PSECURITY_ATTRIBUTES psa, 
    DWORD cbStack,
    PTHREAD_START_ROUTINE  pfnStartAddr, 
    PVOID pvParam, 
    DWORD fdwCreate, 
    PDWORD pdwThreadID);

    Прогон программы создания потока

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

    #include <windows.h>
    #include <stdio.h>
    
    DWORD WINAPI MyThread( LPVOID lpParam ) 
    { 
     printf("Parameter = %d\n", *(DWORD*)lpParam);
     return 0; 
    } 
     
    VOID main( VOID ) 
    { 
      DWORD ThreadId, ThreadParameter = 10; 
      HANDLE hThread; 
      
      hThread = CreateThread( 
        NULL,            // атрибуты безопасности по умолчанию 
        0,               // размер стека по умолчанию  
        MyThread  ,        // указатель на процедуру создаваемого потока
        ThreadParameter,      // аргумент, передаваемый функции потока 
        0,               // флаги создания по умолчанию
        ThreadId);          // возвращаемый идентификатор потока
    
    	 if (hThread == NULL)  printf("CreateThread failed." );    
      
        getchar();
        CloseHandle( hThread );
       }

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

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

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

    Контекст потока, переключение контекстов

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

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

    Контекст отражает состояние регистров процессора на момент последнего исполнения потока и хранится в структуре CONTEXT, определенной в заголовочном файле WinNT.h. Элементы этой структуры соответствуют регистрам процессора, например, для процессоров x86 процессоров в ее состав входят Eax, Ebx, Ecx, Edx и т д.. Win32-функция GetThreadContext позволяет получить текущее состояние контекста, а функция SetThreadContext - задать новое содержимое контекста. Перед этой операцией поток рекомендуется приостановить.

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

    Заключение

    Поток представляет собой набор исполняющихся команд для текущего момента исполнения. С одним или несколькими потоками ассоциирован набор ресурсов, которые объединены в рамках процесса. Для описания процесса в системе поддерживается связанная совокупность структур, главной из которых является структура EPROCESS. В свою очередь, структура ETHREAD и связанные с ней структуры необходимы для реализации потоков. В лекции проанализированы функции CreateProcess и CreateThread и этапы создания процессов и потоков. Важными характеристиками потока являются его контекст и состояние. Наблюдение за состоянием потоков предлагается осуществить при помощи инструментальных средств системы.

    Вернуться к учебному плану