02. Процессы и задания в прикладном программном интерфейсе

Лекция №2. Процессы и задания в прикладном программном интерфейсе

Курс: Системное программирование (2026–2027)
Учебная программа: 2025, регистрационный № УП-46/2025Пп/уч
Специальность переподготовки: 9-09-0612-02 «Программное обеспечение информационных систем»
Квалификация: Инженер-программист
Тема по программе: Тема 2. Процессы и задания в прикладном программном интерфейсе (4 академических часа)
Формируемые компетенции: СП-23, СП-24


Введение и цели занятия

📌 Слайд 1: Тема 2. Процессы и задания в прикладном программном интерфейсе

Если на первой лекции мы изучили шлюз взаимодействия между прикладным кодом и ядром (системный вызов и API), то на втором занятии мы переходим к фундаментальным объектам операционной системы — процессам и потокам.

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

📌 Слайд 2: План лекции

Учебные вопросы лекции:

  1. Концепция процесса и виртуализация системных ресурсов (память, процессор).
  2. Анатомия виртуального адресного пространства процесса.
  3. Граф состояний процесса и механизм переключения контекста (Context Switch).
  4. Системные интерфейсы создания процессов: модель POSIX (fork/exec) против модели WinAPI (CreateProcess).
  5. Управление группами процессов и заданиями: cgroups в Linux и Job Objects в Windows.
  6. Многопоточность: разграничение ресурсов процесса и потока, архитектурные модели потоков (1:1, N:1, M:N).
  7. Асинхронное выполнение системных вызовов (IOCP, io_uring) и концепции удаленного запуска.

1. Понятие процесса и виртуализация системных ресурсов

📌 Слайд 3: Понятие процесса и виртуализация системных ресурсов

В операционных системах необходимо четко разделять понятия «программа» и «процесс»:

  • Программа (Program): пассивный объект, файл на дисковом накопителе (исполняемый образ в формате ELF или PE), содержащий скомпилированный машинный код, константы и метаданные.
  • Процесс (Process): активная сущность, экземпляр выполняющейся программы в оперативной памяти, обладающий собственным изолированным адресным пространством и набором системных ресурсов.

Фундаментальный принцип современных ОС — виртуализация ресурсов:

  1. Виртуализация процессора (Time-Sharing / Псевдопараллелизм): операционная система создает для процесса иллюзию того, что он монопольно владеет центральным процессором. Планировщик ядра периодически переключает процессор между десятками и сотнями потоков, выделяя каждому поток квант времени (Time Slice, обычно от 1 до 20 мс).
  2. Виртуализация памяти (Virtual Memory): каждому процессу предоставляется собственное сплошное виртуальное адресное пространство (например, 128 ТБ в 64-битной архитектуре x86-64). Физическая память делится на страницы размером 4 КБ. Процесс работает только с виртуальными адресами, которые аппаратура MMU транслирует в физические кадры памяти (Page Frames). Процесс физически изолирован и не может обратиться к адресам другого процесса.

📌 Слайд 4: Анатомия адресного пространства процесса

Адресное пространство процесса состоит из логических секций (сегментов), обладающих различными атрибутами защиты:

  • Секция кода (.text): содержит машинные инструкции исполняемой программы. Доступна только для чтения и выполнения (Read-Only + Execute). Повторная запись в эту область блокируется аппаратурой (защита от внедрения шеллкода).
  • Секция инициализированных данных (.data): глобальные и статические переменные, значения которых заданы программистом до старта (Read/Write).
  • Секция неинициализированных данных (.bss): глобальные и статические переменные, не имеющие явной инициализации. В исполняемом файле хранится лишь размер этой области; при создании процесса ядро заполняет ее физическими страницами, содержащими нули.
  • Куча (Heap): область динамического выделения памяти во время работы программы (malloc, free, new, delete, HeapAlloc). Растет от младших адресов к старшим.
  • Область разделяемой памяти и отображения файлов (Memory Mapping / Shared Libraries): страницы, выделяемые вызовом mmap в Linux или MapViewOfFile в Windows. Здесь размещаются исполняемые файлы динамических библиотек (libc.so, kernel32.dll) и разделяемые сегменты IPC.
  • Стек вызовов (User Stack): выделяется операционной системой под аргументы функций, локальные переменные и адреса возврата. Растет от старших адресов к младшим навстречу куче.
  • Область ядра (Kernel Space): отображение кода и структур данных ядра. Доступно только при CPL = 0 (Ring 0).

center

📌 Слайд 5: Управляющие структуры процесса в ядре

Ядро ОС отслеживает состояние каждого процесса с помощью специализированного дескриптора — Блока управления процессом (Process Control Block, PCB).

1. Ядро Linux: структура task_struct

В ядре Linux каждый исполняемый контекст (и процесс, и поток) представляется структурой struct task_struct (определена в заголовке <linux/sched.h>). Дескриптор содержит:

  • Идентификаторы: pid (идентификатор потока/процесса), tgid (Thread Group ID — соответствует общепринятому понятию PID процесса), real_parent (родительский процесс);
  • Состояние планировщика: state (Ready, Running, Stopped, Zombie);
  • Указатель на структуру управления памятью struct mm_struct *mm (каталог таблиц страниц pgd, список областей виртуальной памяти vma);
  • Таблицу открытых файлов struct files_struct *files (массив указателей на struct file);
  • Структуры обработки сигналов signal, sighand;
  • Контекст безопасности: cred (UID, GID, capabilities).

2. Ядро Windows: структуры EPROCESS, KPROCESS и PEB

В операционной системе Windows процесс описывается тремя ключевыми блоками:

  • KPROCESS (Kernel Process Block): низкоуровневая структура ядра. Содержит базовый приоритет, маску привязки к процессорам (Affinity), указатель на таблицу страниц (Directory Table Base, значение для CR3).
  • EPROCESS (Executive Process Block): высокоуровневая надстройка ядра над KPROCESS. Содержит PID, имя исполняемого файла, маркер доступа (Access Token), таблицу дескрипторов (Handle Table), квоты на ресурсы.
  • PEB (Process Environment Block): структура пользовательского режима (Ring 3). Доступна процессу напрямую без системных вызовов (адрес доступен через регистр GS на x64 или FS на x86). Содержит список загруженных DLL-модулей, аргументы командной строки, переменные окружения и кучу по умолчанию.

2. Жизненный цикл процесса и механизм переключения контекста

📌 Слайд 6: Жизненный цикл и состояния процесса

С момента порождения до полного уничтожения процесс проходит через последовательность состояний:

  1. Создание (New / Created): процесс формируется. Ядро выделяет память под структуры PCB/EPROCESS, инициализирует дескрипторы и настраивает начальное адресное пространство.
  2. Готовность (Ready): процесс полностью готов к исполнению и находится в очереди диспетчера планировщика (Run Queue). Он ожидает, когда планировщик предоставит ему свободное процессорное ядро.
  3. Выполнение (Running): инструкции процесса непосредственно исполняются на физическом ядре процессора.
  4. Ожидание / Блокировка (Blocked / Waiting): процесс добровольно уступает процессор, так как ожидает завершения внешней операции (чтение сектора с диска, сетевой пакет, освобождение мьютекса или завершение таймера sleep). Процесс исключается из очереди планирования и не расходует такты CPU.
  5. Завершение (Terminated / Zombie): процесс прекратил выполнение инструкций (вызов exit() или аварийный сигнал). Большая часть ресурсов (память, открытые файлы) освобождается, но запись о процессе в таблице ядра сохраняется до тех пор, пока родительский процесс не заберет код возврата.

center

📌 Слайд 7: Переключение контекста процесса (Context Switch)

Переключение контекста (Context Switch) — это процедура остановки одного процесса и загрузки сохраненного состояния другого процесса.

Процедура выполняется планировщиком ядра по прерыванию таймера (квант времени исчерпан) либо при блокировке текущего потока:

  1. Сохранение регистров общего назначения (RAX, RBX, RCX, RDX, RSI, RDI, RBP, R8–R15) в структуру стека ядра текущего процесса.
  2. Сохранение указателя текущей инструкции (RIP), регистра флагов (RFLAGS) и указателя стека (RSP).
  3. Сохранение регистров расширенных инструкций (FPU, SSE, AVX через инструкции fxsave/xsave).
  4. Смена адресного пространства: запись адреса корневой таблицы страниц нового процесса в регистр управления процессора CR3 (mov cr3, new_cr3).
  5. Загрузка сохраненного состояния регистров нового процесса.
  6. Выполнение инструкции возврата (sysretq или iretq) для продолжения исполнения нового процесса в Ring 3.

Накладные расходы переключения контекста:

  • Прямые расходы: выполнение 500–1500 машинных инструкций сохранения и восстановления регистров.
  • Косвенные расходы (Cache Pollution):
    • При перезаписи регистра CR3 процессор аппаратно инвалидирует буфер ассоциативной трансляции адресов (TLB, Translation Lookaside Buffer). Первые тысячи обращений к памяти нового процесса будут вызывать долгие многоуровневые промахи TLB. Для минимизации этого эффекта в современных процессорах применяется технология PCID (Process-Context Identifiers).
    • Холодный кэш процессора: кэш-память L1/L2/L3 заполнена данными предыдущего процесса, что приводит к задержкам выборки строк кэша из оперативной памяти.

3. Системные интерфейсы создания процессов: Linux против Windows

📌 Слайд 8: Модель создания процессов в Linux: fork() и execve()

В операционной системе Linux создание процессов основано на двух ортогональных операциях: размножении (fork) и замещении исполняемого образа (execve).

1. Системный вызов fork()

Вызов fork() создает почти точную копию вызывающего родительского процесса:

  • Дочерний процесс получает собственный уникальный PID.
  • Дочерний процесс наследует копию всех файловых дескрипторов родителя (с общими файловыми позициями смещения offset).
  • fork() вызывается один раз, но возвращает управление дважды:
    • В родительском процессе вызов возвращает PID созданного потомка (положительное целое число).
    • В дочернем процессе вызов возвращает 0.
    • При ошибке нехватки ресурсов возвращается -1.

2. Механизм Copy-On-Write (Копирование при записи, COW)

Если бы fork() физически копировал все гигабайты памяти процесса в новые страницы, вызов был бы крайне медленным. Вместо этого ядро Linux дублирует только структуры таблиц страниц. Страницы памяти помечаются как Read-Only и разделяются обоими процессами:

  • Пока процессы только читают память, физического дублирования не происходит.
  • В момент, когда один из процессов пытается записать данные в страницу, аппаратура MMU генерирует исключение Page Fault (#PF).
  • Обработчик ядра перехватывает #PF, выделяет отдельный физический фрейм памяти, копирует туда 4 КБ данных, настраивает таблицу страниц на новый адрес, включает флаг записи (R/W) и возобновляет работу процесса.

center

📌 Слайд 9: Замещение образа программы execve() и сбор статуса waitpid()

После ветвления дочерний процесс обычно вызывает функцию семейства exec:

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <unistd.h>
 4#include <sys/wait.h>
 5
 6int main(void) {
 7    pid_t pid = fork();
 8
 9    if (pid < 0) {
10        perror("Ошибка fork");
11        return 1;
12    }
13
14    if (pid == 0) {
15        // Дочерний процесс: замещаем свой образ утилитой /bin/ls
16        char *args[] = {"ls", "-l", "/tmp", NULL};
17        char *env[]  = {NULL};
18        
19        execve("/bin/ls", args, env);
20        // Если execve вернул управление, произошла ошибка
21        perror("Ошибка execve");
22        exit(127);
23    } else {
24        // Родительский процесс: ждем завершения потомка
25        int status;
26        waitpid(pid, &status, 0);
27
28        if (WIFEXITED(status)) {
29            printf("Потомок PID %d завершился с кодом %d\n", 
30                   pid, WEXITSTATUS(status));
31        } else if (WIFSIGNALED(status)) {
32            printf("Потомок PID %d убит сигналом %d\n", 
33                   pid, WTERMSIG(status));
34        }
35    }
36    return 0;
37}

Проблема зомби и сирот (Zombies & Orphans):

  • Процесс-зомби (Zombie / Defunct): процесс завершился (exit()), но родитель еще не вызвал waitpid(). Память освобождена, но запись в таблице ядра занимает память и расходует дескриптор PID. При накоплении зомби система исчерпывает пул доступных PID и не может создавать новые процессы.
  • Процесс-сирота (Orphan): родитель завершился раньше своего потомка. Ядро Linux автоматически переподчиняет такого потомка процессу init (PID 1 / systemd). init периодически вызывает wait(), гарантированно утилизируя завершившихся сирот.

📌 Слайд 10: Создание процессов в Windows: CreateProcessW()

В отличие от Unix-модели, в операционной системе Windows создание процесса выполняется за один вызов функции CreateProcessW(). Ядро Windows не имеет нативного механизма fork() (за исключением специальных интерфейсов WSL):

 1#include <windows.h>
 2#include <stdio.h>
 3
 4int main(void) {
 5    STARTUPINFOW si;
 6    PROCESS_INFORMATION pi;
 7
 8    ZeroMemory(&si, sizeof(si));
 9    si.cb = sizeof(si);
10    ZeroMemory(&pi, sizeof(pi));
11
12    WCHAR cmdLine[] = L"notepad.exe C:\\example.txt";
13
14    BOOL success = CreateProcessW(
15        NULL,           // Имя исполняемого модуля (если NULL, берется из cmdLine)
16        cmdLine,        // Командная строка с аргументами
17        NULL,           // Атрибуты безопасности процесса
18        NULL,           // Атрибуты безопасности первичного потока
19        FALSE,          // Флаг наследования дескрипторов
20        0,              // Флаги создания (например, CREATE_SUSPENDED)
21        NULL,           // Блок переменных окружения (NULL = окружение родителя)
22        NULL,           // Текущий рабочий каталог (NULL = каталог родителя)
23        &si,            // Указатель на структуру STARTUPINFO
24        &pi             // Указатель на структуру PROCESS_INFORMATION
25    );
26
27    if (!success) {
28        printf("CreateProcessW завершился с ошибкой: %lu\n", GetLastError());
29        return 1;
30    }
31
32    printf("Создан процесс PID: %lu, Первичный поток TID: %lu\n", 
33           pi.dwProcessId, pi.dwThreadId);
34
35    // Ожидание завершения блокнота (бесконечный таймаут)
36    WaitForSingleObject(pi.hProcess, INFINITE);
37
38    DWORD exitCode;
39    GetExitCodeProcess(pi.hProcess, &exitCode);
40    printf("Процесс завершился с кодом: %lu\n", exitCode);
41
42    // Обязательное закрытие дескрипторов процесса и потока
43    CloseHandle(pi.hThread);
44    CloseHandle(pi.hProcess);
45    return 0;
46}

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


4. Управление группами процессов и заданиями

📌 Слайд 11: Иерархия и группы процессов в Linux

Операционная система организует процессы в иерархические структуры:

  • Идентификатор группы процессов (Process Group ID, PGID): объединяет процессы, решающие общую задачу (например, конвейер команд оболочки cat file | grep pattern | wc -l). Сигнал, отправленный отрицательному значению PID (например, kill(-pgid, SIGINT)), доставляется всем процессам группы одновременно.
  • Сессия (Session ID, SID): коллекция групп процессов, привязанных к одному управляющему терминалу (tty/pty). Лидер сессии создает ее системным вызовом setsid().

Контрольные группы Linux (Control Groups / cgroups v2)

Для системного управления вычислительными ресурсами ядро Linux предоставляет подсистему cgroups:

  • Ограничение оперативной памяти (memory.max): принудительное завершение через OOM-Killer при превышении квоты.
  • Ограничение процессорного времени (cpu.max): задание доли времени CPU в микросекундах.
  • Ограничение дискового ввода-вывода (io.max): лимиты по числу IOPS и байт в секунду. Механизм cgroups в связке с пространствами имен (Namespaces: PID, NET, MNT, IPC, UTS) составляет технологический базис современных систем изоляции и контейнеризации (Docker, Podman, Kubernetes).

📌 Слайд 12: Управление группами через Job Objects в Windows

В Windows эквивалентом групп процессов и квот ресурсов выступают Объекты-задания (Job Objects).

Объект-задание позволяет управлять совокупностью связанных процессов как единым целым:

  1. Создание задания: HANDLE hJob = CreateJobObjectW(NULL, L"MyWorkerPool");
  2. Настройка жестких лимитов через SetInformationJobObject:
    • JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE — если дескриптор задания закрывается, операционная система автоматически и надежно уничтожает все дочерние процессы в задании (защита от процессов-зомби и утечек фоновых демонов).
    • JOB_OBJECT_LIMIT_PROCESS_MEMORY — лимит частной памяти на каждый процесс.
    • JOB_OBJECT_LIMIT_JOB_MEMORY — лимит суммарной памяти всех процессов группы.
    • JOB_OBJECT_LIMIT_ACTIVE_PROCESS — ограничение на максимальное число одновременно запущенных процессов.
  3. Добавление процесса в задание: AssignProcessToJobObject(hJob, pi.hProcess);
  4. Мгновенное прекращение работы всех процессов группы: TerminateJobObject(hJob, exitCode);

5. Многопоточность и модели потоков

📌 Слайд 13: Разграничение ресурсов между процессом и потоком

Поток исполнения (Thread) — это наименьшая базовая единица распределения процессорного времени, которой оперирует планировщик операционной системы.

Процесс выступает в роли контейнера ресурсов, а потоки внутри процесса исполняют код:

РесурсОбщий для всех потоков процессаИндивидуальный для каждого потока
Виртуальное адресное пространство✅❌
Секция кода (.text) и глобальные данные (.data, .bss)✅❌
Куча (Heap)✅❌
Таблица открытых дескрипторов файлов / Handles✅❌
Стек вызовов (User Call Stack)❌✅
Регистры процессора (RIP, RSP, RAX…)❌✅
Локальное хранилище потока (Thread-Local Storage, TLS)❌✅
Приоритет и маска привязки (Affinity Mask)❌✅

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

center

📌 Слайд 14: Модели многопоточности: 1:1, N:1 и M:N

В теории операционных систем выделяют три архитектурные модели связи пользовательских потоков (User Threads) и ядерных контекстов (Kernel Threads):

  1. Модель 1:1 (Ядерные потоки / Kernel-Level Threads):
    • На каждый пользовательский поток создается соответствующий поток ядра.
    • Применяется в Linux (библиотека NPTL на базе вызова clone()) и в Windows Threads.
    • Достоинства: истинный параллелизм на многоядерных процессорах; если один поток блокируется на операции ввода-вывода, остальные потоки продолжают вычисления.
    • Недостатки: создание потока и переключение контекста требуют системного вызова и участия ядра.
  2. Модель N:1 (Пользовательские потоки / Green Threads / Fibers):
    • Множество пользовательских потоков отображаются на один поток ядра. Планирование выполняет библиотека в User Space.
    • Достоинства: ультрабыстрое переключение контекста без входа в ядро (несколько наносекунд); малый расход памяти.
    • Недостатки: невозможно задействовать более одного физического ядра ЦПУ; если один пользовательский поток выполнит блокирующий системный вызов (read), ядро заблокирует весь процесс целиком.
  3. Модель M:N (Гибридная модель):
    • $M$ легковесных задач рантайма отображаются на $N$ системных потоков ядра ($M \gg N$).
    • Реализуется в рантаймах языков Go (Goroutines), Rust (Tokio) и Erlang (BEAM).

center

📌 Слайд 15: Системные API управления потоками: POSIX Threads и WinAPI

1. Интерфейс POSIX Threads (pthreads) в Linux

Стандарт pthread объявляется в <pthread.h>. Для компиляции требуется флаг компилятора -pthread:

 1#include <pthread.h>
 2#include <stdio.h>
 3
 4void* thread_func(void* arg) {
 5    long id = (long)arg;
 6    printf("Фоновый поток %ld исполняется\n", id);
 7    return (void*)(id * 10);
 8}
 9
10int main(void) {
11    pthread_t thread;
12    // Создание потока
13    pthread_create(&thread, NULL, thread_func, (void*)1);
14
15    void* result;
16    // Ожидание завершения потока (join)
17    pthread_join(thread, &result);
18    printf("Поток завершился с результатом: %ld\n", (long)result);
19    return 0;
20}

Системный базис в Linux: Под капотом библиотеки glibc вызов pthread_create() обращается к универсальному системному вызову clone() с флагами CLONE_VM (разделение памяти), CLONE_FS (файловая система), CLONE_FILES (таблица дескрипторов), CLONE_SIGHAND (обработчики сигналов) и CLONE_THREAD (включение в общую группу потоков TGID). В Linux концептуально потоки и процессы описываются одинаковыми структурами task_struct, отличаясь лишь степенью разделения ресурсов.

2. Управление потоками в Windows API

Создание системного потока осуществляется функцией CreateThread():

  • HANDLE CreateThread(..., LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ...);
  • Поток завершается выходом из функции потока либо вызовом ExitThread().
  • Родительский поток ожидает завершения через WaitForSingleObject(hThread, INFINITE).
  • Внимание: в программах на C/C++, использующих стандартную библиотеку времени исполнения CRT, вместо CreateThread следует вызывать функцию _beginthreadex() (из <process.h>), чтобы корректно инициализировать потоко-локальные структуры CRT (буферы errno, строковые манипуляторы).

6. Асинхронное выполнение системных вызовов и удаленный запуск

📌 Слайд 16: Синхронный против асинхронного ввода-вывода

При синхронном вызове (read, write, ReadFile) поток переводится ядром в состояние ожидания (Blocked) до момента фактического физического чтения данных с устройства. Если программа обслуживает десятки тысяч сетевых соединений, поддержание десятков тысяч синхронных потоков истощает память (каждый стек требует от 1 до 8 МБ) и перегружает планировщик ядра.

Асинхронный ввод-вывод (Asynchronous I/O) разделяет инициализацию операции и получение результата:

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

📌 Слайд 17: Высокопроизводительные механизмы: IOCP и io_uring

1. Windows: Порты завершения ввода-вывода (I/O Completion Ports, IOCP)

Считается наиболее масштабируемой моделью асинхронного ввода-вывода в Windows:

  • Создается порт завершения: CreateIoCompletionPort().
  • Файловые дескрипторы или сокеты, открытые с флагом FILE_FLAG_OVERLAPPED, привязываются к порту.
  • Асинхронное чтение инициируется через ReadFile(..., &overlapped).
  • Пул рабочих потоков вызывает функцию GetQueuedCompletionStatus(), засыпая на очереди ядра. Ядро пробуждает ровно столько потоков, сколько ядер процессора доступно, минимизируя лишние переключения контекста.

2. Linux: Интерфейс очередей io_uring

Эволюция асинхронных средств Linux шла через мультиплексирование select -> poll -> epoll. Начиная с ядра Linux 5.1, был внедрен революционный интерфейс io_uring:

  • Взаимодействие прикладного процесса и ядра организуется через две кольцевые очереди (Ring Buffers) в разделяемой памяти:
    1. Submission Queue (SQ): очередь запросов на ввод-вывод, куда приложение без единого системного вызова записывает команды.
    2. Completion Queue (CQ): очередь завершенных операций, откуда приложение считывает результаты.
  • Поддерживается режим опроса без прерываний (Kernel Polling, IORING_SETUP_SQPOLL), устраняющий накладные расходы на системные вызовы.

📌 Слайд 18: Концепция удаленного запуска приложений

Системный удаленный запуск программ предполагает инициализацию процесса на удаленном узле вычислительной сети с передачей аргументов, окружения и контролем потоков ввода-вывода (stdin/stdout/stderr):

  1. Удаленный вызов процедур (Remote Procedure Call, RPC):
    • Клиентская программа вызывает процедуру локально; клиентский стаб (stub) сериализует аргументы в сетевое сообщение (Marshalling) и отправляет по сети на сервер.
    • Серверный скелетон (skeleton) десериализует параметры, вызывает локальную функцию и возвращает результат.
    • Примеры: MS-RPC (базис многих служб Windows), gRPC, ONC RPC.
  2. Сетевые терминальные протоколы:
    • SSH (Secure Shell): стандарт удаленного исполнения в Linux. Демон sshd порождает псевдотерминал (pty), вызывает fork() и execve() целевой команды, мультиплексируя шифрованные потоки данных.
    • WinRM (Windows Remote Management): протокол на базе стандарта WS-Management, применяемый для удаленного администрирования серверов и выполнения скриптов PowerShell.

📌 Слайд 19: Резюме лекции

Основные итоги:

  1. Процесс — это базовый контейнер системных ресурсов и изолированное виртуальное адресное пространство.
  2. Адресное пространство процесса логически разбито на сегменты с различными атрибутами защиты: .text (код), .data/.bss (статические данные), куча (Heap) и стек (Stack).
  3. Граф состояний процесса включает состояния Создания, Готовности, Исполнения, Ожидания и Завершения. Смена процесса на ядре ЦПУ сопровождается дорогостоящим переключением контекста и сбросом кэша TLB.
  4. В Linux процессы создаются разделением на fork() с оптимизацией Copy-On-Write и замещением через execve().
  5. В Windows создание процесса атомарно осуществляется функцией CreateProcessW().
  6. Для группового управления ресурсами применяются cgroups в Linux и Job Objects в Windows.
  7. Потоки внутри процесса разделяют общую память и дескрипторы, но имеют собственные регистры, стеки и TLS. Современные ОС используют модель потоков 1:1.
  8. Масштабируемый ввод-вывод реализуется асинхронными архитектурами без блокировки потоков (IOCP в Windows и io_uring в Linux).

📌 Слайд 20: Вопросы для самопроверки

  1. Чем отличается виртуализация процессора от виртуализации оперативной памяти?
  2. В какую сторону по адресам растут стек вызовов и куча в классической модели процесса?
  3. Что происходит со страницами физической памяти при вызове fork() благодаря механизму Copy-On-Write?
  4. В чем разница между процессом-зомби и процессом-сиротой? Как ядро предотвращает утечку ресурсов?
  5. Какие структуры данных возвращает функция CreateProcessW() в Windows и почему оба полученных дескриптора нужно закрывать?
  6. С какой целью системный инженер объединяет процессы в Job Object в среде Windows?
  7. Почему переключение между потоками одного процесса выполняется быстрее, чем между потоками разных процессов?
  8. Каковы достоинства и недостатки модели многопоточности 1:1 по сравнению с моделью N:1?
  9. За счет чего механизм io_uring в Linux обеспечивает сверхвысокую производительность ввода-вывода?

📌 Слайд 21: Литература и рекомендуемые ресурсы

Литература по курсу:

  • Современные операционные системы (4-е изд.) — Таненбаум Э., Бос Х. СПб.: Питер, 2021. 1119 с.
  • Устройство и функционирование OC Windows. Практикум к курсу «Операционные системы» — Коньков К. А. М.: Бином, 2008. 208 с.
  • Операционные системы (2-е изд.) — Гордеев А. В. СПб.: Питер, 2009. 415 с.
  • Системное программирование: методические указания по выполнению лабораторных работ — Бизюк А. Н., Соколова А. С. Витебск: УО «ВГТУ», 2024.
  • Справочник Windows API: Процессы и потоки — https://learn.microsoft.com/en-us/windows/win32/procthread/
  • Руководство Linux Man-Pages: fork(2), execve(2), clone(2) — https://man7.org/linux/man-pages/
← 01. Прикладной программный интерфейс 03. Объекты ядра и их использование в … →