Лабораторна робота № 1
Тема: «Процеси та потоки»
Мета: Засвоїти поняття «процесів» та «потоків» як основних компонентів сучасних операційних систем. Здобути навики створення, керування та знищення «процесів» та «потоків» в операційній системі Windows.
1. Теоретична частина
В ОС Windows XP реалізована пріоритетна (витісняюча) багатозадачність. Це означає, що ОС може тимчасово припинити виконання однієї програми і перемкнути процесор на виконання іншої. Перемикання відбувається незалежно від бажання кожної з програм, завдяки чому зависання однієї програми не приводить до зависання всієї системи.
Запускаючи на виконання яку-небудь програму, ви створюєте новий процес. Процес звичайно визначають як “екземпляр” (іноді говорять, копію) виконуваного додатку. Для управління процесом система створює об’єкт ядра “процес”. Кожному об’єкту ядра виділяється в системі блок пам’яті, ініціалізований тією або іншою управляючою інформацією, зіставляється лічильник числа користувачів і описувач — ідентифікатор об’єкту (дескриптор). Сам по собі процес нічого не виконує — він просто “володіє” чотирьох гігабайтним адресним простором, що містить код і дані для ехе-файлу програми. Крім адресного простору, процесу належать такі ресурси, як файли, динамічні області пам’яті і потоки. Ресурси, створювані за життя процесу, обов’язково знищуються при його завершенні.

Щоб процес що-небудь THREAD_PRIORITY_NORMAL
Пріоритет потоку повинен відповідати класу пріоритету процесу

THREAD_PRIORITY_ABOVE_NORMAL
Пріоритет потоку повинен бути на 1 одиницю більше класу пріоритету процесу

THREAD_PRIORITY_HIGHEST
Пріоритет потоку повинен бути на 2 одиниці більше класу пріоритету процесу


Використовування декількох потоків в одному процесі дуже важливо з кількох причин. По-перше, це дозволяє добитися мінімального простою процесора, а значить – працювати більш ефективно. По-друге, потоки можуть виконувати які-небудь дії у фоновому режимі щодо основної програми. По-третє, потоки зручно використовувати також у випадку, якщо блокування або підвисання якої-небудь процедури не повинне стати причиною порушень функціонування основної програми.
2. Системні виклики для роботи з процесами
Простіше всього для створення нового процесу використовувати виклик WinExec(). При зверненні до цього виклику необхідно повідомити ім’я програми (повний шлях до ехе-файлу) і спосіб відображення вікна програми. Функція визначається таким чином:
int WinExec (const char* CmdLine, unsigned int CmdShow)
Параметр CmdLine є покажчиком на рядок, що містить ім’я виконуваного файлу, параметр CmdShow найчастіше має значення SW_RESTORE. При успішному виконанні функція повертає значення, більше 31.
Більш широкі можливості надаються функцією ShellExecute(). Вона може не тільки запускати заданий додаток, але і відкривати документ, пов’язаний з цим додатком. Для використовування функції ShellExecute() в модуль треба додати директиву препроцесора
# include <ShellAPl.h>
яка підключає модуль ShellAPI, в якому описана функція. Функція визначена таким чином:
void ShellExecute(HWnd Wnd, const char * Operation,
const char *FileName, const_ char *Parameters,
const char *Directory, unsigned int ShowCmd).
Параметр Wnd є дескриптором батьківського вікна, в якому відображаються повідомлення додатку, що запускається. Звичайно як він можна просто вказати Handle.
Параметр Operation указує на рядок з нульовим символом в кінці, яка визначає виконувану операцію. Цей рядок може містити текст “open” (відкрити), “print” (надрукувати) або “explore” (досліджувати). Якщо параметр Operation рівний NULL, то за замовчанням виконується операція “open”.
Параметр FileName указує на рядок з нульовим символом в кінці, яка визначає ім’я файлу, що відкривається, або ім’я теки, що відкривається.
Параметр Parameters указує на рядок з нульовим символом в кінці, яка визначає передавані в додаток параметри, якщо FileName визначає виконуваний файл. Якщо FileName указує на рядок, що визначає документ або теку, що відкривається, то цей параметр задається рівним NULL.
Параметр Directory указує на рядок з нульовим символом в кінці, що визначає каталог за замовчуванням.
Параметр ShowCmd визначає режим відкриття вказаного файлу, звичайно, як і для функції WinExec, використовується значення SW_RESTORE.
Для того, щоб мати нагоду управляти створеним процесом в додатку слід використовувати виклик CreateProcess(). Функція була визначена таким чином:
BOOL CreateProcess(LPСTSTR IpszImageName
LPCTSTR IpszCommandLine
LPSECURITY_ATTRIBUTES IpsaProcess
LPSECURITY_ATTRIBUTES IpsaThread
BOOL fInheritHandles
DWORD fdwCreate
LPVOID IpvEnvironment
LPTSTR IpszCurDir
LPSTARTUPINFO IpsiStartInfo
LPPROCESS_INFORMATION IppiProcinfo);
Коли в додатку викликається CreateProcess(), система створює об’єкт ядра "процес" з початковим значенням лічильника числа користувачів, рівним одиниці. Цей об’єкт —компактна структура даних, через яку операційна система управляє процесом. Далі система переходить до створення об’єкту ядра "потік" (з лічильником числа користувачів, рівним одиниці) для управління первинним потоком нового процесу. Якщо системі вдається створити новий процес і його первинний потік, функція повертає TRUE.
Параметр lpszImageName містить адресу рядка з повним шляхом до ехе-файлу.
Параметр lpszCommandLine містить покажчик на командний рядок, але звичайно його задають NULL.
Параметри lpsaProcess і lpsaThread визначають потрібні атрибути захисту для об’єктів “процес” і “потік” відповідно. У ці параметри частіше за все заносять NULL, і тоді система закріплює за даними об’єктами дескриптори захисту за умовчанням.
Параметр fInheritHandles указує, чи успадковує новий процес дескриптори, що належать поточному процесу. Звичайно цей параметр рівний FALSE.
Параметр fdwCreate визначає прапори, що впливають на спосіб створення нового процесу, але на практиці вони використовуються не часто, тому значення цього параметра слід задати рівним нулю.
Параметр lpvEnvironment указує на блок пам’яті, що містить рядки змінних оточення, якими користуватиметься новий процес. Звичайно замість нього передається NULL, внаслідок чого породжуваний процес успадковує рядки змінних оточення від батьківського процесу.
Параметр lpszCurDir дозволяє батьківському процесу встановити в "дочірньому" поточний диск і каталог. Якщо його значення — NULL, робочий каталог нового процесу буде розташований там же, де і у додатку, що його породив.
Параметр lpsiStartInfo містить інформацію про запуск процесу і указує на структуру STARTUPINFO. При створенні процесу її слід визначити таким чином:
STARTUPINFO SProcess;
//ініціалізація
ZeroMemory(&SProcess,sizeof(SProcess));
SProcess.cb = sizeof(SProcess);
Параметр lppiProcInfo вказує на структуру PROCESS_INFORMATION, яку потрібно заздалегідь створити; її елементи ініціалізувалися самою функцією CreateProcess(). Структура є наступною:
typedef struct _PROCESS_INFORMATION {
HANDLEhProcess;
HANDLE hTh read;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION;
Як наголошувалося, створення нового процесу викликає і створення об’єктів ядра "процес" і "потік". У момент створення система присвоює кожному об’єкту початкове значення лічильника числа користувачів — одиницю. Далі функція CreateProcess() — перед самим поверненням — відкриває об’єкт "процес" і об’єкт "потік" і заносить їх описувачі (дескриптори) в елементи hProcess і hThread структури PROCESS_INFORMATION. Коли функція CreateProcess() відкриває ці об’єкти, лічильники кожного з них збільшуються до 2. Це означає, що — перш ніж система зможе вивільнити з пам’яті об’єкт "процес" — процес повинен бути завершений (лічильник зменшується до 1), а батьківський процес — викликати функцію CloseHandle() (і тим самим скинути лічильник до 0). Те ж саме відноситься і до об’єкту "потік": потік повинен бути завершений, а батьківський процес — закрити описувач об'єкту "потік".
Створеному процесу присвоюється унікальний ідентифікатор; ні у яких процесів, що виконуються в системі, не може бути однакових ідентифікаторів. Те ж стосується і потоків. Завершуючи свою роботу, CreateProcess() заносить значення ідентифікаторів в елементи dwProcessId і dwThreadId структури. Щоб надалі управляти створеними процесом і його потоком, використовуються їх дескриптори.
Коли відпадає необхідність у використовуванні об’єктів ядра "потік" і "процес" необхідно з потоків, що їх створили, викликати функцію CloseHandle(). Тим самим зменшується лічильник числа їх користувачів на 1. При досягненні лічильником нуля об’єкти видаляються системою. Функція визначена таким чином:
CloseHandle (HANDLE hObject);
Параметр hObject містить дескриптор об’єкту ядра, лічильник якого треба зменшити.
Володіючи дескриптором процесу, можна управляти процесом за допомогою різних викликів.
Звичайно процес створюється з класом пріоритету normal. Отримати клас пріоритету процесу можна використовуючи функцію GetPriorityClass().Функція визначена таким чином:
DWORD GetPriorityClass (HANDLE hProcess).
Параметр hProcess містить дескриптор процесу, клас пріоритету якого треба визначити. Функція повертає один з прапорів, перерахованих в таблиці 2.
Таблиця 2 – Рівні пріоритету процесу
Прапор
Клас пріоритету

IDLE_PRIORITY_CLASS
Idle

NORMAL_PRIORITY_CLASS
Normal

HIGH_PRIORITY_CLASS
High

REALTIME_PRIORITY_CLASS
Realtime


Змінити клас пріоритету процесу можна функцією SetPriorityClass().Функция визначена таким чином:
DWORD GetPriorityClass (HANDLE hProcess, DWORD fdwPriority).
Параметр hProcess містить дескриптор процесу, клас пріоритету якого треба змінити, параметр fdwPriority містить один з прапорів, перерахованих в таблиці 2.
Щоб визначити момент завершення процесу, можна використовувати виклик GetExitCodeProcess(). Цей виклик повертає або значення STILL_ACTIVE (якщо процес все ще продовжує роботу), або код завершення процесу (якщо процес був завершений). Функція визначена таким чином:
BOOL GetExitCodeProcess (HANDLE hProcess, DWORD ExitCode).
Параметр hProcess містить дескриптор процесу, а в параметр ExitCode після виконання функції записується код завершення процесу. Якщо виклик успішний, функція повертає TRUE.
Для завершення процесу використовується виклики ExitProcess() або TerminateProcess(). Виклик ExitProcess() використовується, якщо потрібно завершити поточний процес, для чого цю функцію повинен викликати один з потоків процесу. Функція визначена таким чином:
ExitProcess (UINT fuExitCode).
Ніяких значень функція не повертає, а як параметр передається нуль.
Функція TerminateProcess() дозволяє завершити будь-який процес. Вона була визначена таким чином:
BOOL TerminateProcess (HANDLE hProcess, UINT fuExitCode).
Параметр hProcess містить дескриптор завешуваного процесу, а в параметр ExitCode слід передати нуль. Якщо виклик успішний, функція повертає TRUE.
Щоб мати нагоду використовувати перераховані функції управління процесом, необхідний відповідний рівень доступу до дескриптора процесу. Цей рівень доступу можна отримати застосувавши функцію OpenProcess(). Ця функція використовується також отримання дескриптора вже створеного процесу по відомому ідентифікатору. Функція визначена таким чином:
HANDLE OpenProcess (DWORD dwFlag, DWORD dwProcessId).
Параметр dwFlag містить необхідний рівень або комбінацію рівнів, представлених в таблиці 3, а в параметрі dwProcessId указується ідентифікатор процесу. Функція повертає дескриптор процесу із заданими рівнями доступу.
Таблиця 3 – Рівні доступу до процесу
Рівень доступу
Призначення

PROCESS_TERMINATE
Для завершення роботи процесу

PROCESS_QUERY_INFORMATION
Для отримання класу пріоритету і коду завершення процесу

PROCESS_SET_INFORMATION
Для встановлення класу пріоритету процесу

PROCESS_ALL_ACCESS
Для отримання повного доступу


Отримати ідентифікатор процесу можна за допомогою функції GetCurrentProcessId(). Функція визначена таким чином:
DWORD GetCurrentProcessId (VOID).
Функція не містить параметрів і повертає ідентифікатор того процесу, чий потік її викликав.
Приклад створення процесу з отриманням всіх рівнів доступу до дескриптора цього процесу.
//дескриптор процесу
HANDLE hProc=NULL;
//установки нового процесу
STARTUPINFO SProcess;
//структура з інформацією про створений процес
PROCESS_INFORMATION ProcInfo;
//ініціалізація
ZeroMemory(&SProcess,sizeof(SProcess));
SProcess.cb = sizeof(SProcess);
//створення дочірнього процесу
CreateProcess(NULL, ”Notepad.exe” ,NULL, NULL, FALSE, 0, NULL, NULL
&SProcess, &ProcInfo);
//відособлення процесу
CloseHandle(ProcInfo.hThread);
CloseHandle(ProcInfo.hProcess);
//отримання необхідних рівнів доступу для управління процесом
hProc = OpenProcess(PROCESS_ALL_ASSECC, FALSE, ProcInfo.dwProcessId);
3. Системні виклики для роботи з потоками
Якщо ви для розробки своїх програм використовуєте С++, то для використовування стандартною функцією створення потоку в модуль треба додати директиву препроцесора
# include <process.h>.
Бібліотечна функція створення потоку була визначена таким чином:
unsigned long beginthreadex(void *security, unsigned stack_side
unsigned (*start_address)(void*), void* arglist,
unsigned initflag, unsigned *thrdaddr).
Функція містить декілька параметрів. Розглянемо послідовно кожний з них. Параметр security є покажчиком на структуру SECURITY_ATTRIBUTES, яка визначає потрібні атрибути захисту для створюваного об’єкту “потік. Передайте в цей параметр NULL.
Параметр stack_side визначає яку частину потік може використовувати під свій стек. В цей параметр потрібно передати нуль, тоді система виділить 1Мб, що цілком достатньо для потоку.
Третій параметр є покажчиком на адресу функції потоку, з якою він почне свою роботу.
Параметр arglist використовується для передачі у функцію потоку якого-небудь простого 32-бітового покажчика на структуру даних або просто 32-бітове число.
Параметр initflag визначає додаткові прапори, що управляють створенням процесу. Якщо він рівний нулю, то виконання потоку починається негайно. Можна присвоїти йому ідентифікатор CREATE_SUSPENDED, тоді система створить потік, але притримуватиме його виконання до наступних вказівок.
Параметр thrdaddr є покажчиком на змінну, куди функція помістить ідентифікатор потоку.
Функція повертає дескриптор створеного потоку.
Приклад створення потоку з передачею в його функцію деякого цілого числа.
//------------------------------------------------------------------
//функція потоку
static unsigned __stdcall ThreadFunc(void *Num)
{
int Number = *(int*) Num; //число, що приймається
Number++;
return(0);
}
//------------------------------------------------------------------
//функція створення потоку по натисненню кнопки
void __fastcall TForm1::Button1Click(TObject *Sender)
{
DWORD ThreadId; // ідентифікатор потоку
HANDLE hThread; // дескриптор потоку
int i = 5; // передаване число

//створення потоку
hThread=(HANDLE)_beginthreadex(NULL, 0, ThreadFunc(void *), &i,
0, &ThreadId);
}
//-----------------------------------------------------------------
Після створення потоку можна виконувати які-небудь операції управління потоком, використовуючи його дескриптор.
Для збільшення або зменшення швидкості виконання потоку (точніше за програмний код, який він реалізує) потрібно відповідно міняти його пріоритет, використовуючи функцію SetThreadPriority(). Функція визначена таким чином:
BOOL SetThreadPriority (HANDLE hThread, int nPriority).
Параметр hThread містить дескриптор потоку, рівень пріоритету якого треба змінити, а параметр nPriority містить один з прапорів, визначених в таблиці 1.Функция повертає TRUE, якщо пріоритет вдалося встановити.
Отримати рівень пріоритету потоку можна функцією GetThreadPriority(). Функція визначена таким чином:
int GetThreadPriority (HANDLE hTherad).
Параметр hThread містить дескриптор потоку, рівень пріоритету якого треба отримати. Функція повертає один з прапорів, визначених в таблиці 1.
Виконання потоку можна припинити і знову відновити. Це може бути потрібно у випадку, якщо первинний потік виконує деякі процедури ініціалізацій глобальних змінних, що використовуються в обох потоках. Припинити виконання потоку можна при його створінні, використовуючи прапор CREATE_SUSPENDED, про що вже згадувалося, або використовуючи функцію SuspendThread(). Функція визначена таким чином:
DWORD SuspendThread (HANDLE hThread).
Параметр hThread містить дескриптор потоку, чиє виконання треба припинити. У випадку, якщо виклик невдалий, функція повертає 0xFFFFFFFF.
Продовжити виконання потоку можна використовуючи функцію ResumeThread(). Функція визначена таким чином:
DWORD ResumeThread (HANDLE hThread).
Параметр hThread містить дескриптор потоку, чиє виконання треба відновити. У випадку, якщо виклик невдалий, функція повертає 0xFFFFFFFF.
Щоб визначити момент завершення потоку, використовується виклик GetExitCodeThread(). Цей виклик повертає або значення STILL_ACTIVE (якщо потік все ще продовжує роботу), або код завершення потоку. Функція визначена таким чином:
BOOL GetExitCodeThread (HANDLE hThread, DWORD ExitCode).
Параметр hThread містить дескриптор потоку, а в параметр ExitCode після виконання функції записується код завершення потоку. Якщо виклик успішний, функція повертає TRUE.
Для завершення потоку використовуються виклики _endthreadex() або TerminateThread(). Функція _endthreadex() викликається зсередини потоку, який треба завершити. Вона визначена таким чином:
_endthreadex(unsigned int).
Ніяких значень функція не повертає, а як параметр передається нуль.
Функція TerminateThread() дозволяє завершити будь-який потік. Вона визначена таким чином:
BOOL TerminateThread (HANDLE hThread, UINT fuExitCode).
Параметр hThread містить дескриптор потоку який необхідно завершити, а в параметр ExitCode слід передати нуль. Якщо виклик успішний, функція повертає TRUE.
Після завершення потоку слід викликати функцію CloseHandle(). Тим самим зменшивши лічильник числа користувачів об’єкту ядра “потік” на 1. При досягненні лічильником нуля об’єкт видаляється системою.
4. Завдання на лабораторну роботу
1. Розробити програму в середовищі Visual Studio, що демонструє використання системних викликів, відзначених символом “+”, відповідно до варіанту (таблиця 4).
Таблиця 4 – Варіанти завдань
Системний
виклик
Варіант


1
2
3
4
5
6
7
8
9
10

WinExec
+

+

+

+

+


ShellExecute

+

+

+

+

+

CreateProcess
+
+
+
+
+
+
+
+
+
+

OpenProcess
+
+
+
+
+
+
+
+
+
+

GetCurrentProcessId
+

+
+

+

+

+

CloseHandle
+
+
+
+
+
+
+
+
+
+

GetExitCodeProcess

+
+

+

+

+


TerminateProcess
+
+

+
+

+
+

+

GetPriorityClass
+

+

+
+
+

+


SetPriorityClass

+

+

+

+
+
+

_beginthreadex
+
+
+
+
+
+
+
+
+
+

_endthreadex

+

+
+

+
+

+

GetExitCodeThread
+
+
+
+

+


+


TerminateThread
+

+

+
+
+
+
+
+

SetThreadPriority

+

+

+

+

+

GetThreadPriority

+

+

+

+

+

SuspendThread
+

+

+

+

+


ResumeThread
+

+

+

+

+



2. Написати функцію потоку, яка як вхідний параметр приймає дескриптор відкритого текстового файлу. Функція повинна здійснювати посимвольне виведення у файл номер процесу, номер потоку та системний час. Передбачити достатню кількість ітерації запису в файл з одної функції потоку. Відкриття файлу слід виконувати до створення потоку, використовуючи функцію FileOpen(), а після завершення роботи з файлом слід викликати функцію FileClose().
3. Щоб продемонструвати паралельне виконання створеного потоку з первинним потоком процесу, в цикл запису з файлу потрібно додати виклик Sleep(Num), де Num – час в мілісекундах, на яке слід призупинити виконання потоку. Затримку також можна організувати за допомогою лічильника до якогось досить великого числа.
4. Проаналізувати вміст файлу після завершення програми та порівняти записи при активному та пасивному очікуванні.