Раздел 14 — Ресурсы ОС и процессы

Первые программы на Windows API: как попросить систему о ресурсе и как запустить другую программу

Прогресс курса Раздел 14 из 20

Что вы освоите в этом разделе

6 академических часов теории, практической работы нет. В конце — тест по разделу на портале: пока он не сдан на 70 баллов, раздел 15 закрыт. Программы раздела работают только в Windows. Сборка — та же команда: g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe; заголовок <windows.h> и нужные библиотеки MinGW-w64 подключает сам. Правила курса (решение Д1): функции Windows API — в ANSI-версиях с буквой A на конце, строки — массивы char (раздел 10).
01

Операционная система — менеджер ресурсов

На компьютере одновременно работают десятки программ, а процессор, память, диск и сеть — одни на всех. Кто решает, какая программа сейчас выполняется, где лежат её данные в памяти, можно ли ей открыть этот файл? Операционная система. Её главная работа — распределять ресурсы между программами и не давать одной программе испортить другую или саму систему.

РесурсЧто делает ОСРаздел курса
процессорное времяделит его между потоками, переключая их много раз в секунду14, 16
оперативная памятьвыделяет каждому процессу своё адресное пространство, защищает чужое18
файлы и устройствапроверяет права, упорядочивает доступ11, 17
каналы связи, сетьпередаёт данные между процессами и компьютерами17, 19

Защита держится на двух режимах работы процессора. В режиме ядра выполняется код самой ОС и драйверов: ему доступны вся память и всё оборудование. В режиме пользователя выполняются обычные программы — и ваши тоже: они не могут напрямую обратиться к диску или к памяти другой программы. Когда программе нужен ресурс, она просит систему: вызывает функцию, которая переключает процессор в режим ядра, выполняет запрос с проверками и возвращает результат. Набор таких функций для программ Windows — Windows API.

Вы уже пользовались им не зная того: std::cout в итоге вызывает функцию Windows WriteFile, а std::ifstream — CreateFile и ReadFile. Стандартная библиотека C++ — удобная обёртка; в этом блоке мы спускаемся на уровень ниже и обращаемся к системе напрямую.

02

Первая программа на Windows API. Типы

// Первая программа на Windows API: сведения о своём процессе и о компьютере
#include <windows.h>
#include <iostream>

int main()
{
    std::cout << "PID: " << GetCurrentProcessId() << '\n';
    std::cout << "Command line: " << GetCommandLineA() << '\n';

    char name[MAX_COMPUTERNAME_LENGTH + 1];
    DWORD size = sizeof(name);                  // на входе — размер буфера, на выходе — длина имени
    if (GetComputerNameA(name, &size)) {
        std::cout << "Computer: " << name << " (" << size << " chars)\n";
    }

    char dir[MAX_PATH];
    const UINT length = GetWindowsDirectoryA(dir, MAX_PATH);
    if (length > 0 && length < MAX_PATH) {      // 0 — ошибка, >= MAX_PATH — буфер мал
        std::cout << "Windows directory: " << dir << '\n';
    }

    const DWORD priority = GetPriorityClass(GetCurrentProcess());
    std::cout << "Priority class: " << (priority == NORMAL_PRIORITY_CLASS ? "normal" : "not normal") << '\n';
    std::cout << "Uptime: " << GetTickCount64() / 1000 / 60 << " min\n";
    return 0;
}
> info
PID: 7412
Command line: info
Computer: DESKTOP-K3L7 (12 chars)
Windows directory: C:\Windows
Priority class: normal
Uptime: 312 min

Числа и имена у вас будут свои. Типы Windows API — псевдонимы обычных типов C++, объявленные в <windows.h>. Их имена пишут заглавными буквами:

Тип WindowsЧто это в C++Где встречается
BOOLint; TRUE = 1, FALSE = 0результат большинства функций: успех или нет
DWORDбеззнаковое целое 32 битаидентификаторы, размеры, коды ошибок и завершения
UINT, ULONGLONGunsigned int, беззнаковое 64 битадлины, время в миллисекундах
HANDLEvoid* — адрес «чего угодно»дескриптор объекта ядра (пункт 03)
LPSTR, LPCSTRchar*, const char*строки: изменяемые и только для чтения
MAX_PATHконстанта 260вместимость буфера для пути к файлу
03

Объекты ядра и дескрипторы

Процессы, потоки, открытые файлы, каналы, события — всё это объекты ядра: структуры данных, которые хранит и которыми управляет ОС в своей памяти. Программа не может до них дотянуться — её память отделена. Вместо адреса объекта она получает дескриптор (HANDLE) — номер в таблице объектов своего процесса. По этому номеру она потом просит систему что-то сделать с объектом: прочитать, подождать, завершить.

ДействиеФункцияРезультат
создать или открыть объектCreateFileA, CreateProcessA, OpenProcess, CreateToolhelp32Snapshot…дескриптор
работать с нимReadFile, WaitForSingleObject, TerminateProcess…дескриптор — первый параметр
закрыть дескрипторCloseHandle(h)запись в таблице процесса освобождается
04

Ошибки: результат и GetLastError

Функция Windows API сообщает о неудаче своим результатом, а причину записывает в «код последней ошибки» потока. Его получают GetLastError() — сразу после неудачного вызова: следующая функция Windows может его перезаписать.

Функция возвращаетПризнак неудачиПримеры
BOOLFALSE (0)CreateProcessA, CloseHandle, GetExitCodeProcess
HANDLE от функций создания файлов и снимковINVALID_HANDLE_VALUE (−1)CreateFileA, CreateToolhelp32Snapshot
HANDLE от остальныхNULL (0)OpenProcess, CreateThread, CreateEventA
длину строки0 (а длина ≥ размера буфера — буфер мал)GetWindowsDirectoryA

Разное значение ошибки у HANDLE — историческая особенность; какое именно, написано в документации каждой функции (Microsoft Learn, раздел «Return value»). Проверка не того значения — частая ошибка: if (file == NULL) после CreateFileA не ловит ничего.

// Ошибки Windows API: о неудаче функция сообщает результатом, о причине — через GetLastError
#include <windows.h>
#include <iostream>

void printError(const char what[]);

int main(int argc, char* argv[])
{
    SetConsoleOutputCP(1251);                   // текст системы — в кодировке 1251 (раздел 2)
    const char* name = argc > 1 ? argv[1] : "no-such-file.txt";
    HANDLE file = CreateFileA(name, GENERIC_READ, FILE_SHARE_READ, nullptr,
                              OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (file == INVALID_HANDLE_VALUE) {         // у CreateFile ошибка — не NULL, а INVALID_HANDLE_VALUE
        printError("CreateFile");
        return 2;
    }
    LARGE_INTEGER size;
    if (GetFileSizeEx(file, &size)) {
        std::cout << name << ": " << size.QuadPart << " bytes\n";
    } else {
        printError("GetFileSizeEx");
    }
    CloseHandle(file);                          // дескриптор получен — должен быть закрыт
    return 0;
}

// Что не удалось, код ошибки и её текст от системы; вызывается сразу после неудачной функции
void printError(const char what[])
{
    const DWORD code = GetLastError();
    char text[256];
    const DWORD length = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                                        nullptr, code, 0, text, sizeof(text), nullptr);
    std::cout << what << " failed, error " << code;
    if (length > 0) {
        std::cout << ": " << text;              // текст системы уже кончается переводом строки
    } else {
        std::cout << '\n';
    }
}
> lasterror
CreateFile failed, error 2: Не удается найти указанный файл.
> lasterror info.cpp
info.cpp: 1034 bytes

FormatMessageA превращает код в текст на языке системы. Функцию printError вы будете копировать во все программы блока В. Самые частые коды: 2 — файл не найден, 3 — путь не найден, 5 — доступ запрещён, 6 — неверный дескриптор, 87 — неверный параметр, 183 — объект уже существует.

05

Процесс

Процесс — запущенная программа вместе со всем, что ей выдала система. Файл notepad.exe на диске — программа; три открытых окна Блокнота — три процесса одной программы.

Что есть у процессаПояснение
адресное пространствосвоя виртуальная память: код, данные, стек, кучи. Адрес 0x5ffe9c из раздела 9 в двух процессах — две разные ячейки. Один процесс не может прочитать память другого (подробно — раздел 18)
идентификатор PIDчисло, уникальное среди живых процессов системы; после завершения может достаться новому процессу
хотя бы один потокпоток выполняет код; процесс без потоков завершается. Первый поток выполняет main (раздел 16)
таблица дескрипторовобъекты ядра, которые процесс открыл
приоритет, права, текущий каталог, переменные окружения, командная строкаполучены от родителя или заданы при создании
код завершенияSTILL_ACTIVE (259), пока процесс работает; затем — значение return из main

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

Диспетчер задач (Ctrl+Shift+Esc), вкладка «Подробности», показывает процессы с PID, состоянием, объёмом памяти и именем пользователя. Правый щелчок по заголовку столбцов → «Выбрать столбцы» — можно добавить «Потоки» и «Дескрипторы». Держите его открытым при опытах этого раздела: видно, как процесс появляется, сколько у него дескрипторов и как он исчезает.
06

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

У функции десять параметров. В курсе почти всегда нужны три: командная строка, структура STARTUPINFOA и структура PROCESS_INFORMATION, куда система запишет результат (раздел 11, пункт 06: функция получает адрес структуры и заполняет её поля).

ПараметрВ курсеСмысл
lpApplicationNamenullptrимя программы берётся из командной строки
lpCommandLineсвой массив char«программа аргументы»; функция может временно его менять, поэтому литерал не подходит
lpProcessAttributes, lpThreadAttributesnullptrправа доступа по умолчанию
bInheritHandlesFALSEнаследует ли дочерний процесс дескрипторы (раздел 17 — TRUE)
dwCreationFlags0флаги: CREATE_NEW_CONSOLE — своя консоль, CREATE_SUSPENDED — создать приостановленным, класс приоритета
lpEnvironment, lpCurrentDirectorynullptrокружение и текущий каталог — как у родителя
lpStartupInfo&siокно и стандартные потоки дочернего процесса; обязательно si.cb = sizeof(si)
lpProcessInformation&piна выходе: hProcess, hThread — дескрипторы; dwProcessId, dwThreadId — идентификаторы

Для опытов пригодится маленькая «подопытная» программа child: ждёт заданное время и завершается с заданным кодом.

// Дочерняя программа для опытов: ждёт заданное число миллисекунд и завершается с заданным кодом
#include <windows.h>
#include <iostream>
#include <cstdlib>

int main(int argc, char* argv[])
{
    const int code = argc > 1 ? std::atoi(argv[1]) : 0;    // atoi — без проверки: для опытов достаточно
    const int ms = argc > 2 ? std::atoi(argv[2]) : 0;
    std::cout << "child " << GetCurrentProcessId() << ": sleep " << ms << " ms, exit " << code << '\n';
    Sleep(ms);
    return code;
}
07

Ожидание и код завершения

WaitForSingleObject(h, миллисекунды) приостанавливает поток, пока объект не перейдёт в «сигнальное» состояние. Для процесса это значит — «завершился». Время ожидания INFINITE — ждать сколько угодно. Пока поток ждёт, он не расходует процессор: ОС просто не выделяет ему времени.

// Фрагмент run.cpp: запуск, ожидание, код завершения. Полностью — в примерах раздела
    STARTUPINFOA si = {};                       // все поля — нули
    si.cb = sizeof(si);                         // размер структуры — обязательное поле
    PROCESS_INFORMATION pi = {};
    const ULONGLONG start = GetTickCount64();
    if (!CreateProcessA(nullptr, cmd, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) {
        printError("CreateProcess");
        return 2;
    }
    std::cout << "Started: " << cmd << ", PID " << pi.dwProcessId << '\n';
    CloseHandle(pi.hThread);                    // дескриптор главного потока не нужен

    WaitForSingleObject(pi.hProcess, INFINITE); // ждать, пока процесс не завершится
    DWORD exitCode = 0;
    GetExitCodeProcess(pi.hProcess, &exitCode);
    std::cout << "Finished in " << GetTickCount64() - start << " ms, exit code " << exitCode << '\n';
    CloseHandle(pi.hProcess);
> run child 3 1500
Started: child 3 1500, PID 9120
child 9120: sleep 1500 ms, exit 3
Finished in 1516 ms, exit code 3
> run nosuch
CreateProcess failed, error 2: Не удается найти указанный файл.
08

Завершение процесса. Тайм-аут

СпособЧто происходит
return из mainнормальное завершение: деструкторы объектов, закрытие потоков std::ofstream (данные дописываются на диск), код завершения — возвращённое значение
ExitProcess(code)немедленное завершение своего процесса; деструкторы локальных объектов не вызываются
TerminateProcess(h, code)принудительное завершение чужого процесса: он не узнаёт об этом и ничего не успевает сохранить. Крайняя мера — как «Снять задачу» в Диспетчере задач

Если ждать не бесконечно, а с тайм-аутом, WaitForSingleObject возвращает, почему ожидание кончилось:

// Фрагмент timeout.cpp: ожидание не дольше seconds секунд. Полностью — в примерах раздела
    DWORD exitCode = 0;
    const DWORD result = WaitForSingleObject(pi.hProcess, static_cast<DWORD>(seconds) * 1000);
    switch (result) {
    case WAIT_OBJECT_0:                         // завершился сам
        GetExitCodeProcess(pi.hProcess, &exitCode);
        std::cout << "Finished, exit code " << exitCode << '\n';
        break;
    case WAIT_TIMEOUT:                          // время вышло — завершить принудительно
        if (TerminateProcess(pi.hProcess, TIMEOUT_EXIT_CODE)) {
            WaitForSingleObject(pi.hProcess, INFINITE);     // TerminateProcess только начинает завершение
            std::cout << "Timeout: terminated after " << seconds << " s\n";
            exitCode = TIMEOUT_EXIT_CODE;
        } else {
            printError("TerminateProcess");
        }
        break;
    default:
        printError("WaitForSingleObject");
        break;
    }
    CloseHandle(pi.hProcess);
> timeout 2 "child 0 5000"
child 5208: sleep 5000 ms, exit 0
Timeout: terminated after 2 s
> timeout 5 "child 7 1000"
child 6044: sleep 1000 ms, exit 7
Finished, exit code 7
Результат WaitForSingleObjectЗначит
WAIT_OBJECT_0объект в сигнальном состоянии: процесс завершился
WAIT_TIMEOUTвремя вышло, процесс ещё работает
WAIT_FAILEDошибка ожидания (например, неверный дескриптор) — причина в GetLastError

Чужой процесс по PID открывают OpenProcess(PROCESS_TERMINATE, FALSE, pid) — получают дескриптор с правом завершения (или NULL и ошибку 5, если прав нет: процессы других пользователей и системы так не завершить).

09

Список процессов

Как Диспетчер задач узнаёт, какие процессы есть? Один из способов — снимок: CreateToolhelp32Snapshot копирует список процессов на данный момент, а Process32First и Process32Next выдают его записи по одной в структуру PROCESSENTRY32 (заголовок <tlhelp32.h>):

// Фрагмент ps.cpp: процессы, в имени которых есть строка из аргумента. Полностью — в примерах раздела
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printError("CreateToolhelp32Snapshot");
        return 2;
    }
    PROCESSENTRY32 entry = {};
    entry.dwSize = sizeof(entry);
    int count = 0;
    std::cout << "     PID  Threads  Name\n";
    if (Process32First(snapshot, &entry)) {
        do {
            if (containsIgnoreCase(entry.szExeFile, filter)) {
                std::cout << std::setw(8) << entry.th32ProcessID << std::setw(9) << entry.cntThreads
                          << "  " << entry.szExeFile << '\n';
                ++count;
            }
        } while (Process32Next(snapshot, &entry));
    }
    CloseHandle(snapshot);
    std::cout << count << " processes\n";
> ps note
     PID  Threads  Name
   10344        7  notepad.exe
1 processes
10

Ловушки раздела

ОшибкаПризнакСообщение g++ / ошибка WindowsКак избежать
литерал в командной строке CreateProcessAсобирается с предупреждением; при запуске функция может записать в литерал — аварийное завершениеISO C++ forbids converting a string constant to 'LPSTR' {aka 'char*'} [-Wwrite-strings]командная строка — свой массив char
результат функции не проверенпрограмма «работает», но ничего не делает—каждый вызов — с проверкой и printError
GetLastError не сразу после ошибкикод 0 или чужая ошибка—вызывать первой строкой обработки ошибки
== NULL вместо == INVALID_HANDLE_VALUE (или наоборот)ошибка не замечена, дальше — работа с неверным дескрипторомошибка 6 у следующей функциизначение ошибки — из документации функции
не заполнены si.cb или entry.dwSizeфункция отказывает или ведёт себя непредсказуемоу Process32First — ошибка 24структура — = {}, затем размер
дескрипторы не закрытырастёт счётчик дескрипторов в Диспетчере задач—CloseHandle для hProcess, hThread, снимков, файлов
код завершения прочитан до завершениякод 259—сначала WaitForSingleObject
после TerminateProcess сразу читают код или удаляют файлы процессапроцесс ещё не успел завершиться—ожидание после TerminateProcess
программа не найденаCreateProcess отказываетошибка 2путь к программе или запуск из её каталога
русский текст ошибки — «кракозябры»нечитаемый вывод—SetConsoleOutputCP(1251) (раздел 2)

Ключевые выводы раздела

Запомните главное

  • ОС распределяет ресурсы; программа в режиме пользователя получает их только через функции ОС — Windows API
  • Функции Windows — версии A, строки — массивы char с размером буфера
  • Объект ядра доступен по дескриптору HANDLE; каждый полученный дескриптор закрывается CloseHandle
  • Неудача — по результату (FALSE, NULL или INVALID_HANDLE_VALUE), причина — GetLastError сразу после вызова
  • Процесс — адресное пространство, PID, потоки, дескрипторы, код завершения
  • CreateProcessA: изменяемая командная строка, si.cb, два дескриптора на выходе
  • WaitForSingleObject — ожидание с тайм-аутом; код завершения — после ожидания
  • TerminateProcess — крайняя мера; список процессов — снимок Toolhelp
Связи раздела. Опирается на раздел 2 — сборка и кодовая страница консоли; раздел 6 — do while, код завершения; раздел 9 — указатели, nullptr, параметр-указатель; раздел 10 — строки char, буфер и размер, argc/argv; раздел 11 — структуры, которые заполняет функция; раздел 13 — ТЗ на утилиту. Нужен для: раздела 15 — дескриптор консоли; раздела 16 — потоки, WaitForSingleObject для потока; раздела 17 — дочерний процесс с перенаправленным вводом-выводом; раздела 18 — адресное пространство процесса.
Тест по разделу — на учебном портале. Тест, попытки и оценки — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 13: Техническое задание Тест на портале