Лекції "Системне програмування"

5. Лекція 5.2. Реалізація процесів і потоків

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

Поняття процесу і потоку

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

З курсу теорії операційних систем відомо, що процес є динамічним об'єктом, що описує виконання програми. Процесу виділяються системні ресурси: закритий адресний простір, семафори, комунікаційні порти, файли і т.д. Процес характеризується поточним станом (виконання, очікування, готовність і т.д.).

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

  • стан, в якому знаходиться процес;
  • програмний лічильник процесу або, іншими словами, адреса команди, яка повинна бути виконана наступною;
  • вміст регістрів процесора;
  • дані, необхідні для планування використання процесора і управління пам'яттю (пріоритет процесу, розмір і розташування адресного простору і т. д.);
  • облікові дані (ідентифікаційний номер процесу, який користувач ініціював його роботу, загальний час використання процесора даним процесом і т. д.);
  • інформацію про пристрої введення-виведення, пов'язаних з процесом (наприклад, які пристрої закріплені за процесом; таблиця відкритих файлів).

Блок управління процесом є моделлю процесу для операційної системи. Будь-яка операція, вироблена операційною системою над процесом, викликає певні зміни в PCB. Псевдопараллельное виконання процесів передбачає періодичне припинення  поточнї діяльністі та її подальше відновлення. Для цього потрібно вміти зберігати якісь дані з PCB, які зазвичай називають контекстом процесу, а операцію по збереженню даних одного процесу і відновлення даних іншого називають перемиканням контекстів. Перемикання контексту не має відношення до корисної роботи, що виконується процесами, і час, витрачений на нього, скорочує корисний час роботи процесора.

Потоки

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

Мал. 5.1. Процес з декількома потоками

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

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

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

У 32-розрядної версії системи у кожного процесу є 4-гігабайтний адресний простір, в якому користувальницький код займає нижні 2 гігабайти (в серверах 3 Гб). У своєму адресному просторі, яке представляє собою набір регіонів і описується спеціальними структурами даних, процес містить потоки, облікову інформацію і посилання на ресурси.

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

Мал. 5.2. Керуючі структури даних процесу

Блок 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)

Зазвичай в стані "Готовності" є черга готових до виконання (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 відповідають очікуванню натискання клавіші введення.

Окремі характеристики потоків

Ідентифікатори потоків, так само як і ідентифікатори процесів, кратні чотирьом, вибираються з того ж простору, що і ідентифікатори процесів, і з ними не перетинаються.

Як вже говорилося, коли потік звертається до системного виклику, то перемикається в режим ядра, після чого залишиться активним той же потік, але вже в режимі ядра. Тому у кожного потоку два стека, один працює в режимі ядра, інший - в режимі користувача. Один і той же стек не може використовуватися і в режимі користувача, і в режимі ядра. Будь який потік може робити все що завгодно зі своїм власним стеком (стеком режиму користувача), в тому числі організовувати кілька стеків і перемикатися між ними. Потік сам може визначати розмір свого стека. При цьому не можна гарантувати, що стек буде мати достатній розмір, щоб код ядра виконався без жодних проблем. Оскільки виникнення виняткової ситуації в режимі ядра може привести до краху всієї системи, необхідно виключити таку можливість, що і здійснюється шляхом організації окремого стека для режиму ядра. Так як в режимі ядра можуть одночасно перебувати кілька потоків і між ними може відбуватися перемикання, у кожного з них повинен бути окремий стек режиму ядра.

Крім стану, ідентифікатора і двох стеків, у кожного потоку є контекст, маркер доступу, а також невелика власна пам'ять для зберігання локальних змінних, наприклад, для запам'ятовування коду помилки. Оскільки процес є контейнером ресурсів всіх вхідних в нього потоків, будь-який потік може отримати доступ до всіх об'єктів свого процесу, незалежно від того, яким потоком даного процесу цей об'єкт створений.

Волокна і завдання

Перемикання між потоками займає досить багато часу, тому для полегшеного псевдопаралелізма в системі підтримуються волокна (fibers). Наявність волокон дозволяє реалізувати власний механізм планування, не використовуючи вбудований механізм планування потоків на основі пріоритетів.

ОС не знає про зміну волокон, для управління волокнами немає системних викликів, однак є виклики WinAPI ConvertThreadToFiber, CreateFiber, SwitchToFiber і т.д.

В системі є також завдання (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);

   }

Завершення потоку можна організувати різними способами, наприклад, за допомогою функцій ExitThread або TerminateThread.

Подібно процесам при завершенні потоку зіставлений з ним об'єкт ядра "потік" не звільняється допоки не будуть закриті всі зовнішні посилання на цей об'єкт.

Контекст потоку, перемикання контекстів

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

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

Ця інформація зберігається в поточному стеку ядра потоку.

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

Крім перерахованих в системі є багато корисних функцій, що реалізують API для керування потоками. Їх повний перелік міститься в MSDN.

Висновок

Потік являє собою набір команд для поточного моменту виконання. З одним або декількома потоками асоційований набір ресурсів, які об'єднані в рамках процесу. Для опису процесу в системі підтримується пов'язана сукупність структур, головною з яких є структура EPROCESS. У свою чергу, структура ETHREAD і пов'язані з нею структури необхідні для реалізації потоків. Важливими характеристиками потоку є його контекст і стан. Спостереження за станом потоків пропонується здійснити за допомогою інструментальних засобів системи.