Skip to content

1. Философские и исторические предпосылки архитектуры

Архитектура Linux не возникла на пустом месте. Она является прямым наследником идей, заложенных в операционной системе UNIX в 1970-х годах.

  • Философия UNIX:

    • "Все есть файл" (Everything is a file): Это фундаментальная абстракция, упрощающая взаимодействие с системой. Не только данные на диске, но и устройства (клавиатура, мышь, жесткий диск), процессы, сетевое соединение представлены как файлы, с которыми можно работать через стандартные операции: open, read, write, close.
    • Модульность (Modularity): Система должна состоять из небольших, простых программ (утилит), каждая из которых идеально выполняет одну функцию. Эти программы можно комбинировать через конвейеры (pipes) для решения сложных задач. Эта идея проецируется и на ядро через механизм загружаемых модулей.
    • Примитивы (KISS - Keep It Simple, Stupid): Предоставление простых, но мощных механизмов (примитивов), из которых можно собрать сложное поведение. Например, системные вызовы fork, exec, pipe являются такими примитивами для управления процессами.
  • Влияние проектов GNU и Linux:

    • Проект GNU (1984, Ричард Столлман): Ставил целью создание полностью свободной UNIX-совместимой операционной системы. К началу 1990-х был создан почти весь пользовательский пространственный софт: компилятор GCC, библиотека C (glibc), оболочка Bash, coreutils. Не хватало лишь ядра (Hurd разрабатывалось, но не было готово).
    • Ядро Linux (1991, Линус Торвальдс): Появилось как хобби-проект по созданию минимального работоспособного ядра для персонального компьютера. Оно идеально дополнило проект GNU. Союз ядра Linux и набора утилит GNU дал миру операционную систему GNU/Linux.
  • Монолитное ядро vs. Микроядро:

    • Микроядро: Реализует лишь минимальный набор функций (управление памятью, IPC, базовое планирование), а всё остальное (драйверы, файловые системы, сетевой стек) работает в виде изолированных сервисов в пользовательском пространстве. Плюсы: стабильность (сбой драйвера не падает всё ядро), безопасность. Минусы: высокие накладные расходы на IPC между компонентами, что снижает производительность. Примеры: GNU Hurd, Minix, QNX.
    • Монолитное ядро: Все основные подсистемы (перечисленные выше) выполняются в едином адресном пространстве ядра. Плюсы: высочайшая производительность за счет отсутствия переключений контекста при вызове функций между подсистемами. Минусы: сложность разработки и отладки; сбой в любом драйвере ведет к падению всей системы.
    • Компромисс Linux: Linux — это монолитное ядро, но с поддержкой загружаемых модулей. Это значит, что ядро компилируется как единое целое, но необязательные его части (многие драйверы, файловые системы) могут быть динамически загружены и выгружены в пространство ядра по мере необходимости. Это сочетает производительность монолитного ядра с гибкостью, близкой к микроядерной модели.

2. Обзор высокоуровневой архитектуры: взгляд слоями

Архитектуру GNU/Linux можно представить в виде трех основных слоев:

  1. Аппаратное обеспечение (Hardware):

    • Центральный процессор (CPU), оперативная память (RAM), жесткие диски (HDD/SSD), сетевые карты (NIC), устройства ввода-вывода и т.д.
    • Ядро напрямую взаимодействует с "железом", абстрагируя его особенности для верхних уровней.
  2. Ядро (Kernel Space):

    • Это привилегированная часть ОС, выполняющаяся в отдельном адресном пространстве. Имеет прямой доступ ко всей памяти и аппаратным ресурсам.
    • Код ядра загружается в память при старте системы и остается там до выключения.
    • Основные функции:
      • Управление процессами и их планирование.
      • Управление памятью.
      • Предоставление интерфейса к устройствам через драйверы.
      • Реализация файловых систем и сетевого стека.
      • Обеспечение безопасности и разграничения доступа.
  3. Пользовательское пространство (User Space):

    • Это среда, в которой выполняются все пользовательские приложения (от текстового редактора до веб-сервера).
    • Каждое приложение работает в своем собственном виртуальном адресном пространстве, изолированном от других приложений и от ядра.
    • Компоненты:
      • Системные библиотеки: Самая важная — glibc (GNU C Library). Она предоставляет реализации стандартных функций (например, printf, malloc), но главное — она содержит обертки для системных вызовов, которые являются точкой входа в ядро.
      • Оболочка (Shell): Интерпретатор команд (bash, zsh), который является основным интерфейсом взаимодействия пользователя с системой.
      • Утилиты и приложения: Все программы, которые пользователь запускает явно.
  4. Системные вызовы (System Calls):

    • Это не слой, а четко определенный интерфейс между User Space и Kernel Space.
    • Приложения не могут напрямую обращаться к ресурсам или функциям ядра. Вместо этого они запрашивают услугу у ядра, выполняя системный вызов.
    • Процесс:
      1. Приложение вызывает функцию из библиотеки (например, glibc).
      2. Библиотека подготавливает аргументы и инициирует специальную инструкцию процессора (например, syscall или int 0x80 на x86), которая вызывает переключение контекста из пользовательского режима в режим ядра.
      3. Процессор передает управление заранее определенному обработчику в ядре.
      4. Ядро проверяет корректность запроса и права процесса, выполняет запрошенную операцию.
      5. Ядро возвращает результат и управление обратно в пользовательское пространство.
    • Примеры системных вызовов: read, write, open, close, fork, execve, kill.

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

3. Детальный разбор подсистем ядра Linux

3.1. Подсистема управления процессами и планировщик (Process Scheduler)

Эта подсистема отвечает за создание, уничтожение, управление и планирование выполнения всех процессов и потоков в системе.

  • Задачи, процессы, потоки:

    • В Linux нет фундаментальной разницы между процессом и потоком. Ядро оперирует понятием "задача" (task). Поток — это просто задача, которая разделяет с другими задачами (потоками) свое адресное пространство (память, файловые дескрипторы и т.д.).
    • Дескриптор задачи struct task_struct: Это огромная структура данных в ядре, которая содержит всю информацию о задаче: идентификатор (PID), состояние, приоритет, указатель на адресное пространство, открытые файлы, сигналы, родительский процесс и т.д. Все задачи образуют связный список (и древовидную структуру), что позволяет планировщику эффективно их перебирать.
  • Планировщик (Completely Fair Scheduler - CFS):

    • Цель: Справедливо распределить процессорное время между всеми исполняемыми задачами, минимизируя задержки и обеспечивая высокую пропускную способность.
    • Принцип работы: CFS использует концепцию виртуального времени. Каждой задаче назначается доля процессорного времени (ее "вес"), основанная на ее приоритете (nice value). CFS стремится к идеалу, где каждая задача получила ровно n-ую часть процессорного времени за данный период.
    • Очереди выполнения: CFS использует красно-черное дерево (эффективная структура данных для сортировки) для организации задач, готовых к выполнению. Ключом в дереве является vruntime (виртуальное время, которое задача уже использовала). Задача с наименьшим vruntime (та, которой досталось меньше всего CPU) будет выбрана для выполнения следующей.
    • Кванты времени: CFS не использует фиксированные кванты времени. Задача выполняется, пока ее vruntime не станет больше, чем у следующей задачи в дереве. Это предотвращает инверсию приоритетов.
  • Создание процессов: fork(), exec(), clone():

    • fork(): Системный вызов, создающий почти точную копию текущего процесса. Дочерний процесс получает копии всех ресурсов родителя (памяти, файловых дескрипторов и т.д.).
    • Copy-on-Write (CoW): Ключевая оптимизация. При fork() память родителя и потомка логически разделяется, но помечается как read-only. Физическое копирование страниц памяти происходит только тогда, когда один из процессов пытается изменить данные. Это drastically сокращает накладные расходы на создание процессов.
    • exec(): Системный вызов, который заменяет образ текущего процесса новым образом, загружаемым из исполняемого файла. Происходит после fork(): родительский процесс (например, оболочка) делает fork(), а дочерний делает exec(), чтобы запустить новую программу.
    • clone(): Более низкоуровневый системный вызов, который позволяет точно контролировать, какие ресурсы родительского процесса будут разделены с потомком. Используется библиотеками (например, pthreads) для создания потоков.
  • Состояния процесса:

    • TASK_RUNNING: Готов к выполнению или выполняется.
    • TASK_INTERRUPTIBLE / TASK_UNINTERRUPTIBLE: Ожидание события (например, данных от устройства). Может быть прервано сигналом / не может.
    • TASK_STOPPED: Процесс остановлен (сигналом SIGSTOP).
    • TASK_ZOMBIE: Процесс завершился, но его родитель еще не забрал его статус завершения (wait()).
  • Контекстные переключения: Когда планировщик решает переключиться с одной задачи на другую, происходит контекстное переключение. Это включает в себя сохранение состояния регистров процессора и указателя на текущую таблицу страниц старой задачи и загрузку состояния новой задачи. Это дорогая операция, поэтому CFS старается минимизировать их количество.

3.2. Подсистема управления памятью (Memory Management - MM)

MM отвечает за управление виртуальной и физической памятью, обеспечивая изоляцию процессов и эффективное использование RAM.

  • Менеджер виртуальной памяти:

    • Каждый процесс в User Space работает в своем собственном виртуальном адресном пространстве. Ему кажется, что он один владеет всей памятью машины (например, от адреса 0x0 до 0xffff...).
    • Ядро и MMU (Memory Management Unit) процессора транслируют эти виртуальные адреса в физические адреса в RAM.
    • Это обеспечивает безопасность: процесс не может получить доступ к памяти другого процесса или ядра, так как его виртуальные адреса отображаются на его собственный набор физических страниц.
  • Страничная организация памяти:

    • Память делится на фиксированные блоки — страницы (обычно 4 KiB). Физическая память — на фреймы такого же размера.
    • Таблицы страниц (Page Tables): Иерархические структуры данных (многоуровневые), которые хранят mapping виртуальных страниц на физические фреймы. У каждого процесса есть своя собственная таблица страниц.
    • TLB (Translation Lookaside Buffer): Кэш внутри процессора для трансляции адресов. Содержит недавно использованные mapping'и виртуальных адресов в физические. При промахе TLB обращение идет к таблице страниц в RAM, что медленно.
  • Выделение памяти:

    • В пользовательском пространстве: Функция malloc() (из glibc) не всегда запрашивает память у ядра. Сначала она использует заранее выделенный кусок виртуальной памяти процесса — кучу (heap). Когда куча исчерпана, malloc() вызывает системные вызовы brk() или sbrk(), чтобы расширить кучу, или mmap(), чтобы выделить большие, анонимные регионы памяти.
    • В ядре: Используются быстрые аллокаторы для часто используемых небольших объектов (kmallocslab/slub/slob allocator). Они предотвращают фрагментацию и кэшируют готовые объекты для быстрого повторного использования.
  • Механизм подкачки (Swapping) и кэширования:

    • Page Cache: Ядро использует незанятую оперативную память для кэширования данных с диска. Когда процесс читает файл, данные помещаются в кэш страниц. При повторном чтении данные берутся из быстрой RAM, а не с медленного диска. Запись также часто буферизуется в кэше и сбрасывается на диск позже (это можно контролировать).
    • Swapping: Когда физической памяти становится мало, подсистема MM вытесняет редко используемые анонимные страницы (не связанные с файлом, например, стек процесса) в специальную область на диске — swap-раздел или swap-файл. Это освобождает RAM. При попытке доступа к вытесненной странице возникает page fault, и ядро загружает ее обратно в RAM, возможно, вытеснив другую страницу.

3.3. Виртуальная файловая система (Virtual File System - VFS)

VFS — это прослойка абстракции между системными вызовами, связанными с файлами (open, read, write), и конкретными реализациями файловых систем (ext4, NTFS, NFS).

  • Абстракции VFS:

    • superblock: Представляет собой смонтированную файловую систему.
    • inode (index node): Уникально идентифицирует файл на диске и содержит его метаданные (права доступа, владелец, размер, временные метки, указатели на блоки данных). Не содержит имени файла.
    • dentry (directory entry): Представляет собой элемент каталога. Связывает имя файла с его inode. Кэшируется в RAM для ускорения поиска путей (dentry cache).
    • file: Представляет открытый файл. Содержит информацию о текущей позиции в файле (offset) и права доступа, с которыми он был открыт.
  • Принцип "Все есть файл":

    • VFS позволяет представлять через файловый интерфейс что угодно. Для этого драйвер устройства или другая подсистема должна реализовать стандартный набор операций (file_operations, inode_operations).
    • Например, при открытии файла /dev/sda VFS перенаправляет вызов драйверу блочного устройства. При открытии файла в /proc (виртуальная ФС процессов) вызов перенаправляется подсистеме ядра, которая генерирует информацию о процессе на лету.
  • Поддержка конкретных ФС:

    • Каждая ФС (ext4, Btrfs, XFS, FAT) предоставляет ядру свою реализацию функций для работы с VFS (например, ext4_read(), ext4_write()).
    • Когда процесс вызывает read() для файла на ext4:
      1. VFS находит file, dentry, inode для этого файла.
      2. VFS определяет, что inode принадлежит ФС ext4.
      3. VFS вызывает конкретную функцию ext4_read(), передавая ей необходимые параметры.
      4. ext4, в свою очередь, может обращаться к подсистеме блочных устройств для чтения конкретных секторов с диска.

3.4. Сетевая подсистема (Networking Stack)

Сетевой стек Linux реализует многоуровневую модель сетевых протоколов (в основном TCP/IP).

  • Архитектура:

    • Сокеты (Sockets): Интерфейс между пользовательским пространством и сетевым стеком. Системный вызов socket() создает конечную точку для связи. bind(), listen(), accept(), connect(), send(), recv() работают с сокетами.
    • Уровни стека:
      • Уровень приложения (Application Layer): Реализуется в пользовательском пространстве (например, веб-браузер, sshd).
      • Транспортный уровень (Transport Layer): В ядре (TCP, UDP). Отвечает за надежность, управление потоком.
      • Сетевой уровень (Network Layer): IP (IPv4, IPv6), маршрутизация.
      • Канальный уровень (Link Layer): Ethernet, обработка MAC-адресов.
      • Физический уровень (Physical Layer): Реализуется драйвером сетевой карты (NIC).
  • sk_buff (socket buffer):

    • Это основная структура данных для представления сетевого пакета внутри ядра. Когда пакет прибывает с сетевой карты, драйвер создает sk_buff и заполняет его данными.
    • sk_buff проходит вверх по стеку: к канальному уровню, сетевому (где проверяется IP-адрес, принимается решение о маршрутизации), транспортному (TCP/UDP).
    • При отправке данные из пользовательского пространства копируются в sk_buff и проходят стек вниз, пока не будут переданы драйверу сетевой карты для отправки.
    • sk_buff эффективно управляет данными пакета, позволяя добавлять и удалять заголовки протоколов без копирования всего пакета.
  • Netfilter:

    • Это framework внутри ядра, который позволяет перехватывать и manipulateровать пакеты на различных этапах их прохождения по стеку.
    • Место iptables/nftables: Пользовательские утилиты iptables и nftables всего лишь являются фронтендами для настройки правил в таблицах Netfilter. Эти правила определяют, что делать с пакетом: принять (ACCEPT), отбросить (DROP), перенаправить (NAT).

3.5. Подсистема ввода-вывода и управления устройствами

  • Абстракция "Устройство-файл":

    • Все устройства представлены файлами в каталоге /dev. Это прямое следствие философии "все есть файл".
    • Блочные устройства: Устройства с случайным доступом к данным блоками (жесткие диски, SSD). Файлы: /dev/sda1, /dev/nvme0n1p2.
    • Символьные устройства: Устройства с последовательным или потоковым доступом (клавиатура, мышь, принтер, терминал). Файлы: /dev/ttyS0 (COM-порт), /dev/input/event0.
    • Сетевые устройства: Не имеют файлов в /dev. Доступ к ним осуществляется исключительно через сетевой стек и сокеты.
  • Драйверы устройств:

    • Это модули ядра (или часть монолитного ядра), которые "знают", как общаться с конкретным hardware.
    • Они регистрируются в ядре и предоставляют стандартный набор операций (file_operations для символьных устройств, block_device_operations для блочных).
    • Когда пользовательская программа читает из /dev/sda1, VFS определяет, что это блочное устройство, и перенаправляет вызов read соответствующему драйверу, который преобразует его в команды, понятные контроллеру диска.
  • Менеджер устройств (udev):

    • Важно понимать, что udev — это пользовательский демон (работает в User Space).
    • Его задача — динамически создавать и удалять файлы устройств в /dev в ответ на события от ядра. Например, при подключении флешки ядро обнаруживает новое блочное устройство и отправляет сообщение в User Space через специальную файловую систему sysfs (/sys). udev получает это сообщение, считывает свойства устройства (например, vendor ID, model) и создает соответствующий файл в /dev (например, /dev/sdb1), возможно, применяя заранее заданные правила (создать символьную ссылку /dev/my_usb).

4. Механизмы взаимодействия подсистем: как всё работает вместе

Подсистемы ядра не живут изолированно. Они постоянно взаимодействуют через четко определенные механизмы.

  • Прерывания (Interrupts):

    • Это аппаратный механизм, уведомляющий процессор о том, что устройству требуется внимание (например, пришел сетевой пакет, нажата клавиша, завершена операция дискового ввода-вывода).
    • Процессор приостанавливает текущее выполнение, сохраняет контекст и прыгает на заранее заданный обработчик прерывания (Interrupt Handler - ISR) в ядре.
    • ISR должен быть очень быстрым. Обычно он только acknowledges прерывание, копирует критически важные данные из hardware в память и планирует отложенную обработку.
  • Очереди отложенного выполнения:

    • Поскольку выполнять всю работу в контексте прерывания нельзя (это блокирует все другие прерывания и задачи), используются три основных механизма:
      • SoftIRQs: Статические, заранее определенные точки для отложенной обработки с высокими требованиями к производительности (обработка сетевых пакетов, таймеры). Выполняются в контексте ядра с отключенными прерываниями.
      • Tasklets: Основаны на SoftIRQs, но более динамичны и безопасны. Гарантируется, что один и тот же tasklet не будет выполняться одновременно на нескольких процессорах.
      • Workqueues: Выполняют работу в контексте отдельного потока ядра (kernel thread). Это значит, что они могут "спать" (блокироваться), что недоступно для SoftIRQs и tasklets. Используются для более тяжелой работы, не критичной ко времени.
  • Средства межпроцессного взаимодействия (IPC):

    • Сигналы (Signals): Асинхронные уведомления, посылаемые ядром или одним процессом другому (например, SIGKILL, SIGTERM).
    • Каналы (Pipes) и именованные каналы (FIFOs): Односторонние каналы связи. | в shell создает pipe.
    • Очереди сообщений (SysV IPC, POSIX MQ): Позволяют процессам обмениваться структурированными сообщениями.
    • Разделяемая память (Shared Memory): Самый быстрый метод IPC. Несколько процессов отображают один и тот же сегмент физической памяти в свое адресное пространство.
    • Сокеты (Sockets): Могут использоваться для IPC между процессами на одной машине (Unix domain sockets) помимо сетевого обмена.
  • Синхронизация в ядре:

    • В многопроцессорных (SMP) системах несколько потоков ядра могут одновременно обращаться к одним и тем же данным, что приводит к race conditions.
    • Spinlocks: Низкоуровневая блокировка. Поток в ядре, пытающийся захватить занятый spinlock, будет крутиться в цикле ("spin"), активно потребляя CPU, пока lock не освободится. Используется для очень кратковременной блокировки.
    • Мьютексы (Mutexes): Если мьютекс занят, поток переходит в состояние ожидания и не потребляет CPU, пока его не разбудят. Подходит для более долгих критических секций.
    • Семафоры (Semaphores): Обобщение мьютексов, позволяющее одновременно доступ N потокам к ресурсу.

5. Пример сквозного выполнения: от нажатия клавиши до вывода на экран

Рассмотрим, как взаимодействуют подсистемы на примере ввода команды ls в оболочке.

  1. Аппаратное прерывание: Пользователь нажимает клавишу 'L' на клавиатуре. Контроллер клавиатуры генерирует прерывание (IRQ).
  2. Обработка прерывания: Процессор переключается в режим ядра и выполняет ISR драйвера клавиатуры. ISR считывает скан-код клавиши из порта, преобразует его в код символа (например, ASCII 'l') и помещает этот код в буфер ввода в ядре. ISR отмечает, что прерывание обработано, и планирует выполнение tasklet для дальнейшей обработки.
  3. Пробуждение процесса: Tasklet, связанный с вводом, выполняется. Он видит, что в буфере появились данные для терминального устройства (/dev/ttyX), к которому привязана оболочка. Он будит процессы, которые ждут чтения с этого терминала. Планировщик помещает спящий процесс оболочки в очередь выполнения.
  4. Чтение из устройства: В какой-то момент планировщик передает управление процессу оболочки. Оболочка до этого была заблокирована на системном вызове read() от своего стандартного ввода (терминала). Вызов read() проходит через VFS. VFS определяет, что файловый дескриптор STDIN указывает на символьное устройство (терминал), и вызывает функцию read драйвера терминала (tty). Драйвер копирует данные ('l') из внутреннего буфера ядра в буфер пользовательского пространства оболочки.
  5. Обработка ввода оболочкой: Оболочка получает символ, добавляет его в свою внутреннюю строку, и, получив символ новой строки (Enter), понимает, что команда готова к исполнению.
  6. Создание процесса ls: Оболочка вызывает fork(), создавая свою копию (с помощью CoW). Затем дочерний процесс вызывает execve("/bin/ls", ...). Подсистема управления процессами находит исполняемый файл /bin/ls, загружает его код и данные в память (через подсистему управления памятью и VFS, которая читает файл с диска), настраивает стек и регистры и начинает выполнение ls.
  7. Работа ls: ls с помощью системного вызова getdents64() (или подобного) читает содержимое текущего каталога. VFS и конкретная файловая система (например, ext4) обрабатывают этот запрос, возвращая список файлов.
  8. Вывод на экран: ls выводит список через стандартный вывод (STDOUT), используя системный вызов write(). VFS определяет, что STDOUT — это тоже терминальное устройство, и передает данные драйверу терминала. Драйвер терминала копирует данные (буквы 'l', 's', '\n' и т.д.) в свой буфер вывода.
  9. Драйвер видеокарты: Драйвер терминала (tty) взаимодействует с драйвером видеокарты (через подсистему framebuffer или более сложные интерфейсы вроде DRM/KMS). Драйвер видеокарты преобразует символы в пиксели и обновляет видеопамять, что приводит к появлению букв на экране.
  10. Завершение: Процесс ls завершается. Его родитель (оболочка) получает об этом уведомление через системный вызов wait(), выводит приглашение командной строки и снова блокируется в read(), ожидая следующую команду.

Этот пример наглядно показывает, как практически все рассмотренные подсистемы участвуют в выполнении даже такой простой задачи.

6. Заключение

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

  1. Четком разделении ответственности: Каждая подсистема решает свою узкую задачу и делает это хорошо.
  2. Мощных абстракциях: VFS, задачи, виртуальная память — эти абстракции скрывают сложность hardware и предоставляют единообразный интерфейс для разработки.
  3. Эффективности: Монолитное ядро с модулями обеспечивает высочайшую производительность, сравнимую с микроядерными архитектурами.
  4. Гибкости: Модульность позволяет добавлять поддержку нового hardware и файловых систем без перекомпиляции всего ядра.

Архитектура продолжает развиваться. Появление таких технологий, как:

  • cgroups и namespaces — основа контейнеризации (Docker, LXC), обеспечивающая изоляцию и управление ресурсами.
  • eBPF — позволяет безопасно выполнять пользовательский код в ядре для наблюдения и манипуляции в реальном времени без изменения самого ядра. доказывает, что архитектура Linux не только прочна, но и достаточно гибка, чтобы адаптироваться к новым вызовам и парадигмам вычислений.

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

Контакты: bystrovno@basealt.ru