Основы Computer Science Что делает операционная система: процессы, память, файлы — обзор
0%

Что делает операционная система: процессы, память, файлы — обзор

Что делает операционная система: процессы, память, файлы — обзор

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

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

Зачем вообще нужна ОС: две роли

Представьте, что ОС нет. Ваша программа — единственная на машине, и она сама делает всё: сама раскладывает себя по адресам RAM, сама шлёт байты в контроллер диска по его собственному протоколу, сама знает физическое расположение дорожек. Ровно так работали первые компьютеры и работают простейшие микроконтроллеры. Проблемы начинаются, как только программ становится больше одной, а железа — много и разного. Отсюда две фундаментальные роли ОС.

Роль первая — абстракция. Железо уродливо, разнообразно и низкоуровнево. SSD одного производителя, HDD другого и флешка третьего общаются командами, которые не совпадают ни в чём. ОС прячет это за небольшим набором чистых понятий: вместо «сектор на дорожке такой-то» — файл, вместо «физический адрес в микросхеме RAM» — виртуальная память процесса, вместо «поставь процессор на эти команды и следи за таймером» — процесс. Программист работает с удобными абстракциями, а перевод их в язык конкретного железа берёт на себя ОС и её драйверы.

Роль вторая — арбитр. Ресурсов мало, а желающих много. Одно ядро процессора, но двести запущенных программ; восемь гигабайт RAM, но каждая программа хотела бы всю; один диск, одна сетевая карта. ОС распределяет эти ресурсы между всеми — по времени (планирование процессора), по пространству (раздача памяти), по очереди (доступ к устройствам) — и следит, чтобы одна программа не залезла в чужую память и не подвесила всю машину. Это одновременно и справедливость, и защита.

Всё остальное в статье — это конкретизация этих двух ролей на трёх главных ресурсах: процессор (превращается в абстракцию «процесс»), память (в «виртуальную память») и устройства (в «файлы»).

Ядро, режимы процессора и системные вызовы

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

Решение встроено в сам процессор — два режима работы (в x86 их называют «кольцами», ring 0 и ring 3). В режиме ядра (kernel mode) доступны все команды, включая опасные: управление памятью, таймерами, устройствами. В пользовательском режиме (user mode) привилегированные команды запрещены — попытка их выполнить вызывает аппаратное исключение. Ядро ОС работает в привилегированном режиме, все ваши программы — в пользовательском. Это тот самый механизм, который делает изоляцию не пожеланием, а физическим фактом: браузер просто не может выполнить команду, читающую память редактора, — процессор ему не даст.

Но программе всё равно нужно читать файлы и рисовать на экране, а это требует железа. Как? Через единственную легальную дверь — системный вызов (syscall). Программа кладёт номер нужной услуги и аргументы в регистры и выполняет специальную команду (syscall на x86-64, svc на ARM), которая аппаратно переключает процессор в режим ядра и прыгает в заранее заданную точку — обработчик ОС. Ядро проверяет права, выполняет запрошенное своими привилегированными руками и возвращает управление обратно в пользовательский режим. Это тот же механизм прерывания, что мы видели у процессора (Как работает процессор): управление насильно уходит из вашего кода в код ОС.

Когда вы пишете print("hi") на Python или System.out.println на Java, в самом низу это оборачивается в системный вызов write к ОС. Библиотеки и рантаймы языков — удобная обёртка, но пересечь границу с железом можно только через syscall. Их удивительно мало: в Linux около трёхсот, и на паре десятков (read, write, open, close, mmap, fork, execve, wait) держится почти всё. Вся мощь ОС раздаётся через этот узкий, строго охраняемый интерфейс.

Процессы: абстракция запущенной программы

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

Чтобы процесс был изолированной единицей, ОС даёт каждому собственное виртуальное адресное пространство — иллюзию, будто вся память машины принадлежит только ему, пронумерованная с нуля. Внутри этого пространства память разложена по назначению.

Адресное пространство процесса: стек, куча, данные, код

  • Text/Code — машинный код программы, только для чтения (чтобы код случайно не затёр сам себя).
  • Data / BSS — глобальные переменные: в Data — с начальными значениями, в BSS — зануляемые (ОС хранит лишь их размер, экономя место в файле).
  • Куча (heap) — растёт вверх; сюда идут malloc, new, все объекты, чьё время жизни не привязано к функции.
  • Стек (stack) — растёт вниз навстречу куче; на нём живут кадры вызовов функций, локальные переменные, адреса возврата. Слишком глубокая рекурсия упирается стеком в кучу — это и есть переполнение стека.

Всё, что ОС хранит про один процесс — состояние регистров, карту памяти, таблицу открытых файлов, идентификатор (PID), права, — лежит в структуре ядра, которую традиционно называют PCB (process control block). Именно её ОС сохраняет и восстанавливает, переключаясь между процессами.

Жизненный цикл процесса

Процесс не всё время считает. Чаще всего он ждёт — ввода, диска, сети, своей очереди на процессор. ОС отслеживает его состояние.

Ключевой переход — Running → Waiting. Как только процесс запросил чтение с диска, он блокируется: ждать данные, держа при этом процессор, — расточительство, ведь диск отвечает миллионы тактов. ОС снимает процесс с процессора и отдаёт CPU другому, готовому к работе. Когда диск ответит (аппаратным прерыванием), ОС переведёт процесс из Waiting в Ready, и он дождётся своей очереди снова. Эта пляска состояний — то, как одно ядро обслуживает сотни процессов, почти не простаивая.

Планировщик и иллюзия параллелизма

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

Сам акт «снять один процесс, поставить другой» — это переключение контекста (context switch): ОС сохраняет все регистры текущего процесса в его PCB, загружает регистры следующего и переключает карту памяти. Оно не бесплатно (сотни-тысячи тактов плюс остывшие кэши), поэтому слишком частые переключения сами по себе съедают производительность.

Кого запустить следующим — решает планировщик по своей стратегии: честно раздать время всем, отдать приоритет интерактивным задачам (чтобы курсор не тормозил), уложиться в реальные сроки. Это отдельная богатая тема с собственными алгоритмами — за ней в трек Операционные системы.

Потоки vs процессы. Внутри одного процесса может быть несколько потоков (threads) — независимых нитей исполнения, которые делят одну память процесса, но имеют свои стеки и регистры. Процессы изолированы друг от друга, потоки внутри процесса — нет, и это одновременно их сила (дешёвый обмен данными) и их проклятие (гонки, блокировки). Почему совместная память так коварна — тема статьи Многозадачность и параллелизм.

Управление памятью: виртуальная память

Мы уже сказали, что каждый процесс видит своё адресное пространство с адресами от нуля. Но физическая RAM одна на всех, и в ней нет двух ячеек «номер 0». Разрешает противоречие виртуальная память — пожалуй, самая красивая абстракция ОС.

Идея: адреса, которыми оперирует программа, — виртуальные, ненастоящие. Между процессором и RAM стоит аппаратный блок MMU (memory management unit), который на каждое обращение к памяти переводит виртуальный адрес в физический по таблице страниц — карте, которую для текущего процесса поддерживает ОС. Память нарезана на блоки фиксированного размера — страницы (обычно 4 КБ); таблица говорит, в каком физическом кадре RAM лежит каждая виртуальная страница.

Виртуальная память: перевод виртуальных страниц в физические кадры через MMU

Из этой одной механики следует сразу несколько крупных выигрышей:

  • Изоляция. У процесса A и процесса B «страница 1» — это один и тот же виртуальный адрес, но таблицы страниц ведут их в разные физические кадры. A физически не может адресовать память B: в его таблице просто нет такой записи. Защита памяти — не проверка в коде, а следствие того, что чужих адресов в карте нет.
  • Иллюзия сплошной памяти. Программе кажется, что у неё непрерывное пространство, а физически её страницы могут быть разбросаны по RAM как попало — MMU всё равно соберёт. ОС больше не нужно искать большой непрерывный кусок.
  • Память больше, чем RAM (подкачка). Если RAM не хватает, ОС может вытеснить редко используемую страницу на диск (в swap), пометив в таблице «её здесь нет». Когда программа обратится к этой странице, MMU не найдёт кадра и поднимет страничную ошибку (page fault) — прерывание, по которому ОС подгружает страницу с диска обратно в RAM и повторяет обращение. Программа об этом даже не знает — только чувствует, что «подтормозило».

Виртуальная память намертво завязана на скорости уровней памяти: перевод адреса кэшируется в TLB, обращения к RAM — сотни тактов, к swap на диске — миллионы. Почему это пропасть, а не разница — в статье Иерархия памяти.

Файлы и ввод-вывод: как диск становится файлом

Третий большой ресурс — устройства, и здесь ОС предъявляет свою самую известную абстракцию. Диск физически — это массив пронумерованных блоков по 512 или 4096 байт, без всякого понятия «файл» или «папка». Файловая система — это структура данных поверх этих блоков, которая превращает их в дерево именованных файлов и каталогов: хранит, какие блоки принадлежат какому файлу, где его метаданные (размер, права, время), как устроены директории.

Программа же не видит ни блоков, ни файловой системы. Она работает с файлом через крошечный интерфейс: open (получить доступ), read/write (перекачать байты), close. При open ОС возвращает файловый дескриптор — маленькое целое число, которым программа потом называет этот открытый файл. Дескриптор — это индекс в таблице открытых файлов процесса; за ним ОС прячет всё: какой это файл, на каком устройстве, где мы в нём находимся.

Мощь этой абстракции — в её единообразии. В Unix-философии «всё есть файл»: не только файлы на диске, но и клавиатура, экран, сетевое соединение (сокет), даже устройства вроде /dev/null и датчики — всё читается и пишется тем же read/write. Программе, копирующей данные, безразлично, откуда они текут — с диска, из сети или с клавиатуры: интерфейс один.

Посмотрим, что реально происходит при read — это заодно связывает воедино процессы, блокировку и ввод-вывод:

Обратите внимание на кэш страниц (page cache): ОС держит недавно прочитанные блоки диска в свободной RAM и на повторный read отдаёт их из памяти, не трогая диск. Поэтому второй запуск программы часто ощутимо быстрее первого — файлы уже «прогреты» в кэше. И обратная сторона: write часто лишь помечает данные в кэше как «грязные», а на диск они уходят позже, пачкой. Отсюда — важное свойство: пока не выполнен fsync, записанное может не пережить внезапного выключения. На этой тонкой границе стоит вся тема надёжного хранения — файлы, БД, транзакции (Как данные переживают выключение).

Как это складывается: жизнь одной команды

Соберём всё вместе на знакомом действии — вы набрали в терминале имя программы и нажали Enter. Что делает ОС:

  1. Создание процесса. Оболочка (сама процесс) делает fork — просит ОС создать копию себя, затем execve — просит заменить содержимое нового процесса на нужную программу. ОС читает исполняемый файл, раскладывает его секции по виртуальному адресному пространству (code, data), заводит стек и кучу, создаёт таблицу страниц.
  2. Планирование. Новый процесс переходит New → Ready. Планировщик рано или поздно выбирает его, происходит переключение контекста, процессор ставится на точку входа — процесс Running.
  3. Исполнение и системные вызовы. Программа считает в пользовательском режиме. Нужно вывести строку — write в дескриптор 1 (стандартный вывод), trap в ядро, ОС шлёт байты драйверу терминала. Нужна память под данные — обращение к невыделенной странице, page fault, ОС выдаёт физический кадр и правит таблицу страниц.
  4. Ожидание. Нужно прочитать файл — read, процесс блокируется (Waiting), CPU уходит другим; прерывание от диска будит его обратно.
  5. Завершение. Программа вызывает exit. ОС освобождает её память, закрывает дескрипторы, убирает из таблицы процессов и сообщает результат родителю (оболочке), которая печатает новое приглашение.

За доли секунды процессы создаются, планируются, блокируются и умирают сотнями — и всё это оркеструет ОС, оставаясь для вас невидимой.

Где эти абстракции протекают

Абстракции ОС великолепны, но, как любые абстракции, они протекают — и знать, где именно, полезно на практике.

  • Виртуальная память → thrashing. Пока рабочий набор влезает в RAM, подкачка незаметна. Стоит открыть слишком много вкладок — и ОС начинает лихорадочно гонять страницы между RAM и swap: почти каждое обращение к памяти оборачивается page fault и чтением с диска. Машина «замерзает» при загрузке CPU близкой к нулю — она вся в ожидании диска. Абстракция «памяти как будто бесконечно много» протекает скоростью.
  • Процессы как будто параллельны → на деле делят одно ядро. Двадцать «параллельных» потоков на четырёхъядерном CPU по-настоящему параллельны лишь вчетвером; остальное — переключение контекста, и слишком мелкое дробление задач только добавляет накладных расходов. А ещё соседний «шумный» процесс может вытеснить ваши данные из общего кэша — и ваш код замедлится, хотя вы ничего не меняли.
  • Файл как поток байтов → буферизация и долговечность. write вернул успех — это ещё не значит, что данные на диске: они могут висеть в page cache. Внезапное отключение питания — и последних записей нет. Базы данных и журналируемые ФС существуют во многом ради того, чтобы латать эту течь (fsync, журналы, транзакции).
  • Изоляция процессов → каналы утечки. Логически процессы не видят память друг друга, но делят физический процессор, кэши и время. По разнице во времени доступа можно косвенно вытащить чужие секреты — так работают атаки вроде Spectre. Абстракция «полной изоляции» протекает через физику общего железа (Основы безопасности).

Вывод не «абстракции плохи», а «знай их цену». Абстракции ОС экономят вам 99% забот; понимание границ оставшегося процента — это ровно то, что отличает инженера, умеющего диагностировать «непонятные тормоза», от того, кто разводит руками.

Типичные заблуждения

  • «ОС — это рабочий стол, окна и меню». Нет. Графический интерфейс — обычные пользовательские программы поверх ОС. Сердце ОС — ядро: планировщик, память, файлы, драйверы; сервер способен вообще не иметь GUI.
  • «Программа обращается к железу напрямую». Почти никогда. Любое касание диска, сети, экрана идёт через системный вызов к ядру; прямой доступ запрещён процессором.
  • «Больше процессов/потоков — всегда быстрее». До числа ядер — да, дальше начинается борьба за процессор и рост накладных расходов на переключения. Иногда меньше — быстрее.
  • write записал — значит, сохранено». Не обязательно: данные могут лежать в кэше и потеряться при сбое питания, пока не выполнен fsync.
  • «У каждого процесса своя физическая память». Своё виртуальное пространство — да; физически же страницы могут быть разбросаны, вытеснены на диск или даже разделяемы между процессами (общие библиотеки грузятся в RAM один раз на всех).

Мини-итог

Операционная система — это слой между железом и программами, играющий две роли: абстракция (прячет уродливое разнообразное железо за чистыми понятиями) и арбитр (делит скудные ресурсы между многими и защищает их друг от друга). Три её главные абстракции соответствуют трём ресурсам: процесс превращает процессор в иллюзию, что у программы своя машина; виртуальная память превращает общую RAM в личное адресное пространство каждого; файл превращает блоки диска (и вообще любое устройство) в удобный поток байтов. Держит всё это разделение режимов процессора (user/kernel), а единственная легальная дверь из программы к железу — системный вызов. Каждая из этих абстракций местами протекает — подкачкой, накладными расходами, буферизацией, каналами утечки, — и знание этих течей и есть практическая ценность понимания ОС.

Что дальше

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

Как хранят данные: обзор структур данных и зачем их так много

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов