Перейти к содержанию
Educora
Университет25 мин54 / 59

Операционные системы

Изучи процессы и потоки, планирование процессора, виртуальную память и страничную организацию, файловые системы и взаимные блокировки на расчётных примерах.

Проверь себя
В этом уроке ты узнаешь
  • Различать процессы и потоки и объяснять состояние гонки
  • Вычислять среднее время ожидания для FCFS, SJF и Round Robin
  • Переводить виртуальный адрес в физический и подсчитывать страничные прерывания
  • Применять четыре условия взаимной блокировки и алгоритм банкира

На ноутбуке одновременно работают браузер, музыка, мессенджер и антивирус, хотя ядер у процессора всего несколько, а память ограничена. Каждая программа «думает», что весь компьютер принадлежит ей, — эту иллюзию создаёт операционная система (ОС). ОС делит процессорное время, выдаёт каждой программе собственную память, организует файлы на диске и не даёт программам заблокировать друг друга.

Процессы, потоки и планирование

Определение
Процесс и поток

Процесс — выполняющаяся программа: у него собственное виртуальное адресное пространство (код, данные, куча), открытые файлы и хотя бы один поток. Поток (thread) — последовательность выполнения внутри процесса: у него свой стек, регистры и PC, но память процесса он делит с другими потоками. Потоки дешевле создавать и переключать, чем процессы, но общая память требует синхронизации.

Пример 1: состояние гонки

Два потока увеличивают общую переменную counter = 5 на 1. Каждый делает три шага: чтение (tmp = counter), прибавление (tmp + 1), запись (counter = tmp). Каким будет результат при таком чередовании: A читает, B читает, A прибавляет, A пишет, B прибавляет, B пишет?

Показать решение
A читает: tmpA = 5. B читает: tmpB = 5.
A: tmpA = 6, counter = 6. B: tmpB = 6, counter = 6.
После двух увеличений получилось 6 вместо 7 — одно обновление «потерялось». Результат зависит от чередования: это состояние гонки. Решение: защитить критическую секцию мьютексом (блокировкой), чтобы «чтение–прибавление–запись» стало неделимым.

Готовые процессы ждут в очереди, а планировщик решает, кому достанется процессор. FCFS — в порядке поступления; SJF — сначала самая короткая задача (минимизирует среднее время ожидания, но требует заранее знать длину задач); Round Robin — каждый процесс получает квант времени q, а затем уходит в конец очереди. Каждое переключение требует смены контекста: сохранения и восстановления регистров.

W = (1 / n) · ∑ (Cᵢ − Aᵢ − Bᵢ)W = (1 / n) · ∑ (Cᵢ − Aᵢ − Bᵢ)
где:
  • Wсреднее время ожидания
  • Cᵢмомент завершения процесса i
  • Aᵢмомент поступления
  • Bᵢдлительность работы на процессоре (burst)
Пример 2: три алгоритма планирования

Процессы P1 = 10, P2 = 4, P3 = 2 мс поступили в момент 0 в этом порядке. Найди среднее время ожидания для FCFS, SJF и Round Robin (q = 3).

Показать решение
FCFS: P1 0–10, P2 10–14, P3 14–16 → ожидание 0, 10, 14 → W = 24/3 = 8 мс.
SJF: P3 0–2, P2 2–6, P1 6–16 → ожидание 0, 2, 6 → W = 8/3 ≈ 2,67 мс.
RR, q = 3: P1 0–3, P2 3–6, P3 6–8 (готов), P1 8–11, P2 11–12 (готов), P1 12–16 (готов).
Ожидание = завершение − работа: P1 16 − 10 = 6, P2 12 − 4 = 8, P3 8 − 2 = 6 → W = 20/3 ≈ 6,67 мс.
RR проигрывает SJF по ожиданию, зато каждый процесс начинает обслуживаться в течение 6 мс — а для интерактивных систем важно именно это.
Python
from collections import deque

def fcfs(bursts):
    t, waits = 0, []
    for b in bursts:
        waits.append(t)
        t += b
    return waits

def round_robin(bursts, q):
    rem, t, done = list(bursts), 0, [0] * len(bursts)
    queue = deque(range(len(bursts)))
    while queue:
        i = queue.popleft()
        run = min(q, rem[i])
        t += run
        rem[i] -= run
        if rem[i]:
            queue.append(i)
        else:
            done[i] = t
    return [done[i] - bursts[i] for i in range(len(bursts))]

jobs = [10, 4, 2]
for name, w in [('FCFS', fcfs(jobs)), ('SJF', fcfs(sorted(jobs))), ('RR q=3', round_robin(jobs, 3))]:
    print(name, w, 'average wait:', round(sum(w) / len(w), 2))
▸ Ожидаемый результат
FCFS [0, 10, 14] average wait: 8.0
SJF [0, 2, 6] average wait: 2.67
RR q=3 [6, 8, 6] average wait: 6.67
Симуляция подтверждает ручной расчёт (в строке SJF ожидания указаны в отсортированном порядке задач — P3, P2, P1). Поменяй q: при очень большом q RR превращается в FCFS, при очень малом время съедают переключения контекста.

Виртуальная память и страничная организация

Каждый процесс видит собственное виртуальное адресное пространство, начинающееся с 0. Память делится на одинаковые страницы (например, 4 КБ), физическая RAM — на такие же кадры; таблица страниц каждого процесса хранит соответствие «страница → кадр». Перевод выполняет аппаратура (MMU), а недавние переводы держит в кэше TLB. Если страницы нет в RAM, происходит страничное прерывание (page fault), и ОС загружает её с диска — так программы могут использовать больше памяти, чем есть RAM, и не видят память друг друга.

p = ⌊VA / P⌋, d = VA mod P, PA = frame(p) · P + dp = ⌊VA / P⌋, d = VA mod P, PA = frame(p) · P + d
где:
  • VA, PAвиртуальный и физический адрес
  • Pразмер страницы, байт
  • p, dномер страницы и смещение внутри неё

При P = 2ᵏ деление — это просто разделение битов: для страниц 4 КБ = 2¹² младшие 12 бит — смещение, остальные — номер страницы.

Пример 3: перевод адреса и цена страничного прерывания

а) Страница 4 КБ, VA = 0x3A7C, в таблице страниц 3 → 7. Каков физический адрес?
б) Доступ к RAM — 100 нс, обработка страничного прерывания — 8 мс, вероятность прерывания p = 0,001. Каково эффективное время доступа?

Показать решение
а) 0x3A7C = 14 972. p = ⌊14 972 / 4096⌋ = 3, d = 14 972 − 12 288 = 2684 (= 0xA7C).
PA = 7 · 4096 + 2684 = 28 672 + 2684 = 31 356 = 0x7A7C. В шестнадцатеричной записи изменилась только цифра страницы: 0x3A7C → 0x7A7C.
б) EAT = (1 − p) · 100 + p · 8 000 000 нс = 99,9 + 8000 ≈ 8100 нс ≈ 8,1 мкс — одно прерывание на тысячу обращений замедляет память в 81 раз!

Когда RAM заполнена, ОС должна выбрать, какую страницу вытеснить на диск. FIFO вытесняет страницу, загруженную раньше всех, LRU — ту, что дольше всех не использовалась. Теоретический идеал OPT вытесняет страницу, которая понадобится позже всех, но будущее неизвестно, поэтому он служит лишь эталоном.

Python
def faults(refs, frames, policy):
    mem, count = [], 0
    for p in refs:
        if p in mem:
            if policy == 'LRU':
                mem.remove(p)
                mem.append(p)
            continue
        count += 1
        if len(mem) == frames:
            mem.pop(0)
        mem.append(p)
    return count

refs = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5]
for frames in [3, 4]:
    print(frames, 'frames: FIFO', faults(refs, frames, 'FIFO'), '| LRU', faults(refs, frames, 'LRU'))

page_size, va = 4096, 0x3A7C
page, offset = divmod(va, page_size)
page_table = {3: 7}
pa = page_table[page] * page_size + offset
print('page', page, 'offset', offset, '-> physical', pa, hex(pa))
▸ Ожидаемый результат
3 frames: FIFO 9 | LRU 10
4 frames: FIFO 10 | LRU 8
page 3 offset 2684 -> physical 31356 0x7a7c
Сюрприз: при FIFO увеличение числа кадров с 3 до 4 увеличило число прерываний с 9 до 10 — это аномалия Белади. LRU ей не подвержен: 10 → 8. Последняя строка подтверждает пункт (а) примера 3.

Файловые системы

Диск делится на блоки фиксированного размера (обычно 4 КБ). В файловых системах Unix (ext4) индексный дескриптор (inode) каждого файла хранит размер, владельца, права, временные метки и указатели на блоки данных; каталог — это просто таблица «имя → номер inode». В классическом inode ext2/ext3 12 прямых указателей и одно-, двух- и трёхуровневые косвенные: при блоках 4 КБ и 4-байтовых указателях прямые блоки покрывают 48 КБ, одноуровневый косвенный — ещё 1024 · 4 КБ = 4 МБ, двухуровневый — 4 ГБ, трёхуровневый — 4 ТБ. Журналирование (ext4, NTFS) сначала записывает изменения в журнал, чтобы файловая система пережила отключение питания. В FAT32 один файл должен быть меньше 4 ГБ.

Взаимная блокировка (deadlock)

Поток A держит блокировку 1 и ждёт блокировку 2, а поток B держит 2 и ждёт 1 — никто не может продвинуться. Взаимная блокировка возможна, когда одновременно выполняются четыре условия Коффмана: взаимное исключение (ресурсом пользуется один), удержание и ожидание, отсутствие вытеснения (ресурс не отбирают) и циклическое ожидание. Достаточно нарушить любое; самый практичный способ — всегда захватывать блокировки в одном глобальном порядке, что исключает циклическое ожидание.

Пример 4: алгоритм банкира

В системе 10 одинаковых ресурсов. P0: максимум 7, держит 3; P1: максимум 3, держит 1; P2: максимум 6, держит 3. Безопасно ли состояние? Если P0 запросит ещё 2, можно ли их выдать?

Показать решение
Свободно: 10 − (3 + 1 + 3) = 3. Потребности: P0 4, P1 2, P2 3.
P1: 2 ≤ 3 → завершается и возвращает → свободно 4. P2: 3 ≤ 4 → свободно 7. P0: 4 ≤ 7 → свободно 10.
Есть безопасная последовательность P1, P2, P0 → состояние безопасно.
Если выдать P0 ещё 2: P0 держит 5, нужно 2, свободно 1. Ни у кого потребность не ≤ 1 (P0 2, P1 2, P2 3) → небезопасное состояние, запрос откладывается. Небезопасное — ещё не взаимная блокировка, но ОС не рискует.

Главное

  • У процесса собственное адресное пространство; его потоки делят память, поэтому общие данные нужно защищать блокировками.
  • SJF минимизирует среднее ожидание, Round Robin сокращает время отклика; ожидание = завершение − поступление − работа.
  • Виртуальный адрес = страница + смещение; PA = frame · P + d; страничные прерывания резко увеличивают эффективное время доступа.
  • LRU обычно лучше FIFO; у FIFO возможна аномалия Белади.
  • Для взаимной блокировки нужны все четыре условия; единый порядок захвата и алгоритм банкира её предотвращают.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Два процесса поступают в момент 0: P1 = 6 мс, P2 = 3 мс (в этом порядке). Round Robin, q = 4. Каково среднее время ожидания?