Что делает операционная система: процессы, память, файлы — обзор
В предыдущей статье трека мы проследили путь от исходного кода до потока машинных
команд (От кода к исполнению). Но
получить .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 лежит каждая виртуальная страница.
Из этой одной механики следует сразу несколько крупных выигрышей:
- Изоляция. У процесса 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. Что делает ОС:
- Создание процесса. Оболочка (сама процесс) делает
fork— просит ОС создать копию себя, затемexecve— просит заменить содержимое нового процесса на нужную программу. ОС читает исполняемый файл, раскладывает его секции по виртуальному адресному пространству (code, data), заводит стек и кучу, создаёт таблицу страниц. - Планирование. Новый процесс переходит
New → Ready. Планировщик рано или поздно выбирает его, происходит переключение контекста, процессор ставится на точку входа — процессRunning. - Исполнение и системные вызовы. Программа считает в пользовательском режиме. Нужно
вывести строку —
writeв дескриптор 1 (стандартный вывод), trap в ядро, ОС шлёт байты драйверу терминала. Нужна память под данные — обращение к невыделенной странице, page fault, ОС выдаёт физический кадр и правит таблицу страниц. - Ожидание. Нужно прочитать файл —
read, процесс блокируется (Waiting), CPU уходит другим; прерывание от диска будит его обратно. - Завершение. Программа вызывает
exit. ОС освобождает её память, закрывает дескрипторы, убирает из таблицы процессов и сообщает результат родителю (оболочке), которая печатает новое приглашение.
За доли секунды процессы создаются, планируются, блокируются и умирают сотнями — и всё это оркеструет ОС, оставаясь для вас невидимой.
Где эти абстракции протекают
Абстракции ОС великолепны, но, как любые абстракции, они протекают — и знать, где именно, полезно на практике.
- Виртуальная память → thrashing. Пока рабочий набор влезает в RAM, подкачка незаметна. Стоит открыть слишком много вкладок — и ОС начинает лихорадочно гонять страницы между RAM и swap: почти каждое обращение к памяти оборачивается page fault и чтением с диска. Машина «замерзает» при загрузке CPU близкой к нулю — она вся в ожидании диска. Абстракция «памяти как будто бесконечно много» протекает скоростью.
- Процессы как будто параллельны → на деле делят одно ядро. Двадцать «параллельных» потоков на четырёхъядерном CPU по-настоящему параллельны лишь вчетвером; остальное — переключение контекста, и слишком мелкое дробление задач только добавляет накладных расходов. А ещё соседний «шумный» процесс может вытеснить ваши данные из общего кэша — и ваш код замедлится, хотя вы ничего не меняли.
- Файл как поток байтов → буферизация и долговечность.
writeвернул успех — это ещё не значит, что данные на диске: они могут висеть в page cache. Внезапное отключение питания — и последних записей нет. Базы данных и журналируемые ФС существуют во многом ради того, чтобы латать эту течь (fsync, журналы, транзакции). - Изоляция процессов → каналы утечки. Логически процессы не видят память друг друга, но делят физический процессор, кэши и время. По разнице во времени доступа можно косвенно вытащить чужие секреты — так работают атаки вроде Spectre. Абстракция «полной изоляции» протекает через физику общего железа (Основы безопасности).
Вывод не «абстракции плохи», а «знай их цену». Абстракции ОС экономят вам 99% забот; понимание границ оставшегося процента — это ровно то, что отличает инженера, умеющего диагностировать «непонятные тормоза», от того, кто разводит руками.
Типичные заблуждения
- «ОС — это рабочий стол, окна и меню». Нет. Графический интерфейс — обычные пользовательские программы поверх ОС. Сердце ОС — ядро: планировщик, память, файлы, драйверы; сервер способен вообще не иметь GUI.
- «Программа обращается к железу напрямую». Почти никогда. Любое касание диска, сети, экрана идёт через системный вызов к ядру; прямой доступ запрещён процессором.
- «Больше процессов/потоков — всегда быстрее». До числа ядер — да, дальше начинается борьба за процессор и рост накладных расходов на переключения. Иногда меньше — быстрее.
writeзаписал — значит, сохранено». Не обязательно: данные могут лежать в кэше и потеряться при сбое питания, пока не выполненfsync.- «У каждого процесса своя физическая память». Своё виртуальное пространство — да; физически же страницы могут быть разбросаны, вытеснены на диск или даже разделяемы между процессами (общие библиотеки грузятся в RAM один раз на всех).
Мини-итог
Операционная система — это слой между железом и программами, играющий две роли: абстракция (прячет уродливое разнообразное железо за чистыми понятиями) и арбитр (делит скудные ресурсы между многими и защищает их друг от друга). Три её главные абстракции соответствуют трём ресурсам: процесс превращает процессор в иллюзию, что у программы своя машина; виртуальная память превращает общую RAM в личное адресное пространство каждого; файл превращает блоки диска (и вообще любое устройство) в удобный поток байтов. Держит всё это разделение режимов процессора (user/kernel), а единственная легальная дверь из программы к железу — системный вызов. Каждая из этих абстракций местами протекает — подкачкой, накладными расходами, буферизацией, каналами утечки, — и знание этих течей и есть практическая ценность понимания ОС.
Что дальше
Мы много говорили о том, как ОС хранит своё состояние — таблицы процессов, таблицы страниц, структуры файловой системы. Всё это устроено на конкретных структурах данных, выбранных под задачу: где-то массив, где-то дерево, где-то хеш-таблица. Почему их так много и как выбрать правильную — следующая статья.
Как хранят данные: обзор структур данных и зачем их так много