Логотип коледжу
Оптико-механічний фаховий коледж

Системні виклики, переривання та взаємодія з обладнанням

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

Цілі лекції

Після опрацювання матеріалу ви зможете:

  • пояснювати призначення системного виклику;
  • розрізняти API, POSIX, WinAPI та ABI;
  • простежувати шлях запиту від функції програми до ядра і драйвера;
  • відрізняти зміну режиму процесора від перемикання контексту;
  • класифікувати апаратні переривання, програмно ініційовані переходи та процесорні винятки;
  • спостерігати системні виклики безпечними засобами Linux.

Передумови

Потрібно розуміти різницю між User Mode і Kernel Mode, роль ядра та драйверів, уміти виконувати звичайні команди Linux і компілювати невелику C-програму командою cc або gcc. Усі приклади виконуються без sudo.

1. Навіщо потрібна контрольована межа

Процесор обмежує код у User Mode: привілейовані інструкції та захищені області пам'яті йому недоступні. Це не заважає програмі працювати з файлами або мережею. Програма формує запит, а ядро:

  1. перевіряє аргументи та права;
  2. знаходить потрібний об'єкт ядра;
  3. виконує операцію саме від імені процесу;
  4. повертає результат або код помилки.

Системний виклик (system call) - визначена точка входу до ядра для запиту захищеної послуги. Прикладами в Linux є read, write, openat, mmap, clone та exit_group.

Системний виклик не означає, що застосунок отримав привілеї ядра. Код ядра виконує лише дозволену операцію після перевірок, а потім повертає процесор до User Mode.

2. API, POSIX, WinAPI та ABI: різні рівні договору

У розмові ці поняття іноді змішують, але вони відповідають на різні запитання.

РівеньЗапитанняПриклад
APIЯкі функції, типи й правила бачить вихідний код?write(fd, buffer, count)
POSIXЯкі переносні інтерфейси та поведінку мають надавати сумісні системи?fork, exec, read, waitpid
WinAPIЯкий прикладний API надає Windows?CreateFileW, ReadFile, CreateProcessW
ABIЯк уже скомпільований код взаємодіє на двійковому рівні?регістри аргументів, calling convention, розміри типів
System call interfaceЯк саме здійснюється вхід до конкретного ядра?номер виклику та інструкція syscall на Linux x86-64

POSIX стандартизує багато інтерфейсів вихідного коду та їхню поведінку, але не вимагає однакової внутрішньої реалізації ядра. Linux, FreeBSD і macOS можуть надавати POSIX-подібну функцію write, хоча їхні таблиці системних викликів і внутрішні структури відрізняються.

WinAPI є основним прикладним інтерфейсом Windows. Виклик CreateProcessW не є просто іншою назвою POSIX fork: Windows створює процес та його первинний потік за іншою моделлю.

ABI (Application Binary Interface) потрібний уже скомпільованим компонентам. Він охоплює викличні угоди, використання регістрів, вирівнювання і подання типів, формат об'єктних файлів та інші двійкові правила. API може виглядати однаково у вихідному коді, але програма потребуватиме перекомпіляції для іншої архітектури або ABI.

Шлях запиту від API до пристрою та повернення результату

Типовий шлях write

Для виклику write(STDOUT_FILENO, text, size) шлях спрощено виглядає так:

  1. C-програма викликає функцію-обгортку write у стандартній бібліотеці;
  2. обгортка готує номер системного виклику та аргументи відповідно до ABI;
  3. спеціальна інструкція переводить процесор до визначеної точки входу ядра;
  4. ядро перевіряє файловий дескриптор і буфер;
  5. підсистема введення-виведення спрямовує дані відповідному об'єкту, а за потреби - драйверу;
  6. ядро повертає кількість записаних байтів або помилку;
  7. бібліотечна обгортка повертає значення програмі та за помилки встановлює errno.

Не кожна бібліотечна функція робить системний виклик. Наприклад, strlen лише рахує символи в пам'яті процесу. І навпаки, одна функція API може виконати кілька системних викликів.

3. Безпечне спостереження системного виклику

Створіть файл syscall_demo.c:

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(void) {
    const char message[] = "Запит із User Mode\n";
    ssize_t written = write(STDOUT_FILENO, message, sizeof(message) - 1);

    if (written == -1) {
        fprintf(stderr, "write: %s\n", strerror(errno));
        return 1;
    }

    fprintf(stderr, "Записано байтів: %zd\n", written);
    return 0;
}

Скомпілюйте й запустіть:

cc -std=c11 -Wall -Wextra -pedantic syscall_demo.c -o syscall_demo
./syscall_demo

Очікувані повідомлення мають такий зміст:

Запит із User Mode
Записано байтів: 26

Кількість 26 стосується байтів UTF-8, а не кількості видимих літер. Програма не припускає, що один символ дорівнює одному байту.

Якщо встановлено strace, простежте лише виклики write:

strace -e trace=write ./syscall_demo

У трасуванні можна побачити два write: один для стандартного виведення з дескриптором 1, другий для діагностичного потоку з дескриптором 2. Наприкінці рядка write показано повернене ядром число. Точне розбиття бібліотечного виведення на виклики може залежати від реалізації та буферизації, тому аналізуйте фактичне трасування, а не вгадуйте його кількість.

strace спостерігає межу процесу та ядра. Він не показує звичайні виклики функцій, які не входять до ядра.

4. Вхід у ядро не дорівнює перемиканню контексту

Під час системного виклику процесор переходить із User Mode у Kernel Mode. Ядро при цьому часто продовжує виконувати роботу від імені того самого процесу. Це зміна режиму, але ще не обов'язково перемикання на інший процес.

Контекст виконання охоплює стан, потрібний для продовження роботи: лічильник команд, регістри, покажчик стека, параметри адресного простору та інші дані. Context switch відбувається, коли ОС зберігає стан одного потоку виконання і відновлює стан іншого.

ПодіяПерехід User/KernelІнший потік або процес обов'язково?
Швидкий getpid()такні
read() із уже доступними данимитакні
read() очікує данихтакможливий: поточний потік блокується
Переривання таймеравхід до ядрапланувальник може залишити той самий потік або обрати інший

Отже, твердження «кожний system call викликає context switch» неправильне. Перемикання контексту може статися, якщо потік блокується, вичерпав квант або планувальник має іншу причину обрати наступний потік.

5. Переривання та винятки

Усі ці події можуть передавати керування спеціальному обробнику, але їхні джерела і зв'язок із поточними інструкціями різні.

Апаратні переривання

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

Спрощена обробка:

  1. пристрій або контролер сигналізує про подію;
  2. контролер переривань доставляє сигнал процесору;
  3. процесор зберігає мінімальний стан і переходить до зареєстрованого обробника;
  4. ядро підтверджує подію та виконує термінову частину роботи;
  5. відкладена частина може опрацювати більше даних пізніше;
  6. виконання повертається до перерваного коду або планувальник обирає інший потік.

ОС намагається не виконувати довгу роботу в найтерміновішій частині обробника, адже інші події теж мають бути обслужені.

Процесорні винятки

Exception виникає синхронно через виконання конкретної інструкції: ділення на нуль, недопустиму інструкцію, порушення доступу або відсутню сторінку пам'яті.

Виняток не завжди означає фатальну помилку. Наприклад, page fault може бути нормальною частиною demand paging: ядро підвантажує сторінку, після чого інструкція повторюється. Якщо доступ справді недозволений, ОС зазвичай повідомляє процесу про помилку, у Linux часто через сигнал SIGSEGV.

У документації архітектури винятки можуть уточнювати як fault, trap або abort залежно від можливості продовження та адреси збереженої інструкції. На цьому етапі важливо спочатку встановити джерело: воно пов'язане з поточною інструкцією.

Програмно ініційований перехід

Програма може навмисно виконати спеціальну інструкцію входу до ядра. На сучасному Linux x86-64 для системних викликів типовою є інструкція syscall; історично на x86 застосовували int 0x80. Через це в навчальній літературі трапляється термін software interrupt.

Не слід ототожнювати:

  • програмно ініційований перехід процесора до ядра;
  • Unix-сигнал на кшталт SIGTERM;
  • апаратне переривання від пристрою.

Це різні механізми, навіть якщо зрештою вони змінюють потік виконання.

6. Як переривання допомагає введенню-виведенню

Без переривань процесор міг би постійно опитувати пристрій: «операцію вже завершено?». Це називають polling. Переривання дозволяє контролеру повідомити про готовність, а процесору тим часом виконувати іншу роботу.

Приклад читання з термінала:

  1. процес викликає read;
  2. якщо даних немає, ядро блокує потік;
  3. планувальник запускає інший готовий потік;
  4. натискання клавіші породжує апаратну подію;
  5. драйвер отримує дані й будить потік, який очікував;
  6. коли планувальник знову обере цей потік, read завершиться.

Тут є і system call, і interrupt, і щонайменше одне планувальне рішення. Це не одна подія, а узгоджений ланцюжок механізмів.

7. Безпечні спостереження в Linux

Визначте архітектуру, бо деталі ABI залежать від неї:

uname -m

Простежте кілька типів викликів без зміни системи:

strace -e trace=read,write,openat,close /bin/echo "Перевірка"

За наявності дозволу можна лише прочитати лічильники переривань:

head -n 12 /proc/interrupts

Вміст залежить від ядра, апаратури та віртуальної машини. Порожній або обмежений результат у контейнері не спростовує механізм: середовище може приховувати дані хоста.

Практичні завдання

Завдання 1. Відтворіть спостереження

Скомпілюйте syscall_demo.c, запустіть його звичайно й через strace -e trace=write. Знайдіть у трасуванні файлові дескриптори 1 і 2, аргументи та повернені значення.

Завдання 2. Змініть напрям виведення

Змініть перший виклик на write(STDERR_FILENO, ...). Перед запуском передбачте, який номер дескриптора з'явиться в strace. Перевірте відповідь експериментом.

Завдання 3. Дослідіть помилку без небезпечних дій

Створіть файл missing_file_demo.c, який викликає open("definitely-missing-os03.txt", O_RDONLY), перевіряє -1, друкує strerror(errno) і коректно завершується. Простежте лише openat і close, але відфільтруйте вивід до рядка з потрібним іменем, щоб не розглядати виклики динамічного завантажувача та локалей:

strace -e trace=openat,close ./missing_file_demo 2>&1 | grep definitely-missing-os03.txt

Очікуваний рядок матиме вигляд:

openat(AT_FDCWD, "definitely-missing-os03.txt", O_RDONLY) = -1 ENOENT (No such file or directory)

Поясніть у трьох реченнях: який API викликано, який системний виклик видно, як помилка ядра стала повідомленням програми. Не створюйте файл із такою назвою: відсутність є безпечною умовою досліду.

Підсумок

  • System call є контрольованим запитом із User Mode до ядра.
  • API описує інтерфейс вихідного коду, ABI - двійкову взаємодію; POSIX і WinAPI не є назвами ядер.
  • Бібліотечна функція та системний виклик не завжди відповідають один одному як один до одного.
  • Перехід у Kernel Mode не обов'язково означає context switch.
  • Апаратне переривання зазвичай асинхронне й надходить від обладнання.
  • Exception синхронно пов'язаний із конкретною інструкцією; деякі винятки ядро може коректно обробити.
  • Введення-виведення поєднує системні виклики, драйвери, переривання, блокування та планування.

Завдання

1. Навіщо програмі в User Mode потрібен системний виклик?

2. Що найточніше описує POSIX у контексті системного програмування?

3. Що визначає ABI на межі двійкового коду?

4. Чи кожний перехід із User Mode у Kernel Mode обов'язково перемикає виконання на інший процес? Введіть `так` або `ні`.

5. Яка подія є апаратним перериванням?

6. Чому процесорний виняток називають синхронним щодо потоку інструкцій?