Uinc процеси в windows
Шановним Новомосковсктелям, які (поки що)
не знають хто такий Дж.Ріхтер і що таке SDK.
Процеси в Windows
Процесом зазвичай називають екземпляр виконуваної програми.
Хоча на перший погляд здається, що програма і процес поняття практично однакові, вони фундаментально відрізняються один від одного. Програма представляє собою статичний набір команд, а процес це набір ресурсів і даних, що використовуються при виконанні програми. Процес в Windows складається з наступних компонентів:
- Структура даних, що містить всю інформацію про процес, в тому числі список відкритих дескрипторів різних системних ресурсів, унікальний ідентифікатор процесу, різну статистичну інформацію і т.д .;
- Унікальний ідентифікатор потоку;
- Зміст набору регістрів процесора, що відображають стан процесора;
- Два стека, один з яких використовується потоком при виконанні в режимі ядра, а інший - в режимі користувача;
- Закриту область пам'яті, звану локальною пам'яттю потоку (thread local storage, TLS) і використовувану підсистемами, run-time бібліотеками і DLL.
Щоб все потоки працювали, операційна система відводить кожному з них певний процесорний час. Тим самим створюється ілюзія одночасного виконання потоків (зрозуміло, для багатопроцесорних комп'ютерів можливий істинний паралелізм). У Windows реалізована система витісняє планування на основі пріоритетів, в якій завжди виконується потік з найбільшим пріоритетом, готовий до виконання. Обраний для виконання потік працює протягом деякого періоду, званого квантом. Квант визначає, скільки часу буде виконуватися потік, поки операційна система не перерве його. Після закінчення кванта операційна система перевіряє, чи готовий до виконання інший потік з таким же (або великим) рівнем пріоритету. Якщо таких потоків не виявилося, поточного потоку виділяється ще один квант. Однак потік може не повністю використовувати свій квант. Як тільки інший потік з вищим пріоритетом готовий до виконання, поточний потік витісняється, навіть якщо його квант ще не закінчився.
Всякий раз, коли виникає переривання от таймера, з кванта потоку віднімається 3, і так до тих пір, поки він не досягне нуля. Частота спрацьовування таймера залежить від апаратної платформи. Наприклад, для більшості однопроцесорних x86 систем він становить 10мс, але в більшості багатопроцесорних x86 систем - 15мс.
Планування в Windows здійснюється на рівні потоків, а не процесів. Це здається зрозумілим, так як самі процеси не виконуються, а лише надають ресурси і контекст для виконання потоків. Тому при плануванні потоків, система не звертає уваги на те, якому процесу вони належать. Наприклад, якщо процес А має 10 готових до виконання потоків, а процес Б - два, і все 12 потоків мають однаковий пріоритет, кожен з потоків отримає 1/12 процесорного часу.
У Windows існує 32 рівня пріоритету, від 0 до 31. Вони групуються так: 31 - 16 рівні реального часу; 15 - 1 динамічні рівні; 0 - системний рівень, зарезервований для потоку обнулення сторінок (zero-page thread).
При створенні процесу, йому призначається один з шести класів пріоритетів:
Real time class (значення 24),
High class (значення 13),
Above normal class (значення 10),
Normal class (значення 8),
Below normal class (значення 6),
і Idle class (значення 4).
Прив'язка до процесорів
Якщо операційна система виконується на машині, де встановлено більше одного процесори, то по замовчуванню, потік виконується на будь-якому доступному процесорі. Однак в деяких випадках, набір процесорів, на яких потік може працювати, може бути обмежений. Це явище називається прив'язкою до процесорів (processor affinity). Можна змінити прив'язку до процесорів програмно, через Win32-функції планування.
- Відкривається файл образу (EXE), який буде виконуватися в процесі. Якщо виконуваний файл не є Win32 додатком, то шукається спосіб підтримки (support image) для запуску цієї програми. Наприклад, якщо виконується файл з розширенням .bat, запускається cmd.exe і т.п.
- Створюється об'єкт Win32 «процес».
- Створюється первинний потік (стек, контекст і об'єкт "потік").
- Підсистема Win32 повідомляється про створення нового процесу і потоку.
- Розпочинається виконання первинного потоку.
Процес завершується якщо:
- Вхідна функція первинного потоку повернула управління.
- Один з потоків процесу викликав функцію ExitProcess.
- Потік іншого процесу викликав функцію TerminateProcess.
Приклад 1. Програма створює процес "Калькулятор".
Об'єкт ядра це, по суті, структура, створена ядром й доступна тільки йому. У користувальницький додаток передається тільки описувач (handle) об'єкта, а управляти об'єктом ядра можна за допомогою функцій Win32 API.
Як можна призупинити роботу потоку? Існує багато способів. Ось деякі з них.
Функція Sleep () призупиняє роботу потоку на задане число мілісекунд. Якщо в якості аргументу ви вкажете 0 ms, то відбудеться наступне. Потік відмовиться від свого кванта процесорного часу, однак тут же з'явиться в списку потоків готових до виконання. Іншими словами відбудеться навмисне перемикання потоків. (Вірніше сказати, спроба перемикання. Адже таким для виконання потоком цілком може стати той же самий.)
Функція WaitForSingleObject () призупиняє виконання потоку до тих пір, поки не відбудеться одне з двох подій:
- закінчиться затримку читання;
- очікуваний об'єкт перейде в сигнальне (signaled) стан.
По поверненню значенням можна зрозуміти, яке з двох подій відбулося. Очікувати за допомогою wait-функцій можна більшість об'єктів ядра, наприклад, об'єкт "процес" або "потік", щоб визначити, коли вони завершать свою роботу.
Функції WaitForMultipleObjects передається відразу масив об'єктів. Можна очікувати спрацьовування відразу всіх об'єктів або якогось одного з них.
Приклад 2. Програма створює два однакових потоку і очікує їх завершення.
Потоки просто виводять текстове повідомлення, яке передано їм при ініціалізації.
М'ютекси (Mutex) це об'єкти ядра, які створюються функцією CreateMutex (). М'ютекс буває в двох станах - зайнятому і вільному. М'ютексів добре захищати одиничний ресурс від одночасного звернення до нього різними потоками.
Приклад 3. Припустимо, в програмі використовується ресурс, наприклад, файл або буфер в пам'яті. Функція WriteToBuffer () викликається з різних потоків. Щоб уникнути колізій при одночасному зверненні до буферу з різних потоків, використовуємо м'ютекс. Перш ніж звернутися до буферу, очікуємо мютекса.
Семафор (Semaphore) створюється функцією CreateSemaphore (). Він дуже схожий на м'ютекс, тільки на відміну від нього у семафора є лічильник. Семафор відкритий якщо лічильник більше 0 і закритий, якщо лічильник дорівнює 0. семафора зазвичай "огороджують" набори рівнозначних ресурсів (елементів), наприклад чергу, список і т.п.
Приклад 4. Класичний приклад використання семафора це чергу елементів, яку обробляють кілька потоків. Потоки "розбирають" елементи з черги. Якщо чергу порожня, потоки повинні "спати", чекаючи появи нових елементів. Для обліку елементів в черзі використовується семафор.
Зауваження. У цьому прикладі ми вважаємо, що самі процедури додавання елемента в чергу і видалення з черги безпечні з точки зору многопоточности. Не будемо поки стосуватися деталей їх реалізації. Детальніше ми розглянемо це в прикладі 9.
Події (Event), також як і м'ютекси мають два стани - встановлене та скинуте. Події бувають зі скиданням вручну і з автоскиданням. Коли потік дочекався (wait-функція повернула управління) події з автоскиданням, така подія автоматично скидається. В іншому випадку подія потрібно скидати вручну, викликавши функцію ResetEvent (). Припустимо, відразу кілька потоків очікують одного і того ж події, і подія спрацювало. Якщо це була подія з автоскиданням, то воно дозволить працювати тільки одному потоку (адже відразу ж після повернення з його wait-функції подія скинеться автоматично!), А інші потоки залишаться чекати. Якщо ж це була подія зі скиданням вручну, то всі потоки отримають керування, а подія так і залишиться в установленому стані, поки який-небудь потік не викличе ResetEvent ().
Приклад 5. Ось ще один приклад многопоточного додатки. Програма має два потоки; один готує дані, а другий відсилає їх на сервер. Розумно распараллелить їх роботу. Тут потоки повинні працювати по черзі. Спочатку перший потік готує порцію даних. Потім другий потік відправляє її, а перший тим часом готує наступну порцію і т.д. Для такої синхронізації знадобиться два event'а з автоскиданням.
Функція PulseEvent () встановлює подія й тут же переводить його назад в скинуте стан; її виклик рівнозначний послідовному викликом SetEvent () і ResetEvent (). Якщо PulseEvent викликається для події зі скиданням у ручну, то все потоки, які очікують цей об'єкт, отримують управління. При виклику PulseEvent для події з автоскиданням пробуджується тільки один з чекають потоків. А якщо жоден з потоків не чекає об'єкт-подія, виклик функції не дає ніякого ефекту.
Приклад 7. Реалізуємо функцію NextFrame () для попереднього прикладу для промоткі файлу вручну по кадрам.
Мабуть, очікувані таймери - найвитонченіший об'єкт ядра для синхронізації. З'явилися вони, починаючи з Windows 98. Таймери створюються функцією CreateWaitableTimer і бувають, також як і події, з автоскиданням і без нього. Потім таймер треба налаштувати функцією SetWaitableTimer. Таймер переходить в сигнальний стан, коли закінчується його таймаут. Скасувати "цокання" таймера можна функцією CancelWaitableTimer. Примітно, що можна вказати callback функцію при установці таймера. Вона буде виконуватися, коли спрацьовує таймер.
Приклад 8. Напишемо програму-будильник використовуючи WaitableTimer'и. Будильник буде срабативат раз в день о 8 ранку і "піку" 10 разів. Використовуємо для цього два таймера, один з яких з callback-функцією.
Критичні секції. Синхронізація в призначеному для користувача режимі.
Критична секція гарантує вам, що шматки коду програми, обгороджені їй, не виконуватимуться одночасно. Строго кажучи, критична секція не є об'єктом ядра. Вона являє собою структуру, що містить кілька прапорів і якоїсь (не важливо) об'єкт ядра. При вході в критичну секцію спочатку перевіряються прапори, і якщо виявляється, що вона вже зайнята іншим потоком, то виконується звичайна wait-функція. Критична секція примітна тим, що для перевірки, зайнята вона чи ні, програма не переходить в режим ядра (не виконується wait-функція) а лише перевіряються прапори. Через це вважається, що синхронізація за допомогою критичних секцій найбільш швидка. Таку синхронізації називають "синхронізація в призначеному для користувача режимі".
Приклад 9 Знову розглянемо чергу елементів. Один з варіантів її реалізації - двусвязний список. З точки зору багатопоточності, небезпечними є операції додавання і видалення елементів з черги. Існує ймовірність, що кілька потоків одночасно почнуть перебудовувати покажчики і зв'язність черзі порушиться. Щоб цього уникнути, використовуємо критичну секцію.
Описувачі об'єктів ядра залежні від конкретного процесу (process specific). Простіше кажучи, handle об'єкта, отриманий в одному процесі, не має сенсу в іншому. Однак існують способи роботи з одними і тими ж об'єктами ядра з різних процесів.
По-перше, це успадкування опису. Під час створення об'єкта можна вказати чи буде його описувач успадковуватися дочірніми (породженими цим процесом) процесами.
По-друге, дублювання опису. Функція DuplicateHandle дублює описатель об'єкта одного процесу в інший, тобто по суті, бере запис в таблиці описателей одного процесу й створює її копію в таблиці іншого.
І, нарешті, іменування об'єкта ядра. При створенні об'єкта ядра для синхронізації (мьютекса, семафора, очікуваного таймера або події) можна задати його ім'я. Воно має бути унікальним в системі. Тоді інший процес може відкрити цей об'єкт ядра, вказавши в функції Open ... (OpenMutex, OpenSemaphore, OpenWaitableTimer, OpenEvent) це ім'я.
Насправді, при виконанні функції Create. () Система спочатку перевіряє, чи не існує вже об'єкт ядра з таким ім'ям. Якщо немає, то створюється новий об'єкт. Якщо так, ядро перевіряє тип цього об'єкта і права доступу. Якщо типи не збігаються або ж викликає, не має повних прав на доступ до об'єкта, виклик Create ... функції закінчується невдало і повертається NULL. Якщо все нормально, то просто створюється новий описувач (handle) існуючого вже об'єкта ядра. За кодом повернення функції GetLastError () можна зрозуміти що сталося: створився нового об'єкт або Create () повернула вже існуючий.
Тому, синхронізувати потоки всередині різних процесів можна точно також як і в межах одного. Потрібно тільки правильно передати описатель синхронизирующего об'єкта від одного процесу до іншого будь-яким з перерахованих вище способів.
Приклад 10. Багато додатків при запуску перевіряють, чи запущений ще один екземпляр цієї програми. Стандартний спосіб реалізації цієї перевірки - використання пойменованого м'ютексів.
Інші механізми (сокети, pipe)
Канали використовуються для пересилання даних в одному напрямку між дочірнім і батьківським процесами або між двома дочірніми процесами. Операції читання / запису в канал схожі на подібні операції при роботі з файлами.
Зазначені канали використовуються для двостороннього обміну даними між процесом-сервером і одним або декількома процесами-клієнтами. Як і анонімні канали, вони використовують файлоподобний інтерфейс, але, на відміну від перших, придатні також для обміну даними по мережі.
Сокет - це абстрактний об'єкт для позначення одного з кінців мережевого з'єднання, в тому числі і через Internet. Сокети Windows бувають двох типів: сокети дейтаграм і сокети потоків. Інтерфейс Windows Sockets (WinSock) заснований на BSD-версії сокетов, але в ньому є також розширення, специфічні для Windows.
Повідомлення в Windows (віконні повідомлення)
Говорячи про Windows не можна не згадати про такі поняття як windows (вікна), messages (повідомлення), message queue (черга повідомлень) і т.д.
Window - це (прямокутна) область екрану в якій додаток відображає інформацію (якщо воно мабуть, звичайно) і отримує інформацію від користувача. Вікна належать потокам. Потік, який створив вікно вважається власником цього вікна. Потік може бути власником кількох вікон.
Вікна управляються повідомленнями. Всі події відбуваються з вікном, супроводжуються посилкою їй повідомлень: створення та знищення вiкна, введення з клавіатури, переміщення миші, перерисовка і переміщення вікна і т.д. Повідомлення вікна можуть надсилатися як самою системою, так і для користувача додатками. Кожному вікна приписана функція звана віконної процедурою (window procedure), яка і викликається при обробці повідомлення.
Повідомлення можна надсилати не тільки вікна, а й самому потоку. Кожен потік, який володіє вікном, має чергу повідомлень. Як правило, потік, що володіє вікнами, лише тим і займається, що обробляє повідомлення, що посилаються його вікон.
Якщо описатели об'єктів ядра процесо-залежні, то описатели вікон унікальні в межах Deskop. Тому одному процесу не складає ніяких труднощів отримати і використовувати описувач вікна належить потоку іншого процесу.
Посилка ж повідомлень з однієї програми іншому є не що інше, як один із способів межпроцессного спілкування.
Приклад 12. Програма знаходить вікно з заголовком "Калькулятор" та закриває його, посилаючи повідомлення WM_CLOSE.